After my previous series of post about Design Patterns, I'm planning to tackle a new subject over the course of the following articles.

We'll have a look at how to build a Continuous Integration Build Server from scratch.

I've recently dusted off an old desktop and decided to convert it to my personal build and backup server.

First things first, I won't cover the initial steps in much details as they are basic to building any server, I'll just list my starting point so you can compare.

  • Windows Server 2008 Standard.
  • IIS 7.
  • Sql Server 2008 (this is solely because I will be running integration tests against a database).

First of all, we need a place to store our source code. I'm going to use Subversion for this, since I don't have any MSDN subscriptions lying around to install Team Foundation Server at home. (Anyone with free MSDN vouchers lying around, hint :p)

Important Update: It has come to my attention that the 1-Click Setup is outdated, read my next article on how to install the latest Subversion. I also advice to install TortoiseSVN manually with the latest version. After installing SVN, continue reading after the next paragraph on how to configure your firewall and setup your first project.

Start by downloading Svn 1-Click Setup and running the installer, I skipped the TortoiseSVN step since I already had it installed. You can install it, or get it from tortoisesvn.tigris.org and install it manually afterwards.

After installing SVN, open up the Windows Firewall Configuration and add an inbound rule for TCP port 3690 to allow clients to connect using the svn:// protocol.

Find a file called passwd in the repo directory that you created during the installation and add yourself as a user, for example:

 
[users]
david=mysuperpassword

On another machine, or locally if you want, right click somewhere in Explorer and select TortoiseSVN - Repo-browser. Enter svn://servername as a Url and press OK.

This should show you your server with one project in it. I deleted this project to end up with an empty repository.

Decide which directory will be your main working directory on your machine, right click inside it and select SVN Checkout. The default options should be all right, press OK and it will fetch your empty repository.

At this point you can create new directories below this folder and simply right click and select SVN Commit to store them on your build server.

So far for the first step in our installation of a build server, you can now already start coding and safely store your code in a source control system, where you can easily go back to different versions and branch and merge code. Have a look at the SVN Book if you want to learn to unlock the full power of Subversion.

Even when you don't follow any other steps in this series, setting up a repository on another machine would be a best practice in my eyes, and will make you sleep in peace at night :)

 

When looking back at my World of Warcraft experience, I came to the conclusion that when added up, I've been playing this game for over 3 years already. I've even participated in the very first beta ;)

Over time, a lot has changed, I took a few breaks, leveled plenty of classes to the max level, had my days of hardcore end-game raiding (pre-TBC, Naxx), Reputation grinding, Honor grinding (pre-TBC, Warlord), war effort grinding (our guild opened the gates of AQ).

After a year and a half, I took a break from what had become a huge grind, before TBC came out. I returned a while later with some colleagues on an RP realm however.

I've seen the introduction of Blood Elves, the change in faction balance, the faster leveling, and the lack of instance groups at lower levels due to this, combined with the lack of understanding of game mechanics by an ever increasing number of new players (no time or groups for them to get the experience at a low level).

I've also greatly enjoyed doing all new TBC quests a few months after it came out, with less crowded zones, and now I'm liking the casual side of WoW :)

As part of staying on the casual side (casual meaning no hardcore raiding/grinding) I've given the geek in me more freedom to fool around with anything WoW related.

One of the first result of this was the C# World Of Warcraft Armory Library 0.1 I recently released.

The next thing I'm on, is trying out Multiboxing, which is the subject for today's post. There is a lot of information out there, a lot of misconception and taboo around it. Hopefully you'll have a better view on the concept after reading this, as well as an easy to follow guide to try it out.

Multiboxing

First things first, what is Multiboxing?

Multiboxing is the action of controlling multiple accounts from a central point, where each key press results in a single action or macro on all your accounts.

It is important to note this 1 to 1 relation. It's this relation that seperates multiboxing from botting. Bots combine multiple actions into a single key press, which is not allowed and will get you banned.

What is allowed is for example pressing 1 on your keyboard and having one account perform Frost Nova, while another account has Flash Heal bound to 1.

Creating in-game macros and binding these to keybinds is also allowed, since you are using the existing macro system. (e.g.: Popping a trinket and then casting is perfectly fine when you play on one account, therefore it is allowed)

Legality

Lots of people think that multiboxing is a bannable offense and report it.

