Merge branch 'olc' into develop

This commit is contained in:
Griatch 2018-08-12 13:37:44 +02:00
commit 4e34cac6c8
29 changed files with 6126 additions and 1057 deletions

View file

@ -1,7 +1,53 @@
# Evennia Changelog
# Changelog
# Sept 2017:
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
## Evennia 0.8 (2018)
### Prototype changes
- A new form of prototype - database-stored prototypes, editable from in-game. The old,
module-created prototypes remain as read-only prototypes.
- All prototypes must have a key `prototype_key` identifying the prototype in listings. This is
checked to be server-unique. Prototypes created in a module will use the global variable name they
are assigned to if no `prototype_key` is given.
- Prototype field `prototype` was renamed to `prototype_parent` to avoid mixing terms.
- All prototypes must either have `typeclass` or `prototype_parent` defined. If using
`prototype_parent`, `typeclass` must be defined somewhere in the inheritance chain. This is a
change from Evennia 0.7 which allowed 'mixin' prototypes without `typeclass`/`prototype_key`. To
make a mixin now, give it a default typeclass, like `evennia.objects.objects.DefaultObject` and just
override in the child as needed.
- The spawn command was extended to accept a full prototype on one line.
- The spawn command got the /save switch to save the defined prototype and its key.
- The command spawn/menu will now start an OLC (OnLine Creation) menu to load/save/edit/spawn prototypes.
### EvMenu
- Added `EvMenu.helptext_formatter(helptext)` to allow custom formatting of per-node help.
- Added `evennia.utils.evmenu.list_node` decorator for turning an EvMenu node into a multi-page listing.
- A `goto` option callable returning None (rather than the name of the next node) will now rerun the
current node instead of failing.
- Better error handling of in-node syntax errors.
- Improve dedent of default text/helptext formatter. Right-strip whitespace.
### Utils
- Added new `columnize` function for easily splitting text into multiple columns. At this point it
is not working too well with ansi-colored text however.
- Extend the `dedent` function with a new `baseline_index` kwarg. This allows to force all lines to
the indentation given by the given line regardless of if other lines were already a 0 indentation.
This removes a problem with the original `textwrap.dedent` which will only dedent to the least
indented part of a text.
- Added `exit_cmd` to EvMore pager, to allow for calling a command (e.g. 'look') when leaving the pager.
### Genaral
- Start structuring the `CHANGELOG` to list features in more detail.
# Overviews
## Sept 2017:
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
'Account', rework the website template and a slew of other updates.
Info on what changed and how to migrate is found here:
https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ
@ -14,9 +60,9 @@ Lots of bugfixes and considerable uptick in contributors. Unittest coverage
and PEP8 adoption and refactoring.
## May 2016:
Evennia 0.6 with completely reworked Out-of-band system, making
Evennia 0.6 with completely reworked Out-of-band system, making
the message path completely flexible and built around input/outputfuncs.
A completely new webclient, split into the evennia.js library and a
A completely new webclient, split into the evennia.js library and a
gui library, making it easier to customize.
## Feb 2016:
@ -33,15 +79,15 @@ library format with a stand-alone launcher, in preparation for making
an 'evennia' pypy package and using versioning. The version we will
merge with will likely be 0.5. There is also work with an expanded
testing structure and the use of threading for saves. We also now
use Travis for automatic build checking.
use Travis for automatic build checking.
## Sept 2014:
Updated to Django 1.7+ which means South dependency was dropped and
minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added
and the web customization system was overhauled using the latest
functionality of django. Otherwise, mostly bug-fixes and
and the web customization system was overhauled using the latest
functionality of django. Otherwise, mostly bug-fixes and
implementation of various smaller feature requests as we got used
to github. Many new users have appeared.
to github. Many new users have appeared.
## Jan 2014:
Moved Evennia project from Google Code to github.com/evennia/evennia.

View file

@ -174,7 +174,7 @@ def _init():
from .utils import logger
from .utils import gametime
from .utils import ansi
from .utils.spawner import spawn
from .prototypes.spawner import spawn
from . import contrib
from .utils.evmenu import EvMenu
from .utils.evtable import EvTable

View file

@ -633,10 +633,31 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# this will only be set if the utils.create_account
# function was used to create the object.
cdict = self._createdict
updates = []
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#%i" % self.dbid
updates.append("db_key")
elif self.key != cdict.get("key"):
updates.append("db_key")
self.db_key = cdict["key"]
if updates:
self.save(update_fields=updates)
if cdict.get("locks"):
self.locks.add(cdict["locks"])
if cdict.get("permissions"):
permissions = cdict["permissions"]
if cdict.get("tags"):
# this should be a list of tags, tuples (key, category) or (key, category, data)
self.tags.batch_add(*cdict["tags"])
if cdict.get("attributes"):
# this should be tuples (key, val, ...)
self.attributes.batch_add(*cdict["attributes"])
if cdict.get("nattributes"):
# this should be a dict of nattrname:value
for key, value in cdict["nattributes"]:
self.nattributes.add(key, value)
del self._createdict
self.permissions.batch_add(*permissions)

View file

