mirror of
https://github.com/evennia/evennia.git
synced 2026-03-26 01:36:32 +01:00
Merge branch 'olc' into develop
This commit is contained in:
commit
4e34cac6c8
29 changed files with 6126 additions and 1057 deletions
64
CHANGELOG.md
64
CHANGELOG.md
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
||||
|
|
|
|||
|
|
@ -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,"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
145
evennia/prototypes/README.md
Normal file
145
evennia/prototypes/README.md
Normal 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.
|
||||
0
evennia/prototypes/__init__.py
Normal file
0
evennia/prototypes/__init__.py
Normal file
2399
evennia/prototypes/menus.py
Normal file
2399
evennia/prototypes/menus.py
Normal file
File diff suppressed because it is too large
Load diff
317
evennia/prototypes/protfuncs.py
Normal file
317
evennia/prototypes/protfuncs.py
Normal 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)]
|
||||
695
evennia/prototypes/prototypes.py
Normal file
695
evennia/prototypes/prototypes.py
Normal 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
|
||||
602
evennia/prototypes/spawner.py
Normal file
602
evennia/prototypes/spawner.py
Normal 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
531
evennia/prototypes/tests.py
Normal 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']]]
|
||||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)`
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)])
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue