Merge branch 'develop' into building_menu

This commit is contained in:
Vincent Le Goff 2018-09-02 12:33:22 +02:00
commit 415322fe1a
60 changed files with 10614 additions and 1566 deletions

View file

@ -1,6 +1,90 @@
# Evennia Changelog # Changelog
# Sept 2017: ## Evennia 0.8 (2018)
### Server/Portal
- Removed `evennia_runner`, completely refactor `evennia_launcher.py` (the 'evennia' program)
with different functionality).
- Both Portal/Server are now stand-alone processes (easy to run as daemon)
- Made Portal the AMP Server for starting/restarting the Server (the AMP client)
- Dynamic logging now happens using `evennia -l` rather than by interactive.
- Made AMP secure against erroneous HTTP requests on the wrong port (return error messages).
- The `evennia istart` option will start/switch the Server in foreground (interactive) mode, where it logs
to terminal and can be stopped with Ctrl-C. Using `evennia reload`, or reloading in-game, will
return Server to normal daemon operation.
### Prototype changes
- Moved evennia/utils/spawner.py into the new evennia/prototypes/ along with all new
functionality around prototypes.
- A new form of prototype - database-stored prototypes, editable from in-game, was added. 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.
- Spawning an object using a prototype will automatically assign a new tag to it, named the same as
the `prototype_key` and with the category `from_prototype`.
- 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.
- The OLC allows for updating all objects previously created using a given prototype with any
changes done.
### 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.
- Add `debug` option when creating menu - this turns of persistence and makes the `menudebug`
command available for examining the current menu state.
### Webclient
- Refactoring of webclient structure.
### 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.
### General
- Start structuring the `CHANGELOG` to list features in more detail.
- Inflection and grouping of multiple objects in default room (an box, three boxes)
### Contribs
- `Health Bar` (Tim Ashley Jenkins): Easily create colorful bars/meters.
- `Tree select` (Fluttersprite): Wrapper around EvMenu to easier create
a common form of menu from a string.
- `Turnbattle suite` (Tim Ashley Jenkins)- the old `turnbattle.py` was moved into its own
`turnbattle/` package and reworked with many different flavors of combat systems:
- `tb_basic` - The basic turnbattle system, with initiative/turn order attack/defense/damage.
- `tb_equip` - Adds weapon and armor, wielding, accuracy modifiers.
- `tb_items` - Extends `tb_equip` with item use with conditions/status effects.
- `tb_magic` - Extends `tb_equip` with spellcasting.
- `tb_range` - Adds system for abstract positioning and movement.
- Updates and some cleanup of existing contribs.
# Overviews
## Sept 2017:
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
'Account', rework the website template and a slew of other updates. 'Account', rework the website template and a slew of other updates.
Info on what changed and how to migrate is found here: Info on what changed and how to migrate is found here:

View file

@ -97,15 +97,15 @@ def funcname(a, b, c, d=False, **kwargs):
Args: Args:
a (str): This is a string argument that we can talk about a (str): This is a string argument that we can talk about
over multiple lines. over multiple lines.
b (int or str): Another argument b (int or str): Another argument.
c (list): A list argument c (list): A list argument.
d (bool, optional): An optional keyword argument d (bool, optional): An optional keyword argument.
Kwargs: Kwargs:
test (list): A test keyword test (list): A test keyword.
Returns: Returns:
e (str): The result of the function e (str): The result of the function.
Raises: Raises:
RuntimeException: If there is a critical error, RuntimeException: If there is a critical error,

View file

@ -21,20 +21,30 @@
# #
FROM alpine FROM alpine
MAINTAINER www.evennia.com LABEL maintainer="www.evennia.com"
# install compilation environment # install compilation environment
RUN apk update && apk add python py-pip python-dev py-setuptools gcc musl-dev jpeg-dev zlib-dev bash RUN apk update && apk add bash gcc jpeg-dev musl-dev procps py-pip \
py-setuptools py2-openssl python python-dev zlib-dev
# add the project source # add the files required for pip installation
ADD . /usr/src/evennia COPY ./setup.py /usr/src/evennia/
COPY ./requirements.txt /usr/src/evennia/
COPY ./evennia/VERSION.txt /usr/src/evennia/evennia/
COPY ./bin /usr/src/evennia/bin/
# install dependencies # install dependencies
RUN pip install --upgrade pip && pip install /usr/src/evennia --trusted-host pypi.python.org RUN pip install --upgrade pip && pip install -e /usr/src/evennia --trusted-host pypi.python.org
RUN pip install cryptography pyasn1 service_identity
# add the project source; this should always be done after all
# expensive operations have completed to avoid prematurely
# invalidating the build cache.
COPY . /usr/src/evennia
# add the game source when rebuilding a new docker image from inside # add the game source when rebuilding a new docker image from inside
# a game dir # a game dir
ONBUILD ADD . /usr/src/game ONBUILD COPY . /usr/src/game
# make the game source hierarchy persistent with a named volume. # make the game source hierarchy persistent with a named volume.
# mount on-disk game location here when using the container # mount on-disk game location here when using the container

View file

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

View file

@ -421,17 +421,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
kwargs["options"] = options kwargs["options"] = options
if text is not None:
if not (isinstance(text, basestring) or isinstance(text, tuple)): if not (isinstance(text, basestring) or isinstance(text, tuple)):
# sanitize text before sending across the wire # sanitize text before sending across the wire
try: try:
text = to_str(text, force_string=True) text = to_str(text, force_string=True)
except Exception: except Exception:
text = repr(text) text = repr(text)
kwargs['text'] = text
# session relay # session relay
sessions = make_iter(session) if session else self.sessions.all() sessions = make_iter(session) if session else self.sessions.all()
for session in sessions: for session in sessions:
session.data_out(text=text, **kwargs) session.data_out(**kwargs)
def execute_cmd(self, raw_string, session=None, **kwargs): def execute_cmd(self, raw_string, session=None, **kwargs):
""" """
@ -631,10 +633,31 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# this will only be set if the utils.create_account # this will only be set if the utils.create_account
# function was used to create the object. # function was used to create the object.
cdict = self._createdict 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"): if cdict.get("locks"):
self.locks.add(cdict["locks"]) self.locks.add(cdict["locks"])
if cdict.get("permissions"): if cdict.get("permissions"):
permissions = cdict["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 del self._createdict
self.permissions.batch_add(*permissions) self.permissions.batch_add(*permissions)
@ -775,7 +798,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
# any was deleted in the interim. # any was deleted in the interim.
self.db._playable_characters = [char for char in self.db._playable_characters if char] self.db._playable_characters = [char for char in self.db._playable_characters if char]
self.msg(self.at_look(target=self.db._playable_characters, self.msg(self.at_look(target=self.db._playable_characters,
session=session)) session=session), session=session)
def at_failed_login(self, session, **kwargs): def at_failed_login(self, session, **kwargs):
""" """

View file

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

View file

@ -296,9 +296,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
result (any): An instantiated Command or the input unmodified. result (any): An instantiated Command or the input unmodified.
""" """
try: if callable(cmd):
return cmd() return cmd()
except TypeError: else:
return cmd return cmd
def _duplicate(self): def _duplicate(self):

View file

@ -10,9 +10,10 @@ from evennia.objects.models import ObjectDB
from evennia.locks.lockhandler import LockException from evennia.locks.lockhandler import LockException
from evennia.commands.cmdhandler import get_and_merge_cmdsets from evennia.commands.cmdhandler import get_and_merge_cmdsets
from evennia.utils import create, utils, search 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.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 from evennia.utils.ansi import raw
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -26,12 +27,8 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy",
"CmdLock", "CmdExamine", "CmdFind", "CmdTeleport", "CmdLock", "CmdExamine", "CmdFind", "CmdTeleport",
"CmdScript", "CmdTag", "CmdSpawn") "CmdScript", "CmdTag", "CmdSpawn")
try:
# used by @set # used by @set
from ast import literal_eval as _LITERAL_EVAL from ast import literal_eval as _LITERAL_EVAL
except ImportError:
# literal_eval is not available before Python 2.6
_LITERAL_EVAL = None
# used by @find # used by @find
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
@ -1458,7 +1455,6 @@ def _convert_from_string(cmd, strobj):
# if nothing matches, return as-is # if nothing matches, return as-is
return obj return obj
if _LITERAL_EVAL:
# Use literal_eval to parse python structure exactly. # Use literal_eval to parse python structure exactly.
try: try:
return _LITERAL_EVAL(strobj) return _LITERAL_EVAL(strobj)
@ -1702,17 +1698,22 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
@typeclass[/switch] <object> [= typeclass.path] @typeclass[/switch] <object> [= typeclass.path]
@type '' @type ''
@parent '' @parent ''
@typeclass/list/show [typeclass.path]
@swap - this is a shorthand for using /force/reset flags. @swap - this is a shorthand for using /force/reset flags.
@update - this is a shorthand for using the /force/reload flag. @update - this is a shorthand for using the /force/reload flag.
Switch: 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 update - *only* re-run at_object_creation on this object
meaning locks or other properties set later may remain. meaning locks or other properties set later may remain.
reset - clean out *all* the attributes and properties on the reset - clean out *all* the attributes and properties on the
object - basically making this a new clean object. object - basically making this a new clean object.
force - change to the typeclass also if the object force - change to the typeclass also if the object
already has a typeclass of the same name. already has a typeclass of the same name.
list - show available typeclasses.
Example: Example:
@type button = examples.red_button.RedButton @type button = examples.red_button.RedButton
@ -1736,6 +1737,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
key = "@typeclass" key = "@typeclass"
aliases = ["@type", "@parent", "@swap", "@update"] aliases = ["@type", "@parent", "@swap", "@update"]
switch_options = ("show", "examine", "update", "reset", "force", "list")
locks = "cmd:perm(typeclass) or perm(Builder)" locks = "cmd:perm(typeclass) or perm(Builder)"
help_category = "Building" help_category = "Building"
@ -1744,10 +1746,56 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
caller = self.caller 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: if not self.args:
caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring) caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
return 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 # get object to swap on
obj = caller.search(self.lhs) obj = caller.search(self.lhs)
if not obj: if not obj:
@ -1760,7 +1808,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
new_typeclass = self.rhs or obj.path 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__) string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
caller.msg(string) caller.msg(string)
return return
@ -2179,11 +2227,14 @@ class CmdExamine(ObjManipCommand):
else: else:
things.append(content) things.append(content)
if exits: 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: 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: if things:
string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents 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]) if cont not in exits and cont not in pobjs])
separator = "-" * _DEFAULT_WIDTH separator = "-" * _DEFAULT_WIDTH
# output info # output info
@ -2275,6 +2326,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
char - only look for characters (BASE_CHARACTER_TYPECLASS) char - only look for characters (BASE_CHARACTER_TYPECLASS)
exact - only exact matches are returned. exact - only exact matches are returned.
loc - display object location if exists and match has one result loc - display object location if exists and match has one result
startswith - search for names starting with the string, rather than containing
Searches the database for an object of a particular name or exact #dbref. Searches the database for an object of a particular name or exact #dbref.
Use *accountname to search for an account. The switches allows for Use *accountname to search for an account. The switches allows for
@ -2285,7 +2337,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
key = "@find" key = "@find"
aliases = "@search, @locate" aliases = "@search, @locate"
switch_options = ("room", "exit", "char", "exact", "loc") switch_options = ("room", "exit", "char", "exact", "loc", "startswith")
locks = "cmd:perm(find) or perm(Builder)" locks = "cmd:perm(find) or perm(Builder)"
help_category = "Building" help_category = "Building"
@ -2359,10 +2411,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high) keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
aliasquery = Q(db_tags__db_key__iexact=searchstring, aliasquery = Q(db_tags__db_key__iexact=searchstring,
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
else: elif "startswith" in switches:
keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high) keyquery = Q(db_key__istartswith=searchstring, id__gte=low, id__lte=high)
aliasquery = Q(db_tags__db_key__istartswith=searchstring, aliasquery = Q(db_tags__db_key__istartswith=searchstring,
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high) db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
else:
keyquery = Q(db_key__icontains=searchstring, id__gte=low, id__lte=high)
aliasquery = Q(db_tags__db_key__icontains=searchstring,
db_tags__db_tagtype__iexact="alias", id__gte=low, id__lte=high)
results = ObjectDB.objects.filter(keyquery | aliasquery).distinct() results = ObjectDB.objects.filter(keyquery | aliasquery).distinct()
nresults = results.count() nresults = results.count()
@ -2733,101 +2789,312 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
string = "No tags attached to %s." % obj string = "No tags attached to %s." % obj
self.caller.msg(string) 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): class CmdSpawn(COMMAND_DEFAULT_CLASS):
""" """
spawn objects from prototype spawn objects from prototype
Usage: Usage:
@spawn @spawn[/noloc] <prototype_key>
@spawn[/switch] <prototype_name> @spawn[/noloc] <prototype_dict>
@spawn[/switch] {prototype dictionary}
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, noloc - allow location to be None if not specified explicitly. Otherwise,
location will default to caller's current location. 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: Example:
@spawn GOBLIN @spawn GOBLIN
@spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"} @spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
@spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
Dictionary keys: Dictionary keys:
|wprototype |n - name of parent prototype to use. Can be a list for |wprototype_parent |n - name of parent prototype to use. Required if typeclass is
multiple inheritance (inherits left to right) 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 |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 |wlocation |n - this should be a valid object or #dbref
|whome |n - valid object or #dbref |whome |n - valid object or #dbref
|wdestination|n - only valid for exits (object or dbref) |wdestination|n - only valid for exits (object or dbref)
|wpermissions|n - string or list of permission strings |wpermissions|n - string or list of permission strings
|wlocks |n - a lock-string |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) |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. any other keywords are interpreted as Attributes and their values.
The available prototypes are defined globally in modules set in The available prototypes are defined globally in modules set in
settings.PROTOTYPE_MODULES. If @spawn is used without arguments it settings.PROTOTYPE_MODULES. If @spawn is used without arguments it
displays a list of available prototypes. displays a list of available prototypes.
""" """
key = "@spawn" 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)" locks = "cmd:perm(spawn) or perm(Builder)"
help_category = "Building" help_category = "Building"
def func(self): def func(self):
"""Implements the spawner""" """Implements the spawner"""
def _show_prototypes(prototypes): def _parse_prototype(inp, expect=dict):
"""Helper to show a list of available prototypes""" err = None
prots = ", ".join(sorted(prototypes.keys())) try:
return "\nAvailable prototypes (case sensitive): %s" % ( prototype = _LITERAL_EVAL(inp)
"\n" + utils.fill(prots) if prots else "None") 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) 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 'search' in self.switches:
# query for a key match
if not self.args: if not self.args:
string = "Usage: @spawn {key:value, key, value, ... }" self.switches.append("list")
self.caller.msg(string + _show_prototypes(prototypes)) 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:
return
# 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
# 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 return
try: try:
# make use of _convert_from_string from the SetAttribute command success = protlib.delete_db_prototype(caller, self.args)
prototype = _convert_from_string(self, self.args) except protlib.PermissionError as err:
except SyntaxError: caller.msg("|rError deleting:|R {}|n".format(err))
# this means literal_eval tried to parse a faulty string caller.msg("Deletion {}.".format(
string = "|RCritical Python syntax error in argument. " 'successful' if success else 'failed (does the prototype exist?)'))
string += "Only primitive Python structures are allowed. "
string += "\nYou also need to use correct Python syntax. "
string += "Remember especially to put quotes around all "
string += "strings inside lists and dicts.|n"
self.caller.msg(string)
return
if isinstance(prototype, basestring):
# A prototype key
keystr = prototype
prototype = prototypes.get(prototype, None)
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.")
return return
else: else:
self.caller.msg("The prototype must be a prototype key or a Python dictionary.") 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 return
if "noloc" not in self.switches and "location" not in prototype: if "noloc" not in self.switches and "location" not in prototype:
prototype["location"] = self.caller.location prototype["location"] = self.caller.location
for obj in spawn(prototype): # proceed to spawning
try:
for obj in spawner.spawn(prototype):
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller)) self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
except RuntimeError as err:
caller.msg(err)

View file

@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet):
self.add(unloggedin.CmdUnconnectedHelp()) self.add(unloggedin.CmdUnconnectedHelp())
self.add(unloggedin.CmdUnconnectedEncoding()) self.add(unloggedin.CmdUnconnectedEncoding())
self.add(unloggedin.CmdUnconnectedScreenreader()) self.add(unloggedin.CmdUnconnectedScreenreader())
self.add(unloggedin.CmdUnconnectedInfo())

View file

@ -71,7 +71,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
target = caller.search(self.args) target = caller.search(self.args)
if not target: if not target:
return return
self.msg(caller.at_look(target)) self.msg((caller.at_look(target), {'type': 'look'}), options=None)
class CmdNick(COMMAND_DEFAULT_CLASS): class CmdNick(COMMAND_DEFAULT_CLASS):

View file

@ -14,24 +14,26 @@ main test suite started with
import re import re
import types import types
import datetime
from django.conf import settings from django.conf import settings
from mock import Mock, mock from mock import Mock, mock
from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.commands.default.cmdset_character import CharacterCmdSet
from evennia.utils.test_resources import EvenniaTest from evennia.utils.test_resources import EvenniaTest
from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms from evennia.commands.default import help, general, system, admin, account, building, batchprocess, comms, unloggedin
from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.muxcommand import MuxCommand
from evennia.commands.command import Command, InterruptCommand from evennia.commands.command import Command, InterruptCommand
from evennia.utils import ansi, utils from evennia.utils import ansi, utils, gametime
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
from evennia import search_object from evennia import search_object
from evennia import DefaultObject, DefaultCharacter from evennia import DefaultObject, DefaultCharacter
from evennia.prototypes import prototypes as protlib
# set up signal here since we are not starting the server # set up signal here since we are not starting the server
_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE) _RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
# ------------------------------------------------------------ # ------------------------------------------------------------
@ -44,7 +46,7 @@ class CommandTest(EvenniaTest):
Tests a command Tests a command
""" """
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None, 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 Test a command by assigning all the needed
properties to cmdobj and running properties to cmdobj and running
@ -73,27 +75,48 @@ class CommandTest(EvenniaTest):
cmdobj.obj = obj or (caller if caller else self.char1) cmdobj.obj = obj or (caller if caller else self.char1)
# test # test
old_msg = receiver.msg old_msg = receiver.msg
inputs = inputs or []
try: try:
receiver.msg = Mock() receiver.msg = Mock()
cmdobj.at_pre_cmd() if cmdobj.at_pre_cmd():
return
cmdobj.parse() cmdobj.parse()
ret = cmdobj.func() ret = cmdobj.func()
# handle func's with yield in them (generators)
if isinstance(ret, types.GeneratorType): if isinstance(ret, types.GeneratorType):
while True:
try:
inp = inputs.pop() if inputs else None
if inp:
try:
ret.send(inp)
except TypeError:
ret.next() ret.next()
ret = ret.send(inp)
else:
ret.next()
except StopIteration:
break
cmdobj.at_post_cmd() cmdobj.at_post_cmd()
except StopIteration: except StopIteration:
pass pass
except InterruptCommand: except InterruptCommand:
pass pass
finally:
# clean out evtable sugar. We only operate on text-type # clean out evtable sugar. We only operate on text-type
stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs, force_string=True)) stored_msg = [args[0] if args and args[0] else kwargs.get("text", utils.to_str(kwargs, force_string=True))
for name, args, kwargs in receiver.msg.mock_calls] for name, args, kwargs in receiver.msg.mock_calls]
# Get the first element of a tuple if msg received a tuple instead of a string # Get the first element of a tuple if msg received a tuple instead of a string
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg] stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
if msg is not None: if msg is not None:
returned_msg = "||".join(_RE.sub("", mess) for mess in stored_msg) # set our separator for returned messages based on parsing ansi or not
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip() msg_sep = "|" if noansi else "||"
# Have to strip ansi for each returned message for the regex to handle it correctly
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
for mess in stored_msg).strip()
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()): if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n" sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n" sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
@ -127,11 +150,11 @@ class TestGeneral(CommandTest):
def test_nick(self): def test_nick(self):
self.call(general.CmdNick(), "testalias = testaliasedstring1", self.call(general.CmdNick(), "testalias = testaliasedstring1",
"Inputlinenick 'testalias' mapped to 'testaliasedstring1'.") "Inputline-nick 'testalias' mapped to 'testaliasedstring1'.")
self.call(general.CmdNick(), "/account testalias = testaliasedstring2", self.call(general.CmdNick(), "/account testalias = testaliasedstring2",
"Accountnick 'testalias' mapped to 'testaliasedstring2'.") "Account-nick 'testalias' mapped to 'testaliasedstring2'.")
self.call(general.CmdNick(), "/object testalias = testaliasedstring3", self.call(general.CmdNick(), "/object testalias = testaliasedstring3",
"Objectnick 'testalias' mapped to 'testaliasedstring3'.") "Object-nick 'testalias' mapped to 'testaliasedstring3'.")
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias")) self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="account")) self.assertEqual(u"testaliasedstring2", self.char1.nicks.get("testalias", category="account"))
self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account")) self.assertEqual(None, self.char1.account.nicks.get("testalias", category="account"))
@ -194,7 +217,7 @@ class TestSystem(CommandTest):
self.call(system.CmdPy(), "1+2", ">>> 1+2|3") self.call(system.CmdPy(), "1+2", ">>> 1+2|3")
def test_scripts(self): def test_scripts(self):
self.call(system.CmdScripts(), "", "| dbref |") self.call(system.CmdScripts(), "", "dbref ")
def test_objects(self): def test_objects(self):
self.call(system.CmdObjects(), "", "Object subtype totals") self.call(system.CmdObjects(), "", "Object subtype totals")
@ -218,14 +241,14 @@ class TestAdmin(CommandTest):
self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...") self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")
def test_ban(self): def test_ban(self):
self.call(admin.CmdBan(), "Char", "NameBan char was added.") self.call(admin.CmdBan(), "Char", "Name-Ban char was added.")
class TestAccount(CommandTest): class TestAccount(CommandTest):
def test_ooc_look(self): def test_ooc_look(self):
if settings.MULTISESSION_MODE < 2: if settings.MULTISESSION_MODE < 2:
self.call(account.CmdOOCLook(), "", "You are outofcharacter (OOC).", caller=self.account) self.call(account.CmdOOCLook(), "", "You are out-of-character (OOC).", caller=self.account)
if settings.MULTISESSION_MODE == 2: if settings.MULTISESSION_MODE == 2:
self.call(account.CmdOOCLook(), "", "Account TestAccount (you are OutofCharacter)", caller=self.account) self.call(account.CmdOOCLook(), "", "Account TestAccount (you are OutofCharacter)", caller=self.account)
@ -282,8 +305,8 @@ class TestBuilding(CommandTest):
def test_attribute_commands(self): def test_attribute_commands(self):
self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'") self.call(building.CmdSetAttribute(), "Obj/test1=\"value1\"", "Created attribute Obj/test1 = 'value1'")
self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'") self.call(building.CmdSetAttribute(), "Obj2/test2=\"value2\"", "Created attribute Obj2/test2 = 'value2'")
self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 > Obj.test3") self.call(building.CmdMvAttr(), "Obj2/test2 = Obj/test3", "Moved Obj2.test2 -> Obj.test3")
self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 > Obj2.test3") self.call(building.CmdCpAttr(), "Obj/test1 = Obj2/test3", "Copied Obj.test1 -> Obj2.test3")
self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.") self.call(building.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
def test_name(self): def test_name(self):
@ -309,7 +332,7 @@ class TestBuilding(CommandTest):
def test_exit_commands(self): def test_exit_commands(self):
self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2") self.call(building.CmdOpen(), "TestExit1=Room2", "Created new Exit 'TestExit1' from Room to Room2")
self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 > Room (one way).") self.call(building.CmdLink(), "TestExit1=Room", "Link created TestExit1 -> Room (one way).")
self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.") self.call(building.CmdUnLink(), "TestExit1", "Former exit TestExit1 no longer links anywhere.")
def test_set_home(self): def test_set_home(self):
@ -327,9 +350,9 @@ class TestBuilding(CommandTest):
self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.") self.call(building.CmdLock(), "Obj = test:perm(Developer)", "Added lock 'test:perm(Developer)' to Obj.")
def test_find(self): def test_find(self):
self.call(building.CmdFind(), "Room2", "One Match") self.call(building.CmdFind(), "oom2", "One Match")
expect = "One Match(#1#7, loc):\n " +\ expect = "One Match(#1-#7, loc):\n " +\
"Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))" "Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))"
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate") self.call(building.CmdFind(), "Char2", expect, cmdstring="locate")
self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch
"locate: Ambiguous switch supplied: Did you mean /exit or /exact?|" + expect, "locate: Ambiguous switch supplied: Did you mean /exit or /exact?|" + expect,
@ -337,6 +360,7 @@ class TestBuilding(CommandTest):
self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate") self.call(building.CmdFind(), "Char2", expect, cmdstring="@locate")
self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc self.call(building.CmdFind(), "/l Char2", expect, cmdstring="find") # /l switch is abbreviated form of /loc
self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find") self.call(building.CmdFind(), "Char2", "One Match", cmdstring="@find")
self.call(building.CmdFind(), "/startswith Room2", "One Match")
def test_script(self): def test_script(self):
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added") self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
@ -344,7 +368,7 @@ class TestBuilding(CommandTest):
def test_teleport(self): def test_teleport(self):
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.") self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.")
self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone self.call(building.CmdTeleport(), "/t", # /t switch is abbreviated form of /tonone
"Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a Nonelocation.") "Cannot teleport a puppeted object (Char, puppeted by TestAccount(account 1)) to a None-location.")
self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc self.call(building.CmdTeleport(), "/l Room2", # /l switch is abbreviated form of /loc
"Destination has no location.") "Destination has no location.")
self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet self.call(building.CmdTeleport(), "/q me to Room2", # /q switch is abbreviated form of /quiet
@ -356,6 +380,7 @@ class TestBuilding(CommandTest):
# check that it exists in the process. # check that it exists in the process.
query = search_object(objKeyStr) query = search_object(objKeyStr)
commandTest.assertIsNotNone(query) commandTest.assertIsNotNone(query)
commandTest.assertTrue(bool(query))
obj = query[0] obj = query[0]
commandTest.assertIsNotNone(obj) commandTest.assertIsNotNone(obj)
return obj return obj
@ -364,17 +389,20 @@ class TestBuilding(CommandTest):
self.call(building.CmdSpawn(), " ", "Usage: @spawn") self.call(building.CmdSpawn(), " ", "Usage: @spawn")
# Tests "@spawn <prototype_dictionary>" without specifying location. # Tests "@spawn <prototype_dictionary>" without specifying location.
self.call(building.CmdSpawn(), self.call(building.CmdSpawn(),
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin") "/save {'prototype_key': 'testprot', 'key':'Test Char', "
goblin = getObject(self, "goblin") "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
"Saved prototype: testprot", inputs=['y'])
# Tests that the spawned object's type is a DefaultCharacter. self.call(building.CmdSpawn(), "/list", "Key ")
self.assertIsInstance(goblin, DefaultCharacter)
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
# Tests that the spawned object's location is the same as the caharacter's location, since # Tests that the spawned object's location is the same as the caharacter's location, since
# we did not specify it. # we did not specify it.
self.assertEqual(goblin.location, self.char1.location) testchar = getObject(self, "Test Char")
goblin.delete() self.assertEqual(testchar.location, self.char1.location)
testchar.delete()
# Test "@spawn <prototype_dictionary>" with a location other than the character's. # Test "@spawn <prototype_dictionary>" with a location other than the character's.
spawnLoc = self.room2 spawnLoc = self.room2
@ -384,14 +412,23 @@ class TestBuilding(CommandTest):
spawnLoc = self.room1 spawnLoc = self.room1
self.call(building.CmdSpawn(), self.call(building.CmdSpawn(),
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}" "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
% spawnLoc.dbref, "Spawned goblin") "'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin")
goblin = getObject(self, "goblin") goblin = getObject(self, "goblin")
# Tests that the spawned object's type is a DefaultCharacter.
self.assertIsInstance(goblin, DefaultCharacter)
self.assertEqual(goblin.location, spawnLoc) self.assertEqual(goblin.location, spawnLoc)
goblin.delete() goblin.delete()
# create prototype
protlib.create_prototype(**{'key': 'Ball',
'typeclass': 'evennia.objects.objects.DefaultCharacter',
'prototype_key': 'testball'})
# Tests "@spawn <prototype_name>" # Tests "@spawn <prototype_name>"
self.call(building.CmdSpawn(), "'BALL'", "Spawned Ball") self.call(building.CmdSpawn(), "testball", "Spawned Ball")
ball = getObject(self, "Ball") ball = getObject(self, "Ball")
self.assertEqual(ball.location, self.char1.location) self.assertEqual(ball.location, self.char1.location)
self.assertIsInstance(ball, DefaultObject) self.assertIsInstance(ball, DefaultObject)
@ -404,10 +441,14 @@ class TestBuilding(CommandTest):
self.assertIsNone(ball.location) self.assertIsNone(ball.location)
ball.delete() 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. # Tests "@spawn/noloc ...", but DO specify a location.
# Location should be the specified location. # Location should be the specified location.
self.call(building.CmdSpawn(), self.call(building.CmdSpawn(),
"/noloc {'prototype':'BALL', 'location':'%s'}" "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
% spawnLoc.dbref, "Spawned Ball") % spawnLoc.dbref, "Spawned Ball")
ball = getObject(self, "Ball") ball = getObject(self, "Ball")
self.assertEqual(ball.location, spawnLoc) self.assertEqual(ball.location, spawnLoc)
@ -416,6 +457,9 @@ class TestBuilding(CommandTest):
# test calling spawn with an invalid prototype. # test calling spawn with an invalid prototype.
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'") self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
# Test listing commands
self.call(building.CmdSpawn(), "/list", "Key ")
class TestComms(CommandTest): class TestComms(CommandTest):
@ -471,7 +515,7 @@ class TestBatchProcess(CommandTest):
def test_batch_commands(self): def test_batch_commands(self):
# cannot test batchcode here, it must run inside the server process # cannot test batchcode here, it must run inside the server process
self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds", self.call(batchprocess.CmdBatchCommands(), "example_batch_cmds",
"Running Batchcommand processor Automatic mode for example_batch_cmds") "Running Batch-command processor - Automatic mode for example_batch_cmds")
# we make sure to delete the button again here to stop the running reactor # we make sure to delete the button again here to stop the running reactor
confirm = building.CmdDestroy.confirm confirm = building.CmdDestroy.confirm
building.CmdDestroy.confirm = False building.CmdDestroy.confirm = False
@ -494,3 +538,12 @@ class TestInterruptCommand(CommandTest):
def test_interrupt_command(self): def test_interrupt_command(self):
ret = self.call(CmdInterrupt(), "") ret = self.call(CmdInterrupt(), "")
self.assertEqual(ret, "") self.assertEqual(ret, "")
class TestUnconnectedCommand(CommandTest):
def test_info_command(self):
expected = "## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
settings.SERVERNAME,
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
SESSIONS.account_count(), utils.get_evennia_version())
self.call(unloggedin.CmdUnconnectedInfo(), "", expected)

View file

@ -3,6 +3,7 @@ Commands that are available from the connect screen.
""" """
import re import re
import time import time
import datetime
from collections import defaultdict from collections import defaultdict
from random import getrandbits from random import getrandbits
from django.conf import settings from django.conf import settings
@ -11,8 +12,9 @@ from evennia.accounts.models import AccountDB
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.server.models import ServerConfig from evennia.server.models import ServerConfig
from evennia.comms.models import ChannelDB from evennia.comms.models import ChannelDB
from evennia.server.sessionhandler import SESSIONS
from evennia.utils import create, logger, utils from evennia.utils import create, logger, utils, gametime
from evennia.commands.cmdhandler import CMD_LOGINSTART from evennia.commands.cmdhandler import CMD_LOGINSTART
COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS) COMMAND_DEFAULT_CLASS = utils.class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -516,6 +518,24 @@ class CmdUnconnectedScreenreader(COMMAND_DEFAULT_CLASS):
self.session.sessionhandler.session_portal_sync(self.session) self.session.sessionhandler.session_portal_sync(self.session)
class CmdUnconnectedInfo(COMMAND_DEFAULT_CLASS):
"""
Provides MUDINFO output, so that Evennia games can be added to Mudconnector
and Mudstats. Sadly, the MUDINFO specification seems to have dropped off the
face of the net, but it is still used by some crawlers. This implementation
was created by looking at the MUDINFO implementation in MUX2, TinyMUSH, Rhost,
and PennMUSH.
"""
key = "info"
locks = "cmd:all()"
def func(self):
self.caller.msg("## BEGIN INFO 1.1\nName: %s\nUptime: %s\nConnected: %d\nVersion: Evennia %s\n## END INFO" % (
settings.SERVERNAME,
datetime.datetime.fromtimestamp(gametime.SERVER_START_TIME).ctime(),
SESSIONS.account_count(), utils.get_evennia_version()))
def _create_account(session, accountname, password, permissions, typeclass=None, email=None): def _create_account(session, accountname, password, permissions, typeclass=None, email=None):
""" """
Helper function, creates an account of the specified typeclass. Helper function, creates an account of the specified typeclass.

View file

@ -97,7 +97,8 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
@property @property
def wholist(self): def wholist(self):
subs = self.subscriptions.all() subs = self.subscriptions.all()
listening = [ob for ob in subs if ob.is_connected and ob not in self.mutelist] muted = list(self.mutelist)
listening = [ob for ob in subs if ob.is_connected and ob not in muted]
if subs: if subs:
# display listening subscribers in bold # display listening subscribers in bold
string = ", ".join([account.key if account not in listening else "|w%s|n" % account.key for account in subs]) string = ", ".join([account.key if account not in listening else "|w%s|n" % account.key for account in subs])

View file

@ -31,6 +31,7 @@ things you want from here into your game folder and change them there.
multiple descriptions for time and season as well as details. multiple descriptions for time and season as well as details.
* GenderSub (Griatch 2015) - Simple example (only) of storing gender * GenderSub (Griatch 2015) - Simple example (only) of storing gender
on a character and access it in an emote with a custom marker. on a character and access it in an emote with a custom marker.
* Health Bar (Tim Ashley Jenkins 2017) - Tool to create colorful bars/meters.
* Mail (grungies1138 2016) - An in-game mail system for communication. * Mail (grungies1138 2016) - An in-game mail system for communication.
* Menu login (Griatch 2011) - A login system using menus asking * Menu login (Griatch 2011) - A login system using menus asking
for name/password rather than giving them as one command. for name/password rather than giving them as one command.
@ -53,6 +54,9 @@ things you want from here into your game folder and change them there.
* Tree Select (FlutterSprite 2017) - A simple system for creating a * Tree Select (FlutterSprite 2017) - A simple system for creating a
branching EvMenu with selection options sourced from a single branching EvMenu with selection options sourced from a single
multi-line string. multi-line string.
* Turnbattle (Tim Ashley Jenkins 2017) - This is a framework for a turn-based
combat system with different levels of complexity, including versions with
equipment and magic as well as ranged combat.
* Wilderness (titeuf87 2017) - Make infinitely large wilderness areas * Wilderness (titeuf87 2017) - Make infinitely large wilderness areas
with dynamically created locations. with dynamically created locations.
* UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax. * UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax.

View file

@ -253,7 +253,7 @@ class CmdMail(default_cmds.MuxCommand):
index += 1 index += 1
table.reformat_column(0, width=6) table.reformat_column(0, width=6)
table.reformat_column(1, width=17) table.reformat_column(1, width=18)
table.reformat_column(2, width=34) table.reformat_column(2, width=34)
table.reformat_column(3, width=13) table.reformat_column(3, width=13)
table.reformat_column(4, width=7) table.reformat_column(4, width=7)

View file

@ -1088,7 +1088,7 @@ class CmdMask(RPCommand):
if self.cmdstring == "mask": if self.cmdstring == "mask":
# wear a mask # wear a mask
if not self.args: if not self.args:
caller.msg("Usage: (un)wearmask sdesc") caller.msg("Usage: (un)mask sdesc")
return return
if caller.db.unmasked_sdesc: if caller.db.unmasked_sdesc:
caller.msg("You are already wearing a mask.") caller.msg("You are already wearing a mask.")
@ -1111,7 +1111,7 @@ class CmdMask(RPCommand):
del caller.db.unmasked_sdesc del caller.db.unmasked_sdesc
caller.locks.remove("enable_recog") caller.locks.remove("enable_recog")
caller.sdesc.add(old_sdesc) caller.sdesc.add(old_sdesc)
caller.msg("You remove your mask and is again '%s'." % old_sdesc) caller.msg("You remove your mask and are again '%s'." % old_sdesc)
class RPSystemCmdSet(CmdSet): class RPSystemCmdSet(CmdSet):

View file

@ -723,9 +723,9 @@ class TestMapBuilder(CommandTest):
"evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND", "evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND",
"""Creating Map...|≈ ≈ ≈ ≈ ≈ """Creating Map...|≈ ≈ ≈ ≈ ≈
--
--
|Creating Landmass...|""") |Creating Landmass...|""")
@ -768,8 +768,8 @@ from evennia.contrib import simpledoor
class TestSimpleDoor(CommandTest): class TestSimpleDoor(CommandTest):
def test_cmdopen(self): def test_cmdopen(self):
self.call(simpledoor.CmdOpen(), "newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2", self.call(simpledoor.CmdOpen(), "newdoor;door:contrib.simpledoor.SimpleDoor,backdoor;door = Room2",
"Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A doortype exit was " "Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was "
"created ignored eventual custom returnexit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).") "created - ignored eventual custom return-exit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).")
self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You close newdoor.", cmdstring="close")
self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already closed.", cmdstring="close") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "newdoor is already closed.", cmdstring="close")
self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open") self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open")
@ -798,7 +798,7 @@ from evennia.contrib import talking_npc
class TestTalkingNPC(CommandTest): class TestTalkingNPC(CommandTest):
def test_talkingnpc(self): def test_talkingnpc(self):
npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1) npc = create_object(talking_npc.TalkingNPC, key="npctalker", location=self.room1)
self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)|") self.call(talking_npc.CmdTalk(), "", "(You walk up and talk to Char.)")
npc.delete() npc.delete()
@ -953,13 +953,13 @@ class TestTutorialWorldRooms(CommandTest):
# test turnbattle # test turnbattle
from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range from evennia.contrib.turnbattle import tb_basic, tb_equip, tb_range, tb_items, tb_magic
from evennia.objects.objects import DefaultRoom from evennia.objects.objects import DefaultRoom
class TestTurnBattleCmd(CommandTest): class TestTurnBattleBasicCmd(CommandTest):
# Test combat commands # Test basic combat commands
def test_turnbattlecmd(self): def test_turnbattlecmd(self):
self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!") self.call(tb_basic.CmdFight(), "", "You can't start a fight if you've been defeated!")
self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
@ -967,13 +967,19 @@ class TestTurnBattleCmd(CommandTest):
self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_basic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.") self.call(tb_basic.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleEquipCmd(CommandTest):
def setUp(self):
super(TestTurnBattleEquipCmd, self).setUp()
self.testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
self.testarmor = create_object(tb_equip.TBEArmor, key="test armor")
self.testweapon.move_to(self.char1)
self.testarmor.move_to(self.char1)
# Test equipment commands # Test equipment commands
def test_turnbattleequipcmd(self): def test_turnbattleequipcmd(self):
# Start with equip module specific commands. # Start with equip module specific commands.
testweapon = create_object(tb_equip.TBEWeapon, key="test weapon")
testarmor = create_object(tb_equip.TBEArmor, key="test armor")
testweapon.move_to(self.char1)
testarmor.move_to(self.char1)
self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.") self.call(tb_equip.CmdWield(), "weapon", "Char wields test weapon.")
self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.") self.call(tb_equip.CmdUnwield(), "", "Char lowers test weapon.")
self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.") self.call(tb_equip.CmdDon(), "armor", "Char dons test armor.")
@ -985,6 +991,8 @@ class TestTurnBattleCmd(CommandTest):
self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)") self.call(tb_equip.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.") self.call(tb_equip.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleRangeCmd(CommandTest):
# Test range commands # Test range commands
def test_turnbattlerangecmd(self): def test_turnbattlerangecmd(self):
# Start with range module specific commands. # Start with range module specific commands.
@ -1000,257 +1008,531 @@ class TestTurnBattleCmd(CommandTest):
self.call(tb_range.CmdRest(), "", "Char rests to recover HP.") self.call(tb_range.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleFunc(EvenniaTest): class TestTurnBattleItemsCmd(CommandTest):
def setUp(self):
super(TestTurnBattleItemsCmd, self).setUp()
self.testitem = create_object(key="test item")
self.testitem.move_to(self.char1)
# Test item commands
def test_turnbattleitemcmd(self):
self.call(tb_items.CmdUse(), "item", "'Test item' is not a usable item.")
# Also test the commands that are the same in the basic module
self.call(tb_items.CmdFight(), "", "You can't start a fight if you've been defeated!")
self.call(tb_items.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_items.CmdPass(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_items.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_items.CmdRest(), "", "Char rests to recover HP.")
class TestTurnBattleMagicCmd(CommandTest):
# Test magic commands
def test_turnbattlemagiccmd(self):
self.call(tb_magic.CmdStatus(), "", "You have 100 / 100 HP and 20 / 20 MP.")
self.call(tb_magic.CmdLearnSpell(), "test spell", "There is no spell with that name.")
self.call(tb_magic.CmdCast(), "", "Usage: cast <spell name> = <target>, <target2>")
# Also test the commands that are the same in the basic module
self.call(tb_magic.CmdFight(), "", "There's nobody here to fight!")
self.call(tb_magic.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_magic.CmdPass(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_magic.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
self.call(tb_magic.CmdRest(), "", "Char rests to recover HP and MP.")
class TestTurnBattleBasicFunc(EvenniaTest):
def setUp(self):
super(TestTurnBattleBasicFunc, self).setUp()
self.testroom = create_object(DefaultRoom, key="Test Room")
self.attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker", location=self.testroom)
self.defender = create_object(tb_basic.TBBasicCharacter, key="Defender", location=self.testroom)
self.joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner", location=None)
def tearDown(self):
super(TestTurnBattleBasicFunc, self).tearDown()
self.attacker.delete()
self.defender.delete()
self.joiner.delete()
self.testroom.delete()
self.turnhandler.stop()
# Test combat functions # Test combat functions
def test_tbbasicfunc(self): def test_tbbasicfunc(self):
attacker = create_object(tb_basic.TBBasicCharacter, key="Attacker")
defender = create_object(tb_basic.TBBasicCharacter, key="Defender")
testroom = create_object(DefaultRoom, key="Test Room")
attacker.location = testroom
defender.loaction = testroom
# Initiative roll # Initiative roll
initiative = tb_basic.roll_init(attacker) initiative = tb_basic.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000) self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll # Attack roll
attack_roll = tb_basic.get_attack(attacker, defender) attack_roll = tb_basic.get_attack(self.attacker, self.defender)
self.assertTrue(attack_roll >= 0 and attack_roll <= 100) self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
# Defense roll # Defense roll
defense_roll = tb_basic.get_defense(attacker, defender) defense_roll = tb_basic.get_defense(self.attacker, self.defender)
self.assertTrue(defense_roll == 50) self.assertTrue(defense_roll == 50)
# Damage roll # Damage roll
damage_roll = tb_basic.get_damage(attacker, defender) damage_roll = tb_basic.get_damage(self.attacker, self.defender)
self.assertTrue(damage_roll >= 15 and damage_roll <= 25) self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
# Apply damage # Apply damage
defender.db.hp = 10 self.defender.db.hp = 10
tb_basic.apply_damage(defender, 3) tb_basic.apply_damage(self.defender, 3)
self.assertTrue(defender.db.hp == 7) self.assertTrue(self.defender.db.hp == 7)
# Resolve attack # Resolve attack
defender.db.hp = 40 self.defender.db.hp = 40
tb_basic.resolve_attack(attacker, defender, attack_value=20, defense_value=10) tb_basic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
self.assertTrue(defender.db.hp < 40) self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup # Combat cleanup
attacker.db.Combat_attribute = True self.attacker.db.Combat_attribute = True
tb_basic.combat_cleanup(attacker) tb_basic.combat_cleanup(self.attacker)
self.assertFalse(attacker.db.combat_attribute) self.assertFalse(self.attacker.db.combat_attribute)
# Is in combat # Is in combat
self.assertFalse(tb_basic.is_in_combat(attacker)) self.assertFalse(tb_basic.is_in_combat(self.attacker))
# Set up turn handler script for further tests # Set up turn handler script for further tests
attacker.location.scripts.add(tb_basic.TBBasicTurnHandler) self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler)
turnhandler = attacker.db.combat_TurnHandler self.turnhandler = self.attacker.db.combat_TurnHandler
self.assertTrue(attacker.db.combat_TurnHandler) self.assertTrue(self.attacker.db.combat_TurnHandler)
# Set the turn handler's interval very high to keep it from repeating during tests. # Set the turn handler's interval very high to keep it from repeating during tests.
turnhandler.interval = 10000 self.turnhandler.interval = 10000
# Force turn order # Force turn order
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
# Test is turn # Test is turn
self.assertTrue(tb_basic.is_turn(attacker)) self.assertTrue(tb_basic.is_turn(self.attacker))
# Spend actions # Spend actions
attacker.db.Combat_ActionsLeft = 1 self.attacker.db.Combat_ActionsLeft = 1
tb_basic.spend_action(attacker, 1, action_name="Test") tb_basic.spend_action(self.attacker, 1, action_name="Test")
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "Test") self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
# Initialize for combat # Initialize for combat
attacker.db.Combat_ActionsLeft = 983 self.attacker.db.Combat_ActionsLeft = 983
turnhandler.initialize_for_combat(attacker) self.turnhandler.initialize_for_combat(self.attacker)
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "null") self.assertTrue(self.attacker.db.Combat_LastAction == "null")
# Start turn # Start turn
defender.db.Combat_ActionsLeft = 0 self.defender.db.Combat_ActionsLeft = 0
turnhandler.start_turn(defender) self.turnhandler.start_turn(self.defender)
self.assertTrue(defender.db.Combat_ActionsLeft == 1) self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
# Next turn # Next turn
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
turnhandler.next_turn() self.turnhandler.next_turn()
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Turn end check # Turn end check
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
attacker.db.Combat_ActionsLeft = 0 self.attacker.db.Combat_ActionsLeft = 0
turnhandler.turn_end_check(attacker) self.turnhandler.turn_end_check(self.attacker)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Join fight # Join fight
joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner") self.joiner.location = self.testroom
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
turnhandler.join_fight(joiner) self.turnhandler.join_fight(self.joiner)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender]) self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])
# Remove the script at the end
turnhandler.stop()
class TestTurnBattleEquipFunc(EvenniaTest):
def setUp(self):
super(TestTurnBattleEquipFunc, self).setUp()
self.testroom = create_object(DefaultRoom, key="Test Room")
self.attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker", location=self.testroom)
self.defender = create_object(tb_equip.TBEquipCharacter, key="Defender", location=self.testroom)
self.joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner", location=None)
def tearDown(self):
super(TestTurnBattleEquipFunc, self).tearDown()
self.attacker.delete()
self.defender.delete()
self.joiner.delete()
self.testroom.delete()
self.turnhandler.stop()
# Test the combat functions in tb_equip too. They work mostly the same. # Test the combat functions in tb_equip too. They work mostly the same.
def test_tbequipfunc(self): def test_tbequipfunc(self):
attacker = create_object(tb_equip.TBEquipCharacter, key="Attacker")
defender = create_object(tb_equip.TBEquipCharacter, key="Defender")
testroom = create_object(DefaultRoom, key="Test Room")
attacker.location = testroom
defender.loaction = testroom
# Initiative roll # Initiative roll
initiative = tb_equip.roll_init(attacker) initiative = tb_equip.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000) self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll # Attack roll
attack_roll = tb_equip.get_attack(attacker, defender) attack_roll = tb_equip.get_attack(self.attacker, self.defender)
self.assertTrue(attack_roll >= -50 and attack_roll <= 150) self.assertTrue(attack_roll >= -50 and attack_roll <= 150)
# Defense roll # Defense roll
defense_roll = tb_equip.get_defense(attacker, defender) defense_roll = tb_equip.get_defense(self.attacker, self.defender)
self.assertTrue(defense_roll == 50) self.assertTrue(defense_roll == 50)
# Damage roll # Damage roll
damage_roll = tb_equip.get_damage(attacker, defender) damage_roll = tb_equip.get_damage(self.attacker, self.defender)
self.assertTrue(damage_roll >= 0 and damage_roll <= 50) self.assertTrue(damage_roll >= 0 and damage_roll <= 50)
# Apply damage # Apply damage
defender.db.hp = 10 self.defender.db.hp = 10
tb_equip.apply_damage(defender, 3) tb_equip.apply_damage(self.defender, 3)
self.assertTrue(defender.db.hp == 7) self.assertTrue(self.defender.db.hp == 7)
# Resolve attack # Resolve attack
defender.db.hp = 40 self.defender.db.hp = 40
tb_equip.resolve_attack(attacker, defender, attack_value=20, defense_value=10) tb_equip.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
self.assertTrue(defender.db.hp < 40) self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup # Combat cleanup
attacker.db.Combat_attribute = True self.attacker.db.Combat_attribute = True
tb_equip.combat_cleanup(attacker) tb_equip.combat_cleanup(self.attacker)
self.assertFalse(attacker.db.combat_attribute) self.assertFalse(self.attacker.db.combat_attribute)
# Is in combat # Is in combat
self.assertFalse(tb_equip.is_in_combat(attacker)) self.assertFalse(tb_equip.is_in_combat(self.attacker))
# Set up turn handler script for further tests # Set up turn handler script for further tests
attacker.location.scripts.add(tb_equip.TBEquipTurnHandler) self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler)
turnhandler = attacker.db.combat_TurnHandler self.turnhandler = self.attacker.db.combat_TurnHandler
self.assertTrue(attacker.db.combat_TurnHandler) self.assertTrue(self.attacker.db.combat_TurnHandler)
# Set the turn handler's interval very high to keep it from repeating during tests. # Set the turn handler's interval very high to keep it from repeating during tests.
turnhandler.interval = 10000 self.turnhandler.interval = 10000
# Force turn order # Force turn order
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
# Test is turn # Test is turn
self.assertTrue(tb_equip.is_turn(attacker)) self.assertTrue(tb_equip.is_turn(self.attacker))
# Spend actions # Spend actions
attacker.db.Combat_ActionsLeft = 1 self.attacker.db.Combat_ActionsLeft = 1
tb_equip.spend_action(attacker, 1, action_name="Test") tb_equip.spend_action(self.attacker, 1, action_name="Test")
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "Test") self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
# Initialize for combat # Initialize for combat
attacker.db.Combat_ActionsLeft = 983 self.attacker.db.Combat_ActionsLeft = 983
turnhandler.initialize_for_combat(attacker) self.turnhandler.initialize_for_combat(self.attacker)
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "null") self.assertTrue(self.attacker.db.Combat_LastAction == "null")
# Start turn # Start turn
defender.db.Combat_ActionsLeft = 0 self.defender.db.Combat_ActionsLeft = 0
turnhandler.start_turn(defender) self.turnhandler.start_turn(self.defender)
self.assertTrue(defender.db.Combat_ActionsLeft == 1) self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
# Next turn # Next turn
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
turnhandler.next_turn() self.turnhandler.next_turn()
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Turn end check # Turn end check
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
attacker.db.Combat_ActionsLeft = 0 self.attacker.db.Combat_ActionsLeft = 0
turnhandler.turn_end_check(attacker) self.turnhandler.turn_end_check(self.attacker)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Join fight # Join fight
joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner") self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.turn = 0
turnhandler.db.turn = 0 self.turnhandler.join_fight(self.joiner)
turnhandler.join_fight(joiner) self.assertTrue(self.turnhandler.db.turn == 1)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
# Remove the script at the end
turnhandler.stop() class TestTurnBattleRangeFunc(EvenniaTest):
def setUp(self):
super(TestTurnBattleRangeFunc, self).setUp()
self.testroom = create_object(DefaultRoom, key="Test Room")
self.attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=self.testroom)
self.defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=self.testroom)
self.joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=self.testroom)
def tearDown(self):
super(TestTurnBattleRangeFunc, self).tearDown()
self.attacker.delete()
self.defender.delete()
self.joiner.delete()
self.testroom.delete()
self.turnhandler.stop()
# Test combat functions in tb_range too. # Test combat functions in tb_range too.
def test_tbrangefunc(self): def test_tbrangefunc(self):
testroom = create_object(DefaultRoom, key="Test Room")
attacker = create_object(tb_range.TBRangeCharacter, key="Attacker", location=testroom)
defender = create_object(tb_range.TBRangeCharacter, key="Defender", location=testroom)
# Initiative roll # Initiative roll
initiative = tb_range.roll_init(attacker) initiative = tb_range.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000) self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll # Attack roll
attack_roll = tb_range.get_attack(attacker, defender, "test") attack_roll = tb_range.get_attack(self.attacker, self.defender, "test")
self.assertTrue(attack_roll >= 0 and attack_roll <= 100) self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
# Defense roll # Defense roll
defense_roll = tb_range.get_defense(attacker, defender, "test") defense_roll = tb_range.get_defense(self.attacker, self.defender, "test")
self.assertTrue(defense_roll == 50) self.assertTrue(defense_roll == 50)
# Damage roll # Damage roll
damage_roll = tb_range.get_damage(attacker, defender) damage_roll = tb_range.get_damage(self.attacker, self.defender)
self.assertTrue(damage_roll >= 15 and damage_roll <= 25) self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
# Apply damage # Apply damage
defender.db.hp = 10 self.defender.db.hp = 10
tb_range.apply_damage(defender, 3) tb_range.apply_damage(self.defender, 3)
self.assertTrue(defender.db.hp == 7) self.assertTrue(self.defender.db.hp == 7)
# Resolve attack # Resolve attack
defender.db.hp = 40 self.defender.db.hp = 40
tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10) tb_range.resolve_attack(self.attacker, self.defender, "test", attack_value=20, defense_value=10)
self.assertTrue(defender.db.hp < 40) self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup # Combat cleanup
attacker.db.Combat_attribute = True self.attacker.db.Combat_attribute = True
tb_range.combat_cleanup(attacker) tb_range.combat_cleanup(self.attacker)
self.assertFalse(attacker.db.combat_attribute) self.assertFalse(self.attacker.db.combat_attribute)
# Is in combat # Is in combat
self.assertFalse(tb_range.is_in_combat(attacker)) self.assertFalse(tb_range.is_in_combat(self.attacker))
# Set up turn handler script for further tests # Set up turn handler script for further tests
attacker.location.scripts.add(tb_range.TBRangeTurnHandler) self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler)
turnhandler = attacker.db.combat_TurnHandler self.turnhandler = self.attacker.db.combat_TurnHandler
self.assertTrue(attacker.db.combat_TurnHandler) self.assertTrue(self.attacker.db.combat_TurnHandler)
# Set the turn handler's interval very high to keep it from repeating during tests. # Set the turn handler's interval very high to keep it from repeating during tests.
turnhandler.interval = 10000 self.turnhandler.interval = 10000
# Force turn order # Force turn order
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
# Test is turn # Test is turn
self.assertTrue(tb_range.is_turn(attacker)) self.assertTrue(tb_range.is_turn(self.attacker))
# Spend actions # Spend actions
attacker.db.Combat_ActionsLeft = 1 self.attacker.db.Combat_ActionsLeft = 1
tb_range.spend_action(attacker, 1, action_name="Test") tb_range.spend_action(self.attacker, 1, action_name="Test")
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "Test") self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
# Initialize for combat # Initialize for combat
attacker.db.Combat_ActionsLeft = 983 self.attacker.db.Combat_ActionsLeft = 983
turnhandler.initialize_for_combat(attacker) self.turnhandler.initialize_for_combat(self.attacker)
self.assertTrue(attacker.db.Combat_ActionsLeft == 0) self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(attacker.db.Combat_LastAction == "null") self.assertTrue(self.attacker.db.Combat_LastAction == "null")
# Set up ranges again, since initialize_for_combat clears them # Set up ranges again, since initialize_for_combat clears them
attacker.db.combat_range = {} self.attacker.db.combat_range = {}
attacker.db.combat_range[attacker] = 0 self.attacker.db.combat_range[self.attacker] = 0
attacker.db.combat_range[defender] = 1 self.attacker.db.combat_range[self.defender] = 1
defender.db.combat_range = {} self.defender.db.combat_range = {}
defender.db.combat_range[defender] = 0 self.defender.db.combat_range[self.defender] = 0
defender.db.combat_range[attacker] = 1 self.defender.db.combat_range[self.attacker] = 1
# Start turn # Start turn
defender.db.Combat_ActionsLeft = 0 self.defender.db.Combat_ActionsLeft = 0
turnhandler.start_turn(defender) self.turnhandler.start_turn(self.defender)
self.assertTrue(defender.db.Combat_ActionsLeft == 2) self.assertTrue(self.defender.db.Combat_ActionsLeft == 2)
# Next turn # Next turn
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
turnhandler.next_turn() self.turnhandler.next_turn()
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Turn end check # Turn end check
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.turn = 0 self.turnhandler.db.turn = 0
attacker.db.Combat_ActionsLeft = 0 self.attacker.db.Combat_ActionsLeft = 0
turnhandler.turn_end_check(attacker) self.turnhandler.turn_end_check(self.attacker)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.turn == 1)
# Join fight # Join fight
joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom) self.turnhandler.db.fighters = [self.attacker, self.defender]
turnhandler.db.fighters = [attacker, defender] self.turnhandler.db.turn = 0
turnhandler.db.turn = 0 self.turnhandler.join_fight(self.joiner)
turnhandler.join_fight(joiner) self.assertTrue(self.turnhandler.db.turn == 1)
self.assertTrue(turnhandler.db.turn == 1) self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
# Now, test for approach/withdraw functions # Now, test for approach/withdraw functions
self.assertTrue(tb_range.get_range(attacker, defender) == 1) self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1)
# Approach # Approach
tb_range.approach(attacker, defender) tb_range.approach(self.attacker, self.defender)
self.assertTrue(tb_range.get_range(attacker, defender) == 0) self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 0)
# Withdraw # Withdraw
tb_range.withdraw(attacker, defender) tb_range.withdraw(self.attacker, self.defender)
self.assertTrue(tb_range.get_range(attacker, defender) == 1) self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1)
# Remove the script at the end
turnhandler.stop()
class TestTurnBattleItemsFunc(EvenniaTest):
@patch("evennia.contrib.turnbattle.tb_items.tickerhandler", new=MagicMock())
def setUp(self):
super(TestTurnBattleItemsFunc, self).setUp()
self.testroom = create_object(DefaultRoom, key="Test Room")
self.attacker = create_object(tb_items.TBItemsCharacter, key="Attacker", location=self.testroom)
self.defender = create_object(tb_items.TBItemsCharacter, key="Defender", location=self.testroom)
self.joiner = create_object(tb_items.TBItemsCharacter, key="Joiner", location=self.testroom)
self.user = create_object(tb_items.TBItemsCharacter, key="User", location=self.testroom)
self.test_healpotion = create_object(key="healing potion")
self.test_healpotion.db.item_func = "heal"
self.test_healpotion.db.item_uses = 3
def tearDown(self):
super(TestTurnBattleItemsFunc, self).tearDown()
self.attacker.delete()
self.defender.delete()
self.joiner.delete()
self.user.delete()
self.testroom.delete()
self.turnhandler.stop()
# Test functions in tb_items.
def test_tbitemsfunc(self):
# Initiative roll
initiative = tb_items.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll
attack_roll = tb_items.get_attack(self.attacker, self.defender)
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
# Defense roll
defense_roll = tb_items.get_defense(self.attacker, self.defender)
self.assertTrue(defense_roll == 50)
# Damage roll
damage_roll = tb_items.get_damage(self.attacker, self.defender)
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
# Apply damage
self.defender.db.hp = 10
tb_items.apply_damage(self.defender, 3)
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_items.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True
tb_items.combat_cleanup(self.attacker)
self.assertFalse(self.attacker.db.combat_attribute)
# Is in combat
self.assertFalse(tb_items.is_in_combat(self.attacker))
# Set up turn handler script for further tests
self.attacker.location.scripts.add(tb_items.TBItemsTurnHandler)
self.turnhandler = self.attacker.db.combat_TurnHandler
self.assertTrue(self.attacker.db.combat_TurnHandler)
# Set the turn handler's interval very high to keep it from repeating during tests.
self.turnhandler.interval = 10000
# Force turn order
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
# Test is turn
self.assertTrue(tb_items.is_turn(self.attacker))
# Spend actions
self.attacker.db.Combat_ActionsLeft = 1
tb_items.spend_action(self.attacker, 1, action_name="Test")
self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
# Initialize for combat
self.attacker.db.Combat_ActionsLeft = 983
self.turnhandler.initialize_for_combat(self.attacker)
self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(self.attacker.db.Combat_LastAction == "null")
# Start turn
self.defender.db.Combat_ActionsLeft = 0
self.turnhandler.start_turn(self.defender)
self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
# Next turn
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.turnhandler.next_turn()
self.assertTrue(self.turnhandler.db.turn == 1)
# Turn end check
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.attacker.db.Combat_ActionsLeft = 0
self.turnhandler.turn_end_check(self.attacker)
self.assertTrue(self.turnhandler.db.turn == 1)
# Join fight
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.turnhandler.join_fight(self.joiner)
self.assertTrue(self.turnhandler.db.turn == 1)
self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])
# Now time to test item stuff.
# Spend item use
tb_items.spend_item_use(self.test_healpotion, self.user)
self.assertTrue(self.test_healpotion.db.item_uses == 2)
# Use item
self.user.db.hp = 2
tb_items.use_item(self.user, self.test_healpotion, self.user)
self.assertTrue(self.user.db.hp > 2)
# Add contition
tb_items.add_condition(self.user, self.user, "Test", 5)
self.assertTrue(self.user.db.conditions == {"Test":[5, self.user]})
# Condition tickdown
tb_items.condition_tickdown(self.user, self.user)
self.assertTrue(self.user.db.conditions == {"Test":[4, self.user]})
# Test item functions now!
# Item heal
self.user.db.hp = 2
tb_items.itemfunc_heal(self.test_healpotion, self.user, self.user)
# Item add condition
self.user.db.conditions = {}
tb_items.itemfunc_add_condition(self.test_healpotion, self.user, self.user)
self.assertTrue(self.user.db.conditions == {"Regeneration":[5, self.user]})
# Item cure condition
self.user.db.conditions = {"Poisoned":[5, self.user]}
tb_items.itemfunc_cure_condition(self.test_healpotion, self.user, self.user)
self.assertTrue(self.user.db.conditions == {})
class TestTurnBattleMagicFunc(EvenniaTest):
def setUp(self):
super(TestTurnBattleMagicFunc, self).setUp()
self.testroom = create_object(DefaultRoom, key="Test Room")
self.attacker = create_object(tb_magic.TBMagicCharacter, key="Attacker", location=self.testroom)
self.defender = create_object(tb_magic.TBMagicCharacter, key="Defender", location=self.testroom)
self.joiner = create_object(tb_magic.TBMagicCharacter, key="Joiner", location=self.testroom)
def tearDown(self):
super(TestTurnBattleMagicFunc, self).tearDown()
self.attacker.delete()
self.defender.delete()
self.joiner.delete()
self.testroom.delete()
self.turnhandler.stop()
# Test combat functions in tb_magic.
def test_tbbasicfunc(self):
# Initiative roll
initiative = tb_magic.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll
attack_roll = tb_magic.get_attack(self.attacker, self.defender)
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
# Defense roll
defense_roll = tb_magic.get_defense(self.attacker, self.defender)
self.assertTrue(defense_roll == 50)
# Damage roll
damage_roll = tb_magic.get_damage(self.attacker, self.defender)
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
# Apply damage
self.defender.db.hp = 10
tb_magic.apply_damage(self.defender, 3)
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_magic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True
tb_magic.combat_cleanup(self.attacker)
self.assertFalse(self.attacker.db.combat_attribute)
# Is in combat
self.assertFalse(tb_magic.is_in_combat(self.attacker))
# Set up turn handler script for further tests
self.attacker.location.scripts.add(tb_magic.TBMagicTurnHandler)
self.turnhandler = self.attacker.db.combat_TurnHandler
self.assertTrue(self.attacker.db.combat_TurnHandler)
# Set the turn handler's interval very high to keep it from repeating during tests.
self.turnhandler.interval = 10000
# Force turn order
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
# Test is turn
self.assertTrue(tb_magic.is_turn(self.attacker))
# Spend actions
self.attacker.db.Combat_ActionsLeft = 1
tb_magic.spend_action(self.attacker, 1, action_name="Test")
self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(self.attacker.db.Combat_LastAction == "Test")
# Initialize for combat
self.attacker.db.Combat_ActionsLeft = 983
self.turnhandler.initialize_for_combat(self.attacker)
self.assertTrue(self.attacker.db.Combat_ActionsLeft == 0)
self.assertTrue(self.attacker.db.Combat_LastAction == "null")
# Start turn
self.defender.db.Combat_ActionsLeft = 0
self.turnhandler.start_turn(self.defender)
self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
# Next turn
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.turnhandler.next_turn()
self.assertTrue(self.turnhandler.db.turn == 1)
# Turn end check
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.attacker.db.Combat_ActionsLeft = 0
self.turnhandler.turn_end_check(self.attacker)
self.assertTrue(self.turnhandler.db.turn == 1)
# Join fight
self.turnhandler.db.fighters = [self.attacker, self.defender]
self.turnhandler.db.turn = 0
self.turnhandler.join_fight(self.joiner)
self.assertTrue(self.turnhandler.db.turn == 1)
self.assertTrue(self.turnhandler.db.fighters == [self.joiner, self.attacker, self.defender])
# Test tree select # Test tree select
@ -1263,6 +1545,7 @@ Bar
--Baz 2 --Baz 2
-Qux""" -Qux"""
class TestTreeSelectFunc(EvenniaTest): class TestTreeSelectFunc(EvenniaTest):
def test_tree_functions(self): def test_tree_functions(self):