@ -35,7 +35,6 @@ class AccountDBManager(TypedObjectManager, UserManager):
get_account_from_uid
get_account_from_name
account_search (equivalent to evennia.search_account)
#swap_character
"""

View file

@ -10,9 +10,10 @@ from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search
from evennia.utils.utils import inherits_from, class_from_module
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses
from evennia.utils.eveditor import EvEditor
from evennia.utils.spawner import spawn
from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -26,12 +27,8 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy",
"CmdLock", "CmdExamine", "CmdFind", "CmdTeleport",
"CmdScript", "CmdTag", "CmdSpawn")
try:
# used by @set
from ast import literal_eval as _LITERAL_EVAL
except ImportError:
# literal_eval is not available before Python 2.6
_LITERAL_EVAL = None
# used by @set
from ast import literal_eval as _LITERAL_EVAL
# used by @find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
@ -1458,17 +1455,16 @@ def _convert_from_string(cmd, strobj):
# 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
strobj = utils.to_str(strobj)
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
"Make sure this is acceptable." % strobj
cmd.caller.msg(string)
return strobj
# Use literal_eval to parse python structure exactly.
try:
return _LITERAL_EVAL(strobj)
except (SyntaxError, ValueError):
# treat as string
strobj = utils.to_str(strobj)
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
"Make sure this is acceptable." % strobj
cmd.caller.msg(string)
return strobj
else:
# fall back to old recursive solution (does not support
# nested lists/dicts)
@ -1702,17 +1698,22 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
@typeclass[/switch] <object> [= typeclass.path]
@type ''
@parent ''
@typeclass/list/show [typeclass.path]
@swap - this is a shorthand for using /force/reset flags.
@update - this is a shorthand for using the /force/reload flag.
Switch:
show - display the current typeclass of object (default)
show, examine - display the current typeclass of object (default) or, if
given a typeclass path, show the docstring of that typeclass.
update - *only* re-run at_object_creation on this object
meaning locks or other properties set later may remain.
reset - clean out *all* the attributes and properties on the
object - basically making this a new clean object.
force - change to the typeclass also if the object
already has a typeclass of the same name.
list - show available typeclasses.
Example:
@type button = examples.red_button.RedButton
@ -1736,6 +1737,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
key = "@typeclass"
aliases = ["@type", "@parent", "@swap", "@update"]
switch_options = ("show", "examine", "update", "reset", "force", "list")
locks = "cmd:perm(typeclass) or perm(Builder)"
help_category = "Building"
@ -1744,10 +1746,56 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
caller = self.caller
if 'list' in self.switches:
tclasses = get_all_typeclasses()
contribs = [key for key in sorted(tclasses)
if key.startswith("evennia.contrib")] or ["<None loaded>"]
core = [key for key in sorted(tclasses)
if key.startswith("evennia") and key not in contribs] or ["<None loaded>"]
game = [key for key in sorted(tclasses)
if not key.startswith("evennia")] or ["<None loaded>"]
string = ("|wCore typeclasses|n\n"
" {core}\n"
"|wLoaded Contrib typeclasses|n\n"
" {contrib}\n"
"|wGame-dir typeclasses|n\n"
" {game}").format(core="\n ".join(core),
contrib="\n ".join(contribs),
game="\n ".join(game))
EvMore(caller, string, exit_on_lastpage=True)
return
if not self.args:
caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
return
if "show" in self.switches or "examine" in self.switches:
oquery = self.lhs
obj = caller.search(oquery, quiet=True)
if not obj:
# no object found to examine, see if it's a typeclass-path instead
tclasses = get_all_typeclasses()
matches = [(key, tclass)
for key, tclass in tclasses.items() if key.endswith(oquery)]
nmatches = len(matches)
if nmatches > 1:
caller.msg("Multiple typeclasses found matching {}:\n {}".format(
oquery, "\n ".join(tup[0] for tup in matches)))
elif not matches:
caller.msg("No object or typeclass path found to match '{}'".format(oquery))
else:
# one match found
caller.msg("Docstring for typeclass '{}':\n{}".format(
oquery, matches[0][1].__doc__))
else:
# do the search again to get the error handling in case of multi-match
obj = caller.search(oquery)
if not obj:
return
caller.msg("{}'s current typeclass is '{}.{}'".format(
obj.name, obj.__class__.__module__, obj.__class__.__name__))
return
# get object to swap on
obj = caller.search(self.lhs)
if not obj:
@ -1760,7 +1808,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
new_typeclass = self.rhs or obj.path
if "show" in self.switches:
if "show" in self.switches or "examine" in self.switches:
string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
caller.msg(string)
return
@ -2179,12 +2227,15 @@ class CmdExamine(ObjManipCommand):
else:
things.append(content)
if exits:
string += "\n|wExits|n: %s" % ", ".join(["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
string += "\n|wExits|n: %s" % ", ".join(
["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
if pobjs:
string += "\n|wCharacters|n: %s" % ", ".join(["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
string += "\n|wCharacters|n: %s" % ", ".join(
["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
if things:
string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
if cont not in exits and cont not in pobjs])
string += "\n|wContents|n: %s" % ", ".join(
["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
if cont not in exits and cont not in pobjs])
separator = "-" * _DEFAULT_WIDTH
# output info
return '%s\n%s\n%s' % (separator, string.strip(), separator)
@ -2738,101 +2789,312 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
string = "No tags attached to %s." % obj
self.caller.msg(string)
#
# To use the prototypes with the @spawn function set
# PROTOTYPE_MODULES = ["commands.prototypes"]
# Reload the server and the prototypes should be available.
#
class CmdSpawn(COMMAND_DEFAULT_CLASS):
"""
spawn objects from prototype
Usage:
@spawn
@spawn[/switch] <prototype_name>
@spawn[/switch] {prototype dictionary}
@spawn[/noloc] <prototype_key>
@spawn[/noloc] <prototype_dict>
Switch:
@spawn/search [prototype_keykey][;tag[,tag]]
@spawn/list [tag, tag, ...]
@spawn/show [<prototype_key>]
@spawn/update <prototype_key>
@spawn/save <prototype_dict>
@spawn/edit [<prototype_key>]
@olc - equivalent to @spawn/edit
Switches:
noloc - allow location to be None if not specified explicitly. Otherwise,
location will default to caller's current location.
search - search prototype by name or tags.
list - list available prototypes, optionally limit by tags.
show, examine - inspect prototype by key. If not given, acts like list.
save - save a prototype to the database. It will be listable by /list.
delete - remove a prototype from database, if allowed to.
update - find existing objects with the same prototype_key and update
them with latest version of given prototype. If given with /save,
will auto-update all objects with the old version of the prototype
without asking first.
edit, olc - create/manipulate prototype in a menu interface.
Example:
@spawn GOBLIN
@spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
@spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
Dictionary keys:
|wprototype |n - name of parent prototype to use. Can be a list for
multiple inheritance (inherits left to right)
|wprototype_parent |n - name of parent prototype to use. Required if typeclass is
not set. Can be a path or a list for multiple inheritance (inherits
left to right). If set one of the parents must have a typeclass.
|wtypeclass |n - string. Required if prototype_parent is not set.
|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
|waliases |n - string or list of strings.
|wndb_|n<name> - value of a nattribute (ndb_ is stripped)
|wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db
and update existing prototyped objects if desired.
|wprototype_desc|n - desc of this prototype. Used in listings
|wprototype_locks|n - locks of this prototype. Limits who may use prototype
|wprototype_tags|n - tags of this prototype. Used to find prototype
any other keywords are interpreted as Attributes and their values.
The available prototypes are defined globally in modules set in
settings.PROTOTYPE_MODULES. If @spawn is used without arguments it
displays a list of available prototypes.
"""
key = "@spawn"
switch_options = ("noloc", )
aliases = ["olc"]
switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update")
locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building"
def func(self):
"""Implements the spawner"""
def _show_prototypes(prototypes):
"""Helper to show a list of available prototypes"""
prots = ", ".join(sorted(prototypes.keys()))
return "\nAvailable prototypes (case sensitive): %s" % (
"\n" + utils.fill(prots) if prots else "None")
def _parse_prototype(inp, expect=dict):
err = None
try:
prototype = _LITERAL_EVAL(inp)
except (SyntaxError, ValueError) as err:
# treat as string
prototype = utils.to_str(inp)
finally:
if not isinstance(prototype, expect):
if err:
string = ("{}\n|RCritical Python syntax error in argument. Only primitive "
"Python structures are allowed. \nYou also need to use correct "
"Python syntax. Remember especially to put quotes around all "
"strings inside lists and dicts.|n For more advanced uses, embed "
"inline functions in the strings.".format(err))
else:
string = "Expected {}, got {}.".format(expect, type(prototype))
self.caller.msg(string)
return None
if expect == dict:
# an actual prototype. We need to make sure it's safe. Don't allow exec
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg("Spawn aborted: You are not allowed to "
"use the 'exec' prototype key.")
return None
try:
protlib.validate_prototype(prototype)
except RuntimeError as err:
self.caller.msg(str(err))
return
return prototype
prototypes = spawn(return_prototypes=True)
if not self.args:
string = "Usage: @spawn {key:value, key, value, ... }"
self.caller.msg(string + _show_prototypes(prototypes))
return
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)
def _search_show_prototype(query, prototypes=None):
# prototype detail
if not prototypes:
prototypes = protlib.search_prototype(key=query)
if prototypes:
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
else:
return False
caller = self.caller
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches:
# OLC menu mode
prototype = None
if self.lhs:
key = self.lhs
prototype = spawner.search_prototype(key=key, return_meta=True)
if len(prototype) > 1:
caller.msg("More than one match for {}:\n{}".format(
key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
return
elif prototype:
# one match
prototype = prototype[0]
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
return
if isinstance(prototype, basestring):
# A prototype key
keystr = prototype
prototype = prototypes.get(prototype, None)
if 'search' in self.switches:
# query for a key match
if not self.args:
self.switches.append("list")
else:
key, tags = self.args.strip(), None
if ';' in self.args:
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
tags = [tag.strip() for tag in tags.split(",")] if tags else None
EvMore(caller, unicode(protlib.list_prototypes(caller, key=key, tags=tags)),
exit_on_lastpage=True)
return
if 'show' in self.switches or 'examine' in self.switches:
# the argument is a key in this case (may be a partial key)
if not self.args:
self.switches.append('list')
else:
matchstring = _search_show_prototype(self.args)
if matchstring:
caller.msg(matchstring)
else:
caller.msg("No prototype '{}' was found.".format(self.args))
return
if 'list' in self.switches:
# for list, all optional arguments are tags
# import pudb; pudb.set_trace()
EvMore(caller, unicode(protlib.list_prototypes(caller,
tags=self.lhslist)), exit_on_lastpage=True)
return
if 'save' in self.switches:
# store a prototype to the database store
if not self.args:
caller.msg(
"Usage: @spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>")
return
# handle rhs:
prototype = _parse_prototype(self.lhs.strip())
if not prototype:
string = "No prototype named '%s'." % keystr
self.caller.msg(string + _show_prototypes(prototypes))
return
elif isinstance(prototype, dict):
# we got the prototype on the command line. We must make sure to not allow
# the 'exec' key unless we are developers or higher.
if "exec" in prototype and not self.caller.check_permstring("Developer"):
self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.")
# present prototype to save
new_matchstring = _search_show_prototype("", prototypes=[prototype])
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
question = "\nDo you want to continue saving? [Y]/N"
prototype_key = prototype.get("prototype_key")
if not prototype_key:
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.")
return
else:
self.caller.msg("The prototype must be a prototype key or a Python dictionary.")
# check for existing prototype,
old_matchstring = _search_show_prototype(prototype_key)
if old_matchstring:
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
answer = yield(string + question)
if answer.lower() in ["n", "no"]:
caller.msg("|rSave cancelled.|n")
return
# all seems ok. Try to save.
try:
prot = protlib.save_prototype(**prototype)
if not prot:
caller.msg("|rError saving:|R {}.|n".format(prototype_key))
return
except protlib.PermissionError as err:
caller.msg("|rError saving:|R {}|n".format(err))
return
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
# check if we want to update existing objects
existing_objects = protlib.search_objects_with_prototype(prototype_key)
if existing_objects:
if 'update' not in self.switches:
n_existing = len(existing_objects)
slow = " (note that this may be slow)" if n_existing > 10 else ""
string = ("There are {} objects already created with an older version "
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
n_existing, prototype_key, slow))
answer = yield(string)
if answer.lower() in ["n", "no"]:
caller.msg("|rNo update was done of existing objects. "
"Use @spawn/update <key> to apply later as needed.|n")
return
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
caller.msg("{} objects were updated.".format(n_updated))
return
if not self.args:
ncount = len(protlib.search_prototype())
caller.msg("Usage: @spawn <prototype-key> or {{key: value, ...}}"
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
return
if 'delete' in self.switches:
# remove db-based prototype
matchstring = _search_show_prototype(self.args)
if matchstring:
string = "|rDeleting prototype:|n\n{}".format(matchstring)
question = "\nDo you want to continue deleting? [Y]/N"
answer = yield(string + question)
if answer.lower() in ["n", "no"]:
caller.msg("|rDeletion cancelled.|n")
return
try:
success = protlib.delete_db_prototype(caller, self.args)
except protlib.PermissionError as err:
caller.msg("|rError deleting:|R {}|n".format(err))
caller.msg("Deletion {}.".format(
'successful' if success else 'failed (does the prototype exist?)'))
return
else:
caller.msg("Could not find prototype '{}'".format(key))
if 'update' in self.switches:
# update existing prototypes
key = self.args.strip().lower()
existing_objects = protlib.search_objects_with_prototype(key)
if existing_objects:
n_existing = len(existing_objects)
slow = " (note that this may be slow)" if n_existing > 10 else ""
string = ("There are {} objects already created with an older version "
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
n_existing, key, slow))
answer = yield(string)
if answer.lower() in ["n", "no"]:
caller.msg("|rUpdate cancelled.")
return
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
caller.msg("{} objects were updated.".format(n_updated))
# A direct creation of an object from a given prototype
prototype = _parse_prototype(
self.args, expect=dict if self.args.strip().startswith("{") else basestring)
if not prototype:
# this will only let through dicts or strings
return
key = '<unnamed>'
if isinstance(prototype, basestring):
# A prototype key we are looking to apply
key = prototype
prototypes = protlib.search_prototype(prototype)
nprots = len(prototypes)
if not prototypes:
caller.msg("No prototype named '%s'." % prototype)
return
elif nprots > 1:
caller.msg("Found {} prototypes matching '{}':\n {}".format(
nprots, prototype, ", ".join(prot.get('prototype_key', '')
for proto in prototypes)))
return
# we have a prototype, check access
prototype = prototypes[0]
if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'):
caller.msg("You don't have access to use this prototype.")
return
if "noloc" not in self.switches and "location" not in prototype:
prototype["location"] = self.caller.location
for obj in spawn(prototype):
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
# proceed to spawning
try:
for obj in spawner.spawn(prototype):
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
except RuntimeError as err:
caller.msg(err)

View file

@ -28,6 +28,7 @@ from evennia.utils import ansi, utils, gametime
from evennia.server.sessionhandler import SESSIONS
from evennia import search_object
from evennia import DefaultObject, DefaultCharacter
from evennia.prototypes import prototypes as protlib
# set up signal here since we are not starting the server
@ -45,7 +46,7 @@ class CommandTest(EvenniaTest):
Tests a command
"""
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
receiver=None, cmdstring=None, obj=None):
receiver=None, cmdstring=None, obj=None, inputs=None):
"""
Test a command by assigning all the needed
properties to cmdobj and running
@ -74,14 +75,31 @@ class CommandTest(EvenniaTest):
cmdobj.obj = obj or (caller if caller else self.char1)
# test
old_msg = receiver.msg
inputs = inputs or []
try:
receiver.msg = Mock()
if cmdobj.at_pre_cmd():
return
cmdobj.parse()
ret = cmdobj.func()
# handle func's with yield in them (generators)
if isinstance(ret, types.GeneratorType):
ret.next()
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
ret.send(inp)
except TypeError:
ret.next()
ret = ret.send(inp)
else:
ret.next()
except StopIteration:
break
cmdobj.at_post_cmd()
except StopIteration:
pass
@ -362,6 +380,7 @@ class TestBuilding(CommandTest):
# check that it exists in the process.
query = search_object(objKeyStr)
commandTest.assertIsNotNone(query)
commandTest.assertTrue(bool(query))
obj = query[0]
commandTest.assertIsNotNone(obj)
return obj
@ -370,17 +389,20 @@ class TestBuilding(CommandTest):
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
# Tests "@spawn <prototype_dictionary>" without specifying location.
self.call(building.CmdSpawn(),
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin")
goblin = getObject(self, "goblin")
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"Saved prototype: testprot", inputs=['y'])
# Tests that the spawned object's type is a DefaultCharacter.
self.assertIsInstance(goblin, DefaultCharacter)
self.call(building.CmdSpawn(), "/list", "Key ")
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
# Tests that the spawned object's location is the same as the caharacter's location, since
# we did not specify it.
self.assertEqual(goblin.location, self.char1.location)
goblin.delete()
testchar = getObject(self, "Test Char")
self.assertEqual(testchar.location, self.char1.location)
testchar.delete()
# Test "@spawn <prototype_dictionary>" with a location other than the character's.
spawnLoc = self.room2
@ -390,14 +412,23 @@ class TestBuilding(CommandTest):
spawnLoc = self.room1
self.call(building.CmdSpawn(),
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}"
% spawnLoc.dbref, "Spawned goblin")
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin")
goblin = getObject(self, "goblin")
# Tests that the spawned object's type is a DefaultCharacter.
self.assertIsInstance(goblin, DefaultCharacter)
self.assertEqual(goblin.location, spawnLoc)
goblin.delete()
# create prototype
protlib.create_prototype(**{'key': 'Ball',
'typeclass': 'evennia.objects.objects.DefaultCharacter',
'prototype_key': 'testball'})
# Tests "@spawn <prototype_name>"
self.call(building.CmdSpawn(), "'BALL'", "Spawned Ball")
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
ball = getObject(self, "Ball")
self.assertEqual(ball.location, self.char1.location)
self.assertIsInstance(ball, DefaultObject)
@ -410,10 +441,14 @@ class TestBuilding(CommandTest):
self.assertIsNone(ball.location)
ball.delete()
self.call(building.CmdSpawn(),
"/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
% spawnLoc.dbref, "Error: Prototype testball tries to parent itself.")
# Tests "@spawn/noloc ...", but DO specify a location.
# Location should be the specified location.
self.call(building.CmdSpawn(),
"/noloc {'prototype':'BALL', 'location':'%s'}"
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
% spawnLoc.dbref, "Spawned Ball")
ball = getObject(self, "Ball")
self.assertEqual(ball.location, spawnLoc)
@ -422,6 +457,9 @@ class TestBuilding(CommandTest):
# test calling spawn with an invalid prototype.
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
# Test listing commands
self.call(building.CmdSpawn(), "/list", "Key ")
class TestComms(CommandTest):

View file

@ -24,7 +24,7 @@ import random
from evennia import DefaultObject, DefaultExit, Command, CmdSet
from evennia.utils import search, delay
from evennia.utils.spawner import spawn
from evennia.prototypes.spawner import spawn
# -------------------------------------------------------------
#
@ -905,19 +905,19 @@ WEAPON_PROTOTYPES = {
"magic": False,
"desc": "A generic blade."},
"knife": {
"prototype": "weapon",
"prototype_parent": "weapon",
"aliases": "sword",
"key": "Kitchen knife",
"desc": "A rusty kitchen knife. Better than nothing.",
"damage": 3},
"dagger": {
"prototype": "knife",
"prototype_parent": "knife",
"key": "Rusty dagger",
"aliases": ["knife", "dagger"],
"desc": "A double-edged dagger with a nicked edge and a wooden handle.",
"hit": 0.25},
"sword": {
"prototype": "weapon",
"prototype_parent": "weapon",
"key": "Rusty sword",
"aliases": ["sword"],
"desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.",
@ -925,28 +925,28 @@ WEAPON_PROTOTYPES = {
"damage": 5,
"parry": 0.5},
"club": {
"prototype": "weapon",
"prototype_parent": "weapon",
"key": "Club",
"desc": "A heavy wooden club, little more than a heavy branch.",
"hit": 0.4,
"damage": 6,
"parry": 0.2},
"axe": {
"prototype": "weapon",
"prototype_parent": "weapon",
"key": "Axe",
"desc": "A woodcutter's axe with a keen edge.",
"hit": 0.4,
"damage": 6,
"parry": 0.2},
"ornate longsword": {
"prototype": "sword",
"prototype_parent": "sword",
"key": "Ornate longsword",
"desc": "A fine longsword with some swirling patterns on the handle.",
"hit": 0.5,
"magic": True,
"damage": 5},
"warhammer": {
"prototype": "club",
"prototype_parent": "club",
"key": "Silver Warhammer",
"aliases": ["hammer", "warhammer", "war"],
"desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.",
@ -954,21 +954,21 @@ WEAPON_PROTOTYPES = {
"magic": True,
"damage": 8},
"rune axe": {
"prototype": "axe",
"prototype_parent": "axe",
"key": "Runeaxe",
"aliases": ["axe"],
"hit": 0.4,
"magic": True,
"damage": 6},
"thruning": {
"prototype": "ornate longsword",
"prototype_parent": "ornate longsword",
"key": "Broadsword named Thruning",
"desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.",
"hit": 0.6,
"parry": 0.6,
"damage": 7},
"slayer waraxe": {
"prototype": "rune axe",
"prototype_parent": "rune axe",
"key": "Slayer waraxe",
"aliases": ["waraxe", "war", "slayer"],
"desc": "A huge double-bladed axe marked with the runes for 'Slayer'."
@ -976,7 +976,7 @@ WEAPON_PROTOTYPES = {
"hit": 0.7,
"damage": 8},
"ghostblade": {
"prototype": "ornate longsword",
"prototype_parent": "ornate longsword",
"key": "The Ghostblade",
"aliases": ["blade", "ghost"],
"desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing."
@ -985,7 +985,7 @@ WEAPON_PROTOTYPES = {
"parry": 0.8,
"damage": 10},
"hawkblade": {
"prototype": "ghostblade",
"prototype_parent": "ghostblade",
"key": "The Hawkblade",
"aliases": ["hawk", "blade"],
"desc": "The weapon of a long-dead heroine and a more civilized age,"

View file

@ -65,7 +65,6 @@ class Account(DefaultAccount):
* Helper methods
msg(text=None, **kwargs)
swap_character(new_character, delete_old_character=False)
execute_cmd(raw_string, session=None)
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False)
is_typeclass(typeclass, exact=False)

View file

@ -287,7 +287,7 @@ class LockHandler(object):
"""
self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser
def add(self, lockstring):
def add(self, lockstring, validate_only=False):
"""
Add a new lockstring to handler.
@ -296,10 +296,12 @@ class LockHandler(object):
`"<access_type>:<functions>"`. Multiple access types
should be separated by semicolon (`;`). Alternatively,
a list with lockstrings.
validate_only (bool, optional): If True, validate the lockstring but
don't actually store it.
Returns:
success (bool): The outcome of the addition, `False` on
error.
error. If `validate_only` is True, this will be a tuple
(bool, error), for pass/fail and a string error.
"""
if isinstance(lockstring, basestring):
@ -308,21 +310,41 @@ class LockHandler(object):
lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")]
lockstring = ";".join(lockdefs)
err = ""
# sanity checks
for lockdef in lockdefs:
if ':' not in lockdef:
self._log_error(_("Lock: '%s' contains no colon (:).") % lockdef)
return False
err = _("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False
access_type, rhs = [part.strip() for part in lockdef.split(':', 1)]
if not access_type:
self._log_error(_("Lock: '%s' has no access_type (left-side of colon is empty).") % lockdef)
return False
err = _("Lock: '{lockdef}' has no access_type "
"(left-side of colon is empty).").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False
if rhs.count('(') != rhs.count(')'):
self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef)
return False
err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False
if not _RE_FUNCS.findall(rhs):
self._log_error(_("Lock: '%s' has no valid lock functions.") % lockdef)
return False
err = _("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False
if validate_only:
return True, None
# get the lock string
storage_lockstring = self.obj.lock_storage
if storage_lockstring:
@ -334,6 +356,18 @@ class LockHandler(object):
self._save_locks()
return True
def validate(self, lockstring):
"""
Validate lockstring syntactically, without saving it.
Args:
lockstring (str): Lockstring to validate.
Returns:
valid (bool): If validation passed or not.
"""
return self.add(lockstring, validate_only=True)
def replace(self, lockstring):
"""
Replaces the lockstring entirely.
@ -421,6 +455,28 @@ class LockHandler(object):
self._cache_locks(self.obj.lock_storage)
self.cache_lock_bypass(self.obj)
def append(self, access_type, lockstring, op='or'):
"""
Append a lock definition to access_type if it doesn't already exist.
Args:
access_type (str): Access type.
lockstring (str): A valid lockstring, without the operator to
link it to an eventual existing lockstring.
op (str): An operator 'and', 'or', 'and not', 'or not' used
for appending the lockstring to an existing access-type.
Note:
The most common use of this method is for use in commands where
the user can specify their own lockstrings. This method allows
the system to auto-add things like Admin-override access.
"""
old_lockstring = self.get(access_type)
if not lockstring.strip().lower() in old_lockstring.lower():
lockstring = "{old} {op} {new}".format(
old=old_lockstring, op=op, new=lockstring.strip())
self.add(lockstring)
def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False):
"""
Checks a lock of the correct type by passing execution off to
@ -459,9 +515,13 @@ class LockHandler(object):
return True
except AttributeError:
# happens before session is initiated.
if not no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
if not no_superuser_bypass and (
(hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'account') and
hasattr(accessing_obj.account, 'is_superuser') and
accessing_obj.account.is_superuser) or
(hasattr(accessing_obj, 'get_account') and
(not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
return True
# no superuser or bypass -> normal lock operation
@ -469,7 +529,8 @@ class LockHandler(object):
# we have a lock, test it.
evalstring, func_tup, raw_string = self.locks[access_type]
# execute all lock funcs in the correct order, producing a tuple of True/False results.
true_false = tuple(bool(tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
true_false = tuple(bool(
tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
# the True/False tuple goes into evalstring, which combines them
# with AND/OR/NOT in order to get the final result.
return eval(evalstring % true_false)
@ -520,9 +581,13 @@ class LockHandler(object):
if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
return True
except AttributeError:
if no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
if no_superuser_bypass and (
(hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'account') and
hasattr(accessing_obj.account, 'is_superuser') and
accessing_obj.account.is_superuser) or
(hasattr(accessing_obj, 'get_account') and
(not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
return True
if ":" not in lockstring:
lockstring = "%s:%s" % ("_dummy", lockstring)
@ -538,7 +603,8 @@ class LockHandler(object):
else:
# if no access types was given and multiple locks were
# embedded in the lockstring we assume all must be true
return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks)
return all(self._eval_access_type(
accessing_obj, locks, access_type) for access_type in locks)
# convenience access function
@ -581,6 +647,35 @@ def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False,
default=default, access_type=access_type)
def validate_lockstring(lockstring):
"""
Validate so lockstring is on a valid form.
Args:
lockstring (str): Lockstring to validate.
Returns:
is_valid (bool): If the lockstring is valid or not.
error (str or None): A string describing the error, or None
if no error was found.
"""
return _LOCK_HANDLER.validate(lockstring)
def get_all_lockfuncs():
"""
Get a dict of available lock funcs.
Returns:
lockfuncs (dict): Mapping {lockfuncname:func}.
"""
if not _LOCKFUNCS:
_cache_lockfuncs()
return _LOCKFUNCS
def _test():
# testing

View file

@ -1003,14 +1003,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
cdict["location"].at_object_receive(self, None)
self.at_after_move(None)
if cdict.get("tags"):
# this should be a list of tags
# this should be a list of tags, tuples (key, category) or (key, category, data)
self.tags.batch_add(*cdict["tags"])
if cdict.get("attributes"):
# this should be a dict of attrname:value
# this should be tuples (key, val, ...)
self.attributes.batch_add(*cdict["attributes"])
if cdict.get("nattributes"):
# this should be a dict of nattrname:value
for key, value in cdict["nattributes"].items():
for key, value in cdict["nattributes"]:
self.nattributes.add(key, value)
del self._createdict
@ -1753,6 +1753,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
else:
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
msg_location = msg_location or '{object} says, "{speech}"'
msg_receivers = msg_receivers or message
custom_mapping = kwargs.get('mapping', {})
receivers = make_iter(receivers) if receivers else None

View file

@ -0,0 +1,145 @@
# Prototypes
A 'Prototype' is a normal Python dictionary describing unique features of individual instance of a
Typeclass. The prototype is used to 'spawn' a new instance with custom features detailed by said
prototype. This allows for creating variations without having to create a large number of actual
Typeclasses. It is a good way to allow Builders more freedom of creation without giving them full
Python access to create Typeclasses.
For example, if a Typeclass 'Cat' describes all the coded differences between a Cat and
other types of animals, then prototypes could be used to quickly create unique individual cats with
different Attributes/properties (like different colors, stats, names etc) without having to make a new
Typeclass for each. Prototypes have inheritance and can be scripted when they are applied to create
a new instance of a typeclass - a common example would be to randomize stats and name.
The prototype is a normal dictionary with specific keys. Almost all values can be callables
triggered when the prototype is used to spawn a new instance. Below is an example:
```
{
# meta-keys - these are used only when listing prototypes in-game. Only prototype_key is mandatory,
# but it must be globally unique.
"prototype_key": "base_goblin",
"prototype_desc": "A basic goblin",
"prototype_locks": "edit:all();spawn:all()",
"prototype_tags": "mobs",
# fixed-meaning keys, modifying the spawned instance. 'typeclass' may be
# replaced by 'parent', referring to the prototype_key of an existing prototype
# to inherit from.
"typeclass": "types.objects.Monster",
"key": "goblin grunt",
"tags": ["mob", "evil", ('greenskin','mob')] # tags as well as tags with category etc
"attrs": [("weapon", "sword")] # this allows to set Attributes with categories etc
# non-fixed keys are interpreted as Attributes and their
"health": lambda: randint(20,30),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
}
```
## Using prototypes
Prototypes are generally used as inputs to the `spawn` command:
@spawn prototype_key
This will spawn a new instance of the prototype in the caller's current location unless the
`location` key of the prototype was set (see below). The caller must pass the prototype's 'spawn'
lock to be able to use it.
@spawn/list [prototype_key]
will show all available prototypes along with meta info, or look at a specific prototype in detail.
## Creating prototypes
The `spawn` command can also be used to directly create/update prototypes from in-game.
spawn/save {"prototype_key: "goblin", ... }
but it is probably more convenient to use the menu-driven prototype wizard:
spawn/menu goblin
In code:
```python
from evennia import prototypes
goblin = {"prototype_key": "goblin:, ... }
prototype = prototypes.save_prototype(caller, **goblin)
```
Prototypes will normally be stored in the database (internally this is done using a Script, holding
the meta-info and the prototype). One can also define prototypes outside of the game by assigning
the prototype dictionary to a global variable in a module defined by `settings.PROTOTYPE_MODULES`:
```python
# in e.g. mygame/world/prototypes.py
GOBLIN = {
"prototype_key": "goblin",
...
}
```
Such prototypes cannot be modified from inside the game no matter what `edit` lock they are given
(we refer to them as 'readonly') but can be a fast and efficient way to give builders a starting
library of prototypes to inherit from.
## Valid Prototype keys
Every prototype key also accepts a callable (taking no arguments) for producing its value or a
string with an $protfunc definition. That callable/protfunc must then return a value on a form the
prototype key expects.
- `prototype_key` (str): name of this prototype. This is used when storing prototypes and should
be unique. This should always be defined but for prototypes defined in modules, the
variable holding the prototype dict will become the prototype_key if it's not explicitly
given.
- `prototype_desc` (str, optional): describes prototype in listings
- `prototype_locks` (str, optional): locks for restricting access to this prototype. Locktypes
supported are 'edit' and 'use'.
- `prototype_tags` (list, optional): List of tags or tuples (tag, category) used to group prototype
in listings
- `parent` (str or tuple, optional): name (`prototype_key`) of eventual parent prototype, or a
list of parents for multiple left-to-right inheritance.
- `prototype`: Deprecated. Same meaning as 'parent'.
- `typeclass` (str, optional): if not set, will use typeclass of parent prototype or use
`settings.BASE_OBJECT_TYPECLASS`
- `key` (str, optional): the name of the spawned object. If not given this will set to a
random hash
- `location` (obj, optional): location of the object - a valid object or #dbref
- `home` (obj or str, optional): valid object or #dbref
- `destination` (obj or str, optional): only valid for exits (object or #dbref)
- `permissions` (str or list, optional): which permissions for spawned object to have
- `locks` (str, optional): lock-string for the spawned object
- `aliases` (str or list, optional): Aliases for the spawned object.
- `exec` (str, optional): this is a string of python code to execute or a list of such
codes. This can be used e.g. to trigger custom handlers on the object. The execution
namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
this functionality to Developer/superusers. Usually it's better to use callables or
prototypefuncs instead of this.
- `tags` (str, tuple or list, optional): string or list of strings or tuples
`(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
- `attrs` (tuple or list, optional): tuple or list of tuples of Attributes to add. This
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
lockstring but not a category, set the category to `None`.
- `ndb_<name>` (any): value of a nattribute (`ndb_` is stripped). This is usually not useful to
put in a prototype unless the NAttribute is used immediately upon spawning.
- `other` (any): any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories.

View file

2399
evennia/prototypes/menus.py Normal file

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,317 @@
"""
Protfuncs are function-strings embedded in a prototype and allows for a builder to create a
prototype with custom logics without having access to Python. The Protfunc is parsed using the
inlinefunc parser but is fired at the moment the spawning happens, using the creating object's
session as input.
In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.:
{ ...
"key": "$funcname(arg1, arg2, ...)"
... }
and multiple functions can be nested (no keyword args are supported). The result will be used as the
value for that prototype key for that individual spawn.
Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They
are specified as functions
def funcname (*args, **kwargs)
where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia:
- session (Session): The Session of the entity spawning using this prototype.
- prototype (dict): The dict this protfunc is a part of.
- current_key (str): The active key this value belongs to in the prototype.
- testing (bool): This is set if this function is called as part of the prototype validation; if
set, the protfunc should take care not to perform any persistent actions, such as operate on
objects or add things to the database.
Any traceback raised by this function will be handled at the time of spawning and abort the spawn
before any object is created/updated. It must otherwise return the value to store for the specified
prototype key (this value must be possible to serialize in an Attribute).
"""
from ast import literal_eval
from random import randint as base_randint, random as base_random
from evennia.utils import search
from evennia.utils.utils import justify as base_justify, is_iter, to_str
_PROTLIB = None
# default protfuncs
def random(*args, **kwargs):
"""
Usage: $random()
Returns a random value in the interval [0, 1)
"""
return base_random()
def randint(*args, **kwargs):
"""
Usage: $randint(start, end)
Returns random integer in interval [start, end]
"""
if len(args) != 2:
raise TypeError("$randint needs two arguments - start and end.")
start, end = int(args[0]), int(args[1])
return base_randint(start, end)
def left_justify(*args, **kwargs):
"""
Usage: $left_justify(<text>)
Returns <text> left-justified.
"""
if args:
return base_justify(args[0], align='l')
return ""
def right_justify(*args, **kwargs):
"""
Usage: $right_justify(<text>)
Returns <text> right-justified across screen width.
"""
if args:
return base_justify(args[0], align='r')
return ""
def center_justify(*args, **kwargs):
"""
Usage: $center_justify(<text>)
Returns <text> centered in screen width.
"""
if args:
return base_justify(args[0], align='c')
return ""
def full_justify(*args, **kwargs):
"""
Usage: $full_justify(<text>)
Returns <text> filling up screen width by adding extra space.
"""
if args:
return base_justify(args[0], align='f')
return ""
def protkey(*args, **kwargs):
"""
Usage: $protkey(<key>)
Returns the value of another key in this prototoype. Will raise an error if
the key is not found in this prototype.
"""
if args:
prototype = kwargs['prototype']
return prototype[args[0].strip()]
def add(*args, **kwargs):
"""
Usage: $add(val1, val2)
Returns the result of val1 + val2. Values must be
valid simple Python structures possible to add,
such as numbers, lists etc.
"""
if len(args) > 1:
val1, val2 = args[0], args[1]
# try to convert to python structures, otherwise, keep as strings
try:
val1 = literal_eval(val1.strip())
except Exception:
pass
try:
val2 = literal_eval(val2.strip())
except Exception:
pass
return val1 + val2
raise ValueError("$add requires two arguments.")
def sub(*args, **kwargs):
"""
Usage: $del(val1, val2)
Returns the value of val1 - val2. Values must be
valid simple Python structures possible to
subtract.
"""
if len(args) > 1:
val1, val2 = args[0], args[1]
# try to convert to python structures, otherwise, keep as strings
try:
val1 = literal_eval(val1.strip())
except Exception:
pass
try:
val2 = literal_eval(val2.strip())
except Exception:
pass
return val1 - val2
raise ValueError("$sub requires two arguments.")
def mult(*args, **kwargs):
"""
Usage: $mul(val1, val2)
Returns the value of val1 * val2. The values must be
valid simple Python structures possible to
multiply, like strings and/or numbers.
"""
if len(args) > 1:
val1, val2 = args[0], args[1]
# try to convert to python structures, otherwise, keep as strings
try:
val1 = literal_eval(val1.strip())
except Exception:
pass
try:
val2 = literal_eval(val2.strip())
except Exception:
pass
return val1 * val2
raise ValueError("$mul requires two arguments.")
def div(*args, **kwargs):
"""
Usage: $div(val1, val2)
Returns the value of val1 / val2. Values must be numbers and
the result is always a float.
"""
if len(args) > 1:
val1, val2 = args[0], args[1]
# try to convert to python structures, otherwise, keep as strings
try:
val1 = literal_eval(val1.strip())
except Exception:
pass
try:
val2 = literal_eval(val2.strip())
except Exception:
pass
return val1 / float(val2)
raise ValueError("$mult requires two arguments.")
def toint(*args, **kwargs):
"""
Usage: $toint(<number>)
Returns <number> as an integer.
"""
if args:
val = args[0]
try:
return int(literal_eval(val.strip()))
except ValueError:
return val
raise ValueError("$toint requires one argument.")
def eval(*args, **kwargs):
"""
Usage $eval(<expression>)
Returns evaluation of a simple Python expression. The string may *only* consist of the following
Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..)
- those will then be evaluated *after* $eval.
"""
global _PROTLIB
if not _PROTLIB:
from evennia.prototypes import prototypes as _PROTLIB
string = ",".join(args)
struct = literal_eval(string)
if isinstance(struct, basestring):
# we must shield the string, otherwise it will be merged as a string and future
# literal_evas will pick up e.g. '2' as something that should be converted to a number
struct = '"{}"'.format(struct)
# convert any #dbrefs to objects (also in nested structures)
struct = _PROTLIB.value_to_obj_or_any(struct)
return struct
def _obj_search(*args, **kwargs):
"Helper function to search for an object"
query = "".join(args)
session = kwargs.get("session", None)
return_list = kwargs.pop("return_list", False)
account = None
if session:
account = session.account
targets = search.search_object(query)
if return_list:
retlist = []
if account:
for target in targets:
if target.access(account, target, 'control'):
retlist.append(target)
else:
retlist = targets
return retlist
else:
# single-match
if not targets:
raise ValueError("$obj: Query '{}' gave no matches.".format(query))
if len(targets) > 1:
raise ValueError("$obj: Query '{query}' gave {nmatches} matches. Limit your "
"query or use $objlist instead.".format(
query=query, nmatches=len(targets)))
target = targets[0]
if account:
if not target.access(account, target, 'control'):
raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - "
"Account {account} does not have 'control' access.".format(
target=target.key, dbref=target.id, account=account))
return target
def obj(*args, **kwargs):
"""
Usage $obj(<query>)
Returns one Object searched globally by key, alias or #dbref. Error if more than one.
"""
obj = _obj_search(return_list=False, *args, **kwargs)
if obj:
return "#{}".format(obj.id)
return "".join(args)
def objlist(*args, **kwargs):
"""
Usage $objlist(<query>)
Returns list with one or more Objects searched globally by key, alias or #dbref.
"""
return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)]

View file

@ -0,0 +1,695 @@
"""
Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
(Read-only prototypes).
"""
import re
from ast import literal_eval
from django.conf import settings
from evennia.scripts.scripts import DefaultScript
from evennia.objects.models import ObjectDB
from evennia.utils.create import create_script
from evennia.utils.utils import (
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
get_all_typeclasses, to_str, dbref, justify)
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
from evennia.utils import logger
from evennia.utils import inlinefuncs
from evennia.utils.evtable import EvTable
_MODULE_PROTOTYPE_MODULES = {}
_MODULE_PROTOTYPES = {}
_PROTOTYPE_META_NAMES = (
"prototype_key", "prototype_desc", "prototype_tags", "prototype_locks", "prototype_parent")
_PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
"key", "aliases", "typeclass", "location", "home", "destination",
"permissions", "locks", "exec", "tags", "attrs")
_PROTOTYPE_TAG_CATEGORY = "from_prototype"
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
PROT_FUNCS = {}
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
class PermissionError(RuntimeError):
pass
class ValidationError(RuntimeError):
"""
Raised on prototype validation errors
"""
pass
# Protfunc parsing
for mod in settings.PROT_FUNC_MODULES:
try:
callables = callables_from_module(mod)
PROT_FUNCS.update(callables)
except ImportError:
logger.log_trace()
raise
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs):
"""
Parse a prototype value string for a protfunc and process it.
Available protfuncs are specified as callables in one of the modules of
`settings.PROTFUNC_MODULES`, or specified on the command line.
Args:
value (any): The value to test for a parseable protfunc. Only strings will be parsed for
protfuncs, all other types are returned as-is.
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
If not set, use default sources.
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
behave differently.
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
Kwargs:
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
any (any): Passed on to the protfunc.
Returns:
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
either None or a string detailing the error from protfunc_parser or seen when trying to
run `literal_eval` on the parsed string.
any (any): A structure to replace the string on the prototype level. If this is a
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
it to the prototype directly. This structure is also passed through literal_eval so one
can get actual Python primitives out of it (not just strings). It will also identify
eventual object #dbrefs in the output from the protfunc.
"""
if not isinstance(value, basestring):
try:
value = value.dbref
except AttributeError:
pass
value = to_str(value, force_string=True)
available_functions = PROT_FUNCS if available_functions is None else available_functions
# insert $obj(#dbref) for #dbref
value = _RE_DBREF.sub("$obj(\\1)", value)
result = inlinefuncs.parse_inlinefunc(
value, available_funcs=available_functions,
stacktrace=stacktrace, testing=testing, **kwargs)
err = None
try:
result = literal_eval(result)
except ValueError:
pass
except Exception as err:
err = str(err)
if testing:
return err, result
return result
def format_available_protfuncs():
"""
Get all protfuncs in a pretty-formatted form.
Args:
clr (str, optional): What coloration tag to use.
"""
out = []
for protfunc_name, protfunc in PROT_FUNCS.items():
out.append("- |c${name}|n - |W{docs}".format(
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
return justify("\n".join(out), indent=8)
# helper functions
def value_to_obj(value, force=True):
"Always convert value(s) to Object, or None"
stype = type(value)
if is_iter(value):
if stype == dict:
return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()}
else:
return stype([value_to_obj_or_any(val) for val in value])
return dbid_to_obj(value, ObjectDB)
def value_to_obj_or_any(value):
"Convert value(s) to Object if possible, otherwise keep original value"
stype = type(value)
if is_iter(value):
if stype == dict:
return {value_to_obj_or_any(key):
value_to_obj_or_any(val) for key, val in value.items()}
else:
return stype([value_to_obj_or_any(val) for val in value])
obj = dbid_to_obj(value, ObjectDB)
return obj if obj is not None else value
def prototype_to_str(prototype):
"""
Format a prototype to a nice string representation.
Args:
prototype (dict): The prototype.
"""
header = """
|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n
|c-desc|n: {prototype_desc}
|cprototype-parent:|n {prototype_parent}
\n""".format(
prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'),
prototype_tags=prototype.get('prototype_tags', '|wNone|n'),
prototype_locks=prototype.get('prototype_locks', '|wNone|n'),
prototype_desc=prototype.get('prototype_desc', '|wNone|n'),
prototype_parent=prototype.get('prototype_parent', '|wNone|n'))
key = prototype.get('key', '')
if key:
key = "|ckey:|n {key}".format(key=key)
aliases = prototype.get("aliases", '')
if aliases:
aliases = "|caliases:|n {aliases}".format(
aliases=", ".join(aliases))
attrs = prototype.get("attrs", '')
if attrs:
out = []
for (attrkey, value, category, locks) in attrs:
locks = ", ".join(lock for lock in locks if lock)
category = "|ccategory:|n {}".format(category) if category else ''
cat_locks = ""
if category or locks:
cat_locks = " (|ccategory:|n {category}, ".format(
category=category if category else "|wNone|n")
out.append(
"{attrkey}{cat_locks} |c=|n {value}".format(
attrkey=attrkey,
cat_locks=cat_locks,
locks=locks if locks else "|wNone|n",
value=value))
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
tags = prototype.get('tags', '')
if tags:
out = []
for (tagkey, category, data) in tags:
out.append("{tagkey} (category: {category}{dat})".format(
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""))
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
locks = prototype.get('locks', '')
if locks:
locks = "|clocks:|n\n {locks}".format(locks=locks)
permissions = prototype.get("permissions", '')
if permissions:
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
location = prototype.get("location", '')
if location:
location = "|clocation:|n {location}".format(location=location)
home = prototype.get("home", '')
if home:
home = "|chome:|n {home}".format(home=home)
destination = prototype.get("destination", '')
if destination:
destination = "|cdestination:|n {destination}".format(destination=destination)
body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions,
location, home, destination) if part)
return header.lstrip() + body.strip()
def check_permission(prototype_key, action, default=True):
"""
Helper function to check access to actions on given prototype.
Args:
prototype_key (str): The prototype to affect.
action (str): One of "spawn" or "edit".
default (str): If action is unknown or prototype has no locks
Returns:
passes (bool): If permission for action is granted or not.
"""
if action == 'edit':
if prototype_key in _MODULE_PROTOTYPES:
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
logger.log_err("{} is a read-only prototype "
"(defined as code in {}).".format(prototype_key, mod))
return False
prototype = search_prototype(key=prototype_key)
if not prototype:
logger.log_err("Prototype {} not found.".format(prototype_key))
return False
lockstring = prototype.get("prototype_locks")
if lockstring:
return check_lockstring(None, lockstring, default=default, access_type=action)
return default
def init_spawn_value(value, validator=None):
"""
Analyze the prototype value and produce a value useful at the point of spawning.
Args:
value (any): This can be:
callable - will be called as callable()
(callable, (args,)) - will be called as callable(*args)
other - will be assigned depending on the variable type
validator (callable, optional): If given, this will be called with the value to
check and guarantee the outcome is of a given type.
Returns:
any (any): The (potentially pre-processed value to use for this prototype key)
"""
value = protfunc_parser(value)
validator = validator if validator else lambda o: o
if callable(value):
return validator(value())
elif value and is_iter(value) and callable(value[0]):
# a structure (callable, (args, ))
args = value[1:]
return validator(value[0](*make_iter(args)))
else:
return validator(value)
# module-based prototypes
for mod in settings.PROTOTYPE_MODULES:
# to remove a default prototype, override it with an empty dict.
# internally we store as (key, desc, locks, tags, prototype_dict)
prots = [(prototype_key.lower(), prot) for prototype_key, prot in all_from_module(mod).items()
if prot and isinstance(prot, dict)]
# assign module path to each prototype_key for easy reference
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
# make sure the prototype contains all meta info
for prototype_key, prot in prots:
actual_prot_key = prot.get('prototype_key', prototype_key).lower()
prot.update({
"prototype_key": actual_prot_key,
"prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod,
"prototype_locks": (prot['prototype_locks']
if 'prototype_locks' in prot else "use:all();edit:false()"),
"prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))})
_MODULE_PROTOTYPES[actual_prot_key] = prot
# Db-based prototypes
class DbPrototype(DefaultScript):
"""
This stores a single prototype, in an Attribute `prototype`.
"""
def at_script_creation(self):
self.key = "empty prototype" # prototype_key
self.desc = "A prototype" # prototype_desc
self.db.prototype = {} # actual prototype
# Prototype manager functions
def save_prototype(**kwargs):
"""
Create/Store a prototype persistently.
Kwargs:
prototype_key (str): This is required for any storage.
All other kwargs are considered part of the new prototype dict.
Returns:
prototype (dict or None): The prototype stored using the given kwargs, None if deleting.
Raises:
prototypes.ValidationError: If prototype does not validate.
Note:
No edit/spawn locks will be checked here - if this function is called the caller
is expected to have valid permissions.
"""
def _to_batchtuple(inp, *args):
"build tuple suitable for batch-creation"
if is_iter(inp):
# already a tuple/list, use as-is
return inp
return (inp, ) + args
prototype_key = kwargs.get("prototype_key")
if not prototype_key:
raise ValidationError("Prototype requires a prototype_key")
prototype_key = str(prototype_key).lower()
# we can't edit a prototype defined in a module
if prototype_key in _MODULE_PROTOTYPES:
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
raise PermissionError("{} is a read-only prototype "
"(defined as code in {}).".format(prototype_key, mod))
# make sure meta properties are included with defaults
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
prototype = dict(stored_prototype[0].db.prototype) if stored_prototype else {}
kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", ""))
prototype_locks = kwargs.get(
"prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)"))
is_valid, err = validate_lockstring(prototype_locks)
if not is_valid:
raise ValidationError("Lock error: {}".format(err))
kwargs['prototype_locks'] = prototype_locks
prototype_tags = [
_to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY)
for tag in make_iter(kwargs.get("prototype_tags",
prototype.get('prototype_tags', [])))]
kwargs["prototype_tags"] = prototype_tags
prototype.update(kwargs)
if stored_prototype:
# edit existing prototype
stored_prototype = stored_prototype[0]
stored_prototype.desc = prototype['prototype_desc']
if prototype_tags:
stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
stored_prototype.tags.batch_add(*prototype['prototype_tags'])
stored_prototype.locks.add(prototype['prototype_locks'])
stored_prototype.attributes.add('prototype', prototype)
else:
# create a new prototype
stored_prototype = create_script(
DbPrototype, key=prototype_key, desc=prototype['prototype_desc'], persistent=True,
locks=prototype_locks, tags=prototype['prototype_tags'],
attributes=[("prototype", prototype)])
return stored_prototype.db.prototype
# alias
create_prototype = save_prototype
def delete_prototype(prototype_key, caller=None):
"""
Delete a stored prototype
Args:
key (str): The persistent prototype to delete.
caller (Account or Object, optionsl): Caller aiming to delete a prototype.
Note that no locks will be checked if`caller` is not passed.
Returns:
success (bool): If deletion worked or not.
Raises:
PermissionError: If 'edit' lock was not passed or deletion failed for some other reason.
"""
if prototype_key in _MODULE_PROTOTYPES:
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(), "N/A")
raise PermissionError("{} is a read-only prototype "
"(defined as code in {}).".format(prototype_key, mod))
stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)
if not stored_prototype:
raise PermissionError("Prototype {} was not found.".format(prototype_key))
stored_prototype = stored_prototype[0]
if caller:
if not stored_prototype.access(caller, 'edit'):
raise PermissionError("{} does not have permission to "
"delete prototype {}.".format(caller, prototype_key))
stored_prototype.delete()
return True
def search_prototype(key=None, tags=None):
"""
Find prototypes based on key and/or tags, or all prototypes.
Kwargs:
key (str): An exact or partial key to query for.
tags (str or list): Tag key or keys to query for. These
will always be applied with the 'db_protototype'
tag category.
Return:
matches (list): All found prototype dicts. If no keys
or tags are given, all available prototypes will be returned.
Note:
The available prototypes is a combination of those supplied in
PROTOTYPE_MODULES and those stored in the database. Note that if
tags are given and the prototype has no tags defined, it will not
be found as a match.
"""
# search module prototypes
mod_matches = {}
if tags:
# use tags to limit selection
tagset = set(tags)
mod_matches = {prototype_key: prototype
for prototype_key, prototype in _MODULE_PROTOTYPES.items()
if tagset.intersection(prototype.get("prototype_tags", []))}
else:
mod_matches = _MODULE_PROTOTYPES
if key:
if key in mod_matches:
# exact match
module_prototypes = [mod_matches[key]]
else:
# fuzzy matching
module_prototypes = [prototype for prototype_key, prototype in mod_matches.items()
if key in prototype_key]
else:
module_prototypes = [match for match in mod_matches.values()]
# search db-stored prototypes
if tags:
# exact match on tag(s)
tags = make_iter(tags)
tag_categories = ["db_prototype" for _ in tags]
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
else:
db_matches = DbPrototype.objects.all()
if key:
# exact or partial match on key
db_matches = db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
# return prototype
db_prototypes = [dict(dbprot.attributes.get("prototype", {})) for dbprot in db_matches]
matches = db_prototypes + module_prototypes
nmatches = len(matches)
if nmatches > 1 and key:
key = key.lower()
# avoid duplicates if an exact match exist between the two types
filter_matches = [mta for mta in matches
if mta.get('prototype_key') and mta['prototype_key'] == key]
if filter_matches and len(filter_matches) < nmatches:
matches = filter_matches
return matches
def search_objects_with_prototype(prototype_key):
"""
Retrieve all object instances created by a given prototype.
Args:
prototype_key (str): The exact (and unique) prototype identifier to query for.
Returns:
matches (Queryset): All matching objects spawned from this prototype.
"""
return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
"""
Collate a list of found prototypes based on search criteria and access.
Args:
caller (Account or Object): The object requesting the list.
key (str, optional): Exact or partial prototype key to query for.
tags (str or list, optional): Tag key or keys to query for.
show_non_use (bool, optional): Show also prototypes the caller may not use.
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
Returns:
table (EvTable or None): An EvTable representation of the prototypes. None
if no prototypes were found.
"""
# this allows us to pass lists of empty strings
tags = [tag for tag in make_iter(tags) if tag]
# get prototypes for readonly and db-based prototypes
prototypes = search_prototype(key, tags)
# get use-permissions of readonly attributes (edit is always False)
display_tuples = []
for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')):
lock_use = caller.locks.check_lockstring(
caller, prototype.get('prototype_locks', ''), access_type='spawn')
if not show_non_use and not lock_use:
continue
if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES:
lock_edit = False
else:
lock_edit = caller.locks.check_lockstring(
caller, prototype.get('prototype_locks', ''), access_type='edit')
if not show_non_edit and not lock_edit:
continue
ptags = []
for ptag in prototype.get('prototype_tags', []):
if is_iter(ptag):
if len(ptag) > 1:
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
else:
ptags.append(ptag[0])
else:
ptags.append(str(ptag))
display_tuples.append(
(prototype.get('prototype_key', '<unset>'),
prototype.get('prototype_desc', '<unset>'),
"{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'),
",".join(ptags)))
if not display_tuples:
return ""
table = []
width = 78
for i in range(len(display_tuples[0])):
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
table.reformat_column(0, width=22)
table.reformat_column(1, width=29)
table.reformat_column(2, width=11, align='c')
table.reformat_column(3, width=16)
return table
def validate_prototype(prototype, protkey=None, protparents=None,
is_prototype_base=True, strict=True, _flags=None):
"""
Run validation on a prototype, checking for inifinite regress.
Args:
prototype (dict): Prototype to validate.
protkey (str, optional): The name of the prototype definition. If not given, the prototype
dict needs to have the `prototype_key` field set.
protpartents (dict, optional): The available prototype parent library. If
note given this will be determined from settings/database.
is_prototype_base (bool, optional): We are trying to create a new object *based on this
object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent
etc.
strict (bool, optional): If unset, don't require needed keys, only check against infinite
recursion etc.
_flags (dict, optional): Internal work dict that should not be set externally.
Raises:
RuntimeError: If prototype has invalid structure.
RuntimeWarning: If prototype has issues that would make it unsuitable to build an object
with (it may still be useful as a mix-in prototype).
"""
assert isinstance(prototype, dict)
if _flags is None:
_flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []}
if not protparents:
protparents = {prototype.get('prototype_key', "").lower(): prototype
for prototype in search_prototype()}
protkey = protkey and protkey.lower() or prototype.get('prototype_key', None)
if strict and not bool(protkey):
_flags['errors'].append("Prototype lacks a `prototype_key`.")
protkey = "[UNSET]"
typeclass = prototype.get('typeclass')
prototype_parent = prototype.get('prototype_parent', [])
if strict and not (typeclass or prototype_parent):
if is_prototype_base:
_flags['errors'].append("Prototype {} requires `typeclass` "
"or 'prototype_parent'.".format(protkey))
else:
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
"a typeclass or a prototype_parent.".format(protkey))
if strict and typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"):
_flags['errors'].append(
"Prototype {} is based on typeclass {}, which could not be imported!".format(
protkey, typeclass))
# recursively traverese prototype_parent chain
for protstring in make_iter(prototype_parent):
protstring = protstring.lower()
if protkey is not None and protstring == protkey:
_flags['errors'].append("Prototype {} tries to parent itself.".format(protkey))
protparent = protparents.get(protstring)
if not protparent:
_flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format(
(protkey, protstring)))
if id(prototype) in _flags['visited']:
_flags['errors'].append(
"{} has infinite nesting of prototypes.".format(protkey or prototype))
if _flags['errors']:
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
_flags['visited'].append(id(prototype))
_flags['depth'] += 1
validate_prototype(protparent, protstring, protparents,
is_prototype_base=is_prototype_base, _flags=_flags)
_flags['visited'].pop()
_flags['depth'] -= 1
if typeclass and not _flags['typeclass']:
_flags['typeclass'] = typeclass
# if we get back to the current level without a typeclass it's an error.
if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']:
_flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.".format(protkey))
if _flags['depth'] <= 0:
if _flags['errors']:
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
if _flags['warnings']:
raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags['warnings']))
# make sure prototype_locks are set to defaults
prototype_locks = [lstring.split(":", 1)
for lstring in prototype.get("prototype_locks", "").split(';') if ":" in lstring]
locktypes = [tup[0].strip() for tup in prototype_locks]
if "spawn" not in locktypes:
prototype_locks.append(("spawn", "all()"))
if "edit" not in locktypes:
prototype_locks.append(("edit", "all()"))
prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
prototype['prototype_locks'] = prototype_locks

View file

@ -0,0 +1,602 @@
"""
Spawner
The spawner takes input files containing object definitions in
dictionary forms. These use a prototype architecture 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:
```python
GOBLIN = {
"typeclass": "types.objects.Monster",
"key": "goblin grunt",
"health": lambda: randint(20,30),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
"tags": ["mob", "evil", ('greenskin','mob')]
"attrs": [("weapon", "sword")]
}
```
Possible keywords are:
prototype_key (str): name of this prototype. This is used when storing prototypes and should
be unique. This should always be defined but for prototypes defined in modules, the
variable holding the prototype dict will become the prototype_key if it's not explicitly
given.
prototype_desc (str, optional): describes prototype in listings
prototype_locks (str, optional): locks for restricting access to this prototype. Locktypes
supported are 'edit' and 'use'.
prototype_tags(list, optional): List of tags or tuples (tag, category) used to group prototype
in listings
prototype_parent (str, tuple or callable, optional): name (prototype_key) of eventual parent prototype, or
a list of parents, for multiple left-to-right inheritance.
prototype: Deprecated. Same meaning as 'parent'.
typeclass (str or callable, optional): if not set, will use typeclass of parent prototype or use
`settings.BASE_OBJECT_TYPECLASS`
key (str or callable, optional): the name of the spawned object. If not given this will set to a
random hash
location (obj, str or callable, optional): location of the object - a valid object or #dbref
home (obj, str or callable, optional): valid object or #dbref
destination (obj, str or callable, optional): only valid for exits (object or #dbref)
permissions (str, list or callable, optional): which permissions for spawned object to have
locks (str or callable, optional): lock-string for the spawned object
aliases (str, list or callable, optional): Aliases for the spawned object
exec (str or callable, optional): this is a string of python code to execute or a list of such
codes. This can be used e.g. to trigger custom handlers on the object. The execution
namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
this functionality to Developer/superusers. Usually it's better to use callables or
prototypefuncs instead of this.
tags (str, tuple, list or callable, optional): string or list of strings or tuples
`(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
attrs (tuple, list or callable, optional): tuple or list of tuples of Attributes to add. This
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
lockstring but not a category, set the category to `None`.
ndb_<name> (any): value of a nattribute (ndb_ is stripped)
other (any): any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories.
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. Note, if you want to store
a callable in an Attribute, embed it in a tuple to the `args` keyword.
By specifying the "prototype" key, the prototype becomes a child of
that prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify.
```python
import random
GOBLIN_WIZARD = {
"prototype_parent": GOBLIN,
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
}
GOBLIN_ARCHER = {
"prototype_parent": GOBLIN,
"key": "goblin archer",
"attack_skill": (random, (5, 10))"
"attacks": ["short bow"]
}
```
One can also have multiple prototypes. These are inherited from the
left, with the ones further to the right taking precedence.
```python
ARCHWIZARD = {
"attack": ["archwizard staff", "eye of doom"]
GOBLIN_ARCHWIZARD = {
"key" : "goblin archwizard"
"prototype_parent": (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*.
Storage mechanism:
This sets up a central storage for prototypes. The idea is to make these
available in a repository for buildiers to use. Each prototype is stored
in a Script so that it can be tagged for quick sorting/finding and locked for limiting
access.
This system also takes into consideration prototypes defined and stored in modules.
Such prototypes are considered 'read-only' to the system and can only be modified
in code. To replace a default prototype, add the same-name prototype in a
custom module read later in the settings.PROTOTYPE_MODULES list. To remove a default
prototype, override its name with an empty dict.
"""
from __future__ import print_function
import copy
import hashlib
import time
from django.conf import settings
import evennia
from evennia.objects.models import ObjectDB
from evennia.utils.utils import make_iter, is_iter
from evennia.prototypes import prototypes as protlib
from evennia.prototypes.prototypes import (
value_to_obj, value_to_obj_or_any, init_spawn_value, _PROTOTYPE_TAG_CATEGORY)
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags", "prototype_locks")
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
# Helper
def _get_prototype(dic, prot, protparents):
"""
Recursively traverse a prototype dictionary, including multiple
inheritance. Use validate_prototype before this, we don't check
for infinite recursion here.
"""
# we don't overload the prototype_key
prototype_key = prot.get('prototype_key', None)
if "prototype_parent" in dic:
# move backwards through the inheritance
for prototype in make_iter(dic["prototype_parent"]):
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(protparents.get(prototype.lower(), {}), prot, protparents)
prot.update(new_prot)
prot.update(dic)
prot['prototype_key'] = prototype_key
prot.pop("prototype_parent", None) # we don't need this anymore
return prot
def flatten_prototype(prototype, validate=False):
"""
Produce a 'flattened' prototype, where all prototype parents in the inheritance tree have been
merged into a final prototype.
Args:
prototype (dict): Prototype to flatten. Its `prototype_parent` field will be parsed.
validate (bool, optional): Validate for valid keys etc.
Returns:
flattened (dict): The final, flattened prototype.
"""
if prototype:
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
protlib.validate_prototype(prototype, None, protparents,
is_prototype_base=validate, strict=validate)
return _get_prototype(prototype, {}, protparents)
return {}
# obj-related prototype functions
def prototype_from_object(obj):
"""
Guess a minimal prototype from an existing object.
Args:
obj (Object): An object to analyze.
Returns:
prototype (dict): A prototype estimating the current state of the object.
"""
# first, check if this object already has a prototype
prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
if prot:
prot = protlib.search_prototype(prot[0])
if not prot or len(prot) > 1:
# no unambiguous prototype found - build new prototype
prot = {}
prot['prototype_key'] = "From-Object-{}-{}".format(
obj.key, hashlib.md5(str(time.time())).hexdigest()[:7])
prot['prototype_desc'] = "Built from {}".format(str(obj))
prot['prototype_locks'] = "spawn:all();edit:all()"
prot['prototype_tags'] = []
else:
prot = prot[0]
prot['key'] = obj.db_key or hashlib.md5(str(time.time())).hexdigest()[:6]
prot['typeclass'] = obj.db_typeclass_path
location = obj.db_location
if location:
prot['location'] = location.dbref
home = obj.db_home
if home:
prot['home'] = home.dbref
destination = obj.db_destination
if destination:
prot['destination'] = destination.dbref
locks = obj.locks.all()
if locks:
prot['locks'] = ";".join(locks)
perms = obj.permissions.get()
if perms:
prot['permissions'] = make_iter(perms)
aliases = obj.aliases.get()
if aliases:
prot['aliases'] = aliases
tags = [(tag.db_key, tag.db_category, tag.db_data)
for tag in obj.tags.get(return_tagobj=True, return_list=True) if tag]
if tags:
prot['tags'] = tags
attrs = [(attr.key, attr.value, attr.category, attr.locks.all())
for attr in obj.attributes.get(return_obj=True, return_list=True) if attr]
if attrs:
prot['attrs'] = attrs
return prot
def prototype_diff_from_object(prototype, obj):
"""
Get a simple diff for a prototype compared to an object which may or may not already have a
prototype (or has one but changed locally). For more complex migratations a manual diff may be
needed.
Args:
prototype (dict): Prototype.
obj (Object): Object to
Returns:
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
other_prototype (dict): The prototype for the given object. The diff is a how to convert
this prototype into the new prototype.
"""
prot1 = prototype
prot2 = prototype_from_object(obj)
diff = {}
for key, value in prot1.items():
diff[key] = "KEEP"
if key in prot2:
if callable(prot2[key]) or value != prot2[key]:
if key in ('attrs', 'tags', 'permissions', 'locks', 'aliases'):
diff[key] = 'REPLACE'
else:
diff[key] = "UPDATE"
elif key not in prot2:
diff[key] = "UPDATE"
for key in prot2:
if key not in diff and key not in prot1:
diff[key] = "REMOVE"
return diff, prot2
def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
"""
Update existing objects with the latest version of the prototype.
Args:
prototype (str or dict): Either the `prototype_key` to use or the
prototype dict itself.
diff (dict, optional): This a diff structure that describes how to update the protototype.
If not given this will be constructed from the first object found.
objects (list, optional): List of objects to update. If not given, query for these
objects using the prototype's `prototype_key`.
Returns:
changed (int): The number of objects that had changes applied to them.
"""
if isinstance(prototype, basestring):
new_prototype = protlib.search_prototype(prototype)
else:
new_prototype = prototype
prototype_key = new_prototype['prototype_key']
if not objects:
objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
if not objects:
return 0
if not diff:
diff, _ = prototype_diff_from_object(new_prototype, objects[0])
changed = 0
for obj in objects:
do_save = False
old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
old_prot_key = old_prot_key[0] if old_prot_key else None
if prototype_key != old_prot_key:
obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
for key, directive in diff.items():
if directive in ('UPDATE', 'REPLACE'):
if key in _PROTOTYPE_META_NAMES:
# prototype meta keys are not stored on-object
continue
val = new_prototype[key]
do_save = True
if key == 'key':
obj.db_key = init_spawn_value(val, str)
elif key == 'typeclass':
obj.db_typeclass_path = init_spawn_value(val, str)
elif key == 'location':
obj.db_location = init_spawn_value(val, value_to_obj)
elif key == 'home':
obj.db_home = init_spawn_value(val, value_to_obj)
elif key == 'destination':
obj.db_destination = init_spawn_value(val, value_to_obj)
elif key == 'locks':
if directive == 'REPLACE':
obj.locks.clear()
obj.locks.add(init_spawn_value(val, str))
elif key == 'permissions':
if directive == 'REPLACE':
obj.permissions.clear()
obj.permissions.batch_add(*init_spawn_value(val, make_iter))
elif key == 'aliases':
if directive == 'REPLACE':
obj.aliases.clear()
obj.aliases.batch_add(*init_spawn_value(val, make_iter))
elif key == 'tags':
if directive == 'REPLACE':
obj.tags.clear()
obj.tags.batch_add(*init_spawn_value(val, make_iter))
elif key == 'attrs':
if directive == 'REPLACE':
obj.attributes.clear()
obj.attributes.batch_add(*init_spawn_value(val, make_iter))
elif key == 'exec':
# we don't auto-rerun exec statements, it would be huge security risk!
pass
else:
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
elif directive == 'REMOVE':
do_save = True
if key == 'key':
obj.db_key = ''
elif key == 'typeclass':
# fall back to default
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
elif key == 'location':
obj.db_location = None
elif key == 'home':
obj.db_home = None
elif key == 'destination':
obj.db_destination = None
elif key == 'locks':
obj.locks.clear()
elif key == 'permissions':
obj.permissions.clear()
elif key == 'aliases':
obj.aliases.clear()
elif key == 'tags':
obj.tags.clear()
elif key == 'attrs':
obj.attributes.clear()
elif key == 'exec':
# we don't auto-rerun exec statements, it would be huge security risk!
pass
else:
obj.attributes.remove(key)
if do_save:
changed += 1
obj.save()
return changed
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!
Args:
objsparams (tuple): Each paremter tuple will create one object instance using the parameters
within.
The parameters should be given in the following order:
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
- `aliases` (list): A list of alias strings for
adding with `new_object.aliases.batch_add(*aliases)`.
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
add with `new_obj.nattributes.add(*tuple)`.
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
adding with `new_obj.attributes.batch_add(*attributes)`.
- `tags` (list): list of tuples `(key, category)` for adding
with `new_obj.tags.batch_add(*tags)`.
- `execs` (list): Code strings to execute together with the creation
of each object. They will be executed with `evennia` and `obj`
(the newly created object) available in the namespace. Execution
will happend after all other properties have been assigned and
is intended for calling custom handlers etc.
Returns:
objects (list): A list of created objects
Notes:
The `exec` list will execute arbitrary python code so don't allow this to be available to
unprivileged users!
"""
# bulk create all objects in one go
# unfortunately this doesn't work since bulk_create doesn't creates pks;
# the result would be duplicate objects at the next stage, so we comment
# it out for now:
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
objs = []
for iobj, obj in enumerate(dbobjs):
# call all setup hooks on each object
objparam = objparams[iobj]
# setup
obj._createdict = {"permissions": make_iter(objparam[1]),
"locks": objparam[2],
"aliases": make_iter(objparam[3]),
"nattributes": objparam[4],
"attributes": objparam[5],
"tags": make_iter(objparam[6])}
# this triggers all hooks
obj.save()
# run eventual extra code
for code in objparam[7]:
if code:
exec(code, {}, {"evennia": evennia, "obj": obj})
objs.append(obj)
return objs
# Spawner mechanism
def spawn(*prototypes, **kwargs):
"""
Spawn a number of prototyped objects.
Args:
prototypes (dict): Each argument should be a prototype
dictionary.
Kwargs:
prototype_modules (str or list): A python-path to a prototype
module, or a list of such paths. These will be used to build
the global protparents dictionary accessible by the input
prototypes. If not given, it will instead look for modules
defined by settings.PROTOTYPE_MODULES.
prototype_parents (dict): A dictionary holding a custom
prototype-parent dictionary. Will overload same-named
prototypes from prototype_modules.
return_parents (bool): Only return a dict of the
prototype-parents (no object creation happens)
only_validate (bool): Only run validation of prototype/parents
(no object creation) and return the create-kwargs.
Returns:
object (Object, dict or list): Spawned object. If `only_validate` is given, return
a list of the creation kwargs to build the object(s) without actually creating it. If
`return_parents` is set, return dict of prototype parents.
"""
# get available protparents
protparents = {prot['prototype_key'].lower(): prot for prot in protlib.search_prototype()}
# overload module's protparents with specifically given protparents
# we allow prototype_key to be the key of the protparent dict, to allow for module-level
# prototype imports. We need to insert prototype_key in this case
for key, protparent in kwargs.get("prototype_parents", {}).items():
key = str(key).lower()
protparent['prototype_key'] = str(protparent.get("prototype_key", key)).lower()
protparents[key] = protparent
if "return_parents" in kwargs:
# only return the parents
return copy.deepcopy(protparents)
objsparams = []
for prototype in prototypes:
protlib.validate_prototype(prototype, None, protparents, is_prototype_base=True)
prot = _get_prototype(prototype, {}, protparents)
if not prot:
continue
# extract the keyword args we need to create the object itself. If we get a callable,
# call that to get the value (don't catch errors)
create_kwargs = {}
# we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
# chance this is not unique but it should usually not be a problem.
val = prot.pop("key", "Spawned-{}".format(
hashlib.md5(str(time.time())).hexdigest()[:6]))
create_kwargs["db_key"] = init_spawn_value(val, str)
val = prot.pop("location", None)
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
val = prot.pop("home", settings.DEFAULT_HOME)
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
val = prot.pop("destination", None)
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str)
# extract calls to handlers
val = prot.pop("permissions", [])
permission_string = init_spawn_value(val, make_iter)
val = prot.pop("locks", "")
lock_string = init_spawn_value(val, str)
val = prot.pop("aliases", [])
alias_string = init_spawn_value(val, make_iter)
val = prot.pop("tags", [])
tags = []
for (tag, category, data) in tags:
tags.append((init_spawn_value(val, str), category, data))
prototype_key = prototype.get('prototype_key', None)
if prototype_key:
# we make sure to add a tag identifying which prototype created this object
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
val = prot.pop("exec", "")
execs = init_spawn_value(val, make_iter)
# extract ndb assignments
nattributes = dict((key.split("_", 1)[1], init_spawn_value(val, value_to_obj))
for key, val in prot.items() if key.startswith("ndb_"))
# the rest are attribute tuples (attrname, value, category, locks)
val = make_iter(prot.pop("attrs", []))
attributes = []
for (attrname, value, category, locks) in val:
attributes.append((attrname, init_spawn_value(val), category, locks))
simple_attributes = []
for key, value in ((key, value) for key, value in prot.items()
if not (key.startswith("ndb_"))):
if key in _PROTOTYPE_META_NAMES:
continue
if is_iter(value) and len(value) > 1:
# (value, category)
simple_attributes.append((key,
init_spawn_value(value[0], value_to_obj_or_any),
init_spawn_value(value[1], str)))
else:
simple_attributes.append((key,
init_spawn_value(value, value_to_obj_or_any)))
attributes = attributes + simple_attributes
attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS]
# pack for call into _batch_create_object
objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs))
if kwargs.get("only_validate"):
return objsparams
return batch_create_object(*objsparams)

531
evennia/prototypes/tests.py Normal file
View file

@ -0,0 +1,531 @@
"""
Unit tests for the prototypes and spawner
"""
from random import randint
import mock
from anything import Something
from django.test.utils import override_settings
from evennia.utils.test_resources import EvenniaTest
from evennia.utils.tests.test_evmenu import TestEvMenu
from evennia.prototypes import spawner, prototypes as protlib
from evennia.prototypes import menus as olc_menus
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
_PROTPARENTS = {
"NOBODY": {},
"GOBLIN": {
"prototype_key": "GOBLIN",
"typeclass": "evennia.objects.objects.DefaultObject",
"key": "goblin grunt",
"health": lambda: randint(1, 1),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
},
"GOBLIN_WIZARD": {
"prototype_parent": "GOBLIN",
"key": "goblin wizard",
"spells": ["fire ball", "lighting bolt"]
},
"GOBLIN_ARCHER": {
"prototype_parent": "GOBLIN",
"key": "goblin archer",
"attacks": ["short bow"]
},
"ARCHWIZARD": {
"prototype_parent": "GOBLIN",
"attacks": ["archwizard staff"],
},
"GOBLIN_ARCHWIZARD": {
"key": "goblin archwizard",
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD")
}
}
class TestSpawner(EvenniaTest):
def setUp(self):
super(TestSpawner, self).setUp()
self.prot1 = {"prototype_key": "testprototype",
"typeclass": "evennia.objects.objects.DefaultObject"}
def test_spawn(self):
obj1 = spawner.spawn(self.prot1)
# check spawned objects have the right tag
self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1)
self.assertEqual([o.key for o in spawner.spawn(
_PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"],
prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard'])
class TestUtils(EvenniaTest):
def test_prototype_from_object(self):
self.maxDiff = None
self.obj1.attributes.add("test", "testval")
self.obj1.tags.add('foo')
new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual(
{'attrs': [('test', 'testval', None, [''])],
'home': Something,
'key': 'Obj',
'location': Something,
'locks': ";".join([
'call:true()',
'control:perm(Developer)',
'delete:perm(Admin)',
'edit:perm(Admin)',
'examine:perm(Builder)',
'get:all()',
'puppet:pperm(Developer)',
'tell:perm(Admin)',
'view:all()']),
'prototype_desc': 'Built from Obj',
'prototype_key': Something,
'prototype_locks': 'spawn:all();edit:all()',
'prototype_tags': [],
'tags': [(u'foo', None, None)],
'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot)
def test_update_objects_from_prototypes(self):
self.maxDiff = None
self.obj1.attributes.add('oldtest', 'to_remove')
old_prot = spawner.prototype_from_object(self.obj1)
# modify object away from prototype
self.obj1.attributes.add('test', 'testval')
self.obj1.aliases.add('foo')
self.obj1.key = 'NewObj'
# modify prototype
old_prot['new'] = 'new_val'
old_prot['test'] = 'testval_changed'
old_prot['permissions'] = 'Builder'
# this will not update, since we don't update the prototype on-disk
old_prot['prototype_desc'] = 'New version of prototype'
# diff obj/prototype
pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1)
self.assertEqual(
pdiff,
({'aliases': 'REMOVE',
'attrs': 'REPLACE',
'home': 'KEEP',
'key': 'UPDATE',
'location': 'KEEP',
'locks': 'KEEP',
'new': 'UPDATE',
'permissions': 'UPDATE',
'prototype_desc': 'UPDATE',
'prototype_key': 'UPDATE',
'prototype_locks': 'KEEP',
'prototype_tags': 'KEEP',
'test': 'UPDATE',
'typeclass': 'KEEP'},
{'attrs': [('oldtest', 'to_remove', None, ['']),
('test', 'testval', None, [''])],
'prototype_locks': 'spawn:all();edit:all()',
'prototype_key': Something,
'locks': ";".join([
'call:true()', 'control:perm(Developer)',
'delete:perm(Admin)', 'edit:perm(Admin)',
'examine:perm(Builder)', 'get:all()',
'puppet:pperm(Developer)', 'tell:perm(Admin)',
'view:all()']),
'prototype_tags': [],
'location': "#1",
'key': 'NewObj',
'home': '#1',
'typeclass': 'evennia.objects.objects.DefaultObject',
'prototype_desc': 'Built from NewObj',
'aliases': 'foo'})
)
# apply diff
count = spawner.batch_update_objects_with_prototype(
old_prot, diff=pdiff[0], objects=[self.obj1])
self.assertEqual(count, 1)
new_prot = spawner.prototype_from_object(self.obj1)
self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']),
('new', 'new_val', None, [''])],
'home': Something,
'key': 'Obj',
'location': Something,
'locks': ";".join([
'call:true()',
'control:perm(Developer)',
'delete:perm(Admin)',
'edit:perm(Admin)',
'examine:perm(Builder)',
'get:all()',
'puppet:pperm(Developer)',
'tell:perm(Admin)',
'view:all()']),
'permissions': ['builder'],
'prototype_desc': 'Built from Obj',
'prototype_key': Something,
'prototype_locks': 'spawn:all();edit:all()',
'prototype_tags': [],
'typeclass': 'evennia.objects.objects.DefaultObject'},
new_prot)
class TestProtLib(EvenniaTest):
def setUp(self):
super(TestProtLib, self).setUp()
self.obj1.attributes.add("testattr", "testval")
self.prot = spawner.prototype_from_object(self.obj1)
def test_prototype_to_str(self):
prstr = protlib.prototype_to_str(self.prot)
self.assertTrue(prstr.startswith("|cprototype-key:|n"))
def test_check_permission(self):
pass
@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20)
class TestProtFuncs(EvenniaTest):
def setUp(self):
super(TestProtFuncs, self).setUp()
self.prot = {"prototype_key": "test_prototype",
"prototype_desc": "testing prot",
"key": "ExampleObj"}
@mock.patch("evennia.prototypes.protfuncs.base_random", new=mock.MagicMock(return_value=0.5))
@mock.patch("evennia.prototypes.protfuncs.base_randint", new=mock.MagicMock(return_value=5))
def test_protfuncs(self):
self.assertEqual(protlib.protfunc_parser("$random()"), 0.5)
self.assertEqual(protlib.protfunc_parser("$randint(1, 10)"), 5)
self.assertEqual(protlib.protfunc_parser("$left_justify( foo )"), "foo ")
self.assertEqual(protlib.protfunc_parser("$right_justify( foo )"), " foo")
self.assertEqual(protlib.protfunc_parser("$center_justify(foo )"), " foo ")
self.assertEqual(protlib.protfunc_parser(
"$full_justify(foo bar moo too)"), 'foo bar moo too')
self.assertEqual(
protlib.protfunc_parser("$right_justify( foo )", testing=True),
('unexpected indent (<unknown>, line 1)', ' foo'))
test_prot = {"key1": "value1",
"key2": 2}
self.assertEqual(protlib.protfunc_parser(
"$protkey(key1)", testing=True, prototype=test_prot), (None, "value1"))
self.assertEqual(protlib.protfunc_parser(
"$protkey(key2)", testing=True, prototype=test_prot), (None, 2))
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
self.assertEqual(protlib.protfunc_parser(
"$add('''[1,2,3]''', '''[4,5,6]''')"), [1, 2, 3, 4, 5, 6])
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar")
self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3)
self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)")
self.assertEqual(protlib.protfunc_parser("$mult(5, 2)"), 10)
self.assertEqual(protlib.protfunc_parser("$mult( 5 , 10)"), 50)
self.assertEqual(protlib.protfunc_parser("$mult('foo',3)"), "foofoofoo")
self.assertEqual(protlib.protfunc_parser("$mult(foo,3)"), "foofoofoo")
self.assertRaises(TypeError, protlib.protfunc_parser, "$mult(foo, foo)")
self.assertEqual(protlib.protfunc_parser("$toint(5.3)"), 5)
self.assertEqual(protlib.protfunc_parser("$div(5, 2)"), 2.5)
self.assertEqual(protlib.protfunc_parser("$toint($div(5, 2))"), 2)
self.assertEqual(protlib.protfunc_parser("$sub($add(5, 3), $add(10, 2))"), -4)
self.assertEqual(protlib.protfunc_parser("$eval('2')"), '2')
self.assertEqual(protlib.protfunc_parser(
"$eval(['test', 1, '2', 3.5, \"foo\"])"), ['test', 1, '2', 3.5, 'foo'])
self.assertEqual(protlib.protfunc_parser(
"$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3})
self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1')
self.assertEqual(protlib.protfunc_parser("#1", session=self.session), '#1')
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1'])
self.assertEqual(protlib.value_to_obj(
protlib.protfunc_parser("#6", session=self.session)), self.char1)
self.assertEqual(protlib.value_to_obj_or_any(
protlib.protfunc_parser("#6", session=self.session)), self.char1)
self.assertEqual(protlib.value_to_obj_or_any(
protlib.protfunc_parser("[1,2,3,'#6',5]", session=self.session)),
[1, 2, 3, self.char1, 5])
class TestPrototypeStorage(EvenniaTest):
def setUp(self):
super(TestPrototypeStorage, self).setUp()
self.maxDiff = None
self.prot1 = spawner.prototype_from_object(self.obj1)
self.prot1['prototype_key'] = 'testprototype1'
self.prot1['prototype_desc'] = 'testdesc1'
self.prot1['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
self.prot2 = self.prot1.copy()
self.prot2['prototype_key'] = 'testprototype2'
self.prot2['prototype_desc'] = 'testdesc2'
self.prot2['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
self.prot3 = self.prot2.copy()
self.prot3['prototype_key'] = 'testprototype3'
self.prot3['prototype_desc'] = 'testdesc3'
self.prot3['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
def test_prototype_storage(self):
prot1 = protlib.create_prototype(**self.prot1)
self.assertTrue(bool(prot1))
self.assertEqual(prot1, self.prot1)
self.assertEqual(prot1['prototype_desc'], "testdesc1")
self.assertEqual(prot1['prototype_tags'], [("foo1", _PROTOTYPE_TAG_META_CATEGORY)])
self.assertEqual(
protlib.DbPrototype.objects.get_by_tag(
"foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1)
prot2 = protlib.create_prototype(**self.prot2)
self.assertEqual(
[pobj.db.prototype
for pobj in protlib.DbPrototype.objects.get_by_tag(
"foo1", _PROTOTYPE_TAG_META_CATEGORY)],
[prot1, prot2])
# add to existing prototype
prot1b = protlib.create_prototype(
prototype_key='testprototype1', foo='bar', prototype_tags=['foo2'])
self.assertEqual(
[pobj.db.prototype
for pobj in protlib.DbPrototype.objects.get_by_tag(
"foo2", _PROTOTYPE_TAG_META_CATEGORY)],
[prot1b])
self.assertEqual(list(protlib.search_prototype("testprototype2")), [prot2])
self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1])
self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b])
prot3 = protlib.create_prototype(**self.prot3)
# partial match
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
class _MockMenu(object):
pass
class TestMenuModule(EvenniaTest):
def setUp(self):
super(TestMenuModule, self).setUp()
# set up fake store
self.caller = self.char1
menutree = _MockMenu()
self.caller.ndb._menutree = menutree
self.test_prot = {"prototype_key": "test_prot",
"typeclass": "evennia.objects.objects.DefaultObject",
"prototype_locks": "edit:all();spawn:all()"}
def test_helpers(self):
caller = self.caller
# general helpers
self.assertEqual(olc_menus._get_menu_prototype(caller), {})
self.assertEqual(olc_menus._is_new_prototype(caller), True)
self.assertEqual(olc_menus._set_menu_prototype(caller, {}), {})
self.assertEqual(
olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"})
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"})
self.assertEqual(olc_menus._format_option_value(
"key", required=True, prototype=olc_menus._get_menu_prototype(caller)), " (TestKey|n)")
self.assertEqual(olc_menus._format_option_value(
[1, 2, 3, "foo"], required=True), ' (1, 2, 3, foo|n)')
self.assertEqual(olc_menus._set_property(
caller, "ChangedKey", prop="key", processor=str, next_node="foo"), "foo")
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "ChangedKey"})
self.assertEqual(olc_menus._wizard_options(
"ThisNode", "PrevNode", "NextNode"),
[{'goto': 'node_PrevNode', 'key': ('|wB|Wack', 'b'), 'desc': '|W(PrevNode)|n'},
{'goto': 'node_NextNode', 'key': ('|wF|Worward', 'f'), 'desc': '|W(NextNode)|n'},
{'goto': 'node_index', 'key': ('|wI|Wndex', 'i')},
{'goto': ('node_validate_prototype', {'back': 'ThisNode'}),
'key': ('|wV|Walidate prototype', 'validate', 'v')}])
self.assertEqual(olc_menus._validate_prototype(self.test_prot), (False, Something))
self.assertEqual(olc_menus._validate_prototype(
{"prototype_key": "testthing", "key": "mytest"}),
(True, Something))
choices = ["test1", "test2", "test3", "test4"]
actions = (("examine", "e", "l"), ("add", "a"), ("foo", "f"))
self.assertEqual(olc_menus._default_parse("l4", choices, *actions), ('test4', 'examine'))
self.assertEqual(olc_menus._default_parse("add 2", choices, *actions), ('test2', 'add'))
self.assertEqual(olc_menus._default_parse("foo3", choices, *actions), ('test3', 'foo'))
self.assertEqual(olc_menus._default_parse("f3", choices, *actions), ('test3', 'foo'))
self.assertEqual(olc_menus._default_parse("f5", choices, *actions), (None, None))
def test_node_helpers(self):
caller = self.caller
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[self.test_prot])):
# prototype_key helpers
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), None)
caller.ndb._menutree.olc_new = True
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_index")
# prototype_parent helpers
self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot'])
# self.assertEqual(olc_menus._prototype_parent_parse(
# caller, 'test_prot'),
# "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() "
# "\n|cdesc:|n None \n|cprototype:|n "
# "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}")
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
self.assertEqual(olc_menus._prototype_parent_select(caller, "goblin"), "node_prototype_parent")
self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()',
'prototype_parent': 'goblin',
'typeclass': 'evennia.objects.objects.DefaultObject'})
# typeclass helpers
with mock.patch("evennia.utils.utils.get_all_typeclasses",
new=mock.MagicMock(return_value={"foo": None, "bar": None})):
self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"])
self.assertEqual(olc_menus._typeclass_select(
caller, "evennia.objects.objects.DefaultObject"), None)
# prototype_parent should be popped off here
self.assertEqual(olc_menus._get_menu_prototype(caller),
{'prototype_key': 'test_prot',
'prototype_locks': 'edit:all();spawn:all()',
'prototype_parent': 'goblin',
'typeclass': 'evennia.objects.objects.DefaultObject'})
# attr helpers
self.assertEqual(olc_menus._caller_attrs(caller), [])
self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), Something)
self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), Something)
self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), Something)
self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), Something)
self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), Something)
self.assertEqual(olc_menus._add_attr(caller, "test1=foo1_changed"), Something)
self.assertEqual(olc_menus._get_menu_prototype(caller)['attrs'],
[("test1", "foo1_changed", None, ''),
("test2", "foo2", "cat1", ''),
("test3", "foo3", "cat2", "edit:false()"),
("test4", "foo4", "cat3", "set:true();edit:false()"),
("test5", '123', "cat4", "set:true();edit:false()")])
# tag helpers
self.assertEqual(olc_menus._caller_tags(caller), [])
self.assertEqual(olc_menus._add_tag(caller, "foo1"), Something)
self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), Something)
self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), Something)
self.assertEqual(olc_menus._caller_tags(caller), ['foo1', 'foo2', 'foo3'])
self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'],
[('foo1', None, ""),
('foo2', 'cat1', ""),
('foo3', 'cat2', "dat1")])
self.assertEqual(olc_menus._add_tag(caller, "foo1", delete=True), "Removed Tag 'foo1'.")
self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'],
[('foo2', 'cat1', ""),
('foo3', 'cat2', "dat1")])
self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something)
self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"])
protlib.save_prototype(**self.test_prot)
# locks helpers
self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.")
self.assertEqual(olc_menus._lock_add(caller, "foo2:false()"), "Added lock 'foo2:false()'.")
self.assertEqual(olc_menus._lock_add(caller, "foo2:true()"), "Lock with locktype 'foo2' updated.")
self.assertEqual(olc_menus._get_menu_prototype(caller)["locks"], "foo:false();foo2:true()")
# perm helpers
self.assertEqual(olc_menus._add_perm(caller, "foo"), "Added Permission 'foo'")
self.assertEqual(olc_menus._add_perm(caller, "foo2"), "Added Permission 'foo2'")
self.assertEqual(olc_menus._get_menu_prototype(caller)["permissions"], ["foo", "foo2"])
# prototype_tags helpers
self.assertEqual(olc_menus._add_prototype_tag(caller, "foo"), "Added Prototype-Tag 'foo'.")
self.assertEqual(olc_menus._add_prototype_tag(caller, "foo2"), "Added Prototype-Tag 'foo2'.")
self.assertEqual(olc_menus._get_menu_prototype(caller)["prototype_tags"], ["foo", "foo2"])
# spawn helpers
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
self.assertEqual(olc_menus._spawn(caller, prototype=self.test_prot), Something)
obj = caller.contents[0]
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key'])
# update helpers
self.assertEqual(olc_menus._apply_diff(
caller, prototype=self.test_prot, back_node="foo", objects=[obj]), 'foo') # no changes to apply
self.test_prot['key'] = "updated key" # change prototype
self.assertEqual(olc_menus._apply_diff(
caller, prototype=self.test_prot, objects=[obj], back_node='foo'), 'foo') # apply change to the one obj
# load helpers
self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']),
('node_examine_entity', {'text': '|gLoaded prototype test_prot.|n', 'back': 'index'}) )
@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
return_value=[{"prototype_key": "TestPrototype",
"typeclass": "TypeClassTest", "key": "TestObj"}]))
@mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(
return_value={"TypeclassTest": None}))
class TestOLCMenu(TestEvMenu):
maxDiff = None
menutree = "evennia.prototypes.menus"
startnode = "node_index"
# debug_output = True
expect_all_nodes = True
expected_node_texts = {
"node_index": "|c --- Prototype wizard --- |n"
}
expected_tree = ['node_index', ['node_prototype_key', ['node_index', 'node_index', 'node_validate_prototype', ['node_index', 'node_index'], 'node_index'], 'node_prototype_parent', ['node_prototype_parent', 'node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_typeclass', ['node_typeclass', 'node_prototype_parent', 'node_typeclass', 'node_index', 'node_validate_prototype', 'node_index'], 'node_key', ['node_typeclass', 'node_key', 'node_index', 'node_validate_prototype', 'node_index'], 'node_aliases', ['node_key', 'node_aliases', 'node_index', 'node_validate_prototype', 'node_index'], 'node_attrs', ['node_aliases', 'node_attrs', 'node_index', 'node_validate_prototype', 'node_index'], 'node_tags', ['node_attrs', 'node_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_locks', ['node_tags', 'node_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_permissions', ['node_locks', 'node_permissions', 'node_index', 'node_validate_prototype', 'node_index'], 'node_location', ['node_permissions', 'node_location', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_home', ['node_location', 'node_home', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_destination', ['node_home', 'node_destination', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_prototype_desc', ['node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_tags', ['node_prototype_desc', 'node_prototype_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_locks', ['node_examine_entity', ['node_prototype_locks', 'node_prototype_locks', 'node_prototype_locks'], 'node_examine_entity', 'node_prototype_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_validate_prototype', 'node_index', 'node_prototype_spawn', ['node_index', 'node_validate_prototype'], 'node_index', 'node_search_object', ['node_index', 'node_index']]]

View file

@ -152,15 +152,6 @@ class ScriptBase(with_metaclass(TypeclassBase, ScriptDB)):
"""
objects = ScriptManager()
class DefaultScript(ScriptBase):
"""
This is the base TypeClass for all Scripts. Scripts describe
events, timers and states in game, they can have a time component
or describe a state that changes under certain conditions.
"""
def __eq__(self, other):
"""
Compares two Scripts. Compares dbids.
@ -250,7 +241,96 @@ class DefaultScript(ScriptBase):
logger.log_trace()
return None
# Public methods
def at_script_creation(self):
"""
Should be overridden in child.
"""
pass
def at_first_save(self, **kwargs):
"""
This is called after very first time this object is saved.
Generally, you don't need to overload this, but only the hooks
called by this method.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
self.at_script_creation()
if hasattr(self, "_createdict"):
# this will only be set if the utils.create_script
# function was used to create the object. We want
# the create call's kwargs to override the values
# set by hooks.
cdict = self._createdict
updates = []
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#%i" % self.dbid
updates.append("db_key")
elif self.db_key != cdict["key"]:
self.db_key = cdict["key"]
updates.append("db_key")
if cdict.get("interval") and self.interval != cdict["interval"]:
self.db_interval = cdict["interval"]
updates.append("db_interval")
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
self.db_start_delay = cdict["start_delay"]
updates.append("db_start_delay")
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
self.db_repeats = cdict["repeats"]
updates.append("db_repeats")
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
self.db_persistent = cdict["persistent"]
updates.append("db_persistent")
if cdict.get("desc") and self.desc != cdict["desc"]:
self.db_desc = cdict["desc"]
updates.append("db_desc")
if updates:
self.save(update_fields=updates)
if cdict.get("permissions"):
self.permissions.batch_add(*cdict["permissions"])
if cdict.get("locks"):
self.locks.add(cdict["locks"])
if cdict.get("tags"):
# this should be a list of tags, tuples (key, category) or (key, category, data)
self.tags.batch_add(*cdict["tags"])
if cdict.get("attributes"):
# this should be tuples (key, val, ...)
self.attributes.batch_add(*cdict["attributes"])
if cdict.get("nattributes"):
# this should be a dict of nattrname:value
for key, value in cdict["nattributes"]:
self.nattributes.add(key, value)
if not cdict.get("autostart"):
# don't auto-start the script
return
# auto-start script (default)
self.start()
class DefaultScript(ScriptBase):
"""
This is the base TypeClass for all Scripts. Scripts describe
events, timers and states in game, they can have a time component
or describe a state that changes under certain conditions.
"""
def at_script_creation(self):
"""
Only called once, when script is first created.
"""
pass
def time_until_next_repeat(self):
"""
@ -514,61 +594,6 @@ class DefaultScript(ScriptBase):
if task:
task.force_repeat()
def at_first_save(self, **kwargs):
"""
This is called after very first time this object is saved.
Generally, you don't need to overload this, but only the hooks
called by this method.
Args:
**kwargs (dict): Arbitrary, optional arguments for users
overriding the call (unused by default).
"""
self.at_script_creation()
if hasattr(self, "_createdict"):
# this will only be set if the utils.create_script
# function was used to create the object. We want
# the create call's kwargs to override the values
# set by hooks.
cdict = self._createdict
updates = []
if not cdict.get("key"):
if not self.db_key:
self.db_key = "#%i" % self.dbid
updates.append("db_key")
elif self.db_key != cdict["key"]:
self.db_key = cdict["key"]
updates.append("db_key")
if cdict.get("interval") and self.interval != cdict["interval"]:
self.db_interval = cdict["interval"]
updates.append("db_interval")
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
self.db_start_delay = cdict["start_delay"]
updates.append("db_start_delay")
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
self.db_repeats = cdict["repeats"]
updates.append("db_repeats")
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
self.db_persistent = cdict["persistent"]
updates.append("db_persistent")
if updates:
self.save(update_fields=updates)
if not cdict.get("autostart"):
# don't auto-start the script
return
# auto-start script (default)
self.start()
def at_script_creation(self):
"""
Only called once, by the create function.
"""
pass
def is_valid(self):
"""
Is called to check if the script is valid to run at this time.

View file

@ -1037,9 +1037,11 @@ def tail_log_files(filename1, filename2, start_lines1=20, start_lines2=20, rate=
new_linecount = sum(blck.count("\n") for blck in _block(filehandle))
if new_linecount < old_linecount:
# this could happen if the file was manually deleted or edited
print("Log file has shrunk. Restart log reader.")
sys.exit()
# this happens if the file was cycled or manually deleted/edited.
print(" ** Log file {filename} has cycled or been edited. "
"Restarting log. ".format(filehandle.name))
new_linecount = 0
old_linecount = 0
lines_to_get = max(0, new_linecount - old_linecount)

View file

@ -354,6 +354,9 @@ LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",)
INPUT_FUNC_MODULES = ["evennia.server.inputfuncs", "server.conf.inputfuncs"]
# Modules that contain prototypes for use with the spawner mechanism.
PROTOTYPE_MODULES = ["world.prototypes"]
# Modules containining Prototype functions able to be embedded in prototype
# definitions from in-game.
PROT_FUNC_MODULES = ["evennia.prototypes.protfuncs"]
# Module holding settings/actions for the dummyrunner program (see the
# dummyrunner for more information)
DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings"
@ -513,7 +516,7 @@ TIME_GAME_EPOCH = None
TIME_IGNORE_DOWNTIMES = False
######################################################################
# Inlinefunc
# Inlinefunc & PrototypeFuncs
######################################################################
# Evennia supports inline function preprocessing. This allows users
# to supply inline calls on the form $func(arg, arg, ...) to do
@ -525,6 +528,10 @@ INLINEFUNC_ENABLED = False
# is loaded from left-to-right, same-named functions will overload
INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs",
"server.conf.inlinefuncs"]
# Module holding handlers for OLCFuncs. These allow for embedding
# functional code in prototypes
PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs",
"server.conf.prototypefuncs"]
######################################################################
# Default Account setup and access

View file

@ -435,6 +435,7 @@ class AttributeHandler(object):
def __init__(self):
self.key = None
self.value = default
self.category = None
self.strvalue = str(default) if default is not None else None
ret = []
@ -530,8 +531,8 @@ class AttributeHandler(object):
repeat-calling add when having many Attributes to add.
Args:
indata (tuple): Tuples of varying length representing the
Attribute to add to this object.
indata (list): List of tuples of varying length representing the
Attribute to add to this object. Supported tuples are
- `(key, value)`
- `(key, value, category)`
- `(key, value, category, lockstring)`

View file

@ -54,7 +54,8 @@ _GA = object.__getattribute__
def create_object(typeclass=None, key=None, location=None, home=None,
permissions=None, locks=None, aliases=None, tags=None,
destination=None, report_to=None, nohome=False):
destination=None, report_to=None, nohome=False, attributes=None,
nattributes=None):
"""
Create a new in-game object.
@ -68,13 +69,18 @@ def create_object(typeclass=None, key=None, location=None, home=None,
permissions (list): A list of permission strings or tuples (permstring, category).
locks (str): one or more lockstrings, separated by semicolons.
aliases (list): A list of alternative keys or tuples (aliasstring, category).
tags (list): List of tag keys or tuples (tagkey, category).
tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
destination (Object or str): Obj or #dbref to use as an Exit's
target.
report_to (Object): The object to return error messages to.
nohome (bool): This allows the creation of objects without a
default home location; only used when creating the default
location itself or during unittests.
attributes (list): Tuples on the form (key, value) or (key, value, category),
(key, value, lockstring) or (key, value, lockstring, default_access).
to set as Attributes on the new object.
nattributes (list): Non-persistent tuples on the form (key, value). Note that
adding this rarely makes sense since this data will not survive a reload.
Returns:
object (Object): A newly created object of the given typeclass.
@ -95,6 +101,7 @@ def create_object(typeclass=None, key=None, location=None, home=None,
locks = make_iter(locks) if locks is not None else None
aliases = make_iter(aliases) if aliases is not None else None
tags = make_iter(tags) if tags is not None else None
attributes = make_iter(attributes) if attributes is not None else None
if isinstance(typeclass, basestring):
@ -122,7 +129,8 @@ def create_object(typeclass=None, key=None, location=None, home=None,
# store the call signature for the signal
new_object._createdict = dict(key=key, location=location, destination=destination, home=home,
typeclass=typeclass.path, permissions=permissions, locks=locks,
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome)
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome,
attributes=attributes, nattributes=nattributes)
# this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict can be
# used.
@ -139,7 +147,8 @@ object = create_object
def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
interval=None, start_delay=None, repeats=None,
persistent=None, autostart=True, report_to=None, desc=None):
persistent=None, autostart=True, report_to=None, desc=None,
tags=None, attributes=None):
"""
Create a new script. All scripts are a combination of a database
object that communicates with the database, and an typeclass that
@ -169,7 +178,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
created or if the `start` method must be called explicitly.
report_to (Object): The object to return error messages to.
desc (str): Optional description of script
tags (list): List of tags or tuples (tag, category).
attributes (list): List if tuples (key, value) or (key, value, category)
(key, value, lockstring) or (key, value, lockstring, default_access).
See evennia.scripts.manager for methods to manipulate existing
scripts in the database.
@ -190,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
if key:
kwarg["db_key"] = key
if account:
kwarg["db_account"] = dbid_to_obj(account, _ScriptDB)
kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
if obj:
kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB)
kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
if interval:
kwarg["db_interval"] = interval
if start_delay:
@ -203,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
kwarg["db_persistent"] = persistent
if desc:
kwarg["db_desc"] = desc
tags = make_iter(tags) if tags is not None else None
attributes = make_iter(attributes) if attributes is not None else None
# create new instance
new_script = typeclass(**kwarg)
@ -210,7 +223,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
# store the call signature for the signal
new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval,
start_delay=start_delay, repeats=repeats, persistent=persistent,
autostart=autostart, report_to=report_to)
autostart=autostart, report_to=report_to, desc=desc,
tags=tags, attributes=attributes)
# this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict
# can be used.

View file

@ -167,14 +167,13 @@ from __future__ import print_function
import random
from builtins import object, range
from textwrap import dedent
from inspect import isfunction, getargspec
from django.conf import settings
from evennia import Command, CmdSet
from evennia.utils import logger
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter
from evennia.utils.utils import mod_import, make_iter, pad, m_len, is_iter, dedent
from evennia.commands import cmdhandler
# read from protocol NAWS later?
@ -796,7 +795,7 @@ class EvMenu(object):
# handle the helptext
if helptext:
self.helptext = helptext
self.helptext = self.helptext_formatter(helptext)
elif options:
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
else:
@ -896,7 +895,20 @@ class EvMenu(object):
nodetext (str): The formatted node text.
"""
return dedent(nodetext).strip()
return dedent(nodetext.strip('\n'), baseline_index=0).rstrip()
def helptext_formatter(self, helptext):
"""
Format the node's help text
Args:
helptext (str): The unformatted help text for the node.
Returns:
helptext (str): The formatted help text.
"""
return dedent(helptext.strip('\n'), baseline_index=0).rstrip()
def options_formatter(self, optionlist):
"""
@ -1042,7 +1054,10 @@ def list_node(option_generator, select=None, pagesize=10):
else:
if callable(select):
try:
return select(caller, selection)
if bool(getargspec(select).keywords):
return select(caller, selection, available_choices=available_choices)
else:
return select(caller, selection)
except Exception:
logger.log_trace()
elif select:
@ -1101,22 +1116,31 @@ def list_node(option_generator, select=None, pagesize=10):
# add data from the decorated node
decorated_options = []
supports_kwargs = bool(getargspec(func).keywords)
try:
text, decorated_options = func(caller, raw_string)
if supports_kwargs:
text, decorated_options = func(caller, raw_string, **kwargs)
else:
text, decorated_options = func(caller, raw_string)
except TypeError:
try:
text, decorated_options = func(caller)
if supports_kwargs:
text, decorated_options = func(caller, **kwargs)
else:
text, decorated_options = func(caller)
except Exception:
raise
except Exception:
logger.log_trace()
else:
if isinstance(decorated_options, {}):
if isinstance(decorated_options, dict):
decorated_options = [decorated_options]
else:
decorated_options = make_iter(decorated_options)
extra_options = []
if isinstance(decorated_options, dict):
decorated_options = [decorated_options]
for eopt in decorated_options:
cback = ("goto" in eopt and "goto") or ("exec" in eopt and "exec") or None
if cback:

View file

@ -122,7 +122,8 @@ class EvMore(object):
"""
def __init__(self, caller, text, always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=False, **kwargs):
justify_kwargs=None, exit_on_lastpage=False,
exit_cmd=None, **kwargs):
"""
Initialization of the text handler.
@ -141,6 +142,10 @@ class EvMore(object):
page being completely filled, exit pager immediately. If unset,
another move forward is required to exit. If set, the pager
exit message will not be shown.
exit_cmd (str, optional): If given, this command-string will be executed on
the caller when the more page exits. Note that this will be using whatever
cmdset the user had *before* the evmore pager was activated (so none of
the evmore commands will be available when this is run).
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
@ -151,6 +156,7 @@ class EvMore(object):
self._npages = []
self._npos = []
self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager."
if not session:
# if not supplied, use the first session to
@ -202,15 +208,18 @@ class EvMore(object):
# goto top of the text
self.page_top()
def display(self):
def display(self, show_footer=True):
"""
Pretty-print the page.
"""
pos = self._pos
text = self._pages[pos]
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
if show_footer:
page = _DISPLAY.format(text=text,
pageno=pos + 1,
pagemax=self._npages)
else:
page = text
# check to make sure our session is still valid
sessions = self._caller.sessions.get()
if not sessions:
@ -245,9 +254,11 @@ class EvMore(object):
self.page_quit()
else:
self._pos += 1
self.display()
if self.exit_on_lastpage and self._pos >= self._npages - 1:
self.page_quit()
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
self.display(show_footer=False)
self.page_quit(quiet=True)
else:
self.display()
def page_back(self):
"""
@ -256,16 +267,20 @@ class EvMore(object):
self._pos = max(0, self._pos - 1)
self.display()
def page_quit(self):
def page_quit(self, quiet=False):
"""
Quit the pager
"""
del self._caller.ndb._more
self._caller.msg(text=self._exit_msg, **self._kwargs)
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore)
if self.exit_cmd:
self._caller.execute_cmd(self.exit_cmd, session=self._session)
def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs):
def msg(caller, text="", always_page=False, session=None,
justify_kwargs=None, exit_on_lastpage=True, **kwargs):
"""
More-supported version of msg, mimicking the normal msg method.
@ -280,9 +295,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, *
justify_kwargs (dict, bool or None, optional): If given, this should
be valid keyword arguments to the utils.justify() function. If False,
no justification will be done.
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
kwargs (any, optional): These will be passed on
to the `caller.msg` method.
"""
EvMore(caller, text, always_page=always_page, session=session,
justify_kwargs=justify_kwargs, **kwargs)
justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)

View file

@ -1,467 +1,528 @@
"""
Inline functions (nested form).
This parser accepts nested inlinefunctions on the form
```
$funcname(arg, arg, ...)
```
embedded in any text where any arg can be another $funcname{} call.
This functionality is turned off by default - to activate,
`settings.INLINEFUNC_ENABLED` must be set to `True`.
Each token starts with "$funcname(" where there must be no space
between the $funcname and (. It ends with a matched ending parentesis.
")".
Inside the inlinefunc definition, one can use `\` to escape. This is
mainly needed for escaping commas in flowing text (which would
otherwise be interpreted as an argument separator), or to escape `}`
when not intended to close the function block. Enclosing text in
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
also escape *everything* within without needing to escape individual
characters.
The available inlinefuncs are defined as global-level functions in
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
by their function name (and ignored if this name starts with `_`). They
should be on the following form:
```python
def funcname (*args, **kwargs):
# ...
```
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
`*args` tuple. This will be populated by the arguments given to the
inlinefunc in-game - the only part that will be available from
in-game. `**kwargs` are not supported from in-game but are only used
internally by Evennia to make details about the caller available to
the function. The kwarg passed to all functions is `session`, the
Sessionobject for the object seeing the string. This may be `None` if
the string is sent to a non-puppetable object. The inlinefunc should
never raise an exception.
There are two reserved function names:
- "nomatch": This is called if the user uses a functionname that is
not registered. The nomatch function will get the name of the
not-found function as its first argument followed by the normal
arguments to the given function. If not defined the default effect is
to print `<UNKNOWN>` to replace the unknown function.
- "stackfull": This is called when the maximum nested function stack is reached.
When this happens, the original parsed string is returned and the result of
the `stackfull` inlinefunc is appended to the end. By default this is an
error message.
Error handling:
Syntax errors, notably not completely closing all inlinefunc
blocks, will lead to the entire string remaining unparsed.
"""
import re
from django.conf import settings
from evennia.utils import utils
# example/testing inline functions
def pad(*args, **kwargs):
"""
Inlinefunc. Pads text to given width.
Args:
text (str, optional): Text to pad.
width (str, optional): Will be converted to integer. Width
of padding.
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
fillchar (str, optional): Character used for padding. Defaults to a space.
Kwargs:
session (Session): Session performing the pad.
Example:
`$pad(text, width, align, fillchar)`
"""
text, width, align, fillchar = "", 78, 'c', ' '
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
if nargs > 3:
fillchar = args[3]
return utils.pad(text, width=width, align=align, fillchar=fillchar)
def crop(*args, **kwargs):
"""
Inlinefunc. Crops ingoing text to given widths.
Args:
text (str, optional): Text to crop.
width (str, optional): Will be converted to an integer. Width of
crop in characters.
suffix (str, optional): End string to mark the fact that a part
of the string was cropped. Defaults to `[...]`.
Kwargs:
session (Session): Session performing the crop.
Example:
`$crop(text, width=78, suffix='[...]')`
"""
text, width, suffix = "", 78, "[...]"
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
suffix = args[2]
return utils.crop(text, width=width, suffix=suffix)
def clr(*args, **kwargs):
"""
Inlinefunc. Colorizes nested text.
Args:
startclr (str, optional): An ANSI color abbreviation without the
prefix `|`, such as `r` (red foreground) or `[r` (red background).
text (str, optional): Text
endclr (str, optional): The color to use at the end of the string. Defaults
to `|n` (reset-color).
Kwargs:
session (Session): Session object triggering inlinefunc.
Example:
`$clr(startclr, text, endclr)`
"""
text = ""
nargs = len(args)
if nargs > 0:
color = args[0].strip()
if nargs > 1:
text = args[1]
text = "|" + color + text
if nargs > 2:
text += "|" + args[2].strip()
else:
text += "|n"
return text
# we specify a default nomatch function to use if no matching func was
# found. This will be overloaded by any nomatch function defined in
# the imported modules.
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"}
# load custom inline func modules.
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
try:
_INLINE_FUNCS.update(utils.callables_from_module(module))
except ImportError as err:
if module == "server.conf.inlinefuncs":
# a temporary warning since the default module changed name
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
"be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err)
else:
raise
# remove the core function if we include examples in this module itself
#_INLINE_FUNCS.pop("inline_func_parse", None)
# The stack size is a security measure. Set to <=0 to disable.
try:
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
except AttributeError:
_STACK_MAXSIZE = 20
# regex definitions
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call)
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/1
_RE_TOKEN = re.compile(r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
(?P<comma>(?<!\\)\,)| # , (argument sep)
(?P<end>(?<!\\)\))| # ) (end of func call)
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
(?P<escaped> # escaped tokens to re-insert sans backslash
\\\'|\\\"|\\\)|\\\$\w+\()|
(?P<rest> # everything else to re-insert verbatim
\$(?!\w+\()|\'{1}|\"{1}|\\{1}|[^),$\'\"\\]+)""",
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
# Cache for function lookups.
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
class ParseStack(list):
"""
Custom stack that always concatenates strings together when the
strings are added next to one another. Tuples are stored
separately and None is used to mark that a string should be broken
up into a new chunk. Below is the resulting stack after separately
appending 3 strings, None, 2 strings, a tuple and finally 2
strings:
[string + string + string,
None
string + string,
tuple,
string + string]
"""
def __init__(self, *args, **kwargs):
super(ParseStack, self).__init__(*args, **kwargs)
# always start stack with the empty string
list.append(self, "")
# indicates if the top of the stack is a string or not
self._string_last = True
def __eq__(self, other):
return (super(ParseStack).__eq__(other) and
hasattr(other, "_string_last") and self._string_last == other._string_last)
def __ne__(self, other):
return not self.__eq__(other)
def append(self, item):
"""
The stack will merge strings, add other things as normal
"""
if isinstance(item, basestring):
if self._string_last:
self[-1] += item
else:
list.append(self, item)
self._string_last = True
else:
# everything else is added as normal
list.append(self, item)
self._string_last = False
class InlinefuncError(RuntimeError):
pass
def parse_inlinefunc(string, strip=False, **kwargs):
"""
Parse the incoming string.
Args:
string (str): The incoming string to parse.
strip (bool, optional): Whether to strip function calls rather than
execute them.
Kwargs:
session (Session): This is sent to this function by Evennia when triggering
it. It is passed to the inlinefunc.
kwargs (any): All other kwargs are also passed on to the inlinefunc.
"""
global _PARSING_CACHE
if string in _PARSING_CACHE:
# stack is already cached
stack = _PARSING_CACHE[string]
elif not _RE_STARTTOKEN.search(string):
# if there are no unescaped start tokens at all, return immediately.
return string
else:
# no cached stack; build a new stack and continue
stack = ParseStack()
# process string on stack
ncallable = 0
for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict()
if gdict["singlequote"]:
stack.append(gdict["singlequote"])
elif gdict["doublequote"]:
stack.append(gdict["doublequote"])
elif gdict["end"]:
if ncallable <= 0:
stack.append(")")
continue
args = []
while stack:
operation = stack.pop()
if callable(operation):
if not strip:
stack.append((operation, [arg for arg in reversed(args)]))
ncallable -= 1
break
else:
args.append(operation)
elif gdict["start"]:
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
try:
# try to fetch the matching inlinefunc from storage
stack.append(_INLINE_FUNCS[funcname])
except KeyError:
stack.append(_INLINE_FUNCS["nomatch"])
stack.append(funcname)
ncallable += 1
elif gdict["escaped"]:
# escaped tokens
token = gdict["escaped"].lstrip("\\")
stack.append(token)
elif gdict["comma"]:
if ncallable > 0:
# commas outside strings and inside a callable are
# used to mark argument separation - we use None
# in the stack to indicate such a separation.
stack.append(None)
else:
# no callable active - just a string
stack.append(",")
else:
# the rest
stack.append(gdict["rest"])
if ncallable > 0:
# this means not all inlinefuncs were complete
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
# if stack is larger than limit, throw away parsing
return string + gdict["stackfull"](*args, **kwargs)
else:
# cache the stack
_PARSING_CACHE[string] = stack
# run the stack recursively
def _run_stack(item, depth=0):
retval = item
if isinstance(item, tuple):
if strip:
return ""
else:
func, arglist = item
args = [""]
for arg in arglist:
if arg is None:
# an argument-separating comma - start a new arg
args.append("")
else:
# all other args should merge into one string
args[-1] += _run_stack(arg, depth=depth + 1)
# execute the inlinefunc at this point or strip it.
kwargs["inlinefunc_stack_depth"] = depth
retval = "" if strip else func(*args, **kwargs)
return utils.to_str(retval, force_string=True)
# execute the stack from the cache
return "".join(_run_stack(item) for item in _PARSING_CACHE[string])
#
# Nick templating
#
"""
This supports the use of replacement templates in nicks:
This happens in two steps:
1) The user supplies a template that is converted to a regex according
to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given.
@desc $1 $2 $3
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
? matches any single character ->
[seq] matches any entry in sequence
[!seq] matches entries not in sequence
Custom arg markers
$N argument position (1-99)
"""
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
"""
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition.
out_template (str): The template to be used to replace the string
matched by the in_template.
Returns:
regex (regex): Regex to match against strings
template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
NickTemplateInvalid: If the in/out template does not have a matching
number of $args.
"""
# create the regex for in_template
regex_string = fnmatch.translate(in_template)
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
# create the out_template
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs:
print n_inargs, n_outargs
raise NickTemplateInvalid
return re.compile(regex_string), template_string
def parse_nick_template(string, template_regex, outtemplate):
"""
Parse a text using a template and map it to another template
Args:
string (str): The input string to processj
template_regex (regex): A template regex created with
initialize_nick_template.
outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2,
etc to match the regex.
"""
match = template_regex.match(string)
if match:
return outtemplate.format(**match.groupdict())
return string
"""
Inline functions (nested form).
This parser accepts nested inlinefunctions on the form
```
$funcname(arg, arg, ...)
```
embedded in any text where any arg can be another $funcname{} call.
This functionality is turned off by default - to activate,
`settings.INLINEFUNC_ENABLED` must be set to `True`.
Each token starts with "$funcname(" where there must be no space
between the $funcname and (. It ends with a matched ending parentesis.
")".
Inside the inlinefunc definition, one can use `\` to escape. This is
mainly needed for escaping commas in flowing text (which would
otherwise be interpreted as an argument separator), or to escape `}`
when not intended to close the function block. Enclosing text in
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
also escape *everything* within without needing to escape individual
characters.
The available inlinefuncs are defined as global-level functions in
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
by their function name (and ignored if this name starts with `_`). They
should be on the following form:
```python
def funcname (*args, **kwargs):
# ...
```
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
`*args` tuple. This will be populated by the arguments given to the
inlinefunc in-game - the only part that will be available from
in-game. `**kwargs` are not supported from in-game but are only used
internally by Evennia to make details about the caller available to
the function. The kwarg passed to all functions is `session`, the
Sessionobject for the object seeing the string. This may be `None` if
the string is sent to a non-puppetable object. The inlinefunc should
never raise an exception.
There are two reserved function names:
- "nomatch": This is called if the user uses a functionname that is
not registered. The nomatch function will get the name of the
not-found function as its first argument followed by the normal
arguments to the given function. If not defined the default effect is
to print `<UNKNOWN>` to replace the unknown function.
- "stackfull": This is called when the maximum nested function stack is reached.
When this happens, the original parsed string is returned and the result of
the `stackfull` inlinefunc is appended to the end. By default this is an
error message.
Error handling:
Syntax errors, notably not completely closing all inlinefunc
blocks, will lead to the entire string remaining unparsed.
"""
import re
import fnmatch
from django.conf import settings
from evennia.utils import utils, logger
# example/testing inline functions
def pad(*args, **kwargs):
"""
Inlinefunc. Pads text to given width.
Args:
text (str, optional): Text to pad.
width (str, optional): Will be converted to integer. Width
of padding.
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
fillchar (str, optional): Character used for padding. Defaults to a space.
Kwargs:
session (Session): Session performing the pad.
Example:
`$pad(text, width, align, fillchar)`
"""
text, width, align, fillchar = "", 78, 'c', ' '
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
if nargs > 3:
fillchar = args[3]
return utils.pad(text, width=width, align=align, fillchar=fillchar)
def crop(*args, **kwargs):
"""
Inlinefunc. Crops ingoing text to given widths.
Args:
text (str, optional): Text to crop.
width (str, optional): Will be converted to an integer. Width of
crop in characters.
suffix (str, optional): End string to mark the fact that a part
of the string was cropped. Defaults to `[...]`.
Kwargs:
session (Session): Session performing the crop.
Example:
`$crop(text, width=78, suffix='[...]')`
"""
text, width, suffix = "", 78, "[...]"
nargs = len(args)
if nargs > 0:
text = args[0]
if nargs > 1:
width = int(args[1]) if args[1].strip().isdigit() else 78
if nargs > 2:
suffix = args[2]
return utils.crop(text, width=width, suffix=suffix)
def clr(*args, **kwargs):
"""
Inlinefunc. Colorizes nested text.
Args:
startclr (str, optional): An ANSI color abbreviation without the
prefix `|`, such as `r` (red foreground) or `[r` (red background).
text (str, optional): Text
endclr (str, optional): The color to use at the end of the string. Defaults
to `|n` (reset-color).
Kwargs:
session (Session): Session object triggering inlinefunc.
Example:
`$clr(startclr, text, endclr)`
"""
text = ""
nargs = len(args)
if nargs > 0:
color = args[0].strip()
if nargs > 1:
text = args[1]
text = "|" + color + text
if nargs > 2:
text += "|" + args[2].strip()
else:
text += "|n"
return text
def null(*args, **kwargs):
return args[0] if args else ''
def nomatch(name, *args, **kwargs):
"""
Default implementation of nomatch returns the function as-is as a string.
"""
kwargs.pop("inlinefunc_stack_depth", None)
kwargs.pop("session")
return "${name}({args}{kwargs})".format(
name=name,
args=",".join(args),
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()))
_INLINE_FUNCS = {}
# we specify a default nomatch function to use if no matching func was
# found. This will be overloaded by any nomatch function defined in
# the imported modules.
_DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: "}
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
# load custom inline func modules.
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
try:
_INLINE_FUNCS.update(utils.callables_from_module(module))
except ImportError as err:
if module == "server.conf.inlinefuncs":
# a temporary warning since the default module changed name
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
"be renamed to mygame/server/conf/inlinefuncs.py (note "
"the S at the end)." % err)
else:
raise
# The stack size is a security measure. Set to <=0 to disable.
try:
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
except AttributeError:
_STACK_MAXSIZE = 20
# regex definitions
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
_RE_TOKEN = re.compile(r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
(?P<comma>(?<!\\)\,)| # , (argument sep)
(?P<end>(?<!\\)\))| # ) (possible end of func call)
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
(?P<escaped> # escaped tokens to re-insert sans backslash
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
(?P<rest> # everything else to re-insert verbatim
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
# Cache for function lookups.
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
class ParseStack(list):
"""
Custom stack that always concatenates strings together when the
strings are added next to one another. Tuples are stored
separately and None is used to mark that a string should be broken
up into a new chunk. Below is the resulting stack after separately
appending 3 strings, None, 2 strings, a tuple and finally 2
strings:
[string + string + string,
None
string + string,
tuple,
string + string]
"""
def __init__(self, *args, **kwargs):
super(ParseStack, self).__init__(*args, **kwargs)
# always start stack with the empty string
list.append(self, "")
# indicates if the top of the stack is a string or not
self._string_last = True
def __eq__(self, other):
return (super(ParseStack).__eq__(other) and
hasattr(other, "_string_last") and self._string_last == other._string_last)
def __ne__(self, other):
return not self.__eq__(other)
def append(self, item):
"""
The stack will merge strings, add other things as normal
"""
if isinstance(item, basestring):
if self._string_last:
self[-1] += item
else:
list.append(self, item)
self._string_last = True
else:
# everything else is added as normal
list.append(self, item)
self._string_last = False
class InlinefuncError(RuntimeError):
pass
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
"""
Parse the incoming string.
Args:
string (str): The incoming string to parse.
strip (bool, optional): Whether to strip function calls rather than
execute them.
available_funcs (dict, optional): Define an alternative source of functions to parse for.
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
stacktrace (bool, optional): If set, print the stacktrace to log.
Kwargs:
session (Session): This is sent to this function by Evennia when triggering
it. It is passed to the inlinefunc.
kwargs (any): All other kwargs are also passed on to the inlinefunc.
"""
global _PARSING_CACHE
usecache = False
if not available_funcs:
available_funcs = _INLINE_FUNCS
usecache = True
else:
# make sure the default keys are available, but also allow overriding
tmp = _DEFAULT_FUNCS.copy()
tmp.update(available_funcs)
available_funcs = tmp
if usecache and string in _PARSING_CACHE:
# stack is already cached
stack = _PARSING_CACHE[string]
elif not _RE_STARTTOKEN.search(string):
# if there are no unescaped start tokens at all, return immediately.
return string
else:
# no cached stack; build a new stack and continue
stack = ParseStack()
# process string on stack
ncallable = 0
nlparens = 0
nvalid = 0
if stacktrace:
out = "STRING: {} =>".format(string)
print(out)
logger.log_info(out)
for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict()
if stacktrace:
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
print(out)
logger.log_info(out)
if gdict["singlequote"]:
stack.append(gdict["singlequote"])
elif gdict["doublequote"]:
stack.append(gdict["doublequote"])
elif gdict["leftparens"]:
# we have a left-parens inside a callable
if ncallable:
nlparens += 1
stack.append("(")
elif gdict["end"]:
if nlparens > 0:
nlparens -= 1
stack.append(")")
continue
if ncallable <= 0:
stack.append(")")
continue
args = []
while stack:
operation = stack.pop()
if callable(operation):
if not strip:
stack.append((operation, [arg for arg in reversed(args)]))
ncallable -= 1
break
else:
args.append(operation)
elif gdict["start"]:
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
try:
# try to fetch the matching inlinefunc from storage
stack.append(available_funcs[funcname])
nvalid += 1
except KeyError:
stack.append(available_funcs["nomatch"])
stack.append(funcname)
stack.append(None)
ncallable += 1
elif gdict["escaped"]:
# escaped tokens
token = gdict["escaped"].lstrip("\\")
stack.append(token)
elif gdict["comma"]:
if ncallable > 0:
# commas outside strings and inside a callable are
# used to mark argument separation - we use None
# in the stack to indicate such a separation.
stack.append(None)
else:
# no callable active - just a string
stack.append(",")
else:
# the rest
stack.append(gdict["rest"])
if ncallable > 0:
# this means not all inlinefuncs were complete
return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# if stack is larger than limit, throw away parsing
return string + available_funcs["stackfull"](*args, **kwargs)
elif usecache:
# cache the stack - we do this also if we don't check the cache above
_PARSING_CACHE[string] = stack
# run the stack recursively
def _run_stack(item, depth=0):
retval = item
if isinstance(item, tuple):
if strip:
return ""
else:
func, arglist = item
args = [""]
for arg in arglist:
if arg is None:
# an argument-separating comma - start a new arg
args.append("")
else:
# all other args should merge into one string
args[-1] += _run_stack(arg, depth=depth + 1)
# execute the inlinefunc at this point or strip it.
kwargs["inlinefunc_stack_depth"] = depth
retval = "" if strip else func(*args, **kwargs)
return utils.to_str(retval, force_string=True)
retval = "".join(_run_stack(item) for item in stack)
if stacktrace:
out = "STACK: \n{} => {}\n".format(stack, retval)
print(out)
logger.log_info(out)
# execute the stack
return retval
#
# Nick templating
#
"""
This supports the use of replacement templates in nicks:
This happens in two steps:
1) The user supplies a template that is converted to a regex according
to the unix-like templating language.
2) This regex is tested against nicks depending on which nick replacement
strategy is considered (most commonly inputline).
3) If there is a template match and there are templating markers,
these are replaced with the arguments actually given.
@desc $1 $2 $3
This will be converted to the following regex:
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
Supported template markers (through fnmatch)
* matches anything (non-greedy) -> .*?
? matches any single character ->
[seq] matches any entry in sequence
[!seq] matches entries not in sequence
Custom arg markers
$N argument position (1-99)
"""
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
_RE_NICK_SPACE = re.compile(r"\\ ")
class NickTemplateInvalid(ValueError):
pass
def initialize_nick_templates(in_template, out_template):
"""
Initialize the nick templates for matching and remapping a string.
Args:
in_template (str): The template to be used for nick recognition.
out_template (str): The template to be used to replace the string
matched by the in_template.
Returns:
regex (regex): Regex to match against strings
template (str): Template with markers {arg1}, {arg2}, etc for
replacement using the standard .format method.
Raises:
NickTemplateInvalid: If the in/out template does not have a matching
number of $args.
"""
# create the regex for in_template
regex_string = fnmatch.translate(in_template)
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
# create the out_template
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
# validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs:
raise NickTemplateInvalid
return re.compile(regex_string), template_string
def parse_nick_template(string, template_regex, outtemplate):
"""
Parse a text using a template and map it to another template
Args:
string (str): The input string to processj
template_regex (regex): A template regex created with
initialize_nick_template.
outtemplate (str): The template to which to map the matches
produced by the template_regex. This should have $1, $2,
etc to match the regex.
"""
match = template_regex.match(string)
if match:
return outtemplate.format(**match.groupdict())
return string

View file

@ -1,342 +0,0 @@
"""
Spawner
The spawner takes input files containing object definitions in
dictionary forms. These use a prototype architecture 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:
```python
GOBLIN = {
"typeclass": "types.objects.Monster",
"key": "goblin grunt",
"health": lambda: randint(20,30),
"resists": ["cold", "poison"],
"attacks": ["fists"],
"weaknesses": ["fire", "light"]
"tags": ["mob", "evil", ('greenskin','mob')]
"args": [("weapon", "sword")]
}
```
Possible keywords are:
prototype - string parent prototype
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
exec - this is a string of python code to execute or a list of such codes.
This can be used e.g. to trigger custom handlers on the object. The
execution namespace contains 'evennia' for the library and 'obj'
tags - string or list of strings or tuples `(tagstr, category)`. Plain
strings will be result in tags with no category (default tags).
attrs - tuple or list of tuples of Attributes to add. This form allows
more complex Attributes to be set. Tuples at least specify `(key, value)`
but can also specify up to `(key, value, category, lockstring)`. If
you want to specify a lockstring but not a category, set the category
to `None`.
ndb_<name> - value of a nattribute (ndb_ is stripped)
other - any other name is interpreted as the key of an Attribute with
its value. Such Attributes have no categories.
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. Note, if you want to store
a callable in an Attribute, embed it in a tuple to the `args` keyword.
By specifying the "prototype" key, the prototype becomes a child of
that prototype, inheritng all prototype slots it does not explicitly
define itself, while overloading those that it does specify.
```python
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.
```python
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*.
"""
from __future__ import print_function
import copy
from django.conf import settings
from random import randint
import evennia
from evennia.objects.models import ObjectDB
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
def _handle_dbref(inp):
return dbid_to_obj(inp, ObjectDB)
def _validate_prototype(key, prototype, protparents, visited):
"""
Run validation on a prototype, checking for inifinite regress.
"""
assert isinstance(prototype, dict)
if id(prototype) in visited:
raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype)
visited.append(id(prototype))
protstrings = prototype.get("prototype")
if protstrings:
for protstring in make_iter(protstrings):
if key is not None and protstring == key:
raise RuntimeError("%s tries to prototype itself." % key or prototype)
protparent = protparents.get(protstring)
if not protparent:
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
_validate_prototype(protstring, protparent, protparents, visited)
def _get_prototype(dic, prot, protparents):
"""
Recursively traverse a prototype dictionary, including multiple
inheritance. Use _validate_prototype before this, we don't check
for infinite recursion here.
"""
if "prototype" in dic:
# move backwards through the inheritance
for prototype in make_iter(dic["prototype"]):
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents)
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!
Args:
objsparams (tuple): Parameters for the respective creation/add
handlers in the following order:
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
- `aliases` (list): A list of alias strings for
adding with `new_object.aliases.batch_add(*aliases)`.
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
add with `new_obj.nattributes.add(*tuple)`.
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
adding with `new_obj.attributes.batch_add(*attributes)`.
- `tags` (list): list of tuples `(key, category)` for adding
with `new_obj.tags.batch_add(*tags)`.
- `execs` (list): Code strings to execute together with the creation
of each object. They will be executed with `evennia` and `obj`
(the newly created object) available in the namespace. Execution
will happend after all other properties have been assigned and
is intended for calling custom handlers etc.
for the respective creation/add handlers in the following
order: (create_kwargs, permissions, locks, aliases, nattributes,
attributes, tags, execs)
Returns:
objects (list): A list of created objects
Notes:
The `exec` list will execute arbitrary python code so don't allow this to be available to
unprivileged users!
"""
# bulk create all objects in one go
# unfortunately this doesn't work since bulk_create doesn't creates pks;
# the result would be duplicate objects at the next stage, so we comment
# it out for now:
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
objs = []
for iobj, obj in enumerate(dbobjs):
# call all setup hooks on each object
objparam = objparams[iobj]
# setup
obj._createdict = {"permissions": make_iter(objparam[1]),
"locks": objparam[2],
"aliases": make_iter(objparam[3]),
"nattributes": objparam[4],
"attributes": objparam[5],
"tags": make_iter(objparam[6])}
# this triggers all hooks
obj.save()
# run eventual extra code
for code in objparam[7]:
if code:
exec(code, {}, {"evennia": evennia, "obj": obj})
objs.append(obj)
return objs
def spawn(*prototypes, **kwargs):
"""
Spawn a number of prototyped objects.
Args:
prototypes (dict): Each argument should be a prototype
dictionary.
Kwargs:
prototype_modules (str or list): A python-path to a prototype
module, or a list of such paths. These will be used to build
the global protparents dictionary accessible by the input
prototypes. If not given, it will instead look for modules
defined by settings.PROTOTYPE_MODULES.
prototype_parents (dict): A dictionary holding a custom
prototype-parent dictionary. Will overload same-named
prototypes from prototype_modules.
return_prototypes (bool): Only return a list of the
prototype-parents (no object creation happens)
"""
protparents = {}
protmodules = make_iter(kwargs.get("prototype_modules", []))
if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"):
protmodules = make_iter(settings.PROTOTYPE_MODULES)
for prototype_module in protmodules:
protparents.update(dict((key, val) for key, val in
all_from_module(prototype_module).items() if isinstance(val, dict)))
# overload module's protparents with specifically given protparents
protparents.update(kwargs.get("prototype_parents", {}))
for key, prototype in protparents.items():
_validate_prototype(key, prototype, protparents, [])
if "return_prototypes" in kwargs:
# only return the parents
return copy.deepcopy(protparents)
objsparams = []
for prototype in prototypes:
_validate_prototype(None, prototype, protparents, [])
prot = _get_prototype(prototype, {}, protparents)
if not prot:
continue
# extract the keyword args we need to create the object itself. If we get a callable,
# call that to get the value (don't catch errors)
create_kwargs = {}
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
locval = prot.pop("location", None)
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
homval = prot.pop("home", settings.DEFAULT_HOME)
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
destval = prot.pop("destination", None)
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
# extract calls to handlers
permval = prot.pop("permissions", [])
permission_string = permval() if callable(permval) else permval
lockval = prot.pop("locks", "")
lock_string = lockval() if callable(lockval) else lockval
aliasval = prot.pop("aliases", "")
alias_string = aliasval() if callable(aliasval) else aliasval
tagval = prot.pop("tags", [])
tags = tagval() if callable(tagval) else tagval
attrval = prot.pop("attrs", [])
attributes = attrval() if callable(tagval) else attrval
exval = prot.pop("exec", "")
execs = make_iter(exval() if callable(exval) else exval)
# 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
simple_attributes = [(key, value()) if callable(value) else (key, value)
for key, value in prot.items() if not key.startswith("ndb_")]
attributes = attributes + simple_attributes
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS]
# pack for call into _batch_create_object
objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs))
return _batch_create_object(*objsparams)
if __name__ == "__main__":
# testing
protparents = {
"NOBODY": {},
# "INFINITE" : {
# "prototype":"INFINITE"
# },
"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(protparents["GOBLIN"],
protparents["GOBLIN_ARCHWIZARD"],
prototype_parents=protparents)])

View file

@ -58,7 +58,7 @@ class TestEvMenu(TestCase):
def _debug_output(self, indent, msg):
if self.debug_output:
print(" " * indent + msg)
print(" " * indent + ansi.strip_ansi(msg))
def _test_menutree(self, menu):
"""
@ -82,6 +82,8 @@ class TestEvMenu(TestCase):
self.assertIsNotNone(
bool(node_text),
"node: {}: node-text is None, which was not expected.".format(nodename))
if isinstance(node_text, tuple):
node_text, helptext = node_text
node_text = ansi.strip_ansi(node_text.strip())
self.assertTrue(
node_text.startswith(compare_text),
@ -168,6 +170,7 @@ class TestEvMenu(TestCase):
self.caller2.msg = MagicMock()
self.session = MagicMock()
self.session2 = MagicMock()
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
cmdset_mergetype=self.cmdset_mergetype,
cmdset_priority=self.cmdset_priority,

View file

@ -20,18 +20,20 @@ import textwrap
import random
from os.path import join as osjoin
from importlib import import_module
from inspect import ismodule, trace, getmembers, getmodule
from inspect import ismodule, trace, getmembers, getmodule, getmro
from collections import defaultdict, OrderedDict
from twisted.internet import threads, reactor, task
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.apps import apps
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR
try:
import cPickle as pickle
except ImportError:
@ -42,8 +44,6 @@ _GA = object.__getattribute__
_SA = object.__setattr__
_DA = object.__delattr__
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
def is_iter(iterable):
"""
@ -79,7 +79,7 @@ def make_iter(obj):
return not hasattr(obj, '__iter__') and [obj] or obj
def wrap(text, width=_DEFAULT_WIDTH, indent=0):
def wrap(text, width=None, indent=0):
"""
Safely wrap text to a certain number of characters.
@ -92,6 +92,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
text (str): Properly wrapped text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
if not text:
return ""
text = to_unicode(text)
@ -103,7 +104,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
fill = wrap
def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
def pad(text, width=None, align="c", fillchar=" "):
"""
Pads to a given width.
@ -118,6 +119,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
text (str): The padded text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
align = align if align in ('c', 'l', 'r') else 'c'
fillchar = fillchar[0] if fillchar else " "
if align == 'l':
@ -128,7 +130,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
return text.center(width, fillchar)
def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
def crop(text, width=None, suffix="[...]"):
"""
Crop text to a certain width, throwing away text from too-long
lines.
@ -146,7 +148,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
text (str): The cropped text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
utext = to_unicode(text)
ltext = len(utext)
if ltext <= width:
@ -157,12 +159,16 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
return to_str(utext)
def dedent(text):
def dedent(text, baseline_index=None):
"""
Safely clean all whitespace at the left of a paragraph.
Args:
text (str): The text to dedent.
baseline_index (int or None, optional): Which row to use as a 'base'
for the indentation. Lines will be dedented to this level but
no further. If None, indent so as to completely deindent the
least indented text.
Returns:
text (str): Dedented string.
@ -175,10 +181,17 @@ def dedent(text):
"""
if not text:
return ""
return textwrap.dedent(text)
if baseline_index is None:
return textwrap.dedent(text)
else:
lines = text.split('\n')
baseline = lines[baseline_index]
spaceremove = len(baseline) - len(baseline.lstrip(' '))
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
for line in lines)
def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
def justify(text, width=None, align="f", indent=0):
"""
Fully justify a text so that it fits inside `width`. When using
full justification (default) this will be done by padding between
@ -197,6 +210,7 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
justified (str): The justified and indented block of text.
"""
width = width if width else settings.CLIENT_DEFAULT_WIDTH
def _process_line(line):
"""
@ -208,18 +222,27 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
gap = " " # minimum gap between words
if line_rest > 0:
if align == 'l':
line[-1] += " " * line_rest
if line[-1] == "\n\n":
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
else:
line[-1] += " " * line_rest
elif align == 'r':
line[0] = " " * line_rest + line[0]
elif align == 'c':
pad = " " * (line_rest // 2)
line[0] = pad + line[0]
line[-1] = line[-1] + pad + " " * (line_rest % 2)
if line[-1] == "\n\n":
line[-1] += pad + " " * (line_rest % 2 - 1) + \
"\n" + " " * width + "\n" + " " * width
else:
line[-1] = line[-1] + pad + " " * (line_rest % 2)
else: # align 'f'
gap += " " * (line_rest // max(1, ngaps))
rest_gap = line_rest % max(1, ngaps)
for i in range(rest_gap):
line[i] += " "
elif not any(line):
return [" " * width]
return gap.join(line)
# split into paragraphs and words
@ -260,6 +283,62 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
return "\n".join([indentstring + line for line in lines])
def columnize(string, columns=2, spacing=4, align='l', width=None):
"""
Break a string into a number of columns, using as little
vertical space as possible.
Args:
string (str): The string to columnize.
columns (int, optional): The number of columns to use.
spacing (int, optional): How much space to have between columns.
width (int, optional): The max width of the columns.
Defaults to client's default width.
Returns:
columns (str): Text divided into columns.
Raises:
RuntimeError: If given invalid values.
"""
columns = max(1, columns)
spacing = max(1, spacing)
width = width if width else settings.CLIENT_DEFAULT_WIDTH
w_spaces = (columns - 1) * spacing
w_txt = max(1, width - w_spaces)
if w_spaces + columns > width: # require at least 1 char per column
raise RuntimeError("Width too small to fit columns")
colwidth = int(w_txt / (1.0 * columns))
# first make a single column which we then split
onecol = justify(string, width=colwidth, align=align)
onecol = onecol.split("\n")
nrows, dangling = divmod(len(onecol), columns)
nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)]
height = max(nrows)
cols = []
istart = 0
for irows in nrows:
cols.append(onecol[istart:istart+irows])
istart = istart + irows
for col in cols:
if len(col) < height:
col.append(" " * colwidth)
sep = " " * spacing
rows = []
for irow in range(height):
rows.append(sep.join(col[irow] for col in cols))
return "\n".join(rows)
def list_to_string(inlist, endsep="and", addquote=False):
"""
This pretty-formats a list as string output, adding an optional
@ -1546,6 +1625,7 @@ def format_table(table, extra_space=1):
Examples:
```python
ftable = format_table([[...], [...], ...])
for ir, row in enumarate(ftable):
if ir == 0:
# make first row white
@ -1879,3 +1959,29 @@ def get_game_dir_path():
else:
os.chdir(os.pardir)
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
def get_all_typeclasses(parent=None):
"""
List available typeclasses from all available modules.
Args:
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
from this parent.
Returns:
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
Notes:
This will dynamicall retrieve all abstract django models inheriting at any distance
from the TypedObject base (aka a Typeclass) so it will work fine with any custom
classes being added.
"""
from evennia.typeclasses.models import TypedObject
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
for model in apps.get_models() if TypedObject in getmro(model)}
if parent:
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
if inherits_from(typeclass, parent)}
return typeclasses

View file

@ -2,9 +2,11 @@
django > 1.10, < 2.0
twisted == 16.0.0
mock >= 1.0.1
pillow == 2.9.0
pytz
future >= 0.15.2
django-sekizai
inflect
mock >= 1.0.1
anything