mirror of
https://github.com/evennia/evennia.git
synced 2026-04-04 23:17:17 +02:00
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:
parent
d05c92792c
commit
6eafe65076
3 changed files with 357 additions and 68 deletions
|
|
@ -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)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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
229
src/utils/spawner.py
Normal 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)]
|
||||
Loading…
Add table
Add a link
Reference in a new issue