View file

@ -22,6 +22,19 @@ implemented and customized:
donning armor, and modifiers to accuracy and damage based on donning armor, and modifiers to accuracy and damage based on
currently used equipment. currently used equipment.
tb_items.py - Adds usable items and conditions/status effects, and gives
a lot of examples for each. Items can perform nearly any sort of
function, including healing, adding or curing conditions, or
being used to attack. Conditions affect a fighter's attributes
and options in combat and persist outside of fights, counting
down per turn in combat and in real time outside combat.
tb_magic.py - Adds a spellcasting system, allowing characters to cast
spells with a variety of effects by spending MP. Spells are
linked to functions, and as such can perform any sort of action
the developer can imagine - spells for attacking, healing and
conjuring objects are included as examples.
tb_range.py - Adds a system for abstract positioning and movement, which tb_range.py - Adds a system for abstract positioning and movement, which
tracks the distance between different characters and objects in tracks the distance between different characters and objects in
combat, as well as differentiates between melee and ranged combat, as well as differentiates between melee and ranged

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -287,7 +287,7 @@ class LockHandler(object):
""" """
self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser 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. Add a new lockstring to handler.
@ -296,10 +296,12 @@ class LockHandler(object):
`"<access_type>:<functions>"`. Multiple access types `"<access_type>:<functions>"`. Multiple access types
should be separated by semicolon (`;`). Alternatively, should be separated by semicolon (`;`). Alternatively,
a list with lockstrings. a list with lockstrings.
validate_only (bool, optional): If True, validate the lockstring but
don't actually store it.
Returns: Returns:
success (bool): The outcome of the addition, `False` on 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): if isinstance(lockstring, basestring):
@ -308,21 +310,41 @@ class LockHandler(object):
lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")] lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")]
lockstring = ";".join(lockdefs) lockstring = ";".join(lockdefs)
err = ""
# sanity checks # sanity checks
for lockdef in lockdefs: for lockdef in lockdefs:
if ':' not in lockdef: if ':' not in lockdef:
self._log_error(_("Lock: '%s' contains no colon (:).") % lockdef) err = _("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False return False
access_type, rhs = [part.strip() for part in lockdef.split(':', 1)] access_type, rhs = [part.strip() for part in lockdef.split(':', 1)]
if not access_type: if not access_type:
self._log_error(_("Lock: '%s' has no access_type (left-side of colon is empty).") % lockdef) 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 return False
if rhs.count('(') != rhs.count(')'): if rhs.count('(') != rhs.count(')'):
self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef) err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False return False
if not _RE_FUNCS.findall(rhs): if not _RE_FUNCS.findall(rhs):
self._log_error(_("Lock: '%s' has no valid lock functions.") % lockdef) err = _("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef)
if validate_only:
return False, err
else:
self._log_error(err)
return False return False
if validate_only:
return True, None
# get the lock string # get the lock string
storage_lockstring = self.obj.lock_storage storage_lockstring = self.obj.lock_storage
if storage_lockstring: if storage_lockstring:
@ -334,6 +356,18 @@ class LockHandler(object):
self._save_locks() self._save_locks()
return True 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): def replace(self, lockstring):
""" """
Replaces the lockstring entirely. Replaces the lockstring entirely.
@ -421,6 +455,28 @@ class LockHandler(object):
self._cache_locks(self.obj.lock_storage) self._cache_locks(self.obj.lock_storage)
self.cache_lock_bypass(self.obj) 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): 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 Checks a lock of the correct type by passing execution off to
@ -459,9 +515,13 @@ class LockHandler(object):
return True return True
except AttributeError: except AttributeError:
# happens before session is initiated. # happens before session is initiated.
if not no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or if not no_superuser_bypass and (
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or (hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): (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 return True
# no superuser or bypass -> normal lock operation # no superuser or bypass -> normal lock operation
@ -469,7 +529,8 @@ class LockHandler(object):
# we have a lock, test it. # we have a lock, test it.
evalstring, func_tup, raw_string = self.locks[access_type] evalstring, func_tup, raw_string = self.locks[access_type]
# execute all lock funcs in the correct order, producing a tuple of True/False results. # 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 # the True/False tuple goes into evalstring, which combines them
# with AND/OR/NOT in order to get the final result. # with AND/OR/NOT in order to get the final result.
return eval(evalstring % true_false) return eval(evalstring % true_false)
@ -520,9 +581,13 @@ class LockHandler(object):
if accessing_obj.locks.lock_bypass and not no_superuser_bypass: if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
return True return True
except AttributeError: except AttributeError:
if no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or if no_superuser_bypass and (
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or (hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))): (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 return True
if ":" not in lockstring: if ":" not in lockstring:
lockstring = "%s:%s" % ("_dummy", lockstring) lockstring = "%s:%s" % ("_dummy", lockstring)
@ -538,7 +603,77 @@ class LockHandler(object):
else: else:
# if no access types was given and multiple locks were # if no access types was given and multiple locks were
# embedded in the lockstring we assume all must be true # 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
# dummy to be able to call check_lockstring from the outside
class _ObjDummy:
lock_storage = ''
_LOCK_HANDLER = LockHandler(_ObjDummy())
def check_lockstring(self, accessing_obj, lockstring, no_superuser_bypass=False,
default=False, access_type=None):
"""
Do a direct check against a lockstring ('atype:func()..'),
without any intermediary storage on the accessed object.
Args:
accessing_obj (object or None): The object seeking access.
Importantly, this can be left unset if the lock functions
don't access it, no updating or storage of locks are made
against this object in this method.
lockstring (str): Lock string to check, on the form
`"access_type:lock_definition"` where the `access_type`
part can potentially be set to a dummy value to just check
a lock condition.
no_superuser_bypass (bool, optional): Force superusers to heed lock.
default (bool, optional): Fallback result to use if `access_type` is set
but no such `access_type` is found in the given `lockstring`.
access_type (str, bool): If set, only this access_type will be looked up
among the locks defined by `lockstring`.
Return:
access (bool): If check is passed or not.
"""
return _LOCK_HANDLER.check_lockstring(
accessing_obj, lockstring, no_superuser_bypass=no_superuser_bypass,
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(): def _test():

View file

@ -437,7 +437,7 @@ class ObjectDBManager(TypedObjectManager):
""" """
Create and return a new object as a copy of the original object. All Create and return a new object as a copy of the original object. All
will be identical to the original except for the arguments given will be identical to the original except for the arguments given
specifically to this method. specifically to this method. Object contents will not be copied.
Args: Args:
original_object (Object): The object to make a copy from. original_object (Object): The object to make a copy from.
@ -502,6 +502,10 @@ class ObjectDBManager(TypedObjectManager):
for script in original_object.scripts.all(): for script in original_object.scripts.all():
ScriptDB.objects.copy_script(script, new_obj=new_object) ScriptDB.objects.copy_script(script, new_obj=new_object)
# copy over all tags, if any
for tag in original_object.tags.get(return_tagobj=True, return_list=True):
new_object.tags.add(tag=tag.key, category=tag.category, data=tag.data)
return new_object return new_object
def clear_all_sessids(self): def clear_all_sessids(self):

View file

@ -23,6 +23,7 @@ from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
from evennia.utils import search from evennia.utils import search
from evennia.utils import logger from evennia.utils import logger
from evennia.utils import ansi
from evennia.utils.utils import (variable_from_module, lazy_property, from evennia.utils.utils import (variable_from_module, lazy_property,
make_iter, to_unicode, is_iter, list_to_string, make_iter, to_unicode, is_iter, list_to_string,
to_str) to_str)
@ -305,12 +306,13 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
count (int): Number of objects of this type count (int): Number of objects of this type
looker (Object): Onlooker. Not used by default. looker (Object): Onlooker. Not used by default.
Kwargs: Kwargs:
key (str): Optional key to pluralize, use this instead of the object's key. key (str): Optional key to pluralize, if given, use this instead of the object's key.
Returns: Returns:
singular (str): The singular form to display. singular (str): The singular form to display.
plural (str): The determined plural form of the key, including the count. plural (str): The determined plural form of the key, including the count.
""" """
key = kwargs.get("key", self.key) key = kwargs.get("key", self.key)
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
plural = _INFLECT.plural(key, 2) plural = _INFLECT.plural(key, 2)
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural) plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
singular = _INFLECT.an(key) singular = _INFLECT.an(key)
@ -569,17 +571,19 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
except Exception: except Exception:
logger.log_trace() logger.log_trace()
if text is not None:
if not (isinstance(text, basestring) or isinstance(text, tuple)): if not (isinstance(text, basestring) or isinstance(text, tuple)):
# sanitize text before sending across the wire # sanitize text before sending across the wire
try: try:
text = to_str(text, force_string=True) text = to_str(text, force_string=True)
except Exception: except Exception:
text = repr(text) text = repr(text)
kwargs['text'] = text
# relay to session(s) # relay to session(s)
sessions = make_iter(session) if session else self.sessions.all() sessions = make_iter(session) if session else self.sessions.all()
for session in sessions: for session in sessions:
session.data_out(text=text, **kwargs) session.data_out(**kwargs)
def for_contents(self, func, exclude=None, **kwargs): def for_contents(self, func, exclude=None, **kwargs):
@ -1001,14 +1005,14 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
cdict["location"].at_object_receive(self, None) cdict["location"].at_object_receive(self, None)
self.at_after_move(None) self.at_after_move(None)
if cdict.get("tags"): 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"]) self.tags.batch_add(*cdict["tags"])
if cdict.get("attributes"): if cdict.get("attributes"):
# this should be a dict of attrname:value # this should be tuples (key, val, ...)
self.attributes.batch_add(*cdict["attributes"]) self.attributes.batch_add(*cdict["attributes"])
if cdict.get("nattributes"): if cdict.get("nattributes"):
# this should be a dict of nattrname:value # 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) self.nattributes.add(key, value)
del self._createdict del self._createdict
@ -1751,6 +1755,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
else: else:
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
msg_location = msg_location or '{object} says, "{speech}"' msg_location = msg_location or '{object} says, "{speech}"'
msg_receivers = msg_receivers or message
custom_mapping = kwargs.get('mapping', {}) custom_mapping = kwargs.get('mapping', {})
receivers = make_iter(receivers) if receivers else None receivers = make_iter(receivers) if receivers else None
@ -1873,7 +1878,7 @@ class DefaultCharacter(DefaultObject):
""" """
self.msg("\nYou become |c%s|n.\n" % self.name) self.msg("\nYou become |c%s|n.\n" % self.name)
self.msg(self.at_look(self.location)) self.msg((self.at_look(self.location), {'type':'look'}), options = None)
def message(obj, from_obj): def message(obj, from_obj):
obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj) obj.msg("%s has entered the game." % self.get_display_name(obj), from_obj=from_obj)

View file

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

View file

2400
evennia/prototypes/menus.py Normal file

File diff suppressed because it is too large Load diff

View file

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

View file

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

View file

@ -0,0 +1,613 @@
"""
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(inprot, protparents, uninherited=None, _workprot=None):
"""
Recursively traverse a prototype dictionary, including multiple
inheritance. Use validate_prototype before this, we don't check
for infinite recursion here.
Args:
inprot (dict): Prototype dict (the individual prototype, with no inheritance included).
protparents (dict): Available protparents, keyed by prototype_key.
uninherited (dict): Parts of prototype to not inherit.
_workprot (dict, optional): Work dict for the recursive algorithm.
"""
_workprot = {} if _workprot is None else _workprot
if "prototype_parent" in inprot:
# move backwards through the inheritance
for prototype in make_iter(inprot["prototype_parent"]):
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(protparents.get(prototype.lower(), {}),
protparents, _workprot=_workprot)
_workprot.update(new_prot)
# the inprot represents a higher level (a child prot), which should override parents
_workprot.update(inprot)
if uninherited:
# put back the parts that should not be inherited
_workprot.update(uninherited)
_workprot.pop("prototype_parent", None) # we don't need this for spawning
return _workprot
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,
uninherited={"prototype_key": prototype.get("prototype_key")})
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,
uninherited={"prototype_key": prototype.get("prototype_key")})
if not prot:
continue
# extract the keyword args we need to create the object itself. If we get a callable,
# call that to get the value (don't catch errors)
create_kwargs = {}
# we must always add a key, so if not given we use a shortened md5 hash. There is a (small)
# chance this is not unique but it should usually not be a problem.
val = prot.pop("key", "Spawned-{}".format(
hashlib.md5(str(time.time())).hexdigest()[:6]))
create_kwargs["db_key"] = init_spawn_value(val, str)
val = prot.pop("location", None)
create_kwargs["db_location"] = init_spawn_value(val, value_to_obj)
val = prot.pop("home", settings.DEFAULT_HOME)
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj)
val = prot.pop("destination", None)
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj)
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str)
# extract calls to handlers
val = prot.pop("permissions", [])
permission_string = init_spawn_value(val, make_iter)
val = prot.pop("locks", "")
lock_string = init_spawn_value(val, str)
val = prot.pop("aliases", [])
alias_string = init_spawn_value(val, make_iter)
val = prot.pop("tags", [])
tags = []
for (tag, category, data) in tags:
tags.append((init_spawn_value(val, str), category, data))
prototype_key = prototype.get('prototype_key', None)
if prototype_key:
# we make sure to add a tag identifying which prototype created this object
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
val = prot.pop("exec", "")
execs = init_spawn_value(val, make_iter)
# extract ndb assignments
nattributes = dict((key.split("_", 1)[1], init_spawn_value(val, value_to_obj))
for key, val in prot.items() if key.startswith("ndb_"))
# the rest are attribute tuples (attrname, value, category, locks)
val = make_iter(prot.pop("attrs", []))
attributes = []
for (attrname, value, category, locks) in val:
attributes.append((attrname, init_spawn_value(val), category, locks))
simple_attributes = []
for key, value in ((key, value) for key, value in prot.items()
if not (key.startswith("ndb_"))):
if key in _PROTOTYPE_META_NAMES:
continue
if is_iter(value) and len(value) > 1:
# (value, category)
simple_attributes.append((key,
init_spawn_value(value[0], value_to_obj_or_any),
init_spawn_value(value[1], str)))
else:
simple_attributes.append((key,
init_spawn_value(value, value_to_obj_or_any)))
attributes = attributes + simple_attributes
attributes = [tup for tup in attributes if not tup[0] in _NON_CREATE_KWARGS]
# pack for call into _batch_create_object
objsparams.append((create_kwargs, permission_string, lock_string,
alias_string, nattributes, attributes, tags, execs))
if kwargs.get("only_validate"):
return objsparams
return batch_create_object(*objsparams)

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

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

View file

@ -18,6 +18,17 @@ from future.utils import with_metaclass
__all__ = ["DefaultScript", "DoNothing", "Store"] __all__ = ["DefaultScript", "DoNothing", "Store"]
FLUSHING_INSTANCES = False # whether we're in the process of flushing scripts from the cache
SCRIPT_FLUSH_TIMERS = {} # stores timers for scripts that are currently being flushed
def restart_scripts_after_flush():
"""After instances are flushed, validate scripts so they're not dead for a long period of time"""
global FLUSHING_INSTANCES
ScriptDB.objects.validate()
FLUSHING_INSTANCES = False
class ExtendedLoopingCall(LoopingCall): class ExtendedLoopingCall(LoopingCall):
""" """
LoopingCall that can start at a delay different LoopingCall that can start at a delay different
@ -141,15 +152,6 @@ class ScriptBase(with_metaclass(TypeclassBase, ScriptDB)):
""" """
objects = ScriptManager() 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): def __eq__(self, other):
""" """
Compares two Scripts. Compares dbids. Compares two Scripts. Compares dbids.
@ -239,7 +241,96 @@ class DefaultScript(ScriptBase):
logger.log_trace() logger.log_trace()
return None 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): def time_until_next_repeat(self):
""" """
@ -278,6 +369,27 @@ class DefaultScript(ScriptBase):
return max(0, self.db_repeats - task.callcount) return max(0, self.db_repeats - task.callcount)
return None return None
def at_idmapper_flush(self):
"""If we're flushing this object, make sure the LoopingCall is gone too"""
ret = super(DefaultScript, self).at_idmapper_flush()
if ret and self.ndb._task:
try:
from twisted.internet import reactor
global FLUSHING_INSTANCES
# store the current timers for the _task and stop it to avoid duplicates after cache flush
paused_time = self.ndb._task.next_call_time()
callcount = self.ndb._task.callcount
self._stop_task()
SCRIPT_FLUSH_TIMERS[self.id] = (paused_time, callcount)
# here we ensure that the restart call only happens once, not once per script
if not FLUSHING_INSTANCES:
FLUSHING_INSTANCES = True
reactor.callLater(2, restart_scripts_after_flush)
except Exception:
import traceback
traceback.print_exc()
return ret
def start(self, force_restart=False): def start(self, force_restart=False):
""" """
Called every time the script is started (for persistent Called every time the script is started (for persistent
@ -294,9 +406,19 @@ class DefaultScript(ScriptBase):
started or not. Used in counting. started or not. Used in counting.
""" """
if self.is_active and not force_restart: if self.is_active and not force_restart:
# script already runs and should not be restarted. # The script is already running, but make sure we have a _task if this is after a cache flush
if not self.ndb._task and self.db_interval >= 0:
self.ndb._task = ExtendedLoopingCall(self._step_task)
try:
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
del SCRIPT_FLUSH_TIMERS[self.id]
now = False
except (KeyError, ValueError, TypeError):
now = not self.db_start_delay
start_delay = None
callcount = 0
self.ndb._task.start(self.db_interval, now=now, start_delay=start_delay, count_start=callcount)
return 0 return 0
obj = self.obj obj = self.obj
@ -472,61 +594,6 @@ class DefaultScript(ScriptBase):
if task: if task:
task.force_repeat() 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): def is_valid(self):
""" """
Is called to check if the script is valid to run at this time. Is called to check if the script is valid to run at this time.

View file

@ -51,7 +51,7 @@ class AMPClientFactory(protocol.ReconnectingClientFactory):
def buildProtocol(self, addr): def buildProtocol(self, addr):
""" """
Creates an AMPProtocol instance when connecting to the server. Creates an AMPProtocol instance when connecting to the AMP server.
Args: Args:
addr (str): Connection address. Not used. addr (str): Connection address. Not used.
@ -108,6 +108,8 @@ class AMPServerClientProtocol(amp.AMPMultiConnectionProtocol):
# back with the Server side. We also need the startup mode (reload, reset, shutdown) # back with the Server side. We also need the startup mode (reload, reset, shutdown)
self.send_AdminServer2Portal( self.send_AdminServer2Portal(
amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict) amp.DUMMYSESSION, operation=amp.PSYNC, spid=os.getpid(), info_dict=info_dict)
# run the intial setup if needed
self.factory.server.run_initial_setup()
def data_to_portal(self, command, sessid, **kwargs): def data_to_portal(self, command, sessid, **kwargs):
""" """

View file

@ -467,9 +467,10 @@ ARG_OPTIONS = \
stop - shutdown server+portal stop - shutdown server+portal
reboot - shutdown server+portal, then start again reboot - shutdown server+portal, then start again
reset - restart server in 'shutdown' mode reset - restart server in 'shutdown' mode
sstart - start only server (requires portal) istart - start server in the foreground (until reload)
sstop - stop only server
kill - send kill signal to portal+server (force) kill - send kill signal to portal+server (force)
skill = send kill signal only to server skill - send kill signal only to server
status - show server and portal run state status - show server and portal run state
info - show server and portal port info info - show server and portal port info
menu - show a menu of options menu - show a menu of options
@ -955,12 +956,37 @@ def reboot_evennia(pprofiler=False, sprofiler=False):
send_instruction(PSTATUS, None, _portal_running, _portal_not_running) send_instruction(PSTATUS, None, _portal_running, _portal_not_running)
def stop_server_only(): def start_server_interactive():
"""
Start the Server under control of the launcher process (foreground)
"""
def _iserver():
_, server_twistd_cmd = _get_twistd_cmdline(False, False)
server_twistd_cmd.append("--nodaemon")
print("Starting Server in interactive mode (stop with Ctrl-C)...")
try:
Popen(server_twistd_cmd, env=getenv(), stderr=STDOUT).wait()
except KeyboardInterrupt:
print("... Stopped Server with Ctrl-C.")
else:
print("... Server stopped (leaving interactive mode).")
stop_server_only(when_stopped=_iserver)
def stop_server_only(when_stopped=None):
""" """
Only stop the Server-component of Evennia (this is not useful except for debug) Only stop the Server-component of Evennia (this is not useful except for debug)
Args:
when_stopped (callable): This will be called with no arguments when Server has stopped (or
if it had already stopped when this is called).
""" """
def _server_stopped(*args): def _server_stopped(*args):
if when_stopped:
when_stopped()
else:
print("... Server stopped.") print("... Server stopped.")
_reactor_stop() _reactor_stop()
@ -970,6 +996,9 @@ def stop_server_only():
print("Server stopping ...") print("Server stopping ...")
wait_for_status_reply(_server_stopped) wait_for_status_reply(_server_stopped)
send_instruction(SSHUTD, {}) send_instruction(SSHUTD, {})
else:
if when_stopped:
when_stopped()
else: else:
print("Server is not running.") print("Server is not running.")
_reactor_stop() _reactor_stop()
@ -1037,9 +1066,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)) new_linecount = sum(blck.count("\n") for blck in _block(filehandle))
if new_linecount < old_linecount: if new_linecount < old_linecount:
# this could happen if the file was manually deleted or edited # this happens if the file was cycled or manually deleted/edited.
print("Log file has shrunk. Restart log reader.") print(" ** Log file {filename} has cycled or been edited. "
sys.exit() "Restarting log. ".format(filehandle.name))
new_linecount = 0
old_linecount = 0
lines_to_get = max(0, new_linecount - old_linecount) lines_to_get = max(0, new_linecount - old_linecount)
@ -1935,7 +1966,7 @@ def main():
# launch menu for operation # launch menu for operation
init_game_directory(CURRENT_DIR, check_db=True) init_game_directory(CURRENT_DIR, check_db=True)
run_menu() run_menu()
elif option in ('status', 'info', 'start', 'reload', 'reboot', elif option in ('status', 'info', 'start', 'istart', 'reload', 'reboot',
'reset', 'stop', 'sstop', 'kill', 'skill'): 'reset', 'stop', 'sstop', 'kill', 'skill'):
# operate the server directly # operate the server directly
if not SERVER_LOGFILE: if not SERVER_LOGFILE:
@ -1946,6 +1977,8 @@ def main():
query_info() query_info()
elif option == "start": elif option == "start":
start_evennia(args.profiler, args.profiler) start_evennia(args.profiler, args.profiler)
elif option == "istart":
start_server_interactive()
elif option == 'reload': elif option == 'reload':
reload_evennia(args.profiler) reload_evennia(args.profiler)
elif option == 'reboot': elif option == 'reboot':

