Merge branch 'develop' into building_menu
This commit is contained in:
commit
415322fe1a
60 changed files with 10614 additions and 1566 deletions
88
CHANGELOG.md
88
CHANGELOG.md
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
22
Dockerfile
22
Dockerfile
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -421,17 +421,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
||||||
|
|
||||||
kwargs["options"] = options
|
kwargs["options"] = options
|
||||||
|
|
||||||
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
if text is not None:
|
||||||
# sanitize text before sending across the wire
|
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||||
try:
|
# sanitize text before sending across the wire
|
||||||
text = to_str(text, force_string=True)
|
try:
|
||||||
except Exception:
|
text = to_str(text, force_string=True)
|
||||||
text = repr(text)
|
except Exception:
|
||||||
|
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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,17 +1455,16 @@ 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)
|
except (SyntaxError, ValueError):
|
||||||
except (SyntaxError, ValueError):
|
# treat as string
|
||||||
# treat as string
|
strobj = utils.to_str(strobj)
|
||||||
strobj = utils.to_str(strobj)
|
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
||||||
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
"Make sure this is acceptable." % strobj
|
||||||
"Make sure this is acceptable." % strobj
|
cmd.caller.msg(string)
|
||||||
cmd.caller.msg(string)
|
return strobj
|
||||||
return strobj
|
|
||||||
else:
|
else:
|
||||||
# fall back to old recursive solution (does not support
|
# fall back to old recursive solution (does not support
|
||||||
# nested lists/dicts)
|
# nested lists/dicts)
|
||||||
|
|
@ -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,12 +2227,15 @@ 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(
|
||||||
if cont not in exits and cont not in pobjs])
|
["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
|
||||||
|
if cont not in exits and cont not in pobjs])
|
||||||
separator = "-" * _DEFAULT_WIDTH
|
separator = "-" * _DEFAULT_WIDTH
|
||||||
# output info
|
# output info
|
||||||
return '%s\n%s\n%s' % (separator, string.strip(), separator)
|
return '%s\n%s\n%s' % (separator, string.strip(), separator)
|
||||||
|
|
@ -2270,11 +2321,12 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
||||||
@locate - this is a shorthand for using the /loc switch.
|
@locate - this is a shorthand for using the /loc switch.
|
||||||
|
|
||||||
Switches:
|
Switches:
|
||||||
room - only look for rooms (location=None)
|
room - only look for rooms (location=None)
|
||||||
exit - only look for exits (destination!=None)
|
exit - only look for exits (destination!=None)
|
||||||
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):
|
||||||
if not self.args:
|
# prototype detail
|
||||||
string = "Usage: @spawn {key:value, key, value, ... }"
|
if not prototypes:
|
||||||
self.caller.msg(string + _show_prototypes(prototypes))
|
prototypes = protlib.search_prototype(key=query)
|
||||||
return
|
if prototypes:
|
||||||
try:
|
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
|
||||||
# make use of _convert_from_string from the SetAttribute command
|
else:
|
||||||
prototype = _convert_from_string(self, self.args)
|
return False
|
||||||
except SyntaxError:
|
|
||||||
# this means literal_eval tried to parse a faulty string
|
caller = self.caller
|
||||||
string = "|RCritical Python syntax error in argument. "
|
|
||||||
string += "Only primitive Python structures are allowed. "
|
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches:
|
||||||
string += "\nYou also need to use correct Python syntax. "
|
# OLC menu mode
|
||||||
string += "Remember especially to put quotes around all "
|
prototype = None
|
||||||
string += "strings inside lists and dicts.|n"
|
if self.lhs:
|
||||||
self.caller.msg(string)
|
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
|
return
|
||||||
|
|
||||||
if isinstance(prototype, basestring):
|
if 'search' in self.switches:
|
||||||
# A prototype key
|
# query for a key match
|
||||||
keystr = prototype
|
if not self.args:
|
||||||
prototype = prototypes.get(prototype, None)
|
self.switches.append("list")
|
||||||
|
else:
|
||||||
|
key, tags = self.args.strip(), None
|
||||||
|
if ';' in self.args:
|
||||||
|
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
|
||||||
|
tags = [tag.strip() for tag in tags.split(",")] if tags else None
|
||||||
|
EvMore(caller, unicode(protlib.list_prototypes(caller, key=key, tags=tags)),
|
||||||
|
exit_on_lastpage=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'show' in self.switches or 'examine' in self.switches:
|
||||||
|
# the argument is a key in this case (may be a partial key)
|
||||||
|
if not self.args:
|
||||||
|
self.switches.append('list')
|
||||||
|
else:
|
||||||
|
matchstring = _search_show_prototype(self.args)
|
||||||
|
if matchstring:
|
||||||
|
caller.msg(matchstring)
|
||||||
|
else:
|
||||||
|
caller.msg("No prototype '{}' was found.".format(self.args))
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'list' in self.switches:
|
||||||
|
# for list, all optional arguments are tags
|
||||||
|
# import pudb; pudb.set_trace()
|
||||||
|
|
||||||
|
EvMore(caller, unicode(protlib.list_prototypes(caller,
|
||||||
|
tags=self.lhslist)), exit_on_lastpage=True)
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'save' in self.switches:
|
||||||
|
# store a prototype to the database store
|
||||||
|
if not self.args:
|
||||||
|
caller.msg(
|
||||||
|
"Usage: @spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>")
|
||||||
|
return
|
||||||
|
|
||||||
|
# handle rhs:
|
||||||
|
prototype = _parse_prototype(self.lhs.strip())
|
||||||
if not prototype:
|
if not prototype:
|
||||||
string = "No prototype named '%s'." % keystr
|
|
||||||
self.caller.msg(string + _show_prototypes(prototypes))
|
|
||||||
return
|
return
|
||||||
elif isinstance(prototype, dict):
|
|
||||||
# we got the prototype on the command line. We must make sure to not allow
|
# present prototype to save
|
||||||
# the 'exec' key unless we are developers or higher.
|
new_matchstring = _search_show_prototype("", prototypes=[prototype])
|
||||||
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
|
||||||
self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.")
|
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
|
return
|
||||||
else:
|
|
||||||
self.caller.msg("The prototype must be a prototype key or a Python dictionary.")
|
# check for existing prototype,
|
||||||
|
old_matchstring = _search_show_prototype(prototype_key)
|
||||||
|
|
||||||
|
if old_matchstring:
|
||||||
|
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
|
||||||
|
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
|
||||||
|
|
||||||
|
answer = yield(string + question)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg("|rSave cancelled.|n")
|
||||||
|
return
|
||||||
|
|
||||||
|
# all seems ok. Try to save.
|
||||||
|
try:
|
||||||
|
prot = protlib.save_prototype(**prototype)
|
||||||
|
if not prot:
|
||||||
|
caller.msg("|rError saving:|R {}.|n".format(prototype_key))
|
||||||
|
return
|
||||||
|
except protlib.PermissionError as err:
|
||||||
|
caller.msg("|rError saving:|R {}|n".format(err))
|
||||||
|
return
|
||||||
|
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
||||||
|
|
||||||
|
# check if we want to update existing objects
|
||||||
|
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||||
|
if existing_objects:
|
||||||
|
if 'update' not in self.switches:
|
||||||
|
n_existing = len(existing_objects)
|
||||||
|
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||||
|
string = ("There are {} objects already created with an older version "
|
||||||
|
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||||
|
n_existing, prototype_key, slow))
|
||||||
|
answer = yield(string)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg("|rNo update was done of existing objects. "
|
||||||
|
"Use @spawn/update <key> to apply later as needed.|n")
|
||||||
|
return
|
||||||
|
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||||
|
caller.msg("{} objects were updated.".format(n_updated))
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not self.args:
|
||||||
|
ncount = len(protlib.search_prototype())
|
||||||
|
caller.msg("Usage: @spawn <prototype-key> or {{key: value, ...}}"
|
||||||
|
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
|
||||||
|
return
|
||||||
|
|
||||||
|
if 'delete' in self.switches:
|
||||||
|
# remove db-based prototype
|
||||||
|
matchstring = _search_show_prototype(self.args)
|
||||||
|
if matchstring:
|
||||||
|
string = "|rDeleting prototype:|n\n{}".format(matchstring)
|
||||||
|
question = "\nDo you want to continue deleting? [Y]/N"
|
||||||
|
answer = yield(string + question)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg("|rDeletion cancelled.|n")
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
success = protlib.delete_db_prototype(caller, self.args)
|
||||||
|
except protlib.PermissionError as err:
|
||||||
|
caller.msg("|rError deleting:|R {}|n".format(err))
|
||||||
|
caller.msg("Deletion {}.".format(
|
||||||
|
'successful' if success else 'failed (does the prototype exist?)'))
|
||||||
|
return
|
||||||
|
else:
|
||||||
|
caller.msg("Could not find prototype '{}'".format(key))
|
||||||
|
|
||||||
|
if 'update' in self.switches:
|
||||||
|
# update existing prototypes
|
||||||
|
key = self.args.strip().lower()
|
||||||
|
existing_objects = protlib.search_objects_with_prototype(key)
|
||||||
|
if existing_objects:
|
||||||
|
n_existing = len(existing_objects)
|
||||||
|
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||||
|
string = ("There are {} objects already created with an older version "
|
||||||
|
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||||
|
n_existing, key, slow))
|
||||||
|
answer = yield(string)
|
||||||
|
if answer.lower() in ["n", "no"]:
|
||||||
|
caller.msg("|rUpdate cancelled.")
|
||||||
|
return
|
||||||
|
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||||
|
caller.msg("{} objects were updated.".format(n_updated))
|
||||||
|
|
||||||
|
# A direct creation of an object from a given prototype
|
||||||
|
|
||||||
|
prototype = _parse_prototype(
|
||||||
|
self.args, expect=dict if self.args.strip().startswith("{") else basestring)
|
||||||
|
if not prototype:
|
||||||
|
# this will only let through dicts or strings
|
||||||
|
return
|
||||||
|
|
||||||
|
key = '<unnamed>'
|
||||||
|
if isinstance(prototype, basestring):
|
||||||
|
# A prototype key we are looking to apply
|
||||||
|
key = prototype
|
||||||
|
prototypes = protlib.search_prototype(prototype)
|
||||||
|
nprots = len(prototypes)
|
||||||
|
if not prototypes:
|
||||||
|
caller.msg("No prototype named '%s'." % prototype)
|
||||||
|
return
|
||||||
|
elif nprots > 1:
|
||||||
|
caller.msg("Found {} prototypes matching '{}':\n {}".format(
|
||||||
|
nprots, prototype, ", ".join(prot.get('prototype_key', '')
|
||||||
|
for proto in prototypes)))
|
||||||
|
return
|
||||||
|
# we have a prototype, check access
|
||||||
|
prototype = prototypes[0]
|
||||||
|
if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'):
|
||||||
|
caller.msg("You don't have access to use this prototype.")
|
||||||
|
return
|
||||||
|
|
||||||
if "noloc" not in self.switches and "location" not in prototype:
|
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
|
||||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
try:
|
||||||
|
for obj in spawner.spawn(prototype):
|
||||||
|
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||||
|
except RuntimeError as err:
|
||||||
|
caller.msg(err)
|
||||||
|
|
|
||||||
|
|
@ -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())
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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,37 +75,58 @@ 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):
|
||||||
ret.next()
|
while True:
|
||||||
|
try:
|
||||||
|
inp = inputs.pop() if inputs else None
|
||||||
|
if inp:
|
||||||
|
try:
|
||||||
|
ret.send(inp)
|
||||||
|
except TypeError:
|
||||||
|
ret.next()
|
||||||
|
ret = ret.send(inp)
|
||||||
|
else:
|
||||||
|
ret.next()
|
||||||
|
except StopIteration:
|
||||||
|
break
|
||||||
|
|
||||||
cmdobj.at_post_cmd()
|
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 "||"
|
||||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
# Have to strip ansi for each returned message for the regex to handle it correctly
|
||||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
|
||||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
for mess in stored_msg).strip()
|
||||||
sep3 = "\n" + "=" * 78
|
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||||
raise AssertionError(retval)
|
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||||
else:
|
sep3 = "\n" + "=" * 78
|
||||||
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
raise AssertionError(retval)
|
||||||
receiver.msg = old_msg
|
else:
|
||||||
|
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
||||||
|
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||||
|
receiver.msg = old_msg
|
||||||
|
|
||||||
return returned_msg
|
return returned_msg
|
||||||
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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])
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -697,7 +697,7 @@ class TestMail(CommandTest):
|
||||||
"You have received a new @mail from TestAccount2(account 2)|You sent your message.", caller=self.account2)
|
"You have received a new @mail from TestAccount2(account 2)|You sent your message.", caller=self.account2)
|
||||||
self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2)
|
self.call(mail.CmdMail(), "TestAccount=Message 1", "You sent your message.", caller=self.account2)
|
||||||
self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2)
|
self.call(mail.CmdMail(), "TestAccount=Message 2", "You sent your message.", caller=self.account2)
|
||||||
self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account)
|
self.call(mail.CmdMail(), "", "| ID: From: Subject:", caller=self.account)
|
||||||
self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account)
|
self.call(mail.CmdMail(), "2", "From: TestAccount2", caller=self.account)
|
||||||
self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account)
|
self.call(mail.CmdMail(), "/forward TestAccount2 = 1/Forward message", "You sent your message.|Message forwarded.", caller=self.account)
|
||||||
self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account)
|
self.call(mail.CmdMail(), "/reply 2=Reply Message2", "You sent your message.", caller=self.account)
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
1397
evennia/contrib/turnbattle/tb_items.py
Normal file
1397
evennia/contrib/turnbattle/tb_items.py
Normal file
File diff suppressed because it is too large
Load diff
1290
evennia/contrib/turnbattle/tb_magic.py
Normal file
1290
evennia/contrib/turnbattle/tb_magic.py
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
#
|
#
|
||||||
|
|
@ -674,7 +674,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
||||||
# we found the button by moving the roots
|
# we found the button by moving the roots
|
||||||
result = ["Having moved all the roots aside, you find that the center of the wall, "
|
result = ["Having moved all the roots aside, you find that the center of the wall, "
|
||||||
"previously hidden by the vegetation, hid a curious square depression. It was maybe once "
|
"previously hidden by the vegetation, hid a curious square depression. It was maybe once "
|
||||||
"concealed and made to look a part of the wall, but with the crumbling of stone around it,"
|
"concealed and made to look a part of the wall, but with the crumbling of stone around it, "
|
||||||
"it's now easily identifiable as some sort of button."]
|
"it's now easily identifiable as some sort of button."]
|
||||||
elif self.db.exit_open:
|
elif self.db.exit_open:
|
||||||
# we pressed the button; the exit is open
|
# we pressed the button; the exit is open
|
||||||
|
|
@ -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,"
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
return False
|
if validate_only:
|
||||||
|
return False, err
|
||||||
|
else:
|
||||||
|
self._log_error(err)
|
||||||
|
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 "
|
||||||
return False
|
"(left-side of colon is empty).").format(lockdef=lockdef)
|
||||||
|
if validate_only:
|
||||||
|
return False, err
|
||||||
|
else:
|
||||||
|
self._log_error(err)
|
||||||
|
return False
|
||||||
if rhs.count('(') != rhs.count(')'):
|
if rhs.count('(') != rhs.count(')'):
|
||||||
self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef)
|
err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
|
||||||
return False
|
if validate_only:
|
||||||
|
return False, err
|
||||||
|
else:
|
||||||
|
self._log_error(err)
|
||||||
|
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)
|
||||||
return False
|
if validate_only:
|
||||||
|
return False, err
|
||||||
|
else:
|
||||||
|
self._log_error(err)
|
||||||
|
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():
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
|
|
|
||||||
|
|
@ -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 not (isinstance(text, basestring) or isinstance(text, tuple)):
|
if text is not None:
|
||||||
# sanitize text before sending across the wire
|
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||||
try:
|
# sanitize text before sending across the wire
|
||||||
text = to_str(text, force_string=True)
|
try:
|
||||||
except Exception:
|
text = to_str(text, force_string=True)
|
||||||
text = repr(text)
|
except Exception:
|
||||||
|
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)
|
||||||
|
|
|
||||||
145
evennia/prototypes/README.md
Normal file
145
evennia/prototypes/README.md
Normal file
|
|
@ -0,0 +1,145 @@
|
||||||
|
# Prototypes
|
||||||
|
|
||||||
|
A 'Prototype' is a normal Python dictionary describing unique features of individual instance of a
|
||||||
|
Typeclass. The prototype is used to 'spawn' a new instance with custom features detailed by said
|
||||||
|
prototype. This allows for creating variations without having to create a large number of actual
|
||||||
|
Typeclasses. It is a good way to allow Builders more freedom of creation without giving them full
|
||||||
|
Python access to create Typeclasses.
|
||||||
|
|
||||||
|
For example, if a Typeclass 'Cat' describes all the coded differences between a Cat and
|
||||||
|
other types of animals, then prototypes could be used to quickly create unique individual cats with
|
||||||
|
different Attributes/properties (like different colors, stats, names etc) without having to make a new
|
||||||
|
Typeclass for each. Prototypes have inheritance and can be scripted when they are applied to create
|
||||||
|
a new instance of a typeclass - a common example would be to randomize stats and name.
|
||||||
|
|
||||||
|
The prototype is a normal dictionary with specific keys. Almost all values can be callables
|
||||||
|
triggered when the prototype is used to spawn a new instance. Below is an example:
|
||||||
|
|
||||||
|
```
|
||||||
|
{
|
||||||
|
# meta-keys - these are used only when listing prototypes in-game. Only prototype_key is mandatory,
|
||||||
|
# but it must be globally unique.
|
||||||
|
|
||||||
|
"prototype_key": "base_goblin",
|
||||||
|
"prototype_desc": "A basic goblin",
|
||||||
|
"prototype_locks": "edit:all();spawn:all()",
|
||||||
|
"prototype_tags": "mobs",
|
||||||
|
|
||||||
|
# fixed-meaning keys, modifying the spawned instance. 'typeclass' may be
|
||||||
|
# replaced by 'parent', referring to the prototype_key of an existing prototype
|
||||||
|
# to inherit from.
|
||||||
|
|
||||||
|
"typeclass": "types.objects.Monster",
|
||||||
|
"key": "goblin grunt",
|
||||||
|
"tags": ["mob", "evil", ('greenskin','mob')] # tags as well as tags with category etc
|
||||||
|
"attrs": [("weapon", "sword")] # this allows to set Attributes with categories etc
|
||||||
|
|
||||||
|
# non-fixed keys are interpreted as Attributes and their
|
||||||
|
|
||||||
|
"health": lambda: randint(20,30),
|
||||||
|
"resists": ["cold", "poison"],
|
||||||
|
"attacks": ["fists"],
|
||||||
|
"weaknesses": ["fire", "light"]
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
## Using prototypes
|
||||||
|
|
||||||
|
Prototypes are generally used as inputs to the `spawn` command:
|
||||||
|
|
||||||
|
@spawn prototype_key
|
||||||
|
|
||||||
|
This will spawn a new instance of the prototype in the caller's current location unless the
|
||||||
|
`location` key of the prototype was set (see below). The caller must pass the prototype's 'spawn'
|
||||||
|
lock to be able to use it.
|
||||||
|
|
||||||
|
@spawn/list [prototype_key]
|
||||||
|
|
||||||
|
will show all available prototypes along with meta info, or look at a specific prototype in detail.
|
||||||
|
|
||||||
|
|
||||||
|
## Creating prototypes
|
||||||
|
|
||||||
|
The `spawn` command can also be used to directly create/update prototypes from in-game.
|
||||||
|
|
||||||
|
spawn/save {"prototype_key: "goblin", ... }
|
||||||
|
|
||||||
|
but it is probably more convenient to use the menu-driven prototype wizard:
|
||||||
|
|
||||||
|
spawn/menu goblin
|
||||||
|
|
||||||
|
In code:
|
||||||
|
|
||||||
|
```python
|
||||||
|
|
||||||
|
from evennia import prototypes
|
||||||
|
|
||||||
|
goblin = {"prototype_key": "goblin:, ... }
|
||||||
|
|
||||||
|
prototype = prototypes.save_prototype(caller, **goblin)
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Prototypes will normally be stored in the database (internally this is done using a Script, holding
|
||||||
|
the meta-info and the prototype). One can also define prototypes outside of the game by assigning
|
||||||
|
the prototype dictionary to a global variable in a module defined by `settings.PROTOTYPE_MODULES`:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# in e.g. mygame/world/prototypes.py
|
||||||
|
|
||||||
|
GOBLIN = {
|
||||||
|
"prototype_key": "goblin",
|
||||||
|
...
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
Such prototypes cannot be modified from inside the game no matter what `edit` lock they are given
|
||||||
|
(we refer to them as 'readonly') but can be a fast and efficient way to give builders a starting
|
||||||
|
library of prototypes to inherit from.
|
||||||
|
|
||||||
|
## Valid Prototype keys
|
||||||
|
|
||||||
|
Every prototype key also accepts a callable (taking no arguments) for producing its value or a
|
||||||
|
string with an $protfunc definition. That callable/protfunc must then return a value on a form the
|
||||||
|
prototype key expects.
|
||||||
|
|
||||||
|
- `prototype_key` (str): name of this prototype. This is used when storing prototypes and should
|
||||||
|
be unique. This should always be defined but for prototypes defined in modules, the
|
||||||
|
variable holding the prototype dict will become the prototype_key if it's not explicitly
|
||||||
|
given.
|
||||||
|
- `prototype_desc` (str, optional): describes prototype in listings
|
||||||
|
- `prototype_locks` (str, optional): locks for restricting access to this prototype. Locktypes
|
||||||
|
supported are 'edit' and 'use'.
|
||||||
|
- `prototype_tags` (list, optional): List of tags or tuples (tag, category) used to group prototype
|
||||||
|
in listings
|
||||||
|
|
||||||
|
- `parent` (str or tuple, optional): name (`prototype_key`) of eventual parent prototype, or a
|
||||||
|
list of parents for multiple left-to-right inheritance.
|
||||||
|
- `prototype`: Deprecated. Same meaning as 'parent'.
|
||||||
|
- `typeclass` (str, optional): if not set, will use typeclass of parent prototype or use
|
||||||
|
`settings.BASE_OBJECT_TYPECLASS`
|
||||||
|
- `key` (str, optional): the name of the spawned object. If not given this will set to a
|
||||||
|
random hash
|
||||||
|
- `location` (obj, optional): location of the object - a valid object or #dbref
|
||||||
|
- `home` (obj or str, optional): valid object or #dbref
|
||||||
|
- `destination` (obj or str, optional): only valid for exits (object or #dbref)
|
||||||
|
|
||||||
|
- `permissions` (str or list, optional): which permissions for spawned object to have
|
||||||
|
- `locks` (str, optional): lock-string for the spawned object
|
||||||
|
- `aliases` (str or list, optional): Aliases for the spawned object.
|
||||||
|
- `exec` (str, optional): this is a string of python code to execute or a list of such
|
||||||
|
codes. This can be used e.g. to trigger custom handlers on the object. The execution
|
||||||
|
namespace contains 'evennia' for the library and 'obj'. All default spawn commands limit
|
||||||
|
this functionality to Developer/superusers. Usually it's better to use callables or
|
||||||
|
prototypefuncs instead of this.
|
||||||
|
- `tags` (str, tuple or list, optional): string or list of strings or tuples
|
||||||
|
`(tagstr, category)`. Plain strings will be result in tags with no category (default tags).
|
||||||
|
- `attrs` (tuple or list, optional): tuple or list of tuples of Attributes to add. This
|
||||||
|
form allows more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||||
|
but can also specify up to `(key, value, category, lockstring)`. If you want to specify a
|
||||||
|
lockstring but not a category, set the category to `None`.
|
||||||
|
- `ndb_<name>` (any): value of a nattribute (`ndb_` is stripped). This is usually not useful to
|
||||||
|
put in a prototype unless the NAttribute is used immediately upon spawning.
|
||||||
|
- `other` (any): any other name is interpreted as the key of an Attribute with
|
||||||
|
its value. Such Attributes have no categories.
|
||||||
0
evennia/prototypes/__init__.py
Normal file
0
evennia/prototypes/__init__.py
Normal file
2400
evennia/prototypes/menus.py
Normal file
2400
evennia/prototypes/menus.py
Normal file
File diff suppressed because it is too large
Load diff
317
evennia/prototypes/protfuncs.py
Normal file
317
evennia/prototypes/protfuncs.py
Normal file
|
|
@ -0,0 +1,317 @@
|
||||||
|
"""
|
||||||
|
Protfuncs are function-strings embedded in a prototype and allows for a builder to create a
|
||||||
|
prototype with custom logics without having access to Python. The Protfunc is parsed using the
|
||||||
|
inlinefunc parser but is fired at the moment the spawning happens, using the creating object's
|
||||||
|
session as input.
|
||||||
|
|
||||||
|
In the prototype dict, the protfunc is specified as a string inside the prototype, e.g.:
|
||||||
|
|
||||||
|
{ ...
|
||||||
|
|
||||||
|
"key": "$funcname(arg1, arg2, ...)"
|
||||||
|
|
||||||
|
... }
|
||||||
|
|
||||||
|
and multiple functions can be nested (no keyword args are supported). The result will be used as the
|
||||||
|
value for that prototype key for that individual spawn.
|
||||||
|
|
||||||
|
Available protfuncs are callables in one of the modules of `settings.PROT_FUNC_MODULES`. They
|
||||||
|
are specified as functions
|
||||||
|
|
||||||
|
def funcname (*args, **kwargs)
|
||||||
|
|
||||||
|
where *args are the arguments given in the prototype, and **kwargs are inserted by Evennia:
|
||||||
|
|
||||||
|
- session (Session): The Session of the entity spawning using this prototype.
|
||||||
|
- prototype (dict): The dict this protfunc is a part of.
|
||||||
|
- current_key (str): The active key this value belongs to in the prototype.
|
||||||
|
- testing (bool): This is set if this function is called as part of the prototype validation; if
|
||||||
|
set, the protfunc should take care not to perform any persistent actions, such as operate on
|
||||||
|
objects or add things to the database.
|
||||||
|
|
||||||
|
Any traceback raised by this function will be handled at the time of spawning and abort the spawn
|
||||||
|
before any object is created/updated. It must otherwise return the value to store for the specified
|
||||||
|
prototype key (this value must be possible to serialize in an Attribute).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from ast import literal_eval
|
||||||
|
from random import randint as base_randint, random as base_random
|
||||||
|
|
||||||
|
from evennia.utils import search
|
||||||
|
from evennia.utils.utils import justify as base_justify, is_iter, to_str
|
||||||
|
|
||||||
|
_PROTLIB = None
|
||||||
|
|
||||||
|
|
||||||
|
# default protfuncs
|
||||||
|
|
||||||
|
def random(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $random()
|
||||||
|
Returns a random value in the interval [0, 1)
|
||||||
|
|
||||||
|
"""
|
||||||
|
return base_random()
|
||||||
|
|
||||||
|
|
||||||
|
def randint(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $randint(start, end)
|
||||||
|
Returns random integer in interval [start, end]
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(args) != 2:
|
||||||
|
raise TypeError("$randint needs two arguments - start and end.")
|
||||||
|
start, end = int(args[0]), int(args[1])
|
||||||
|
return base_randint(start, end)
|
||||||
|
|
||||||
|
|
||||||
|
def left_justify(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $left_justify(<text>)
|
||||||
|
Returns <text> left-justified.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
return base_justify(args[0], align='l')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def right_justify(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $right_justify(<text>)
|
||||||
|
Returns <text> right-justified across screen width.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
return base_justify(args[0], align='r')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def center_justify(*args, **kwargs):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Usage: $center_justify(<text>)
|
||||||
|
Returns <text> centered in screen width.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
return base_justify(args[0], align='c')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def full_justify(*args, **kwargs):
|
||||||
|
|
||||||
|
"""
|
||||||
|
Usage: $full_justify(<text>)
|
||||||
|
Returns <text> filling up screen width by adding extra space.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
return base_justify(args[0], align='f')
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def protkey(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $protkey(<key>)
|
||||||
|
Returns the value of another key in this prototoype. Will raise an error if
|
||||||
|
the key is not found in this prototype.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
prototype = kwargs['prototype']
|
||||||
|
return prototype[args[0].strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def add(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $add(val1, val2)
|
||||||
|
Returns the result of val1 + val2. Values must be
|
||||||
|
valid simple Python structures possible to add,
|
||||||
|
such as numbers, lists etc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(args) > 1:
|
||||||
|
val1, val2 = args[0], args[1]
|
||||||
|
# try to convert to python structures, otherwise, keep as strings
|
||||||
|
try:
|
||||||
|
val1 = literal_eval(val1.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
val2 = literal_eval(val2.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return val1 + val2
|
||||||
|
raise ValueError("$add requires two arguments.")
|
||||||
|
|
||||||
|
|
||||||
|
def sub(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $del(val1, val2)
|
||||||
|
Returns the value of val1 - val2. Values must be
|
||||||
|
valid simple Python structures possible to
|
||||||
|
subtract.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(args) > 1:
|
||||||
|
val1, val2 = args[0], args[1]
|
||||||
|
# try to convert to python structures, otherwise, keep as strings
|
||||||
|
try:
|
||||||
|
val1 = literal_eval(val1.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
val2 = literal_eval(val2.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return val1 - val2
|
||||||
|
raise ValueError("$sub requires two arguments.")
|
||||||
|
|
||||||
|
|
||||||
|
def mult(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $mul(val1, val2)
|
||||||
|
Returns the value of val1 * val2. The values must be
|
||||||
|
valid simple Python structures possible to
|
||||||
|
multiply, like strings and/or numbers.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(args) > 1:
|
||||||
|
val1, val2 = args[0], args[1]
|
||||||
|
# try to convert to python structures, otherwise, keep as strings
|
||||||
|
try:
|
||||||
|
val1 = literal_eval(val1.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
val2 = literal_eval(val2.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return val1 * val2
|
||||||
|
raise ValueError("$mul requires two arguments.")
|
||||||
|
|
||||||
|
|
||||||
|
def div(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $div(val1, val2)
|
||||||
|
Returns the value of val1 / val2. Values must be numbers and
|
||||||
|
the result is always a float.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if len(args) > 1:
|
||||||
|
val1, val2 = args[0], args[1]
|
||||||
|
# try to convert to python structures, otherwise, keep as strings
|
||||||
|
try:
|
||||||
|
val1 = literal_eval(val1.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
val2 = literal_eval(val2.strip())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return val1 / float(val2)
|
||||||
|
raise ValueError("$mult requires two arguments.")
|
||||||
|
|
||||||
|
|
||||||
|
def toint(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage: $toint(<number>)
|
||||||
|
Returns <number> as an integer.
|
||||||
|
"""
|
||||||
|
if args:
|
||||||
|
val = args[0]
|
||||||
|
try:
|
||||||
|
return int(literal_eval(val.strip()))
|
||||||
|
except ValueError:
|
||||||
|
return val
|
||||||
|
raise ValueError("$toint requires one argument.")
|
||||||
|
|
||||||
|
|
||||||
|
def eval(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage $eval(<expression>)
|
||||||
|
Returns evaluation of a simple Python expression. The string may *only* consist of the following
|
||||||
|
Python literal structures: strings, numbers, tuples, lists, dicts, booleans,
|
||||||
|
and None. The strings can also contain #dbrefs. Escape embedded protfuncs as $$protfunc(..)
|
||||||
|
- those will then be evaluated *after* $eval.
|
||||||
|
|
||||||
|
"""
|
||||||
|
global _PROTLIB
|
||||||
|
if not _PROTLIB:
|
||||||
|
from evennia.prototypes import prototypes as _PROTLIB
|
||||||
|
|
||||||
|
string = ",".join(args)
|
||||||
|
struct = literal_eval(string)
|
||||||
|
|
||||||
|
if isinstance(struct, basestring):
|
||||||
|
# we must shield the string, otherwise it will be merged as a string and future
|
||||||
|
# literal_evas will pick up e.g. '2' as something that should be converted to a number
|
||||||
|
struct = '"{}"'.format(struct)
|
||||||
|
|
||||||
|
# convert any #dbrefs to objects (also in nested structures)
|
||||||
|
struct = _PROTLIB.value_to_obj_or_any(struct)
|
||||||
|
|
||||||
|
return struct
|
||||||
|
|
||||||
|
|
||||||
|
def _obj_search(*args, **kwargs):
|
||||||
|
"Helper function to search for an object"
|
||||||
|
|
||||||
|
query = "".join(args)
|
||||||
|
session = kwargs.get("session", None)
|
||||||
|
return_list = kwargs.pop("return_list", False)
|
||||||
|
account = None
|
||||||
|
|
||||||
|
if session:
|
||||||
|
account = session.account
|
||||||
|
|
||||||
|
targets = search.search_object(query)
|
||||||
|
|
||||||
|
if return_list:
|
||||||
|
retlist = []
|
||||||
|
if account:
|
||||||
|
for target in targets:
|
||||||
|
if target.access(account, target, 'control'):
|
||||||
|
retlist.append(target)
|
||||||
|
else:
|
||||||
|
retlist = targets
|
||||||
|
return retlist
|
||||||
|
else:
|
||||||
|
# single-match
|
||||||
|
if not targets:
|
||||||
|
raise ValueError("$obj: Query '{}' gave no matches.".format(query))
|
||||||
|
if len(targets) > 1:
|
||||||
|
raise ValueError("$obj: Query '{query}' gave {nmatches} matches. Limit your "
|
||||||
|
"query or use $objlist instead.".format(
|
||||||
|
query=query, nmatches=len(targets)))
|
||||||
|
target = targets[0]
|
||||||
|
if account:
|
||||||
|
if not target.access(account, target, 'control'):
|
||||||
|
raise ValueError("$obj: Obj {target}(#{dbref} cannot be added - "
|
||||||
|
"Account {account} does not have 'control' access.".format(
|
||||||
|
target=target.key, dbref=target.id, account=account))
|
||||||
|
return target
|
||||||
|
|
||||||
|
|
||||||
|
def obj(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage $obj(<query>)
|
||||||
|
Returns one Object searched globally by key, alias or #dbref. Error if more than one.
|
||||||
|
|
||||||
|
"""
|
||||||
|
obj = _obj_search(return_list=False, *args, **kwargs)
|
||||||
|
if obj:
|
||||||
|
return "#{}".format(obj.id)
|
||||||
|
return "".join(args)
|
||||||
|
|
||||||
|
|
||||||
|
def objlist(*args, **kwargs):
|
||||||
|
"""
|
||||||
|
Usage $objlist(<query>)
|
||||||
|
Returns list with one or more Objects searched globally by key, alias or #dbref.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ["#{}".format(obj.id) for obj in _obj_search(return_list=True, *args, **kwargs)]
|
||||||
695
evennia/prototypes/prototypes.py
Normal file
695
evennia/prototypes/prototypes.py
Normal file
|
|
@ -0,0 +1,695 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
Handling storage of prototypes, both database-based ones (DBPrototypes) and those defined in modules
|
||||||
|
(Read-only prototypes).
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
import re
|
||||||
|
from ast import literal_eval
|
||||||
|
from django.conf import settings
|
||||||
|
from evennia.scripts.scripts import DefaultScript
|
||||||
|
from evennia.objects.models import ObjectDB
|
||||||
|
from evennia.utils.create import create_script
|
||||||
|
from evennia.utils.utils import (
|
||||||
|
all_from_module, make_iter, is_iter, dbid_to_obj, callables_from_module,
|
||||||
|
get_all_typeclasses, to_str, dbref, justify)
|
||||||
|
from evennia.locks.lockhandler import validate_lockstring, check_lockstring
|
||||||
|
from evennia.utils import logger
|
||||||
|
from evennia.utils import inlinefuncs
|
||||||
|
from evennia.utils.evtable import EvTable
|
||||||
|
|
||||||
|
|
||||||
|
_MODULE_PROTOTYPE_MODULES = {}
|
||||||
|
_MODULE_PROTOTYPES = {}
|
||||||
|
_PROTOTYPE_META_NAMES = (
|
||||||
|
"prototype_key", "prototype_desc", "prototype_tags", "prototype_locks", "prototype_parent")
|
||||||
|
_PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
|
||||||
|
"key", "aliases", "typeclass", "location", "home", "destination",
|
||||||
|
"permissions", "locks", "exec", "tags", "attrs")
|
||||||
|
_PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
||||||
|
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||||
|
PROT_FUNCS = {}
|
||||||
|
|
||||||
|
|
||||||
|
_RE_DBREF = re.compile(r"(?<!\$obj\()(#[0-9]+)")
|
||||||
|
|
||||||
|
|
||||||
|
class PermissionError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class ValidationError(RuntimeError):
|
||||||
|
"""
|
||||||
|
Raised on prototype validation errors
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Protfunc parsing
|
||||||
|
|
||||||
|
for mod in settings.PROT_FUNC_MODULES:
|
||||||
|
try:
|
||||||
|
callables = callables_from_module(mod)
|
||||||
|
PROT_FUNCS.update(callables)
|
||||||
|
except ImportError:
|
||||||
|
logger.log_trace()
|
||||||
|
raise
|
||||||
|
|
||||||
|
|
||||||
|
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Parse a prototype value string for a protfunc and process it.
|
||||||
|
|
||||||
|
Available protfuncs are specified as callables in one of the modules of
|
||||||
|
`settings.PROTFUNC_MODULES`, or specified on the command line.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (any): The value to test for a parseable protfunc. Only strings will be parsed for
|
||||||
|
protfuncs, all other types are returned as-is.
|
||||||
|
available_functions (dict, optional): Mapping of name:protfunction to use for this parsing.
|
||||||
|
If not set, use default sources.
|
||||||
|
testing (bool, optional): Passed to protfunc. If in a testing mode, some protfuncs may
|
||||||
|
behave differently.
|
||||||
|
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||||
|
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
|
||||||
|
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
|
||||||
|
any (any): Passed on to the protfunc.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
testresult (tuple): If `testing` is set, returns a tuple (error, result) where error is
|
||||||
|
either None or a string detailing the error from protfunc_parser or seen when trying to
|
||||||
|
run `literal_eval` on the parsed string.
|
||||||
|
any (any): A structure to replace the string on the prototype level. If this is a
|
||||||
|
callable or a (callable, (args,)) structure, it will be executed as if one had supplied
|
||||||
|
it to the prototype directly. This structure is also passed through literal_eval so one
|
||||||
|
can get actual Python primitives out of it (not just strings). It will also identify
|
||||||
|
eventual object #dbrefs in the output from the protfunc.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not isinstance(value, basestring):
|
||||||
|
try:
|
||||||
|
value = value.dbref
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
value = to_str(value, force_string=True)
|
||||||
|
|
||||||
|
available_functions = PROT_FUNCS if available_functions is None else available_functions
|
||||||
|
|
||||||
|
# insert $obj(#dbref) for #dbref
|
||||||
|
value = _RE_DBREF.sub("$obj(\\1)", value)
|
||||||
|
|
||||||
|
result = inlinefuncs.parse_inlinefunc(
|
||||||
|
value, available_funcs=available_functions,
|
||||||
|
stacktrace=stacktrace, testing=testing, **kwargs)
|
||||||
|
|
||||||
|
err = None
|
||||||
|
try:
|
||||||
|
result = literal_eval(result)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
except Exception as err:
|
||||||
|
err = str(err)
|
||||||
|
if testing:
|
||||||
|
return err, result
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def format_available_protfuncs():
|
||||||
|
"""
|
||||||
|
Get all protfuncs in a pretty-formatted form.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
clr (str, optional): What coloration tag to use.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for protfunc_name, protfunc in PROT_FUNCS.items():
|
||||||
|
out.append("- |c${name}|n - |W{docs}".format(
|
||||||
|
name=protfunc_name, docs=protfunc.__doc__.strip().replace("\n", "")))
|
||||||
|
return justify("\n".join(out), indent=8)
|
||||||
|
|
||||||
|
|
||||||
|
# helper functions
|
||||||
|
|
||||||
|
def value_to_obj(value, force=True):
|
||||||
|
"Always convert value(s) to Object, or None"
|
||||||
|
stype = type(value)
|
||||||
|
if is_iter(value):
|
||||||
|
if stype == dict:
|
||||||
|
return {value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.iter()}
|
||||||
|
else:
|
||||||
|
return stype([value_to_obj_or_any(val) for val in value])
|
||||||
|
return dbid_to_obj(value, ObjectDB)
|
||||||
|
|
||||||
|
|
||||||
|
def value_to_obj_or_any(value):
|
||||||
|
"Convert value(s) to Object if possible, otherwise keep original value"
|
||||||
|
stype = type(value)
|
||||||
|
if is_iter(value):
|
||||||
|
if stype == dict:
|
||||||
|
return {value_to_obj_or_any(key):
|
||||||
|
value_to_obj_or_any(val) for key, val in value.items()}
|
||||||
|
else:
|
||||||
|
return stype([value_to_obj_or_any(val) for val in value])
|
||||||
|
obj = dbid_to_obj(value, ObjectDB)
|
||||||
|
return obj if obj is not None else value
|
||||||
|
|
||||||
|
|
||||||
|
def prototype_to_str(prototype):
|
||||||
|
"""
|
||||||
|
Format a prototype to a nice string representation.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prototype (dict): The prototype.
|
||||||
|
"""
|
||||||
|
|
||||||
|
header = """
|
||||||
|
|cprototype-key:|n {prototype_key}, |c-tags:|n {prototype_tags}, |c-locks:|n {prototype_locks}|n
|
||||||
|
|c-desc|n: {prototype_desc}
|
||||||
|
|cprototype-parent:|n {prototype_parent}
|
||||||
|
\n""".format(
|
||||||
|
prototype_key=prototype.get('prototype_key', '|r[UNSET](required)|n'),
|
||||||
|
prototype_tags=prototype.get('prototype_tags', '|wNone|n'),
|
||||||
|
prototype_locks=prototype.get('prototype_locks', '|wNone|n'),
|
||||||
|
prototype_desc=prototype.get('prototype_desc', '|wNone|n'),
|
||||||
|
prototype_parent=prototype.get('prototype_parent', '|wNone|n'))
|
||||||
|
|
||||||
|
key = prototype.get('key', '')
|
||||||
|
if key:
|
||||||
|
key = "|ckey:|n {key}".format(key=key)
|
||||||
|
aliases = prototype.get("aliases", '')
|
||||||
|
if aliases:
|
||||||
|
aliases = "|caliases:|n {aliases}".format(
|
||||||
|
aliases=", ".join(aliases))
|
||||||
|
attrs = prototype.get("attrs", '')
|
||||||
|
if attrs:
|
||||||
|
out = []
|
||||||
|
for (attrkey, value, category, locks) in attrs:
|
||||||
|
locks = ", ".join(lock for lock in locks if lock)
|
||||||
|
category = "|ccategory:|n {}".format(category) if category else ''
|
||||||
|
cat_locks = ""
|
||||||
|
if category or locks:
|
||||||
|
cat_locks = " (|ccategory:|n {category}, ".format(
|
||||||
|
category=category if category else "|wNone|n")
|
||||||
|
out.append(
|
||||||
|
"{attrkey}{cat_locks} |c=|n {value}".format(
|
||||||
|
attrkey=attrkey,
|
||||||
|
cat_locks=cat_locks,
|
||||||
|
locks=locks if locks else "|wNone|n",
|
||||||
|
value=value))
|
||||||
|
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
||||||
|
tags = prototype.get('tags', '')
|
||||||
|
if tags:
|
||||||
|
out = []
|
||||||
|
for (tagkey, category, data) in tags:
|
||||||
|
out.append("{tagkey} (category: {category}{dat})".format(
|
||||||
|
tagkey=tagkey, category=category, dat=", data: {}".format(data) if data else ""))
|
||||||
|
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
||||||
|
locks = prototype.get('locks', '')
|
||||||
|
if locks:
|
||||||
|
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
||||||
|
permissions = prototype.get("permissions", '')
|
||||||
|
if permissions:
|
||||||
|
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
||||||
|
location = prototype.get("location", '')
|
||||||
|
if location:
|
||||||
|
location = "|clocation:|n {location}".format(location=location)
|
||||||
|
home = prototype.get("home", '')
|
||||||
|
if home:
|
||||||
|
home = "|chome:|n {home}".format(home=home)
|
||||||
|
destination = prototype.get("destination", '')
|
||||||
|
if destination:
|
||||||
|
destination = "|cdestination:|n {destination}".format(destination=destination)
|
||||||
|
|
||||||
|
body = "\n".join(part for part in (key, aliases, attrs, tags, locks, permissions,
|
||||||
|
location, home, destination) if part)
|
||||||
|
|
||||||
|
return header.lstrip() + body.strip()
|
||||||
|
|
||||||
|
|
||||||
|
def check_permission(prototype_key, action, default=True):
|
||||||
|
"""
|
||||||
|
Helper function to check access to actions on given prototype.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prototype_key (str): The prototype to affect.
|
||||||
|
action (str): One of "spawn" or "edit".
|
||||||
|
default (str): If action is unknown or prototype has no locks
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
passes (bool): If permission for action is granted or not.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if action == 'edit':
|
||||||
|
if prototype_key in _MODULE_PROTOTYPES:
|
||||||
|
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||||
|
logger.log_err("{} is a read-only prototype "
|
||||||
|
"(defined as code in {}).".format(prototype_key, mod))
|
||||||
|
return False
|
||||||
|
|
||||||
|
prototype = search_prototype(key=prototype_key)
|
||||||
|
if not prototype:
|
||||||
|
logger.log_err("Prototype {} not found.".format(prototype_key))
|
||||||
|
return False
|
||||||
|
|
||||||
|
lockstring = prototype.get("prototype_locks")
|
||||||
|
|
||||||
|
if lockstring:
|
||||||
|
return check_lockstring(None, lockstring, default=default, access_type=action)
|
||||||
|
return default
|
||||||
|
|
||||||
|
|
||||||
|
def init_spawn_value(value, validator=None):
|
||||||
|
"""
|
||||||
|
Analyze the prototype value and produce a value useful at the point of spawning.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
value (any): This can be:
|
||||||
|
callable - will be called as callable()
|
||||||
|
(callable, (args,)) - will be called as callable(*args)
|
||||||
|
other - will be assigned depending on the variable type
|
||||||
|
validator (callable, optional): If given, this will be called with the value to
|
||||||
|
check and guarantee the outcome is of a given type.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
any (any): The (potentially pre-processed value to use for this prototype key)
|
||||||
|
|
||||||
|
"""
|
||||||
|
value = protfunc_parser(value)
|
||||||
|
validator = validator if validator else lambda o: o
|
||||||
|
if callable(value):
|
||||||
|
return validator(value())
|
||||||
|
elif value and is_iter(value) and callable(value[0]):
|
||||||
|
# a structure (callable, (args, ))
|
||||||
|
args = value[1:]
|
||||||
|
return validator(value[0](*make_iter(args)))
|
||||||
|
else:
|
||||||
|
return validator(value)
|
||||||
|
|
||||||
|
|
||||||
|
# module-based prototypes
|
||||||
|
|
||||||
|
for mod in settings.PROTOTYPE_MODULES:
|
||||||
|
# to remove a default prototype, override it with an empty dict.
|
||||||
|
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||||
|
prots = [(prototype_key.lower(), prot) for prototype_key, prot in all_from_module(mod).items()
|
||||||
|
if prot and isinstance(prot, dict)]
|
||||||
|
# assign module path to each prototype_key for easy reference
|
||||||
|
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||||
|
# make sure the prototype contains all meta info
|
||||||
|
for prototype_key, prot in prots:
|
||||||
|
actual_prot_key = prot.get('prototype_key', prototype_key).lower()
|
||||||
|
prot.update({
|
||||||
|
"prototype_key": actual_prot_key,
|
||||||
|
"prototype_desc": prot['prototype_desc'] if 'prototype_desc' in prot else mod,
|
||||||
|
"prototype_locks": (prot['prototype_locks']
|
||||||
|
if 'prototype_locks' in prot else "use:all();edit:false()"),
|
||||||
|
"prototype_tags": list(set(make_iter(prot.get('prototype_tags', [])) + ["module"]))})
|
||||||
|
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||||
|
|
||||||
|
|
||||||
|
# Db-based prototypes
|
||||||
|
|
||||||
|
|
||||||
|
class DbPrototype(DefaultScript):
|
||||||
|
"""
|
||||||
|
This stores a single prototype, in an Attribute `prototype`.
|
||||||
|
"""
|
||||||
|
def at_script_creation(self):
|
||||||
|
self.key = "empty prototype" # prototype_key
|
||||||
|
self.desc = "A prototype" # prototype_desc
|
||||||
|
self.db.prototype = {} # actual prototype
|
||||||
|
|
||||||
|
|
||||||
|
# Prototype manager functions
|
||||||
|
|
||||||
|
|
||||||
|
def save_prototype(**kwargs):
|
||||||
|
"""
|
||||||
|
Create/Store a prototype persistently.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
prototype_key (str): This is required for any storage.
|
||||||
|
All other kwargs are considered part of the new prototype dict.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
prototype (dict or None): The prototype stored using the given kwargs, None if deleting.
|
||||||
|
|
||||||
|
Raises:
|
||||||
|
prototypes.ValidationError: If prototype does not validate.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
No edit/spawn locks will be checked here - if this function is called the caller
|
||||||
|
is expected to have valid permissions.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
def _to_batchtuple(inp, *args):
|
||||||
|
"build tuple suitable for batch-creation"
|
||||||
|
if is_iter(inp):
|
||||||
|
# already a tuple/list, use as-is
|
||||||
|
return inp
|
||||||
|
return (inp, ) + args
|
||||||
|
|
||||||
|
prototype_key = kwargs.get("prototype_key")
|
||||||
|
if not prototype_key:
|
||||||
|
raise ValidationError("Prototype requires a prototype_key")
|
||||||
|
|
||||||
|
prototype_key = str(prototype_key).lower()
|
||||||
|
|
||||||
|
# we can't edit a prototype defined in a module
|
||||||
|
if prototype_key in _MODULE_PROTOTYPES:
|
||||||
|
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key, "N/A")
|
||||||
|
raise PermissionError("{} is a read-only prototype "
|
||||||
|
"(defined as code in {}).".format(prototype_key, mod))
|
||||||
|
|
||||||
|
# make sure meta properties are included with defaults
|
||||||
|
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||||
|
prototype = dict(stored_prototype[0].db.prototype) if stored_prototype else {}
|
||||||
|
|
||||||
|
kwargs['prototype_desc'] = kwargs.get("prototype_desc", prototype.get("prototype_desc", ""))
|
||||||
|
prototype_locks = kwargs.get(
|
||||||
|
"prototype_locks", prototype.get('prototype_locks', "spawn:all();edit:perm(Admin)"))
|
||||||
|
is_valid, err = validate_lockstring(prototype_locks)
|
||||||
|
if not is_valid:
|
||||||
|
raise ValidationError("Lock error: {}".format(err))
|
||||||
|
kwargs['prototype_locks'] = prototype_locks
|
||||||
|
|
||||||
|
prototype_tags = [
|
||||||
|
_to_batchtuple(tag, _PROTOTYPE_TAG_META_CATEGORY)
|
||||||
|
for tag in make_iter(kwargs.get("prototype_tags",
|
||||||
|
prototype.get('prototype_tags', [])))]
|
||||||
|
kwargs["prototype_tags"] = prototype_tags
|
||||||
|
|
||||||
|
prototype.update(kwargs)
|
||||||
|
|
||||||
|
if stored_prototype:
|
||||||
|
# edit existing prototype
|
||||||
|
stored_prototype = stored_prototype[0]
|
||||||
|
stored_prototype.desc = prototype['prototype_desc']
|
||||||
|
if prototype_tags:
|
||||||
|
stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
|
||||||
|
stored_prototype.tags.batch_add(*prototype['prototype_tags'])
|
||||||
|
stored_prototype.locks.add(prototype['prototype_locks'])
|
||||||
|
stored_prototype.attributes.add('prototype', prototype)
|
||||||
|
else:
|
||||||
|
# create a new prototype
|
||||||
|
stored_prototype = create_script(
|
||||||
|
DbPrototype, key=prototype_key, desc=prototype['prototype_desc'], persistent=True,
|
||||||
|
locks=prototype_locks, tags=prototype['prototype_tags'],
|
||||||
|
attributes=[("prototype", prototype)])
|
||||||
|
return stored_prototype.db.prototype
|
||||||
|
|
||||||
|
# alias
|
||||||
|
create_prototype = save_prototype
|
||||||
|
|
||||||
|
|
||||||
|
def delete_prototype(prototype_key, caller=None):
|
||||||
|
"""
|
||||||
|
Delete a stored prototype
|
||||||
|
|
||||||
|
Args:
|
||||||
|
key (str): The persistent prototype to delete.
|
||||||
|
caller (Account or Object, optionsl): Caller aiming to delete a prototype.
|
||||||
|
Note that no locks will be checked if`caller` is not passed.
|
||||||
|
Returns:
|
||||||
|
success (bool): If deletion worked or not.
|
||||||
|
Raises:
|
||||||
|
PermissionError: If 'edit' lock was not passed or deletion failed for some other reason.
|
||||||
|
|
||||||
|
"""
|
||||||
|
if prototype_key in _MODULE_PROTOTYPES:
|
||||||
|
mod = _MODULE_PROTOTYPE_MODULES.get(prototype_key.lower(), "N/A")
|
||||||
|
raise PermissionError("{} is a read-only prototype "
|
||||||
|
"(defined as code in {}).".format(prototype_key, mod))
|
||||||
|
|
||||||
|
stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)
|
||||||
|
|
||||||
|
if not stored_prototype:
|
||||||
|
raise PermissionError("Prototype {} was not found.".format(prototype_key))
|
||||||
|
|
||||||
|
stored_prototype = stored_prototype[0]
|
||||||
|
if caller:
|
||||||
|
if not stored_prototype.access(caller, 'edit'):
|
||||||
|
raise PermissionError("{} does not have permission to "
|
||||||
|
"delete prototype {}.".format(caller, prototype_key))
|
||||||
|
stored_prototype.delete()
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
def search_prototype(key=None, tags=None):
|
||||||
|
"""
|
||||||
|
Find prototypes based on key and/or tags, or all prototypes.
|
||||||
|
|
||||||
|
Kwargs:
|
||||||
|
key (str): An exact or partial key to query for.
|
||||||
|
tags (str or list): Tag key or keys to query for. These
|
||||||
|
will always be applied with the 'db_protototype'
|
||||||
|
tag category.
|
||||||
|
|
||||||
|
Return:
|
||||||
|
matches (list): All found prototype dicts. If no keys
|
||||||
|
or tags are given, all available prototypes will be returned.
|
||||||
|
|
||||||
|
Note:
|
||||||
|
The available prototypes is a combination of those supplied in
|
||||||
|
PROTOTYPE_MODULES and those stored in the database. Note that if
|
||||||
|
tags are given and the prototype has no tags defined, it will not
|
||||||
|
be found as a match.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# search module prototypes
|
||||||
|
|
||||||
|
mod_matches = {}
|
||||||
|
if tags:
|
||||||
|
# use tags to limit selection
|
||||||
|
tagset = set(tags)
|
||||||
|
mod_matches = {prototype_key: prototype
|
||||||
|
for prototype_key, prototype in _MODULE_PROTOTYPES.items()
|
||||||
|
if tagset.intersection(prototype.get("prototype_tags", []))}
|
||||||
|
else:
|
||||||
|
mod_matches = _MODULE_PROTOTYPES
|
||||||
|
if key:
|
||||||
|
if key in mod_matches:
|
||||||
|
# exact match
|
||||||
|
module_prototypes = [mod_matches[key]]
|
||||||
|
else:
|
||||||
|
# fuzzy matching
|
||||||
|
module_prototypes = [prototype for prototype_key, prototype in mod_matches.items()
|
||||||
|
if key in prototype_key]
|
||||||
|
else:
|
||||||
|
module_prototypes = [match for match in mod_matches.values()]
|
||||||
|
|
||||||
|
# search db-stored prototypes
|
||||||
|
|
||||||
|
if tags:
|
||||||
|
# exact match on tag(s)
|
||||||
|
tags = make_iter(tags)
|
||||||
|
tag_categories = ["db_prototype" for _ in tags]
|
||||||
|
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
|
||||||
|
else:
|
||||||
|
db_matches = DbPrototype.objects.all()
|
||||||
|
if key:
|
||||||
|
# exact or partial match on key
|
||||||
|
db_matches = db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
||||||
|
# return prototype
|
||||||
|
db_prototypes = [dict(dbprot.attributes.get("prototype", {})) for dbprot in db_matches]
|
||||||
|
|
||||||
|
matches = db_prototypes + module_prototypes
|
||||||
|
nmatches = len(matches)
|
||||||
|
if nmatches > 1 and key:
|
||||||
|
key = key.lower()
|
||||||
|
# avoid duplicates if an exact match exist between the two types
|
||||||
|
filter_matches = [mta for mta in matches
|
||||||
|
if mta.get('prototype_key') and mta['prototype_key'] == key]
|
||||||
|
if filter_matches and len(filter_matches) < nmatches:
|
||||||
|
matches = filter_matches
|
||||||
|
|
||||||
|
return matches
|
||||||
|
|
||||||
|
|
||||||
|
def search_objects_with_prototype(prototype_key):
|
||||||
|
"""
|
||||||
|
Retrieve all object instances created by a given prototype.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prototype_key (str): The exact (and unique) prototype identifier to query for.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
matches (Queryset): All matching objects spawned from this prototype.
|
||||||
|
|
||||||
|
"""
|
||||||
|
return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||||
|
|
||||||
|
|
||||||
|
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
||||||
|
"""
|
||||||
|
Collate a list of found prototypes based on search criteria and access.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
caller (Account or Object): The object requesting the list.
|
||||||
|
key (str, optional): Exact or partial prototype key to query for.
|
||||||
|
tags (str or list, optional): Tag key or keys to query for.
|
||||||
|
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
||||||
|
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
||||||
|
Returns:
|
||||||
|
table (EvTable or None): An EvTable representation of the prototypes. None
|
||||||
|
if no prototypes were found.
|
||||||
|
|
||||||
|
"""
|
||||||
|
# this allows us to pass lists of empty strings
|
||||||
|
tags = [tag for tag in make_iter(tags) if tag]
|
||||||
|
|
||||||
|
# get prototypes for readonly and db-based prototypes
|
||||||
|
prototypes = search_prototype(key, tags)
|
||||||
|
|
||||||
|
# get use-permissions of readonly attributes (edit is always False)
|
||||||
|
display_tuples = []
|
||||||
|
for prototype in sorted(prototypes, key=lambda d: d.get('prototype_key', '')):
|
||||||
|
lock_use = caller.locks.check_lockstring(
|
||||||
|
caller, prototype.get('prototype_locks', ''), access_type='spawn')
|
||||||
|
if not show_non_use and not lock_use:
|
||||||
|
continue
|
||||||
|
if prototype.get('prototype_key', '') in _MODULE_PROTOTYPES:
|
||||||
|
lock_edit = False
|
||||||
|
else:
|
||||||
|
lock_edit = caller.locks.check_lockstring(
|
||||||
|
caller, prototype.get('prototype_locks', ''), access_type='edit')
|
||||||
|
if not show_non_edit and not lock_edit:
|
||||||
|
continue
|
||||||
|
ptags = []
|
||||||
|
for ptag in prototype.get('prototype_tags', []):
|
||||||
|
if is_iter(ptag):
|
||||||
|
if len(ptag) > 1:
|
||||||
|
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
|
||||||
|
else:
|
||||||
|
ptags.append(ptag[0])
|
||||||
|
else:
|
||||||
|
ptags.append(str(ptag))
|
||||||
|
|
||||||
|
display_tuples.append(
|
||||||
|
(prototype.get('prototype_key', '<unset>'),
|
||||||
|
prototype.get('prototype_desc', '<unset>'),
|
||||||
|
"{}/{}".format('Y' if lock_use else 'N', 'Y' if lock_edit else 'N'),
|
||||||
|
",".join(ptags)))
|
||||||
|
|
||||||
|
if not display_tuples:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
table = []
|
||||||
|
width = 78
|
||||||
|
for i in range(len(display_tuples[0])):
|
||||||
|
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
|
||||||
|
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
|
||||||
|
table.reformat_column(0, width=22)
|
||||||
|
table.reformat_column(1, width=29)
|
||||||
|
table.reformat_column(2, width=11, align='c')
|
||||||
|
table.reformat_column(3, width=16)
|
||||||
|
return table
|
||||||
|
|
||||||
|
|
||||||
|
def validate_prototype(prototype, protkey=None, protparents=None,
|
||||||
|
is_prototype_base=True, strict=True, _flags=None):
|
||||||
|
"""
|
||||||
|
Run validation on a prototype, checking for inifinite regress.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
prototype (dict): Prototype to validate.
|
||||||
|
protkey (str, optional): The name of the prototype definition. If not given, the prototype
|
||||||
|
dict needs to have the `prototype_key` field set.
|
||||||
|
protpartents (dict, optional): The available prototype parent library. If
|
||||||
|
note given this will be determined from settings/database.
|
||||||
|
is_prototype_base (bool, optional): We are trying to create a new object *based on this
|
||||||
|
object*. This means we can't allow 'mixin'-style prototypes without typeclass/parent
|
||||||
|
etc.
|
||||||
|
strict (bool, optional): If unset, don't require needed keys, only check against infinite
|
||||||
|
recursion etc.
|
||||||
|
_flags (dict, optional): Internal work dict that should not be set externally.
|
||||||
|
Raises:
|
||||||
|
RuntimeError: If prototype has invalid structure.
|
||||||
|
RuntimeWarning: If prototype has issues that would make it unsuitable to build an object
|
||||||
|
with (it may still be useful as a mix-in prototype).
|
||||||
|
|
||||||
|
"""
|
||||||
|
assert isinstance(prototype, dict)
|
||||||
|
|
||||||
|
if _flags is None:
|
||||||
|
_flags = {"visited": [], "depth": 0, "typeclass": False, "errors": [], "warnings": []}
|
||||||
|
|
||||||
|
if not protparents:
|
||||||
|
protparents = {prototype.get('prototype_key', "").lower(): prototype
|
||||||
|
for prototype in search_prototype()}
|
||||||
|
|
||||||
|
protkey = protkey and protkey.lower() or prototype.get('prototype_key', None)
|
||||||
|
|
||||||
|
if strict and not bool(protkey):
|
||||||
|
_flags['errors'].append("Prototype lacks a `prototype_key`.")
|
||||||
|
protkey = "[UNSET]"
|
||||||
|
|
||||||
|
typeclass = prototype.get('typeclass')
|
||||||
|
prototype_parent = prototype.get('prototype_parent', [])
|
||||||
|
|
||||||
|
if strict and not (typeclass or prototype_parent):
|
||||||
|
if is_prototype_base:
|
||||||
|
_flags['errors'].append("Prototype {} requires `typeclass` "
|
||||||
|
"or 'prototype_parent'.".format(protkey))
|
||||||
|
else:
|
||||||
|
_flags['warnings'].append("Prototype {} can only be used as a mixin since it lacks "
|
||||||
|
"a typeclass or a prototype_parent.".format(protkey))
|
||||||
|
|
||||||
|
if strict and typeclass and typeclass not in get_all_typeclasses("evennia.objects.models.ObjectDB"):
|
||||||
|
_flags['errors'].append(
|
||||||
|
"Prototype {} is based on typeclass {}, which could not be imported!".format(
|
||||||
|
protkey, typeclass))
|
||||||
|
|
||||||
|
# recursively traverese prototype_parent chain
|
||||||
|
|
||||||
|
for protstring in make_iter(prototype_parent):
|
||||||
|
protstring = protstring.lower()
|
||||||
|
if protkey is not None and protstring == protkey:
|
||||||
|
_flags['errors'].append("Prototype {} tries to parent itself.".format(protkey))
|
||||||
|
protparent = protparents.get(protstring)
|
||||||
|
if not protparent:
|
||||||
|
_flags['errors'].append("Prototype {}'s prototype_parent '{}' was not found.".format(
|
||||||
|
(protkey, protstring)))
|
||||||
|
if id(prototype) in _flags['visited']:
|
||||||
|
_flags['errors'].append(
|
||||||
|
"{} has infinite nesting of prototypes.".format(protkey or prototype))
|
||||||
|
|
||||||
|
if _flags['errors']:
|
||||||
|
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
|
||||||
|
_flags['visited'].append(id(prototype))
|
||||||
|
_flags['depth'] += 1
|
||||||
|
validate_prototype(protparent, protstring, protparents,
|
||||||
|
is_prototype_base=is_prototype_base, _flags=_flags)
|
||||||
|
_flags['visited'].pop()
|
||||||
|
_flags['depth'] -= 1
|
||||||
|
|
||||||
|
if typeclass and not _flags['typeclass']:
|
||||||
|
_flags['typeclass'] = typeclass
|
||||||
|
|
||||||
|
# if we get back to the current level without a typeclass it's an error.
|
||||||
|
if strict and is_prototype_base and _flags['depth'] <= 0 and not _flags['typeclass']:
|
||||||
|
_flags['errors'].append("Prototype {} has no `typeclass` defined anywhere in its parent\n "
|
||||||
|
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
|
||||||
|
"prototype with a typeclass.".format(protkey))
|
||||||
|
|
||||||
|
if _flags['depth'] <= 0:
|
||||||
|
if _flags['errors']:
|
||||||
|
raise RuntimeError("Error: " + "\nError: ".join(_flags['errors']))
|
||||||
|
if _flags['warnings']:
|
||||||
|
raise RuntimeWarning("Warning: " + "\nWarning: ".join(_flags['warnings']))
|
||||||
|
|
||||||
|
# make sure prototype_locks are set to defaults
|
||||||
|
prototype_locks = [lstring.split(":", 1)
|
||||||
|
for lstring in prototype.get("prototype_locks", "").split(';') if ":" in lstring]
|
||||||
|
locktypes = [tup[0].strip() for tup in prototype_locks]
|
||||||
|
if "spawn" not in locktypes:
|
||||||
|
prototype_locks.append(("spawn", "all()"))
|
||||||
|
if "edit" not in locktypes:
|
||||||
|
prototype_locks.append(("edit", "all()"))
|
||||||
|
prototype_locks = ";".join(":".join(tup) for tup in prototype_locks)
|
||||||
|
prototype['prototype_locks'] = prototype_locks
|
||||||
613
evennia/prototypes/spawner.py
Normal file
613
evennia/prototypes/spawner.py
Normal 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
531
evennia/prototypes/tests.py
Normal file
|
|
@ -0,0 +1,531 @@
|
||||||
|
"""
|
||||||
|
Unit tests for the prototypes and spawner
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
from random import randint
|
||||||
|
import mock
|
||||||
|
from anything import Something
|
||||||
|
from django.test.utils import override_settings
|
||||||
|
from evennia.utils.test_resources import EvenniaTest
|
||||||
|
from evennia.utils.tests.test_evmenu import TestEvMenu
|
||||||
|
from evennia.prototypes import spawner, prototypes as protlib
|
||||||
|
from evennia.prototypes import menus as olc_menus
|
||||||
|
|
||||||
|
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
||||||
|
|
||||||
|
_PROTPARENTS = {
|
||||||
|
"NOBODY": {},
|
||||||
|
"GOBLIN": {
|
||||||
|
"prototype_key": "GOBLIN",
|
||||||
|
"typeclass": "evennia.objects.objects.DefaultObject",
|
||||||
|
"key": "goblin grunt",
|
||||||
|
"health": lambda: randint(1, 1),
|
||||||
|
"resists": ["cold", "poison"],
|
||||||
|
"attacks": ["fists"],
|
||||||
|
"weaknesses": ["fire", "light"]
|
||||||
|
},
|
||||||
|
"GOBLIN_WIZARD": {
|
||||||
|
"prototype_parent": "GOBLIN",
|
||||||
|
"key": "goblin wizard",
|
||||||
|
"spells": ["fire ball", "lighting bolt"]
|
||||||
|
},
|
||||||
|
"GOBLIN_ARCHER": {
|
||||||
|
"prototype_parent": "GOBLIN",
|
||||||
|
"key": "goblin archer",
|
||||||
|
"attacks": ["short bow"]
|
||||||
|
},
|
||||||
|
"ARCHWIZARD": {
|
||||||
|
"prototype_parent": "GOBLIN",
|
||||||
|
"attacks": ["archwizard staff"],
|
||||||
|
},
|
||||||
|
"GOBLIN_ARCHWIZARD": {
|
||||||
|
"key": "goblin archwizard",
|
||||||
|
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class TestSpawner(EvenniaTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestSpawner, self).setUp()
|
||||||
|
self.prot1 = {"prototype_key": "testprototype",
|
||||||
|
"typeclass": "evennia.objects.objects.DefaultObject"}
|
||||||
|
|
||||||
|
def test_spawn(self):
|
||||||
|
obj1 = spawner.spawn(self.prot1)
|
||||||
|
# check spawned objects have the right tag
|
||||||
|
self.assertEqual(list(protlib.search_objects_with_prototype("testprototype")), obj1)
|
||||||
|
self.assertEqual([o.key for o in spawner.spawn(
|
||||||
|
_PROTPARENTS["GOBLIN"], _PROTPARENTS["GOBLIN_ARCHWIZARD"],
|
||||||
|
prototype_parents=_PROTPARENTS)], ['goblin grunt', 'goblin archwizard'])
|
||||||
|
|
||||||
|
|
||||||
|
class TestUtils(EvenniaTest):
|
||||||
|
|
||||||
|
def test_prototype_from_object(self):
|
||||||
|
self.maxDiff = None
|
||||||
|
self.obj1.attributes.add("test", "testval")
|
||||||
|
self.obj1.tags.add('foo')
|
||||||
|
new_prot = spawner.prototype_from_object(self.obj1)
|
||||||
|
self.assertEqual(
|
||||||
|
{'attrs': [('test', 'testval', None, [''])],
|
||||||
|
'home': Something,
|
||||||
|
'key': 'Obj',
|
||||||
|
'location': Something,
|
||||||
|
'locks': ";".join([
|
||||||
|
'call:true()',
|
||||||
|
'control:perm(Developer)',
|
||||||
|
'delete:perm(Admin)',
|
||||||
|
'edit:perm(Admin)',
|
||||||
|
'examine:perm(Builder)',
|
||||||
|
'get:all()',
|
||||||
|
'puppet:pperm(Developer)',
|
||||||
|
'tell:perm(Admin)',
|
||||||
|
'view:all()']),
|
||||||
|
'prototype_desc': 'Built from Obj',
|
||||||
|
'prototype_key': Something,
|
||||||
|
'prototype_locks': 'spawn:all();edit:all()',
|
||||||
|
'prototype_tags': [],
|
||||||
|
'tags': [(u'foo', None, None)],
|
||||||
|
'typeclass': 'evennia.objects.objects.DefaultObject'}, new_prot)
|
||||||
|
|
||||||
|
def test_update_objects_from_prototypes(self):
|
||||||
|
|
||||||
|
self.maxDiff = None
|
||||||
|
self.obj1.attributes.add('oldtest', 'to_remove')
|
||||||
|
|
||||||
|
old_prot = spawner.prototype_from_object(self.obj1)
|
||||||
|
|
||||||
|
# modify object away from prototype
|
||||||
|
self.obj1.attributes.add('test', 'testval')
|
||||||
|
self.obj1.aliases.add('foo')
|
||||||
|
self.obj1.key = 'NewObj'
|
||||||
|
|
||||||
|
# modify prototype
|
||||||
|
old_prot['new'] = 'new_val'
|
||||||
|
old_prot['test'] = 'testval_changed'
|
||||||
|
old_prot['permissions'] = 'Builder'
|
||||||
|
# this will not update, since we don't update the prototype on-disk
|
||||||
|
old_prot['prototype_desc'] = 'New version of prototype'
|
||||||
|
|
||||||
|
# diff obj/prototype
|
||||||
|
pdiff = spawner.prototype_diff_from_object(old_prot, self.obj1)
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
pdiff,
|
||||||
|
({'aliases': 'REMOVE',
|
||||||
|
'attrs': 'REPLACE',
|
||||||
|
'home': 'KEEP',
|
||||||
|
'key': 'UPDATE',
|
||||||
|
'location': 'KEEP',
|
||||||
|
'locks': 'KEEP',
|
||||||
|
'new': 'UPDATE',
|
||||||
|
'permissions': 'UPDATE',
|
||||||
|
'prototype_desc': 'UPDATE',
|
||||||
|
'prototype_key': 'UPDATE',
|
||||||
|
'prototype_locks': 'KEEP',
|
||||||
|
'prototype_tags': 'KEEP',
|
||||||
|
'test': 'UPDATE',
|
||||||
|
'typeclass': 'KEEP'},
|
||||||
|
{'attrs': [('oldtest', 'to_remove', None, ['']),
|
||||||
|
('test', 'testval', None, [''])],
|
||||||
|
'prototype_locks': 'spawn:all();edit:all()',
|
||||||
|
'prototype_key': Something,
|
||||||
|
'locks': ";".join([
|
||||||
|
'call:true()', 'control:perm(Developer)',
|
||||||
|
'delete:perm(Admin)', 'edit:perm(Admin)',
|
||||||
|
'examine:perm(Builder)', 'get:all()',
|
||||||
|
'puppet:pperm(Developer)', 'tell:perm(Admin)',
|
||||||
|
'view:all()']),
|
||||||
|
'prototype_tags': [],
|
||||||
|
'location': "#1",
|
||||||
|
'key': 'NewObj',
|
||||||
|
'home': '#1',
|
||||||
|
'typeclass': 'evennia.objects.objects.DefaultObject',
|
||||||
|
'prototype_desc': 'Built from NewObj',
|
||||||
|
'aliases': 'foo'})
|
||||||
|
)
|
||||||
|
|
||||||
|
# apply diff
|
||||||
|
count = spawner.batch_update_objects_with_prototype(
|
||||||
|
old_prot, diff=pdiff[0], objects=[self.obj1])
|
||||||
|
self.assertEqual(count, 1)
|
||||||
|
|
||||||
|
new_prot = spawner.prototype_from_object(self.obj1)
|
||||||
|
self.assertEqual({'attrs': [('test', 'testval_changed', None, ['']),
|
||||||
|
('new', 'new_val', None, [''])],
|
||||||
|
'home': Something,
|
||||||
|
'key': 'Obj',
|
||||||
|
'location': Something,
|
||||||
|
'locks': ";".join([
|
||||||
|
'call:true()',
|
||||||
|
'control:perm(Developer)',
|
||||||
|
'delete:perm(Admin)',
|
||||||
|
'edit:perm(Admin)',
|
||||||
|
'examine:perm(Builder)',
|
||||||
|
'get:all()',
|
||||||
|
'puppet:pperm(Developer)',
|
||||||
|
'tell:perm(Admin)',
|
||||||
|
'view:all()']),
|
||||||
|
'permissions': ['builder'],
|
||||||
|
'prototype_desc': 'Built from Obj',
|
||||||
|
'prototype_key': Something,
|
||||||
|
'prototype_locks': 'spawn:all();edit:all()',
|
||||||
|
'prototype_tags': [],
|
||||||
|
'typeclass': 'evennia.objects.objects.DefaultObject'},
|
||||||
|
new_prot)
|
||||||
|
|
||||||
|
|
||||||
|
class TestProtLib(EvenniaTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestProtLib, self).setUp()
|
||||||
|
self.obj1.attributes.add("testattr", "testval")
|
||||||
|
self.prot = spawner.prototype_from_object(self.obj1)
|
||||||
|
|
||||||
|
def test_prototype_to_str(self):
|
||||||
|
prstr = protlib.prototype_to_str(self.prot)
|
||||||
|
self.assertTrue(prstr.startswith("|cprototype-key:|n"))
|
||||||
|
|
||||||
|
def test_check_permission(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@override_settings(PROT_FUNC_MODULES=['evennia.prototypes.protfuncs'], CLIENT_DEFAULT_WIDTH=20)
|
||||||
|
class TestProtFuncs(EvenniaTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestProtFuncs, self).setUp()
|
||||||
|
self.prot = {"prototype_key": "test_prototype",
|
||||||
|
"prototype_desc": "testing prot",
|
||||||
|
"key": "ExampleObj"}
|
||||||
|
|
||||||
|
@mock.patch("evennia.prototypes.protfuncs.base_random", new=mock.MagicMock(return_value=0.5))
|
||||||
|
@mock.patch("evennia.prototypes.protfuncs.base_randint", new=mock.MagicMock(return_value=5))
|
||||||
|
def test_protfuncs(self):
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$random()"), 0.5)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$randint(1, 10)"), 5)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$left_justify( foo )"), "foo ")
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$right_justify( foo )"), " foo")
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$center_justify(foo )"), " foo ")
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$full_justify(foo bar moo too)"), 'foo bar moo too')
|
||||||
|
self.assertEqual(
|
||||||
|
protlib.protfunc_parser("$right_justify( foo )", testing=True),
|
||||||
|
('unexpected indent (<unknown>, line 1)', ' foo'))
|
||||||
|
|
||||||
|
test_prot = {"key1": "value1",
|
||||||
|
"key2": 2}
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$protkey(key1)", testing=True, prototype=test_prot), (None, "value1"))
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$protkey(key2)", testing=True, prototype=test_prot), (None, 2))
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$add(1, 2)"), 3)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$add(10, 25)"), 35)
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$add('''[1,2,3]''', '''[4,5,6]''')"), [1, 2, 3, 4, 5, 6])
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$add(foo, bar)"), "foo bar")
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$sub(5, 2)"), 3)
|
||||||
|
self.assertRaises(TypeError, protlib.protfunc_parser, "$sub(5, test)")
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$mult(5, 2)"), 10)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$mult( 5 , 10)"), 50)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$mult('foo',3)"), "foofoofoo")
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$mult(foo,3)"), "foofoofoo")
|
||||||
|
self.assertRaises(TypeError, protlib.protfunc_parser, "$mult(foo, foo)")
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$toint(5.3)"), 5)
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$div(5, 2)"), 2.5)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$toint($div(5, 2))"), 2)
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$sub($add(5, 3), $add(10, 2))"), -4)
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$eval('2')"), '2')
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$eval(['test', 1, '2', 3.5, \"foo\"])"), ['test', 1, '2', 3.5, 'foo'])
|
||||||
|
self.assertEqual(protlib.protfunc_parser(
|
||||||
|
"$eval({'test': '1', 2:3, 3: $toint(3.5)})"), {'test': '1', 2: 3, 3: 3})
|
||||||
|
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$obj(#1)", session=self.session), '#1')
|
||||||
|
self.assertEqual(protlib.protfunc_parser("#1", session=self.session), '#1')
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$obj(Char)", session=self.session), '#6')
|
||||||
|
self.assertEqual(protlib.protfunc_parser("$objlist(#1)", session=self.session), ['#1'])
|
||||||
|
|
||||||
|
self.assertEqual(protlib.value_to_obj(
|
||||||
|
protlib.protfunc_parser("#6", session=self.session)), self.char1)
|
||||||
|
self.assertEqual(protlib.value_to_obj_or_any(
|
||||||
|
protlib.protfunc_parser("#6", session=self.session)), self.char1)
|
||||||
|
self.assertEqual(protlib.value_to_obj_or_any(
|
||||||
|
protlib.protfunc_parser("[1,2,3,'#6',5]", session=self.session)),
|
||||||
|
[1, 2, 3, self.char1, 5])
|
||||||
|
|
||||||
|
|
||||||
|
class TestPrototypeStorage(EvenniaTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestPrototypeStorage, self).setUp()
|
||||||
|
self.maxDiff = None
|
||||||
|
|
||||||
|
self.prot1 = spawner.prototype_from_object(self.obj1)
|
||||||
|
self.prot1['prototype_key'] = 'testprototype1'
|
||||||
|
self.prot1['prototype_desc'] = 'testdesc1'
|
||||||
|
self.prot1['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
|
||||||
|
|
||||||
|
self.prot2 = self.prot1.copy()
|
||||||
|
self.prot2['prototype_key'] = 'testprototype2'
|
||||||
|
self.prot2['prototype_desc'] = 'testdesc2'
|
||||||
|
self.prot2['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
|
||||||
|
|
||||||
|
self.prot3 = self.prot2.copy()
|
||||||
|
self.prot3['prototype_key'] = 'testprototype3'
|
||||||
|
self.prot3['prototype_desc'] = 'testdesc3'
|
||||||
|
self.prot3['prototype_tags'] = [('foo1', _PROTOTYPE_TAG_META_CATEGORY)]
|
||||||
|
|
||||||
|
def test_prototype_storage(self):
|
||||||
|
|
||||||
|
prot1 = protlib.create_prototype(**self.prot1)
|
||||||
|
|
||||||
|
self.assertTrue(bool(prot1))
|
||||||
|
self.assertEqual(prot1, self.prot1)
|
||||||
|
|
||||||
|
self.assertEqual(prot1['prototype_desc'], "testdesc1")
|
||||||
|
|
||||||
|
self.assertEqual(prot1['prototype_tags'], [("foo1", _PROTOTYPE_TAG_META_CATEGORY)])
|
||||||
|
self.assertEqual(
|
||||||
|
protlib.DbPrototype.objects.get_by_tag(
|
||||||
|
"foo1", _PROTOTYPE_TAG_META_CATEGORY)[0].db.prototype, prot1)
|
||||||
|
|
||||||
|
prot2 = protlib.create_prototype(**self.prot2)
|
||||||
|
self.assertEqual(
|
||||||
|
[pobj.db.prototype
|
||||||
|
for pobj in protlib.DbPrototype.objects.get_by_tag(
|
||||||
|
"foo1", _PROTOTYPE_TAG_META_CATEGORY)],
|
||||||
|
[prot1, prot2])
|
||||||
|
|
||||||
|
# add to existing prototype
|
||||||
|
prot1b = protlib.create_prototype(
|
||||||
|
prototype_key='testprototype1', foo='bar', prototype_tags=['foo2'])
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
[pobj.db.prototype
|
||||||
|
for pobj in protlib.DbPrototype.objects.get_by_tag(
|
||||||
|
"foo2", _PROTOTYPE_TAG_META_CATEGORY)],
|
||||||
|
[prot1b])
|
||||||
|
|
||||||
|
self.assertEqual(list(protlib.search_prototype("testprototype2")), [prot2])
|
||||||
|
self.assertNotEqual(list(protlib.search_prototype("testprototype1")), [prot1])
|
||||||
|
self.assertEqual(list(protlib.search_prototype("testprototype1")), [prot1b])
|
||||||
|
|
||||||
|
prot3 = protlib.create_prototype(**self.prot3)
|
||||||
|
|
||||||
|
# partial match
|
||||||
|
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
||||||
|
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
||||||
|
|
||||||
|
self.assertTrue(str(unicode(protlib.list_prototypes(self.char1))))
|
||||||
|
|
||||||
|
|
||||||
|
class _MockMenu(object):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
class TestMenuModule(EvenniaTest):
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
super(TestMenuModule, self).setUp()
|
||||||
|
|
||||||
|
# set up fake store
|
||||||
|
self.caller = self.char1
|
||||||
|
menutree = _MockMenu()
|
||||||
|
self.caller.ndb._menutree = menutree
|
||||||
|
|
||||||
|
self.test_prot = {"prototype_key": "test_prot",
|
||||||
|
"typeclass": "evennia.objects.objects.DefaultObject",
|
||||||
|
"prototype_locks": "edit:all();spawn:all()"}
|
||||||
|
|
||||||
|
def test_helpers(self):
|
||||||
|
|
||||||
|
caller = self.caller
|
||||||
|
|
||||||
|
# general helpers
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller), {})
|
||||||
|
self.assertEqual(olc_menus._is_new_prototype(caller), True)
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._set_menu_prototype(caller, {}), {})
|
||||||
|
|
||||||
|
self.assertEqual(
|
||||||
|
olc_menus._set_prototype_value(caller, "key", "TestKey"), {"key": "TestKey"})
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "TestKey"})
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._format_option_value(
|
||||||
|
"key", required=True, prototype=olc_menus._get_menu_prototype(caller)), " (TestKey|n)")
|
||||||
|
self.assertEqual(olc_menus._format_option_value(
|
||||||
|
[1, 2, 3, "foo"], required=True), ' (1, 2, 3, foo|n)')
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._set_property(
|
||||||
|
caller, "ChangedKey", prop="key", processor=str, next_node="foo"), "foo")
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller), {"key": "ChangedKey"})
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._wizard_options(
|
||||||
|
"ThisNode", "PrevNode", "NextNode"),
|
||||||
|
[{'goto': 'node_PrevNode', 'key': ('|wB|Wack', 'b'), 'desc': '|W(PrevNode)|n'},
|
||||||
|
{'goto': 'node_NextNode', 'key': ('|wF|Worward', 'f'), 'desc': '|W(NextNode)|n'},
|
||||||
|
{'goto': 'node_index', 'key': ('|wI|Wndex', 'i')},
|
||||||
|
{'goto': ('node_validate_prototype', {'back': 'ThisNode'}),
|
||||||
|
'key': ('|wV|Walidate prototype', 'validate', 'v')}])
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._validate_prototype(self.test_prot), (False, Something))
|
||||||
|
self.assertEqual(olc_menus._validate_prototype(
|
||||||
|
{"prototype_key": "testthing", "key": "mytest"}),
|
||||||
|
(True, Something))
|
||||||
|
|
||||||
|
choices = ["test1", "test2", "test3", "test4"]
|
||||||
|
actions = (("examine", "e", "l"), ("add", "a"), ("foo", "f"))
|
||||||
|
self.assertEqual(olc_menus._default_parse("l4", choices, *actions), ('test4', 'examine'))
|
||||||
|
self.assertEqual(olc_menus._default_parse("add 2", choices, *actions), ('test2', 'add'))
|
||||||
|
self.assertEqual(olc_menus._default_parse("foo3", choices, *actions), ('test3', 'foo'))
|
||||||
|
self.assertEqual(olc_menus._default_parse("f3", choices, *actions), ('test3', 'foo'))
|
||||||
|
self.assertEqual(olc_menus._default_parse("f5", choices, *actions), (None, None))
|
||||||
|
|
||||||
|
def test_node_helpers(self):
|
||||||
|
|
||||||
|
caller = self.caller
|
||||||
|
|
||||||
|
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
|
||||||
|
new=mock.MagicMock(return_value=[self.test_prot])):
|
||||||
|
# prototype_key helpers
|
||||||
|
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), None)
|
||||||
|
caller.ndb._menutree.olc_new = True
|
||||||
|
self.assertEqual(olc_menus._check_prototype_key(caller, "test_prot"), "node_index")
|
||||||
|
|
||||||
|
# prototype_parent helpers
|
||||||
|
self.assertEqual(olc_menus._all_prototype_parents(caller), ['test_prot'])
|
||||||
|
# self.assertEqual(olc_menus._prototype_parent_parse(
|
||||||
|
# caller, 'test_prot'),
|
||||||
|
# "|cprototype key:|n test_prot, |ctags:|n None, |clocks:|n edit:all();spawn:all() "
|
||||||
|
# "\n|cdesc:|n None \n|cprototype:|n "
|
||||||
|
# "{\n 'typeclass': 'evennia.objects.objects.DefaultObject', \n}")
|
||||||
|
|
||||||
|
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
|
||||||
|
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
|
||||||
|
self.assertEqual(olc_menus._prototype_parent_select(caller, "goblin"), "node_prototype_parent")
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller),
|
||||||
|
{'prototype_key': 'test_prot',
|
||||||
|
'prototype_locks': 'edit:all();spawn:all()',
|
||||||
|
'prototype_parent': 'goblin',
|
||||||
|
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
||||||
|
|
||||||
|
# typeclass helpers
|
||||||
|
with mock.patch("evennia.utils.utils.get_all_typeclasses",
|
||||||
|
new=mock.MagicMock(return_value={"foo": None, "bar": None})):
|
||||||
|
self.assertEqual(olc_menus._all_typeclasses(caller), ["bar", "foo"])
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._typeclass_select(
|
||||||
|
caller, "evennia.objects.objects.DefaultObject"), None)
|
||||||
|
# prototype_parent should be popped off here
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller),
|
||||||
|
{'prototype_key': 'test_prot',
|
||||||
|
'prototype_locks': 'edit:all();spawn:all()',
|
||||||
|
'prototype_parent': 'goblin',
|
||||||
|
'typeclass': 'evennia.objects.objects.DefaultObject'})
|
||||||
|
|
||||||
|
# attr helpers
|
||||||
|
self.assertEqual(olc_menus._caller_attrs(caller), [])
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test1=foo1"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test2;cat1=foo2"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test3;cat2;edit:false()=foo3"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test4;cat3;set:true();edit:false()=foo4"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test5;cat4;set:true();edit:false()=123"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_attr(caller, "test1=foo1_changed"), Something)
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)['attrs'],
|
||||||
|
[("test1", "foo1_changed", None, ''),
|
||||||
|
("test2", "foo2", "cat1", ''),
|
||||||
|
("test3", "foo3", "cat2", "edit:false()"),
|
||||||
|
("test4", "foo4", "cat3", "set:true();edit:false()"),
|
||||||
|
("test5", '123', "cat4", "set:true();edit:false()")])
|
||||||
|
|
||||||
|
# tag helpers
|
||||||
|
self.assertEqual(olc_menus._caller_tags(caller), [])
|
||||||
|
self.assertEqual(olc_menus._add_tag(caller, "foo1"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_tag(caller, "foo2;cat1"), Something)
|
||||||
|
self.assertEqual(olc_menus._add_tag(caller, "foo3;cat2;dat1"), Something)
|
||||||
|
self.assertEqual(olc_menus._caller_tags(caller), ['foo1', 'foo2', 'foo3'])
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'],
|
||||||
|
[('foo1', None, ""),
|
||||||
|
('foo2', 'cat1', ""),
|
||||||
|
('foo3', 'cat2', "dat1")])
|
||||||
|
self.assertEqual(olc_menus._add_tag(caller, "foo1", delete=True), "Removed Tag 'foo1'.")
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)['tags'],
|
||||||
|
[('foo2', 'cat1', ""),
|
||||||
|
('foo3', 'cat2', "dat1")])
|
||||||
|
|
||||||
|
self.assertEqual(olc_menus._display_tag(olc_menus._get_menu_prototype(caller)['tags'][0]), Something)
|
||||||
|
self.assertEqual(olc_menus._caller_tags(caller), ["foo2", "foo3"])
|
||||||
|
|
||||||
|
protlib.save_prototype(**self.test_prot)
|
||||||
|
|
||||||
|
# locks helpers
|
||||||
|
self.assertEqual(olc_menus._lock_add(caller, "foo:false()"), "Added lock 'foo:false()'.")
|
||||||
|
self.assertEqual(olc_menus._lock_add(caller, "foo2:false()"), "Added lock 'foo2:false()'.")
|
||||||
|
self.assertEqual(olc_menus._lock_add(caller, "foo2:true()"), "Lock with locktype 'foo2' updated.")
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)["locks"], "foo:false();foo2:true()")
|
||||||
|
|
||||||
|
# perm helpers
|
||||||
|
self.assertEqual(olc_menus._add_perm(caller, "foo"), "Added Permission 'foo'")
|
||||||
|
self.assertEqual(olc_menus._add_perm(caller, "foo2"), "Added Permission 'foo2'")
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)["permissions"], ["foo", "foo2"])
|
||||||
|
|
||||||
|
# prototype_tags helpers
|
||||||
|
self.assertEqual(olc_menus._add_prototype_tag(caller, "foo"), "Added Prototype-Tag 'foo'.")
|
||||||
|
self.assertEqual(olc_menus._add_prototype_tag(caller, "foo2"), "Added Prototype-Tag 'foo2'.")
|
||||||
|
self.assertEqual(olc_menus._get_menu_prototype(caller)["prototype_tags"], ["foo", "foo2"])
|
||||||
|
|
||||||
|
# spawn helpers
|
||||||
|
with mock.patch("evennia.prototypes.menus.protlib.search_prototype",
|
||||||
|
new=mock.MagicMock(return_value=[_PROTPARENTS['GOBLIN']])):
|
||||||
|
self.assertEqual(olc_menus._spawn(caller, prototype=self.test_prot), Something)
|
||||||
|
obj = caller.contents[0]
|
||||||
|
|
||||||
|
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
|
||||||
|
self.assertEqual(obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot['prototype_key'])
|
||||||
|
|
||||||
|
# update helpers
|
||||||
|
self.assertEqual(olc_menus._apply_diff(
|
||||||
|
caller, prototype=self.test_prot, back_node="foo", objects=[obj]), 'foo') # no changes to apply
|
||||||
|
self.test_prot['key'] = "updated key" # change prototype
|
||||||
|
self.assertEqual(olc_menus._apply_diff(
|
||||||
|
caller, prototype=self.test_prot, objects=[obj], back_node='foo'), 'foo') # apply change to the one obj
|
||||||
|
|
||||||
|
# load helpers
|
||||||
|
self.assertEqual(olc_menus._prototype_load_select(caller, self.test_prot['prototype_key']),
|
||||||
|
('node_examine_entity', {'text': '|gLoaded prototype test_prot.|n', 'back': 'index'}) )
|
||||||
|
|
||||||
|
|
||||||
|
@mock.patch("evennia.prototypes.menus.protlib.search_prototype", new=mock.MagicMock(
|
||||||
|
return_value=[{"prototype_key": "TestPrototype",
|
||||||
|
"typeclass": "TypeClassTest", "key": "TestObj"}]))
|
||||||
|
@mock.patch("evennia.utils.utils.get_all_typeclasses", new=mock.MagicMock(
|
||||||
|
return_value={"TypeclassTest": None}))
|
||||||
|
class TestOLCMenu(TestEvMenu):
|
||||||
|
|
||||||
|
maxDiff = None
|
||||||
|
menutree = "evennia.prototypes.menus"
|
||||||
|
startnode = "node_index"
|
||||||
|
|
||||||
|
# debug_output = True
|
||||||
|
expect_all_nodes = True
|
||||||
|
|
||||||
|
expected_node_texts = {
|
||||||
|
"node_index": "|c --- Prototype wizard --- |n"
|
||||||
|
}
|
||||||
|
|
||||||
|
expected_tree = ['node_index', ['node_prototype_key', ['node_index', 'node_index', 'node_validate_prototype', ['node_index', 'node_index'], 'node_index'], 'node_prototype_parent', ['node_prototype_parent', 'node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_typeclass', ['node_typeclass', 'node_prototype_parent', 'node_typeclass', 'node_index', 'node_validate_prototype', 'node_index'], 'node_key', ['node_typeclass', 'node_key', 'node_index', 'node_validate_prototype', 'node_index'], 'node_aliases', ['node_key', 'node_aliases', 'node_index', 'node_validate_prototype', 'node_index'], 'node_attrs', ['node_aliases', 'node_attrs', 'node_index', 'node_validate_prototype', 'node_index'], 'node_tags', ['node_attrs', 'node_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_locks', ['node_tags', 'node_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_permissions', ['node_locks', 'node_permissions', 'node_index', 'node_validate_prototype', 'node_index'], 'node_location', ['node_permissions', 'node_location', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_home', ['node_location', 'node_home', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_destination', ['node_home', 'node_destination', 'node_index', 'node_validate_prototype', 'node_index', 'node_index'], 'node_prototype_desc', ['node_prototype_key', 'node_prototype_parent', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_tags', ['node_prototype_desc', 'node_prototype_tags', 'node_index', 'node_validate_prototype', 'node_index'], 'node_prototype_locks', ['node_examine_entity', ['node_prototype_locks', 'node_prototype_locks', 'node_prototype_locks'], 'node_examine_entity', 'node_prototype_locks', 'node_index', 'node_validate_prototype', 'node_index'], 'node_validate_prototype', 'node_index', 'node_prototype_spawn', ['node_index', 'node_validate_prototype'], 'node_index', 'node_search_object', ['node_index', 'node_index']]]
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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,14 +956,39 @@ 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):
|
||||||
print("... Server stopped.")
|
if when_stopped:
|
||||||
_reactor_stop()
|
when_stopped()
|
||||||
|
else:
|
||||||
|
print("... Server stopped.")
|
||||||
|
_reactor_stop()
|
||||||
|
|
||||||
def _portal_running(response):
|
def _portal_running(response):
|
||||||
_, srun, _, _, _, _ = _parse_status(response)
|
_, srun, _, _, _, _ = _parse_status(response)
|
||||||
|
|
@ -971,8 +997,11 @@ def stop_server_only():
|
||||||
wait_for_status_reply(_server_stopped)
|
wait_for_status_reply(_server_stopped)
|
||||||
send_instruction(SSHUTD, {})
|
send_instruction(SSHUTD, {})
|
||||||
else:
|
else:
|
||||||
print("Server is not running.")
|
if when_stopped:
|
||||||
_reactor_stop()
|
when_stopped()
|
||||||
|
else:
|
||||||
|
print("Server is not running.")
|
||||||
|
_reactor_stop()
|
||||||
|
|
||||||
def _portal_not_running(fail):
|
def _portal_not_running(fail):
|
||||||
print("Evennia is not running.")
|
print("Evennia is not running.")
|
||||||
|
|
@ -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':
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
@ -159,8 +169,8 @@ 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
|
||||||
|
|
|
||||||
|
|
@ -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):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
sessid, kwargs = self.data_in(packed_data)
|
try:
|
||||||
session = self.factory.portal.sessions.get(sessid, None)
|
sessid, kwargs = self.data_in(packed_data)
|
||||||
if session:
|
session = self.factory.portal.sessions.get(sessid, None)
|
||||||
self.factory.portal.sessions.data_out(session, **kwargs)
|
if session:
|
||||||
|
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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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,10 +507,11 @@ 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:
|
||||||
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
|
# custom logging, but only if we are not running in interactive mode
|
||||||
os.path.dirname(settings.SERVER_LOG_FILE))
|
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
|
||||||
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
|
os.path.dirname(settings.SERVER_LOG_FILE))
|
||||||
|
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
|
||||||
|
|
||||||
# The main evennia server program. This sets up the database
|
# The main evennia server program. This sets up the database
|
||||||
# and is where we store all the other services.
|
# and is where we store all the other services.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)`
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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.
|
||||||
|
|
|
||||||
|
|
@ -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))
|
||||||
|
|
|
||||||
|
|
@ -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,9 +682,49 @@ 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 extract_goto_exec(self, nodename, option_dict):
|
||||||
|
"""
|
||||||
|
Helper: Get callables and their eventual kwargs.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
nodename (str): The current node name (used for error reporting).
|
||||||
|
option_dict (dict): The seleted option's dict.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
goto (str, callable or None): The goto directive in the option.
|
||||||
|
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.
|
||||||
|
|
||||||
|
"""
|
||||||
|
goto_kwargs, exec_kwargs = {}, {}
|
||||||
|
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
||||||
|
if goto and isinstance(goto, (tuple, list)):
|
||||||
|
if len(goto) > 1:
|
||||||
|
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(goto_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
|
else:
|
||||||
|
goto = goto[0]
|
||||||
|
if execute and isinstance(execute, (tuple, list)):
|
||||||
|
if len(execute) > 1:
|
||||||
|
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
||||||
|
if not hasattr(exec_kwargs, "__getitem__"):
|
||||||
|
# not a dict-like structure
|
||||||
|
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
||||||
|
nodename, goto_kwargs))
|
||||||
|
else:
|
||||||
|
execute = execute[0]
|
||||||
|
return goto, goto_kwargs, execute, exec_kwargs
|
||||||
|
|
||||||
def goto(self, nodename, raw_string, **kwargs):
|
def goto(self, nodename, raw_string, **kwargs):
|
||||||
"""
|
"""
|
||||||
Run a node by name, optionally dynamically generating that name first.
|
Run a node by name, optionally dynamically generating that name first.
|
||||||
|
|
@ -681,29 +738,6 @@ class EvMenu(object):
|
||||||
argument)
|
argument)
|
||||||
|
|
||||||
"""
|
"""
|
||||||
def _extract_goto_exec(option_dict):
|
|
||||||
"Helper: Get callables and their eventual kwargs"
|
|
||||||
goto_kwargs, exec_kwargs = {}, {}
|
|
||||||
goto, execute = option_dict.get("goto", None), option_dict.get("exec", None)
|
|
||||||
if goto and isinstance(goto, (tuple, list)):
|
|
||||||
if len(goto) > 1:
|
|
||||||
goto, goto_kwargs = goto[:2] # ignore any extra arguments
|
|
||||||
if not hasattr(goto_kwargs, "__getitem__"):
|
|
||||||
# not a dict-like structure
|
|
||||||
raise EvMenuError("EvMenu node {}: goto kwargs is not a dict: {}".format(
|
|
||||||
nodename, goto_kwargs))
|
|
||||||
else:
|
|
||||||
goto = goto[0]
|
|
||||||
if execute and isinstance(execute, (tuple, list)):
|
|
||||||
if len(execute) > 1:
|
|
||||||
execute, exec_kwargs = execute[:2] # ignore any extra arguments
|
|
||||||
if not hasattr(exec_kwargs, "__getitem__"):
|
|
||||||
# not a dict-like structure
|
|
||||||
raise EvMenuError("EvMenu node {}: exec kwargs is not a dict: {}".format(
|
|
||||||
nodename, goto_kwargs))
|
|
||||||
else:
|
|
||||||
execute = execute[0]
|
|
||||||
return goto, goto_kwargs, execute, exec_kwargs
|
|
||||||
|
|
||||||
if callable(nodename):
|
if callable(nodename):
|
||||||
# run the "goto" callable, if possible
|
# run the "goto" callable, if possible
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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]
|
||||||
page = _DISPLAY.format(text=text,
|
if show_footer:
|
||||||
pageno=pos + 1,
|
page = _DISPLAY.format(text=text,
|
||||||
pagemax=self._npages)
|
pageno=pos + 1,
|
||||||
|
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
|
||||||
self.display()
|
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
|
||||||
if self.exit_on_lastpage and self._pos >= self._npages - 1:
|
self.display(show_footer=False)
|
||||||
self.page_quit()
|
self.page_quit(quiet=True)
|
||||||
|
else:
|
||||||
|
self.display()
|
||||||
|
|
||||||
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
|
||||||
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
if not quiet:
|
||||||
|
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
||||||
self._caller.cmdset.remove(CmdSetMore)
|
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)
|
||||||
|
|
|
||||||
|
|
@ -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,25 +1286,59 @@ 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))
|
||||||
cwidths_min[ci] += 1
|
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
|
||||||
|
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))
|
||||||
cwidths_min[ci] += 1
|
if ci in locked_cols:
|
||||||
cwidths[ci] -= 3
|
# locked column, make sure it's not picked again
|
||||||
|
cwidths[ci] -= 9999
|
||||||
|
cwidths_min[ci] = locked_cols[ci]
|
||||||
|
else:
|
||||||
|
cwidths_min[ci] += 1
|
||||||
|
correction += 1
|
||||||
|
# give a just changed col less prio next run
|
||||||
|
cwidths[ci] -= 3
|
||||||
cwidths = cwidths_min
|
cwidths = cwidths_min
|
||||||
|
|
||||||
# reformat worktable (for width align)
|
# reformat worktable (for width align)
|
||||||
|
|
@ -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))
|
||||||
cheights_min[ci] += 1
|
if ci in locked_cols:
|
||||||
if ci == 0 and self.header:
|
# locked row, make sure it's not picked again
|
||||||
# it doesn't look very good if header expands too fast
|
cheights[ci] -= 9999
|
||||||
cheights[ci] -= 2 if even else 3
|
cheights_min[ci] = locked_cols[ci]
|
||||||
cheights[ci] -= 2 if even else 1
|
else:
|
||||||
|
cheights_min[ci] += 1
|
||||||
|
# change balance
|
||||||
|
if ci == 0 and self.header:
|
||||||
|
# it doesn't look very good if header expands too fast
|
||||||
|
cheights[ci] -= 2 if even else 3
|
||||||
|
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):
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,342 +0,0 @@
|
||||||
"""
|
|
||||||
Spawner
|
|
||||||
|
|
||||||
The spawner takes input files containing object definitions in
|
|
||||||
dictionary forms. These use a prototype architecture to define
|
|
||||||
unique objects without having to make a Typeclass for each.
|
|
||||||
|
|
||||||
The main function is `spawn(*prototype)`, where the `prototype`
|
|
||||||
is a dictionary like this:
|
|
||||||
|
|
||||||
```python
|
|
||||||
GOBLIN = {
|
|
||||||
"typeclass": "types.objects.Monster",
|
|
||||||
"key": "goblin grunt",
|
|
||||||
"health": lambda: randint(20,30),
|
|
||||||
"resists": ["cold", "poison"],
|
|
||||||
"attacks": ["fists"],
|
|
||||||
"weaknesses": ["fire", "light"]
|
|
||||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
|
||||||
"args": [("weapon", "sword")]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Possible keywords are:
|
|
||||||
prototype - string parent prototype
|
|
||||||
key - string, the main object identifier
|
|
||||||
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`
|
|
||||||
location - this should be a valid object or #dbref
|
|
||||||
home - valid object or #dbref
|
|
||||||
destination - only valid for exits (object or dbref)
|
|
||||||
|
|
||||||
permissions - string or list of permission strings
|
|
||||||
locks - a lock-string
|
|
||||||
aliases - string or list of strings
|
|
||||||
exec - this is a string of python code to execute or a list of such codes.
|
|
||||||
This can be used e.g. to trigger custom handlers on the object. The
|
|
||||||
execution namespace contains 'evennia' for the library and 'obj'
|
|
||||||
tags - string or list of strings or tuples `(tagstr, category)`. Plain
|
|
||||||
strings will be result in tags with no category (default tags).
|
|
||||||
attrs - tuple or list of tuples of Attributes to add. This form allows
|
|
||||||
more complex Attributes to be set. Tuples at least specify `(key, value)`
|
|
||||||
but can also specify up to `(key, value, category, lockstring)`. If
|
|
||||||
you want to specify a lockstring but not a category, set the category
|
|
||||||
to `None`.
|
|
||||||
ndb_<name> - value of a nattribute (ndb_ is stripped)
|
|
||||||
other - any other name is interpreted as the key of an Attribute with
|
|
||||||
its value. Such Attributes have no categories.
|
|
||||||
|
|
||||||
Each value can also be a callable that takes no arguments. It should
|
|
||||||
return the value to enter into the field and will be called every time
|
|
||||||
the prototype is used to spawn an object. Note, if you want to store
|
|
||||||
a callable in an Attribute, embed it in a tuple to the `args` keyword.
|
|
||||||
|
|
||||||
By specifying the "prototype" key, the prototype becomes a child of
|
|
||||||
that prototype, inheritng all prototype slots it does not explicitly
|
|
||||||
define itself, while overloading those that it does specify.
|
|
||||||
|
|
||||||
```python
|
|
||||||
GOBLIN_WIZARD = {
|
|
||||||
"prototype": GOBLIN,
|
|
||||||
"key": "goblin wizard",
|
|
||||||
"spells": ["fire ball", "lighting bolt"]
|
|
||||||
}
|
|
||||||
|
|
||||||
GOBLIN_ARCHER = {
|
|
||||||
"prototype": GOBLIN,
|
|
||||||
"key": "goblin archer",
|
|
||||||
"attacks": ["short bow"]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
One can also have multiple prototypes. These are inherited from the
|
|
||||||
left, with the ones further to the right taking precedence.
|
|
||||||
|
|
||||||
```python
|
|
||||||
ARCHWIZARD = {
|
|
||||||
"attack": ["archwizard staff", "eye of doom"]
|
|
||||||
|
|
||||||
GOBLIN_ARCHWIZARD = {
|
|
||||||
"key" : "goblin archwizard"
|
|
||||||
"prototype": (GOBLIN_WIZARD, ARCHWIZARD),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The *goblin archwizard* will have some different attacks, but will
|
|
||||||
otherwise have the same spells as a *goblin wizard* who in turn shares
|
|
||||||
many traits with a normal *goblin*.
|
|
||||||
|
|
||||||
"""
|
|
||||||
from __future__ import print_function
|
|
||||||
|
|
||||||
import copy
|
|
||||||
from django.conf import settings
|
|
||||||
from random import randint
|
|
||||||
import evennia
|
|
||||||
from evennia.objects.models import ObjectDB
|
|
||||||
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
|
|
||||||
|
|
||||||
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
|
||||||
|
|
||||||
|
|
||||||
def _handle_dbref(inp):
|
|
||||||
return dbid_to_obj(inp, ObjectDB)
|
|
||||||
|
|
||||||
|
|
||||||
def _validate_prototype(key, prototype, protparents, visited):
|
|
||||||
"""
|
|
||||||
Run validation on a prototype, checking for inifinite regress.
|
|
||||||
|
|
||||||
"""
|
|
||||||
assert isinstance(prototype, dict)
|
|
||||||
if id(prototype) in visited:
|
|
||||||
raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype)
|
|
||||||
visited.append(id(prototype))
|
|
||||||
protstrings = prototype.get("prototype")
|
|
||||||
if protstrings:
|
|
||||||
for protstring in make_iter(protstrings):
|
|
||||||
if key is not None and protstring == key:
|
|
||||||
raise RuntimeError("%s tries to prototype itself." % key or prototype)
|
|
||||||
protparent = protparents.get(protstring)
|
|
||||||
if not protparent:
|
|
||||||
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
|
|
||||||
_validate_prototype(protstring, protparent, protparents, visited)
|
|
||||||
|
|
||||||
|
|
||||||
def _get_prototype(dic, prot, protparents):
|
|
||||||
"""
|
|
||||||
Recursively traverse a prototype dictionary, including multiple
|
|
||||||
inheritance. Use _validate_prototype before this, we don't check
|
|
||||||
for infinite recursion here.
|
|
||||||
|
|
||||||
"""
|
|
||||||
if "prototype" in dic:
|
|
||||||
# move backwards through the inheritance
|
|
||||||
for prototype in make_iter(dic["prototype"]):
|
|
||||||
# Build the prot dictionary in reverse order, overloading
|
|
||||||
new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents)
|
|
||||||
prot.update(new_prot)
|
|
||||||
prot.update(dic)
|
|
||||||
prot.pop("prototype", None) # we don't need this anymore
|
|
||||||
return prot
|
|
||||||
|
|
||||||
|
|
||||||
def _batch_create_object(*objparams):
|
|
||||||
"""
|
|
||||||
This is a cut-down version of the create_object() function,
|
|
||||||
optimized for speed. It does NOT check and convert various input
|
|
||||||
so make sure the spawned Typeclass works before using this!
|
|
||||||
|
|
||||||
Args:
|
|
||||||
objsparams (tuple): Parameters for the respective creation/add
|
|
||||||
handlers in the following order:
|
|
||||||
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
|
|
||||||
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
|
|
||||||
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
|
|
||||||
- `aliases` (list): A list of alias strings for
|
|
||||||
adding with `new_object.aliases.batch_add(*aliases)`.
|
|
||||||
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
|
|
||||||
add with `new_obj.nattributes.add(*tuple)`.
|
|
||||||
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
|
|
||||||
adding with `new_obj.attributes.batch_add(*attributes)`.
|
|
||||||
- `tags` (list): list of tuples `(key, category)` for adding
|
|
||||||
with `new_obj.tags.batch_add(*tags)`.
|
|
||||||
- `execs` (list): Code strings to execute together with the creation
|
|
||||||
of each object. They will be executed with `evennia` and `obj`
|
|
||||||
(the newly created object) available in the namespace. Execution
|
|
||||||
will happend after all other properties have been assigned and
|
|
||||||
is intended for calling custom handlers etc.
|
|
||||||
for the respective creation/add handlers in the following
|
|
||||||
order: (create_kwargs, permissions, locks, aliases, nattributes,
|
|
||||||
attributes, tags, execs)
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
objects (list): A list of created objects
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
The `exec` list will execute arbitrary python code so don't allow this to be available to
|
|
||||||
unprivileged users!
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# bulk create all objects in one go
|
|
||||||
|
|
||||||
# unfortunately this doesn't work since bulk_create doesn't creates pks;
|
|
||||||
# the result would be duplicate objects at the next stage, so we comment
|
|
||||||
# it out for now:
|
|
||||||
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
|
|
||||||
|
|
||||||
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
|
|
||||||
objs = []
|
|
||||||
for iobj, obj in enumerate(dbobjs):
|
|
||||||
# call all setup hooks on each object
|
|
||||||
objparam = objparams[iobj]
|
|
||||||
# setup
|
|
||||||
obj._createdict = {"permissions": make_iter(objparam[1]),
|
|
||||||
"locks": objparam[2],
|
|
||||||
"aliases": make_iter(objparam[3]),
|
|
||||||
"nattributes": objparam[4],
|
|
||||||
"attributes": objparam[5],
|
|
||||||
"tags": make_iter(objparam[6])}
|
|
||||||
# this triggers all hooks
|
|
||||||
obj.save()
|
|
||||||
# run eventual extra code
|
|
||||||
for code in objparam[7]:
|
|
||||||
if code:
|
|
||||||
exec(code, {}, {"evennia": evennia, "obj": obj})
|
|
||||||
objs.append(obj)
|
|
||||||
return objs
|
|
||||||
|
|
||||||
|
|
||||||
def spawn(*prototypes, **kwargs):
|
|
||||||
"""
|
|
||||||
Spawn a number of prototyped objects.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
prototypes (dict): Each argument should be a prototype
|
|
||||||
dictionary.
|
|
||||||
Kwargs:
|
|
||||||
prototype_modules (str or list): A python-path to a prototype
|
|
||||||
module, or a list of such paths. These will be used to build
|
|
||||||
the global protparents dictionary accessible by the input
|
|
||||||
prototypes. If not given, it will instead look for modules
|
|
||||||
defined by settings.PROTOTYPE_MODULES.
|
|
||||||
prototype_parents (dict): A dictionary holding a custom
|
|
||||||
prototype-parent dictionary. Will overload same-named
|
|
||||||
prototypes from prototype_modules.
|
|
||||||
return_prototypes (bool): Only return a list of the
|
|
||||||
prototype-parents (no object creation happens)
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
protparents = {}
|
|
||||||
protmodules = make_iter(kwargs.get("prototype_modules", []))
|
|
||||||
if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"):
|
|
||||||
protmodules = make_iter(settings.PROTOTYPE_MODULES)
|
|
||||||
for prototype_module in protmodules:
|
|
||||||
protparents.update(dict((key, val) for key, val in
|
|
||||||
all_from_module(prototype_module).items() if isinstance(val, dict)))
|
|
||||||
# overload module's protparents with specifically given protparents
|
|
||||||
protparents.update(kwargs.get("prototype_parents", {}))
|
|
||||||
for key, prototype in protparents.items():
|
|
||||||
_validate_prototype(key, prototype, protparents, [])
|
|
||||||
|
|
||||||
if "return_prototypes" in kwargs:
|
|
||||||
# only return the parents
|
|
||||||
return copy.deepcopy(protparents)
|
|
||||||
|
|
||||||
objsparams = []
|
|
||||||
for prototype in prototypes:
|
|
||||||
|
|
||||||
_validate_prototype(None, prototype, protparents, [])
|
|
||||||
prot = _get_prototype(prototype, {}, protparents)
|
|
||||||
if not prot:
|
|
||||||
continue
|
|
||||||
|
|
||||||
# extract the keyword args we need to create the object itself. If we get a callable,
|
|
||||||
# call that to get the value (don't catch errors)
|
|
||||||
create_kwargs = {}
|
|
||||||
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
|
|
||||||
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
|
|
||||||
|
|
||||||
locval = prot.pop("location", None)
|
|
||||||
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
|
|
||||||
|
|
||||||
homval = prot.pop("home", settings.DEFAULT_HOME)
|
|
||||||
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
|
|
||||||
|
|
||||||
destval = prot.pop("destination", None)
|
|
||||||
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
|
|
||||||
|
|
||||||
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
|
||||||
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
|
|
||||||
|
|
||||||
# extract calls to handlers
|
|
||||||
permval = prot.pop("permissions", [])
|
|
||||||
permission_string = permval() if callable(permval) else permval
|
|
||||||
lockval = prot.pop("locks", "")
|
|
||||||
lock_string = lockval() if callable(lockval) else lockval
|
|
||||||
aliasval = prot.pop("aliases", "")
|
|
||||||
alias_string = aliasval() if callable(aliasval) else aliasval
|
|
||||||
tagval = prot.pop("tags", [])
|
|
||||||
tags = tagval() if callable(tagval) else tagval
|
|
||||||
attrval = prot.pop("attrs", [])
|
|
||||||
attributes = attrval() if callable(tagval) else attrval
|
|
||||||
|
|
||||||
exval = prot.pop("exec", "")
|
|
||||||
execs = make_iter(exval() if callable(exval) else exval)
|
|
||||||
|
|
||||||
# extract ndb assignments
|
|
||||||
nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value)
|
|
||||||
for key, value in prot.items() if key.startswith("ndb_"))
|
|
||||||
|
|
||||||
# the rest are attributes
|
|
||||||
simple_attributes = [(key, value()) if callable(value) else (key, value)
|
|
||||||
for key, value in prot.items() if not key.startswith("ndb_")]
|
|
||||||
attributes = attributes + simple_attributes
|
|
||||||
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS]
|
|
||||||
|
|
||||||
# pack for call into _batch_create_object
|
|
||||||
objsparams.append((create_kwargs, permission_string, lock_string,
|
|
||||||
alias_string, nattributes, attributes, tags, execs))
|
|
||||||
|
|
||||||
return _batch_create_object(*objsparams)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
# testing
|
|
||||||
|
|
||||||
protparents = {
|
|
||||||
"NOBODY": {},
|
|
||||||
# "INFINITE" : {
|
|
||||||
# "prototype":"INFINITE"
|
|
||||||
# },
|
|
||||||
"GOBLIN": {
|
|
||||||
"key": "goblin grunt",
|
|
||||||
"health": lambda: randint(20, 30),
|
|
||||||
"resists": ["cold", "poison"],
|
|
||||||
"attacks": ["fists"],
|
|
||||||
"weaknesses": ["fire", "light"]
|
|
||||||
},
|
|
||||||
"GOBLIN_WIZARD": {
|
|
||||||
"prototype": "GOBLIN",
|
|
||||||
"key": "goblin wizard",
|
|
||||||
"spells": ["fire ball", "lighting bolt"]
|
|
||||||
},
|
|
||||||
"GOBLIN_ARCHER": {
|
|
||||||
"prototype": "GOBLIN",
|
|
||||||
"key": "goblin archer",
|
|
||||||
"attacks": ["short bow"]
|
|
||||||
},
|
|
||||||
"ARCHWIZARD": {
|
|
||||||
"attacks": ["archwizard staff"],
|
|
||||||
},
|
|
||||||
"GOBLIN_ARCHWIZARD": {
|
|
||||||
"key": "goblin archwizard",
|
|
||||||
"prototype": ("GOBLIN_WIZARD", "ARCHWIZARD")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
# test
|
|
||||||
print([o.key for o in spawn(protparents["GOBLIN"],
|
|
||||||
protparents["GOBLIN_ARCHWIZARD"],
|
|
||||||
prototype_parents=protparents)])
|
|
||||||
|
|
@ -58,7 +58,7 @@ class TestEvMenu(TestCase):
|
||||||
|
|
||||||
def _debug_output(self, indent, msg):
|
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,
|
||||||
|
|
|
||||||
|
|
@ -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 ""
|
||||||
return textwrap.dedent(text)
|
if baseline_index is None:
|
||||||
|
return textwrap.dedent(text)
|
||||||
|
else:
|
||||||
|
lines = text.split('\n')
|
||||||
|
baseline = lines[baseline_index]
|
||||||
|
spaceremove = len(baseline) - len(baseline.lstrip(' '))
|
||||||
|
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
|
||||||
|
for line in lines)
|
||||||
|
|
||||||
|
|
||||||
def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
def justify(text, width=None, align="f", indent=0):
|
||||||
"""
|
"""
|
||||||
Fully justify a text so that it fits inside `width`. When using
|
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':
|
||||||
line[-1] += " " * line_rest
|
if line[-1] == "\n\n":
|
||||||
|
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
|
||||||
|
else:
|
||||||
|
line[-1] += " " * line_rest
|
||||||
elif align == 'r':
|
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]
|
||||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
if line[-1] == "\n\n":
|
||||||
|
line[-1] += pad + " " * (line_rest % 2 - 1) + \
|
||||||
|
"\n" + " " * width + "\n" + " " * width
|
||||||
|
else:
|
||||||
|
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||||
else: # align 'f'
|
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
|
||||||
|
|
@ -931,17 +1010,17 @@ def delay(timedelay, callback, *args, **kwargs):
|
||||||
Delay the return of a value.
|
Delay the return of a value.
|
||||||
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -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 */
|
||||||
|
|
||||||
|
|
|
||||||
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal file
145
evennia/web/webclient/static/webclient/js/splithandler.js
Normal 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,
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
|
@ -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;
|
return function() {
|
||||||
var max_height = 300;
|
var wrapper = $("#inputform")
|
||||||
var prev_text_len = 0;
|
var input = $("#inputcontrol")
|
||||||
|
var prompt = $("#prompt")
|
||||||
|
|
||||||
// Check to see if we should change the height of the input area
|
input.height(wrapper.height() - (input.offset().top - wrapper.offset().top));
|
||||||
return function () {
|
|
||||||
var inputfield = $("#inputfield");
|
|
||||||
var scrollh = inputfield.prop("scrollHeight");
|
|
||||||
var clienth = inputfield.prop("clientHeight");
|
|
||||||
var newh = 0;
|
|
||||||
var curr_text_len = inputfield.val().length;
|
|
||||||
|
|
||||||
if (scrollh > clienth && scrollh <= max_height) {
|
|
||||||
// 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");
|
||||||
|
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -8,20 +8,29 @@
|
||||||
|
|
||||||
|
|
||||||
{% block client %}
|
{% block client %}
|
||||||
|
<div id="toolbar">
|
||||||
|
<button id="optionsbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Settings</span></button>
|
||||||
|
<button id="splitbutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⇹<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
<button id="panebutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">⚙<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
<button id="undobutton" type="button" aria-haspopup="true" aria-owns="#optionsdialog">↶<span class="sr-only sr-only-focusable">Splits</span></button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<div id="wrapper">
|
<!-- The "Main" Content -->
|
||||||
<div id="toolbar">
|
<div id="main" class="split split-vertical" data-role-default>
|
||||||
<button id="optionsbutton" type="button" class="hidden">⚙</button>
|
<div id="main-sub" class="split-sub">
|
||||||
</div>
|
<div id="messagewindow"></div>
|
||||||
<div id="messagewindow" role="log"></div>
|
</div>
|
||||||
<div id="inputform">
|
</div>
|
||||||
<div id="prompt"></div>
|
<!-- The "Input" Pane -->
|
||||||
<div id="inputcontrol">
|
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
|
||||||
<textarea id="inputfield" type="text"></textarea>
|
|
||||||
<input id="inputsend" type="button" value=">"/>
|
<!-- Basic UI Components -->
|
||||||
|
<div id="splitdialog" class="dialog">
|
||||||
|
<div class="dialogtitle">Split Pane<span class="dialogclose">×</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="">></button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
{% block scripts %}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue