Implemented src.utils.spawner along with a test command @spawn. This allows for spawning individualized objects based on a prototype dictionary rather than having to make a new Typeclass for small changes. Allows for setting basid properties as well as Attributes and NAttributes. Supports prototype multiple inheritance (see header of src/utils/spawner.py)

This commit is contained in:
Griatch 2014-07-01 02:14:48 +02:00
parent d05c92792c
commit 6eafe65076
3 changed files with 357 additions and 68 deletions

View file

@ -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<name> - 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)

View file

@ -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())

229
src/utils/spawner.py Normal file
View file

@ -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_<name> - 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)]