View file

@ -59,7 +59,7 @@ def create_objects():
""" """
logger.log_info("Creating objects (Account #1 and Limbo room) ...") logger.log_info("Initial setup: Creating objects (Account #1 and Limbo room) ...")
# Set the initial User's account object's username on the #1 object. # Set the initial User's account object's username on the #1 object.
# This object is pure django and only holds name, email and password. # This object is pure django and only holds name, email and password.
@ -121,7 +121,7 @@ def create_channels():
Creates some sensible default channels. Creates some sensible default channels.
""" """
logger.log_info("Creating default channels ...") logger.log_info("Initial setup: Creating default channels ...")
goduser = get_god_account() goduser = get_god_account()
for channeldict in settings.DEFAULT_CHANNELS: for channeldict in settings.DEFAULT_CHANNELS:
@ -144,11 +144,21 @@ def at_initial_setup():
mod = __import__(modname, fromlist=[None]) mod = __import__(modname, fromlist=[None])
except (ImportError, ValueError): except (ImportError, ValueError):
return return
logger.log_info(" Running at_initial_setup() hook.") logger.log_info("Initial setup: Running at_initial_setup() hook.")
if mod.__dict__.get("at_initial_setup", None): if mod.__dict__.get("at_initial_setup", None):
mod.at_initial_setup() mod.at_initial_setup()
def collectstatic():
"""
Run collectstatic to make sure all web assets are loaded.
"""
from django.core.management import call_command
logger.log_info("Initial setup: Gathering static resources using 'collectstatic'")
call_command('collectstatic', '--noinput')
def reset_server(): def reset_server():
""" """
We end the initialization by resetting the server. This makes sure We end the initialization by resetting the server. This makes sure
@ -160,7 +170,7 @@ def reset_server():
ServerConfig.objects.conf("server_epoch", time.time()) ServerConfig.objects.conf("server_epoch", time.time())
from evennia.server.sessionhandler import SESSIONS from evennia.server.sessionhandler import SESSIONS
logger.log_info("Initial setup complete. Restarting Server once.") logger.log_info("Initial setup complete. Restarting Server once.")
SESSIONS.server.shutdown(mode='reset') SESSIONS.portal_reset_server()
def handle_setup(last_step): def handle_setup(last_step):
@ -186,6 +196,7 @@ def handle_setup(last_step):
setup_queue = [create_objects, setup_queue = [create_objects,
create_channels, create_channels,
at_initial_setup, at_initial_setup,
collectstatic,
reset_server] reset_server]
# step through queue, from last completed function # step through queue, from last completed function

View file