This is one of the greatest misconceptions out there, because multiboxing is allowed by Blizzard, illustrated by the following collection of GM and CM responses: Dual Boxing - GM Conversations.

As you can see, there is no doubt about the legality, various blue posts have shown it is acceptable.

There are two main rules which have to be respected however, to stay legal.

First of all, all accounts used to multibox with have to be in the same name.

And second of all, the one to one relation between a key press and one actions, as described above, can not be violated.

Advantages

As soon as people accept the fact that they can not get a multiboxer banned simply for the fact they are multiboxing, a new argument gets raised, about how multiboxers gain an unfair advantage (mostly used as a PvP argument).

While this may hold up on a PvE realm, for PvE actions, GM Belfaire strongly counters this argument by suggesting grouping gives the same advantage.

On a PvP realm, or for PvP action (PvE or PvP realm), Malkorix writes several blue posts stating that it might even give a disadvantage for PvP.

Blue posters are objective on this matter, and clearly see that 5 good individual players are still better then 1 good player controlling 5 accounts and having to think a lot more on coordinating them all together, while also being more limited in dealing with various actions.

Opinions on Fun

We've seen multiboxing is legal, does not give an advantage someone else couldn't achieve, yet there are still arguments, which I categorize into opinions.

One of these arguments is on the aspect of fun, people believing it can't be fun to multibox.

This is an opinion about multiboxing, you are free to have them, but what does it matter to you if you think I'm not having fun multiboxing?

These arguments are the hardest kind to argue with, because they are subjective instead of objective.

You might not like multiboxing, that's fine, I don't mind, nobody is forcing you to multibox, no multiboxer is making you do something against your will, so I would politely like to point out the following as a counter to all subjective opinion-based arguments:

Different people have different definitions of fun. Let each have it their way.

Conclusion

As you can see, multiboxing is legal, provides a real challenge, enables you to learn more about the game (macro's, game mechanics), eliminates dependencies on others when looking for groups at lower levels, and is a great way for a geek to spend his time tinkering with World of Warcraft.

In the next post, I will go through all the steps I performed to set up my multiboxing setup (5 shamans on 2 physical machines), stay tuned!

Resources

Some of the sites I used to learn more about multiboxing:

 

I just downloaded the ASP.NET MVC Preview 5 bits from Codeplex and started on my first experiment.

One of the first things I did was to modify the default AccountController to use the new Form Posting and Form Validation features of the Preview 5, somebody probably overlooked updating those :)

If anyone else wants the reworked code, feel free to copy paste.

Note this was something done during lunch break in a hurry, it seems to all work logically, but it's possible I'll have to tune it a bit later on.

Controller:

 
[HandleError]
[OutputCache(Location = OutputCacheLocation.None)]
public class AccountController : Controller
{
    public AccountController()
        : this(null, null)
    {
    }

    public AccountController(IFormsAuthentication formsAuth, MembershipProvider provider)
    {
        FormsAuth = formsAuth ?? new FormsAuthenticationWrapper();
        Provider = provider ?? Membership.Provider;
    }

    public IFormsAuthentication FormsAuth
    {
        get;
        private set;
    }

    public MembershipProvider Provider
    {
        get;
        private set;
    }

    [Authorize]
    [AcceptVerbs("GET")]
    public ActionResult ChangePassword()
    {
        ViewData["Title"] = "Change Password";
        ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;

        return View();
    }

    [Authorize]
    [AcceptVerbs("POST")]
    public ActionResult ChangePassword(string currentPassword, string newPassword, string confirmPassword)
    {
        // Basic parameter validation
        if (String.IsNullOrEmpty(currentPassword))
        {
            ViewData.ModelState.AddModelError("currentPassword", currentPassword, "You must specify a current password.");
        }
        if (newPassword == null || newPassword.Length < Provider.MinRequiredPasswordLength)
        {
            ViewData.ModelState.AddModelError("newPassword", newPassword, String.Format(CultureInfo.InvariantCulture,
                     "You must specify a new password of {0} or more characters.",
                     Provider.MinRequiredPasswordLength));
        }
        if (!String.Equals(newPassword, confirmPassword, StringComparison.Ordinal))
        {
            ViewData.ModelState.AddModelError("newPassword", newPassword, "The new password and confirmation password do not match.");
        }

        if (ViewData.ModelState.IsValid)
        {
            // Attempt to change password
            MembershipUser currentUser = Provider.GetUser(User.Identity.Name, true /* userIsOnline */);
            bool changeSuccessful = false;
            try
            {
                changeSuccessful = currentUser.ChangePassword(currentPassword, newPassword);
            }
            catch
            {
                // An exception is thrown if the new password does not meet the provider's requirements
            }

            if (changeSuccessful)
            {
                return RedirectToAction("ChangePasswordSuccess");
            }
            else
            {
                ViewData.ModelState.AddModelError("password", currentPassword, "The current password is incorrect or the new password is invalid.");
            }
        }

        // If we got this far, something failed, redisplay form
        ViewData["Title"] = "Change Password";
        ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;

        return View();
    }

