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.

As somebody who’s followed Aaron Swartz technologically and personally for many years now, it saddens me to hear about his recent passing.

I’ve lost friends to suicide, it’s no joke. There is grievance, there is sorrow, there is curiosity. We can never put ourselves in the shoes of that individual and truly empathize. We try to blame and criticize to help cope with the loss, but it’s never quite enough.

What makes it worse is that I know people have/will label him as a sort of “martyr” in regards to freedom of information, net neutrality, along with countless other initiatives he took part in. Now I’m not undermining his accomplishments at all, don’t get me wrong.

People will naturally take advantage of the situation to run a soap box campaign against whoever they feel is responsible for this and part of that sickens me. Nobody knows for certain why he did it, but in that same aspect, it’s hard to discredit the quick and easy assumption that the prosecutors involved in recent cases were namely to blame.

This has obviously already started to happen, fingers are being pointed. While I hate when tragedies are exploited for political/personal gain, I think it would be a bit disrespectful to Aaron to not look at this with a bit of skepticism.

You can’t tell me that a looming 35 year prison sentence combined with a 1 million dollar fine doesn’t make you consider taking a route like this.

While we won’t ever be able to truly understand what Aaron was thinking at the time, if it was indeed a result of tremendous legal pressure, all I can say is that this country has disappointed me once again. While I’m not going to justify or criminalize Aaron’s actions, it’s quite sad that a white-collar crime like this fetches a heavier sentence than one who rapes or murders.

While this post doesn’t do Aaron justice for everything that he’s done, I know he’s made an impact on countless individuals who will hopefully carry on his ideologies, both in spirit and in practice.

Rest easy Aaron and know that you’ve inspired the hearts of many.

Here are some interesting links pertaining to Aarons’ death:

Lawrence Lessig:

http://lessig.tumblr.com/post/40347463044/prosecutor-as-bully

Alex Stamos:

http://unhandled.com/2013/01/12/the-truth-about-aaron-swartzs-crime