@ -44,9 +44,10 @@ SSHUTD = chr(17) # server shutdown
PSTATUS = chr(18) # ping server or portal status PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode SRESET = chr(19) # server shutdown in reset mode
NUL = b'\0'
NULNUL = '\0\0'
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed) AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
BATCH_RATE = 250 # max commands/sec before switching to batch-sending
BATCH_TIMEOUT = 0.5 # how often to poll to empty batch queue, in seconds
# buffers # buffers
_SENDBATCH = defaultdict(list) _SENDBATCH = defaultdict(list)
@ -61,11 +62,15 @@ _HTTP_WARNING = """
HTTP/1.1 200 OK HTTP/1.1 200 OK
Content-Type: text/html Content-Type: text/html
<html><body> <html>
This is Evennia's interal AMP port. It handles communication <body>
between Evennia's different processes.<h3><p>This port should NOT be This is Evennia's internal AMP port. It handles communication
publicly visible.</p></h3> between Evennia's different processes.
</body></html>""".strip() <p>
<h3>This port should NOT be publicly visible.</h3>
</p>
</body>
</html>""".strip()
# Helper functions for pickling. # Helper functions for pickling.
@ -107,43 +112,45 @@ class Compressed(amp.String):
def fromBox(self, name, strings, objects, proto): def fromBox(self, name, strings, objects, proto):
""" """
Converts from box representation to python. We Converts from box string representation to python. We read back too-long batched data and
group very long data into batches. put it back together here.
""" """
value = StringIO() value = StringIO()
value.write(strings.get(name)) value.write(self.fromStringProto(strings.get(name), proto))
for counter in count(2): for counter in count(2):
# count from 2 upwards # count from 2 upwards
chunk = strings.get("%s.%d" % (name, counter)) chunk = strings.get("%s.%d" % (name, counter))
if chunk is None: if chunk is None:
break break
value.write(chunk) value.write(self.fromStringProto(chunk, proto))
objects[name] = value.getvalue() objects[name] = value.getvalue()
def toBox(self, name, strings, objects, proto): def toBox(self, name, strings, objects, proto):
""" """
Convert from data to box. We handled too-long Convert from python object to string box representation.
batched data and put it together here. we break up too-long data snippets into multiple batches here.
""" """
value = StringIO(objects[name]) value = StringIO(objects[name])
strings[name] = value.read(AMP_MAXLEN) strings[name] = self.toStringProto(value.read(AMP_MAXLEN), proto)
for counter in count(2): for counter in count(2):
chunk = value.read(AMP_MAXLEN) chunk = value.read(AMP_MAXLEN)
if not chunk: if not chunk:
break break
strings["%s.%d" % (name, counter)] = chunk strings["%s.%d" % (name, counter)] = self.toStringProto(chunk, proto)
def toString(self, inObject): def toString(self, inObject):
""" """
Convert to send on the wire, with compression. Convert to send as a string on the wire, with compression.
""" """
return zlib.compress(inObject, 9) return zlib.compress(super(Compressed, self).toString(inObject), 9)
def fromString(self, inString): def fromString(self, inString):
""" """
Convert (decompress) from the wire to Python. Convert (decompress) from the string-representation on the wire to Python.
""" """
return zlib.decompress(inString) return super(Compressed, self).fromString(zlib.decompress(inString))
class MsgLauncher2Portal(amp.Command): class MsgLauncher2Portal(amp.Command):
@ -261,16 +268,29 @@ class AMPMultiConnectionProtocol(amp.AMP):
self.send_reset_time = time.time() self.send_reset_time = time.time()
self.send_mode = True self.send_mode = True
self.send_task = None self.send_task = None
self.multibatches = 0
def dataReceived(self, data): def dataReceived(self, data):
""" """
Handle non-AMP messages, such as HTTP communication. Handle non-AMP messages, such as HTTP communication.
""" """
if data[0] != b'\0': if data[0] == NUL:
# an AMP communication
if data[-2:] != NULNUL:
# an incomplete AMP box means more batches are forthcoming.
self.multibatches += 1
super(AMPMultiConnectionProtocol, self).dataReceived(data)
elif self.multibatches:
# invalid AMP, but we have a pending multi-batch that is not yet complete
if data[-2:] == NULNUL:
# end of existing multibatch
self.multibatches = max(0, self.multibatches - 1)
super(AMPMultiConnectionProtocol, self).dataReceived(data)
else:
# not an AMP communication, return warning
self.transport.write(_HTTP_WARNING) self.transport.write(_HTTP_WARNING)
self.transport.loseConnection() self.transport.loseConnection()
else: print("HTML received: %s" % data)
super(AMPMultiConnectionProtocol, self).dataReceived(data)
def makeConnection(self, transport): def makeConnection(self, transport):
""" """

View file

@ -356,10 +356,13 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
packed_data (str): Pickled data (sessid, kwargs) coming over the wire. packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
""" """
try:
sessid, kwargs = self.data_in(packed_data) sessid, kwargs = self.data_in(packed_data)
session = self.factory.portal.sessions.get(sessid, None) session = self.factory.portal.sessions.get(sessid, None)
if session: if session:
self.factory.portal.sessions.data_out(session, **kwargs) self.factory.portal.sessions.data_out(session, **kwargs)
except Exception:
logger.log_trace("packed_data len {}".format(len(packed_data)))
return {} return {}
@amp.AdminServer2Portal.responder @amp.AdminServer2Portal.responder

View file

@ -343,7 +343,7 @@ if WEBSERVER_ENABLED:
proxy_service = internet.TCPServer(proxyport, proxy_service = internet.TCPServer(proxyport,
web_root, web_root,
interface=interface) interface=interface)
proxy_service.setName('EvenniaWebProxy%s' % pstring) proxy_service.setName('EvenniaWebProxy%s:%s' % (ifacestr, proxyport))
PORTAL.services.addService(proxy_service) PORTAL.services.addService(proxy_service)
INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport)) INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
INFO_DICT["webserver_internal"].append("webserver: %s" % serverport) INFO_DICT["webserver_internal"].append("webserver: %s" % serverport)

View file

@ -116,7 +116,7 @@ class Ttype(object):
self.protocol.protocol_flags["FORCEDENDLINE"] = False self.protocol.protocol_flags["FORCEDENDLINE"] = False
if cupper.startswith("TINTIN++"): if cupper.startswith("TINTIN++"):
self.protocol.protocol_flags["FORCEDENDLINE"] = False self.protocol.protocol_flags["FORCEDENDLINE"] = True
if (cupper.startswith("XTERM") or if (cupper.startswith("XTERM") or
cupper.endswith("-256COLOR") or cupper.endswith("-256COLOR") or

View file

@ -181,9 +181,6 @@ class Evennia(object):
self.start_time = time.time() self.start_time = time.time()
# Run the initial setup if needed
self.run_initial_setup()
# initialize channelhandler # initialize channelhandler
channelhandler.CHANNELHANDLER.update() channelhandler.CHANNELHANDLER.update()
@ -274,6 +271,8 @@ class Evennia(object):
def run_initial_setup(self): def run_initial_setup(self):
""" """
This is triggered by the amp protocol when the connection
to the portal has been established.
This attempts to run the initial_setup script of the server. This attempts to run the initial_setup script of the server.
It returns if this is not the first time the server starts. It returns if this is not the first time the server starts.
Once finished the last_initial_setup_step is set to -1. Once finished the last_initial_setup_step is set to -1.
@ -508,7 +507,8 @@ ServerConfig.objects.conf("server_starting_mode", True)
# what to execute from. # what to execute from.
application = service.Application('Evennia') application = service.Application('Evennia')
# custom logging if "--nodaemon" not in sys.argv:
# custom logging, but only if we are not running in interactive mode
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE), logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
os.path.dirname(settings.SERVER_LOG_FILE)) os.path.dirname(settings.SERVER_LOG_FILE))
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit) application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)

View file

@ -278,7 +278,7 @@ class ServerSessionHandler(SessionHandler):
""" """
super(ServerSessionHandler, self).__init__(*args, **kwargs) super(ServerSessionHandler, self).__init__(*args, **kwargs)
self.server = None self.server = None # set at server initialization
self.server_data = {"servername": _SERVERNAME} self.server_data = {"servername": _SERVERNAME}
def _run_cmd_login(self, session): def _run_cmd_login(self, session):
@ -290,7 +290,6 @@ class ServerSessionHandler(SessionHandler):
if not session.logged_in: if not session.logged_in:
self.data_in(session, text=[[CMD_LOGINSTART], {}]) self.data_in(session, text=[[CMD_LOGINSTART], {}])
def portal_connect(self, portalsessiondata): def portal_connect(self, portalsessiondata):
""" """
Called by Portal when a new session has connected. Called by Portal when a new session has connected.

View file

@ -142,7 +142,7 @@ LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, 'lockwarnings.log')
CYCLE_LOGFILES = True CYCLE_LOGFILES = True
# Number of lines to append to rotating channel logs when they rotate # Number of lines to append to rotating channel logs when they rotate
CHANNEL_LOG_NUM_TAIL_LINES = 20 CHANNEL_LOG_NUM_TAIL_LINES = 20
# Max size of channel log files before they rotate # Max size (in bytes) of channel log files before they rotate
CHANNEL_LOG_ROTATE_SIZE = 1000000 CHANNEL_LOG_ROTATE_SIZE = 1000000
# Local time zone for this installation. All choices can be found here: # Local time zone for this installation. All choices can be found here:
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
@ -354,6 +354,9 @@ LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",)
INPUT_FUNC_MODULES = ["evennia.server.inputfuncs", "server.conf.inputfuncs"] INPUT_FUNC_MODULES = ["evennia.server.inputfuncs", "server.conf.inputfuncs"]
# Modules that contain prototypes for use with the spawner mechanism. # Modules that contain prototypes for use with the spawner mechanism.
PROTOTYPE_MODULES = ["world.prototypes"] 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 # Module holding settings/actions for the dummyrunner program (see the
# dummyrunner for more information) # dummyrunner for more information)
DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings" DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings"
@ -513,7 +516,7 @@ TIME_GAME_EPOCH = None
TIME_IGNORE_DOWNTIMES = False TIME_IGNORE_DOWNTIMES = False
###################################################################### ######################################################################
# Inlinefunc # Inlinefunc & PrototypeFuncs
###################################################################### ######################################################################
# Evennia supports inline function preprocessing. This allows users # Evennia supports inline function preprocessing. This allows users
# to supply inline calls on the form $func(arg, arg, ...) to do # 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 # is loaded from left-to-right, same-named functions will overload
INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs", INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs",
"server.conf.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 # Default Account setup and access

View file

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

View file

@ -653,6 +653,42 @@ class TypeclassManager(TypedObjectManager):
""" """
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).count() return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).count()
def annotate(self, *args, **kwargs):
"""
Overload annotate method to filter on typeclass before annotating.
Args:
*args (any): Positional arguments passed along to queryset annotate method.
**kwargs (any): Keyword arguments passed along to queryset annotate method.
Returns:
Annotated queryset.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).annotate(*args, **kwargs)
def values(self, *args, **kwargs):
"""
Overload values method to filter on typeclass first.
Args:
*args (any): Positional arguments passed along to values method.
**kwargs (any): Keyword arguments passed along to values method.
Returns:
Queryset of values dictionaries, just filtered by typeclass first.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values(*args, **kwargs)
def values_list(self, *args, **kwargs):
"""
Overload values method to filter on typeclass first.
Args:
*args (any): Positional arguments passed along to values_list method.
**kwargs (any): Keyword arguments passed along to values_list method.
Returns:
Queryset of value_list tuples, just filtered by typeclass first.
"""
return super(TypeclassManager, self).filter(db_typeclass_path=self.model.path).values_list(*args, **kwargs)
def _get_subclasses(self, cls): def _get_subclasses(self, cls):
""" """
Recursively get all subclasses to a class. Recursively get all subclasses to a class.

View file

@ -54,7 +54,8 @@ _GA = object.__getattribute__
def create_object(typeclass=None, key=None, location=None, home=None, def create_object(typeclass=None, key=None, location=None, home=None,
permissions=None, locks=None, aliases=None, tags=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. 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). permissions (list): A list of permission strings or tuples (permstring, category).
locks (str): one or more lockstrings, separated by semicolons. locks (str): one or more lockstrings, separated by semicolons.
aliases (list): A list of alternative keys or tuples (aliasstring, category). 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 destination (Object or str): Obj or #dbref to use as an Exit's
target. target.
report_to (Object): The object to return error messages to. report_to (Object): The object to return error messages to.
nohome (bool): This allows the creation of objects without a nohome (bool): This allows the creation of objects without a
default home location; only used when creating the default default home location; only used when creating the default
location itself or during unittests. 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: Returns:
object (Object): A newly created object of the given typeclass. 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 locks = make_iter(locks) if locks is not None else None
aliases = make_iter(aliases) if aliases 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 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): 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 # store the call signature for the signal
new_object._createdict = dict(key=key, location=location, destination=destination, home=home, new_object._createdict = dict(key=key, location=location, destination=destination, home=home,
typeclass=typeclass.path, permissions=permissions, locks=locks, 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 # this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict can be # at_first_save hook on the typeclass, where the _createdict can be
# used. # used.
@ -139,7 +147,8 @@ object = create_object
def create_script(typeclass=None, key=None, obj=None, account=None, locks=None, def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
interval=None, start_delay=None, repeats=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 Create a new script. All scripts are a combination of a database
object that communicates with the database, and an typeclass that 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. created or if the `start` method must be called explicitly.
report_to (Object): The object to return error messages to. report_to (Object): The object to return error messages to.
desc (str): Optional description of script 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 See evennia.scripts.manager for methods to manipulate existing
scripts in the database. scripts in the database.
@ -190,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
if key: if key:
kwarg["db_key"] = key kwarg["db_key"] = key
if account: if account:
kwarg["db_account"] = dbid_to_obj(account, _ScriptDB) kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
if obj: if obj:
kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB) kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
if interval: if interval:
kwarg["db_interval"] = interval kwarg["db_interval"] = interval
if start_delay: if start_delay:
@ -203,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
kwarg["db_persistent"] = persistent kwarg["db_persistent"] = persistent
if desc: if desc:
kwarg["db_desc"] = 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 # create new instance
new_script = typeclass(**kwarg) 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 # store the call signature for the signal
new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval, new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval,
start_delay=start_delay, repeats=repeats, persistent=persistent, 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 # this will trigger the save signal which in turn calls the
# at_first_save hook on the typeclass, where the _createdict # at_first_save hook on the typeclass, where the _createdict
# can be used. # can be used.

View file

@ -237,10 +237,13 @@ class _SaverList(_SaverMutable, MutableSequence):
self._data = list() self._data = list()
@_save @_save
def __add__(self, otherlist): def __iadd__(self, otherlist):
self._data = self._data.__add__(otherlist) self._data = self._data.__add__(otherlist)
return self._data return self._data
def __add__(self, otherlist):
return list(self._data) + otherlist
@_save @_save
def insert(self, index, value): def insert(self, index, value):
self._data.insert(index, self._convert_mutables(value)) self._data.insert(index, self._convert_mutables(value))

View file

@ -43,13 +43,18 @@ command definition too) with function definitions:
def node_with_other_name(caller, input_string): def node_with_other_name(caller, input_string):
# code # code
return text, options return text, options
def another_node(caller, input_string, **kwargs):
# code
return text, options
``` ```
Where caller is the object using the menu and input_string is the Where caller is the object using the menu and input_string is the
command entered by the user on the *previous* node (the command command entered by the user on the *previous* node (the command
entered to get to this node). The node function code will only be entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with executed once per node-visit and the system will accept nodes with
both one or two arguments interchangeably. both one or two arguments interchangeably. It also accepts nodes
that takes **kwargs.
The menu tree itself is available on the caller as The menu tree itself is available on the caller as
`caller.ndb._menutree`. This makes it a convenient place to store `caller.ndb._menutree`. This makes it a convenient place to store
@ -82,12 +87,14 @@ menu is immediately exited and the default "look" command is called.
the callable. Those kwargs will also be passed into the next node if possible. the callable. Those kwargs will also be passed into the next node if possible.
Such a callable should return either a str or a (str, dict), where the Such a callable should return either a str or a (str, dict), where the
string is the name of the next node to go to and the dict is the new, string is the name of the next node to go to and the dict is the new,
(possibly modified) kwarg to pass into the next node. (possibly modified) kwarg to pass into the next node. If the callable returns
None or the empty string, the current node will be revisited.
- `exec` (str, callable or tuple, optional): This takes the same input as `goto` above - `exec` (str, callable or tuple, optional): This takes the same input as `goto` above
and runs before it. If given a node name, the node will be executed but will not and runs before it. If given a node name, the node will be executed but will not
be considered the next node. If node/callback returns str or (str, dict), these will be considered the next node. If node/callback returns str or (str, dict), these will
replace the `goto` step (`goto` callbacks will not fire), with the string being the replace the `goto` step (`goto` callbacks will not fire), with the string being the
next node name and the optional dict acting as the kwargs-input for the next node. next node name and the optional dict acting as the kwargs-input for the next node.
If an exec callable returns the empty string (only), the current node is re-run.
If key is not given, the option will automatically be identified by If key is not given, the option will automatically be identified by
its number 1..N. its number 1..N.
@ -158,16 +165,16 @@ evennia.utils.evmenu`.
""" """
from __future__ import print_function from __future__ import print_function
import random import random
import inspect
from builtins import object, range from builtins import object, range
from textwrap import dedent
from inspect import isfunction, getargspec from inspect import isfunction, getargspec
from django.conf import settings from django.conf import settings
from evennia import Command, CmdSet from evennia import Command, CmdSet
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.evtable import EvTable from evennia.utils.evtable import EvTable
from evennia.utils.ansi import strip_ansi from evennia.utils.ansi import strip_ansi
from evennia.utils.utils import mod_import, make_iter, pad, m_len from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
from evennia.commands import cmdhandler from evennia.commands import cmdhandler
# read from protocol NAWS later? # read from protocol NAWS later?
@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# i18n # i18n
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.") _ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is either not implemented or "
"caused an error. Make another choice.")
_ERR_GENERAL = _("Error in menu node '{nodename}'.") _ERR_GENERAL = _("Error in menu node '{nodename}'.")
_ERR_NO_OPTION_DESC = _("No description.") _ERR_NO_OPTION_DESC = _("No description.")
_HELP_FULL = _("Commands: <menu option>, help, quit") _HELP_FULL = _("Commands: <menu option>, help, quit")
@ -315,7 +323,7 @@ class EvMenu(object):
auto_quit=True, auto_look=True, auto_help=True, auto_quit=True, auto_look=True, auto_help=True,
cmd_on_exit="look", cmd_on_exit="look",
persistent=False, startnode_input="", session=None, persistent=False, startnode_input="", session=None,
**kwargs): debug=False, **kwargs):
""" """
Initialize the menu tree and start the caller onto the first node. Initialize the menu tree and start the caller onto the first node.
@ -368,7 +376,8 @@ class EvMenu(object):
*pickle*. When the server is reloaded, the latest node shown will be completely *pickle*. When the server is reloaded, the latest node shown will be completely
re-run with the same input arguments - so be careful if you are counting re-run with the same input arguments - so be careful if you are counting
up some persistent counter or similar - the counter may be run twice if up some persistent counter or similar - the counter may be run twice if
reload happens on the node that does that. reload happens on the node that does that. Note that if `debug` is True,
this setting is ignored and assumed to be False.
startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if startnode_input (str or (str, dict), optional): Send an input text to `startnode` as if
a user input text from a fictional previous node. If including the dict, this will a user input text from a fictional previous node. If including the dict, this will
be passed as **kwargs to that node. When the server reloads, be passed as **kwargs to that node. When the server reloads,
@ -378,6 +387,10 @@ class EvMenu(object):
for the very first display of the first node - after that, EvMenu itself for the very first display of the first node - after that, EvMenu itself
will keep the session updated from the command input. So a persistent will keep the session updated from the command input. So a persistent
menu will *not* be using this same session anymore after a reload. menu will *not* be using this same session anymore after a reload.
debug (bool, optional): If set, the 'menudebug' command will be made available
by default in all nodes of the menu. This will print out the current state of
the menu. Deactivate for production use! When the debug flag is active, the
`persistent` flag is deactivated.
Kwargs: Kwargs:
any (any): All kwargs will become initialization variables on `caller.ndb._menutree`, any (any): All kwargs will become initialization variables on `caller.ndb._menutree`,
@ -401,7 +414,7 @@ class EvMenu(object):
""" """
self._startnode = startnode self._startnode = startnode
self._menutree = self._parse_menudata(menudata) self._menutree = self._parse_menudata(menudata)
self._persistent = persistent self._persistent = persistent if not debug else False
self._quitting = False self._quitting = False
if startnode not in self._menutree: if startnode not in self._menutree:
@ -415,6 +428,7 @@ class EvMenu(object):
self.auto_quit = auto_quit self.auto_quit = auto_quit
self.auto_look = auto_look self.auto_look = auto_look
self.auto_help = auto_help self.auto_help = auto_help
self.debug_mode = debug
self._session = session self._session = session
if isinstance(cmd_on_exit, str): if isinstance(cmd_on_exit, str):
# At this point menu._session will have been replaced by the # At this point menu._session will have been replaced by the
@ -573,6 +587,7 @@ class EvMenu(object):
except EvMenuError: except EvMenuError:
errmsg = _ERR_GENERAL.format(nodename=callback) errmsg = _ERR_GENERAL.format(nodename=callback)
self.caller.msg(errmsg, self._session) self.caller.msg(errmsg, self._session)
logger.log_trace()
raise raise
return ret return ret
@ -606,9 +621,11 @@ class EvMenu(object):
nodetext, options = ret, None nodetext, options = ret, None
except KeyError: except KeyError:
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session) self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
logger.log_trace()
raise EvMenuError raise EvMenuError
except Exception: except Exception:
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session) self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
logger.log_trace()
raise raise
# store options to make them easier to test # store options to make them easier to test
@ -665,24 +682,27 @@ class EvMenu(object):
if isinstance(ret, basestring): if isinstance(ret, basestring):
# only return a value if a string (a goto target), ignore all other returns # only return a value if a string (a goto target), ignore all other returns
if not ret:
# an empty string - rerun the same node
return self.nodename
return ret, kwargs return ret, kwargs
return None return None
def goto(self, nodename, raw_string, **kwargs): def extract_goto_exec(self, nodename, option_dict):
""" """
Run a node by name, optionally dynamically generating that name first. Helper: Get callables and their eventual kwargs.
Args: Args:
nodename (str or callable): Name of node or a callable nodename (str): The current node name (used for error reporting).
to be called as `function(caller, raw_string)` or `function(caller)` option_dict (dict): The seleted option's dict.
to return the actual goto string.
raw_string (str): The raw default string entered on the Returns:
previous node (only used if the node accepts it as an goto (str, callable or None): The goto directive in the option.
argument) goto_kwargs (dict): Kwargs for `goto` if the former is callable, otherwise empty.
execute (callable or None): Executable given by the `exec` directive.
exec_kwargs (dict): Kwargs for `execute` if it's callable, otherwise empty.
""" """
def _extract_goto_exec(option_dict):
"Helper: Get callables and their eventual kwargs"
goto_kwargs, exec_kwargs = {}, {} goto_kwargs, exec_kwargs = {}, {}
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None) goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
if goto and isinstance(goto, (tuple, list)): if goto and isinstance(goto, (tuple, list)):
@ -705,6 +725,20 @@ class EvMenu(object):
execute = execute[0] execute = execute[0]
return goto, goto_kwargs, execute, exec_kwargs return goto, goto_kwargs, execute, exec_kwargs
def goto(self, nodename, raw_string, **kwargs):
"""
Run a node by name, optionally dynamically generating that name first.
Args:
nodename (str or callable): Name of node or a callable
to be called as `function(caller, raw_string)` or `function(caller)`
to return the actual goto string.
raw_string (str): The raw default string entered on the
previous node (only used if the node accepts it as an
argument)
"""
if callable(nodename): if callable(nodename):
# run the "goto" callable, if possible # run the "goto" callable, if possible
inp_nodename = nodename inp_nodename = nodename
@ -714,6 +748,9 @@ class EvMenu(object):
raise EvMenuError( raise EvMenuError(
"{}: goto callable must return str or (str, dict)".format(inp_nodename)) "{}: goto callable must return str or (str, dict)".format(inp_nodename))
nodename, kwargs = nodename[:2] nodename, kwargs = nodename[:2]
if not nodename:
# no nodename return. Re-run current node
nodename = self.nodename
try: try:
# execute the found node, make use of the returns. # execute the found node, make use of the returns.
nodetext, options = self._execute_node(nodename, raw_string, **kwargs) nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
@ -746,12 +783,12 @@ class EvMenu(object):
desc = dic.get("desc", dic.get("text", None)) desc = dic.get("desc", dic.get("text", None))
if "_default" in keys: if "_default" in keys:
keys = [key for key in keys if key != "_default"] keys = [key for key in keys if key != "_default"]
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
self.default = (goto, goto_kwargs, execute, exec_kwargs) self.default = (goto, goto_kwargs, execute, exec_kwargs)
else: else:
# use the key (only) if set, otherwise use the running number # use the key (only) if set, otherwise use the running number
keys = list(make_iter(dic.get("key", str(inum + 1).strip()))) keys = list(make_iter(dic.get("key", str(inum + 1).strip())))
goto, goto_kwargs, execute, exec_kwargs = _extract_goto_exec(dic) goto, goto_kwargs, execute, exec_kwargs = self.extract_goto_exec(nodename, dic)
if keys: if keys:
display_options.append((keys[0], desc)) display_options.append((keys[0], desc))
for key in keys: for key in keys:
@ -765,7 +802,7 @@ class EvMenu(object):
# handle the helptext # handle the helptext
if helptext: if helptext:
self.helptext = helptext self.helptext = self.helptext_formatter(helptext)
elif options: elif options:
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
else: else:
@ -814,6 +851,51 @@ class EvMenu(object):
if self.cmd_on_exit is not None: if self.cmd_on_exit is not None:
self.cmd_on_exit(self.caller, self) self.cmd_on_exit(self.caller, self)
def print_debug_info(self, arg):
"""
Messages the caller with the current menu state, for debug purposes.
Args:
arg (str): Arg to debug instruction, either nothing, 'full' or the name
of a property to inspect.
"""
all_props = inspect.getmembers(self)
all_methods = [name for name, _ in inspect.getmembers(self, predicate=inspect.ismethod)]
all_builtins = [name for name, _ in inspect.getmembers(self, predicate=inspect.isbuiltin)]
props = {prop: value for prop, value in all_props if prop not in all_methods and
prop not in all_builtins and not prop.endswith("__")}
local = {key: var for key, var in locals().items()
if key not in all_props and not key.endswith("__")}
if arg:
if arg in props:
debugtxt = " |y* {}:|n\n{}".format(arg, props[arg])
elif arg in local:
debugtxt = " |y* {}:|n\n{}".format(arg, local[arg])
elif arg == 'full':
debugtxt = ("|yMENU DEBUG full ... |n\n" + "\n".join(
"|y *|n {}: {}".format(key, val)
for key, val in sorted(props.items())) +
"\n |yLOCAL VARS:|n\n" + "\n".join(
"|y *|n {}: {}".format(key, val)
for key, val in sorted(local.items())) +
"\n |y... END MENU DEBUG|n")
else:
debugtxt = "|yUsage: menudebug full|<name of property>|n"
else:
debugtxt = ("|yMENU DEBUG properties ... |n\n" + "\n".join(
"|y *|n {}: {}".format(
key, crop(to_str(val, force_string=True), width=50))
for key, val in sorted(props.items())) +
"\n |yLOCAL VARS:|n\n" + "\n".join(
"|y *|n {}: {}".format(
key, crop(to_str(val, force_string=True), width=50))
for key, val in sorted(local.items())) +
"\n |y... END MENU DEBUG|n")
self.caller.msg(debugtxt)
def parse_input(self, raw_string): def parse_input(self, raw_string):
""" """
Parses the incoming string from the menu user. Parses the incoming string from the menu user.
@ -840,6 +922,8 @@ class EvMenu(object):
self.display_helptext() self.display_helptext()
elif self.auto_quit and cmd in ("quit", "q", "exit"): elif self.auto_quit and cmd in ("quit", "q", "exit"):
self.close_menu() self.close_menu()
elif self.debug_mode and cmd.startswith("menudebug"):
self.print_debug_info(cmd[9:].strip())
elif self.default: elif self.default:
goto, goto_kwargs, execfunc, exec_kwargs = self.default goto, goto_kwargs, execfunc, exec_kwargs = self.default
self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs) self.run_exec_then_goto(execfunc, goto, raw_string, exec_kwargs, goto_kwargs)
@ -865,7 +949,20 @@ class EvMenu(object):
nodetext (str): The formatted node text. 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): def options_formatter(self, optionlist):
""" """
@ -945,14 +1042,188 @@ class EvMenu(object):
node (str): The formatted node to display. node (str): The formatted node to display.
""" """
if self._session:
screen_width = self._session.protocol_flags.get(
"SCREENWIDTH", {0: _MAX_TEXT_WIDTH})[0]
else:
screen_width = _MAX_TEXT_WIDTH
nodetext_width_max = max(m_len(line) for line in nodetext.split("\n")) nodetext_width_max = max(m_len(line) for line in nodetext.split("\n"))
options_width_max = max(m_len(line) for line in optionstext.split("\n")) options_width_max = max(m_len(line) for line in optionstext.split("\n"))
total_width = max(options_width_max, nodetext_width_max) total_width = min(screen_width, max(options_width_max, nodetext_width_max))
separator1 = "_" * total_width + "\n\n" if nodetext_width_max else "" separator1 = "_" * total_width + "\n\n" if nodetext_width_max else ""
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else "" separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext return separator1 + "|n" + nodetext + "|n" + separator2 + "|n" + optionstext
# -----------------------------------------------------------
#
# List node (decorator turning a node into a list with
# look/edit/add functionality for the elements)
#
# -----------------------------------------------------------
def list_node(option_generator, select=None, pagesize=10):
"""
Decorator for making an EvMenu node into a multi-page list node. Will add new options,
prepending those options added in the node.
Args:
option_generator (callable or list): A list of strings indicating the options, or a callable
that is called as option_generator(caller) to produce such a list.
select (callable or str, optional): Node to redirect a selection to. Its `**kwargs` will
contain the `available_choices` list and `selection` will hold one of the elements in
that list. If a callable, it will be called as select(caller, menuchoice) where
menuchoice is the chosen option as a string. Should return the target node to goto after
this selection (or None to repeat the list-node). Note that if this is not given, the
decorated node must itself provide a way to continue from the node!
pagesize (int): How many options to show per page.
Example:
@list_node(['foo', 'bar'], select)
def node_index(caller):
text = "describing the list"
return text, []
Notes:
All normal `goto` or `exec` callables returned from the decorated nodes will, if they accept
**kwargs, get a new kwarg 'available_choices' injected. These are the ordered list of named
options (descs) visible on the current node page.
"""
def decorator(func):
def _select_parser(caller, raw_string, **kwargs):
"""
Parse the select action
"""
available_choices = kwargs.get("available_choices", [])
try:
index = int(raw_string.strip()) - 1
selection = available_choices[index]
except Exception:
caller.msg("|rInvalid choice.|n")
else:
if callable(select):
try:
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:
# we assume a string was given, we inject the result into the kwargs
# to pass on to the next node
kwargs['selection'] = selection
return str(select)
# this means the previous node will be re-run with these same kwargs
return None
def _list_node(caller, raw_string, **kwargs):
option_list = option_generator(caller) \
if callable(option_generator) else option_generator
npages = 0
page_index = 0
page = []
options = []
if option_list:
nall_options = len(option_list)
pages = [option_list[ind:ind + pagesize]
for ind in range(0, nall_options, pagesize)]
npages = len(pages)
page_index = max(0, min(npages - 1, kwargs.get("optionpage_index", 0)))
page = pages[page_index]
text = ""
extra_text = None
# dynamic, multi-page option list. Each selection leads to the `select`
# callback being called with a result from the available choices
options.extend([{"desc": opt,
"goto": (_select_parser,
{"available_choices": page})} for opt in page])
if npages > 1:
# if the goto callable returns None, the same node is rerun, and
# kwargs not used by the callable are passed on to the node. This
# allows us to call ourselves over and over, using different kwargs.
options.append({"key": ("|Wcurrent|n", "c"),
"desc": "|W({}/{})|n".format(page_index + 1, npages),
"goto": (lambda caller: None,
{"optionpage_index": page_index})})
if page_index > 0:
options.append({"key": ("|wp|Wrevious page|n", "p"),
"goto": (lambda caller: None,
{"optionpage_index": page_index - 1})})
if page_index < npages - 1:
options.append({"key": ("|wn|Wext page|n", "n"),
"goto": (lambda caller: None,
{"optionpage_index": page_index + 1})})
# add data from the decorated node
decorated_options = []
supports_kwargs = bool(getargspec(func).keywords)
try:
if supports_kwargs:
text, decorated_options = func(caller, raw_string, **kwargs)
else:
text, decorated_options = func(caller, raw_string)
except TypeError:
try:
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, 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:
signature = eopt[cback]
if callable(signature):
# callable with no kwargs defined
eopt[cback] = (signature, {"available_choices": page})
elif is_iter(signature):
if len(signature) > 1 and isinstance(signature[1], dict):
signature[1]["available_choices"] = page
eopt[cback] = signature
elif signature:
# a callable alone in a tuple (i.e. no previous kwargs)
eopt[cback] = (signature[0], {"available_choices": page})
else:
# malformed input.
logger.log_err("EvMenu @list_node decorator found "
"malformed option to decorate: {}".format(eopt))
extra_options.append(eopt)
options.extend(extra_options)
text = text + "\n\n" + extra_text if extra_text else text
return text, options
return _list_node
return decorator
# ------------------------------------------------------------------------------------------------- # -------------------------------------------------------------------------------------------------
# #
# Simple input shortcuts # Simple input shortcuts

View file

@ -122,7 +122,8 @@ class EvMore(object):
""" """
def __init__(self, caller, text, always_page=False, session=None, 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. Initialization of the text handler.
@ -141,6 +142,10 @@ class EvMore(object):
page being completely filled, exit pager immediately. If unset, page being completely filled, exit pager immediately. If unset,
another move forward is required to exit. If set, the pager another move forward is required to exit. If set, the pager
exit message will not be shown. 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 kwargs (any, optional): These will be passed on
to the `caller.msg` method. to the `caller.msg` method.
@ -151,6 +156,7 @@ class EvMore(object):
self._npages = [] self._npages = []
self._npos = [] self._npos = []
self.exit_on_lastpage = exit_on_lastpage self.exit_on_lastpage = exit_on_lastpage
self.exit_cmd = exit_cmd
self._exit_msg = "Exited |wmore|n pager." self._exit_msg = "Exited |wmore|n pager."
if not session: if not session:
# if not supplied, use the first session to # if not supplied, use the first session to
@ -202,15 +208,18 @@ class EvMore(object):
# goto top of the text # goto top of the text
self.page_top() self.page_top()
def display(self): def display(self, show_footer=True):
""" """
Pretty-print the page. Pretty-print the page.
""" """
pos = self._pos pos = self._pos
text = self._pages[pos] text = self._pages[pos]
if show_footer:
page = _DISPLAY.format(text=text, page = _DISPLAY.format(text=text,
pageno=pos + 1, pageno=pos + 1,
pagemax=self._npages) pagemax=self._npages)
else:
page = text
# check to make sure our session is still valid # check to make sure our session is still valid
sessions = self._caller.sessions.get() sessions = self._caller.sessions.get()
if not sessions: if not sessions:
@ -245,9 +254,11 @@ class EvMore(object):
self.page_quit() self.page_quit()
else: else:
self._pos += 1 self._pos += 1
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
self.display(show_footer=False)
self.page_quit(quiet=True)
else:
self.display() self.display()
if self.exit_on_lastpage and self._pos >= self._npages - 1:
self.page_quit()
def page_back(self): def page_back(self):
""" """
@ -256,16 +267,20 @@ class EvMore(object):
self._pos = max(0, self._pos - 1) self._pos = max(0, self._pos - 1)
self.display() self.display()
def page_quit(self): def page_quit(self, quiet=False):
""" """
Quit the pager Quit the pager
""" """
del self._caller.ndb._more del self._caller.ndb._more
if not quiet:
self._caller.msg(text=self._exit_msg, **self._kwargs) self._caller.msg(text=self._exit_msg, **self._kwargs)
self._caller.cmdset.remove(CmdSetMore) 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. 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 justify_kwargs (dict, bool or None, optional): If given, this should
be valid keyword arguments to the utils.justify() function. If False, be valid keyword arguments to the utils.justify() function. If False,
no justification will be done. 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 kwargs (any, optional): These will be passed on
to the `caller.msg` method. to the `caller.msg` method.
""" """
EvMore(caller, text, always_page=always_page, session=session, EvMore(caller, text, always_page=always_page, session=session,
justify_kwargs=justify_kwargs, **kwargs) justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)

View file

@ -893,6 +893,9 @@ class EvColumn(object):
""" """
col = self.column col = self.column
# fixed options for the column will override those requested in the call!
# this is particularly relevant to things like width/height, to avoid
# fixed-widths columns from being auto-balanced
kwargs.update(self.options) kwargs.update(self.options)
# use fixed width or adjust to the largest cell # use fixed width or adjust to the largest cell
if "width" not in kwargs: if "width" not in kwargs:
@ -1283,24 +1286,58 @@ class EvTable(object):
cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable] cwidths_min = [max(cell.get_min_width() for cell in col) for col in self.worktable]
cwmin = sum(cwidths_min) cwmin = sum(cwidths_min)
if cwmin > width: # get which cols have separately set widths - these should be locked
# we cannot shrink any more # note that we need to remove cwidths_min for each lock to avoid counting
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin)) # it twice (in cwmin and in locked_cols)
locked_cols = {icol: col.options['width'] - cwidths_min[icol]
for icol, col in enumerate(self.worktable) if 'width' in col.options}
locked_width = sum(locked_cols.values())
excess = width - cwmin - locked_width
if len(locked_cols) >= ncols and excess:
# we can't adjust the width at all - all columns are locked
raise Exception("Cannot balance table to width %s - "
"all columns have a set, fixed width summing to %s!" % (
self.width, sum(cwidths)))
if excess < 0:
# the locked cols makes it impossible
raise Exception("Cannot shrink table width to %s. "
"Minimum size (and/or fixed-width columns) "
"sets minimum at %s." % (self.width, cwmin + locked_width))
excess = width - cwmin
if self.evenwidth: if self.evenwidth:
# make each column of equal width # make each column of equal width
for _ in range(excess): # use cwidths as a work-array to track weights
cwidths = copy(cwidths_min)
correction = 0
while correction < excess:
# flood-fill the minimum table starting with the smallest columns # flood-fill the minimum table starting with the smallest columns
ci = cwidths_min.index(min(cwidths_min)) ci = cwidths.index(min(cwidths))
if ci in locked_cols:
# locked column, make sure it's not picked again
cwidths[ci] += 9999
cwidths_min[ci] = locked_cols[ci]
else:
cwidths_min[ci] += 1 cwidths_min[ci] += 1
correction += 1
cwidths = cwidths_min cwidths = cwidths_min
else: else:
# make each column expand more proportional to their data size # make each column expand more proportional to their data size
for _ in range(excess): # we use cwidth as a work-array to track weights
correction = 0
while correction < excess:
# fill wider columns first # fill wider columns first
ci = cwidths.index(max(cwidths)) ci = cwidths.index(max(cwidths))
if ci in locked_cols:
# locked column, make sure it's not picked again
cwidths[ci] -= 9999
cwidths_min[ci] = locked_cols[ci]
else:
cwidths_min[ci] += 1 cwidths_min[ci] += 1
correction += 1
# give a just changed col less prio next run
cwidths[ci] -= 3 cwidths[ci] -= 3
cwidths = cwidths_min cwidths = cwidths_min
@ -1323,28 +1360,46 @@ class EvTable(object):
for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)] for cell in (col[iy] for col in self.worktable)) for iy in range(nrowmax)]
chmin = sum(cheights_min) chmin = sum(cheights_min)
# get which cols have separately set heights - these should be locked
# note that we need to remove cheights_min for each lock to avoid counting
# it twice (in chmin and in locked_cols)
locked_cols = {icol: col.options['height'] - cheights_min[icol]
for icol, col in enumerate(self.worktable) if 'height' in col.options}
locked_height = sum(locked_cols.values())
excess = self.height - chmin - locked_height
if chmin > self.height: if chmin > self.height:
# we cannot shrink any more # we cannot shrink any more
raise Exception("Cannot shrink table height to %s. Minimum size is %s." % (self.height, chmin)) raise Exception("Cannot shrink table height to %s. Minimum "
"size (and/or fixed-height rows) sets minimum at %s." % (
self.height, chmin + locked_height))
# now we add all the extra height up to the desired table-height. # now we add all the extra height up to the desired table-height.
# We do this so that the tallest cells gets expanded first (and # We do this so that the tallest cells gets expanded first (and
# thus avoid getting cropped) # thus avoid getting cropped)
excess = self.height - chmin
even = self.height % 2 == 0 even = self.height % 2 == 0
for position in range(excess): correction = 0
while correction < excess:
# expand the cells with the most rows first # expand the cells with the most rows first
if 0 <= position < nrowmax and nrowmax > 1: if 0 <= correction < nrowmax and nrowmax > 1:
# avoid adding to header first round (looks bad on very small tables) # avoid adding to header first round (looks bad on very small tables)
ci = cheights[1:].index(max(cheights[1:])) + 1 ci = cheights[1:].index(max(cheights[1:])) + 1
else: else:
ci = cheights.index(max(cheights)) ci = cheights.index(max(cheights))
if ci in locked_cols:
# locked row, make sure it's not picked again
cheights[ci] -= 9999
cheights_min[ci] = locked_cols[ci]
else:
cheights_min[ci] += 1 cheights_min[ci] += 1
# change balance
if ci == 0 and self.header: if ci == 0 and self.header:
# it doesn't look very good if header expands too fast # it doesn't look very good if header expands too fast
cheights[ci] -= 2 if even else 3 cheights[ci] -= 2 if even else 3
cheights[ci] -= 2 if even else 1 cheights[ci] -= 2 if even else 1
correction += 1
cheights = cheights_min cheights = cheights_min
# we must tell cells to crop instead of expanding # we must tell cells to crop instead of expanding
@ -1554,6 +1609,8 @@ class EvTable(object):
""" """
if index > len(self.table): if index > len(self.table):
raise Exception("Not a valid column index") raise Exception("Not a valid column index")
# we update the columns' options which means eventual width/height
# will be 'locked in' and withstand auto-balancing width/height from the table later
self.table[index].options.update(kwargs) self.table[index].options.update(kwargs)
self.table[index].reformat(**kwargs) self.table[index].reformat(**kwargs)
@ -1569,6 +1626,7 @@ class EvTable(object):
def __str__(self): def __str__(self):
"""print table (this also balances it)""" """print table (this also balances it)"""
# h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()]))) return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()])))
def __unicode__(self): def __unicode__(self):

View file

@ -61,8 +61,10 @@ Error handling:
""" """
import re import re
import fnmatch
from django.conf import settings from django.conf import settings
from evennia.utils import utils
from evennia.utils import utils, logger
# example/testing inline functions # example/testing inline functions
@ -157,12 +159,32 @@ def clr(*args, **kwargs):
return text 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 # we specify a default nomatch function to use if no matching func was
# found. This will be overloaded by any nomatch function defined in # found. This will be overloaded by any nomatch function defined in
# the imported modules. # the imported modules.
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>", _DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"} "stackfull": lambda *args, **kwargs: "\n (not parsed: "}
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
# load custom inline func modules. # load custom inline func modules.
for module in utils.make_iter(settings.INLINEFUNC_MODULES): for module in utils.make_iter(settings.INLINEFUNC_MODULES):
@ -172,15 +194,12 @@ for module in utils.make_iter(settings.INLINEFUNC_MODULES):
if module == "server.conf.inlinefuncs": if module == "server.conf.inlinefuncs":
# a temporary warning since the default module changed name # a temporary warning since the default module changed name
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should " 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) "be renamed to mygame/server/conf/inlinefuncs.py (note "
"the S at the end)." % err)
else: else:
raise 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. # The stack size is a security measure. Set to <=0 to disable.
try: try:
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE _STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
@ -189,18 +208,21 @@ except AttributeError:
# regex definitions # regex definitions
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call) _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""" _RE_TOKEN = re.compile(r"""
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # unescaped single-triples (escapes all inside them) (?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # unescaped normal triple quotes (escapes all inside them) (?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
(?P<comma>(?<!\\)\,)| # unescaped , (argument separator) (?P<comma>(?<!\\)\,)| # , (argument sep)
(?P<end>(?<!\\)\))| # unescaped ) (end of function call) (?P<end>(?<!\\)\))| # ) (possible end of func call)
(?P<start>(?<!\\)\$\w+\()| # unescaped $funcname( (start of function call) (?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
(?P<escaped>\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text (?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
(?P<rest>[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""", (?P<escaped> # escaped tokens to re-insert sans backslash
re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL) \\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
(?P<rest> # everything else to re-insert verbatim
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
# Cache for function lookups. # Cache for function lookups.
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000) _PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
@ -257,7 +279,7 @@ class InlinefuncError(RuntimeError):
pass pass
def parse_inlinefunc(string, strip=False, **kwargs): def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
""" """
Parse the incoming string. Parse the incoming string.
@ -265,6 +287,9 @@ def parse_inlinefunc(string, strip=False, **kwargs):
string (str): The incoming string to parse. string (str): The incoming string to parse.
strip (bool, optional): Whether to strip function calls rather than strip (bool, optional): Whether to strip function calls rather than
execute them. 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: Kwargs:
session (Session): This is sent to this function by Evennia when triggering session (Session): This is sent to this function by Evennia when triggering
it. It is passed to the inlinefunc. it. It is passed to the inlinefunc.
@ -273,7 +298,17 @@ def parse_inlinefunc(string, strip=False, **kwargs):
""" """
global _PARSING_CACHE global _PARSING_CACHE
if string in _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 is already cached
stack = _PARSING_CACHE[string] stack = _PARSING_CACHE[string]
elif not _RE_STARTTOKEN.search(string): elif not _RE_STARTTOKEN.search(string):
@ -285,13 +320,36 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# process string on stack # process string on stack
ncallable = 0 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): for match in _RE_TOKEN.finditer(string):
gdict = match.groupdict() 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"]: if gdict["singlequote"]:
stack.append(gdict["singlequote"]) stack.append(gdict["singlequote"])
elif gdict["doublequote"]: elif gdict["doublequote"]:
stack.append(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"]: elif gdict["end"]:
if nlparens > 0:
nlparens -= 1
stack.append(")")
continue
if ncallable <= 0: if ncallable <= 0:
stack.append(")") stack.append(")")
continue continue
@ -309,10 +367,12 @@ def parse_inlinefunc(string, strip=False, **kwargs):
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1) funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
try: try:
# try to fetch the matching inlinefunc from storage # try to fetch the matching inlinefunc from storage
stack.append(_INLINE_FUNCS[funcname]) stack.append(available_funcs[funcname])
nvalid += 1
except KeyError: except KeyError:
stack.append(_INLINE_FUNCS["nomatch"]) stack.append(available_funcs["nomatch"])
stack.append(funcname) stack.append(funcname)
stack.append(None)
ncallable += 1 ncallable += 1
elif gdict["escaped"]: elif gdict["escaped"]:
# escaped tokens # escaped tokens
@ -335,11 +395,11 @@ def parse_inlinefunc(string, strip=False, **kwargs):
# this means not all inlinefuncs were complete # this means not all inlinefuncs were complete
return string return string
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack): if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
# if stack is larger than limit, throw away parsing # if stack is larger than limit, throw away parsing
return string + gdict["stackfull"](*args, **kwargs) return string + available_funcs["stackfull"](*args, **kwargs)
else: elif usecache:
# cache the stack # cache the stack - we do this also if we don't check the cache above
_PARSING_CACHE[string] = stack _PARSING_CACHE[string] = stack
# run the stack recursively # run the stack recursively
@ -362,9 +422,14 @@ def parse_inlinefunc(string, strip=False, **kwargs):
kwargs["inlinefunc_stack_depth"] = depth kwargs["inlinefunc_stack_depth"] = depth
retval = "" if strip else func(*args, **kwargs) retval = "" if strip else func(*args, **kwargs)
return utils.to_str(retval, force_string=True) 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 from the cache # execute the stack
return "".join(_run_stack(item) for item in _PARSING_CACHE[string]) return retval
# #
# Nick templating # Nick templating
@ -398,7 +463,7 @@ Custom arg markers
$N argument position (1-99) $N argument position (1-99)
""" """
import fnmatch
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)") _RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
_RE_NICK_TEMPLATE_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"\\ ") _RE_NICK_SPACE = re.compile(r"\\ ")
@ -439,7 +504,6 @@ def initialize_nick_templates(in_template, out_template):
# validate the tempaltes - they should at least have the same number of args # validate the tempaltes - they should at least have the same number of args
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template)) n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
if n_inargs != n_outargs: if n_inargs != n_outargs:
print n_inargs, n_outargs
raise NickTemplateInvalid raise NickTemplateInvalid
return re.compile(regex_string), template_string return re.compile(regex_string), template_string

