A few months back, I wrote a [post](http://www.nateshoffner.com/2013/03/a-well-deserved-apology “A Well-Deserved Apology) where I apologized to Mathew Kemp (aka Sniped) from Jagex. I finally felt as though I had some closure. Some peace of mind. Everything changed when the Jagex nation attacked.

The other week, an interesting detail was brought to my attention. In a recent update of Ace of Spades, they decided to finally include some credits. Somebody showed me a screenshot of what appeared to be a smear campaign against the original developer. I figured it was Photoshopped, as no reputable company would target an individual like that.

I decided to check in-game for myself and what do you know, there it was.

"Even in the darkest days when one of our best men, the lead programmer, proved to be a double agent and attempted to leave the building with sensitive intel! Fear not the cops always get their man."

As a quick synopsis (as well as a bit of a preface) for those who don’t know, Ben Aksoy was the original developer of Ace of Spades. After selling the rights to Jagex, he later ended up working at their Cambridge office. In mid-late 2012, he was unhappy with the way things were being handled at the company and the direction in which the game was going, resulting in him putting in his final notice. After coming back from lunch one day, his hard drive in his personal laptop somehow magically broke, resulting in a BSOD. Additionally, stuff in his workspace was moved around. A fellow employee said the system admin had been at his desk. Upon approaching the sysadmin and asking about it, they denied everything.

This sounds a bit like a conspiracy at first and maybe it is. But the point remains that the hard drive just spontaneously broke over a short time period. Additionally, I don’t think anybody is surprised by some methods companies use in order to “protect” intellectual property and how poorly they treat their employees. For reference, feel free to read some of the reviews from current/previous Jagex employees online.

Before I propose my theory I have a bit of disclaimer. I’ll admit I have a bit of a personal bias against Jagex. On that same list is the Westboro Baptist Church, copyright/patent trolls, and whoever decided to cancel Oreo O’s. Basically, if you’re going to negatively impact/inhibit other people, society, or my morning ritual of pure bliss with milk, I’m going to have a personal bias against you.

Conspiracy #1

So this is my theory in tying the two incidents together. As soon as I read “attempted to leave the building with sensitive intel”, I immediately thought of that situation from a few months back. Perhaps Jagex was worried that Ben would be leaving with source code on his personal laptop. It seems like it’d be just tad extreme to destroy/swap a hard drive in order to protect an investment, but I wouldn’t rule it out entirely.

As for the “Fear not the cops always get their man” part, I’m not really sure what that entails. I haven’t heard anything regarding Ben and the authorities. I may be looking too far into it, but regardless that’s quite a bit of slander.

Conspiracy #2

Let’s move on and analyze those credits. First we need to take a solid look at that middle paragraph:

"Even in the darkest days when one of our best men, the lead programmer, proved to be a double agent and attempted to leave the building with sensitive intel! Fear not the cops always get their man. But as a team everyone gathered together and rallied and before you stands a game we are all tremendously proud of, enjoyed by a community we have incredible respect for. gathered together and rallied behind the game. Before you stands a game we are all tremendously proud of, enjoyed by a community we have incredible respect for."

Anybody with a 2nd grade education is probably scratching their heads right now. Look at the last 2 sentences and how they repeat. No that wasn’t my typo, that’s in the game. It looks as though somebody made a last minute edit and copy/pasted the text without any final proof-read. Perhaps the text was just shifted to the right during the edit. If that’s the case, I would like to know whether this is something that was known before release or if it was edited in at the very last minute without anybody noticing.

Conclusion

Regardless of conspiracies, motives and other drama, one point still remains. I don’t care what company you work for. No company should be personally attacking an individual like this. Even moreso after everything he was put through until he finally had enough. Is it not bad enough that you ripped the guy off and made false promises, only to screw him over in the end? Then you recreate a watered-down copy of the game that panders to drooling Call of Duty kids while completely alienating the existing userbase. The game has severely flopped and you have yet to make a real profit off of it. Yet your priorities still seem to be personally attacking the developer. Good game. (No, not Ace of Spades 1.0. Are you even paying attention?)

They made sure to give credit to Blitz Game Studios and Mat, but not the primary developer, at least not by name. It’s not as if they could’ve totally forgotten Ben’s involvement, which makes me think this was a known edit.

We could give Jagex the benefit of the doubt. Maybe they aren’t referring to Ben. Maybe it’s just complete satire which just happens to fit that exact situation perfectly. Maybe Jagex didn’t break his hard drive in his laptop before leaving. Maybe Jagex’s “little brother” snuck in and edited the credits without anybody knowing. Who knows?

Because I’m on a roll, here are some other points of interests within the credits:

"It has been a long and arduous campaign to get from a simple blocky green field to an international conflict involving hundreds of thousands of gamers."

Ahem, “hundreds of thousands” should be changed to “hundreds”. Don’t flatter yourself. You may have made claims of several hundred thousand users before, but the actual stats say otherwise.

"Without the awesome Ace of Spades community we would have not been anywhere near the success we have been."

Actually, if you would’ve listened to the community for once, maybe the game would’ve had a chance at actually being a success. Try checking your forums, steam community, in-game players, and the rest of the internet.

"To those of you who directly contributed to building Ace of Spades from the very early days; the code contributors, the community team, the mappers, the modders and the third party software creators, we recognise your efforts and want to give you your very own 21 gun salute!"

As somebody who contributed with the development of the game, the community, as well as my own software, you can keep your salute. You already desecrated one developer, leave myself and any others out of it.

And of course for reference, here are some screenshots of the credits:

This post in no way reflects the collective opinion of Build and Shoot and/or Buld Then Snip, LLC.

Update (06/17/2013)

As of July 13th (5 days after originally posting this entry), Jagex released an update for the game. The complete change log can be found [here](http://www.aceofspades.com/forums/showthread.php?20030-Update-Notes-13th-June-Discussion-Thread).

Not included in that change log is the fact that they completely removed the credits page. Instead, there is an image in the background crediting Blitz Game Studios.

So, kudos to Jagex for removing the defamatory messages from the credits, but they still failed to take responsibility for their mishap.

Not surprised that once again, you guys have have half-assed the job.

Months ago, amidst the chaos and commotion that is IRC, there were constant discussions with Jagex’s community managers Sniped (Mathew Kemp) and SallyTheButcher (Sally Da Costa). Conversation was often heated regarding the Ace of Spades community. This was primarily due to the Jagex community managers waving unwarranted sense of know-all in regards to the game and community based off “experience” from other games/communities. Most of which were dropped months after being launched, so they refused to properly cite said “experience”. Through all the bickering, I think it’s fair to say that nobody bumped heads more than Sniped and myself (IRC handle “StackOverflow”). This post is primarily directed at Sniped but occasionally Jagex as a whole.

September 2nd, 2012:

[16:12] <@StackOverflow> this is a waste of time, you can't talk to people who are too self-absorbed in their own ways of doing things 
[16:12] <sniped> well, you can go and do something else Stack
[16:13] <@StackOverflow> sniped, you're too clueless to have the slightest idea of what we're doing
[16:13] <sniped> kk stack, well come back in 6 months and I'll accept your apology

This quote is taken from a daily argument within an operator IRC channel (that we owned, mind you) and my statement was directed towards Build and Shoot, which at the time was being worked on privately.

Well, here we are. 6 months later. We’ve both changed a lot since then, haven’t we? I sure have. I’ve grown to the point where I can freely admit my wrongdoings and feel no particular shame, but rather learn from them.

So, lets get this going. I have a long, well deserved apology from myself to you:

I’m sorry that your incompetence and persistent self-assurance has resulted in you completely alienating yourselves from the community and in turn, the game entirely

You had a great opportunity to turn this game into something great. Now I don’t mean this in the typical bitter 12 year old fashion. I mean you quite literally had a flourishing community and found a way to piss them off in nearly everywhere possible, some things I would’ve never seen coming. For that, I applaud you.

I know instantly you would assume this is typical, biased banter. You seem to live in some weird alternate reality where Jagex does no wrong and you know the community well. With a game like Ace of Spades, community is everything. If you can’t handle the community, you are bound to fail. A vast majority of the heated flaming towards Jagex was (and still is) at least mildy constructive and yet you decide to focus on the unconstructive criticisms from 12 year olds. I’m not sure if you do this intentionally or you are just unable to differentiate between blatant flaming/trolling and constructive criticism. I’m assuming the latter since this was an ongoing problem on IRC, where you and Sally would focus on people spamming instead of people with legitimate questions (myself included).

Danhezee and myself were often subject to blame for the community’s negative sentiment towards Jagex. Early on, that might have seemed like the case, sure, assuming people aren’t able to form an opinion on their own. It’s very easy to blame a handful of people instead of taking responsibility for your own mismanagement. However, it’s gotten to the point where the criticism has moved beyond just the community. For example, Gamasutra has written an article titled “How Ace of Spades gave Jagex a lesson in community management”. All over the internet you see people complaining about the way Jagex failed to handle the community. I’m curious as to whether you are still so disillusioned as to think that this is the handy work of a few disgruntled individuals or if you’ve finally realized that you’re to blame.

I’m sorry that your marketing campaigns have backfired

Early on, it was quite obvious you were a fan of numbers, saying things like “well, when we have 3 million players…” when discussing scalability, obsessing over Facebook “likes”, along with telling the community there were “over 500k” pre-registered players for 1.0. Scalability is great, but so is realism. At the time, the beta was holding a steady 3,800 players. This was without any sort of advertising and without updates for months on end.

Before release, there was a massive marketing campaign involving paying YouTubers (even though you try to deny it, it's obvious),  media outlets, and even "major" news outlets like [NBC](http://www.nbcnews.com/technology/ingame/machine-guns-meet-minecraft-ace-spades-1C6814146). (Because we all know NBC had nothing better to report than the release of Ace of Spades 1.0).

Promotional graphics were obviously rendered, which wouldn’t have been so much of an issue had you not openly stated they were in-game screenshots. Spoiler: They weren’t.

Now, in retrospect, 1.0 can barely muster more than 1,500 players at any given time with the exception of when it is 50% off on Steam. Any game will sell well on Steam if it’s 50% off. Look a graph charting the playercounts over the last few months and you can easily determine when the game was on sale. 2-3 days after a steam sale, players drop right down to where they were beforehand.

There is nothing that is captivating them. Sure, you got their money, but that’s not sustainable in the long term. Let’s do some math here. I’ve heard Valve takes roughly a 30% cut of sales, I’m not sure how accurate that is myself, if anybody knows for certain feel free to let me know, but it feels like a safe estimate. Based off that estimate, for every 1,000 copies you sell while it is 50% off, Jagex is only making $3,500. Keep in mind, they have been dumping money into this game since early-mid 2011. There have been massive advertising efforts, developer teams (both internal and external), third-party code payoffs (pyspades), servers, and more spanned over multiple years. This is in no way sustainable.

Now there is paid DLC for sale, which is a whole discussion in itself. Even after saying there would be no paid DLC, I think they might be starting to realize that they aren’t going to break even. Next, they’ll likely start leasing privatized servers in an effort to cover server costs. Even though the game servers are hosted on cheap VPSs, it still adds up. People want the flexibility of open server software, not just locked down servers. Sadly, Jagex will still just look at this as another method for potential profit and half-ass it like always.

Some of the primary factors in Ace of Spades’ marketing seem to be:

  • Misleading figures
  • Misleading promotional content (graphics, maps, etc)
  • Misleading system specs. (The steam page claimed it required DirectX 9.0 for the longest time when the game uses OpenGL)

Have fun explaining those figures to your venture capitalists. (As a side note: Anybody who says you should buy 1.0 to “support” the developers, clearly has no idea how Jagex is financially structured.)

I’m sorry that your lack of technical prowess has come back to bite you in the ass

This was evidently a touchy subject, seeing as I was banned on the forums shortly after posting it on the forums.

After seeing you write this:

Sniped wrote: The botting issue in the prototype of AoS will not exist when it is fully released. Simple as that.

I replied with the statement below, resulting in me being banned for “flaming other users”.

lol. Please learn how aimbots work before making a statement like this.

This reminds me of a quote off of IRC:

<Sniped> if there is no cheating possible, would you need extra admin tools?

You shouldn’t make such bold implications, especially if they have no technical backing.

People will cheat in 1.0 just as with any game. OpenGL as a technology is tried and tested when it comes to cheating.

I wouldn’t consider idle legal threats as being a “wealth of experience”. I’ve done my fair share of anti-cheat measures, both technically and socially. I have real-world knowledge and real-world expectations about the subject. Blindly expecting Jagex to obliterate all cheating is quite preposterous. Especially when trying to pull a line like “We have a wealth of experience in dealing with bots too.”, when everybody knows Jagex has little to no control over the cheating in their headlining game.

Protip: The average 12 year old who downloads an aimbot isn’t going to care about some idle legal threat. It’s quite laughable.

I speak for myself when saying I don’t take a company seriously if they try to threaten a lawsuit against anybody who cheats in a game. If anything, it’s likely to stir up interest in people to make cheats as a sort of challenge.

Also, mfw everybody is pretty much reiterating what I wrote in a post, only to have it deleted for being “libelous”. The only problem is my statement was based of of personal expereience from RuneScape, as well as public documentation regarding legalites between Jagex and cheaters. Basically, it’s proven and factual (although my personal experence with RS would be considered opinionated). That’s not anywhere near being “libelous”.

This is just one of many examples where your lack of real-world experience/knowledge has come back to haunt you. By making bold statements and implications such as those, you ignited the interest in many individuals to create cheats, some of which were out on launch day. Why? I’ll give you a hint, people will always cheat. People will do it for whatever reason they want. But for those who make cheats, it’s generally for the challenge to do so and when you antagonize people with statements such as those, you’re only adding fuel to the fire. So congratulations. You not only proved how ignorant you are, but you also helped ruin the in-game experience for players.

Here’s a little bit of advice. I know at Jagex, you guys are quote fond of extravagant (and rather pretentious) titles, but those titles are meaningless unless you have real knowledge, and more importantly, real-world experience. A popular trend among Jagex employees seems to be that of praising the “specialists” within the company. Any problem you can think of, there’s evidently a specialist for it. I’m not doubting that there are people who could know what they’re doing there, but if YOU don’t have an accurate estimation of their skillset, I’m not going to take your word for it. Not to mention, there are many track records with how Jagex has handled, or failed to handle certain situations. Take cheating for instance. You claim you have “anti-cheat specialists”. Where are they? I’m going to assume they’re not involved with Ace of Spades, otherwise you wouldn’t leave such gaping holes like leaving a god damn console within the game.

I’m sorry for your skewed outlook of Build and Shoot and its denial of partnership

The Ace of Spades community as a whole was happy to see Build and Shoot carrying on support for the free to play version and the response to this by Jagex was censorship and bans. I could partially understand this from a company perspective. If people know there’s a free version, they’ll be less inclined to buy the paid version. However, it’s become quite apparent to everybody that the main reason for the irrational responses were the result of petty differences still lingering between the old Ace of Spades staff and Jagex’s community managers.

As a result, the only people who were negatively effected by this was the community, not Build and Shoot. On the contrary, the constant censorship resulted in a Streisand effect and it became even more popular, so thank you for that.

Build and Shoot extended its arms with Jagex in an attempt to patch things up. We tried appeasing Jagex by adding sections on our forums for the 1.0 release. After an exchange of emails, primarily with Sniped, we realized there was always a blockade between us and their appeasement. A snippet from the last email reads:

The passion of your community has overflowed onto some of our other platforms, such as social media and Steam. We have mentioned this to you before but have yet to see any attempts by the site administrators to deal with this issue. This issue is not something which just affects us as the behavior of some of these people who represent your community is going to reflect badly on you.

As I mentioned earlier, you fail to take responsibility for your own mistakes and are under the severe delusion that we are responsible for the negative sentiment towards Jagex.

Partnering with Build and Shoot would’ve only resulted positively for Jagex, yet petty, childish, disagreements still seem to be a problem. The community knows about Build and Shoot, so there’s no real point in censoring it. Partnership would’ve meant less work on your end in terms of moderation and would’ve helped appease the community greatly. It could’ve helped serve as a marketing advantage for new players who are introduced to Build and Shoot and could possibly become interested in 1.0, sadly you seemed to look at things through a very narrow scope.

I’ll cut it short with the apologies. I’m sure there is more to say, but I feel all 3 people who have read this far want to stab me with a soldering iron for wasting their time. Thankfully I actually remembered to type this up today, as I wouldn’t want my punctual reputation to be blemished. As I said, the game has always been centered around the community. Your failure  to properly manage it has resulted in the entire game spiraling downward. Sorry you had to learn that the hard way.

Amidst all of these apologies, I must grant you very sincere “thank you” for helping show me where having an undeserved sense of accomplishment and self-righteous attitude can get you. Second, I would like to thank you for helping make Build and Shoot what it is now and for it’s continuous growth. We have a thriving community that is completely decentralized. Any attempts at patching things up with Jagex has been mainly a philanthropic effort for the community. Granted we would get publicity, which is nice. But you have given us more than enough through your failed attempts at censoring us and the community.

A while back, I was offered to work at Jagex  in Cambridge to help develop Ace of Spades. At the time it seemed like a fantastic opportunity. Sadly, I declined the offer, so we didn’t get to share this roller coaster experience as drinking buddies. But judging by your actions, incoherent thinking, and constantly tripping over yourself, you’ve been drunk this entire time.

tl;dr Sorry.

Sincerely, Nate Shoffner, Senior apology specialist.

This post in no way reflects the collective opinion of Build and Shoot and/or Buld Then Snip, LLC.

Update (3/5/2013):

It seems this article has been passed around quite a bit throughout Jagex.

It has come to my attention that Jagex has been censoring this article on their platforms. Now this doesn’t come as a surprise due to the nature of the article, but their reason for doing so is a bit absurd, in my opinion. Initially, the reasoning for deleting the posts was because it mentioned Build and Shoot. Sorry that Build and Shoot’s success is starting to overshadow your own.

One of the forum staff, lods, said this:

“it’s directing people to a non constructive article which is full of inaccuracies…”

This might be a biased opinion, but I’d say that the article was at least mildy constructive. As for “inaccuracies”, unless you can provide direct examples, don’t say anything. Maybe instead of just skimming the article and making a baseless assumption, you could actually read it before claiming there are “inaccuracies”.

Jagex’s response to this article has done nothing but prove the very behavior it was criticizing.

Thanks for proving my point, again.

Anybody who has worked with the .NET Framework has likely dealt with the native configuration files, especially if you’re using something as intuitive as Visual Studio. While the native functionality is pretty nifty, there’s still one small gripe myself and many other developers have. The .NET Framework is designed in a way that applications are to interface with a single configuration file whose location is found somewhere between AppData and obscurity. The reason for this design, according to Microsoft, was to alleviate the possibility of overwrite collisions between different applications. Despite being asked to allow developers to manage their own relative configuration paths, Microsoft has stood by this design. It’s quite an annoying “feature”, but there are workarounds, albeit tedious. This article provides a good bit of insight on how to tackle something like this. by inheriting the SettingsProvider class.

using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Configuration;
using System.Linq;
using System.Text;
using Microsoft.Win32;
using System.Xml;
using System.Xml.Serialization;
 
public class PortableSettingsProvider : SettingsProvider
{
    // Define some static strings later used in our XML creation
    // XML Root node
    const string XMLROOT = "configuration";
 
    // Configuration declaration node
    const string CONFIGNODE = "configSections";
 
    // Configuration section group declaration node
    const string GROUPNODE = "sectionGroup";
 
    // User section node
    const string USERNODE = "userSettings";
 
    // Application Specific Node
    string APPNODE = System.Reflection.Assembly.GetExecutingAssembly().GetName().Name + ".Properties.Settings";
 
    private System.Xml.XmlDocument xmlDoc = null;
 
 
 
    // Override the Initialize method
    public override void Initialize(string name, NameValueCollection config)
    {
        base.Initialize(this.ApplicationName, config);
    }
 
    // Override the ApplicationName property, returning the solution name.  No need to set anything, we just need to
    // retrieve information, though the set method still needs to be defined.
    public override string ApplicationName
    {
        get
        {
            return (System.Reflection.Assembly.GetExecutingAssembly().GetName().Name);
        }
        set
        {
            return;
        }
    }
 
    // Simply returns the name of the settings file, which is the solution name plus ".config"
    public virtual string GetSettingsFilename()
    {
        return ApplicationName + ".config";
    }
 
    // Gets current executable path in order to determine where to read and write the config file
    public virtual string GetAppPath()
    {
        return new System.IO.FileInfo(System.Reflection.Assembly.GetExecutingAssembly().Location).DirectoryName;
    }
 
    // Retrieve settings from the configuration file
    public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext sContext, SettingsPropertyCollection settingsColl)
    {
        // Create a collection of values to return
        SettingsPropertyValueCollection retValues = new SettingsPropertyValueCollection();
 
        // Create a temporary SettingsPropertyValue to reuse
        SettingsPropertyValue setVal;
 
        // Loop through the list of settings that the application has requested and add them
        // to our collection of return values.
        foreach (SettingsProperty sProp in settingsColl)
        {
            setVal = new SettingsPropertyValue(sProp);
            setVal.IsDirty = false;
            setVal.SerializedValue = GetSetting(sProp);
            retValues.Add(setVal);
        }
        return retValues;
    }
     
    // Save any of the applications settings that have changed (flagged as "dirty")
    public override void SetPropertyValues(SettingsContext sContext, SettingsPropertyValueCollection settingsColl)
    {
        // Set the values in XML
        foreach (SettingsPropertyValue spVal in settingsColl)
        {
            SetSetting(spVal);
        }
 
        // Write the XML file to disk
        try
        {
            XMLConfig.Save(System.IO.Path.Combine(GetAppPath(), GetSettingsFilename()));
        }
        catch (Exception ex)
        {
            // Create an informational message for the user if we cannot save the settings.
            // Enable whichever applies to your application type.
             
            // Uncomment the following line to enable a MessageBox for forms-based apps
            //System.Windows.Forms.MessageBox.Show(ex.Message, "Error writting configuration file to disk", System.Windows.Forms.MessageBoxButtons.OK, System.Windows.Forms.MessageBoxIcon.Error);
 
            // Uncomment the following line to enable a console message for console-based apps
            //Console.WriteLine("Error writing configuration file to disk: " + ex.Message);
        }
    }
 
    private XmlDocument XMLConfig
    {
        get
        {
            // Check if we already have accessed the XML config file. If the xmlDoc object is empty, we have not.
            if (xmlDoc == null)
            {
                xmlDoc = new XmlDocument();
 
                // If we have not loaded the config, try reading the file from disk.
                try
                {
                    xmlDoc.Load(System.IO.Path.Combine(GetAppPath(), GetSettingsFilename()));
                }
 
                // If the file does not exist on disk, catch the exception then create the XML template for the file.
                catch (Exception)
                {
                    // XML Declaration
                    // <?xml version="1.0" encoding="utf-8"?>
                    XmlDeclaration dec = xmlDoc.CreateXmlDeclaration("1.0", "utf-8", null);
                    xmlDoc.AppendChild(dec);
 
                    // Create root node and append to the document
                    // <configuration>
                    XmlElement rootNode = xmlDoc.CreateElement(XMLROOT);
                    xmlDoc.AppendChild(rootNode);
 
                    // Create Configuration Sections node and add as the first node under the root
                    // <configSections>
                    XmlElement configNode = xmlDoc.CreateElement(CONFIGNODE);
                    xmlDoc.DocumentElement.PrependChild(configNode);
 
                    // Create the user settings section group declaration and append to the config node above
                    // <sectionGroup name="userSettings"...>
                    XmlElement groupNode = xmlDoc.CreateElement(GROUPNODE);
                    groupNode.SetAttribute("name", USERNODE);
                    groupNode.SetAttribute("type", "System.Configuration.UserSettingsGroup");
                    configNode.AppendChild(groupNode);
 
                    // Create the Application section declaration and append to the groupNode above
                    // <section name="AppName.Properties.Settings"...>
                    XmlElement newSection = xmlDoc.CreateElement("section");
                    newSection.SetAttribute("name", APPNODE);
                    newSection.SetAttribute("type", "System.Configuration.ClientSettingsSection");
                    groupNode.AppendChild(newSection);
 
                    // Create the userSettings node and append to the root node
                    // <userSettings>
                    XmlElement userNode = xmlDoc.CreateElement(USERNODE);
                    xmlDoc.DocumentElement.AppendChild(userNode);
 
                    // Create the Application settings node and append to the userNode above
                    // <AppName.Properties.Settings>
                    XmlElement appNode = xmlDoc.CreateElement(APPNODE);
                    userNode.AppendChild(appNode);
                }
            }
            return xmlDoc;
        }
    }
 
    // Retrieve values from the configuration file, or if the setting does not exist in the file,
    // retrieve the value from the application's default configuration
    private object GetSetting(SettingsProperty setProp)
    {
        object retVal;
        try
        {
            // Search for the specific settings node we are looking for in the configuration file.
            // If it exists, return the InnerText or InnerXML of its first child node, depending on the setting type.
 
            // If the setting is serialized as a string, return the text stored in the config
            if (setProp.SerializeAs.ToString() == "String")
            {
                return XMLConfig.SelectSingleNode("//setting[@name='" + setProp.Name + "']").FirstChild.InnerText;
            }
 
            // If the setting is stored as XML, deserialize it and return the proper object.  This only supports
            // StringCollections at the moment - I will likely add other types as I use them in applications.
            else
            {
                string settingType = setProp.PropertyType.ToString();
                string xmlData = XMLConfig.SelectSingleNode("//setting[@name='" + setProp.Name + "']").FirstChild.InnerXml;
                XmlSerializer xs = new XmlSerializer(typeof(string[]));
                string[] data = (string[])xs.Deserialize(new XmlTextReader(xmlData, XmlNodeType.Element, null));
 
                switch (settingType)
                {
                    case "System.Collections.Specialized.StringCollection":
                        StringCollection sc = new StringCollection();
                        sc.AddRange(data);
                        return sc;
                    default:
                        return "";
                }
            }
        }
        catch (Exception)
        {
            // Check to see if a default value is defined by the application.
            // If so, return that value, using the same rules for settings stored as Strings and XML as above
            if ((setProp.DefaultValue != null))
            {
                if (setProp.SerializeAs.ToString() == "String")
                {
                    retVal = setProp.DefaultValue.ToString();
                }
                else
                {
                    string settingType = setProp.PropertyType.ToString();
                    string xmlData = setProp.DefaultValue.ToString();
                    XmlSerializer xs = new XmlSerializer(typeof(string[]));
                    string[] data = (string[])xs.Deserialize(new XmlTextReader(xmlData, XmlNodeType.Element, null));
 
                    switch (settingType)
                    {
                        case "System.Collections.Specialized.StringCollection":
                            StringCollection sc = new StringCollection();
                            sc.AddRange(data);
                            return sc;
 
                        default: return "";
                    }
                }
            }
            else
            {
                retVal = "";
            }
        }
        return retVal;
    }
 
    private void SetSetting(SettingsPropertyValue setProp)
    {
        // Define the XML path under which we want to write our settings if they do not already exist
        XmlNode SettingNode = null;
 
        try
        {
            // Search for the specific settings node we want to update.
            // If it exists, return its first child node, (the <value>data here</value> node)
            SettingNode = XMLConfig.SelectSingleNode("//setting[@name='" + setProp.Name + "']").FirstChild;
        }
        catch (Exception)
        {
            SettingNode = null;
        }
 
        // If we have a pointer to an actual XML node, update the value stored there
        if ((SettingNode != null))
        {
            if (setProp.Property.SerializeAs.ToString() == "String")
            {
                SettingNode.InnerText = setProp.SerializedValue.ToString();
            }
            else
            {
                // Write the object to the config serialized as Xml - we must remove the Xml declaration when writing
                // the value, otherwise .Net's configuration system complains about the additional declaration.
                SettingNode.InnerXml = setProp.SerializedValue.ToString().Replace(@"<?xml version=""1.0"" encoding=""utf-16""?>", "");
            }
        }
        else
        {
            // If the value did not already exist in this settings file, create a new entry for this setting
 
            // Search for the application settings node (<Appname.Properties.Settings>) and store it.
            XmlNode tmpNode = XMLConfig.SelectSingleNode("//" + APPNODE);
 
            // Create a new settings node and assign its name as well as how it will be serialized
            XmlElement newSetting = xmlDoc.CreateElement("setting");
            newSetting.SetAttribute("name", setProp.Name);
             
            if (setProp.Property.SerializeAs.ToString() == "String")
            {
                newSetting.SetAttribute("serializeAs", "String");
            }
            else
            {
                newSetting.SetAttribute("serializeAs", "Xml");
            }
 
            // Append this node to the application settings node (<Appname.Properties.Settings>)
            tmpNode.AppendChild(newSetting);
 
            // Create an element under our named settings node, and assign it the value we are trying to save
            XmlElement valueElement = xmlDoc.CreateElement("value");
            if (setProp.Property.SerializeAs.ToString() == "String")
            {
                valueElement.InnerText = setProp.SerializedValue.ToString();
            }
            else
            {
                // Write the object to the config serialized as Xml - we must remove the Xml declaration when writing
                // the value, otherwise .Net's configuration system complains about the additional declaration
                valueElement.InnerXml = setProp.SerializedValue.ToString().Replace(@"<?xml version=""1.0"" encoding=""utf-16""?>", "");
            }           
 
            //Append this new element under the setting node we created above
            newSetting.AppendChild(valueElement);
        }
    }
}

Deserialization Bug

Immediately I found a bug during deserialization using ArrayList or StringCollection within GetSetting(). It seems that the collection was being cast to a string array during deserialization. Luckily this is a simple fix:

private object GetSetting(SettingsProperty setProp)
{
    object retVal;
     
    try
    {
        if (setProp.SerializeAs.ToString() == "String")
        {
            return XMLConfig.SelectSingleNode("//setting[@name='" + setProp.Name + "']").FirstChild.InnerText;
        }
 
        var xmlData = XMLConfig.SelectSingleNode(string.Format("//setting[@name='{0}']", setProp.Name)).FirstChild.InnerXml;
        return string.Format(@"{0}", xmlData);
    }
     
    catch (Exception)
    {
        if ((setProp.DefaultValue != null))
        {
            if (setProp.SerializeAs.ToString() == "String")
            {
                retVal = setProp.DefaultValue.ToString();
            }
            else
            {
                var settingType = setProp.PropertyType.ToString();
                var xmlData = setProp.DefaultValue.ToString();
                var xs = new XmlSerializer(typeof (string[]));
                var data = (string[]) xs.Deserialize(new XmlTextReader(xmlData, XmlNodeType.Element, null));
 
                switch (settingType)
                {
                    case "System.Collections.Specialized.StringCollection":
                        var sc = new StringCollection();
                        sc.AddRange(data);
                        return sc;
 
                    default:
                        return "";
                }
            }
        }
        else
        {
            retVal = "";
        }
    }
    return retVal;
}

Making it Reusable

Great, now it works perfectly. However, I’m far too lazy to copy this snippet into every project, I’d much prefer to reuse it from a referenced library. In order to do this, you’ll have to make use of GetEntryAssembly(). This will allow you to reference the portable settings class within any assembly. Some words of caution: This solution is dependent on the calling assembly name. It will create a directory in the ApplicationData folder using the Environment.GetFolderPath() method and then create the appropriate configuration file using the same name. Use it at your own risk and with common sense, especially if you plan on using this in any sort of production software.

Create a standard library (DLL) and add the following class:

public class PortableSettingsProvider : SettingsProvider
{
    private const string XMLROOT = "configuration"; // XML Root node
    private const string CONFIGNODE = "configSections"; // Configuration declaration node     
    private const string GROUPNODE = "sectionGroup"; // Configuration section group declaration node
    private const string USERNODE = "userSettings"; // User section node
 
    // Application Specific Node
 
    private static string ConfigName;
    private static string ConfigDirectory;
    private string APPNODE;
 
    // Store instace of calling assembly
    private Assembly entryAssembly;
 
    private XmlDocument xmlDoc;
 
    public override string ApplicationName
    {
        get { return (entryAssembly.GetName().Name); }
        set { APPNODE = value; }
    }
 
    private XmlDocument XMLConfig
    {
        get
        {
            // Check if we already have accessed the XML config file. If the xmlDoc object is empty, we have not.
            if (xmlDoc == null)
            {
                xmlDoc = new XmlDocument();
 
                // If we have not loaded the config, try reading the file from disk.
                try
                {
                    xmlDoc.Load(Path.Combine(GetAppPath(), GetSettingsFilename()));
                }
 
                    // If the file does not exist on disk, catch the exception then create the XML template for the file.
                catch (Exception)
                {
                    // XML Declaration
                    // <?xml version="1.0" encoding="utf-8"?>
                    var dec = xmlDoc.CreateXmlDeclaration("1.0", "utf-8", null);
                    xmlDoc.AppendChild(dec);
 
                    // Create root node and append to the document
                    // <configuration>
                    var rootNode = xmlDoc.CreateElement(XMLROOT);
                    xmlDoc.AppendChild(rootNode);
 
                    // Create Configuration Sections node and add as the first node under the root
                    // <configSections>
                    var configNode = xmlDoc.CreateElement(CONFIGNODE);
                    xmlDoc.DocumentElement.PrependChild(configNode);
 
                    // Create the user settings section group declaration and append to the config node above
                    // <sectionGroup name="userSettings"...>
                    var groupNode = xmlDoc.CreateElement(GROUPNODE);
                    groupNode.SetAttribute("name", USERNODE);
                    groupNode.SetAttribute("type", "System.Configuration.UserSettingsGroup");
                    configNode.AppendChild(groupNode);
 
                    // Create the Application section declaration and append to the groupNode above
                    // <section name="AppName.Properties.Settings"...>
                    var newSection = xmlDoc.CreateElement("section");
                    newSection.SetAttribute("name", APPNODE);
                    newSection.SetAttribute("type", "System.Configuration.ClientSettingsSection");
                    groupNode.AppendChild(newSection);
 
                    // Create the userSettings node and append to the root node
                    // <userSettings>
                    var userNode = xmlDoc.CreateElement(USERNODE);
                    xmlDoc.DocumentElement.AppendChild(userNode);
 
                    // Create the Application settings node and append to the userNode above
                    // <AppName.Properties.Settings>
                    var appNode = xmlDoc.CreateElement(APPNODE);
                    userNode.AppendChild(appNode);
                }
            }
            return xmlDoc;
        }
    }
 
    // Override the Initialize method
    public override void Initialize(string name, NameValueCollection config)
    {
        entryAssembly = Assembly.GetEntryAssembly();
 
        ConfigName = entryAssembly.GetName().Name;
 
        ConfigDirectory = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), entryAssembly.GetName().Name);
 
        if (!(Directory.Exists(ConfigDirectory)))
        {
            Directory.CreateDirectory(ConfigDirectory);
        }
 
        APPNODE = ConfigName + ".Properties.Settings";
 
 
        base.Initialize(ApplicationName, config);
    }
 
    // Simply returns the name of the settings file, which is the solution name plus ".config"
    public virtual string GetSettingsFilename()
    {
        return string.Format("{0}.config", ApplicationName);
    }
 
    // Gets current executable path in order to determine where to read and write the config file
    public virtual string GetAppPath()
    {
        return ConfigDirectory;
        //return new FileInfo(callingAssembly.Location).DirectoryName;
    }
 
    // Override the ApplicationName property, returning the solution name.  No need to set anything, we just need to
    // retrieve information, though the set method still needs to be defined.
 
 
    // Retrieve settings from the configuration file
    public override SettingsPropertyValueCollection GetPropertyValues(SettingsContext sContext, SettingsPropertyCollection settingsColl)
    {
        // Create a collection of values to return
        var retValues = new SettingsPropertyValueCollection();
 
        // Create a temporary SettingsPropertyValue to reuse
 
        // Loop through the list of settings that the application has requested and add them
        // to our collection of return values.
        foreach (SettingsProperty sProp in settingsColl)
        {
            var setVal = new SettingsPropertyValue(sProp) {IsDirty = false, SerializedValue = GetSetting(sProp)};
            retValues.Add(setVal);
        }
        return retValues;
    }
 
    // Save any of the applications settings that have changed (flagged as "dirty")
    public override void SetPropertyValues(SettingsContext sContext, SettingsPropertyValueCollection settingsColl)
    {
        // Set the values in XML
        foreach (SettingsPropertyValue spVal in settingsColl)
        {
            SetSetting(spVal);
        }
 
        // Write the XML file to disk
        try
        {
            XMLConfig.Save(Path.Combine(GetAppPath(), GetSettingsFilename()));
        }
        catch (Exception ex)
        {
            // Create an informational message for the user if we cannot save the settings.
            // Enable whichever applies to your application type.
 
            // Uncomment the following line to enable a MessageBox for forms-based apps
            //MessageBox.Show(ex.Message, "Error writting configuration file to disk", MessageBoxButtons.OK, MessageBoxIcon.Error);
 
            // Uncomment the following line to enable a console message for console-based apps
            //Console.WriteLine("Error writing configuration file to disk: " + ex.Message);
        }
    }
 
    // Retrieve values from the configuration file, or if the setting does not exist in the file,
    // retrieve the value from the application's default configuration
    private object GetSetting(SettingsProperty setProp)
    {
        object retVal;
        try
        {
            // Search for the specific settings node we are looking for in the configuration file.
            // If it exists, return the InnerText or InnerXML of its first child node, depending on the setting type.
 
            // If the setting is serialized as a string, return the text stored in the config
            if (setProp.SerializeAs.ToString() == "String")
            {
                return XMLConfig.SelectSingleNode("//setting[@name='" + setProp.Name + "']").FirstChild.InnerText;
            }
 
            // This solves the problem with StringCollections throwing a NullReferenceException
            var xmlData = XMLConfig.SelectSingleNode(string.Format("//setting[@name='{0}']", setProp.Name)).FirstChild.InnerXml;
            return string.Format(@"{0}", xmlData);
        }
        catch (Exception)
        {
            // Check to see if a default value is defined by the application.
            // If so, return that value, using the same rules for settings stored as Strings and XML as above
            if ((setProp.DefaultValue != null))
            {
                if (setProp.SerializeAs.ToString() == "String")
                {
                    retVal = setProp.DefaultValue.ToString();
                }
                else
                {
                    var settingType = setProp.PropertyType.ToString();
                    var xmlData = setProp.DefaultValue.ToString();
                    var xs = new XmlSerializer(typeof (string[]));
                    var data = (string[]) xs.Deserialize(new XmlTextReader(xmlData, XmlNodeType.Element, null));
 
                    switch (settingType)
                    {
                        case "System.Collections.Specialized.StringCollection":
                            var sc = new StringCollection();
                            sc.AddRange(data);
                            return sc;
 
                        default:
                            return "";
                    }
                }
            }
            else
            {
                retVal = "";
            }
        }
        return retVal;
    }
 
    private void SetSetting(SettingsPropertyValue setProp)
    {
        // Define the XML path under which we want to write our settings if they do not already exist
        XmlNode SettingNode;
 
        try
        {
            // Search for the specific settings node we want to update.
            // If it exists, return its first child node, (the <value>data here</value> node)
            SettingNode = XMLConfig.SelectSingleNode(string.Format("//setting[@name='{0}']", setProp.Name)).FirstChild;
        }
        catch (Exception)
        {
            SettingNode = null;
        }
 
        // If we have a pointer to an actual XML node, update the value stored there
        if ((SettingNode != null))
        {
            if (setProp.Property.SerializeAs.ToString() == "String")
            {
                SettingNode.InnerText = setProp.SerializedValue.ToString();
            }
            else
            {
                // Write the object to the config serialized as Xml - we must remove the Xml declaration when writing
                // the value, otherwise .Net's configuration system complains about the additional declaration.
                SettingNode.InnerXml = setProp.SerializedValue.ToString().Replace(@"<?xml version=""1.0"" encoding=""utf-16""?>", "");
            }
        }
        else
        {
            // If the value did not already exist in this settings file, create a new entry for this setting
 
            // Search for the application settings node (<Appname.Properties.Settings>) and store it.
            var tmpNode = XMLConfig.SelectSingleNode(string.Format("//{0}", APPNODE)) ?? XMLConfig.SelectSingleNode(string.Format("//{0}.Properties.Settings", APPNODE));
 
            // Create a new settings node and assign its name as well as how it will be serialized
            var newSetting = xmlDoc.CreateElement("setting");
            newSetting.SetAttribute("name", setProp.Name);
            newSetting.SetAttribute("serializeAs", setProp.Property.SerializeAs.ToString() == "String" ? "String" : "Xml");
 
            // Append this node to the application settings node (<Appname.Properties.Settings>)
            tmpNode.AppendChild(newSetting);
 
            // Create an element under our named settings node, and assign it the value we are trying to save
            var valueElement = xmlDoc.CreateElement("value");
            if (setProp.Property.SerializeAs.ToString() == "String")
            {
                valueElement.InnerText = setProp.SerializedValue.ToString();
            }
            else
            {
                // Write the object to the config serialized as Xml - we must remove the Xml declaration when writing
                // the value, otherwise .Net's configuration system complains about the additional declaration
                valueElement.InnerXml = setProp.SerializedValue.ToString().Replace(@"<?xml version=""1.0"" encoding=""utf-16""?>", "");
            }
 
            //Append this new element under the setting node we created above
            newSetting.AppendChild(valueElement);
        }
    }
}

Assigning the ProviderBase

Next, open your Visual Studio solution and go to the Settings tab located in the project properties for the appropriate project. For each of the settings that you want to make portable, modify the “Provider” property to represent the namespace scheme for your portable settings class. You will have to modify the provider property for each subsequent setting if you want it to remain portable. It can be a bit of a hassle, but in the end, it’s a small cost for better control.

I have been using this code for a few years now on a few smaller projects with no problems. As I’ve stated, it’s not perfect, but it’s still an alright solution, assuming you use it properly.

6