Here’s a quick little class for implementing global hotkeys in C#/.NET 2.0+.

As a bit of a preface, you could easily use [RegisterHotKey]http://msdn.microsoft.com/en-us/library/windows/desktop/ms646309(v=vs.85).aspx) and [UnregisterHotKey]http://msdn.microsoft.com/en-us/library/windows/desktop/ms646327(v=vs.85).aspx) respectively to accomplish something like this, but this has a few caveats:

  • You can’t register a key that has already been previously registered.
  • Some keys are reserved and cannot be registered, such as F12.
  • Although hacks exist, it’s not very ideal to easily implement with console applications due to the lack of a proper window handle.

Alternatively, we can use another WinAPI function: [GetAsyncKeyState]http://msdn.microsoft.com/en-us/library/windows/desktop/ms646293(v=vs.85).aspx). It requires a bit more manual work, but it’s simple in the end.

Note: Without getting into semantics about what constitutes a proper “hook”, I’m just going to refer to the process of monitoring/polling key events via GetAsyncKeyState() as “hooking”.

To keep things simple, the actually hooking process will use the [Keys Enumeration]http://msdn.microsoft.com/en-us/library/system.windows.forms.keys.aspx) and will marshall their underlying integral type during the P/Invoke. When a key is pressed, we will trigger the KeyPressed event.

Modifier Keys

We don’t want to limit the hooking to just basic keys, instead will allow optional modifier keys using the ModifierKeys enum.

[Flags]
public enum ModifierKeys : uint
{
    None = 0,
    Alt = 1,
    Control = 2,
    Shift = 4,
}

Hooking & Unhooking

To hook a key, call the Hook() method, supplying the Keys value as well as any optional modifier keys. Additionally, you can provide a delegate to use for a callback for when the key is pressed.

To unhook a key, simple call the Unhook() method with the appropriate parameters and it will no longer be polled.

[Flags]
var keyboard = new KeyboardHook();
keyboard.Hook(Keys.PrintScreen);
 
// do something
 
keyboard.Unhook(Keys.PrintScreen);

Internally, the hooked keys will be stored as KeyHook objects, which provide Keys and Modifiers properties.

private class KeyHook
{
    public KeyHook(Keys key, ModifierKeys modifiers, KeyHookDelegate pressed = null)
    {
        Key = key;
        Modifiers = modifiers;
        Pressed = pressed;
    }
 
    public Keys Key { get; private set; }
    public ModifierKeys Modifiers { get; private set; }
    public KeyHookDelegate Pressed { get; private set; }
}

Prioritization

For simple hotkeys, there won't be any collisions. However, when you start mixing and matching modifier keys, things can get a little messy. To alleviate this issue, the hooked keys are sorted internally using a custom [IComparer<T>](http://msdn.microsoft.com/en-us/library/8ehhxeaf.aspx):
private class HookComparer : IComparer<KeyHook>
{
    private static int GetModifierCount(ModifierKeys modifiers)
    {
        var iValue = (int) modifiers;
        var bitCount = 0;
 
        while (iValue != 0)
        {
            iValue = iValue & (iValue - 1);
            bitCount++;
        }
 
        return bitCount;
    }
 
    #region Implementation of IComparer<KeyHook>
 
    public int Compare(KeyHook x, KeyHook y)
    {
        var keyCompare = x.Key.CompareTo(y.Key);
        if (keyCompare != 0)
            return keyCompare;
 
        var modifierCount = GetModifierCount(y.Modifiers).CompareTo(GetModifierCount(x.Modifiers));
        if (modifierCount != 0)
            return modifierCount;
 
        return ((int) x.Modifiers).CompareTo((int) y.Modifiers);
    }
 
    #endregion
}