View file

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

View file

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

View file

@ -20,18 +20,20 @@ import textwrap
import random import random
from os.path import join as osjoin from os.path import join as osjoin
from importlib import import_module 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 collections import defaultdict, OrderedDict
from twisted.internet import threads, reactor, task from twisted.internet import threads, reactor, task
from django.conf import settings from django.conf import settings
from django.utils import timezone from django.utils import timezone
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
from django.apps import apps
from evennia.utils import logger from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR _EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR _GAME_DIR = settings.GAME_DIR
try: try:
import cPickle as pickle import cPickle as pickle
except ImportError: except ImportError:
@ -42,8 +44,6 @@ _GA = object.__getattribute__
_SA = object.__setattr__ _SA = object.__setattr__
_DA = object.__delattr__ _DA = object.__delattr__
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
def is_iter(iterable): def is_iter(iterable):
""" """
@ -79,7 +79,7 @@ def make_iter(obj):
return not hasattr(obj, '__iter__') and [obj] or 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. 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. text (str): Properly wrapped text.
""" """
width = width if width else settings.CLIENT_DEFAULT_WIDTH
if not text: if not text:
return "" return ""
text = to_unicode(text) text = to_unicode(text)
@ -103,7 +104,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
fill = wrap fill = wrap
def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "): def pad(text, width=None, align="c", fillchar=" "):
""" """
Pads to a given width. Pads to a given width.
@ -118,6 +119,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
text (str): The padded text. text (str): The padded text.
""" """
width = width if width else settings.CLIENT_DEFAULT_WIDTH
align = align if align in ('c', 'l', 'r') else 'c' align = align if align in ('c', 'l', 'r') else 'c'
fillchar = fillchar[0] if fillchar else " " fillchar = fillchar[0] if fillchar else " "
if align == 'l': if align == 'l':
@ -128,7 +130,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
return text.center(width, 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 Crop text to a certain width, throwing away text from too-long
lines. lines.
@ -146,7 +148,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
text (str): The cropped text. text (str): The cropped text.
""" """
width = width if width else settings.CLIENT_DEFAULT_WIDTH
utext = to_unicode(text) utext = to_unicode(text)
ltext = len(utext) ltext = len(utext)
if ltext <= width: if ltext <= width:
@ -157,12 +159,16 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
return to_str(utext) return to_str(utext)
def dedent(text): def dedent(text, baseline_index=None):
""" """
Safely clean all whitespace at the left of a paragraph. Safely clean all whitespace at the left of a paragraph.
Args: Args:
text (str): The text to dedent. 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: Returns:
text (str): Dedented string. text (str): Dedented string.
@ -175,10 +181,17 @@ def dedent(text):
""" """
if not text: if not text:
return "" return ""
if baseline_index is None:
return textwrap.dedent(text) 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 Fully justify a text so that it fits inside `width`. When using
full justification (default) this will be done by padding between 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. justified (str): The justified and indented block of text.
""" """
width = width if width else settings.CLIENT_DEFAULT_WIDTH
def _process_line(line): def _process_line(line):
""" """
@ -208,18 +222,27 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
gap = " " # minimum gap between words gap = " " # minimum gap between words
if line_rest > 0: if line_rest > 0:
if align == 'l': if align == 'l':
if line[-1] == "\n\n":
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
else:
line[-1] += " " * line_rest line[-1] += " " * line_rest
elif align == 'r': elif align == 'r':
line[0] = " " * line_rest + line[0] line[0] = " " * line_rest + line[0]
elif align == 'c': elif align == 'c':
pad = " " * (line_rest // 2) pad = " " * (line_rest // 2)
line[0] = pad + line[0] line[0] = pad + line[0]
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) line[-1] = line[-1] + pad + " " * (line_rest % 2)
else: # align 'f' else: # align 'f'
gap += " " * (line_rest // max(1, ngaps)) gap += " " * (line_rest // max(1, ngaps))
rest_gap = line_rest % max(1, ngaps) rest_gap = line_rest % max(1, ngaps)
for i in range(rest_gap): for i in range(rest_gap):
line[i] += " " line[i] += " "
elif not any(line):
return [" " * width]
return gap.join(line) return gap.join(line)
# split into paragraphs and words # 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]) 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): def list_to_string(inlist, endsep="and", addquote=False):
""" """
This pretty-formats a list as string output, adding an optional This pretty-formats a list as string output, adding an optional
@ -932,16 +1011,16 @@ def delay(timedelay, callback, *args, **kwargs):
Args: Args:
timedelay (int or float): The delay in seconds timedelay (int or float): The delay in seconds
callback (callable): Will be called with optional callback (callable): Will be called as `callback(*args, **kwargs)`
arguments after `timedelay` seconds. after `timedelay` seconds.
args (any, optional): Will be used as arguments to callback args (any, optional): Will be used as arguments to callback
Kwargs: Kwargs:
persistent (bool, optional): should make the delay persistent persistent (bool, optional): should make the delay persistent
over a reboot or reload over a reboot or reload
any (any): Will be used to call the callback. any (any): Will be used as keyword arguments to callback.
Returns: Returns:
deferred (deferred): Will fire fire with callback after deferred (deferred): Will fire with callback after
`timedelay` seconds. Note that if `timedelay()` is used in the `timedelay` seconds. Note that if `timedelay()` is used in the
commandhandler callback chain, the callback chain can be commandhandler callback chain, the callback chain can be
defined directly in the command body and don't need to be defined directly in the command body and don't need to be
@ -1546,6 +1625,7 @@ def format_table(table, extra_space=1):
Examples: Examples:
```python ```python
ftable = format_table([[...], [...], ...])
for ir, row in enumarate(ftable): for ir, row in enumarate(ftable):
if ir == 0: if ir == 0:
# make first row white # make first row white
@ -1879,3 +1959,29 @@ def get_game_dir_path():
else: else:
os.chdir(os.pardir) os.chdir(os.pardir)
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.") raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
def get_all_typeclasses(parent=None):
"""
List available typeclasses from all available modules.
Args:
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
from this parent.
Returns:
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
Notes:
This will dynamicall retrieve all abstract django models inheriting at any distance
from the TypedObject base (aka a Typeclass) so it will work fine with any custom
classes being added.
"""
from evennia.typeclasses.models import TypedObject
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
for model in apps.get_models() if TypedObject in getmro(model)}
if parent:
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
if inherits_from(typeclass, parent)}
return typeclasses

View file

@ -8,10 +8,11 @@
--- */ --- */
/* Overall element look */ /* Overall element look */
html, body, #clientwrapper { height: 100% } html, body {
height: 100%;
width: 100%;
}
body { body {
margin: 0;
padding: 0;
background: #000; background: #000;
color: #ccc; color: #ccc;
font-size: .9em; font-size: .9em;
@ -19,6 +20,12 @@ body {
line-height: 1.6em; line-height: 1.6em;
overflow: hidden; overflow: hidden;
} }
@media screen and (max-width: 480px) {
body {
font-size: .5rem;
line-height: .7rem;
}
}
a:link, a:visited { color: inherit; } a:link, a:visited { color: inherit; }
@ -74,93 +81,109 @@ div {margin:0px;}
} }
/* Style specific classes corresponding to formatted, narative text. */ /* Style specific classes corresponding to formatted, narative text. */
.wrapper {
height: 100%;
}
/* Container surrounding entire client */ /* Container surrounding entire client */
#wrapper { #clientwrapper {
position: relative; height: 100%;
height: 100%
} }
/* Main scrolling message area */ /* Main scrolling message area */
#messagewindow { #messagewindow {
position: absolute; overflow-y: auto;
overflow: auto; overflow-x: hidden;
padding: 1em; overflow-wrap: break-word;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
top: 0;
left: 0;
right: 0;
bottom: 70px;
} }
/* Input area containing input field and button */ #messagewindow {
#inputform { overflow-y: auto;
position: absolute; overflow-x: hidden;
width: 100%; overflow-wrap: break-word;
padding: 0;
bottom: 0;
margin: 0;
padding-bottom: 10px;
border-top: 1px solid #555;
}
#inputcontrol {
width: 100%;
padding: 0;
} }
/* Input field */ /* Input field */
#inputfield, #inputsend, #inputsizer { #inputfield, #inputsizer {
display: block; height: 100%;
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
height: 50px;
background: #000; background: #000;
color: #fff; color: #fff;
padding: 0 .45em; padding: 0 .45rem;
font-size: 1.1em; font-size: 1.1rem;
font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace; font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace;
}
#inputfield, #inputsizer {
float: left;
width: 95%;
border: 0;
resize: none; resize: none;
line-height: normal; }
#inputsend {
height: 100%;
}
#inputcontrol {
height: 100%;
} }
#inputfield:focus { #inputfield:focus {
outline: 0;
}
#inputsizer {
margin-left: -9999px;
}
/* Input 'send' button */
#inputsend {
float: right;
width: 3%;
max-width: 25px;
margin-right: 10px;
border: 0;
background: #555;
} }
/* prompt area above input field */ /* prompt area above input field */
#prompt { .prompt {
margin-top: 10px; max-height: 3rem;
padding: 0 .45em; }
#splitbutton {
width: 2rem;
font-size: 2rem;
color: #a6a6a6;
background-color: transparent;
border: 0px;
}
#splitbutton:hover {
color: white;
cursor: pointer;
}
#panebutton {
width: 2rem;
font-size: 2rem;
color: #a6a6a6;
background-color: transparent;
border: 0px;
}
#panebutton:hover {
color: white;
cursor: pointer;
}
#undobutton {
width: 2rem;
font-size: 2rem;
color: #a6a6a6;
background-color: transparent;
border: 0px;
}
#undobutton:hover {
color: white;
cursor: pointer;
}
.button {
width: fit-content;
padding: 1em;
color: black;
border: 1px solid black;
background-color: darkgray;
margin: 0 auto;
}
.splitbutton:hover {
cursor: pointer;
} }
#optionsbutton { #optionsbutton {
width: 40px; width: 2rem;
font-size: 20px; font-size: 2rem;
color: #a6a6a6; color: #a6a6a6;
background-color: transparent; background-color: transparent;
border: 0px; border: 0px;
@ -173,8 +196,8 @@ div {margin:0px;}
#toolbar { #toolbar {
position: fixed; position: fixed;
top: 0; top: .5rem;
right: 5px; right: .5rem;
z-index: 1; z-index: 1;
} }
@ -248,6 +271,52 @@ div {margin:0px;}
text-decoration: none; text-decoration: none;
cursor: pointer; cursor: pointer;
} }
.gutter.gutter-vertical {
cursor: row-resize;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAB4AAAAFAQMAAABo7865AAAABlBMVEVHcEzMzMzyAv2sAAAAAXRSTlMAQObYZgAAABBJREFUeF5jOAMEEAIEEFwAn3kMwcB6I2AAAAAASUVORK5CYII=')
}
.gutter.gutter-horizontal {
cursor: col-resize;
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAUAAAAeCAYAAADkftS9AAAAIklEQVQoU2M4c+bMfxAGAgYYmwGrIIiDjrELjpo5aiZeMwF+yNnOs5KSvgAAAABJRU5ErkJggg==')
}
.split {
-webkit-box-sizing: border-box;
-moz-box-sizing: border-box;
box-sizing: border-box;
overflow-y: auto;
overflow-x: hidden;
}
.split-sub {
padding: .5rem;
}
.content {
border: 1px solid #C0C0C0;
box-shadow: inset 0 1px 2px #e4e4e4;
background-color: black;
padding: 1rem;
}
@media screen and (max-width: 480px) {
.content {
padding: .5rem;
}
}
.gutter {
background-color: grey;
background-repeat: no-repeat;
background-position: 50%;
}
.split.split-horizontal, .gutter.gutter-horizontal {
height: 100%;
float: left;
}
/* XTERM256 colors */ /* XTERM256 colors */