    public ActionResult ChangePasswordSuccess()
    {
        ViewData["Title"] = "Change Password";

        return View();
    }

    [AcceptVerbs("GET")]
    public ActionResult Login()
    {
        ViewData["Title"] = "Login";
        ViewData["CurrentPage"] = "login";

        return View();
    }

    [AcceptVerbs("POST")]
    public ActionResult Login(string username, string password, bool? rememberMe)
    {
        // Basic parameter validation
        if (String.IsNullOrEmpty(username))
        {
            ViewData.ModelState.AddModelError("username", username, "You must specify a username.");
        }

        if (ViewData.ModelState.IsValid)
        {
            // Attempt to login
            bool loginSuccessful = Provider.ValidateUser(username, password);

            if (loginSuccessful)
            {
                FormsAuth.SetAuthCookie(username, rememberMe ?? false);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ViewData.ModelState.AddModelError("*", username, "The username or password provided is incorrect.");
            }
        }

        // If we got this far, something failed, redisplay form
        ViewData["Title"] = "Login";
        ViewData["CurrentPage"] = "login";
        ViewData["username"] = username;

        return View();
    }

    public ActionResult Logout()
    {
        FormsAuth.SignOut();
        return RedirectToAction("Index", "Home");
    }

    protected override void OnActionExecuting(ActionExecutingContext filterContext)
    {
        if (filterContext.HttpContext.User.Identity is WindowsIdentity)
        {
            throw new InvalidOperationException("Windows authentication is not supported.");
        }
    }

    [AcceptVerbs("GET")]
    public ActionResult Register()
    {
        ViewData["Title"] = "Register";
        ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;

        return View();
    }

    [AcceptVerbs("POST")]
    public ActionResult Register(string username, string email, string password, string confirmPassword)
    {
        // Basic parameter validation
        if (String.IsNullOrEmpty(username))
        {
            ViewData.ModelState.AddModelError("username", username, "You must specify a username.");
        }

        if (String.IsNullOrEmpty(email))
        {
            ViewData.ModelState.AddModelError("email", email, "You must specify an email address.");
        }

        if (password == null || password.Length < Provider.MinRequiredPasswordLength)
        {
            ViewData.ModelState.AddModelError("password", password, String.Format(CultureInfo.InvariantCulture,
                     "You must specify a password of {0} or more characters.",
                     Provider.MinRequiredPasswordLength));
        }

        if (!String.Equals(password, confirmPassword, StringComparison.Ordinal))
        {
            ViewData.ModelState.AddModelError("confirmPassword", confirmPassword, "The password and confirmation do not match.");
        }

        if (ViewData.ModelState.IsValid)
        {

            // Attempt to register the user
            MembershipCreateStatus createStatus;
            MembershipUser newUser = Provider.CreateUser(username, password, email, null, null, true, null, out createStatus);

            if (newUser != null)
            {
                FormsAuth.SetAuthCookie(username, false /* createPersistentCookie */);
                return RedirectToAction("Index", "Home");
            }
            else
            {
                ViewData.ModelState.AddModelError("*", username, ErrorCodeToString(createStatus));
            }
        }

        // If we got this far, something failed, redisplay form
        ViewData["Title"] = "Register";
        ViewData["PasswordLength"] = Provider.MinRequiredPasswordLength;
        ViewData["username"] = username;
        ViewData["email"] = email;

        return View();
    }

    public static string ErrorCodeToString(MembershipCreateStatus createStatus)
    {
        // See http://msdn.microsoft.com/en-us/library/system.web.security.membershipcreatestatus.aspx for
        // a full list of status codes.
        switch (createStatus)
        {
            case MembershipCreateStatus.DuplicateUserName:
                return "Username already exists. Please enter a different user name.";

            case MembershipCreateStatus.DuplicateEmail:
                return "A username for that e-mail address already exists. Please enter a different e-mail address.";

            case MembershipCreateStatus.InvalidPassword:
                return "The password provided is invalid. Please enter a valid password value.";

            case MembershipCreateStatus.InvalidEmail:
                return "The e-mail address provided is invalid. Please check the value and try again.";

            case MembershipCreateStatus.InvalidAnswer:
                return "The password retrieval answer provided is invalid. Please check the value and try again.";

            case MembershipCreateStatus.InvalidQuestion:
                return "The password retrieval question provided is invalid. Please check the value and try again.";

            case MembershipCreateStatus.InvalidUserName:
                return "The user name provided is invalid. Please check the value and try again.";

            case MembershipCreateStatus.ProviderError:
                return "The authentication provider returned an error. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

            case MembershipCreateStatus.UserRejected:
                return "The user creation request has been canceled. Please verify your entry and try again. If the problem persists, please contact your system administrator.";

            default:
                return "An unknown error occurred. Please verify your entry and try again. If the problem persists, please contact your system administrator.";
        }
    }
}

// The FormsAuthentication type is sealed and contains static members, so it is difficult to
// unit test code that calls its members. The interface and helper class below demonstrate
// how to create an abstract wrapper around such a type in order to make the AccountController
// code unit testable.

public interface IFormsAuthentication
{
    void SetAuthCookie(string userName, bool createPersistentCookie);
    void SignOut();
}

public class FormsAuthenticationWrapper : IFormsAuthentication
{
    public void SetAuthCookie(string userName, bool createPersistentCookie)
    {
        FormsAuthentication.SetAuthCookie(userName, createPersistentCookie);
    }
    public void SignOut()
    {
        FormsAuthentication.SignOut();
    }
}

Login View:

 
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Login.aspx.cs" Inherits="GuildSite.Views.Account.Login" %>


    

Login

Please enter your username and password below. If you don't have an account, please <%= Html.ActionLink("register", "Register") %>.

<%= Html.ValidationSummary()%>
">
Username: <%= Html.TextBox("username") %>
Password: <%= Html.Password("password") %>
Remember me?

Register View:

 
<%@ Page Language="C#" MasterPageFile="~/Views/Shared/Site.Master" AutoEventWireup="true" CodeBehind="Register.aspx.cs" Inherits="GuildSite.Views.Account.Register" %>


    

Account Creation

Use the form below to create a new account.

Passwords are required to be a minimum of <%=Html.Encode(ViewData["PasswordLength"])%> characters in length.

<%= Html.ValidationSummary()%>
">
Username: <%= Html.TextBox("username") %>
Email: <%= Html.TextBox("email") %>
Password: <%= Html.Password("password") %>
Confirm password: <%= Html.Password("confirmPassword") %>
 

Since I started playing World of Warcraft again, I've taken a bit more of a developer approach to it this time, and after founding a little casual guild, I decided to create a site for it.

However, I'm a lazy developer, I don't intend to update the site regularly whenever someone joins or leaves the guild.

Also because I'm quite geeky when it comes to statistics, and a bit of a theory crafter, I planned to populate our guild site with lots of stats.

Where else would be a better place to get them from then the Armory? It contains everything I want!

After searching a little, I found various libraries for PHP, Perl and Ruby, but nothing for the .NET world. At least nothing that fetches everything I wanted, like Reputation and Skills.

So, I decided to just write it myself! :)

Over the last week, I've been developing ArmoryLib, and decided to release it under the LGPL and use Google Code to store the source in.

You can find version 0.1 at http://code.google.com/p/armorylib/ under Downloads.

Have a look at the Documentation to see more details on the API and some example output.

Please, feel free to beta test it and leave your comments! Let me know when you use it for your projects, and remember... LGPL requires you give prominent notice about using the library! (A link to this post will do)

I will make a future post showing how I've used it to integrate into our guild website.

 

Chapter three finished, Searching, Modifying, and Encoding Text.

Implementing globalization, drawing, and text manipulation functionality in a .NET Framework application

This is basically an explanation on what a Regex is, and how you can use it to match and manipulate strings with it.

Besides working with strings, it also deals with reading and writing them in different encodings.

This was quite a short chapter, with some basic knowledge for every developer.