🎉 Celebrating 25 Years of GameDev.net! 🎉

Not many can claim 25 years on the Internet! Join us in celebrating this milestone. Learn more about our history, and thank you for being a part of our community!

No news means Old news.

Published February 23, 2006
Advertisement
Thankfully, this little journal was started late, meaning any nice code tidbits I do have are available for discussion when I have been overly lazy/busy. Like this week.

One particular piece of code that I've found to be far more valuable than I anticipated is the Command Set (cmdset). I originally wrote the Command Set class in C++ as an interpreter for a Quake-style console. The general idea was that you could add functions to the Set, and the Set would handle any parsing/type conversions to take a string and execute the function.

Since then it has undergone some modifications. Most notably, the most recent incarnation is in C#, and that cmdset is now an interface, not a solid class. I use it as a means to make strings follow the command pattern. Different concrete derivations include a method which also contains Help information, a 'fallthrough' class which matches any command and passes the string along, and a group of sets to allow additive set building from reference sets.

It's not as beefy as a full fledged interpreter or scripting solution, and (imo) tends to be a bit more generally useful because of it.


On to code. The first and foremost problem for this sort of solution is the conversion of string commandline parameters to non-string function parameters. In C++ (included for interest), I created a static template class to contain type serialization information:

#ifndef TYPE_REGISTRY#define TYPE_REGISTRY//#include "stringhash.h"//#include #include #include #include using   namespace       std;using   namespace       boost;const   char    unknown_type_name[]="unknown";template struct  def{        typedef         function          serialization_type;        typedef         functionconst char *)>     unserialization_type;        // TODO:        //      clones, deletors        //        static  string                  name;        static  serialization_type      serial_funk;        static  unserialization_type    unserial_funk;        string  serialize(T *t){ if(serial_funk.empty()){return("");} return(serial_funk(t));}        T       *unserialize(string s){ return(unserialize(s.c_str()));}        T       *unserialize(const char *s){ if(unserial_funk.empty()){return(0);} return(unserial_funk(s));}};template struct  null_serialization{        string  operator()(T *t){return("");}};template struct  null_unserialization{        T       *operator()(const char *s){return(0);}};template struct  ios_serialization{        string  operator()(T *t){                if(!t){return("");}                stringstream    ss;                ss << *t;                return(ss.str());        }};template struct  ios_unserialization{        T       *operator()(const char *s){                stringstream    ss(s);                T               *t=new T();                ss >> *t;                return(t);        }};template struct  intrusive_serialization{        string  operator()(T *t){                return(t->serialize());        }};template struct  intrusive_unserialization{        T       *operator()(const char *s){                return(T::unserialize(s));        }};template struct  constructor_unserialization{        T       *operator()(const char *s){                return(new T(s));        }};template struct  string_serialization{        string  operator()(T *t){                return(*t);        }};//// POD types.//def<int>::serialization_type            def<int>::serial_funk=ios_serialization<int>();def<int>::unserialization_type          def<int>::unserial_funk=ios_unserialization<int>();string                                  def<int>::name="int";def<long>::serialization_type           def<long>::serial_funk=ios_serialization<long>();def<long>::unserialization_type         def<long>::unserial_funk=ios_unserialization<long>();string                                  def<long>::name="long";def<double>::serialization_type         def<double>::serial_funk=ios_serialization<double>();def<double>::unserialization_type       def<double>::unserial_funk=ios_unserialization<double>();string                                  def<double>::name="double";def<float>::serialization_type          def<float>::serial_funk=ios_serialization<float>();def<float>::unserialization_type        def<float>::unserial_funk=ios_unserialization<float>();string                                  def<float>::name="float";def::serialization_type         def::serial_funk=string_serialization();def::unserialization_type       def::unserial_funk=constructor_unserialization();string                                  def::name="string";def<bool>::serialization_type           def<bool>::serial_funk=ios_serialization<bool>();def<bool>::unserialization_type         def<bool>::unserial_funk=ios_unserialization<bool>();string                                  def<bool>::name="bool";#endif


The correct template could then be picked based on the ARG parameters in boost::function. The C# version naively follows the same practice even though C# has nicer serialization methods... Ignorance is inversly proportional to solution goodness. Alas.

public static class typdict {        public delegate object destr(string s);        private static Dictionary typemap = new Dictionary();        public static void Add(Type t, destr f) {            typemap.Add(t, f);        }        public static object Parse(Type t, string s) {            if (!typemap.ContainsKey(t)) {                return (null);            }            return (typemap[t](s));        }        public static void Initialize() {            Add(typeof(int), delegate(string s) { return ((object)int.Parse(s)); });            Add(typeof(float), delegate(string s) { return ((object)float.Parse(s)); });            Add(typeof(string), System.Convert.ToString);        }    }