View file

@ -0,0 +1,145 @@
// Use split.js to create a basic ui
var SplitHandler = (function () {
var split_panes = {};
var backout_list = new Array;
var set_pane_types = function(splitpane, types) {
split_panes[splitpane]['types'] = types;
}
var dynamic_split = function(splitpane, direction, pane_name1, pane_name2, update_method1, update_method2, sizes) {
// find the sub-div of the pane we are being asked to split
splitpanesub = splitpane + '-sub';
// create the new div stack to replace the sub-div with.
var first_div = $( '<div id="'+pane_name1+'" class="split split-'+direction+'" />' )
var first_sub = $( '<div id="'+pane_name1+'-sub" class="split-sub" />' )
var second_div = $( '<div id="'+pane_name2+'" class="split split-'+direction+'" />' )
var second_sub = $( '<div id="'+pane_name2+'-sub" class="split-sub" />' )
// check to see if this sub-pane contains anything
contents = $('#'+splitpanesub).contents();
if( contents ) {
// it does, so move it to the first new div-sub (TODO -- selectable between first/second?)
contents.appendTo(first_sub);
}
first_div.append( first_sub );
second_div.append( second_sub );
// update the split_panes array to remove this pane name, but store it for the backout stack
var backout_settings = split_panes[splitpane];
delete( split_panes[splitpane] );
// now vaporize the current split_N-sub placeholder and create two new panes.
$('#'+splitpane).append(first_div);
$('#'+splitpane).append(second_div);
$('#'+splitpane+'-sub').remove();
// And split
Split(['#'+pane_name1,'#'+pane_name2], {
direction: direction,
sizes: sizes,
gutterSize: 4,
minSize: [50,50],
});
// store our new split sub-divs for future splits/uses by the main UI.
split_panes[pane_name1] = { 'types': [], 'update_method': update_method1 };
split_panes[pane_name2] = { 'types': [], 'update_method': update_method2 };
// add our new split to the backout stack
backout_list.push( {'pane1': pane_name1, 'pane2': pane_name2, 'undo': backout_settings} );
}
var undo_split = function() {
// pop off the last split pair
var back = backout_list.pop();
if( !back ) {
return;
}
// Collect all the divs/subs in play
var pane1 = back['pane1'];
var pane2 = back['pane2'];
var pane1_sub = $('#'+pane1+'-sub');
var pane2_sub = $('#'+pane2+'-sub');
var pane1_parent = $('#'+pane1).parent();
var pane2_parent = $('#'+pane2).parent();
if( pane1_parent.attr('id') != pane2_parent.attr('id') ) {
// sanity check failed...somebody did something weird...bail out
console.log( pane1 );
console.log( pane2 );
console.log( pane1_parent );
console.log( pane2_parent );
return;
}
// create a new sub-pane in the panes parent
var parent_sub = $( '<div id="'+pane1_parent.attr('id')+'-sub" class="split-sub" />' )
// check to see if the special #messagewindow is in either of our sub-panes.
var msgwindow = pane1_sub.find('#messagewindow')
if( !msgwindow ) {
//didn't find it in pane 1, try pane 2
msgwindow = pane2_sub.find('#messagewindow')
}
if( msgwindow ) {
// It is, so collect all contents into it instead of our parent_sub div
// then move it to parent sub div, this allows future #messagewindow divs to flow properly
msgwindow.append( pane1_sub.contents() );
msgwindow.append( pane2_sub.contents() );
parent_sub.append( msgwindow );
} else {
//didn't find it, so move the contents of the two panes' sub-panes into the new sub-pane
parent_sub.append( pane1_sub.contents() );
parent_sub.append( pane2_sub.contents() );
}
// clear the parent
pane1_parent.empty();
// add the new sub-pane back to the parent div
pane1_parent.append(parent_sub);
// pull the sub-div's from split_panes
delete split_panes[pane1];
delete split_panes[pane2];
// add our parent pane back into the split_panes list for future splitting
split_panes[pane1_parent.attr('id')] = back['undo'];
}
var init = function(settings) {
//change Mustache tags to ruby-style (Django gets mad otherwise)
var customTags = [ '<%', '%>' ];
Mustache.tags = customTags;
var input_template = $('#input-template').html();
Mustache.parse(input_template);
Split(['#main','#input'], {
direction: 'vertical',
sizes: [90,10],
gutterSize: 4,
minSize: [50,50],
});
split_panes['main'] = { 'types': [], 'update_method': 'append' };
var input_render = Mustache.render(input_template);
$('[data-role-input]').html(input_render);
console.log("SplitHandler initialized");
}
return {
init: init,
set_pane_types: set_pane_types,
dynamic_split: dynamic_split,
split_panes: split_panes,
undo_split: undo_split,
}
})();

View file

@ -15,8 +15,13 @@
(function () { (function () {
"use strict" "use strict"
var num_splits = 0; //unique id counter for default split-panel names
var options = {}; var options = {};
var known_types = new Array();
known_types.push('help');
// //
// GUI Elements // GUI Elements
// //
@ -106,6 +111,7 @@ function togglePopup(dialogname, content) {
// Grab text from inputline and send to Evennia // Grab text from inputline and send to Evennia
function doSendText() { function doSendText() {
console.log("sending text");
if (!Evennia.isConnected()) { if (!Evennia.isConnected()) {
var reconnect = confirm("Not currently connected. Reconnect?"); var reconnect = confirm("Not currently connected. Reconnect?");
if (reconnect) { if (reconnect) {
@ -158,7 +164,11 @@ function onKeydown (event) {
var code = event.which; var code = event.which;
var history_entry = null; var history_entry = null;
var inputfield = $("#inputfield"); var inputfield = $("#inputfield");
inputfield.focus(); if (code === 9) {
return;
}
//inputfield.focus();
if (code === 13) { // Enter key sends text if (code === 13) { // Enter key sends text
doSendText(); doSendText();
@ -205,74 +215,68 @@ function onKeyPress (event) {
} }
var resizeInputField = function () { var resizeInputField = function () {
var min_height = 50;
var max_height = 300;
var prev_text_len = 0;
// Check to see if we should change the height of the input area
return function() { return function() {
var inputfield = $("#inputfield"); var wrapper = $("#inputform")
var scrollh = inputfield.prop("scrollHeight"); var input = $("#inputcontrol")
var clienth = inputfield.prop("clientHeight"); var prompt = $("#prompt")
var newh = 0;
var curr_text_len = inputfield.val().length;
if (scrollh > clienth && scrollh <= max_height) { input.height(wrapper.height() - (input.offset().top - wrapper.offset().top));
// Need to make it bigger
newh = scrollh;
}
else if (curr_text_len < prev_text_len) {
// There is less text in the field; try to make it smaller
// To avoid repaints, we draw the text in an offscreen element and
// determine its dimensions.
var sizer = $('#inputsizer')
.css("width", inputfield.prop("clientWidth"))
.text(inputfield.val());
newh = sizer.prop("scrollHeight");
}
if (newh != 0) {
newh = Math.min(newh, max_height);
if (clienth != newh) {
inputfield.css("height", newh + "px");
doWindowResize();
}
}
prev_text_len = curr_text_len;
} }
}(); }();
// Handle resizing of client // Handle resizing of client
function doWindowResize() { function doWindowResize() {
var formh = $('#inputform').outerHeight(true); resizeInputField();
var message_scrollh = $("#messagewindow").prop("scrollHeight"); var resizable = $("[data-update-append]");
$("#messagewindow") var parents = resizable.closest(".split")
.css({"bottom": formh}) // leave space for the input form parents.animate({
.scrollTop(message_scrollh); // keep the output window scrolled to the bottom scrollTop: parents.prop("scrollHeight")
}, 0);
} }
// Handle text coming from the server // Handle text coming from the server
function onText(args, kwargs) { function onText(args, kwargs) {
// append message to previous ones, then scroll so latest is at var use_default_pane = true;
// the bottom. Send 'cls' kwarg to modify the output class.
var renderto = "main"; if ( kwargs && 'type' in kwargs ) {
if (kwargs["type"] == "help") { var msgtype = kwargs['type'];
if (("helppopup" in options) && (options["helppopup"])) { if ( ! known_types.includes(msgtype) ) {
renderto = "#helpdialog"; // this is a new output type that can be mapped to panes
console.log('detected new output type: ' + msgtype)
known_types.push(msgtype);
}
// pass this message to each pane that has this msgtype mapped
if( SplitHandler ) {
for ( var key in SplitHandler.split_panes) {
var pane = SplitHandler.split_panes[key];
// is this message type mapped to this pane?
if ( (pane['types'].length > 0) && pane['types'].includes(msgtype) ) {
// yes, so append/replace this pane's inner div with this message
var text_div = $('#'+key+'-sub');
if ( pane['update_method'] == 'replace' ) {
text_div.html(args[0])
} else {
text_div.append(args[0]);
var scrollHeight = text_div.parent().prop("scrollHeight");
text_div.parent().animate({ scrollTop: scrollHeight }, 0);
}
// record sending this message to a pane, no need to update the default div
use_default_pane = false;
}
}
} }
} }
if (renderto == "main") { // append message to default pane, then scroll so latest is at the bottom.
if(use_default_pane) {
var mwin = $("#messagewindow"); var mwin = $("#messagewindow");
var cls = kwargs == null ? 'out' : kwargs['cls']; var cls = kwargs == null ? 'out' : kwargs['cls'];
mwin.append("<div class='" + cls + "'>" + args[0] + "</div>"); mwin.append("<div class='" + cls + "'>" + args[0] + "</div>");
mwin.animate({ var scrollHeight = mwin.parent().parent().prop("scrollHeight");
scrollTop: document.getElementById("messagewindow").scrollHeight mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0);
}, 0);
onNewLine(args[0], null); onNewLine(args[0], null);
} else {
openPopup(renderto, args[0]);
} }
} }
@ -430,6 +434,105 @@ function doStartDragDialog(event) {
$(document).bind("mouseup", undrag); $(document).bind("mouseup", undrag);
} }
function onSplitDialogClose() {
var pane = $("input[name=pane]:checked").attr("value");
var direction = $("input[name=direction]:checked").attr("value");
var new_pane1 = $("input[name=new_pane1]").val();
var new_pane2 = $("input[name=new_pane2]").val();
var flow1 = $("input[name=flow1]:checked").attr("value");
var flow2 = $("input[name=flow2]:checked").attr("value");
if( new_pane1 == "" ) {
new_pane1 = 'pane_'+num_splits;
num_splits++;
}
if( new_pane2 == "" ) {
new_pane2 = 'pane_'+num_splits;
num_splits++;
}
if( document.getElementById(new_pane1) ) {
alert('An element: "' + new_pane1 + '" already exists');
return;
}
if( document.getElementById(new_pane2) ) {
alert('An element: "' + new_pane2 + '" already exists');
return;
}
SplitHandler.dynamic_split( pane, direction, new_pane1, new_pane2, flow1, flow2, [50,50] );
closePopup("#splitdialog");
}
function onSplitDialog() {
var dialog = $("#splitdialogcontent");
dialog.empty();
dialog.append("<h3>Split?</h3>");
dialog.append('<input type="radio" name="direction" value="vertical" checked> top/bottom<br />');
dialog.append('<input type="radio" name="direction" value="horizontal"> side-by-side<br />');
dialog.append("<h3>Split Which Pane?</h3>");
for ( var pane in SplitHandler.split_panes ) {
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
}
dialog.append("<h3>New Pane Names</h3>");
dialog.append('<input type="text" name="new_pane1" value="" />');
dialog.append('<input type="text" name="new_pane2" value="" />');
dialog.append("<h3>New First Pane</h3>");
dialog.append('<input type="radio" name="flow1" value="append" checked>append new incoming messages<br />');
dialog.append('<input type="radio" name="flow1" value="replace">replace old messages with new ones<br />');
dialog.append("<h3>New Second Pane</h3>");
dialog.append('<input type="radio" name="flow2" value="append" checked>append new incoming messages<br />');
dialog.append('<input type="radio" name="flow2" value="replace">replace old messages with new ones<br />');
dialog.append('<div id="splitclose" class="button">Split It</div>');
$("#splitclose").bind("click", onSplitDialogClose);
togglePopup("#splitdialog");
}
function onPaneControlDialogClose() {
var pane = $("input[name=pane]:checked").attr("value");
var types = new Array;
$('#splitdialogcontent input[type=checkbox]:checked').each(function() {
types.push( $(this).attr('value') );
});
SplitHandler.set_pane_types( pane, types );
closePopup("#splitdialog");
}
function onPaneControlDialog() {
var dialog = $("#splitdialogcontent");
dialog.empty();
dialog.append("<h3>Set Which Pane?</h3>");
for ( var pane in SplitHandler.split_panes ) {
dialog.append('<input type="radio" name="pane" value="'+ pane +'">'+ pane +'<br />');
}
dialog.append("<h3>Which content types?</h3>");
for ( var type in known_types ) {
dialog.append('<input type="checkbox" value="'+ known_types[type] +'">'+ known_types[type] +'<br />');
}
dialog.append('<div id="paneclose" class="button">Make It So</div>');
$("#paneclose").bind("click", onPaneControlDialogClose);
togglePopup("#splitdialog");
}
// //
// Register Events // Register Events
// //
@ -437,6 +540,18 @@ function doStartDragDialog(event) {
// Event when client finishes loading // Event when client finishes loading
$(document).ready(function() { $(document).ready(function() {
if( SplitHandler ) {
SplitHandler.init();
$("#splitbutton").bind("click", onSplitDialog);
$("#panebutton").bind("click", onPaneControlDialog);
$("#undobutton").bind("click", SplitHandler.undo_split);
$("#optionsbutton").hide();
} else {
$("#splitbutton").hide();
$("#panebutton").hide();
$("#undobutton").hide();
}
if ("Notification" in window) { if ("Notification" in window) {
Notification.requestPermission(); Notification.requestPermission();
} }
@ -453,7 +568,7 @@ $(document).ready(function() {
//$(document).on("visibilitychange", onVisibilityChange); //$(document).on("visibilitychange", onVisibilityChange);
$("#inputfield").bind("resize", doWindowResize) $("[data-role-input]").bind("resize", doWindowResize)
.keypress(onKeyPress) .keypress(onKeyPress)
.bind("paste", resizeInputField) .bind("paste", resizeInputField)
.bind("cut", resizeInputField); .bind("cut", resizeInputField);
@ -506,6 +621,7 @@ $(document).ready(function() {
}, },
60000*3 60000*3
); );
console.log("Completed GUI setup");
}); });

View file

@ -13,6 +13,10 @@ JQuery available.
<meta http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" /> <meta http-equiv="content-type", content="application/xhtml+xml; charset=UTF-8" />
<meta name="author" content="Evennia" /> <meta name="author" content="Evennia" />
<meta name="generator" content="Evennia" /> <meta name="generator" content="Evennia" />
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<!-- Bootstrap CSS -->
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/css/bootstrap.min.css" integrity="sha384-/Y6pD6FV/Vv2HJnA6t+vslU6fwYXjCFtcEpHbNJ0lyAFsXTsjBbfaDjzALeQsN6M" crossorigin="anonymous">
<link rel='stylesheet' type="text/css" media="screen" href={% static "webclient/css/webclient.css" %}> <link rel='stylesheet' type="text/css" media="screen" href={% static "webclient/css/webclient.css" %}>
@ -20,7 +24,7 @@ JQuery available.
<!-- Import JQuery and warn if there is a problem --> <!-- Import JQuery and warn if there is a problem -->
{% block jquery_import %} {% block jquery_import %}
<script src="https://code.jquery.com/jquery-2.1.1.min.js" type="text/javascript" charset="utf-8"></script> <script src="https://code.jquery.com/jquery-3.2.1.min.js" type="text/javascript" charset="utf-8"></script>
{% endblock %} {% endblock %}
<script type="text/javascript" charset="utf-8"> <script type="text/javascript" charset="utf-8">
@ -29,6 +33,14 @@ JQuery available.
} }
</script> </script>
<!-- This is will only fire if javascript is actually active -->
<script language="javascript" type="text/javascript">
$(document).ready(function() {
$('#noscript').remove();
$('#clientwrapper').removeClass('d-none');
})
</script>
<!-- Set up Websocket url and load the evennia.js library--> <!-- Set up Websocket url and load the evennia.js library-->
<script language="javascript" type="text/javascript"> <script language="javascript" type="text/javascript">
{% if websocket_enabled %} {% if websocket_enabled %}
@ -51,6 +63,12 @@ JQuery available.
</script> </script>
<script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script> <script src={% static "webclient/js/evennia.js" %} language="javascript" type="text/javascript" charset="utf-8"/></script>
<!-- set up splits before loading the GUI -->
<script src="https://unpkg.com/split.js/split.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/2.3.0/mustache.min.js"></script>
<script src={% static "webclient/js/splithandler.js" %} language="javascript"></script>
<!-- Load gui library --> <!-- Load gui library -->
{% block guilib_import %} {% block guilib_import %}
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script> <script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
@ -63,7 +81,11 @@ JQuery available.
} }
</script> </script>
<!-- jQuery first, then Tether, then Bootstrap JS. -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.11.0/umd/popper.min.js" integrity="sha384-b/U6ypiBEHpOf/4+1nzFpr53nxSS+GLCkfwBdFNTxtclqqenISfwAzpKaMNFNmj4" crossorigin="anonymous"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.0.0-beta/js/bootstrap.min.js" integrity="sha384-h0AbiXch4ZDo7tp9hKZ4TsHbi047NrKGLO3SEJAg45jXxnGIfYzk4Si90RDIqNm1" crossorigin="anonymous"></script>
{% block scripts %}
{% endblock %}
</head> </head>
<body> <body>
@ -86,10 +108,9 @@ JQuery available.
</div> </div>
<!-- main client --> <!-- main client -->
<div id=clientwrapper> <div id=clientwrapper class="d-none">
{% block client %} {% block client %}
{% endblock %} {% endblock %}
</div> </div>
</body> </body>
</html> </html>

View file

@ -8,20 +8,29 @@
{% block client %} {% block client %}
<div id="wrapper">
<div id="toolbar"> <div id="toolbar">
<button id="optionsbutton" type="button" class="hidden">&#x2699;</button> <button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x2699;<span class="sr-only sr-only-focusable">Settings</span></button>
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x21f9;<span class="sr-only sr-only-focusable">Splits</span></button>
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x2699;<span class="sr-only sr-only-focusable">Splits</span></button>
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">&#x21B6;<span class="sr-only sr-only-focusable">Splits</span></button>
</div> </div>
<div id="messagewindow" role="log"></div>
<div id="inputform"> <!-- The "Main" Content -->
<div id="prompt"></div> <div id="main" class="split split-vertical" data-role-default>
<div id="inputcontrol"> <div id="main-sub" class="split-sub">
<textarea id="inputfield" type="text"></textarea> <div id="messagewindow"></div>
<input id="inputsend" type="button" value="&gt;"/> </div>
</div>
<!-- The "Input" Pane -->
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
<!-- Basic UI Components -->
<div id="splitdialog" class="dialog">
<div class="dialogtitle">Split Pane<span class="dialogclose">&times;</span></div>
<div class="dialogcontentparent">
<div id="splitdialogcontent" class="dialogcontent">
</div> </div>
</div> </div>
<div id="inputsizer"></div>
</div> </div>
<div id="optionsdialog" class="dialog"> <div id="optionsdialog" class="dialog">
@ -47,4 +56,29 @@
</div> </div>
</div> </div>
<script type="text/html" id="split-template">
<div class="split content<%#horizontal%> split-horizontal<%/horizontal%>" id='<%id%>'>
</div>
</script>
<script type="text/html" id="output-template">
<div id="<%id%>" role="log" data-role-output data-update-append data-tags='[<%#tags%>"<%.%>", <%/tags%>]'></div>
</script>
<script type="text/html" id="input-template">
<div id="inputform" class="wrapper">
<div id="prompt" class="prompt">
</div>
<div id="inputcontrol" class="input-group">
<textarea id="inputfield" type="text" class="form-control"></textarea>
<span class="input-group-btn">
<button class="btn btn-large btn-outline-primary" id="inputsend" type="button" value="">&gt;</button>
</span>
</div>
</div>
</script>
{% endblock %}
{% block scripts %}
{% endblock %} {% endblock %}

View file

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