Basically, it just compares hooked keys based on the following:

  1. (Keys](http://msdn.microsoft.com/en-us/library/system.windows.forms.keys.aspx) enumeration
  2. Number of ModiferKeys set
  3. Underlying ModifierKeys integral type summation

To sort the list, we use a simple lambda expression with our comparer:

_keys.Sort((k1, k2) => new HookComparer().Compare(k1, k2));

Polling

The underlying polling is based on a [SystemTimer.Timer]http://msdn.microsoft.com/en-us/library/system.timers.timer.aspx). According to [official Microsoft sources]http://msdn.microsoft.com/en-us/windows/hardware/gg463266.aspx), this has a resolution of 15.6ms:

The default timer resolution on Windows 7 is 15.6 milliseconds (ms). Some applications reduce this to 1 ms, which reduces the battery run time on mobile systems by as much as 25 percent.

I’m not about to perform a case steady on how fast a human can realistically type versus the timer interval, configure the interval as necessary. The polling itself can be enabled/disabled via the Enabled property. Additionally, polling is suppressed when hooking/unhooking keys. During each tick, the key states are polled via PollKeyStates():

private void PollKeyStates()
{
    var altPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.Menu));
    var controlPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.ControlKey));
    var shiftPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.ShiftKey));
 
    var pressedKeys = new List<Keys>();
 
    foreach (var key in _keys)
    {
        if ((GetAsyncKeyState(key.Key) == -32767))
            pressedKeys.Add(key.Key);
    }
 
    foreach (var key in _keys)
    {
        if (!pressedKeys.Contains(key.Key))
            continue;
 
        if ((key.Modifiers & ModifierKeys.None) == ModifierKeys.None)
        {
            if ((key.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt && !altPressed)
                continue;
            if ((key.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && !controlPressed)
                continue;
            if ((key.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift && !shiftPressed)
                continue;
        }
 
        pressedKeys.Remove(key.Key);
 
        if (KeyPressed != null)
            KeyPressed(this, new KeyPressedEventArgs(key.Modifiers, key.Key));
 
        if (key.Pressed != null)
            key.Pressed(this, EventArgs.Empty);
    }
}

When using GetAsyncKeyState() you need to pay attention to the most and least significant bits:

If the function succeeds, the return value specifies whether the key was pressed since the last call to GetAsyncKeyState, and whether the key is currently up or down. If the most significant bit is set, the key is down, and if the least significant bit is set, the key was pressed after the previous call to GetAsyncKeyState. However, you should not rely on this last behavior.

Normally when polling like these, we would end up triggering multiple KeyPressed events within a few milliseconds apart from each other. We can use the return value to get around this.

A temporary List is created which contains all hooked keys which are currently pressed. This way we don't need to call GetAsyncKeyState() while iterating over the hooked keys and we can prevent hook collisions. This is where the key sorting from earlier comes into play. We don't want to prematurely trigger a KeyPressed event for a different hooked key than expected.

This isn’t necessarily a perfect solution but it works and is simple and flexible.

Finally, here’s the complete class:

/*
* KeyboardHook.cs by Nate Shoffner
* http://nateshoffner.com
*/
 
#region
 
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Timers;
using System.Windows.Forms;
using Timer = System.Timers.Timer;
 
#endregion
 
namespace GlobalHotkeys
{
    [Flags]
    public enum ModifierKeys : uint
    {
        None = 0,
        Alt = 1,
        Control = 2,
        Shift = 4,
    }
 
    public class KeyPressedEventArgs : EventArgs
    {
        internal KeyPressedEventArgs(ModifierKeys modifiers, Keys key)
        {
            Modifiers = modifiers;
            Key = key;
        }
 
        public Keys Key { get; private set; }
        public ModifierKeys Modifiers { get; private set; }
    }
 
    public class KeyboardHook : IDisposable
    {
        #region P/Invokes
 
        [DllImport("user32.dll")]
        private static extern short GetAsyncKeyState(Keys vKey);
 
        #endregion
 
        #region Delegates
 
        public delegate void KeyHookDelegate(object sender, EventArgs e);
 
        #endregion
 
        private readonly List<KeyHook> _keys = new List<KeyHook>();
        private readonly Timer _timer;
 
        public KeyboardHook()
        {
            _timer = new Timer {Interval = 75};
            _timer.Elapsed += _timer_Elapsed;
        }
 
        public bool Enabled
        {
            get { return _timer.Enabled; }
 
            set
            {
                if (value)
                    _timer.Start();
                else
                    _timer.Stop();
            }
        }
 
        public event EventHandler<KeyPressedEventArgs> KeyPressed;
 
        public bool Hook(Keys key, ModifierKeys modifiers = ModifierKeys.None, KeyHookDelegate pressed = null)
        {
            if (_timer.Enabled)
                _timer.Stop();
 
            var exists = _keys.Find(x => x.Key == key && x.Modifiers == modifiers) != null;
 
            if (!exists)
            {
                _keys.Add(new KeyHook(key, modifiers, pressed));
                _keys.Sort((k1, k2) => new HookComparer().Compare(k1, k2));
            }
 
            if (_keys.Count > 0)
                _timer.Start();
 
            return !exists;
        }
 
        public bool Unhook(Keys key, ModifierKeys modifiers = ModifierKeys.None)
        {
            if (_timer.Enabled)
                _timer.Stop();
 
            var i = _keys.FindIndex(x => x.Key == key && x.Modifiers == modifiers);
 
            if (i >= 0)
            {
                _keys.RemoveAt(i);
                return true;
            }
 
            if (_keys.Count > 0)
                _timer.Start();
 
            return false;
        }
 
        private void _timer_Elapsed(object sender, ElapsedEventArgs e)
        {
            PollKeyStates();
        }
 
        private void PollKeyStates()
        {
            var altPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.Menu));
            var controlPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.ControlKey));
            var shiftPressed = Convert.ToBoolean(GetAsyncKeyState(Keys.ShiftKey));
 
            var pressedKeys = new List<Keys>();
 
            foreach (var key in _keys)
            {
                if ((GetAsyncKeyState(key.Key) == -32767))
                    pressedKeys.Add(key.Key);
            }
 
            foreach (var key in _keys)
            {
                if (!pressedKeys.Contains(key.Key))
                    continue;
 
                if ((key.Modifiers & ModifierKeys.None) == ModifierKeys.None)
                {
                    if ((key.Modifiers & ModifierKeys.Alt) == ModifierKeys.Alt && !altPressed)
                        continue;
                    if ((key.Modifiers & ModifierKeys.Control) == ModifierKeys.Control && !controlPressed)
                        continue;
                    if ((key.Modifiers & ModifierKeys.Shift) == ModifierKeys.Shift && !shiftPressed)
                        continue;
                }
 
                pressedKeys.Remove(key.Key);
 
                if (KeyPressed != null)
                    KeyPressed(this, new KeyPressedEventArgs(key.Modifiers, key.Key));
            }
        }
 
        #region Nested type: HookComparer
 
        private class HookComparer : IComparer<KeyHook>
        {
            private static int GetModifierCount(ModifierKeys modifiers)
            {
                var iValue = (int) modifiers;
                var bitCount = 0;
 
                while (iValue != 0)
                {
                    iValue = iValue & (iValue - 1);
                    bitCount++;
                }
 
                return bitCount;
            }
 
            #region Implementation of IComparer<KeyHook>
 
            public int Compare(KeyHook x, KeyHook y)
            {
                var keyCompare = x.Key.CompareTo(y.Key);
                if (keyCompare != 0)
                    return keyCompare;
 
                var modifierCount = GetModifierCount(y.Modifiers).CompareTo(GetModifierCount(x.Modifiers));
                if (modifierCount != 0)
                    return modifierCount;
 
                return ((int) x.Modifiers).CompareTo((int) y.Modifiers);
            }
 
            #endregion
        }
 
        #endregion
 
        #region Implementation of IDisposable
 
        public void Dispose()
        {
            _timer.Stop();
            _timer.Dispose();
        }
 
        #endregion
 
        #region Nested type: KeyHook
 
        private class KeyHook
        {
            public KeyHook(Keys key, ModifierKeys modifiers)
            {
                Key = key;
                Modifiers = modifiers;
            }
 
            public Keys Key { get; private set; }
            public ModifierKeys Modifiers { get; private set; }
        }
 
        #endregion
    }
}

Random update time! I have a bad habit of not updating my applications for long periods of time…

A few weeks ago, I received a pingback from AddictiveTips where they wrote an articleon Tabster. I’m still not sure why I just now received the PingBack when the article was published on March 29th of this year. This isn’t the first time Tabster has been reviewed by any means (and has also been used in schools), but it brought the neglected project to my attention.

Well, much re-factoring later and Tabster 1.6 is ready.

Change Log:

  • Added “My Downloads” section
  • Added auto-scrolling
  • Added single-instance functionality
  • Added persistent window size/state
  • Added preview pane to library and search
  • Updated file formats
  • Caching is now forced
  • Removed auto-detect
  • Removed web browser
  • Fixed and improved searching
  • Added multi-downloader
  • Redesigned user interface
  • Refactored just about everything

You can download it on the projects page.

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.