Ruleset

Started by
8 comments, last by Emmanuel Deloget 18 years, 10 months ago
Hi, For a project I need an user hierarchy. For example, a weakling can use only the commands build and attack and an emperor can use build, attack and diplomacy ... Now I want to make a virtual base class user and weakling is derived from this class, emperor is derived from weakling. I have a dynamic parser (using spirit) and when I make for example an object emperor I want to make my parser look for the commands (words) that emperor can do and associate it with a command (a pointer to a member function list whould be ideal). But how can I do this now ? Using RTTI is an option, but then I must make multiple if/else switches in the parser and must manually add the commands to the parser. I can't use a simple loop or something then and I can't simply make a new class later. Multiple dispatch isn't an option either. I have tried it with templates also.

class User
{
 	  public:
	  		 virtual void ArgChecker() =0;
	  		 
      private:
	  		 list<string> PossibleArguments;
};

class Weakling : virtual public User 
{
 	  public:
	  		 void Build(){cout << "Building something: Weakling" << endl;}
	  		 void Attack(){cout << "Attacking something: Weakling" << endl;}
	  		 void ArgChecker() {cout << "Checking arguments: Weakling" << endl;}
//in constructor add all possible arguments 
};

class Emperor : public Weakling
{
 	  public:
                         void ArgChecker() {cout << "Checking arguments: Emperor" << endl;}
	  		 void Diplomacy(){cout << "Supercommand: diplomacy" endl;}
//in constructor add all extra possible arguments
}; 	
Advertisement
I did something like this recently.

Here's generally what it consisted of:

- Object containing a list of user rights.
For me, this meant a simple "is_admin()" list.
- User object with a unique indentifier.
Using the user object's pointer is sufficient, though I'd recommend some sort of uid.
- Helper functions to add a user to the list, usually via password.
You don't seem to need this.
- A command syntax map.
You seem to have this with your parser. Essentially something that takes the initial command given, and does a functor lookup. Mine also included parameter types, and did auto conversion. The code:<br><br><br>cmd_set.add("foo int int", foo());<br>cmd_set.parse("foo 42 1", user);<br><br><br>Would do as you expect, map a function which takes 2 ints, and call foo(42,1); for user. <br>- A wrapper functor for "Admins &#111;nly"<br> This would be a functor [with a templated operator()] which takes another arbitrary functor as a parameter to its constructor. When called, it checks if the caller is a member of the adminlist. If not it returns with a common failure. If so, it calls the stored functor, with the parameters passed to it. The code:<br><br><br>cmd_set.add("foo int int",admins_only(foo()));<br>cmd_set.parse("foo 42 1", user);<br><br><br>Would do as you expect; check if user is an admin first, and run foo if the user is. If not, cmd_set.parse() will return back the error value [or string, or however you do it]<br><br><br>So, this way, every "if admin" goes through &#111;ne piece of code. It has &#111;ne source for the error code. Most importantly of all, for the individual functions, the user rights qualifier is &#111;nly ever done &#111;nce, at the point it's added to the global command set. Thus it would &#111;nly ever need changed in &#111;ne place should the rights change.
argh, AP was me.
Hi,

Thanks for your answer, but I don't understand your idee completely, could you please give some basic code or pseudo-code to clarify ?
Here is some snippets for each in the code I'm currently using. I don't recommend using something like this, because my code is icky :] Plus, this all hasn't undergone rigorous testing yet, so there might be some leaks or gotchas...

[and sorry for the ugly code]

In order then... my admin list is simply:
set<guid>     admins;


Where guid is a unique identifier class.

