diff --git a/src/commands/default/building.py b/src/commands/default/building.py index f040ba843c..909bcd756f 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -1169,6 +1169,74 @@ class CmdOpen(ObjManipCommand): back_exit_typeclass) +def _convert_from_string(cmd, strobj): + """ + Converts a single object in *string form* to its equivalent python + type. + + Python earlier than 2.6: + Handles floats, ints, and limited nested lists and dicts + (can't handle lists in a dict, for example, this is mainly due to + the complexity of parsing this rather than any technical difficulty - + if there is a need for @set-ing such complex structures on the + command line we might consider adding it). + Python 2.6 and later: + Supports all Python structures through literal_eval as long as they + are valid Python syntax. If they are not (such as [test, test2], ie + withtout the quotes around the strings), the entire structure will + be converted to a string and a warning will be given. + + We need to convert like this since all data being sent over the + telnet connection by the Player is text - but we will want to + store it as the "real" python type so we can do convenient + comparisons later (e.g. obj.db.value = 2, if value is stored as a + string this will always fail). + """ + + def rec_convert(obj): + """ + Helper function of recursive conversion calls. This is only + used for Python <=2.5. After that literal_eval is available. + """ + # simple types + try: + return int(obj) + except ValueError: + pass + try: + return float(obj) + except ValueError: + pass + # iterables + if obj.startswith('[') and obj.endswith(']'): + "A list. Traverse recursively." + return [rec_convert(val) for val in obj[1:-1].split(',')] + if obj.startswith('(') and obj.endswith(')'): + "A tuple. Traverse recursively." + return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) + if obj.startswith('{') and obj.endswith('}') and ':' in obj: + "A dict. Traverse recursively." + return dict([(rec_convert(pair.split(":", 1)[0]), + rec_convert(pair.split(":", 1)[1])) + for pair in obj[1:-1].split(',') if ":" in pair]) + # if nothing matches, return as-is + return obj + + if _LITERAL_EVAL: + # Use literal_eval to parse python structure exactly. + try: + return _LITERAL_EVAL(strobj) + except (SyntaxError, ValueError): + # treat as string + string = "{RNote: Value was converted to string. If you don't want this, " + string += "use proper Python syntax, like enclosing strings in quotes.{n" + cmd.caller.msg(string) + return utils.to_str(strobj) + else: + # fall back to old recursive solution (does not support + # nested lists/dicts) + return rec_convert(strobj.strip()) + class CmdSetAttribute(ObjManipCommand): """ set attribute on an object or player @@ -1202,73 +1270,6 @@ class CmdSetAttribute(ObjManipCommand): locks = "cmd:perm(set) or perm(Builders)" help_category = "Building" - def convert_from_string(self, strobj): - """ - Converts a single object in *string form* to its equivalent python - type. - - Python earlier than 2.6: - Handles floats, ints, and limited nested lists and dicts - (can't handle lists in a dict, for example, this is mainly due to - the complexity of parsing this rather than any technical difficulty - - if there is a need for @set-ing such complex structures on the - command line we might consider adding it). - Python 2.6 and later: - Supports all Python structures through literal_eval as long as they - are valid Python syntax. If they are not (such as [test, test2], ie - withtout the quotes around the strings), the entire structure will - be converted to a string and a warning will be given. - - We need to convert like this since all data being sent over the - telnet connection by the Player is text - but we will want to - store it as the "real" python type so we can do convenient - comparisons later (e.g. obj.db.value = 2, if value is stored as a - string this will always fail). - """ - - def rec_convert(obj): - """ - Helper function of recursive conversion calls. This is only - used for Python <=2.5. After that literal_eval is available. - """ - # simple types - try: - return int(obj) - except ValueError: - pass - try: - return float(obj) - except ValueError: - pass - # iterables - if obj.startswith('[') and obj.endswith(']'): - "A list. Traverse recursively." - return [rec_convert(val) for val in obj[1:-1].split(',')] - if obj.startswith('(') and obj.endswith(')'): - "A tuple. Traverse recursively." - return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) - if obj.startswith('{') and obj.endswith('}') and ':' in obj: - "A dict. Traverse recursively." - return dict([(rec_convert(pair.split(":", 1)[0]), - rec_convert(pair.split(":", 1)[1])) - for pair in obj[1:-1].split(',') if ":" in pair]) - # if nothing matches, return as-is - return obj - - if _LITERAL_EVAL: - # Use literal_eval to parse python structure exactly. - try: - return _LITERAL_EVAL(strobj) - except (SyntaxError, ValueError): - # treat as string - string = "{RNote: Value was converted to string. If you don't want this, " - string += "use proper Python syntax, like enclosing strings in quotes.{n" - self.caller.msg(string) - return utils.to_str(strobj) - else: - # fall back to old recursive solution (does not support - # nested lists/dicts) - return rec_convert(strobj.strip()) def func(self): "Implement the set attribute - a limited form of @py." @@ -1318,7 +1319,7 @@ class CmdSetAttribute(ObjManipCommand): # setting attribute(s). Make sure to convert to real Python type before saving. for attr in attrs: try: - obj.attributes.add(attr, self.convert_from_string(value)) + obj.attributes.add(attr, _convert_from_string(self, value)) string += "\nCreated attribute %s/%s = %s" % (obj.name, attr, value) except SyntaxError: # this means literal_eval tried to parse a faulty string @@ -2242,3 +2243,61 @@ class CmdTag(MuxCommand): else: string = "No tags attached to %s." % obj self.caller.msg(string) + +class CmdSpawn(MuxCommand): + """ + spawn objects from prototype + + Usage: + @spawn {prototype dictionary} + + Example: + @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} + + Dictionary keys: + {wkey {n - string, the main object identifier + {wtypeclass {n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + {wlocation {n - this should be a valid object or #dbref + {whome {n - valid object or #dbref + {wdestination{n - only valid for exits (object or dbref) + {wpermissions{n - string or list of permission strings + {wlocks {n - a lock-string + {waliases {n - string or list of strings + {wndb_{n - value of a nattribute (ndb_ is stripped) + any other keywords are interpreted as Attributes and their values. + + This command can't access prototype inheritance. + """ + + key = "@spawn" + locks = "cmd:perm(spawn) or perm(Builders)" + help_category = "Building" + + def func(self): + "Implements the spawn" + if not self.args: + self.caller.msg("Usage: @spawn {key:value, key, value, ...}") + return + from src.utils.spawner import spawn + + try: + # make use of _convert_from_string from the SetAttribute command + prototype = _convert_from_string(self, self.args) + except SyntaxError: + # this means literal_eval tried to parse a faulty string + string = "{RCritical Python syntax error in argument. " + string += "Only primitive Python structures are allowed. " + string += "\nYou also need to use correct Python syntax. " + string += "Remember especially to put quotes around all " + string += "strings inside lists and dicts.{n" + self.caller.msg(string) + return + + if not isinstance(prototype, dict): + self.caller.msg("The prototype must be a Python dictionary.") + return + + for obj in spawn(prototype): + self.caller.msg("Spawned %s." % obj.key) + + diff --git a/src/commands/default/cmdset_character.py b/src/commands/default/cmdset_character.py index 35292bdd8b..b7d533c139 100644 --- a/src/commands/default/cmdset_character.py +++ b/src/commands/default/cmdset_character.py @@ -80,6 +80,7 @@ class CharacterCmdSet(CmdSet): self.add(building.CmdScript()) self.add(building.CmdSetHome()) self.add(building.CmdTag()) + self.add(building.CmdSpawn()) # Batchprocessor commands self.add(batchprocess.CmdBatchCommands()) diff --git a/src/utils/spawner.py b/src/utils/spawner.py new file mode 100644 index 0000000000..ba3005e09a --- /dev/null +++ b/src/utils/spawner.py @@ -0,0 +1,229 @@ +""" +Spawner + +The spawner takes input files containing object definitions in +dictionary forms. These use a prototype architechture to define +unique objects without having to make a Typeclass for each. + +The main function is spawn(*prototype), where the prototype +is a dictionary like this: + +GOBLIN = { + "typeclass": "game.gamesrc.objects.objects.Monster", + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + +Possible keywords are: + prototype - dict, parent prototype of this structure (see below) + key - string, the main object identifier + typeclass - string, if not set, will use settings.BASE_OBJECT_TYPECLASS + location - this should be a valid object or #dbref + home - valid object or #dbref + destination - only valid for exits (object or dbref) + + permissions - string or list of permission strings + locks - a lock-string + aliases - string or list of strings + + ndb_ - value of a nattribute (ndb_ is stripped) + any other keywords are interpreted as Attributes and their values. + +Each value can also be a callable that takes no arguments. It should +return the value to enter into the field and will be called every time +the prototype is used to spawn an object. + +By specifying a prototype, the child will inherit all prototype slots +it does not explicitly define itself, while overloading those that it +does specify. + +GOBLIN_WIZARD = { + "prototype": GOBLIN, + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + +GOBLIN_ARCHER = { + "prototype": GOBLIN, + "key": "goblin archer", + "attacks": ["short bow"] +} + +One can also have multiple prototypes. These are inherited from the +left, with the ones further to the right taking precedence. + +ARCHWIZARD = { + "attack": ["archwizard staff", "eye of doom"] + +GOBLIN_ARCHWIZARD = { + "key" : "goblin archwizard" + "prototype": (GOBLIN_WIZARD, ARCHWIZARD), +} + +The goblin archwizard will have some different attacks, but will +otherwise have the same spells as a goblin wizard who in turn shares +many traits with a normal goblin. + +""" + +import os, sys +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))) +os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' + +from django.conf import settings +from random import randint +from src.objects.models import ObjectDB +from src.utils.create import handle_dbref + +_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") + +_handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) + + +def _get_prototype(dic, prot): + "Recursively traverse a prototype dictionary, including multiple inheritance" + if "prototype" in dic: + # move backwards through the inheritance + prototypes = dic["prototype"] + if isinstance(prototypes, dict): + prototypes = (prototypes,) + for prototype in prototypes: + # Build the prot dictionary in reverse order, overloading + new_prot = _get_prototype(prototype, prot) + prot.update(new_prot) + prot.update(dic) + prot.pop("prototype", None) # we don't need this anymore + return prot + + +def _batch_create_object(*objparams): + """ + This is a cut-down version of the create_object() function, + optimized for speed. It does NOT check and convert various input + so make sure the spawned Typeclass works before using this! + + Input: + objsparams - each argument should be a tuple of arguments for the respective + creation/add handlers in the following order: + (create, permissions, locks, aliases, nattributes, attributes) + Returns: + A list of created objects + """ + + # bulk create all objects in one go + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] + # unfortunately this doesn't work since bulk_create don't creates pks; + # the result are double objects at the next stage + #dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + + objs = [] + for iobj, dbobj in enumerate(dbobjs): + # call all setup hooks on each object + objparam = objparams[iobj] + obj = dbobj.typeclass # this saves dbobj if not done already + obj.basetype_setup() + obj.at_object_creation() + + if objparam[1]: + # permissions + obj.permissions.add(objparam[1]) + if objparam[2]: + # locks + obj.locks.add(objparam[2]) + if objparam[3]: + # aliases + obj.aliases.add(objparam[3]) + if objparam[4]: + # nattributes + for key, value in objparam[4].items(): + obj.nattributes.add(key, value) + if objparam[5]: + # attributes + keys, values = objparam[5].keys(), objparam[5].values() + obj.attributes.batch_add(keys, values) + + obj.basetype_posthook_setup() + objs.append(obj) + return objs + + +def spawn(*prototypes): + """ + Spawn a number of prototyped objects. Each argument should be a + prototype dictionary. + """ + objsparams = [] + + for prototype in prototypes: + + prot = _get_prototype(prototype, {}) + if not prot: + continue + + # extract the keyword args we need to create the object itself + create_kwargs = {} + create_kwargs["db_key"] = prot.pop("key", "Spawned Object %06i" % randint(1,100000)) + create_kwargs["db_location"] = _handle_dbref(prot.pop("location", None)) + create_kwargs["db_home"] = _handle_dbref(prot.pop("home", settings.DEFAULT_HOME)) + create_kwargs["db_destination"] = _handle_dbref(prot.pop("destination", None)) + create_kwargs["db_typeclass_path"] = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS) + + # extract calls to handlers + permission_string = prot.pop("permissions", "") + lock_string = prot.pop("locks", "") + alias_string = prot.pop("aliases", "") + + # extract ndb assignments + nattributes = dict((key.split("_", 1)[1], value if callable(value) else value) + for key, value in prot.items() if key.startswith("ndb_")) + + # the rest are attributes + attributes = dict((key, value() if callable(value) else value) + for key, value in prot.items() + if not (key in _CREATE_OBJECT_KWARGS or key in nattributes)) + + # pack for call into _batch_create_object + objsparams.append( (create_kwargs, permission_string, lock_string, + alias_string, nattributes, attributes) ) + + return _batch_create_object(*objsparams) + + +if __name__ == "__main__": + # testing + + NOBODY = {} + + GOBLIN = { + "key": "goblin grunt", + "health": lambda: randint(20,30), + "resists": ["cold", "poison"], + "attacks": ["fists"], + "weaknesses": ["fire", "light"] + } + + GOBLIN_WIZARD = { + "prototype": GOBLIN, + "key": "goblin wizard", + "spells": ["fire ball", "lighting bolt"] + } + + GOBLIN_ARCHER = { + "prototype": GOBLIN, + "key": "goblin archer", + "attacks": ["short bow"] + } + + ARCHWIZARD = { + "attacks": ["archwizard staff"], + } + + GOBLIN_ARCHWIZARD = { + "key": "goblin archwizard", + "prototype" : (GOBLIN_WIZARD, ARCHWIZARD) + } + # test + print [o.key for o in spawn(GOBLIN, GOBLIN_ARCHWIZARD)]