Hi,
I gived a try to this question and turned in a pretty intrincated but very easy to use module.

EXTENDED VERSION
For a simpler version go to next post.

It calls a function when a combo gets completed. It works with collections of scancodes as references. The data is stored in a linked tree manner where nodes hold their next possible scancodes. This way the combo detector fuction only searchs into the smallest scope of possible combos.

The combo detector works in the scope of a previously created set of scancodes and sequences. This means that, unlike the previous version, any stroke of any key not contained into the scancode list of the running set will have no effect into the running set. This way two different sets of combos can be run parallel with no interferences between them. A necessary feature for single machine multiplayer games or an useful feature when you want to run a different set of events on different player states. f.e: cmbOnGround, cmbOnAir, etc.



Source
Download source and examples

The basics
The functions and their parameters are briefly explained into source//combos.h

Code:
void brokenCombo () {
   str_cpy ( strComboLast, "Failed" );
   snd_play ( sndFail, 100, 0 );
}

var cmbEvent () {
   snd_play ( sndCombo, 100, 0 );
}

action actPlayer () {
   // Create a new combo set
   COMBO_SET *cmbSimple = comboSetCreate ( 4, NULL ); // 4 is the timeout lapse in ticks 
   // Add the desired amount of scancodes
   comboScancodeCollectionAdd ( cmbSimple, _vArray( SC_A, SC_S, SC_D ), 3 );
   // Add a combo to play with
   comboAdd ( cmbSimple, _vArray ( SC_S,  SC_A,  SC_D,  SC_S,  SC_A,  SC_A,  SC_S,  SC_A ), 8, cmbEvent ); 
   while ( player ) {
      // Make the system work
      comboSetRun ( cmbSimple, brokenCombo );
      wait(1);
   }
   ent_remove ( me );
   // Remove data 
   comboSetRemove ( cmbSimple );
}



This little example shows the very basics and its usage is as simple as that. Combos can be added at any time. A single combo can be deleted by calling comboAdd with its scancodes sequence and a NULL event. If your combos planning only needs single key strokes combos, that is all you need to know.

The example uses a helper function called _vArray that locally builds an array of an arbitrary (up to 8) number of scancodes in order to pass them as function parameter.

Key releasing virtual scancodes
It needs the FLAG_RELEASE flag in order to call key releasing events. Unlike the previous version, key releasing events are key specific and activated as their negative scancode.

Code:
// key S needs to be released before pressing D in order to complete the combo
comboAdd ( cmbSimple, _vArray ( SC_S, -SC_S,  SC_D,  SC_A ), 4, cmbEvent );
// key D needs to be released after pressing S in order to complete the combo
comboAdd ( cmbSimple, _vArray ( SC_D,  SC_S, -SC_D,  SC_A ), 4, cmbEvent );



Custom scancodes
This is a new feature that lets you use anything you can imagine as combo member, not only engine scancodes.

Code:
var comboScancodeCustom ( COMBO_SET *_cmbSet, var *_condition );



comboScancodeCustom creates a virtual scancode activated by the return of _condition, adds it to the _cmbSet combo set and returns its value.

The return of the condition function must work as key_... variables do. The function has to return 1 when a certain condition is true and it has to return 0 when not.

Code:
var evn_AD_together () {
   if ( key_a && key_d )
      return 1;
   else
      return 0;
}
...
COMBO_SET *_cmbSet = comboSetCreate ( 4, NULL );
comboScancodeCollectionAdd ( _cmbSet, _vArray( SC_A, SC_D, SC_W, SC_S), 4 );
var _kAandD = comboScancodeCustom ( _cmbSet, evn_AD_together ); // save the returning scancode for combo creation
comboAdd ( cmbSimple, _vArray ( SC_D,  _kAandD, SC_W,  SC_S ), 4, cmbEvent );



The usage of this function can be pretty conflictive when certain key takes effect on more than one event, as in the example above, since A and D are set as valid scancodes on their own. The downloadable pack contains an extended example of a solution for this issue into examples//two_keys.c

Further information
comboRun returns a pointer to an array containing a secuence of scancodes that has been detected as into a combo at that run, so it can be evaluated. This array is allocated as sequence member into the combo set struct.

Code:
str_cpy ( str, "Combo on air: " );
var *_scodes = comboRun ();
for ( ; *_scodes!=0; _scodes++ ) { // the returning array always contains a zero as last member
	str_cat ( str, str_for_key ( NULL, *_scodes ) );
	str_cat ( str, " " );
}
draw_text ( str, 10, 600, COLOR_WHITE );



The behavior of each combo set can be modified by some flags.
FLAG_RELEASE Activates key release virtual scancodes
FLAG_STRICT Makes key releases break combos as key presses do
FLAG_RESTART Makes the system look for a combo that starts by the scancode that made last active combo fail
FLAG_REBUILD Makes the system rebuild the whole scancode sequence retuned by comboRun each run.
FLAG_RETURN Activates the answer to combo events return. f.e: return COMBO_DISABLE in order to remove the event calling branch. More info into source//combos.h

Custom scancodes conditions and combo events can receive a pointer when set at creation time. More info into source//combos.h

The module contains a very useful drawComboSet function in order to debug the module functioning. It also contains all the engine scancodes set as macros in the SC_... form. More info into source//combos.h

I will be pleased of answering any other question (or rant), and don't forget to look at source//combos.h grin

Examples
  • Basic
    A simple setup for simple combos.
  • Two keys
    A demostration of the use of a ghost combo event that works as switch for a custom scancode.
  • Gestures
    A demostration of custom scancodes combos used as mouse gestures detector.