The user class:
struct   playerinfo: public gamebase{       // snip snip.       //        // in gamebase:       guid       uid;       //        // snip snip.};


So when a player [or any other game object for that matter] is added to the adminlist, the gamebase.uid is added to the set.

Now, for the command_set, which does the mappings.

But first some discussion about a class I use throughout here:

vine * - A homegrown variant linked list class. A vine * depending on the context is either the container itself or an iterator. Using an Standard Library container of boost::variant should get you the same sort of functionality. Simply, it's 0 or more objects of any type.

Now. Starting at the bottom...

guiaction - A generalized functor interface. Using something like boost::function would probably eliminate the need for this... One of the more icky parts of the code... It was made before I knew formally what functors even were, and modified afterwards:
struct  guiaction{        virtual vine    *exec(vine *tar,vine *par)=0;        virtual vine    *exec(vine *par)        {return(exec(target,par));}        virtual vine    *exec()                 {return(exec(target,params));}        void            doa(vine *tar, vine *par){vine *v=exec(tar,par);v->nuke(&v);}        void            doa(vine *par){doa(target,par);}        void            doa(){doa(target,params);}        virtual vine    *stringify()=0;        virtual guiaction *clone()=0;        guiaction       *me(){return(this);}        vine    *target;        vine    *params;        guiaction(vine *intt=0, vine *inpp=0):target(intt),params(inpp){}        virtual ~guiaction(){target->nuke(&target);params->nuke(&params);}};

It has two inheritors, one for global functions [and functors], one for member functions:
template <typename F>struct  globalaction:public guiaction{        F       f;        virtual vine    *exec(vine *tar, vine *par){return(f(tar,par));}        virtual vine    *stringify();        virtual guiaction       *clone(){return((new globalaction(f,target->clone(&target),params->clone(&params)))->me());}        globalaction(F inf,vine *intar, vine *inpar):guiaction(intar,inpar),f(inf){}};template <typename T, typename F>struct  memberaction:public guiaction{        F       f;        T       t;        virtual vine    *exec(vine *tar, vine *par){return((t->*f)(tar,par));}        virtual vine    *stringify();        virtual guiaction       *clone(){return((new memberaction(t,f,target->clone(&target),params->clone(&params)))->me());}        memberaction(T inobj,F inf,vine *intar, vine *inpar):t(inobj),f(inf),guiaction(intar,inpar){}};


So now, the various templates can all be referred to from a guiaction *. But that's kind of annoying, since that requires the programmer to keep track of the guiaction and delete it. Plus, it's pretty annoying to specify template parameters for it every time, so this wrapper class and helper function was made:
struct  storega{        guiaction       *ga;        storega(guiaction *inga):ga(inga){}        ~storega(){delete ga;}        storega(const storega &that){                ga=that.ga->clone();        }        vine    *stringify(){return(ga->stringify());}        vine    *exec(vine *t, vine *p){return(ga->exec(t,p));}        vine    *exec(vine *p){return(ga->exec(p));}        vine    *exec(){return(ga->exec());}        void    doa(vine *t, vine *p){return(ga->doa(t,p));}        void    doa(vine *p){return(ga->doa(p));}        void    doa(){return(ga->doa());}        // TODO: other operators.};template <typename F>guiaction       *makega(F f,vine *intar, vine *inpar){return(new globalaction<F>(f,intar,inpar));}template <typename T, typename F>guiaction       *makega(T inobj,F inf,vine *intar, vine *inpar){return(new memberaction<T,F>(inobj,inf,intar,inpar));}


Now the interface plays nicely in Standard Library containers, and anything that returns a vine* and takes 2 vine*'s as parameters can be stored safely [along with it's parameters] in a storega. The templated functions will do auto-detection on the types, make the templated object with those types and return a generic guiaction * for use.

Now. Up to here almost certainly is a less-useful and less-generic re-implimentation of Standard Library or boost functionality. Please use that rather than my stuff. I hope I explained at least enough to allow you to understand what follows.

Now... back to the actual command_set. Well, sort of. First comes a class which is something to define generic syntax storage.
struct		syntax_def{private:	vector<int>	paramids;	string		keyword;public:	string		fetchname(){return(keyword);}	string		human_readable();	bool		match(char *c);		vine		*parse(char *c);	vine		*parse(string &s);	vine		*stringify();	vine		*unstringify(vine *inv);	syntax_def(const char 	*syntax);	syntax_def(){		// For stl containers mostly.		keyword="*";	}};string		syntax_def::human_readable(){//// Convert paramids to human readable form.//string				rtn=keyword;vector<int>::iterator		it;vector<int>::iterator		e=paramids.end();for (it=paramids.begin();it!=e;++it){	rtn=rtn + " <";	rtn=rtn + type_manager.fetchname(*it);	rtn=rtn + ">";}return(rtn);}bool		syntax_def::match(char *c){//////string		st;stringstream	ss(c);ss >> st;if (st == keyword){	return(1);}else{	return(0);}}vine		*syntax_def::parse(char *c){//////if (!match(c)){return(0);}string          st;stringstream    ss(c);vine		*rtn=0;vector<int>::iterator           it=paramids.begin();vector<int>::iterator           e=paramids.end();ss >> st;if (st==keyword){        // nothing, just pull the command name from the line.}else{        if (it!=e){			if (st[0]=='\"'){                st.replace(0,1,"");                string                  quotestr;                getline(ss,quotestr);                string::size_type       pos=quotestr.find_first_of("\"");                if (pos == quotestr.npos) { pos = quotestr.length(); }                st=st+quotestr.substr(0,pos);                ss.clear();		if (pos != quotestr.length()){pos++;}                ss.str(quotestr.substr(pos,quotestr.length()));        }                (((new vine(st.c_str())))->unstringify(*it))->splice(&rtn);                ++it;        }}while(it!=e && ss.good()){        ss >> st;	if (st[0]=='\"'){		st.replace(0,1,"");		string			quotestr;		getline(ss,quotestr);		string::size_type	pos=quotestr.find_first_of("\"");		if (pos == quotestr.npos) { pos = quotestr.length(); }		st=st+quotestr.substr(0,pos);		ss.clear();		if (pos != quotestr.length()){pos++;}		ss.str(quotestr.substr(pos,quotestr.length()));		//cout << "postfixed: " << ss.str() << "\n";	}        (((new vine(st.c_str())))->unstringify(*it))->splice(&rtn);        ++it;}return(rtn);}vine	*syntax_def::parse(string &s){//////char	*c=(char *)malloc(s.length()+1);vine	*v;strncpy(c,s.c_str(),s.length());v=parse(c);free(c);return(v);}syntax_def::syntax_def(const char *syntax){//////string          st;stringstream    ss(syntax);int		x;ss >> st;if (st.empty()){	// bogus entry...	keyword="*";	return;}else{	keyword=st;}while (1){	if (!ss.good()){		return;	}	ss >> st;	if (st.length()==0 ){		return;	}	x=type_manager.fetchid(st.c_str());	if (x!=type_manager.failure()){		paramids.push_back(x);	}}}vine		*syntax_def::stringify(){//////return(new vine((human_readable()).c_str()));}vine		*syntax_def::unstringify(vine *inv){//////string          	st;string::size_type	pos;int             	x;syntax_def		*sd;if (!inv || inv->type!=type_manager.fetchid("cmd")){return(0);}stringstream		ss((char *)inv->data);st = (char *)inv->data ;pos=st.find("<");while(pos!=st.npos){	st.replace(pos,1,"");	pos=st.find("<");}pos=st.find(">");while(pos!=st.npos){	st.replace(pos,1,"");	pos=st.find(">");}sd=new syntax_def(st.c_str());if (sd->fetchname()=="*"){	delete sd;	return(0);}return(new vine(type_manager.fetchid("syntax_def"),sd));}registered_type <syntax_def,class_stringify<syntax_def>, class_unstringify<syntax_def> > *syntax_def_type = new registered_type<syntax_def,class_stringify<syntax_def>, class_unstringify<syntax_def> >("syntax_def",1);struct		guiaction_syntax : public syntax_def{	storega		ga;	vine		*exec(char *c);	guiaction_syntax(const char *c,guiaction *inga=makega(devnull,0,0)):ga(inga),syntax_def(c){}};vine	*guiaction_syntax::exec(char *c){//////vine	*v=parse(c);ga.exec(v);v->nuke(&v);}registered_type <guiaction_syntax,class_stringify<guiaction_syntax>, class_unstringify<guiaction_syntax> > *guiaction_syntax_type = new registered_type<guiaction_syntax,class_stringify<guiaction_syntax>, class_unstringify<guiaction_syntax> >("guiaction_syntax",1);

The paramids is simply a list of ints which correlates to the vine's internal type data. Also, the vine class holds stringify/unstringify [essntially serialization info] about all of the types it can hold. That's also why after each class definition is the monsterous template stuff.

But you said you've already got nifty parsers, so this is rather redundant. Note though that the inheriting guiaction_syntax stores an arbitrary function with the syntax.

The command set is nothing more than a grouping of these:
class   command_set{private:        hash_map<string,guiaction_syntax *>                     cmdmap;        typedef hash_map<string,guiaction_syntax *>::iterator   mapit;        lookup<                guiaction_syntax,                hash_map<string,guiaction_syntax *>,                string        >                                                       fetchgas;public:        vine *syntax(string);        string  str_syntax(string);        vine *stringify();        void add(string,guiaction *);        vine *cmd(vine *,string);        vine *cmd(vine *,vine *);        vine *cmdout(vine *,vine *);        bool match(string);        vine *syntax_check(string);        vine *syntax_check(vine *);        command_set(){}        ~command_set();};// snip snip.void    command_set::add(string s,guiaction *ga){//// Add guiaction ga with syntax s to the command set.//// The guiaction is lost to the command_set which will clean it up.//guiaction_syntax        *gas;guiaction_syntax        *gascheck;string                  gasname;if (!ga){        gas=new guiaction_syntax(s.c_str(),makega(devnull,0,0));}else{        gas=new guiaction_syntax(s.c_str(),ga);}gasname=gas->fetchname();if (gasname == "*"){        // creation failure? Abort.        delete gas;        return;}gascheck=fetchgas(gasname,cmdmap);if (gascheck){        // Overwrite old entry.        mapit   it=cmdmap.find(gasname);        delete  it->second;        cmdmap.erase(gasname);}cmdmap[gasname]=gas;} vine    *command_set::cmd(vine *tar,string c){//// Attempt to execute command defined by string c.//// tar will be passed as the guiaction target. Defines the caller [for multiplayer command parsing].//  called functions must not modify tar.//guiaction_syntax        *gas;stringstream            ss(c);string                  st;vine                    *v;vine                    *rtn=0;ss >> st;gas=fetchgas(st,cmdmap);if (!gas){        // unknown command.        (new vine(st.c_str()))->top(&rtn);        (new vine("Unknown command: "))->top(&rtn);        return(rtn->vinecat());}v=gas->parse(c);rtn=gas->ga.exec(tar,v);v->nuke(&v);return(rtn);}// snip snip.


The cmd_set can then be used to define a series of commands with their functions like I described above. For the admin_only, here's the actual functor wrapper I'm using [remember that admins is merely a global set for now...]:

struct  admin_only{//// A functor modifying functor, which will short circut the functor chain//  if caller [target] is not in the admins list.//        storega ga;        vine    *operator()(vine *target, vine *params){                vine            *v;                playerinfo      *pptr;                if (!target){                        //                        // if null, just run it.                        //                        return(ga.exec(target,params));                }                if (target->type != type_manager.fetchid("player")){                        return(new vine("Caller not player? Abort."));                }                pptr=(playerinfo *)target->data;                if (admins.find(pptr->uid)!=admins.end()){                        return(ga.exec(target,params));                }else{                        return(new vine("Admin rights required."));                }        }        admin_only(storega inga):ga(inga){}};

So, this functor simply wraps around another, adding functionality to it. This is in the large round about way I got to being able to do this:
server_cmdset.add("quit", makega( admin_only( storega( makega( server_shutdown("Server shutdown by admin request."),0,0))),0,0));

Which is a long, obtuse way to simply say "if an admin enters the command 'quit' shutdown the server." Still far, far better than a giant switch statement or if/else tree.
Hmmm what about using functors and inheriting the function for the several cases? Wouldn't that be simpler?
Quote:Original post by yukit0
Hmmm what about using functors and inheriting the function for the several cases? Wouldn't that be simpler?


Inheritance over-rides, it does not extend.

struct A{      virtual void    operator()(){             cout << "A";      }};struct B: public A{      virtual void    operator()(){            cout << "B";      }};int main(){A      a;B      b;a();b();}


will print AB for example not AAB. You could put some magicry in the constructors to automatically build a list of functions to run, but that would be less versatile than the wrapper sort of functor in my example above.
Right now, you are using inheritance:

class Emperor : public Weakling


It do not sound good. From a formal point of view, an emperor is not a weakling, thus your Emperor class should not inherits the Weakling class.

It is a typical case where you should favor composition over inheritance, because both Emperor and Weakling are user roles, not user types. You can define You classes like this:

// USING PATTERN: strategy (see GoF)class Role { ... };class User{  Role *mRole;};class Weakling : public Role { ... };class Emperor : public Role { ... };


Now, you have to implement actions:

// USING PATTERN: command (see GoF)class Action{public:  typedef sometypeyoudefined Result;  virtual Action::Result process(a list of parameters) = 0;};class Role{public:  // redefine for Emperor and Weakling  virtual Action::Result doAction(Action *action) = 0;};class Emperor : public Role{  Action::Result doAction(Action *action)  {    return action->process(some parameters);  }};class User{public:  Action::Result doAction(Action *action)  {    return mRole->doAction(action);  }};


Now:
* you can define whatever action you want. The action code is in the action class (in the process method), thus you are not limited to a set of known operations - it is easier to extend your model.
* you can define whatever role you want. A priest ? nothing simpler: define a new class Priest : public Role, and that's it.
* you can change the role at runtime - yes, a weakling can become an emperor or a priest.
* it may have a small issue: you need to feed the User with a correct Action - for example, if the User has a Role of Weakling, you can't tell him to build a new city. To correct this, you can use rtti in the derived role classes: for example, you can inherit the emperors actions from an EmperorAction class (which inherits from Action) and then test if the action object is an EmperorAction in the process() method. Another way which do not need rtti is to add an action registration mecanism into the derived Role classes and then to consider than an action which is not registered in a particular Role cannot be processed by this Role. This is quite similar to Telastyn's cmdset, and this is the solution I'd implement.

As you can see, this design offer many opportunities which may be harder to develop (and far harder to generalize) with your previous design.

HTH,
Thanks for your post, this is exactly what I need. It's very flexible and easy to use. I considered not using inheritence because I tought that a emperor and a weakling had an "is-a" relation so I dropped that solution immediately. But your absolutely right your option is much much better.

Thanks very much you both.

P.S: One question though, how would you make the registration mechanism ?

[Edited by - Rip7 on May 31, 2005 10:18:59 AM]
Quote:Original post by Rip7
Thanks for your post, this is exactly what I need. It's very flexible and easy to use. I considered not using inheritence because I tought that a emperor and a weakling had an "is-a" relation so I dropped that solution immediately. But your absolutely right your option is much much better.

Thanks very much you both.

P.S: One question though, how would you make the registration mechanism ?


I had a reflexion about this yesterday evening, and here two options which can be used to check whether you are allowed to apply an action on a user or not, depending on the user role:

1) register the action in an action list: you need to have a run-time identifier to identify a particular action. This can be the action name, or the action class name, or a simple integer. In the following example, I use an action name.

class ActionNameList{  std::vector<std::string> mActionNames;public:  void add(const std::string& actionName);  bool verify(const std::string& actionName); // search the name};class Emperor : public Role{private:  static ActionNameList mActionList;  bool verifyAction(Action *a) { return mActionList.verify(action->name()); }public:  void addActionToList(Action *a) { mActionList.add(action->name()); }  Action::Result doAction(Action *action)  {    if (verifyAction(action))    {      return action->process(list of params);    }    return Action::error;  }};


This is simple to implement, extensible, but may be slow if you have a lot of action (because each time you want to process an action, you have to verify if the action name is in the action name list or the role).

2) another possiblity is to use a role check policy; here we choose to verify the name of the role class (because it is simple).
class NameCheckPolicy{public:  virtual verify(const std::string& name) = 0;};class Action{  NameCheckPolicy *mPolicy;public:  Action(RoleCheckPolicy *policy) : mPolicy(policy) { }  bool verify(const std::string& name) { return mPolicy->verify(name); }};class Emperor : public Role{private:  std::string mName;  bool verifyAction(Action *a) { return action->verify(name()); }public:  std::string name() const { return mName; }  Action::Result doAction(Action *action)  {    if (verifyAction(action))    {      return action->process(list of params);    }    return Action::error;  }};

You can then define as many derived NameCheckPolicy as you want. For example, I'd define NoNameCheckPolicy, which doesn't check the name at all, SingleNameCheckPolicy, which checks the name against one single value, and MultipleNameCheckPolicy, which checks the name against multiple values.

Let's face it, checking names (either in the action list case or in the name checking policy case) is not a very good solution, because a small error in a string can ruin the whole system. Fortunately, there is a solution which helps, because it gives us a string that we don't control and which is guaranteed to not be modified: using the typeid() keyword returns a typeid object, fro which we can get the class name using the name() method.

typeid() do not find the real name of an object - it gives the current type of the object. For example:
class A { };class B : public A { };A *a = new B();std::cout << typeid(a).name() << std::endl;

Writes "A" on the console output.

HTH,

This topic is closed to new replies.

Advertisement