The second problem is command definition. In C++ this was rather easy. boost::function objects contain their type information. Tie a string to a function, pass the args as template parameters, and there you go.

C# it's a little trickier. Generics won't do the specification nicely (void is not a valid type for genericness it seems...). So for C# I required an explicit Delegate to be passed rather than a method. Reflection then allows type deduction. Here is the most common Command Definition. (note the base class contains the command name string, as well as the tokenize() function. The tokenize function is essentially a big Regex.Split() with an ugly regex ("split on a full line on whitespace, unless that white space is between double-quotes"). In C++, boost::spirit was used, and was much cleaner).)

[and pardon the variable naming... it was my second week or so with C#]

    public class cmd_del : cmddef {        protected Delegate cmd;        protected Type[] argts;        protected Type rtnt;        cmd_del(string cn, Delegate c, Type rt, params Type[] ats)            : base(cn) {            cmd = c;            rtnt = rt;            argts = ats;        }        protected override Type rtntype() { return (rtnt); }        protected override Type[] argtypes() { return (argts); }        public override object Parse(string cmdargs) {            ArrayList argz = new ArrayList();            string[] sargs = tokenize(cmdargs);            if (sargs.Length != argts.Length) {                return (null);            }            if (argts.Length == 0) {                return (cmd.DynamicInvoke());            }            for (int x = 0; x < sargs.Length; ++x) {                argz.Add(typdict.Parse(argts[x], sargs[x]));            }            object[] oargs = argz.ToArray();            return (cmd.DynamicInvoke(oargs));        }        public cmd_del(string cn, Delegate d)            : base(cn) {            cmd = d;            rtnt = d.Method.ReturnType;            List alargs = new List();            foreach (ParameterInfo p in d.Method.GetParameters()) {                alargs.Add(p.ParameterType);            }            argts = alargs.ToArray();        }    }


Once there, it's fairly simple to create the basic cmdset class:

    public abstract class cmd_interface {        public abstract object Parse(string cmdline);        public abstract string[] cmd_names();        public abstract string[] cmd_usages();        public abstract bool Has(string cmd);        public abstract int Count { get;}    }    public class cmdset :cmd_interface {        protected Hashtable cmdlist = new Hashtable();        public    logging_strategy logstrat = new logging_strategy();        // TODO: logging, events, usage lists, cmdlists.        public override object Parse(string cmdline) {            //            // Parse quake-style console command.            //            // Returns null on cmdlack.            //            string cmd;            string argz;            Regex cmdrx = new Regex(@"^(\w+)(?:[\s$]*)(.*)$");            Match rxmc = cmdrx.Match(cmdline);            if (!rxmc.Success) {                logstrat.log("cmdset: failed to parse: \"" + cmdline + "\"\n");                return (null);            }            cmd = rxmc.Groups[1].Value;            if (!cmdlist.ContainsKey(cmd)) {                logstrat.log("cmdset: Unknown command \"" + cmd + "\"\n");                return (null);            }            if (rxmc.Groups.Count == 1) {                argz = "";            } else {                argz = rxmc.Groups[2].Value;            }            return ((cmdlist[cmd] as cmddef).Parse(argz));        }        public virtual void Add(cmddef cmdf) {            cmdlist.Add(cmdf.cmdname, cmdf);        }        public override bool Has(string cmdname){            return(cmdlist.ContainsKey(cmdname));        }        public override string[] cmd_usages() {            List rtn = new List(cmdlist.Count);            foreach (DictionaryEntry de in cmdlist) {                rtn.Add((de.Value as cmddef).usage());            }            return (rtn.ToArray());        }        public override string[] cmd_names() {            List rtn = new List(cmdlist.Count);            foreach (DictionaryEntry de in cmdlist) {                rtn.Add((de.Value as cmddef).cmdname);            }            return (rtn.ToArray());        }        public override int Count {            get { return (cmdlist.Count); }        }    }


Thus you have a relatively simple class to provide plugable 'commands' for string input handling. While this sort of thing was originally meant for quake-style console input, and server console input one of the more interesting places I've found for it is in threading.

Combined with some code to facilitate inter-thread communication, the cmdset provides a common abstraction between thread communication and whatever methods the thread impliments.

From my threaded sound class:
            cs.Add(new cmd_del("play",new SoundEventType(this.PlaySound)));            cs.Add(new cmd_del("stop",new SoundEventType(this.StopSound)));            cs.Add(new cmd_del("pause",new SoundEventType(this.PauseSound)));


With the common communication, thread management and command set classes, all that needs done to thread-ify a class is to add the command definitions to an initialization method.

And there you go. A little tidbit of stuff to promote thought discussion and threats against my wellbeing.
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Profile
Author
Advertisement
Advertisement