mirror of
https://github.com/evennia/evennia.git
synced 2026-04-03 14:37:17 +02:00
Merge branch 'develop' into building_menu
This commit is contained in:
commit
3011c1c840
60 changed files with 10614 additions and 1566 deletions
102
CHANGELOG.md
102
CHANGELOG.md
|
|
@ -1,7 +1,91 @@
|
|||
# Evennia Changelog
|
||||
# Changelog
|
||||
|
||||
# Sept 2017:
|
||||
Release of Evennia 0.7; upgrade to Django 1.11, change 'Player' to
|
||||
## 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
|
||||
'Account', rework the website template and a slew of other updates.
|
||||
Info on what changed and how to migrate is found here:
|
||||
https://groups.google.com/forum/#!msg/evennia/0JYYNGY-NfE/cDFaIwmPBAAJ
|
||||
|
|
@ -14,9 +98,9 @@ Lots of bugfixes and considerable uptick in contributors. Unittest coverage
|
|||
and PEP8 adoption and refactoring.
|
||||
|
||||
## May 2016:
|
||||
Evennia 0.6 with completely reworked Out-of-band system, making
|
||||
Evennia 0.6 with completely reworked Out-of-band system, making
|
||||
the message path completely flexible and built around input/outputfuncs.
|
||||
A completely new webclient, split into the evennia.js library and a
|
||||
A completely new webclient, split into the evennia.js library and a
|
||||
gui library, making it easier to customize.
|
||||
|
||||
## Feb 2016:
|
||||
|
|
@ -33,15 +117,15 @@ library format with a stand-alone launcher, in preparation for making
|
|||
an 'evennia' pypy package and using versioning. The version we will
|
||||
merge with will likely be 0.5. There is also work with an expanded
|
||||
testing structure and the use of threading for saves. We also now
|
||||
use Travis for automatic build checking.
|
||||
use Travis for automatic build checking.
|
||||
|
||||
## Sept 2014:
|
||||
Updated to Django 1.7+ which means South dependency was dropped and
|
||||
minimum Python version upped to 2.7. MULTISESSION_MODE=3 was added
|
||||
and the web customization system was overhauled using the latest
|
||||
functionality of django. Otherwise, mostly bug-fixes and
|
||||
and the web customization system was overhauled using the latest
|
||||
functionality of django. Otherwise, mostly bug-fixes and
|
||||
implementation of various smaller feature requests as we got used
|
||||
to github. Many new users have appeared.
|
||||
to github. Many new users have appeared.
|
||||
|
||||
## Jan 2014:
|
||||
Moved Evennia project from Google Code to github.com/evennia/evennia.
|
||||
|
|
|
|||
|
|
@ -97,15 +97,15 @@ def funcname(a, b, c, d=False, **kwargs):
|
|||
Args:
|
||||
a (str): This is a string argument that we can talk about
|
||||
over multiple lines.
|
||||
b (int or str): Another argument
|
||||
c (list): A list argument
|
||||
d (bool, optional): An optional keyword argument
|
||||
b (int or str): Another argument.
|
||||
c (list): A list argument.
|
||||
d (bool, optional): An optional keyword argument.
|
||||
|
||||
Kwargs:
|
||||
test (list): A test keyword
|
||||
test (list): A test keyword.
|
||||
|
||||
Returns:
|
||||
e (str): The result of the function
|
||||
e (str): The result of the function.
|
||||
|
||||
Raises:
|
||||
RuntimeException: If there is a critical error,
|
||||
|
|
|
|||
22
Dockerfile
22
Dockerfile
|
|
@ -21,20 +21,30 @@
|
|||
#
|
||||
FROM alpine
|
||||
|
||||
MAINTAINER www.evennia.com
|
||||
LABEL maintainer="www.evennia.com"
|
||||
|
||||
# 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 . /usr/src/evennia
|
||||
# add the files required for pip installation
|
||||
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
|
||||
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
|
||||
# a game dir
|
||||
ONBUILD ADD . /usr/src/game
|
||||
ONBUILD COPY . /usr/src/game
|
||||
|
||||
# make the game source hierarchy persistent with a named volume.
|
||||
# mount on-disk game location here when using the container
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ def _init():
|
|||
from .utils import logger
|
||||
from .utils import gametime
|
||||
from .utils import ansi
|
||||
from .utils.spawner import spawn
|
||||
from .prototypes.spawner import spawn
|
||||
from . import contrib
|
||||
from .utils.evmenu import EvMenu
|
||||
from .utils.evtable import EvTable
|
||||
|
|
|
|||
|
|
@ -421,17 +421,19 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
|
||||
kwargs["options"] = options
|
||||
|
||||
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||
# sanitize text before sending across the wire
|
||||
try:
|
||||
text = to_str(text, force_string=True)
|
||||
except Exception:
|
||||
text = repr(text)
|
||||
if text is not None:
|
||||
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||
# sanitize text before sending across the wire
|
||||
try:
|
||||
text = to_str(text, force_string=True)
|
||||
except Exception:
|
||||
text = repr(text)
|
||||
kwargs['text'] = text
|
||||
|
||||
# session relay
|
||||
sessions = make_iter(session) if session else self.sessions.all()
|
||||
for session in sessions:
|
||||
session.data_out(text=text, **kwargs)
|
||||
session.data_out(**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
|
||||
# function was used to create the object.
|
||||
cdict = self._createdict
|
||||
updates = []
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#%i" % self.dbid
|
||||
updates.append("db_key")
|
||||
elif self.key != cdict.get("key"):
|
||||
updates.append("db_key")
|
||||
self.db_key = cdict["key"]
|
||||
if updates:
|
||||
self.save(update_fields=updates)
|
||||
|
||||
if cdict.get("locks"):
|
||||
self.locks.add(cdict["locks"])
|
||||
if cdict.get("permissions"):
|
||||
permissions = cdict["permissions"]
|
||||
if cdict.get("tags"):
|
||||
# this should be a list of tags, tuples (key, category) or (key, category, data)
|
||||
self.tags.batch_add(*cdict["tags"])
|
||||
if cdict.get("attributes"):
|
||||
# this should be tuples (key, val, ...)
|
||||
self.attributes.batch_add(*cdict["attributes"])
|
||||
if cdict.get("nattributes"):
|
||||
# this should be a dict of nattrname:value
|
||||
for key, value in cdict["nattributes"]:
|
||||
self.nattributes.add(key, value)
|
||||
del self._createdict
|
||||
|
||||
self.permissions.batch_add(*permissions)
|
||||
|
|
@ -775,7 +798,7 @@ class DefaultAccount(with_metaclass(TypeclassBase, AccountDB)):
|
|||
# any was deleted in the interim.
|
||||
self.db._playable_characters = [char for char in self.db._playable_characters if char]
|
||||
self.msg(self.at_look(target=self.db._playable_characters,
|
||||
session=session))
|
||||
session=session), session=session)
|
||||
|
||||
def at_failed_login(self, session, **kwargs):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -35,7 +35,6 @@ class AccountDBManager(TypedObjectManager, UserManager):
|
|||
get_account_from_uid
|
||||
get_account_from_name
|
||||
account_search (equivalent to evennia.search_account)
|
||||
#swap_character
|
||||
|
||||
"""
|
||||
|
||||
|
|
|
|||
|
|
@ -296,9 +296,9 @@ class CmdSet(with_metaclass(_CmdSetMeta, object)):
|
|||
result (any): An instantiated Command or the input unmodified.
|
||||
|
||||
"""
|
||||
try:
|
||||
if callable(cmd):
|
||||
return cmd()
|
||||
except TypeError:
|
||||
else:
|
||||
return cmd
|
||||
|
||||
def _duplicate(self):
|
||||
|
|
|
|||
|
|
@ -10,9 +10,10 @@ from evennia.objects.models import ObjectDB
|
|||
from evennia.locks.lockhandler import LockException
|
||||
from evennia.commands.cmdhandler import get_and_merge_cmdsets
|
||||
from evennia.utils import create, utils, search
|
||||
from evennia.utils.utils import inherits_from, class_from_module
|
||||
from evennia.utils.utils import inherits_from, class_from_module, get_all_typeclasses
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.spawner import spawn
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
|
||||
from evennia.utils.ansi import raw
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
|
@ -26,12 +27,8 @@ __all__ = ("ObjManipCommand", "CmdSetObjAlias", "CmdCopy",
|
|||
"CmdLock", "CmdExamine", "CmdFind", "CmdTeleport",
|
||||
"CmdScript", "CmdTag", "CmdSpawn")
|
||||
|
||||
try:
|
||||
# used by @set
|
||||
from ast import literal_eval as _LITERAL_EVAL
|
||||
except ImportError:
|
||||
# literal_eval is not available before Python 2.6
|
||||
_LITERAL_EVAL = None
|
||||
# used by @set
|
||||
from ast import literal_eval as _LITERAL_EVAL
|
||||
|
||||
# used by @find
|
||||
CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
|
@ -1458,17 +1455,16 @@ def _convert_from_string(cmd, strobj):
|
|||
# if nothing matches, return as-is
|
||||
return obj
|
||||
|
||||
if _LITERAL_EVAL:
|
||||
# Use literal_eval to parse python structure exactly.
|
||||
try:
|
||||
return _LITERAL_EVAL(strobj)
|
||||
except (SyntaxError, ValueError):
|
||||
# treat as string
|
||||
strobj = utils.to_str(strobj)
|
||||
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
||||
"Make sure this is acceptable." % strobj
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
# Use literal_eval to parse python structure exactly.
|
||||
try:
|
||||
return _LITERAL_EVAL(strobj)
|
||||
except (SyntaxError, ValueError):
|
||||
# treat as string
|
||||
strobj = utils.to_str(strobj)
|
||||
string = "|RNote: name \"|r%s|R\" was converted to a string. " \
|
||||
"Make sure this is acceptable." % strobj
|
||||
cmd.caller.msg(string)
|
||||
return strobj
|
||||
else:
|
||||
# fall back to old recursive solution (does not support
|
||||
# nested lists/dicts)
|
||||
|
|
@ -1702,17 +1698,22 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
@typeclass[/switch] <object> [= typeclass.path]
|
||||
@type ''
|
||||
@parent ''
|
||||
@typeclass/list/show [typeclass.path]
|
||||
@swap - this is a shorthand for using /force/reset flags.
|
||||
@update - this is a shorthand for using the /force/reload flag.
|
||||
|
||||
Switch:
|
||||
show - display the current typeclass of object (default)
|
||||
show, examine - display the current typeclass of object (default) or, if
|
||||
given a typeclass path, show the docstring of that typeclass.
|
||||
update - *only* re-run at_object_creation on this object
|
||||
meaning locks or other properties set later may remain.
|
||||
reset - clean out *all* the attributes and properties on the
|
||||
object - basically making this a new clean object.
|
||||
force - change to the typeclass also if the object
|
||||
already has a typeclass of the same name.
|
||||
list - show available typeclasses.
|
||||
|
||||
|
||||
Example:
|
||||
@type button = examples.red_button.RedButton
|
||||
|
||||
|
|
@ -1736,6 +1737,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@typeclass"
|
||||
aliases = ["@type", "@parent", "@swap", "@update"]
|
||||
switch_options = ("show", "examine", "update", "reset", "force", "list")
|
||||
locks = "cmd:perm(typeclass) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -1744,10 +1746,56 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
caller = self.caller
|
||||
|
||||
if 'list' in self.switches:
|
||||
tclasses = get_all_typeclasses()
|
||||
contribs = [key for key in sorted(tclasses)
|
||||
if key.startswith("evennia.contrib")] or ["<None loaded>"]
|
||||
core = [key for key in sorted(tclasses)
|
||||
if key.startswith("evennia") and key not in contribs] or ["<None loaded>"]
|
||||
game = [key for key in sorted(tclasses)
|
||||
if not key.startswith("evennia")] or ["<None loaded>"]
|
||||
string = ("|wCore typeclasses|n\n"
|
||||
" {core}\n"
|
||||
"|wLoaded Contrib typeclasses|n\n"
|
||||
" {contrib}\n"
|
||||
"|wGame-dir typeclasses|n\n"
|
||||
" {game}").format(core="\n ".join(core),
|
||||
contrib="\n ".join(contribs),
|
||||
game="\n ".join(game))
|
||||
EvMore(caller, string, exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
caller.msg("Usage: %s <object> [= typeclass]" % self.cmdstring)
|
||||
return
|
||||
|
||||
if "show" in self.switches or "examine" in self.switches:
|
||||
oquery = self.lhs
|
||||
obj = caller.search(oquery, quiet=True)
|
||||
if not obj:
|
||||
# no object found to examine, see if it's a typeclass-path instead
|
||||
tclasses = get_all_typeclasses()
|
||||
matches = [(key, tclass)
|
||||
for key, tclass in tclasses.items() if key.endswith(oquery)]
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1:
|
||||
caller.msg("Multiple typeclasses found matching {}:\n {}".format(
|
||||
oquery, "\n ".join(tup[0] for tup in matches)))
|
||||
elif not matches:
|
||||
caller.msg("No object or typeclass path found to match '{}'".format(oquery))
|
||||
else:
|
||||
# one match found
|
||||
caller.msg("Docstring for typeclass '{}':\n{}".format(
|
||||
oquery, matches[0][1].__doc__))
|
||||
else:
|
||||
# do the search again to get the error handling in case of multi-match
|
||||
obj = caller.search(oquery)
|
||||
if not obj:
|
||||
return
|
||||
caller.msg("{}'s current typeclass is '{}.{}'".format(
|
||||
obj.name, obj.__class__.__module__, obj.__class__.__name__))
|
||||
return
|
||||
|
||||
# get object to swap on
|
||||
obj = caller.search(self.lhs)
|
||||
if not obj:
|
||||
|
|
@ -1760,7 +1808,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
new_typeclass = self.rhs or obj.path
|
||||
|
||||
if "show" in self.switches:
|
||||
if "show" in self.switches or "examine" in self.switches:
|
||||
string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
|
||||
caller.msg(string)
|
||||
return
|
||||
|
|
@ -2179,12 +2227,15 @@ class CmdExamine(ObjManipCommand):
|
|||
else:
|
||||
things.append(content)
|
||||
if exits:
|
||||
string += "\n|wExits|n: %s" % ", ".join(["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
|
||||
string += "\n|wExits|n: %s" % ", ".join(
|
||||
["%s(%s)" % (exit.name, exit.dbref) for exit in exits])
|
||||
if pobjs:
|
||||
string += "\n|wCharacters|n: %s" % ", ".join(["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
|
||||
string += "\n|wCharacters|n: %s" % ", ".join(
|
||||
["|c%s|n(%s)" % (pobj.name, pobj.dbref) for pobj in pobjs])
|
||||
if things:
|
||||
string += "\n|wContents|n: %s" % ", ".join(["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
|
||||
if cont not in exits and cont not in pobjs])
|
||||
string += "\n|wContents|n: %s" % ", ".join(
|
||||
["%s(%s)" % (cont.name, cont.dbref) for cont in obj.contents
|
||||
if cont not in exits and cont not in pobjs])
|
||||
separator = "-" * _DEFAULT_WIDTH
|
||||
# output info
|
||||
return '%s\n%s\n%s' % (separator, string.strip(), separator)
|
||||
|
|
@ -2270,11 +2321,12 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
@locate - this is a shorthand for using the /loc switch.
|
||||
|
||||
Switches:
|
||||
room - only look for rooms (location=None)
|
||||
exit - only look for exits (destination!=None)
|
||||
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
||||
exact- only exact matches are returned.
|
||||
loc - display object location if exists and match has one result
|
||||
room - only look for rooms (location=None)
|
||||
exit - only look for exits (destination!=None)
|
||||
char - only look for characters (BASE_CHARACTER_TYPECLASS)
|
||||
exact - only exact matches are returned.
|
||||
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.
|
||||
Use *accountname to search for an account. The switches allows for
|
||||
|
|
@ -2285,7 +2337,7 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "@find"
|
||||
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)"
|
||||
help_category = "Building"
|
||||
|
||||
|
|
@ -2359,10 +2411,14 @@ class CmdFind(COMMAND_DEFAULT_CLASS):
|
|||
keyquery = Q(db_key__iexact=searchstring, id__gte=low, id__lte=high)
|
||||
aliasquery = Q(db_tags__db_key__iexact=searchstring,
|
||||
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)
|
||||
aliasquery = Q(db_tags__db_key__istartswith=searchstring,
|
||||
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()
|
||||
nresults = results.count()
|
||||
|
|
@ -2733,101 +2789,312 @@ class CmdTag(COMMAND_DEFAULT_CLASS):
|
|||
string = "No tags attached to %s." % obj
|
||||
self.caller.msg(string)
|
||||
|
||||
#
|
||||
# To use the prototypes with the @spawn function set
|
||||
# PROTOTYPE_MODULES = ["commands.prototypes"]
|
||||
# Reload the server and the prototypes should be available.
|
||||
#
|
||||
|
||||
|
||||
class CmdSpawn(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
spawn objects from prototype
|
||||
|
||||
Usage:
|
||||
@spawn
|
||||
@spawn[/switch] <prototype_name>
|
||||
@spawn[/switch] {prototype dictionary}
|
||||
@spawn[/noloc] <prototype_key>
|
||||
@spawn[/noloc] <prototype_dict>
|
||||
|
||||
Switch:
|
||||
@spawn/search [prototype_keykey][;tag[,tag]]
|
||||
@spawn/list [tag, tag, ...]
|
||||
@spawn/show [<prototype_key>]
|
||||
@spawn/update <prototype_key>
|
||||
|
||||
@spawn/save <prototype_dict>
|
||||
@spawn/edit [<prototype_key>]
|
||||
@olc - equivalent to @spawn/edit
|
||||
|
||||
Switches:
|
||||
noloc - allow location to be None if not specified explicitly. Otherwise,
|
||||
location will default to caller's current location.
|
||||
search - search prototype by name or tags.
|
||||
list - list available prototypes, optionally limit by tags.
|
||||
show, examine - inspect prototype by key. If not given, acts like list.
|
||||
save - save a prototype to the database. It will be listable by /list.
|
||||
delete - remove a prototype from database, if allowed to.
|
||||
update - find existing objects with the same prototype_key and update
|
||||
them with latest version of given prototype. If given with /save,
|
||||
will auto-update all objects with the old version of the prototype
|
||||
without asking first.
|
||||
edit, olc - create/manipulate prototype in a menu interface.
|
||||
|
||||
Example:
|
||||
@spawn GOBLIN
|
||||
@spawn {"key":"goblin", "typeclass":"monster.Monster", "location":"#2"}
|
||||
@spawn/save {"key": "grunt", prototype: "goblin"};;mobs;edit:all()
|
||||
|
||||
Dictionary keys:
|
||||
|wprototype |n - name of parent prototype to use. Can be a list for
|
||||
multiple inheritance (inherits left to right)
|
||||
|wprototype_parent |n - name of parent prototype to use. Required if typeclass is
|
||||
not set. Can be a path or a list for multiple inheritance (inherits
|
||||
left to right). If set one of the parents must have a typeclass.
|
||||
|wtypeclass |n - string. Required if prototype_parent is not set.
|
||||
|wkey |n - string, the main object identifier
|
||||
|wtypeclass |n - string, if not set, will use settings.BASE_OBJECT_TYPECLASS
|
||||
|wlocation |n - this should be a valid object or #dbref
|
||||
|whome |n - valid object or #dbref
|
||||
|wdestination|n - only valid for exits (object or dbref)
|
||||
|wpermissions|n - string or list of permission strings
|
||||
|wlocks |n - a lock-string
|
||||
|waliases |n - string or list of strings
|
||||
|waliases |n - string or list of strings.
|
||||
|wndb_|n<name> - value of a nattribute (ndb_ is stripped)
|
||||
|
||||
|wprototype_key|n - name of this prototype. Unique. Used to store/retrieve from db
|
||||
and update existing prototyped objects if desired.
|
||||
|wprototype_desc|n - desc of this prototype. Used in listings
|
||||
|wprototype_locks|n - locks of this prototype. Limits who may use prototype
|
||||
|wprototype_tags|n - tags of this prototype. Used to find prototype
|
||||
|
||||
any other keywords are interpreted as Attributes and their values.
|
||||
|
||||
The available prototypes are defined globally in modules set in
|
||||
settings.PROTOTYPE_MODULES. If @spawn is used without arguments it
|
||||
displays a list of available prototypes.
|
||||
|
||||
"""
|
||||
|
||||
key = "@spawn"
|
||||
switch_options = ("noloc", )
|
||||
aliases = ["olc"]
|
||||
switch_options = ("noloc", "search", "list", "show", "save", "delete", "menu", "olc", "update")
|
||||
locks = "cmd:perm(spawn) or perm(Builder)"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"""Implements the spawner"""
|
||||
|
||||
def _show_prototypes(prototypes):
|
||||
"""Helper to show a list of available prototypes"""
|
||||
prots = ", ".join(sorted(prototypes.keys()))
|
||||
return "\nAvailable prototypes (case sensitive): %s" % (
|
||||
"\n" + utils.fill(prots) if prots else "None")
|
||||
def _parse_prototype(inp, expect=dict):
|
||||
err = None
|
||||
try:
|
||||
prototype = _LITERAL_EVAL(inp)
|
||||
except (SyntaxError, ValueError) as err:
|
||||
# treat as string
|
||||
prototype = utils.to_str(inp)
|
||||
finally:
|
||||
if not isinstance(prototype, expect):
|
||||
if err:
|
||||
string = ("{}\n|RCritical Python syntax error in argument. Only primitive "
|
||||
"Python structures are allowed. \nYou also need to use correct "
|
||||
"Python syntax. Remember especially to put quotes around all "
|
||||
"strings inside lists and dicts.|n For more advanced uses, embed "
|
||||
"inline functions in the strings.".format(err))
|
||||
else:
|
||||
string = "Expected {}, got {}.".format(expect, type(prototype))
|
||||
self.caller.msg(string)
|
||||
return None
|
||||
if expect == dict:
|
||||
# an actual prototype. We need to make sure it's safe. Don't allow exec
|
||||
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
||||
self.caller.msg("Spawn aborted: You are not allowed to "
|
||||
"use the 'exec' prototype key.")
|
||||
return None
|
||||
try:
|
||||
protlib.validate_prototype(prototype)
|
||||
except RuntimeError as err:
|
||||
self.caller.msg(str(err))
|
||||
return
|
||||
return prototype
|
||||
|
||||
prototypes = spawn(return_prototypes=True)
|
||||
if not self.args:
|
||||
string = "Usage: @spawn {key:value, key, value, ... }"
|
||||
self.caller.msg(string + _show_prototypes(prototypes))
|
||||
return
|
||||
try:
|
||||
# make use of _convert_from_string from the SetAttribute command
|
||||
prototype = _convert_from_string(self, self.args)
|
||||
except SyntaxError:
|
||||
# this means literal_eval tried to parse a faulty string
|
||||
string = "|RCritical Python syntax error in argument. "
|
||||
string += "Only primitive Python structures are allowed. "
|
||||
string += "\nYou also need to use correct Python syntax. "
|
||||
string += "Remember especially to put quotes around all "
|
||||
string += "strings inside lists and dicts.|n"
|
||||
self.caller.msg(string)
|
||||
def _search_show_prototype(query, prototypes=None):
|
||||
# prototype detail
|
||||
if not prototypes:
|
||||
prototypes = protlib.search_prototype(key=query)
|
||||
if prototypes:
|
||||
return "\n".join(protlib.prototype_to_str(prot) for prot in prototypes)
|
||||
else:
|
||||
return False
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if self.cmdstring == "olc" or 'menu' in self.switches or 'olc' in self.switches:
|
||||
# OLC menu mode
|
||||
prototype = None
|
||||
if self.lhs:
|
||||
key = self.lhs
|
||||
prototype = spawner.search_prototype(key=key, return_meta=True)
|
||||
if len(prototype) > 1:
|
||||
caller.msg("More than one match for {}:\n{}".format(
|
||||
key, "\n".join(proto.get('prototype_key', '') for proto in prototype)))
|
||||
return
|
||||
elif prototype:
|
||||
# one match
|
||||
prototype = prototype[0]
|
||||
olc_menus.start_olc(caller, session=self.session, prototype=prototype)
|
||||
return
|
||||
|
||||
if isinstance(prototype, basestring):
|
||||
# A prototype key
|
||||
keystr = prototype
|
||||
prototype = prototypes.get(prototype, None)
|
||||
if 'search' in self.switches:
|
||||
# query for a key match
|
||||
if not self.args:
|
||||
self.switches.append("list")
|
||||
else:
|
||||
key, tags = self.args.strip(), None
|
||||
if ';' in self.args:
|
||||
key, tags = (part.strip().lower() for part in self.args.split(";", 1))
|
||||
tags = [tag.strip() for tag in tags.split(",")] if tags else None
|
||||
EvMore(caller, unicode(protlib.list_prototypes(caller, key=key, tags=tags)),
|
||||
exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if 'show' in self.switches or 'examine' in self.switches:
|
||||
# the argument is a key in this case (may be a partial key)
|
||||
if not self.args:
|
||||
self.switches.append('list')
|
||||
else:
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
caller.msg(matchstring)
|
||||
else:
|
||||
caller.msg("No prototype '{}' was found.".format(self.args))
|
||||
return
|
||||
|
||||
if 'list' in self.switches:
|
||||
# for list, all optional arguments are tags
|
||||
# import pudb; pudb.set_trace()
|
||||
|
||||
EvMore(caller, unicode(protlib.list_prototypes(caller,
|
||||
tags=self.lhslist)), exit_on_lastpage=True)
|
||||
return
|
||||
|
||||
if 'save' in self.switches:
|
||||
# store a prototype to the database store
|
||||
if not self.args:
|
||||
caller.msg(
|
||||
"Usage: @spawn/save <key>[;desc[;tag,tag[,...][;lockstring]]] = <prototype_dict>")
|
||||
return
|
||||
|
||||
# handle rhs:
|
||||
prototype = _parse_prototype(self.lhs.strip())
|
||||
if not prototype:
|
||||
string = "No prototype named '%s'." % keystr
|
||||
self.caller.msg(string + _show_prototypes(prototypes))
|
||||
return
|
||||
elif isinstance(prototype, dict):
|
||||
# we got the prototype on the command line. We must make sure to not allow
|
||||
# the 'exec' key unless we are developers or higher.
|
||||
if "exec" in prototype and not self.caller.check_permstring("Developer"):
|
||||
self.caller.msg("Spawn aborted: You don't have access to use the 'exec' prototype key.")
|
||||
|
||||
# present prototype to save
|
||||
new_matchstring = _search_show_prototype("", prototypes=[prototype])
|
||||
string = "|yCreating new prototype:|n\n{}".format(new_matchstring)
|
||||
question = "\nDo you want to continue saving? [Y]/N"
|
||||
|
||||
prototype_key = prototype.get("prototype_key")
|
||||
if not prototype_key:
|
||||
caller.msg("\n|yTo save a prototype it must have the 'prototype_key' set.")
|
||||
return
|
||||
else:
|
||||
self.caller.msg("The prototype must be a prototype key or a Python dictionary.")
|
||||
|
||||
# check for existing prototype,
|
||||
old_matchstring = _search_show_prototype(prototype_key)
|
||||
|
||||
if old_matchstring:
|
||||
string += "\n|yExisting saved prototype found:|n\n{}".format(old_matchstring)
|
||||
question = "\n|yDo you want to replace the existing prototype?|n [Y]/N"
|
||||
|
||||
answer = yield(string + question)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rSave cancelled.|n")
|
||||
return
|
||||
|
||||
# all seems ok. Try to save.
|
||||
try:
|
||||
prot = protlib.save_prototype(**prototype)
|
||||
if not prot:
|
||||
caller.msg("|rError saving:|R {}.|n".format(prototype_key))
|
||||
return
|
||||
except protlib.PermissionError as err:
|
||||
caller.msg("|rError saving:|R {}|n".format(err))
|
||||
return
|
||||
caller.msg("|gSaved prototype:|n {}".format(prototype_key))
|
||||
|
||||
# check if we want to update existing objects
|
||||
existing_objects = protlib.search_objects_with_prototype(prototype_key)
|
||||
if existing_objects:
|
||||
if 'update' not in self.switches:
|
||||
n_existing = len(existing_objects)
|
||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||
string = ("There are {} objects already created with an older version "
|
||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||
n_existing, prototype_key, slow))
|
||||
answer = yield(string)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rNo update was done of existing objects. "
|
||||
"Use @spawn/update <key> to apply later as needed.|n")
|
||||
return
|
||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||
caller.msg("{} objects were updated.".format(n_updated))
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
ncount = len(protlib.search_prototype())
|
||||
caller.msg("Usage: @spawn <prototype-key> or {{key: value, ...}}"
|
||||
"\n ({} existing prototypes. Use /list to inspect)".format(ncount))
|
||||
return
|
||||
|
||||
if 'delete' in self.switches:
|
||||
# remove db-based prototype
|
||||
matchstring = _search_show_prototype(self.args)
|
||||
if matchstring:
|
||||
string = "|rDeleting prototype:|n\n{}".format(matchstring)
|
||||
question = "\nDo you want to continue deleting? [Y]/N"
|
||||
answer = yield(string + question)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rDeletion cancelled.|n")
|
||||
return
|
||||
try:
|
||||
success = protlib.delete_db_prototype(caller, self.args)
|
||||
except protlib.PermissionError as err:
|
||||
caller.msg("|rError deleting:|R {}|n".format(err))
|
||||
caller.msg("Deletion {}.".format(
|
||||
'successful' if success else 'failed (does the prototype exist?)'))
|
||||
return
|
||||
else:
|
||||
caller.msg("Could not find prototype '{}'".format(key))
|
||||
|
||||
if 'update' in self.switches:
|
||||
# update existing prototypes
|
||||
key = self.args.strip().lower()
|
||||
existing_objects = protlib.search_objects_with_prototype(key)
|
||||
if existing_objects:
|
||||
n_existing = len(existing_objects)
|
||||
slow = " (note that this may be slow)" if n_existing > 10 else ""
|
||||
string = ("There are {} objects already created with an older version "
|
||||
"of prototype {}. Should it be re-applied to them{}? [Y]/N".format(
|
||||
n_existing, key, slow))
|
||||
answer = yield(string)
|
||||
if answer.lower() in ["n", "no"]:
|
||||
caller.msg("|rUpdate cancelled.")
|
||||
return
|
||||
n_updated = spawner.batch_update_objects_with_prototype(existing_objects, key)
|
||||
caller.msg("{} objects were updated.".format(n_updated))
|
||||
|
||||
# A direct creation of an object from a given prototype
|
||||
|
||||
prototype = _parse_prototype(
|
||||
self.args, expect=dict if self.args.strip().startswith("{") else basestring)
|
||||
if not prototype:
|
||||
# this will only let through dicts or strings
|
||||
return
|
||||
|
||||
key = '<unnamed>'
|
||||
if isinstance(prototype, basestring):
|
||||
# A prototype key we are looking to apply
|
||||
key = prototype
|
||||
prototypes = protlib.search_prototype(prototype)
|
||||
nprots = len(prototypes)
|
||||
if not prototypes:
|
||||
caller.msg("No prototype named '%s'." % prototype)
|
||||
return
|
||||
elif nprots > 1:
|
||||
caller.msg("Found {} prototypes matching '{}':\n {}".format(
|
||||
nprots, prototype, ", ".join(prot.get('prototype_key', '')
|
||||
for proto in prototypes)))
|
||||
return
|
||||
# we have a prototype, check access
|
||||
prototype = prototypes[0]
|
||||
if not caller.locks.check_lockstring(caller, prototype.get('prototype_locks', ''), access_type='spawn'):
|
||||
caller.msg("You don't have access to use this prototype.")
|
||||
return
|
||||
|
||||
if "noloc" not in self.switches and "location" not in prototype:
|
||||
prototype["location"] = self.caller.location
|
||||
|
||||
for obj in spawn(prototype):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
# proceed to spawning
|
||||
try:
|
||||
for obj in spawner.spawn(prototype):
|
||||
self.caller.msg("Spawned %s." % obj.get_display_name(self.caller))
|
||||
except RuntimeError as err:
|
||||
caller.msg(err)
|
||||
|
|
|
|||
|
|
@ -23,3 +23,4 @@ class UnloggedinCmdSet(CmdSet):
|
|||
self.add(unloggedin.CmdUnconnectedHelp())
|
||||
self.add(unloggedin.CmdUnconnectedEncoding())
|
||||
self.add(unloggedin.CmdUnconnectedScreenreader())
|
||||
self.add(unloggedin.CmdUnconnectedInfo())
|
||||
|
|
|
|||
|
|
@ -71,7 +71,7 @@ class CmdLook(COMMAND_DEFAULT_CLASS):
|
|||
target = caller.search(self.args)
|
||||
if not target:
|
||||
return
|
||||
self.msg(caller.at_look(target))
|
||||
self.msg((caller.at_look(target), {'type': 'look'}), options=None)
|
||||
|
||||
|
||||
class CmdNick(COMMAND_DEFAULT_CLASS):
|
||||
|
|
|
|||
|
|
@ -14,24 +14,26 @@ main test suite started with
|
|||
|
||||
import re
|
||||
import types
|
||||
import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from mock import Mock, mock
|
||||
|
||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
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.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 import search_object
|
||||
from evennia import DefaultObject, DefaultCharacter
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
|
||||
|
||||
# set up signal here since we are not starting the server
|
||||
|
||||
_RE = re.compile(r"^\+|-+\+|\+-+|--*|\|(?:\s|$)", re.MULTILINE)
|
||||
_RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
|
||||
|
||||
|
||||
# ------------------------------------------------------------
|
||||
|
|
@ -44,7 +46,7 @@ class CommandTest(EvenniaTest):
|
|||
Tests a command
|
||||
"""
|
||||
def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None,
|
||||
receiver=None, cmdstring=None, obj=None):
|
||||
receiver=None, cmdstring=None, obj=None, inputs=None):
|
||||
"""
|
||||
Test a command by assigning all the needed
|
||||
properties to cmdobj and running
|
||||
|
|
@ -73,37 +75,58 @@ class CommandTest(EvenniaTest):
|
|||
cmdobj.obj = obj or (caller if caller else self.char1)
|
||||
# test
|
||||
old_msg = receiver.msg
|
||||
inputs = inputs or []
|
||||
|
||||
try:
|
||||
receiver.msg = Mock()
|
||||
cmdobj.at_pre_cmd()
|
||||
if cmdobj.at_pre_cmd():
|
||||
return
|
||||
cmdobj.parse()
|
||||
ret = cmdobj.func()
|
||||
|
||||
# handle func's with yield in them (generators)
|
||||
if isinstance(ret, types.GeneratorType):
|
||||
ret.next()
|
||||
while True:
|
||||
try:
|
||||
inp = inputs.pop() if inputs else None
|
||||
if inp:
|
||||
try:
|
||||
ret.send(inp)
|
||||
except TypeError:
|
||||
ret.next()
|
||||
ret = ret.send(inp)
|
||||
else:
|
||||
ret.next()
|
||||
except StopIteration:
|
||||
break
|
||||
|
||||
cmdobj.at_post_cmd()
|
||||
except StopIteration:
|
||||
pass
|
||||
except InterruptCommand:
|
||||
pass
|
||||
finally:
|
||||
# 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))
|
||||
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
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
if msg is not None:
|
||||
returned_msg = "||".join(_RE.sub("", mess) for mess in stored_msg)
|
||||
returned_msg = ansi.parse_ansi(returned_msg, strip_ansi=noansi).strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||
sep3 = "\n" + "=" * 78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
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
|
||||
|
||||
# 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))
|
||||
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
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
if msg is not None:
|
||||
# set our separator for returned messages based on parsing ansi or not
|
||||
msg_sep = "|" if noansi else "||"
|
||||
# Have to strip ansi for each returned message for the regex to handle it correctly
|
||||
returned_msg = msg_sep.join(_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi))
|
||||
for mess in stored_msg).strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||
sep3 = "\n" + "=" * 78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
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
|
||||
|
||||
|
|
@ -127,11 +150,11 @@ class TestGeneral(CommandTest):
|
|||
|
||||
def test_nick(self):
|
||||
self.call(general.CmdNick(), "testalias = testaliasedstring1",
|
||||
"Inputlinenick 'testalias' mapped to 'testaliasedstring1'.")
|
||||
"Inputline-nick 'testalias' mapped to 'testaliasedstring1'.")
|
||||
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",
|
||||
"Objectnick 'testalias' mapped to 'testaliasedstring3'.")
|
||||
"Object-nick 'testalias' mapped to 'testaliasedstring3'.")
|
||||
self.assertEqual(u"testaliasedstring1", self.char1.nicks.get("testalias"))
|
||||
self.assertEqual(u"testaliasedstring2", self.char1.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")
|
||||
|
||||
def test_scripts(self):
|
||||
self.call(system.CmdScripts(), "", "| dbref |")
|
||||
self.call(system.CmdScripts(), "", "dbref ")
|
||||
|
||||
def test_objects(self):
|
||||
self.call(system.CmdObjects(), "", "Object subtype totals")
|
||||
|
|
@ -218,14 +241,14 @@ class TestAdmin(CommandTest):
|
|||
self.call(admin.CmdWall(), "Test", "Announcing to all connected sessions ...")
|
||||
|
||||
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):
|
||||
|
||||
def test_ooc_look(self):
|
||||
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:
|
||||
self.call(account.CmdOOCLook(), "", "Account TestAccount (you are OutofCharacter)", caller=self.account)
|
||||
|
||||
|
|
@ -282,8 +305,8 @@ class TestBuilding(CommandTest):
|
|||
def test_attribute_commands(self):
|
||||
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.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.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.CmdWipe(), "Obj2/test2/test3", "Wiped attributes test2,test3 on Obj2.")
|
||||
|
||||
def test_name(self):
|
||||
|
|
@ -309,7 +332,7 @@ class TestBuilding(CommandTest):
|
|||
|
||||
def test_exit_commands(self):
|
||||
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.")
|
||||
|
||||
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.")
|
||||
|
||||
def test_find(self):
|
||||
self.call(building.CmdFind(), "Room2", "One Match")
|
||||
expect = "One Match(#1#7, loc):\n " +\
|
||||
"Char2(#7) evennia.objects.objects.DefaultCharacter (location: Room(#1))"
|
||||
self.call(building.CmdFind(), "oom2", "One Match")
|
||||
expect = "One Match(#1-#7, loc):\n " +\
|
||||
"Char2(#7) - evennia.objects.objects.DefaultCharacter (location: Room(#1))"
|
||||
self.call(building.CmdFind(), "Char2", expect, cmdstring="locate")
|
||||
self.call(building.CmdFind(), "/ex Char2", # /ex is an ambiguous switch
|
||||
"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(), "/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(), "/startswith Room2", "One Match")
|
||||
|
||||
def test_script(self):
|
||||
self.call(building.CmdScript(), "Obj = scripts.Script", "Script scripts.Script successfully added")
|
||||
|
|
@ -344,7 +368,7 @@ class TestBuilding(CommandTest):
|
|||
def test_teleport(self):
|
||||
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#2)\n|Teleported to Room2.")
|
||||
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
|
||||
"Destination has no location.")
|
||||
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.
|
||||
query = search_object(objKeyStr)
|
||||
commandTest.assertIsNotNone(query)
|
||||
commandTest.assertTrue(bool(query))
|
||||
obj = query[0]
|
||||
commandTest.assertIsNotNone(obj)
|
||||
return obj
|
||||
|
|
@ -364,17 +389,20 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdSpawn(), " ", "Usage: @spawn")
|
||||
|
||||
# Tests "@spawn <prototype_dictionary>" without specifying location.
|
||||
|
||||
self.call(building.CmdSpawn(),
|
||||
"{'key':'goblin', 'typeclass':'evennia.DefaultCharacter'}", "Spawned goblin")
|
||||
goblin = getObject(self, "goblin")
|
||||
"/save {'prototype_key': 'testprot', 'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"Saved prototype: testprot", inputs=['y'])
|
||||
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
self.call(building.CmdSpawn(), 'testprot', "Spawned Test Char")
|
||||
# Tests that the spawned object's location is the same as the caharacter's location, since
|
||||
# we did not specify it.
|
||||
self.assertEqual(goblin.location, self.char1.location)
|
||||
goblin.delete()
|
||||
testchar = getObject(self, "Test Char")
|
||||
self.assertEqual(testchar.location, self.char1.location)
|
||||
testchar.delete()
|
||||
|
||||
# Test "@spawn <prototype_dictionary>" with a location other than the character's.
|
||||
spawnLoc = self.room2
|
||||
|
|
@ -384,14 +412,23 @@ class TestBuilding(CommandTest):
|
|||
spawnLoc = self.room1
|
||||
|
||||
self.call(building.CmdSpawn(),
|
||||
"{'prototype':'GOBLIN', 'key':'goblin', 'location':'%s'}"
|
||||
% spawnLoc.dbref, "Spawned goblin")
|
||||
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
|
||||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "Spawned goblin")
|
||||
goblin = getObject(self, "goblin")
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.assertEqual(goblin.location, spawnLoc)
|
||||
|
||||
goblin.delete()
|
||||
|
||||
# create prototype
|
||||
protlib.create_prototype(**{'key': 'Ball',
|
||||
'typeclass': 'evennia.objects.objects.DefaultCharacter',
|
||||
'prototype_key': 'testball'})
|
||||
|
||||
# Tests "@spawn <prototype_name>"
|
||||
self.call(building.CmdSpawn(), "'BALL'", "Spawned Ball")
|
||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||
|
||||
ball = getObject(self, "Ball")
|
||||
self.assertEqual(ball.location, self.char1.location)
|
||||
self.assertIsInstance(ball, DefaultObject)
|
||||
|
|
@ -404,10 +441,14 @@ class TestBuilding(CommandTest):
|
|||
self.assertIsNone(ball.location)
|
||||
ball.delete()
|
||||
|
||||
self.call(building.CmdSpawn(),
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'prototype_key': 'testball', 'location':'%s'}"
|
||||
% spawnLoc.dbref, "Error: Prototype testball tries to parent itself.")
|
||||
|
||||
# Tests "@spawn/noloc ...", but DO specify a location.
|
||||
# Location should be the specified location.
|
||||
self.call(building.CmdSpawn(),
|
||||
"/noloc {'prototype':'BALL', 'location':'%s'}"
|
||||
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo', 'location':'%s'}"
|
||||
% spawnLoc.dbref, "Spawned Ball")
|
||||
ball = getObject(self, "Ball")
|
||||
self.assertEqual(ball.location, spawnLoc)
|
||||
|
|
@ -416,6 +457,9 @@ class TestBuilding(CommandTest):
|
|||
# test calling spawn with an invalid prototype.
|
||||
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
||||
|
||||
# Test listing commands
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
|
||||
class TestComms(CommandTest):
|
||||
|
||||
|
|
@ -471,7 +515,7 @@ class TestBatchProcess(CommandTest):
|
|||
def test_batch_commands(self):
|
||||
# cannot test batchcode here, it must run inside the server process
|
||||
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
|
||||
confirm = building.CmdDestroy.confirm
|
||||
building.CmdDestroy.confirm = False
|
||||
|
|
@ -494,3 +538,12 @@ class TestInterruptCommand(CommandTest):
|
|||
def test_interrupt_command(self):
|
||||
ret = self.call(CmdInterrupt(), "")
|
||||
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 time
|
||||
import datetime
|
||||
from collections import defaultdict
|
||||
from random import getrandbits
|
||||
from django.conf import settings
|
||||
|
|
@ -11,8 +12,9 @@ from evennia.accounts.models import AccountDB
|
|||
from evennia.objects.models import ObjectDB
|
||||
from evennia.server.models import ServerConfig
|
||||
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
|
||||
|
||||
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)
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
Helper function, creates an account of the specified typeclass.
|
||||
|
|
|
|||
|
|
@ -97,7 +97,8 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
|||
@property
|
||||
def wholist(self):
|
||||
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:
|
||||
# display listening subscribers in bold
|
||||
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.
|
||||
* GenderSub (Griatch 2015) - Simple example (only) of storing gender
|
||||
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.
|
||||
* Menu login (Griatch 2011) - A login system using menus asking
|
||||
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
|
||||
branching EvMenu with selection options sourced from a single
|
||||
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
|
||||
with dynamically created locations.
|
||||
* UnixCommand (Vincent Le Geoff 2017) - Add commands with UNIX-style syntax.
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ class CmdMail(default_cmds.MuxCommand):
|
|||
index += 1
|
||||
|
||||
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(3, width=13)
|
||||
table.reformat_column(4, width=7)
|
||||
|
|
|
|||
|
|
@ -1088,7 +1088,7 @@ class CmdMask(RPCommand):
|
|||
if self.cmdstring == "mask":
|
||||
# wear a mask
|
||||
if not self.args:
|
||||
caller.msg("Usage: (un)wearmask sdesc")
|
||||
caller.msg("Usage: (un)mask sdesc")
|
||||
return
|
||||
if caller.db.unmasked_sdesc:
|
||||
caller.msg("You are already wearing a mask.")
|
||||
|
|
@ -1111,7 +1111,7 @@ class CmdMask(RPCommand):
|
|||
del caller.db.unmasked_sdesc
|
||||
caller.locks.remove("enable_recog")
|
||||
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):
|
||||
|
|
|
|||
|
|
@ -697,7 +697,7 @@ class TestMail(CommandTest):
|
|||
"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 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(), "/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)
|
||||
|
|
@ -723,9 +723,9 @@ class TestMapBuilder(CommandTest):
|
|||
"evennia.contrib.mapbuilder.EXAMPLE2_MAP evennia.contrib.mapbuilder.EXAMPLE2_LEGEND",
|
||||
"""Creating Map...|≈ ≈ ≈ ≈ ≈
|
||||
|
||||
≈ ♣♣♣ ≈
|
||||
≈ ♣-♣-♣ ≈
|
||||
≈ ♣ ♣ ♣ ≈
|
||||
≈ ♣♣♣ ≈
|
||||
≈ ♣-♣-♣ ≈
|
||||
|
||||
≈ ≈ ≈ ≈ ≈
|
||||
|Creating Landmass...|""")
|
||||
|
|
@ -768,8 +768,8 @@ from evennia.contrib import simpledoor
|
|||
class TestSimpleDoor(CommandTest):
|
||||
def test_cmdopen(self):
|
||||
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 ignored eventual custom returnexit type.|Created new Exit 'newdoor' from Room2 to Room (aliases: door).")
|
||||
"Created new Exit 'newdoor' from Room to Room2 (aliases: door).|Note: A door-type exit was "
|
||||
"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", "newdoor is already closed.", cmdstring="close")
|
||||
self.call(simpledoor.CmdOpenCloseDoor(), "newdoor", "You open newdoor.", cmdstring="open")
|
||||
|
|
@ -798,7 +798,7 @@ from evennia.contrib import talking_npc
|
|||
class TestTalkingNPC(CommandTest):
|
||||
def test_talkingnpc(self):
|
||||
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()
|
||||
|
||||
|
||||
|
|
@ -953,13 +953,13 @@ class TestTutorialWorldRooms(CommandTest):
|
|||
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
class TestTurnBattleCmd(CommandTest):
|
||||
class TestTurnBattleBasicCmd(CommandTest):
|
||||
|
||||
# Test combat commands
|
||||
# Test basic combat commands
|
||||
def test_turnbattlecmd(self):
|
||||
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)")
|
||||
|
|
@ -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.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
|
||||
def test_turnbattleequipcmd(self):
|
||||
# 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.CmdUnwield(), "", "Char lowers test weapon.")
|
||||
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.CmdRest(), "", "Char rests to recover HP.")
|
||||
|
||||
|
||||
class TestTurnBattleRangeCmd(CommandTest):
|
||||
# Test range commands
|
||||
def test_turnbattlerangecmd(self):
|
||||
# Start with range module specific commands.
|
||||
|
|
@ -1000,257 +1008,531 @@ class TestTurnBattleCmd(CommandTest):
|
|||
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
|
||||
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 = tb_basic.roll_init(attacker)
|
||||
initiative = tb_basic.roll_init(self.attacker)
|
||||
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||
# 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)
|
||||
# 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)
|
||||
# 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)
|
||||
# Apply damage
|
||||
defender.db.hp = 10
|
||||
tb_basic.apply_damage(defender, 3)
|
||||
self.assertTrue(defender.db.hp == 7)
|
||||
self.defender.db.hp = 10
|
||||
tb_basic.apply_damage(self.defender, 3)
|
||||
self.assertTrue(self.defender.db.hp == 7)
|
||||
# Resolve attack
|
||||
defender.db.hp = 40
|
||||
tb_basic.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
||||
self.assertTrue(defender.db.hp < 40)
|
||||
self.defender.db.hp = 40
|
||||
tb_basic.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
|
||||
self.assertTrue(self.defender.db.hp < 40)
|
||||
# Combat cleanup
|
||||
attacker.db.Combat_attribute = True
|
||||
tb_basic.combat_cleanup(attacker)
|
||||
self.assertFalse(attacker.db.combat_attribute)
|
||||
self.attacker.db.Combat_attribute = True
|
||||
tb_basic.combat_cleanup(self.attacker)
|
||||
self.assertFalse(self.attacker.db.combat_attribute)
|
||||
# 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
|
||||
attacker.location.scripts.add(tb_basic.TBBasicTurnHandler)
|
||||
turnhandler = attacker.db.combat_TurnHandler
|
||||
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||
self.attacker.location.scripts.add(tb_basic.TBBasicTurnHandler)
|
||||
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.
|
||||
turnhandler.interval = 10000
|
||||
self.turnhandler.interval = 10000
|
||||
# Force turn order
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
self.turnhandler.db.fighters = [self.attacker, self.defender]
|
||||
self.turnhandler.db.turn = 0
|
||||
# Test is turn
|
||||
self.assertTrue(tb_basic.is_turn(attacker))
|
||||
self.assertTrue(tb_basic.is_turn(self.attacker))
|
||||
# Spend actions
|
||||
attacker.db.Combat_ActionsLeft = 1
|
||||
tb_basic.spend_action(attacker, 1, action_name="Test")
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||
self.attacker.db.Combat_ActionsLeft = 1
|
||||
tb_basic.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
|
||||
attacker.db.Combat_ActionsLeft = 983
|
||||
turnhandler.initialize_for_combat(attacker)
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||
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
|
||||
defender.db.Combat_ActionsLeft = 0
|
||||
turnhandler.start_turn(defender)
|
||||
self.assertTrue(defender.db.Combat_ActionsLeft == 1)
|
||||
self.defender.db.Combat_ActionsLeft = 0
|
||||
self.turnhandler.start_turn(self.defender)
|
||||
self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
|
||||
# Next turn
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.next_turn()
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
attacker.db.Combat_ActionsLeft = 0
|
||||
turnhandler.turn_end_check(attacker)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
joiner = create_object(tb_basic.TBBasicCharacter, key="Joiner")
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.join_fight(joiner)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||
# Remove the script at the end
|
||||
turnhandler.stop()
|
||||
self.joiner.location = self.testroom
|
||||
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])
|
||||
|
||||
|
||||
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.
|
||||
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 = tb_equip.roll_init(attacker)
|
||||
initiative = tb_equip.roll_init(self.attacker)
|
||||
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||
# 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)
|
||||
# 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)
|
||||
# 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)
|
||||
# Apply damage
|
||||
defender.db.hp = 10
|
||||
tb_equip.apply_damage(defender, 3)
|
||||
self.assertTrue(defender.db.hp == 7)
|
||||
self.defender.db.hp = 10
|
||||
tb_equip.apply_damage(self.defender, 3)
|
||||
self.assertTrue(self.defender.db.hp == 7)
|
||||
# Resolve attack
|
||||
defender.db.hp = 40
|
||||
tb_equip.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
||||
self.assertTrue(defender.db.hp < 40)
|
||||
self.defender.db.hp = 40
|
||||
tb_equip.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
|
||||
self.assertTrue(self.defender.db.hp < 40)
|
||||
# Combat cleanup
|
||||
attacker.db.Combat_attribute = True
|
||||
tb_equip.combat_cleanup(attacker)
|
||||
self.assertFalse(attacker.db.combat_attribute)
|
||||
self.attacker.db.Combat_attribute = True
|
||||
tb_equip.combat_cleanup(self.attacker)
|
||||
self.assertFalse(self.attacker.db.combat_attribute)
|
||||
# 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
|
||||
attacker.location.scripts.add(tb_equip.TBEquipTurnHandler)
|
||||
turnhandler = attacker.db.combat_TurnHandler
|
||||
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||
self.attacker.location.scripts.add(tb_equip.TBEquipTurnHandler)
|
||||
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.
|
||||
turnhandler.interval = 10000
|
||||
self.turnhandler.interval = 10000
|
||||
# Force turn order
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
self.turnhandler.db.fighters = [self.attacker, self.defender]
|
||||
self.turnhandler.db.turn = 0
|
||||
# Test is turn
|
||||
self.assertTrue(tb_equip.is_turn(attacker))
|
||||
self.assertTrue(tb_equip.is_turn(self.attacker))
|
||||
# Spend actions
|
||||
attacker.db.Combat_ActionsLeft = 1
|
||||
tb_equip.spend_action(attacker, 1, action_name="Test")
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||
self.attacker.db.Combat_ActionsLeft = 1
|
||||
tb_equip.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
|
||||
attacker.db.Combat_ActionsLeft = 983
|
||||
turnhandler.initialize_for_combat(attacker)
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||
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
|
||||
defender.db.Combat_ActionsLeft = 0
|
||||
turnhandler.start_turn(defender)
|
||||
self.assertTrue(defender.db.Combat_ActionsLeft == 1)
|
||||
self.defender.db.Combat_ActionsLeft = 0
|
||||
self.turnhandler.start_turn(self.defender)
|
||||
self.assertTrue(self.defender.db.Combat_ActionsLeft == 1)
|
||||
# Next turn
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.next_turn()
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
attacker.db.Combat_ActionsLeft = 0
|
||||
turnhandler.turn_end_check(attacker)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
joiner = create_object(tb_equip.TBEquipCharacter, key="Joiner")
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.join_fight(joiner)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||
# Remove the script at the end
|
||||
turnhandler.stop()
|
||||
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])
|
||||
|
||||
|
||||
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.
|
||||
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 = tb_range.roll_init(attacker)
|
||||
initiative = tb_range.roll_init(self.attacker)
|
||||
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||
# 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)
|
||||
# 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)
|
||||
# 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)
|
||||
# Apply damage
|
||||
defender.db.hp = 10
|
||||
tb_range.apply_damage(defender, 3)
|
||||
self.assertTrue(defender.db.hp == 7)
|
||||
self.defender.db.hp = 10
|
||||
tb_range.apply_damage(self.defender, 3)
|
||||
self.assertTrue(self.defender.db.hp == 7)
|
||||
# Resolve attack
|
||||
defender.db.hp = 40
|
||||
tb_range.resolve_attack(attacker, defender, "test", attack_value=20, defense_value=10)
|
||||
self.assertTrue(defender.db.hp < 40)
|
||||
self.defender.db.hp = 40
|
||||
tb_range.resolve_attack(self.attacker, self.defender, "test", attack_value=20, defense_value=10)
|
||||
self.assertTrue(self.defender.db.hp < 40)
|
||||
# Combat cleanup
|
||||
attacker.db.Combat_attribute = True
|
||||
tb_range.combat_cleanup(attacker)
|
||||
self.assertFalse(attacker.db.combat_attribute)
|
||||
self.attacker.db.Combat_attribute = True
|
||||
tb_range.combat_cleanup(self.attacker)
|
||||
self.assertFalse(self.attacker.db.combat_attribute)
|
||||
# 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
|
||||
attacker.location.scripts.add(tb_range.TBRangeTurnHandler)
|
||||
turnhandler = attacker.db.combat_TurnHandler
|
||||
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||
self.attacker.location.scripts.add(tb_range.TBRangeTurnHandler)
|
||||
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.
|
||||
turnhandler.interval = 10000
|
||||
self.turnhandler.interval = 10000
|
||||
# Force turn order
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
self.turnhandler.db.fighters = [self.attacker, self.defender]
|
||||
self.turnhandler.db.turn = 0
|
||||
# Test is turn
|
||||
self.assertTrue(tb_range.is_turn(attacker))
|
||||
self.assertTrue(tb_range.is_turn(self.attacker))
|
||||
# Spend actions
|
||||
attacker.db.Combat_ActionsLeft = 1
|
||||
tb_range.spend_action(attacker, 1, action_name="Test")
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||
self.attacker.db.Combat_ActionsLeft = 1
|
||||
tb_range.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
|
||||
attacker.db.Combat_ActionsLeft = 983
|
||||
turnhandler.initialize_for_combat(attacker)
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||
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")
|
||||
# Set up ranges again, since initialize_for_combat clears them
|
||||
attacker.db.combat_range = {}
|
||||
attacker.db.combat_range[attacker] = 0
|
||||
attacker.db.combat_range[defender] = 1
|
||||
defender.db.combat_range = {}
|
||||
defender.db.combat_range[defender] = 0
|
||||
defender.db.combat_range[attacker] = 1
|
||||
self.attacker.db.combat_range = {}
|
||||
self.attacker.db.combat_range[self.attacker] = 0
|
||||
self.attacker.db.combat_range[self.defender] = 1
|
||||
self.defender.db.combat_range = {}
|
||||
self.defender.db.combat_range[self.defender] = 0
|
||||
self.defender.db.combat_range[self.attacker] = 1
|
||||
# Start turn
|
||||
defender.db.Combat_ActionsLeft = 0
|
||||
turnhandler.start_turn(defender)
|
||||
self.assertTrue(defender.db.Combat_ActionsLeft == 2)
|
||||
self.defender.db.Combat_ActionsLeft = 0
|
||||
self.turnhandler.start_turn(self.defender)
|
||||
self.assertTrue(self.defender.db.Combat_ActionsLeft == 2)
|
||||
# Next turn
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.next_turn()
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
attacker.db.Combat_ActionsLeft = 0
|
||||
turnhandler.turn_end_check(attacker)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
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
|
||||
joiner = create_object(tb_range.TBRangeCharacter, key="Joiner", location=testroom)
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.join_fight(joiner)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||
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, 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
|
||||
tb_range.approach(attacker, defender)
|
||||
self.assertTrue(tb_range.get_range(attacker, defender) == 0)
|
||||
tb_range.approach(self.attacker, self.defender)
|
||||
self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 0)
|
||||
# Withdraw
|
||||
tb_range.withdraw(attacker, defender)
|
||||
self.assertTrue(tb_range.get_range(attacker, defender) == 1)
|
||||
# Remove the script at the end
|
||||
turnhandler.stop()
|
||||
tb_range.withdraw(self.attacker, self.defender)
|
||||
self.assertTrue(tb_range.get_range(self.attacker, self.defender) == 1)
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
|
@ -1263,6 +1545,7 @@ Bar
|
|||
--Baz 2
|
||||
-Qux"""
|
||||
|
||||
|
||||
class TestTreeSelectFunc(EvenniaTest):
|
||||
|
||||
def test_tree_functions(self):
|
||||
|
|
|
|||
|
|
@ -21,6 +21,19 @@ implemented and customized:
|
|||
the battle system, including commands for wielding weapons and
|
||||
donning armor, and modifiers to accuracy and damage based on
|
||||
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
|
||||
tracks the distance between different characters and objects in
|
||||
|
|
|
|||
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.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
|
||||
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 "
|
||||
"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."]
|
||||
elif self.db.exit_open:
|
||||
# we pressed the button; the exit is open
|
||||
|
|
@ -905,19 +905,19 @@ WEAPON_PROTOTYPES = {
|
|||
"magic": False,
|
||||
"desc": "A generic blade."},
|
||||
"knife": {
|
||||
"prototype": "weapon",
|
||||
"prototype_parent": "weapon",
|
||||
"aliases": "sword",
|
||||
"key": "Kitchen knife",
|
||||
"desc": "A rusty kitchen knife. Better than nothing.",
|
||||
"damage": 3},
|
||||
"dagger": {
|
||||
"prototype": "knife",
|
||||
"prototype_parent": "knife",
|
||||
"key": "Rusty dagger",
|
||||
"aliases": ["knife", "dagger"],
|
||||
"desc": "A double-edged dagger with a nicked edge and a wooden handle.",
|
||||
"hit": 0.25},
|
||||
"sword": {
|
||||
"prototype": "weapon",
|
||||
"prototype_parent": "weapon",
|
||||
"key": "Rusty sword",
|
||||
"aliases": ["sword"],
|
||||
"desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.",
|
||||
|
|
@ -925,28 +925,28 @@ WEAPON_PROTOTYPES = {
|
|||
"damage": 5,
|
||||
"parry": 0.5},
|
||||
"club": {
|
||||
"prototype": "weapon",
|
||||
"prototype_parent": "weapon",
|
||||
"key": "Club",
|
||||
"desc": "A heavy wooden club, little more than a heavy branch.",
|
||||
"hit": 0.4,
|
||||
"damage": 6,
|
||||
"parry": 0.2},
|
||||
"axe": {
|
||||
"prototype": "weapon",
|
||||
"prototype_parent": "weapon",
|
||||
"key": "Axe",
|
||||
"desc": "A woodcutter's axe with a keen edge.",
|
||||
"hit": 0.4,
|
||||
"damage": 6,
|
||||
"parry": 0.2},
|
||||
"ornate longsword": {
|
||||
"prototype": "sword",
|
||||
"prototype_parent": "sword",
|
||||
"key": "Ornate longsword",
|
||||
"desc": "A fine longsword with some swirling patterns on the handle.",
|
||||
"hit": 0.5,
|
||||
"magic": True,
|
||||
"damage": 5},
|
||||
"warhammer": {
|
||||
"prototype": "club",
|
||||
"prototype_parent": "club",
|
||||
"key": "Silver Warhammer",
|
||||
"aliases": ["hammer", "warhammer", "war"],
|
||||
"desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.",
|
||||
|
|
@ -954,21 +954,21 @@ WEAPON_PROTOTYPES = {
|
|||
"magic": True,
|
||||
"damage": 8},
|
||||
"rune axe": {
|
||||
"prototype": "axe",
|
||||
"prototype_parent": "axe",
|
||||
"key": "Runeaxe",
|
||||
"aliases": ["axe"],
|
||||
"hit": 0.4,
|
||||
"magic": True,
|
||||
"damage": 6},
|
||||
"thruning": {
|
||||
"prototype": "ornate longsword",
|
||||
"prototype_parent": "ornate longsword",
|
||||
"key": "Broadsword named Thruning",
|
||||
"desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.",
|
||||
"hit": 0.6,
|
||||
"parry": 0.6,
|
||||
"damage": 7},
|
||||
"slayer waraxe": {
|
||||
"prototype": "rune axe",
|
||||
"prototype_parent": "rune axe",
|
||||
"key": "Slayer waraxe",
|
||||
"aliases": ["waraxe", "war", "slayer"],
|
||||
"desc": "A huge double-bladed axe marked with the runes for 'Slayer'."
|
||||
|
|
@ -976,7 +976,7 @@ WEAPON_PROTOTYPES = {
|
|||
"hit": 0.7,
|
||||
"damage": 8},
|
||||
"ghostblade": {
|
||||
"prototype": "ornate longsword",
|
||||
"prototype_parent": "ornate longsword",
|
||||
"key": "The Ghostblade",
|
||||
"aliases": ["blade", "ghost"],
|
||||
"desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing."
|
||||
|
|
@ -985,7 +985,7 @@ WEAPON_PROTOTYPES = {
|
|||
"parry": 0.8,
|
||||
"damage": 10},
|
||||
"hawkblade": {
|
||||
"prototype": "ghostblade",
|
||||
"prototype_parent": "ghostblade",
|
||||
"key": "The Hawkblade",
|
||||
"aliases": ["hawk", "blade"],
|
||||
"desc": "The weapon of a long-dead heroine and a more civilized age,"
|
||||
|
|
|
|||
|
|
@ -65,7 +65,6 @@ class Account(DefaultAccount):
|
|||
* Helper methods
|
||||
|
||||
msg(text=None, **kwargs)
|
||||
swap_character(new_character, delete_old_character=False)
|
||||
execute_cmd(raw_string, session=None)
|
||||
search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, account=False)
|
||||
is_typeclass(typeclass, exact=False)
|
||||
|
|
|
|||
|
|
@ -287,7 +287,7 @@ class LockHandler(object):
|
|||
"""
|
||||
self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser
|
||||
|
||||
def add(self, lockstring):
|
||||
def add(self, lockstring, validate_only=False):
|
||||
"""
|
||||
Add a new lockstring to handler.
|
||||
|
||||
|
|
@ -296,10 +296,12 @@ class LockHandler(object):
|
|||
`"<access_type>:<functions>"`. Multiple access types
|
||||
should be separated by semicolon (`;`). Alternatively,
|
||||
a list with lockstrings.
|
||||
|
||||
validate_only (bool, optional): If True, validate the lockstring but
|
||||
don't actually store it.
|
||||
Returns:
|
||||
success (bool): The outcome of the addition, `False` on
|
||||
error.
|
||||
error. If `validate_only` is True, this will be a tuple
|
||||
(bool, error), for pass/fail and a string error.
|
||||
|
||||
"""
|
||||
if isinstance(lockstring, basestring):
|
||||
|
|
@ -308,21 +310,41 @@ class LockHandler(object):
|
|||
lockdefs = [lockdef for locks in lockstring for lockdef in locks.split(";")]
|
||||
lockstring = ";".join(lockdefs)
|
||||
|
||||
err = ""
|
||||
# sanity checks
|
||||
for lockdef in lockdefs:
|
||||
if ':' not in lockdef:
|
||||
self._log_error(_("Lock: '%s' contains no colon (:).") % lockdef)
|
||||
return False
|
||||
err = _("Lock: '{lockdef}' contains no colon (:).").format(lockdef=lockdef)
|
||||
if validate_only:
|
||||
return False, err
|
||||
else:
|
||||
self._log_error(err)
|
||||
return False
|
||||
access_type, rhs = [part.strip() for part in lockdef.split(':', 1)]
|
||||
if not access_type:
|
||||
self._log_error(_("Lock: '%s' has no access_type (left-side of colon is empty).") % lockdef)
|
||||
return False
|
||||
err = _("Lock: '{lockdef}' has no access_type "
|
||||
"(left-side of colon is empty).").format(lockdef=lockdef)
|
||||
if validate_only:
|
||||
return False, err
|
||||
else:
|
||||
self._log_error(err)
|
||||
return False
|
||||
if rhs.count('(') != rhs.count(')'):
|
||||
self._log_error(_("Lock: '%s' has mismatched parentheses.") % lockdef)
|
||||
return False
|
||||
err = _("Lock: '{lockdef}' has mismatched parentheses.").format(lockdef=lockdef)
|
||||
if validate_only:
|
||||
return False, err
|
||||
else:
|
||||
self._log_error(err)
|
||||
return False
|
||||
if not _RE_FUNCS.findall(rhs):
|
||||
self._log_error(_("Lock: '%s' has no valid lock functions.") % lockdef)
|
||||
return False
|
||||
err = _("Lock: '{lockdef}' has no valid lock functions.").format(lockdef=lockdef)
|
||||
if validate_only:
|
||||
return False, err
|
||||
else:
|
||||
self._log_error(err)
|
||||
return False
|
||||
if validate_only:
|
||||
return True, None
|
||||
# get the lock string
|
||||
storage_lockstring = self.obj.lock_storage
|
||||
if storage_lockstring:
|
||||
|
|
@ -334,6 +356,18 @@ class LockHandler(object):
|
|||
self._save_locks()
|
||||
return True
|
||||
|
||||
def validate(self, lockstring):
|
||||
"""
|
||||
Validate lockstring syntactically, without saving it.
|
||||
|
||||
Args:
|
||||
lockstring (str): Lockstring to validate.
|
||||
Returns:
|
||||
valid (bool): If validation passed or not.
|
||||
|
||||
"""
|
||||
return self.add(lockstring, validate_only=True)
|
||||
|
||||
def replace(self, lockstring):
|
||||
"""
|
||||
Replaces the lockstring entirely.
|
||||
|
|
@ -421,6 +455,28 @@ class LockHandler(object):
|
|||
self._cache_locks(self.obj.lock_storage)
|
||||
self.cache_lock_bypass(self.obj)
|
||||
|
||||
def append(self, access_type, lockstring, op='or'):
|
||||
"""
|
||||
Append a lock definition to access_type if it doesn't already exist.
|
||||
|
||||
Args:
|
||||
access_type (str): Access type.
|
||||
lockstring (str): A valid lockstring, without the operator to
|
||||
link it to an eventual existing lockstring.
|
||||
op (str): An operator 'and', 'or', 'and not', 'or not' used
|
||||
for appending the lockstring to an existing access-type.
|
||||
Note:
|
||||
The most common use of this method is for use in commands where
|
||||
the user can specify their own lockstrings. This method allows
|
||||
the system to auto-add things like Admin-override access.
|
||||
|
||||
"""
|
||||
old_lockstring = self.get(access_type)
|
||||
if not lockstring.strip().lower() in old_lockstring.lower():
|
||||
lockstring = "{old} {op} {new}".format(
|
||||
old=old_lockstring, op=op, new=lockstring.strip())
|
||||
self.add(lockstring)
|
||||
|
||||
def check(self, accessing_obj, access_type, default=False, no_superuser_bypass=False):
|
||||
"""
|
||||
Checks a lock of the correct type by passing execution off to
|
||||
|
|
@ -459,9 +515,13 @@ class LockHandler(object):
|
|||
return True
|
||||
except AttributeError:
|
||||
# happens before session is initiated.
|
||||
if not no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
|
||||
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or
|
||||
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
|
||||
if not no_superuser_bypass and (
|
||||
(hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
|
||||
(hasattr(accessing_obj, 'account') and
|
||||
hasattr(accessing_obj.account, 'is_superuser') and
|
||||
accessing_obj.account.is_superuser) or
|
||||
(hasattr(accessing_obj, 'get_account') and
|
||||
(not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
|
||||
return True
|
||||
|
||||
# no superuser or bypass -> normal lock operation
|
||||
|
|
@ -469,7 +529,8 @@ class LockHandler(object):
|
|||
# we have a lock, test it.
|
||||
evalstring, func_tup, raw_string = self.locks[access_type]
|
||||
# execute all lock funcs in the correct order, producing a tuple of True/False results.
|
||||
true_false = tuple(bool(tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
|
||||
true_false = tuple(bool(
|
||||
tup[0](accessing_obj, self.obj, *tup[1], **tup[2])) for tup in func_tup)
|
||||
# the True/False tuple goes into evalstring, which combines them
|
||||
# with AND/OR/NOT in order to get the final result.
|
||||
return eval(evalstring % true_false)
|
||||
|
|
@ -520,9 +581,13 @@ class LockHandler(object):
|
|||
if accessing_obj.locks.lock_bypass and not no_superuser_bypass:
|
||||
return True
|
||||
except AttributeError:
|
||||
if no_superuser_bypass and ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
|
||||
(hasattr(accessing_obj, 'account') and hasattr(accessing_obj.account, 'is_superuser') and accessing_obj.account.is_superuser) or
|
||||
(hasattr(accessing_obj, 'get_account') and (not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
|
||||
if no_superuser_bypass and (
|
||||
(hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) or
|
||||
(hasattr(accessing_obj, 'account') and
|
||||
hasattr(accessing_obj.account, 'is_superuser') and
|
||||
accessing_obj.account.is_superuser) or
|
||||
(hasattr(accessing_obj, 'get_account') and
|
||||
(not accessing_obj.get_account() or accessing_obj.get_account().is_superuser))):
|
||||
return True
|
||||
if ":" not in lockstring:
|
||||
lockstring = "%s:%s" % ("_dummy", lockstring)
|
||||
|
|
@ -538,7 +603,77 @@ class LockHandler(object):
|
|||
else:
|
||||
# if no access types was given and multiple locks were
|
||||
# embedded in the lockstring we assume all must be true
|
||||
return all(self._eval_access_type(accessing_obj, locks, access_type) for access_type in locks)
|
||||
return all(self._eval_access_type(
|
||||
accessing_obj, locks, access_type) for access_type in locks)
|
||||
|
||||
|
||||
# convenience access function
|
||||
|
||||
# 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():
|
||||
|
|
|
|||
|
|
@ -437,7 +437,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
"""
|
||||
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
|
||||
specifically to this method.
|
||||
specifically to this method. Object contents will not be copied.
|
||||
|
||||
Args:
|
||||
original_object (Object): The object to make a copy from.
|
||||
|
|
@ -502,6 +502,10 @@ class ObjectDBManager(TypedObjectManager):
|
|||
for script in original_object.scripts.all():
|
||||
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
|
||||
|
||||
def clear_all_sessids(self):
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ from evennia.commands.cmdsethandler import CmdSetHandler
|
|||
from evennia.commands import cmdhandler
|
||||
from evennia.utils import search
|
||||
from evennia.utils import logger
|
||||
from evennia.utils import ansi
|
||||
from evennia.utils.utils import (variable_from_module, lazy_property,
|
||||
make_iter, to_unicode, is_iter, list_to_string,
|
||||
to_str)
|
||||
|
|
@ -305,12 +306,13 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
|||
count (int): Number of objects of this type
|
||||
looker (Object): Onlooker. Not used by default.
|
||||
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:
|
||||
singular (str): The singular form to display.
|
||||
plural (str): The determined plural form of the key, including the count.
|
||||
"""
|
||||
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 = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
|
||||
singular = _INFLECT.an(key)
|
||||
|
|
@ -569,17 +571,19 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
|||
except Exception:
|
||||
logger.log_trace()
|
||||
|
||||
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||
# sanitize text before sending across the wire
|
||||
try:
|
||||
text = to_str(text, force_string=True)
|
||||
except Exception:
|
||||
text = repr(text)
|
||||
if text is not None:
|
||||
if not (isinstance(text, basestring) or isinstance(text, tuple)):
|
||||
# sanitize text before sending across the wire
|
||||
try:
|
||||
text = to_str(text, force_string=True)
|
||||
except Exception:
|
||||
text = repr(text)
|
||||
kwargs['text'] = text
|
||||
|
||||
# relay to session(s)
|
||||
sessions = make_iter(session) if session else self.sessions.all()
|
||||
for session in sessions:
|
||||
session.data_out(text=text, **kwargs)
|
||||
session.data_out(**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)
|
||||
self.at_after_move(None)
|
||||
if cdict.get("tags"):
|
||||
# this should be a list of tags
|
||||
# this should be a list of tags, tuples (key, category) or (key, category, data)
|
||||
self.tags.batch_add(*cdict["tags"])
|
||||
if cdict.get("attributes"):
|
||||
# this should be a dict of attrname:value
|
||||
# this should be tuples (key, val, ...)
|
||||
self.attributes.batch_add(*cdict["attributes"])
|
||||
if cdict.get("nattributes"):
|
||||
# this should be a dict of nattrname:value
|
||||
for key, value in cdict["nattributes"].items():
|
||||
for key, value in cdict["nattributes"]:
|
||||
self.nattributes.add(key, value)
|
||||
|
||||
del self._createdict
|
||||
|
|
@ -1751,6 +1755,7 @@ class DefaultObject(with_metaclass(TypeclassBase, ObjectDB)):
|
|||
else:
|
||||
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
|
||||
msg_location = msg_location or '{object} says, "{speech}"'
|
||||
msg_receivers = msg_receivers or message
|
||||
|
||||
custom_mapping = kwargs.get('mapping', {})
|
||||
receivers = make_iter(receivers) if receivers else None
|
||||
|
|
@ -1873,7 +1878,7 @@ class DefaultCharacter(DefaultObject):
|
|||
|
||||
"""
|
||||
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):
|
||||
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"]
|
||||
|
||||
|
||||
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):
|
||||
"""
|
||||
LoopingCall that can start at a delay different
|
||||
|
|
@ -141,15 +152,6 @@ class ScriptBase(with_metaclass(TypeclassBase, ScriptDB)):
|
|||
"""
|
||||
objects = ScriptManager()
|
||||
|
||||
|
||||
class DefaultScript(ScriptBase):
|
||||
"""
|
||||
This is the base TypeClass for all Scripts. Scripts describe
|
||||
events, timers and states in game, they can have a time component
|
||||
or describe a state that changes under certain conditions.
|
||||
|
||||
"""
|
||||
|
||||
def __eq__(self, other):
|
||||
"""
|
||||
Compares two Scripts. Compares dbids.
|
||||
|
|
@ -239,7 +241,96 @@ class DefaultScript(ScriptBase):
|
|||
logger.log_trace()
|
||||
return None
|
||||
|
||||
# Public methods
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Should be overridden in child.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def at_first_save(self, **kwargs):
|
||||
"""
|
||||
This is called after very first time this object is saved.
|
||||
Generally, you don't need to overload this, but only the hooks
|
||||
called by this method.
|
||||
|
||||
Args:
|
||||
**kwargs (dict): Arbitrary, optional arguments for users
|
||||
overriding the call (unused by default).
|
||||
|
||||
"""
|
||||
self.at_script_creation()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create_script
|
||||
# function was used to create the object. We want
|
||||
# the create call's kwargs to override the values
|
||||
# set by hooks.
|
||||
cdict = self._createdict
|
||||
updates = []
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#%i" % self.dbid
|
||||
updates.append("db_key")
|
||||
elif self.db_key != cdict["key"]:
|
||||
self.db_key = cdict["key"]
|
||||
updates.append("db_key")
|
||||
if cdict.get("interval") and self.interval != cdict["interval"]:
|
||||
self.db_interval = cdict["interval"]
|
||||
updates.append("db_interval")
|
||||
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
|
||||
self.db_start_delay = cdict["start_delay"]
|
||||
updates.append("db_start_delay")
|
||||
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
|
||||
self.db_repeats = cdict["repeats"]
|
||||
updates.append("db_repeats")
|
||||
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
|
||||
self.db_persistent = cdict["persistent"]
|
||||
updates.append("db_persistent")
|
||||
if cdict.get("desc") and self.desc != cdict["desc"]:
|
||||
self.db_desc = cdict["desc"]
|
||||
updates.append("db_desc")
|
||||
if updates:
|
||||
self.save(update_fields=updates)
|
||||
|
||||
if cdict.get("permissions"):
|
||||
self.permissions.batch_add(*cdict["permissions"])
|
||||
if cdict.get("locks"):
|
||||
self.locks.add(cdict["locks"])
|
||||
if cdict.get("tags"):
|
||||
# this should be a list of tags, tuples (key, category) or (key, category, data)
|
||||
self.tags.batch_add(*cdict["tags"])
|
||||
if cdict.get("attributes"):
|
||||
# this should be tuples (key, val, ...)
|
||||
self.attributes.batch_add(*cdict["attributes"])
|
||||
if cdict.get("nattributes"):
|
||||
# this should be a dict of nattrname:value
|
||||
for key, value in cdict["nattributes"]:
|
||||
self.nattributes.add(key, value)
|
||||
|
||||
if not cdict.get("autostart"):
|
||||
# don't auto-start the script
|
||||
return
|
||||
|
||||
# auto-start script (default)
|
||||
self.start()
|
||||
|
||||
|
||||
class DefaultScript(ScriptBase):
|
||||
"""
|
||||
This is the base TypeClass for all Scripts. Scripts describe
|
||||
events, timers and states in game, they can have a time component
|
||||
or describe a state that changes under certain conditions.
|
||||
|
||||
"""
|
||||
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Only called once, when script is first created.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
def time_until_next_repeat(self):
|
||||
"""
|
||||
|
|
@ -278,6 +369,27 @@ class DefaultScript(ScriptBase):
|
|||
return max(0, self.db_repeats - task.callcount)
|
||||
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):
|
||||
"""
|
||||
Called every time the script is started (for persistent
|
||||
|
|
@ -294,9 +406,19 @@ class DefaultScript(ScriptBase):
|
|||
started or not. Used in counting.
|
||||
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
obj = self.obj
|
||||
|
|
@ -472,61 +594,6 @@ class DefaultScript(ScriptBase):
|
|||
if task:
|
||||
task.force_repeat()
|
||||
|
||||
def at_first_save(self, **kwargs):
|
||||
"""
|
||||
This is called after very first time this object is saved.
|
||||
Generally, you don't need to overload this, but only the hooks
|
||||
called by this method.
|
||||
|
||||
Args:
|
||||
**kwargs (dict): Arbitrary, optional arguments for users
|
||||
overriding the call (unused by default).
|
||||
|
||||
"""
|
||||
self.at_script_creation()
|
||||
|
||||
if hasattr(self, "_createdict"):
|
||||
# this will only be set if the utils.create_script
|
||||
# function was used to create the object. We want
|
||||
# the create call's kwargs to override the values
|
||||
# set by hooks.
|
||||
cdict = self._createdict
|
||||
updates = []
|
||||
if not cdict.get("key"):
|
||||
if not self.db_key:
|
||||
self.db_key = "#%i" % self.dbid
|
||||
updates.append("db_key")
|
||||
elif self.db_key != cdict["key"]:
|
||||
self.db_key = cdict["key"]
|
||||
updates.append("db_key")
|
||||
if cdict.get("interval") and self.interval != cdict["interval"]:
|
||||
self.db_interval = cdict["interval"]
|
||||
updates.append("db_interval")
|
||||
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
|
||||
self.db_start_delay = cdict["start_delay"]
|
||||
updates.append("db_start_delay")
|
||||
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
|
||||
self.db_repeats = cdict["repeats"]
|
||||
updates.append("db_repeats")
|
||||
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
|
||||
self.db_persistent = cdict["persistent"]
|
||||
updates.append("db_persistent")
|
||||
if updates:
|
||||
self.save(update_fields=updates)
|
||||
if not cdict.get("autostart"):
|
||||
# don't auto-start the script
|
||||
return
|
||||
|
||||
# auto-start script (default)
|
||||
self.start()
|
||||
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Only called once, by the create function.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
Is called to check if the script is valid to run at this time.
|
||||
|
|
|
|||
|
|
@ -51,7 +51,7 @@ class AMPClientFactory(protocol.ReconnectingClientFactory):
|
|||
|
||||
def buildProtocol(self, addr):
|
||||
"""
|
||||
Creates an AMPProtocol instance when connecting to the server.
|
||||
Creates an AMPProtocol instance when connecting to the AMP server.
|
||||
|
||||
Args:
|
||||
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)
|
||||
self.send_AdminServer2Portal(
|
||||
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):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -467,9 +467,10 @@ ARG_OPTIONS = \
|
|||
stop - shutdown server+portal
|
||||
reboot - shutdown server+portal, then start again
|
||||
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)
|
||||
skill = send kill signal only to server
|
||||
skill - send kill signal only to server
|
||||
status - show server and portal run state
|
||||
info - show server and portal port info
|
||||
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)
|
||||
|
||||
|
||||
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)
|
||||
|
||||
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):
|
||||
print("... Server stopped.")
|
||||
_reactor_stop()
|
||||
if when_stopped:
|
||||
when_stopped()
|
||||
else:
|
||||
print("... Server stopped.")
|
||||
_reactor_stop()
|
||||
|
||||
def _portal_running(response):
|
||||
_, srun, _, _, _, _ = _parse_status(response)
|
||||
|
|
@ -971,8 +997,11 @@ def stop_server_only():
|
|||
wait_for_status_reply(_server_stopped)
|
||||
send_instruction(SSHUTD, {})
|
||||
else:
|
||||
print("Server is not running.")
|
||||
_reactor_stop()
|
||||
if when_stopped:
|
||||
when_stopped()
|
||||
else:
|
||||
print("Server is not running.")
|
||||
_reactor_stop()
|
||||
|
||||
def _portal_not_running(fail):
|
||||
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))
|
||||
|
||||
if new_linecount < old_linecount:
|
||||
# this could happen if the file was manually deleted or edited
|
||||
print("Log file has shrunk. Restart log reader.")
|
||||
sys.exit()
|
||||
# this happens if the file was cycled or manually deleted/edited.
|
||||
print(" ** Log file {filename} has cycled or been edited. "
|
||||
"Restarting log. ".format(filehandle.name))
|
||||
new_linecount = 0
|
||||
old_linecount = 0
|
||||
|
||||
lines_to_get = max(0, new_linecount - old_linecount)
|
||||
|
||||
|
|
@ -1935,7 +1966,7 @@ def main():
|
|||
# launch menu for operation
|
||||
init_game_directory(CURRENT_DIR, check_db=True)
|
||||
run_menu()
|
||||
elif option in ('status', 'info', 'start', 'reload', 'reboot',
|
||||
elif option in ('status', 'info', 'start', 'istart', 'reload', 'reboot',
|
||||
'reset', 'stop', 'sstop', 'kill', 'skill'):
|
||||
# operate the server directly
|
||||
if not SERVER_LOGFILE:
|
||||
|
|
@ -1946,6 +1977,8 @@ def main():
|
|||
query_info()
|
||||
elif option == "start":
|
||||
start_evennia(args.profiler, args.profiler)
|
||||
elif option == "istart":
|
||||
start_server_interactive()
|
||||
elif option == 'reload':
|
||||
reload_evennia(args.profiler)
|
||||
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.
|
||||
# This object is pure django and only holds name, email and password.
|
||||
|
|
@ -121,7 +121,7 @@ def create_channels():
|
|||
Creates some sensible default channels.
|
||||
|
||||
"""
|
||||
logger.log_info("Creating default channels ...")
|
||||
logger.log_info("Initial setup: Creating default channels ...")
|
||||
|
||||
goduser = get_god_account()
|
||||
for channeldict in settings.DEFAULT_CHANNELS:
|
||||
|
|
@ -144,11 +144,21 @@ def at_initial_setup():
|
|||
mod = __import__(modname, fromlist=[None])
|
||||
except (ImportError, ValueError):
|
||||
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):
|
||||
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():
|
||||
"""
|
||||
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())
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
logger.log_info(" Initial setup complete. Restarting Server once.")
|
||||
SESSIONS.server.shutdown(mode='reset')
|
||||
logger.log_info("Initial setup complete. Restarting Server once.")
|
||||
SESSIONS.portal_reset_server()
|
||||
|
||||
|
||||
def handle_setup(last_step):
|
||||
|
|
@ -186,6 +196,7 @@ def handle_setup(last_step):
|
|||
setup_queue = [create_objects,
|
||||
create_channels,
|
||||
at_initial_setup,
|
||||
collectstatic,
|
||||
reset_server]
|
||||
|
||||
# step through queue, from last completed function
|
||||
|
|
|
|||
|
|
@ -44,9 +44,10 @@ SSHUTD = chr(17) # server shutdown
|
|||
PSTATUS = chr(18) # ping server or portal status
|
||||
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)
|
||||
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
|
||||
_SENDBATCH = defaultdict(list)
|
||||
|
|
@ -61,11 +62,15 @@ _HTTP_WARNING = """
|
|||
HTTP/1.1 200 OK
|
||||
Content-Type: text/html
|
||||
|
||||
<html><body>
|
||||
This is Evennia's interal AMP port. It handles communication
|
||||
between Evennia's different processes.<h3><p>This port should NOT be
|
||||
publicly visible.</p></h3>
|
||||
</body></html>""".strip()
|
||||
<html>
|
||||
<body>
|
||||
This is Evennia's internal AMP port. It handles communication
|
||||
between Evennia's different processes.
|
||||
<p>
|
||||
<h3>This port should NOT be publicly visible.</h3>
|
||||
</p>
|
||||
</body>
|
||||
</html>""".strip()
|
||||
|
||||
|
||||
# Helper functions for pickling.
|
||||
|
|
@ -107,43 +112,45 @@ class Compressed(amp.String):
|
|||
|
||||
def fromBox(self, name, strings, objects, proto):
|
||||
"""
|
||||
Converts from box representation to python. We
|
||||
group very long data into batches.
|
||||
Converts from box string representation to python. We read back too-long batched data and
|
||||
put it back together here.
|
||||
|
||||
"""
|
||||
value = StringIO()
|
||||
value.write(strings.get(name))
|
||||
value.write(self.fromStringProto(strings.get(name), proto))
|
||||
for counter in count(2):
|
||||
# count from 2 upwards
|
||||
chunk = strings.get("%s.%d" % (name, counter))
|
||||
if chunk is None:
|
||||
break
|
||||
value.write(chunk)
|
||||
value.write(self.fromStringProto(chunk, proto))
|
||||
objects[name] = value.getvalue()
|
||||
|
||||
def toBox(self, name, strings, objects, proto):
|
||||
"""
|
||||
Convert from data to box. We handled too-long
|
||||
batched data and put it together here.
|
||||
Convert from python object to string box representation.
|
||||
we break up too-long data snippets into multiple batches here.
|
||||
|
||||
"""
|
||||
value = StringIO(objects[name])
|
||||
strings[name] = value.read(AMP_MAXLEN)
|
||||
strings[name] = self.toStringProto(value.read(AMP_MAXLEN), proto)
|
||||
for counter in count(2):
|
||||
chunk = value.read(AMP_MAXLEN)
|
||||
if not chunk:
|
||||
break
|
||||
strings["%s.%d" % (name, counter)] = chunk
|
||||
strings["%s.%d" % (name, counter)] = self.toStringProto(chunk, proto)
|
||||
|
||||
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):
|
||||
"""
|
||||
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):
|
||||
|
|
@ -261,16 +268,29 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
self.send_reset_time = time.time()
|
||||
self.send_mode = True
|
||||
self.send_task = None
|
||||
self.multibatches = 0
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
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.loseConnection()
|
||||
else:
|
||||
super(AMPMultiConnectionProtocol, self).dataReceived(data)
|
||||
print("HTML received: %s" % data)
|
||||
|
||||
def makeConnection(self, transport):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -356,10 +356,13 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
|||
packed_data (str): Pickled data (sessid, kwargs) coming over the wire.
|
||||
|
||||
"""
|
||||
sessid, kwargs = self.data_in(packed_data)
|
||||
session = self.factory.portal.sessions.get(sessid, None)
|
||||
if session:
|
||||
self.factory.portal.sessions.data_out(session, **kwargs)
|
||||
try:
|
||||
sessid, kwargs = self.data_in(packed_data)
|
||||
session = self.factory.portal.sessions.get(sessid, None)
|
||||
if session:
|
||||
self.factory.portal.sessions.data_out(session, **kwargs)
|
||||
except Exception:
|
||||
logger.log_trace("packed_data len {}".format(len(packed_data)))
|
||||
return {}
|
||||
|
||||
@amp.AdminServer2Portal.responder
|
||||
|
|
|
|||
|
|
@ -343,7 +343,7 @@ if WEBSERVER_ENABLED:
|
|||
proxy_service = internet.TCPServer(proxyport,
|
||||
web_root,
|
||||
interface=interface)
|
||||
proxy_service.setName('EvenniaWebProxy%s' % pstring)
|
||||
proxy_service.setName('EvenniaWebProxy%s:%s' % (ifacestr, proxyport))
|
||||
PORTAL.services.addService(proxy_service)
|
||||
INFO_DICT["webserver_proxy"].append("webserver-proxy%s: %s" % (ifacestr, proxyport))
|
||||
INFO_DICT["webserver_internal"].append("webserver: %s" % serverport)
|
||||
|
|
|
|||
|
|
@ -116,7 +116,7 @@ class Ttype(object):
|
|||
self.protocol.protocol_flags["FORCEDENDLINE"] = False
|
||||
|
||||
if cupper.startswith("TINTIN++"):
|
||||
self.protocol.protocol_flags["FORCEDENDLINE"] = False
|
||||
self.protocol.protocol_flags["FORCEDENDLINE"] = True
|
||||
|
||||
if (cupper.startswith("XTERM") or
|
||||
cupper.endswith("-256COLOR") or
|
||||
|
|
|
|||
|
|
@ -181,9 +181,6 @@ class Evennia(object):
|
|||
|
||||
self.start_time = time.time()
|
||||
|
||||
# Run the initial setup if needed
|
||||
self.run_initial_setup()
|
||||
|
||||
# initialize channelhandler
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
|
|
@ -274,6 +271,8 @@ class Evennia(object):
|
|||
|
||||
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.
|
||||
It returns if this is not the first time the server starts.
|
||||
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.
|
||||
application = service.Application('Evennia')
|
||||
|
||||
# custom logging
|
||||
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
|
||||
os.path.dirname(settings.SERVER_LOG_FILE))
|
||||
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
|
||||
if "--nodaemon" not in sys.argv:
|
||||
# custom logging, but only if we are not running in interactive mode
|
||||
logfile = logger.WeeklyLogFile(os.path.basename(settings.SERVER_LOG_FILE),
|
||||
os.path.dirname(settings.SERVER_LOG_FILE))
|
||||
application.setComponent(ILogObserver, logger.ServerLogObserver(logfile).emit)
|
||||
|
||||
# The main evennia server program. This sets up the database
|
||||
# and is where we store all the other services.
|
||||
|
|
|
|||
|
|
@ -278,7 +278,7 @@ class ServerSessionHandler(SessionHandler):
|
|||
|
||||
"""
|
||||
super(ServerSessionHandler, self).__init__(*args, **kwargs)
|
||||
self.server = None
|
||||
self.server = None # set at server initialization
|
||||
self.server_data = {"servername": _SERVERNAME}
|
||||
|
||||
def _run_cmd_login(self, session):
|
||||
|
|
@ -290,7 +290,6 @@ class ServerSessionHandler(SessionHandler):
|
|||
if not session.logged_in:
|
||||
self.data_in(session, text=[[CMD_LOGINSTART], {}])
|
||||
|
||||
|
||||
def portal_connect(self, portalsessiondata):
|
||||
"""
|
||||
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
|
||||
# Number of lines to append to rotating channel logs when they rotate
|
||||
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
|
||||
# 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
|
||||
|
|
@ -354,6 +354,9 @@ LOCK_FUNC_MODULES = ("evennia.locks.lockfuncs", "server.conf.lockfuncs",)
|
|||
INPUT_FUNC_MODULES = ["evennia.server.inputfuncs", "server.conf.inputfuncs"]
|
||||
# Modules that contain prototypes for use with the spawner mechanism.
|
||||
PROTOTYPE_MODULES = ["world.prototypes"]
|
||||
# Modules containining Prototype functions able to be embedded in prototype
|
||||
# definitions from in-game.
|
||||
PROT_FUNC_MODULES = ["evennia.prototypes.protfuncs"]
|
||||
# Module holding settings/actions for the dummyrunner program (see the
|
||||
# dummyrunner for more information)
|
||||
DUMMYRUNNER_SETTINGS_MODULE = "evennia.server.profiling.dummyrunner_settings"
|
||||
|
|
@ -513,7 +516,7 @@ TIME_GAME_EPOCH = None
|
|||
TIME_IGNORE_DOWNTIMES = False
|
||||
|
||||
######################################################################
|
||||
# Inlinefunc
|
||||
# Inlinefunc & PrototypeFuncs
|
||||
######################################################################
|
||||
# Evennia supports inline function preprocessing. This allows users
|
||||
# to supply inline calls on the form $func(arg, arg, ...) to do
|
||||
|
|
@ -525,6 +528,10 @@ INLINEFUNC_ENABLED = False
|
|||
# is loaded from left-to-right, same-named functions will overload
|
||||
INLINEFUNC_MODULES = ["evennia.utils.inlinefuncs",
|
||||
"server.conf.inlinefuncs"]
|
||||
# Module holding handlers for OLCFuncs. These allow for embedding
|
||||
# functional code in prototypes
|
||||
PROTOTYPEFUNC_MODULES = ["evennia.utils.prototypefuncs",
|
||||
"server.conf.prototypefuncs"]
|
||||
|
||||
######################################################################
|
||||
# Default Account setup and access
|
||||
|
|
|
|||
|
|
@ -435,6 +435,7 @@ class AttributeHandler(object):
|
|||
def __init__(self):
|
||||
self.key = None
|
||||
self.value = default
|
||||
self.category = None
|
||||
self.strvalue = str(default) if default is not None else None
|
||||
|
||||
ret = []
|
||||
|
|
@ -530,8 +531,8 @@ class AttributeHandler(object):
|
|||
repeat-calling add when having many Attributes to add.
|
||||
|
||||
Args:
|
||||
indata (tuple): Tuples of varying length representing the
|
||||
Attribute to add to this object.
|
||||
indata (list): List of tuples of varying length representing the
|
||||
Attribute to add to this object. Supported tuples are
|
||||
- `(key, value)`
|
||||
- `(key, value, category)`
|
||||
- `(key, value, category, lockstring)`
|
||||
|
|
|
|||
|
|
@ -653,6 +653,42 @@ class TypeclassManager(TypedObjectManager):
|
|||
"""
|
||||
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):
|
||||
"""
|
||||
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,
|
||||
permissions=None, locks=None, aliases=None, tags=None,
|
||||
destination=None, report_to=None, nohome=False):
|
||||
destination=None, report_to=None, nohome=False, attributes=None,
|
||||
nattributes=None):
|
||||
"""
|
||||
|
||||
Create a new in-game object.
|
||||
|
|
@ -68,13 +69,18 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
permissions (list): A list of permission strings or tuples (permstring, category).
|
||||
locks (str): one or more lockstrings, separated by semicolons.
|
||||
aliases (list): A list of alternative keys or tuples (aliasstring, category).
|
||||
tags (list): List of tag keys or tuples (tagkey, category).
|
||||
tags (list): List of tag keys or tuples (tagkey, category) or (tagkey, category, data).
|
||||
destination (Object or str): Obj or #dbref to use as an Exit's
|
||||
target.
|
||||
report_to (Object): The object to return error messages to.
|
||||
nohome (bool): This allows the creation of objects without a
|
||||
default home location; only used when creating the default
|
||||
location itself or during unittests.
|
||||
attributes (list): Tuples on the form (key, value) or (key, value, category),
|
||||
(key, value, lockstring) or (key, value, lockstring, default_access).
|
||||
to set as Attributes on the new object.
|
||||
nattributes (list): Non-persistent tuples on the form (key, value). Note that
|
||||
adding this rarely makes sense since this data will not survive a reload.
|
||||
|
||||
Returns:
|
||||
object (Object): A newly created object of the given typeclass.
|
||||
|
|
@ -95,6 +101,7 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
locks = make_iter(locks) if locks is not None else None
|
||||
aliases = make_iter(aliases) if aliases is not None else None
|
||||
tags = make_iter(tags) if tags is not None else None
|
||||
attributes = make_iter(attributes) if attributes is not None else None
|
||||
|
||||
|
||||
if isinstance(typeclass, basestring):
|
||||
|
|
@ -122,7 +129,8 @@ def create_object(typeclass=None, key=None, location=None, home=None,
|
|||
# store the call signature for the signal
|
||||
new_object._createdict = dict(key=key, location=location, destination=destination, home=home,
|
||||
typeclass=typeclass.path, permissions=permissions, locks=locks,
|
||||
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome)
|
||||
aliases=aliases, tags=tags, report_to=report_to, nohome=nohome,
|
||||
attributes=attributes, nattributes=nattributes)
|
||||
# this will trigger the save signal which in turn calls the
|
||||
# at_first_save hook on the typeclass, where the _createdict can be
|
||||
# used.
|
||||
|
|
@ -139,7 +147,8 @@ object = create_object
|
|||
|
||||
def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
||||
interval=None, start_delay=None, repeats=None,
|
||||
persistent=None, autostart=True, report_to=None, desc=None):
|
||||
persistent=None, autostart=True, report_to=None, desc=None,
|
||||
tags=None, attributes=None):
|
||||
"""
|
||||
Create a new script. All scripts are a combination of a database
|
||||
object that communicates with the database, and an typeclass that
|
||||
|
|
@ -169,7 +178,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
created or if the `start` method must be called explicitly.
|
||||
report_to (Object): The object to return error messages to.
|
||||
desc (str): Optional description of script
|
||||
|
||||
tags (list): List of tags or tuples (tag, category).
|
||||
attributes (list): List if tuples (key, value) or (key, value, category)
|
||||
(key, value, lockstring) or (key, value, lockstring, default_access).
|
||||
|
||||
See evennia.scripts.manager for methods to manipulate existing
|
||||
scripts in the database.
|
||||
|
|
@ -190,9 +201,9 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
if key:
|
||||
kwarg["db_key"] = key
|
||||
if account:
|
||||
kwarg["db_account"] = dbid_to_obj(account, _ScriptDB)
|
||||
kwarg["db_account"] = dbid_to_obj(account, _AccountDB)
|
||||
if obj:
|
||||
kwarg["db_obj"] = dbid_to_obj(obj, _ScriptDB)
|
||||
kwarg["db_obj"] = dbid_to_obj(obj, _ObjectDB)
|
||||
if interval:
|
||||
kwarg["db_interval"] = interval
|
||||
if start_delay:
|
||||
|
|
@ -203,6 +214,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
kwarg["db_persistent"] = persistent
|
||||
if desc:
|
||||
kwarg["db_desc"] = desc
|
||||
tags = make_iter(tags) if tags is not None else None
|
||||
attributes = make_iter(attributes) if attributes is not None else None
|
||||
|
||||
# create new instance
|
||||
new_script = typeclass(**kwarg)
|
||||
|
|
@ -210,7 +223,8 @@ def create_script(typeclass=None, key=None, obj=None, account=None, locks=None,
|
|||
# store the call signature for the signal
|
||||
new_script._createdict = dict(key=key, obj=obj, account=account, locks=locks, interval=interval,
|
||||
start_delay=start_delay, repeats=repeats, persistent=persistent,
|
||||
autostart=autostart, report_to=report_to)
|
||||
autostart=autostart, report_to=report_to, desc=desc,
|
||||
tags=tags, attributes=attributes)
|
||||
# this will trigger the save signal which in turn calls the
|
||||
# at_first_save hook on the typeclass, where the _createdict
|
||||
# can be used.
|
||||
|
|
|
|||
|
|
@ -237,10 +237,13 @@ class _SaverList(_SaverMutable, MutableSequence):
|
|||
self._data = list()
|
||||
|
||||
@_save
|
||||
def __add__(self, otherlist):
|
||||
def __iadd__(self, otherlist):
|
||||
self._data = self._data.__add__(otherlist)
|
||||
return self._data
|
||||
|
||||
def __add__(self, otherlist):
|
||||
return list(self._data) + otherlist
|
||||
|
||||
@_save
|
||||
def insert(self, index, 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):
|
||||
# code
|
||||
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
|
||||
command entered by the user on the *previous* node (the command
|
||||
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
|
||||
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
|
||||
`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.
|
||||
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,
|
||||
(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
|
||||
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
|
||||
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.
|
||||
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
|
||||
its number 1..N.
|
||||
|
|
@ -158,16 +165,16 @@ evennia.utils.evmenu`.
|
|||
"""
|
||||
from __future__ import print_function
|
||||
import random
|
||||
import inspect
|
||||
from builtins import object, range
|
||||
|
||||
from textwrap import dedent
|
||||
from inspect import isfunction, getargspec
|
||||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.ansi import strip_ansi
|
||||
from evennia.utils.utils import mod_import, make_iter, pad, m_len
|
||||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||
from evennia.commands import cmdhandler
|
||||
|
||||
# read from protocol NAWS later?
|
||||
|
|
@ -182,7 +189,8 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
|
|||
|
||||
# i18n
|
||||
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_NO_OPTION_DESC = _("No description.")
|
||||
_HELP_FULL = _("Commands: <menu option>, help, quit")
|
||||
|
|
@ -315,7 +323,7 @@ class EvMenu(object):
|
|||
auto_quit=True, auto_look=True, auto_help=True,
|
||||
cmd_on_exit="look",
|
||||
persistent=False, startnode_input="", session=None,
|
||||
**kwargs):
|
||||
debug=False, **kwargs):
|
||||
"""
|
||||
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
|
||||
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
|
||||
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
|
||||
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,
|
||||
|
|
@ -378,6 +387,10 @@ class EvMenu(object):
|
|||
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
|
||||
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:
|
||||
any (any): All kwargs will become initialization variables on `caller.ndb._menutree`,
|
||||
|
|
@ -401,7 +414,7 @@ class EvMenu(object):
|
|||
"""
|
||||
self._startnode = startnode
|
||||
self._menutree = self._parse_menudata(menudata)
|
||||
self._persistent = persistent
|
||||
self._persistent = persistent if not debug else False
|
||||
self._quitting = False
|
||||
|
||||
if startnode not in self._menutree:
|
||||
|
|
@ -415,6 +428,7 @@ class EvMenu(object):
|
|||
self.auto_quit = auto_quit
|
||||
self.auto_look = auto_look
|
||||
self.auto_help = auto_help
|
||||
self.debug_mode = debug
|
||||
self._session = session
|
||||
if isinstance(cmd_on_exit, str):
|
||||
# At this point menu._session will have been replaced by the
|
||||
|
|
@ -573,6 +587,7 @@ class EvMenu(object):
|
|||
except EvMenuError:
|
||||
errmsg = _ERR_GENERAL.format(nodename=callback)
|
||||
self.caller.msg(errmsg, self._session)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
return ret
|
||||
|
|
@ -606,9 +621,11 @@ class EvMenu(object):
|
|||
nodetext, options = ret, None
|
||||
except KeyError:
|
||||
self.caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename), session=self._session)
|
||||
logger.log_trace()
|
||||
raise EvMenuError
|
||||
except Exception:
|
||||
self.caller.msg(_ERR_GENERAL.format(nodename=nodename), session=self._session)
|
||||
logger.log_trace()
|
||||
raise
|
||||
|
||||
# store options to make them easier to test
|
||||
|
|
@ -665,9 +682,49 @@ class EvMenu(object):
|
|||
|
||||
if isinstance(ret, basestring):
|
||||
# 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 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):
|
||||
"""
|
||||
Run a node by name, optionally dynamically generating that name first.
|
||||
|
|
@ -681,29 +738,6 @@ class EvMenu(object):
|
|||
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):
|
||||
# run the "goto" callable, if possible
|
||||
|
|
@ -714,6 +748,9 @@ class EvMenu(object):
|
|||
raise EvMenuError(
|
||||
"{}: goto callable must return str or (str, dict)".format(inp_nodename))
|
||||
nodename, kwargs = nodename[:2]
|
||||
if not nodename:
|
||||
# no nodename return. Re-run current node
|
||||
nodename = self.nodename
|
||||
try:
|
||||
# execute the found node, make use of the returns.
|
||||
nodetext, options = self._execute_node(nodename, raw_string, **kwargs)
|
||||
|
|
@ -746,12 +783,12 @@ class EvMenu(object):
|
|||
desc = dic.get("desc", dic.get("text", None))
|
||||
if "_default" in keys:
|
||||
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)
|
||||
else:
|
||||
# use the key (only) if set, otherwise use the running number
|
||||
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:
|
||||
display_options.append((keys[0], desc))
|
||||
for key in keys:
|
||||
|
|
@ -765,7 +802,7 @@ class EvMenu(object):
|
|||
|
||||
# handle the helptext
|
||||
if helptext:
|
||||
self.helptext = helptext
|
||||
self.helptext = self.helptext_formatter(helptext)
|
||||
elif options:
|
||||
self.helptext = _HELP_FULL if self.auto_quit else _HELP_NO_QUIT
|
||||
else:
|
||||
|
|
@ -814,6 +851,51 @@ class EvMenu(object):
|
|||
if self.cmd_on_exit is not None:
|
||||
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):
|
||||
"""
|
||||
Parses the incoming string from the menu user.
|
||||
|
|
@ -840,6 +922,8 @@ class EvMenu(object):
|
|||
self.display_helptext()
|
||||
elif self.auto_quit and cmd in ("quit", "q", "exit"):
|
||||
self.close_menu()
|
||||
elif self.debug_mode and cmd.startswith("menudebug"):
|
||||
self.print_debug_info(cmd[9:].strip())
|
||||
elif self.default:
|
||||
goto, goto_kwargs, execfunc, exec_kwargs = self.default
|
||||
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.
|
||||
|
||||
"""
|
||||
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):
|
||||
"""
|
||||
|
|
@ -945,14 +1042,188 @@ class EvMenu(object):
|
|||
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"))
|
||||
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 ""
|
||||
separator2 = "\n" + "_" * total_width + "\n\n" if total_width else ""
|
||||
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
|
||||
|
|
|
|||
|
|
@ -122,7 +122,8 @@ class EvMore(object):
|
|||
"""
|
||||
|
||||
def __init__(self, caller, text, always_page=False, session=None,
|
||||
justify_kwargs=None, exit_on_lastpage=False, **kwargs):
|
||||
justify_kwargs=None, exit_on_lastpage=False,
|
||||
exit_cmd=None, **kwargs):
|
||||
"""
|
||||
Initialization of the text handler.
|
||||
|
||||
|
|
@ -141,6 +142,10 @@ class EvMore(object):
|
|||
page being completely filled, exit pager immediately. If unset,
|
||||
another move forward is required to exit. If set, the pager
|
||||
exit message will not be shown.
|
||||
exit_cmd (str, optional): If given, this command-string will be executed on
|
||||
the caller when the more page exits. Note that this will be using whatever
|
||||
cmdset the user had *before* the evmore pager was activated (so none of
|
||||
the evmore commands will be available when this is run).
|
||||
kwargs (any, optional): These will be passed on
|
||||
to the `caller.msg` method.
|
||||
|
||||
|
|
@ -151,6 +156,7 @@ class EvMore(object):
|
|||
self._npages = []
|
||||
self._npos = []
|
||||
self.exit_on_lastpage = exit_on_lastpage
|
||||
self.exit_cmd = exit_cmd
|
||||
self._exit_msg = "Exited |wmore|n pager."
|
||||
if not session:
|
||||
# if not supplied, use the first session to
|
||||
|
|
@ -202,15 +208,18 @@ class EvMore(object):
|
|||
# goto top of the text
|
||||
self.page_top()
|
||||
|
||||
def display(self):
|
||||
def display(self, show_footer=True):
|
||||
"""
|
||||
Pretty-print the page.
|
||||
"""
|
||||
pos = self._pos
|
||||
text = self._pages[pos]
|
||||
page = _DISPLAY.format(text=text,
|
||||
pageno=pos + 1,
|
||||
pagemax=self._npages)
|
||||
if show_footer:
|
||||
page = _DISPLAY.format(text=text,
|
||||
pageno=pos + 1,
|
||||
pagemax=self._npages)
|
||||
else:
|
||||
page = text
|
||||
# check to make sure our session is still valid
|
||||
sessions = self._caller.sessions.get()
|
||||
if not sessions:
|
||||
|
|
@ -245,9 +254,11 @@ class EvMore(object):
|
|||
self.page_quit()
|
||||
else:
|
||||
self._pos += 1
|
||||
self.display()
|
||||
if self.exit_on_lastpage and self._pos >= self._npages - 1:
|
||||
self.page_quit()
|
||||
if self.exit_on_lastpage and self._pos >= (self._npages - 1):
|
||||
self.display(show_footer=False)
|
||||
self.page_quit(quiet=True)
|
||||
else:
|
||||
self.display()
|
||||
|
||||
def page_back(self):
|
||||
"""
|
||||
|
|
@ -256,16 +267,20 @@ class EvMore(object):
|
|||
self._pos = max(0, self._pos - 1)
|
||||
self.display()
|
||||
|
||||
def page_quit(self):
|
||||
def page_quit(self, quiet=False):
|
||||
"""
|
||||
Quit the pager
|
||||
"""
|
||||
del self._caller.ndb._more
|
||||
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
||||
if not quiet:
|
||||
self._caller.msg(text=self._exit_msg, **self._kwargs)
|
||||
self._caller.cmdset.remove(CmdSetMore)
|
||||
if self.exit_cmd:
|
||||
self._caller.execute_cmd(self.exit_cmd, session=self._session)
|
||||
|
||||
|
||||
def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, **kwargs):
|
||||
def msg(caller, text="", always_page=False, session=None,
|
||||
justify_kwargs=None, exit_on_lastpage=True, **kwargs):
|
||||
"""
|
||||
More-supported version of msg, mimicking the normal msg method.
|
||||
|
||||
|
|
@ -280,9 +295,10 @@ def msg(caller, text="", always_page=False, session=None, justify_kwargs=None, *
|
|||
justify_kwargs (dict, bool or None, optional): If given, this should
|
||||
be valid keyword arguments to the utils.justify() function. If False,
|
||||
no justification will be done.
|
||||
exit_on_lastpage (bool, optional): Immediately exit pager when reaching the last page.
|
||||
kwargs (any, optional): These will be passed on
|
||||
to the `caller.msg` method.
|
||||
|
||||
"""
|
||||
EvMore(caller, text, always_page=always_page, session=session,
|
||||
justify_kwargs=justify_kwargs, **kwargs)
|
||||
justify_kwargs=justify_kwargs, exit_on_lastpage=exit_on_lastpage, **kwargs)
|
||||
|
|
|
|||
|
|
@ -893,6 +893,9 @@ class EvColumn(object):
|
|||
|
||||
"""
|
||||
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)
|
||||
# use fixed width or adjust to the largest cell
|
||||
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]
|
||||
cwmin = sum(cwidths_min)
|
||||
|
||||
if cwmin > width:
|
||||
# we cannot shrink any more
|
||||
raise Exception("Cannot shrink table width to %s. Minimum size is %s." % (self.width, cwmin))
|
||||
# get which cols have separately set widths - these should be locked
|
||||
# note that we need to remove cwidths_min for each lock to avoid counting
|
||||
# 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:
|
||||
# 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
|
||||
ci = cwidths_min.index(min(cwidths_min))
|
||||
cwidths_min[ci] += 1
|
||||
ci = cwidths.index(min(cwidths))
|
||||
if ci in locked_cols:
|
||||
# locked column, make sure it's not picked again
|
||||
cwidths[ci] += 9999
|
||||
cwidths_min[ci] = locked_cols[ci]
|
||||
else:
|
||||
cwidths_min[ci] += 1
|
||||
correction += 1
|
||||
cwidths = cwidths_min
|
||||
else:
|
||||
# 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
|
||||
ci = cwidths.index(max(cwidths))
|
||||
cwidths_min[ci] += 1
|
||||
cwidths[ci] -= 3
|
||||
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
|
||||
# give a just changed col less prio next run
|
||||
cwidths[ci] -= 3
|
||||
cwidths = cwidths_min
|
||||
|
||||
# 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)]
|
||||
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:
|
||||
# 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.
|
||||
# We do this so that the tallest cells gets expanded first (and
|
||||
# thus avoid getting cropped)
|
||||
|
||||
excess = self.height - chmin
|
||||
even = self.height % 2 == 0
|
||||
for position in range(excess):
|
||||
correction = 0
|
||||
while correction < excess:
|
||||
# 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)
|
||||
ci = cheights[1:].index(max(cheights[1:])) + 1
|
||||
else:
|
||||
ci = cheights.index(max(cheights))
|
||||
cheights_min[ci] += 1
|
||||
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
|
||||
if ci in locked_cols:
|
||||
# locked row, make sure it's not picked again
|
||||
cheights[ci] -= 9999
|
||||
cheights_min[ci] = locked_cols[ci]
|
||||
else:
|
||||
cheights_min[ci] += 1
|
||||
# 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
|
||||
|
||||
# we must tell cells to crop instead of expanding
|
||||
|
|
@ -1554,6 +1609,8 @@ class EvTable(object):
|
|||
"""
|
||||
if index > len(self.table):
|
||||
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].reformat(**kwargs)
|
||||
|
||||
|
|
@ -1569,6 +1626,7 @@ class EvTable(object):
|
|||
|
||||
def __str__(self):
|
||||
"""print table (this also balances it)"""
|
||||
# h = "12345678901234567890123456789012345678901234567890123456789012345678901234567890"
|
||||
return str(unicode(ANSIString("\n").join([line for line in self._generate_lines()])))
|
||||
|
||||
def __unicode__(self):
|
||||
|
|
|
|||
|
|
@ -1,464 +1,528 @@
|
|||
"""
|
||||
Inline functions (nested form).
|
||||
|
||||
This parser accepts nested inlinefunctions on the form
|
||||
|
||||
```
|
||||
$funcname(arg, arg, ...)
|
||||
```
|
||||
|
||||
embedded in any text where any arg can be another $funcname{} call.
|
||||
This functionality is turned off by default - to activate,
|
||||
`settings.INLINEFUNC_ENABLED` must be set to `True`.
|
||||
|
||||
Each token starts with "$funcname(" where there must be no space
|
||||
between the $funcname and (. It ends with a matched ending parentesis.
|
||||
")".
|
||||
|
||||
Inside the inlinefunc definition, one can use `\` to escape. This is
|
||||
mainly needed for escaping commas in flowing text (which would
|
||||
otherwise be interpreted as an argument separator), or to escape `}`
|
||||
when not intended to close the function block. Enclosing text in
|
||||
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
|
||||
also escape *everything* within without needing to escape individual
|
||||
characters.
|
||||
|
||||
The available inlinefuncs are defined as global-level functions in
|
||||
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
|
||||
by their function name (and ignored if this name starts with `_`). They
|
||||
should be on the following form:
|
||||
|
||||
```python
|
||||
def funcname (*args, **kwargs):
|
||||
# ...
|
||||
```
|
||||
|
||||
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
|
||||
`*args` tuple. This will be populated by the arguments given to the
|
||||
inlinefunc in-game - the only part that will be available from
|
||||
in-game. `**kwargs` are not supported from in-game but are only used
|
||||
internally by Evennia to make details about the caller available to
|
||||
the function. The kwarg passed to all functions is `session`, the
|
||||
Sessionobject for the object seeing the string. This may be `None` if
|
||||
the string is sent to a non-puppetable object. The inlinefunc should
|
||||
never raise an exception.
|
||||
|
||||
There are two reserved function names:
|
||||
- "nomatch": This is called if the user uses a functionname that is
|
||||
not registered. The nomatch function will get the name of the
|
||||
not-found function as its first argument followed by the normal
|
||||
arguments to the given function. If not defined the default effect is
|
||||
to print `<UNKNOWN>` to replace the unknown function.
|
||||
- "stackfull": This is called when the maximum nested function stack is reached.
|
||||
When this happens, the original parsed string is returned and the result of
|
||||
the `stackfull` inlinefunc is appended to the end. By default this is an
|
||||
error message.
|
||||
|
||||
Error handling:
|
||||
Syntax errors, notably not completely closing all inlinefunc
|
||||
blocks, will lead to the entire string remaining unparsed.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from evennia.utils import utils
|
||||
|
||||
|
||||
# example/testing inline functions
|
||||
|
||||
def pad(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Pads text to given width.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to pad.
|
||||
width (str, optional): Will be converted to integer. Width
|
||||
of padding.
|
||||
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
|
||||
fillchar (str, optional): Character used for padding. Defaults to a space.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Session performing the pad.
|
||||
|
||||
Example:
|
||||
`$pad(text, width, align, fillchar)`
|
||||
|
||||
"""
|
||||
text, width, align, fillchar = "", 78, 'c', ' '
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
|
||||
if nargs > 3:
|
||||
fillchar = args[3]
|
||||
return utils.pad(text, width=width, align=align, fillchar=fillchar)
|
||||
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Kwargs:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$crop(text, width=78, suffix='[...]')`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
|
||||
|
||||
def clr(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Colorizes nested text.
|
||||
|
||||
Args:
|
||||
startclr (str, optional): An ANSI color abbreviation without the
|
||||
prefix `|`, such as `r` (red foreground) or `[r` (red background).
|
||||
text (str, optional): Text
|
||||
endclr (str, optional): The color to use at the end of the string. Defaults
|
||||
to `|n` (reset-color).
|
||||
Kwargs:
|
||||
session (Session): Session object triggering inlinefunc.
|
||||
|
||||
Example:
|
||||
`$clr(startclr, text, endclr)`
|
||||
|
||||
"""
|
||||
text = ""
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
color = args[0].strip()
|
||||
if nargs > 1:
|
||||
text = args[1]
|
||||
text = "|" + color + text
|
||||
if nargs > 2:
|
||||
text += "|" + args[2].strip()
|
||||
else:
|
||||
text += "|n"
|
||||
return text
|
||||
|
||||
|
||||
# we specify a default nomatch function to use if no matching func was
|
||||
# found. This will be overloaded by any nomatch function defined in
|
||||
# the imported modules.
|
||||
_INLINE_FUNCS = {"nomatch": lambda *args, **kwargs: "<UKNOWN>",
|
||||
"stackfull": lambda *args, **kwargs: "\n (not parsed: inlinefunc stack size exceeded.)"}
|
||||
|
||||
|
||||
# load custom inline func modules.
|
||||
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
|
||||
try:
|
||||
_INLINE_FUNCS.update(utils.callables_from_module(module))
|
||||
except ImportError as err:
|
||||
if module == "server.conf.inlinefuncs":
|
||||
# a temporary warning since the default module changed name
|
||||
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
|
||||
"be renamed to mygame/server/conf/inlinefuncs.py (note the S at the end)." % err)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# remove the core function if we include examples in this module itself
|
||||
#_INLINE_FUNCS.pop("inline_func_parse", None)
|
||||
|
||||
|
||||
# The stack size is a security measure. Set to <=0 to disable.
|
||||
try:
|
||||
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
||||
except AttributeError:
|
||||
_STACK_MAXSIZE = 20
|
||||
|
||||
# regex definitions
|
||||
|
||||
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname{ (start of function call)
|
||||
|
||||
_RE_TOKEN = re.compile(r"""
|
||||
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # unescaped single-triples (escapes all inside them)
|
||||
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # unescaped normal triple quotes (escapes all inside them)
|
||||
(?P<comma>(?<!\\)\,)| # unescaped , (argument separator)
|
||||
(?P<end>(?<!\\)\))| # unescaped ) (end of function call)
|
||||
(?P<start>(?<!\\)\$\w+\()| # unescaped $funcname( (start of function call)
|
||||
(?P<escaped>\\'|\\"|\\\)|\\$\w+\()| # escaped tokens should re-appear in text
|
||||
(?P<rest>[\w\s.-\/#!%\^&\*;:=\-_`~\|\(}{\[\]]+|\"{1}|\'{1}) # everything else should also be included""",
|
||||
re.UNICODE + re.IGNORECASE + re.VERBOSE + re.DOTALL)
|
||||
|
||||
|
||||
# Cache for function lookups.
|
||||
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
|
||||
|
||||
|
||||
class ParseStack(list):
|
||||
"""
|
||||
Custom stack that always concatenates strings together when the
|
||||
strings are added next to one another. Tuples are stored
|
||||
separately and None is used to mark that a string should be broken
|
||||
up into a new chunk. Below is the resulting stack after separately
|
||||
appending 3 strings, None, 2 strings, a tuple and finally 2
|
||||
strings:
|
||||
|
||||
[string + string + string,
|
||||
None
|
||||
string + string,
|
||||
tuple,
|
||||
string + string]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ParseStack, self).__init__(*args, **kwargs)
|
||||
# always start stack with the empty string
|
||||
list.append(self, "")
|
||||
# indicates if the top of the stack is a string or not
|
||||
self._string_last = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (super(ParseStack).__eq__(other) and
|
||||
hasattr(other, "_string_last") and self._string_last == other._string_last)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def append(self, item):
|
||||
"""
|
||||
The stack will merge strings, add other things as normal
|
||||
"""
|
||||
if isinstance(item, basestring):
|
||||
if self._string_last:
|
||||
self[-1] += item
|
||||
else:
|
||||
list.append(self, item)
|
||||
self._string_last = True
|
||||
else:
|
||||
# everything else is added as normal
|
||||
list.append(self, item)
|
||||
self._string_last = False
|
||||
|
||||
|
||||
class InlinefuncError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_inlinefunc(string, strip=False, **kwargs):
|
||||
"""
|
||||
Parse the incoming string.
|
||||
|
||||
Args:
|
||||
string (str): The incoming string to parse.
|
||||
strip (bool, optional): Whether to strip function calls rather than
|
||||
execute them.
|
||||
Kwargs:
|
||||
session (Session): This is sent to this function by Evennia when triggering
|
||||
it. It is passed to the inlinefunc.
|
||||
kwargs (any): All other kwargs are also passed on to the inlinefunc.
|
||||
|
||||
|
||||
"""
|
||||
global _PARSING_CACHE
|
||||
if string in _PARSING_CACHE:
|
||||
# stack is already cached
|
||||
stack = _PARSING_CACHE[string]
|
||||
elif not _RE_STARTTOKEN.search(string):
|
||||
# if there are no unescaped start tokens at all, return immediately.
|
||||
return string
|
||||
else:
|
||||
# no cached stack; build a new stack and continue
|
||||
stack = ParseStack()
|
||||
|
||||
# process string on stack
|
||||
ncallable = 0
|
||||
for match in _RE_TOKEN.finditer(string):
|
||||
gdict = match.groupdict()
|
||||
if gdict["singlequote"]:
|
||||
stack.append(gdict["singlequote"])
|
||||
elif gdict["doublequote"]:
|
||||
stack.append(gdict["doublequote"])
|
||||
elif gdict["end"]:
|
||||
if ncallable <= 0:
|
||||
stack.append(")")
|
||||
continue
|
||||
args = []
|
||||
while stack:
|
||||
operation = stack.pop()
|
||||
if callable(operation):
|
||||
if not strip:
|
||||
stack.append((operation, [arg for arg in reversed(args)]))
|
||||
ncallable -= 1
|
||||
break
|
||||
else:
|
||||
args.append(operation)
|
||||
elif gdict["start"]:
|
||||
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
|
||||
try:
|
||||
# try to fetch the matching inlinefunc from storage
|
||||
stack.append(_INLINE_FUNCS[funcname])
|
||||
except KeyError:
|
||||
stack.append(_INLINE_FUNCS["nomatch"])
|
||||
stack.append(funcname)
|
||||
ncallable += 1
|
||||
elif gdict["escaped"]:
|
||||
# escaped tokens
|
||||
token = gdict["escaped"].lstrip("\\")
|
||||
stack.append(token)
|
||||
elif gdict["comma"]:
|
||||
if ncallable > 0:
|
||||
# commas outside strings and inside a callable are
|
||||
# used to mark argument separation - we use None
|
||||
# in the stack to indicate such a separation.
|
||||
stack.append(None)
|
||||
else:
|
||||
# no callable active - just a string
|
||||
stack.append(",")
|
||||
else:
|
||||
# the rest
|
||||
stack.append(gdict["rest"])
|
||||
|
||||
if ncallable > 0:
|
||||
# this means not all inlinefuncs were complete
|
||||
return string
|
||||
|
||||
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < len(stack):
|
||||
# if stack is larger than limit, throw away parsing
|
||||
return string + gdict["stackfull"](*args, **kwargs)
|
||||
else:
|
||||
# cache the stack
|
||||
_PARSING_CACHE[string] = stack
|
||||
|
||||
# run the stack recursively
|
||||
def _run_stack(item, depth=0):
|
||||
retval = item
|
||||
if isinstance(item, tuple):
|
||||
if strip:
|
||||
return ""
|
||||
else:
|
||||
func, arglist = item
|
||||
args = [""]
|
||||
for arg in arglist:
|
||||
if arg is None:
|
||||
# an argument-separating comma - start a new arg
|
||||
args.append("")
|
||||
else:
|
||||
# all other args should merge into one string
|
||||
args[-1] += _run_stack(arg, depth=depth + 1)
|
||||
# execute the inlinefunc at this point or strip it.
|
||||
kwargs["inlinefunc_stack_depth"] = depth
|
||||
retval = "" if strip else func(*args, **kwargs)
|
||||
return utils.to_str(retval, force_string=True)
|
||||
|
||||
# execute the stack from the cache
|
||||
return "".join(_run_stack(item) for item in _PARSING_CACHE[string])
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This supports the use of replacement templates in nicks:
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1) The user supplies a template that is converted to a regex according
|
||||
to the unix-like templating language.
|
||||
2) This regex is tested against nicks depending on which nick replacement
|
||||
strategy is considered (most commonly inputline).
|
||||
3) If there is a template match and there are templating markers,
|
||||
these are replaced with the arguments actually given.
|
||||
|
||||
@desc $1 $2 $3
|
||||
|
||||
This will be converted to the following regex:
|
||||
|
||||
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
|
||||
|
||||
Supported template markers (through fnmatch)
|
||||
* matches anything (non-greedy) -> .*?
|
||||
? matches any single character ->
|
||||
[seq] matches any entry in sequence
|
||||
[!seq] matches entries not in sequence
|
||||
Custom arg markers
|
||||
$N argument position (1-99)
|
||||
|
||||
"""
|
||||
import fnmatch
|
||||
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_SPACE = re.compile(r"\\ ")
|
||||
|
||||
|
||||
class NickTemplateInvalid(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def initialize_nick_templates(in_template, out_template):
|
||||
"""
|
||||
Initialize the nick templates for matching and remapping a string.
|
||||
|
||||
Args:
|
||||
in_template (str): The template to be used for nick recognition.
|
||||
out_template (str): The template to be used to replace the string
|
||||
matched by the in_template.
|
||||
|
||||
Returns:
|
||||
regex (regex): Regex to match against strings
|
||||
template (str): Template with markers {arg1}, {arg2}, etc for
|
||||
replacement using the standard .format method.
|
||||
|
||||
Raises:
|
||||
NickTemplateInvalid: If the in/out template does not have a matching
|
||||
number of $args.
|
||||
|
||||
"""
|
||||
# create the regex for in_template
|
||||
regex_string = fnmatch.translate(in_template)
|
||||
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
|
||||
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
|
||||
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
|
||||
|
||||
# create the out_template
|
||||
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
|
||||
|
||||
# validate the tempaltes - they should at least have the same number of args
|
||||
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
|
||||
if n_inargs != n_outargs:
|
||||
print n_inargs, n_outargs
|
||||
raise NickTemplateInvalid
|
||||
|
||||
return re.compile(regex_string), template_string
|
||||
|
||||
|
||||
def parse_nick_template(string, template_regex, outtemplate):
|
||||
"""
|
||||
Parse a text using a template and map it to another template
|
||||
|
||||
Args:
|
||||
string (str): The input string to processj
|
||||
template_regex (regex): A template regex created with
|
||||
initialize_nick_template.
|
||||
outtemplate (str): The template to which to map the matches
|
||||
produced by the template_regex. This should have $1, $2,
|
||||
etc to match the regex.
|
||||
|
||||
"""
|
||||
match = template_regex.match(string)
|
||||
if match:
|
||||
return outtemplate.format(**match.groupdict())
|
||||
return string
|
||||
"""
|
||||
Inline functions (nested form).
|
||||
|
||||
This parser accepts nested inlinefunctions on the form
|
||||
|
||||
```
|
||||
$funcname(arg, arg, ...)
|
||||
```
|
||||
|
||||
embedded in any text where any arg can be another $funcname{} call.
|
||||
This functionality is turned off by default - to activate,
|
||||
`settings.INLINEFUNC_ENABLED` must be set to `True`.
|
||||
|
||||
Each token starts with "$funcname(" where there must be no space
|
||||
between the $funcname and (. It ends with a matched ending parentesis.
|
||||
")".
|
||||
|
||||
Inside the inlinefunc definition, one can use `\` to escape. This is
|
||||
mainly needed for escaping commas in flowing text (which would
|
||||
otherwise be interpreted as an argument separator), or to escape `}`
|
||||
when not intended to close the function block. Enclosing text in
|
||||
matched `\"\"\"` (triple quotes) or `'''` (triple single-quotes) will
|
||||
also escape *everything* within without needing to escape individual
|
||||
characters.
|
||||
|
||||
The available inlinefuncs are defined as global-level functions in
|
||||
modules defined by `settings.INLINEFUNC_MODULES`. They are identified
|
||||
by their function name (and ignored if this name starts with `_`). They
|
||||
should be on the following form:
|
||||
|
||||
```python
|
||||
def funcname (*args, **kwargs):
|
||||
# ...
|
||||
```
|
||||
|
||||
Here, the arguments given to `$funcname(arg1,arg2)` will appear as the
|
||||
`*args` tuple. This will be populated by the arguments given to the
|
||||
inlinefunc in-game - the only part that will be available from
|
||||
in-game. `**kwargs` are not supported from in-game but are only used
|
||||
internally by Evennia to make details about the caller available to
|
||||
the function. The kwarg passed to all functions is `session`, the
|
||||
Sessionobject for the object seeing the string. This may be `None` if
|
||||
the string is sent to a non-puppetable object. The inlinefunc should
|
||||
never raise an exception.
|
||||
|
||||
There are two reserved function names:
|
||||
- "nomatch": This is called if the user uses a functionname that is
|
||||
not registered. The nomatch function will get the name of the
|
||||
not-found function as its first argument followed by the normal
|
||||
arguments to the given function. If not defined the default effect is
|
||||
to print `<UNKNOWN>` to replace the unknown function.
|
||||
- "stackfull": This is called when the maximum nested function stack is reached.
|
||||
When this happens, the original parsed string is returned and the result of
|
||||
the `stackfull` inlinefunc is appended to the end. By default this is an
|
||||
error message.
|
||||
|
||||
Error handling:
|
||||
Syntax errors, notably not completely closing all inlinefunc
|
||||
blocks, will lead to the entire string remaining unparsed.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import fnmatch
|
||||
from django.conf import settings
|
||||
|
||||
from evennia.utils import utils, logger
|
||||
|
||||
|
||||
# example/testing inline functions
|
||||
|
||||
def pad(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Pads text to given width.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to pad.
|
||||
width (str, optional): Will be converted to integer. Width
|
||||
of padding.
|
||||
align (str, optional): Alignment of padding; one of 'c', 'l' or 'r'.
|
||||
fillchar (str, optional): Character used for padding. Defaults to a space.
|
||||
|
||||
Kwargs:
|
||||
session (Session): Session performing the pad.
|
||||
|
||||
Example:
|
||||
`$pad(text, width, align, fillchar)`
|
||||
|
||||
"""
|
||||
text, width, align, fillchar = "", 78, 'c', ' '
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
align = args[2] if args[2] in ('c', 'l', 'r') else 'c'
|
||||
if nargs > 3:
|
||||
fillchar = args[3]
|
||||
return utils.pad(text, width=width, align=align, fillchar=fillchar)
|
||||
|
||||
|
||||
def crop(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Crops ingoing text to given widths.
|
||||
|
||||
Args:
|
||||
text (str, optional): Text to crop.
|
||||
width (str, optional): Will be converted to an integer. Width of
|
||||
crop in characters.
|
||||
suffix (str, optional): End string to mark the fact that a part
|
||||
of the string was cropped. Defaults to `[...]`.
|
||||
Kwargs:
|
||||
session (Session): Session performing the crop.
|
||||
|
||||
Example:
|
||||
`$crop(text, width=78, suffix='[...]')`
|
||||
|
||||
"""
|
||||
text, width, suffix = "", 78, "[...]"
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
text = args[0]
|
||||
if nargs > 1:
|
||||
width = int(args[1]) if args[1].strip().isdigit() else 78
|
||||
if nargs > 2:
|
||||
suffix = args[2]
|
||||
return utils.crop(text, width=width, suffix=suffix)
|
||||
|
||||
|
||||
def clr(*args, **kwargs):
|
||||
"""
|
||||
Inlinefunc. Colorizes nested text.
|
||||
|
||||
Args:
|
||||
startclr (str, optional): An ANSI color abbreviation without the
|
||||
prefix `|`, such as `r` (red foreground) or `[r` (red background).
|
||||
text (str, optional): Text
|
||||
endclr (str, optional): The color to use at the end of the string. Defaults
|
||||
to `|n` (reset-color).
|
||||
Kwargs:
|
||||
session (Session): Session object triggering inlinefunc.
|
||||
|
||||
Example:
|
||||
`$clr(startclr, text, endclr)`
|
||||
|
||||
"""
|
||||
text = ""
|
||||
nargs = len(args)
|
||||
if nargs > 0:
|
||||
color = args[0].strip()
|
||||
if nargs > 1:
|
||||
text = args[1]
|
||||
text = "|" + color + text
|
||||
if nargs > 2:
|
||||
text += "|" + args[2].strip()
|
||||
else:
|
||||
text += "|n"
|
||||
return text
|
||||
|
||||
|
||||
def null(*args, **kwargs):
|
||||
return args[0] if args else ''
|
||||
|
||||
|
||||
def nomatch(name, *args, **kwargs):
|
||||
"""
|
||||
Default implementation of nomatch returns the function as-is as a string.
|
||||
|
||||
"""
|
||||
kwargs.pop("inlinefunc_stack_depth", None)
|
||||
kwargs.pop("session")
|
||||
|
||||
return "${name}({args}{kwargs})".format(
|
||||
name=name,
|
||||
args=",".join(args),
|
||||
kwargs=",".join("{}={}".format(key, val) for key, val in kwargs.items()))
|
||||
|
||||
_INLINE_FUNCS = {}
|
||||
|
||||
# we specify a default nomatch function to use if no matching func was
|
||||
# found. This will be overloaded by any nomatch function defined in
|
||||
# the imported modules.
|
||||
_DEFAULT_FUNCS = {"nomatch": lambda *args, **kwargs: "<UNKNOWN>",
|
||||
"stackfull": lambda *args, **kwargs: "\n (not parsed: "}
|
||||
|
||||
_INLINE_FUNCS.update(_DEFAULT_FUNCS)
|
||||
|
||||
# load custom inline func modules.
|
||||
for module in utils.make_iter(settings.INLINEFUNC_MODULES):
|
||||
try:
|
||||
_INLINE_FUNCS.update(utils.callables_from_module(module))
|
||||
except ImportError as err:
|
||||
if module == "server.conf.inlinefuncs":
|
||||
# a temporary warning since the default module changed name
|
||||
raise ImportError("Error: %s\nPossible reason: mygame/server/conf/inlinefunc.py should "
|
||||
"be renamed to mygame/server/conf/inlinefuncs.py (note "
|
||||
"the S at the end)." % err)
|
||||
else:
|
||||
raise
|
||||
|
||||
|
||||
# The stack size is a security measure. Set to <=0 to disable.
|
||||
try:
|
||||
_STACK_MAXSIZE = settings.INLINEFUNC_STACK_MAXSIZE
|
||||
except AttributeError:
|
||||
_STACK_MAXSIZE = 20
|
||||
|
||||
# regex definitions
|
||||
|
||||
_RE_STARTTOKEN = re.compile(r"(?<!\\)\$(\w+)\(") # unescaped $funcname( (start of function call)
|
||||
|
||||
# note: this regex can be experimented with at https://regex101.com/r/kGR3vE/2
|
||||
_RE_TOKEN = re.compile(r"""
|
||||
(?<!\\)\'\'\'(?P<singlequote>.*?)(?<!\\)\'\'\'| # single-triplets escape all inside
|
||||
(?<!\\)\"\"\"(?P<doublequote>.*?)(?<!\\)\"\"\"| # double-triplets escape all inside
|
||||
(?P<comma>(?<!\\)\,)| # , (argument sep)
|
||||
(?P<end>(?<!\\)\))| # ) (possible end of func call)
|
||||
(?P<leftparens>(?<!\\)\()| # ( (lone left-parens)
|
||||
(?P<start>(?<!\\)\$\w+\()| # $funcname (start of func call)
|
||||
(?P<escaped> # escaped tokens to re-insert sans backslash
|
||||
\\\'|\\\"|\\\)|\\\$\w+\(|\\\()|
|
||||
(?P<rest> # everything else to re-insert verbatim
|
||||
\$(?!\w+\()|\'|\"|\\|[^),$\'\"\\\(]+)""",
|
||||
re.UNICODE | re.IGNORECASE | re.VERBOSE | re.DOTALL)
|
||||
|
||||
# Cache for function lookups.
|
||||
_PARSING_CACHE = utils.LimitedSizeOrderedDict(size_limit=1000)
|
||||
|
||||
|
||||
class ParseStack(list):
|
||||
"""
|
||||
Custom stack that always concatenates strings together when the
|
||||
strings are added next to one another. Tuples are stored
|
||||
separately and None is used to mark that a string should be broken
|
||||
up into a new chunk. Below is the resulting stack after separately
|
||||
appending 3 strings, None, 2 strings, a tuple and finally 2
|
||||
strings:
|
||||
|
||||
[string + string + string,
|
||||
None
|
||||
string + string,
|
||||
tuple,
|
||||
string + string]
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super(ParseStack, self).__init__(*args, **kwargs)
|
||||
# always start stack with the empty string
|
||||
list.append(self, "")
|
||||
# indicates if the top of the stack is a string or not
|
||||
self._string_last = True
|
||||
|
||||
def __eq__(self, other):
|
||||
return (super(ParseStack).__eq__(other) and
|
||||
hasattr(other, "_string_last") and self._string_last == other._string_last)
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def append(self, item):
|
||||
"""
|
||||
The stack will merge strings, add other things as normal
|
||||
"""
|
||||
if isinstance(item, basestring):
|
||||
if self._string_last:
|
||||
self[-1] += item
|
||||
else:
|
||||
list.append(self, item)
|
||||
self._string_last = True
|
||||
else:
|
||||
# everything else is added as normal
|
||||
list.append(self, item)
|
||||
self._string_last = False
|
||||
|
||||
|
||||
class InlinefuncError(RuntimeError):
|
||||
pass
|
||||
|
||||
|
||||
def parse_inlinefunc(string, strip=False, available_funcs=None, stacktrace=False, **kwargs):
|
||||
"""
|
||||
Parse the incoming string.
|
||||
|
||||
Args:
|
||||
string (str): The incoming string to parse.
|
||||
strip (bool, optional): Whether to strip function calls rather than
|
||||
execute them.
|
||||
available_funcs (dict, optional): Define an alternative source of functions to parse for.
|
||||
If unset, use the functions found through `settings.INLINEFUNC_MODULES`.
|
||||
stacktrace (bool, optional): If set, print the stacktrace to log.
|
||||
Kwargs:
|
||||
session (Session): This is sent to this function by Evennia when triggering
|
||||
it. It is passed to the inlinefunc.
|
||||
kwargs (any): All other kwargs are also passed on to the inlinefunc.
|
||||
|
||||
|
||||
"""
|
||||
global _PARSING_CACHE
|
||||
usecache = False
|
||||
if not available_funcs:
|
||||
available_funcs = _INLINE_FUNCS
|
||||
usecache = True
|
||||
else:
|
||||
# make sure the default keys are available, but also allow overriding
|
||||
tmp = _DEFAULT_FUNCS.copy()
|
||||
tmp.update(available_funcs)
|
||||
available_funcs = tmp
|
||||
|
||||
if usecache and string in _PARSING_CACHE:
|
||||
# stack is already cached
|
||||
stack = _PARSING_CACHE[string]
|
||||
elif not _RE_STARTTOKEN.search(string):
|
||||
# if there are no unescaped start tokens at all, return immediately.
|
||||
return string
|
||||
else:
|
||||
# no cached stack; build a new stack and continue
|
||||
stack = ParseStack()
|
||||
|
||||
# process string on stack
|
||||
ncallable = 0
|
||||
nlparens = 0
|
||||
nvalid = 0
|
||||
|
||||
if stacktrace:
|
||||
out = "STRING: {} =>".format(string)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
for match in _RE_TOKEN.finditer(string):
|
||||
gdict = match.groupdict()
|
||||
|
||||
if stacktrace:
|
||||
out = " MATCH: {}".format({key: val for key, val in gdict.items() if val})
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
if gdict["singlequote"]:
|
||||
stack.append(gdict["singlequote"])
|
||||
elif gdict["doublequote"]:
|
||||
stack.append(gdict["doublequote"])
|
||||
elif gdict["leftparens"]:
|
||||
# we have a left-parens inside a callable
|
||||
if ncallable:
|
||||
nlparens += 1
|
||||
stack.append("(")
|
||||
elif gdict["end"]:
|
||||
if nlparens > 0:
|
||||
nlparens -= 1
|
||||
stack.append(")")
|
||||
continue
|
||||
if ncallable <= 0:
|
||||
stack.append(")")
|
||||
continue
|
||||
args = []
|
||||
while stack:
|
||||
operation = stack.pop()
|
||||
if callable(operation):
|
||||
if not strip:
|
||||
stack.append((operation, [arg for arg in reversed(args)]))
|
||||
ncallable -= 1
|
||||
break
|
||||
else:
|
||||
args.append(operation)
|
||||
elif gdict["start"]:
|
||||
funcname = _RE_STARTTOKEN.match(gdict["start"]).group(1)
|
||||
try:
|
||||
# try to fetch the matching inlinefunc from storage
|
||||
stack.append(available_funcs[funcname])
|
||||
nvalid += 1
|
||||
except KeyError:
|
||||
stack.append(available_funcs["nomatch"])
|
||||
stack.append(funcname)
|
||||
stack.append(None)
|
||||
ncallable += 1
|
||||
elif gdict["escaped"]:
|
||||
# escaped tokens
|
||||
token = gdict["escaped"].lstrip("\\")
|
||||
stack.append(token)
|
||||
elif gdict["comma"]:
|
||||
if ncallable > 0:
|
||||
# commas outside strings and inside a callable are
|
||||
# used to mark argument separation - we use None
|
||||
# in the stack to indicate such a separation.
|
||||
stack.append(None)
|
||||
else:
|
||||
# no callable active - just a string
|
||||
stack.append(",")
|
||||
else:
|
||||
# the rest
|
||||
stack.append(gdict["rest"])
|
||||
|
||||
if ncallable > 0:
|
||||
# this means not all inlinefuncs were complete
|
||||
return string
|
||||
|
||||
if _STACK_MAXSIZE > 0 and _STACK_MAXSIZE < nvalid:
|
||||
# if stack is larger than limit, throw away parsing
|
||||
return string + available_funcs["stackfull"](*args, **kwargs)
|
||||
elif usecache:
|
||||
# cache the stack - we do this also if we don't check the cache above
|
||||
_PARSING_CACHE[string] = stack
|
||||
|
||||
# run the stack recursively
|
||||
def _run_stack(item, depth=0):
|
||||
retval = item
|
||||
if isinstance(item, tuple):
|
||||
if strip:
|
||||
return ""
|
||||
else:
|
||||
func, arglist = item
|
||||
args = [""]
|
||||
for arg in arglist:
|
||||
if arg is None:
|
||||
# an argument-separating comma - start a new arg
|
||||
args.append("")
|
||||
else:
|
||||
# all other args should merge into one string
|
||||
args[-1] += _run_stack(arg, depth=depth + 1)
|
||||
# execute the inlinefunc at this point or strip it.
|
||||
kwargs["inlinefunc_stack_depth"] = depth
|
||||
retval = "" if strip else func(*args, **kwargs)
|
||||
return utils.to_str(retval, force_string=True)
|
||||
retval = "".join(_run_stack(item) for item in stack)
|
||||
if stacktrace:
|
||||
out = "STACK: \n{} => {}\n".format(stack, retval)
|
||||
print(out)
|
||||
logger.log_info(out)
|
||||
|
||||
# execute the stack
|
||||
return retval
|
||||
|
||||
#
|
||||
# Nick templating
|
||||
#
|
||||
|
||||
|
||||
"""
|
||||
This supports the use of replacement templates in nicks:
|
||||
|
||||
This happens in two steps:
|
||||
|
||||
1) The user supplies a template that is converted to a regex according
|
||||
to the unix-like templating language.
|
||||
2) This regex is tested against nicks depending on which nick replacement
|
||||
strategy is considered (most commonly inputline).
|
||||
3) If there is a template match and there are templating markers,
|
||||
these are replaced with the arguments actually given.
|
||||
|
||||
@desc $1 $2 $3
|
||||
|
||||
This will be converted to the following regex:
|
||||
|
||||
\@desc (?P<1>\w+) (?P<2>\w+) $(?P<3>\w+)
|
||||
|
||||
Supported template markers (through fnmatch)
|
||||
* matches anything (non-greedy) -> .*?
|
||||
? matches any single character ->
|
||||
[seq] matches any entry in sequence
|
||||
[!seq] matches entries not in sequence
|
||||
Custom arg markers
|
||||
$N argument position (1-99)
|
||||
|
||||
"""
|
||||
|
||||
_RE_NICK_ARG = re.compile(r"\\(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_TEMPLATE_ARG = re.compile(r"(\$)([1-9][0-9]?)")
|
||||
_RE_NICK_SPACE = re.compile(r"\\ ")
|
||||
|
||||
|
||||
class NickTemplateInvalid(ValueError):
|
||||
pass
|
||||
|
||||
|
||||
def initialize_nick_templates(in_template, out_template):
|
||||
"""
|
||||
Initialize the nick templates for matching and remapping a string.
|
||||
|
||||
Args:
|
||||
in_template (str): The template to be used for nick recognition.
|
||||
out_template (str): The template to be used to replace the string
|
||||
matched by the in_template.
|
||||
|
||||
Returns:
|
||||
regex (regex): Regex to match against strings
|
||||
template (str): Template with markers {arg1}, {arg2}, etc for
|
||||
replacement using the standard .format method.
|
||||
|
||||
Raises:
|
||||
NickTemplateInvalid: If the in/out template does not have a matching
|
||||
number of $args.
|
||||
|
||||
"""
|
||||
# create the regex for in_template
|
||||
regex_string = fnmatch.translate(in_template)
|
||||
n_inargs = len(_RE_NICK_ARG.findall(regex_string))
|
||||
regex_string = _RE_NICK_SPACE.sub("\s+", regex_string)
|
||||
regex_string = _RE_NICK_ARG.sub(lambda m: "(?P<arg%s>.+?)" % m.group(2), regex_string)
|
||||
|
||||
# create the out_template
|
||||
template_string = _RE_NICK_TEMPLATE_ARG.sub(lambda m: "{arg%s}" % m.group(2), out_template)
|
||||
|
||||
# validate the tempaltes - they should at least have the same number of args
|
||||
n_outargs = len(_RE_NICK_TEMPLATE_ARG.findall(out_template))
|
||||
if n_inargs != n_outargs:
|
||||
raise NickTemplateInvalid
|
||||
|
||||
return re.compile(regex_string), template_string
|
||||
|
||||
|
||||
def parse_nick_template(string, template_regex, outtemplate):
|
||||
"""
|
||||
Parse a text using a template and map it to another template
|
||||
|
||||
Args:
|
||||
string (str): The input string to processj
|
||||
template_regex (regex): A template regex created with
|
||||
initialize_nick_template.
|
||||
outtemplate (str): The template to which to map the matches
|
||||
produced by the template_regex. This should have $1, $2,
|
||||
etc to match the regex.
|
||||
|
||||
"""
|
||||
match = template_regex.match(string)
|
||||
if match:
|
||||
return outtemplate.format(**match.groupdict())
|
||||
return string
|
||||
|
|
|
|||
|
|
@ -1,342 +0,0 @@
|
|||
"""
|
||||
Spawner
|
||||
|
||||
The spawner takes input files containing object definitions in
|
||||
dictionary forms. These use a prototype architecture to define
|
||||
unique objects without having to make a Typeclass for each.
|
||||
|
||||
The main function is `spawn(*prototype)`, where the `prototype`
|
||||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
GOBLIN = {
|
||||
"typeclass": "types.objects.Monster",
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20,30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"args": [("weapon", "sword")]
|
||||
}
|
||||
```
|
||||
|
||||
Possible keywords are:
|
||||
prototype - string parent prototype
|
||||
key - string, the main object identifier
|
||||
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`
|
||||
location - this should be a valid object or #dbref
|
||||
home - valid object or #dbref
|
||||
destination - only valid for exits (object or dbref)
|
||||
|
||||
permissions - string or list of permission strings
|
||||
locks - a lock-string
|
||||
aliases - string or list of strings
|
||||
exec - this is a string of python code to execute or a list of such codes.
|
||||
This can be used e.g. to trigger custom handlers on the object. The
|
||||
execution namespace contains 'evennia' for the library and 'obj'
|
||||
tags - string or list of strings or tuples `(tagstr, category)`. Plain
|
||||
strings will be result in tags with no category (default tags).
|
||||
attrs - tuple or list of tuples of Attributes to add. This form allows
|
||||
more complex Attributes to be set. Tuples at least specify `(key, value)`
|
||||
but can also specify up to `(key, value, category, lockstring)`. If
|
||||
you want to specify a lockstring but not a category, set the category
|
||||
to `None`.
|
||||
ndb_<name> - value of a nattribute (ndb_ is stripped)
|
||||
other - any other name is interpreted as the key of an Attribute with
|
||||
its value. Such Attributes have no categories.
|
||||
|
||||
Each value can also be a callable that takes no arguments. It should
|
||||
return the value to enter into the field and will be called every time
|
||||
the prototype is used to spawn an object. Note, if you want to store
|
||||
a callable in an Attribute, embed it in a tuple to the `args` keyword.
|
||||
|
||||
By specifying the "prototype" key, the prototype becomes a child of
|
||||
that prototype, inheritng all prototype slots it does not explicitly
|
||||
define itself, while overloading those that it does specify.
|
||||
|
||||
```python
|
||||
GOBLIN_WIZARD = {
|
||||
"prototype": GOBLIN,
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
}
|
||||
|
||||
GOBLIN_ARCHER = {
|
||||
"prototype": GOBLIN,
|
||||
"key": "goblin archer",
|
||||
"attacks": ["short bow"]
|
||||
}
|
||||
```
|
||||
|
||||
One can also have multiple prototypes. These are inherited from the
|
||||
left, with the ones further to the right taking precedence.
|
||||
|
||||
```python
|
||||
ARCHWIZARD = {
|
||||
"attack": ["archwizard staff", "eye of doom"]
|
||||
|
||||
GOBLIN_ARCHWIZARD = {
|
||||
"key" : "goblin archwizard"
|
||||
"prototype": (GOBLIN_WIZARD, ARCHWIZARD),
|
||||
}
|
||||
```
|
||||
|
||||
The *goblin archwizard* will have some different attacks, but will
|
||||
otherwise have the same spells as a *goblin wizard* who in turn shares
|
||||
many traits with a normal *goblin*.
|
||||
|
||||
"""
|
||||
from __future__ import print_function
|
||||
|
||||
import copy
|
||||
from django.conf import settings
|
||||
from random import randint
|
||||
import evennia
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils.utils import make_iter, all_from_module, dbid_to_obj
|
||||
|
||||
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
|
||||
|
||||
|
||||
def _handle_dbref(inp):
|
||||
return dbid_to_obj(inp, ObjectDB)
|
||||
|
||||
|
||||
def _validate_prototype(key, prototype, protparents, visited):
|
||||
"""
|
||||
Run validation on a prototype, checking for inifinite regress.
|
||||
|
||||
"""
|
||||
assert isinstance(prototype, dict)
|
||||
if id(prototype) in visited:
|
||||
raise RuntimeError("%s has infinite nesting of prototypes." % key or prototype)
|
||||
visited.append(id(prototype))
|
||||
protstrings = prototype.get("prototype")
|
||||
if protstrings:
|
||||
for protstring in make_iter(protstrings):
|
||||
if key is not None and protstring == key:
|
||||
raise RuntimeError("%s tries to prototype itself." % key or prototype)
|
||||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
raise RuntimeError("%s's prototype '%s' was not found." % (key or prototype, protstring))
|
||||
_validate_prototype(protstring, protparent, protparents, visited)
|
||||
|
||||
|
||||
def _get_prototype(dic, prot, protparents):
|
||||
"""
|
||||
Recursively traverse a prototype dictionary, including multiple
|
||||
inheritance. Use _validate_prototype before this, we don't check
|
||||
for infinite recursion here.
|
||||
|
||||
"""
|
||||
if "prototype" in dic:
|
||||
# move backwards through the inheritance
|
||||
for prototype in make_iter(dic["prototype"]):
|
||||
# Build the prot dictionary in reverse order, overloading
|
||||
new_prot = _get_prototype(protparents.get(prototype, {}), prot, protparents)
|
||||
prot.update(new_prot)
|
||||
prot.update(dic)
|
||||
prot.pop("prototype", None) # we don't need this anymore
|
||||
return prot
|
||||
|
||||
|
||||
def _batch_create_object(*objparams):
|
||||
"""
|
||||
This is a cut-down version of the create_object() function,
|
||||
optimized for speed. It does NOT check and convert various input
|
||||
so make sure the spawned Typeclass works before using this!
|
||||
|
||||
Args:
|
||||
objsparams (tuple): Parameters for the respective creation/add
|
||||
handlers in the following order:
|
||||
- `create_kwargs` (dict): For use as new_obj = `ObjectDB(**create_kwargs)`.
|
||||
- `permissions` (str): Permission string used with `new_obj.batch_add(permission)`.
|
||||
- `lockstring` (str): Lockstring used with `new_obj.locks.add(lockstring)`.
|
||||
- `aliases` (list): A list of alias strings for
|
||||
adding with `new_object.aliases.batch_add(*aliases)`.
|
||||
- `nattributes` (list): list of tuples `(key, value)` to be loop-added to
|
||||
add with `new_obj.nattributes.add(*tuple)`.
|
||||
- `attributes` (list): list of tuples `(key, value[,category[,lockstring]])` for
|
||||
adding with `new_obj.attributes.batch_add(*attributes)`.
|
||||
- `tags` (list): list of tuples `(key, category)` for adding
|
||||
with `new_obj.tags.batch_add(*tags)`.
|
||||
- `execs` (list): Code strings to execute together with the creation
|
||||
of each object. They will be executed with `evennia` and `obj`
|
||||
(the newly created object) available in the namespace. Execution
|
||||
will happend after all other properties have been assigned and
|
||||
is intended for calling custom handlers etc.
|
||||
for the respective creation/add handlers in the following
|
||||
order: (create_kwargs, permissions, locks, aliases, nattributes,
|
||||
attributes, tags, execs)
|
||||
|
||||
Returns:
|
||||
objects (list): A list of created objects
|
||||
|
||||
Notes:
|
||||
The `exec` list will execute arbitrary python code so don't allow this to be available to
|
||||
unprivileged users!
|
||||
|
||||
"""
|
||||
|
||||
# bulk create all objects in one go
|
||||
|
||||
# unfortunately this doesn't work since bulk_create doesn't creates pks;
|
||||
# the result would be duplicate objects at the next stage, so we comment
|
||||
# it out for now:
|
||||
# dbobjs = _ObjectDB.objects.bulk_create(dbobjs)
|
||||
|
||||
dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams]
|
||||
objs = []
|
||||
for iobj, obj in enumerate(dbobjs):
|
||||
# call all setup hooks on each object
|
||||
objparam = objparams[iobj]
|
||||
# setup
|
||||
obj._createdict = {"permissions": make_iter(objparam[1]),
|
||||
"locks": objparam[2],
|
||||
"aliases": make_iter(objparam[3]),
|
||||
"nattributes": objparam[4],
|
||||
"attributes": objparam[5],
|
||||
"tags": make_iter(objparam[6])}
|
||||
# this triggers all hooks
|
||||
obj.save()
|
||||
# run eventual extra code
|
||||
for code in objparam[7]:
|
||||
if code:
|
||||
exec(code, {}, {"evennia": evennia, "obj": obj})
|
||||
objs.append(obj)
|
||||
return objs
|
||||
|
||||
|
||||
def spawn(*prototypes, **kwargs):
|
||||
"""
|
||||
Spawn a number of prototyped objects.
|
||||
|
||||
Args:
|
||||
prototypes (dict): Each argument should be a prototype
|
||||
dictionary.
|
||||
Kwargs:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
the global protparents dictionary accessible by the input
|
||||
prototypes. If not given, it will instead look for modules
|
||||
defined by settings.PROTOTYPE_MODULES.
|
||||
prototype_parents (dict): A dictionary holding a custom
|
||||
prototype-parent dictionary. Will overload same-named
|
||||
prototypes from prototype_modules.
|
||||
return_prototypes (bool): Only return a list of the
|
||||
prototype-parents (no object creation happens)
|
||||
|
||||
"""
|
||||
|
||||
protparents = {}
|
||||
protmodules = make_iter(kwargs.get("prototype_modules", []))
|
||||
if not protmodules and hasattr(settings, "PROTOTYPE_MODULES"):
|
||||
protmodules = make_iter(settings.PROTOTYPE_MODULES)
|
||||
for prototype_module in protmodules:
|
||||
protparents.update(dict((key, val) for key, val in
|
||||
all_from_module(prototype_module).items() if isinstance(val, dict)))
|
||||
# overload module's protparents with specifically given protparents
|
||||
protparents.update(kwargs.get("prototype_parents", {}))
|
||||
for key, prototype in protparents.items():
|
||||
_validate_prototype(key, prototype, protparents, [])
|
||||
|
||||
if "return_prototypes" in kwargs:
|
||||
# only return the parents
|
||||
return copy.deepcopy(protparents)
|
||||
|
||||
objsparams = []
|
||||
for prototype in prototypes:
|
||||
|
||||
_validate_prototype(None, prototype, protparents, [])
|
||||
prot = _get_prototype(prototype, {}, protparents)
|
||||
if not prot:
|
||||
continue
|
||||
|
||||
# extract the keyword args we need to create the object itself. If we get a callable,
|
||||
# call that to get the value (don't catch errors)
|
||||
create_kwargs = {}
|
||||
keyval = prot.pop("key", "Spawned Object %06i" % randint(1, 100000))
|
||||
create_kwargs["db_key"] = keyval() if callable(keyval) else keyval
|
||||
|
||||
locval = prot.pop("location", None)
|
||||
create_kwargs["db_location"] = locval() if callable(locval) else _handle_dbref(locval)
|
||||
|
||||
homval = prot.pop("home", settings.DEFAULT_HOME)
|
||||
create_kwargs["db_home"] = homval() if callable(homval) else _handle_dbref(homval)
|
||||
|
||||
destval = prot.pop("destination", None)
|
||||
create_kwargs["db_destination"] = destval() if callable(destval) else _handle_dbref(destval)
|
||||
|
||||
typval = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
|
||||
create_kwargs["db_typeclass_path"] = typval() if callable(typval) else typval
|
||||
|
||||
# extract calls to handlers
|
||||
permval = prot.pop("permissions", [])
|
||||
permission_string = permval() if callable(permval) else permval
|
||||
lockval = prot.pop("locks", "")
|
||||
lock_string = lockval() if callable(lockval) else lockval
|
||||
aliasval = prot.pop("aliases", "")
|
||||
alias_string = aliasval() if callable(aliasval) else aliasval
|
||||
tagval = prot.pop("tags", [])
|
||||
tags = tagval() if callable(tagval) else tagval
|
||||
attrval = prot.pop("attrs", [])
|
||||
attributes = attrval() if callable(tagval) else attrval
|
||||
|
||||
exval = prot.pop("exec", "")
|
||||
execs = make_iter(exval() if callable(exval) else exval)
|
||||
|
||||
# extract ndb assignments
|
||||
nattributes = dict((key.split("_", 1)[1], value() if callable(value) else value)
|
||||
for key, value in prot.items() if key.startswith("ndb_"))
|
||||
|
||||
# the rest are attributes
|
||||
simple_attributes = [(key, value()) if callable(value) else (key, value)
|
||||
for key, value in prot.items() if not key.startswith("ndb_")]
|
||||
attributes = attributes + simple_attributes
|
||||
attributes = [tup for tup in attributes if not tup[0] in _CREATE_OBJECT_KWARGS]
|
||||
|
||||
# pack for call into _batch_create_object
|
||||
objsparams.append((create_kwargs, permission_string, lock_string,
|
||||
alias_string, nattributes, attributes, tags, execs))
|
||||
|
||||
return _batch_create_object(*objsparams)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# testing
|
||||
|
||||
protparents = {
|
||||
"NOBODY": {},
|
||||
# "INFINITE" : {
|
||||
# "prototype":"INFINITE"
|
||||
# },
|
||||
"GOBLIN": {
|
||||
"key": "goblin grunt",
|
||||
"health": lambda: randint(20, 30),
|
||||
"resists": ["cold", "poison"],
|
||||
"attacks": ["fists"],
|
||||
"weaknesses": ["fire", "light"]
|
||||
},
|
||||
"GOBLIN_WIZARD": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
},
|
||||
"GOBLIN_ARCHER": {
|
||||
"prototype": "GOBLIN",
|
||||
"key": "goblin archer",
|
||||
"attacks": ["short bow"]
|
||||
},
|
||||
"ARCHWIZARD": {
|
||||
"attacks": ["archwizard staff"],
|
||||
},
|
||||
"GOBLIN_ARCHWIZARD": {
|
||||
"key": "goblin archwizard",
|
||||
"prototype": ("GOBLIN_WIZARD", "ARCHWIZARD")
|
||||
}
|
||||
}
|
||||
# test
|
||||
print([o.key for o in spawn(protparents["GOBLIN"],
|
||||
protparents["GOBLIN_ARCHWIZARD"],
|
||||
prototype_parents=protparents)])
|
||||
|
|
@ -58,7 +58,7 @@ class TestEvMenu(TestCase):
|
|||
|
||||
def _debug_output(self, indent, msg):
|
||||
if self.debug_output:
|
||||
print(" " * indent + msg)
|
||||
print(" " * indent + ansi.strip_ansi(msg))
|
||||
|
||||
def _test_menutree(self, menu):
|
||||
"""
|
||||
|
|
@ -82,6 +82,8 @@ class TestEvMenu(TestCase):
|
|||
self.assertIsNotNone(
|
||||
bool(node_text),
|
||||
"node: {}: node-text is None, which was not expected.".format(nodename))
|
||||
if isinstance(node_text, tuple):
|
||||
node_text, helptext = node_text
|
||||
node_text = ansi.strip_ansi(node_text.strip())
|
||||
self.assertTrue(
|
||||
node_text.startswith(compare_text),
|
||||
|
|
@ -168,6 +170,7 @@ class TestEvMenu(TestCase):
|
|||
self.caller2.msg = MagicMock()
|
||||
self.session = MagicMock()
|
||||
self.session2 = MagicMock()
|
||||
|
||||
self.menu = evmenu.EvMenu(self.caller, self.menutree, startnode=self.startnode,
|
||||
cmdset_mergetype=self.cmdset_mergetype,
|
||||
cmdset_priority=self.cmdset_priority,
|
||||
|
|
|
|||
|
|
@ -20,18 +20,20 @@ import textwrap
|
|||
import random
|
||||
from os.path import join as osjoin
|
||||
from importlib import import_module
|
||||
from inspect import ismodule, trace, getmembers, getmodule
|
||||
from inspect import ismodule, trace, getmembers, getmodule, getmro
|
||||
from collections import defaultdict, OrderedDict
|
||||
from twisted.internet import threads, reactor, task
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.apps import apps
|
||||
from evennia.utils import logger
|
||||
|
||||
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
|
||||
_EVENNIA_DIR = settings.EVENNIA_DIR
|
||||
_GAME_DIR = settings.GAME_DIR
|
||||
|
||||
|
||||
try:
|
||||
import cPickle as pickle
|
||||
except ImportError:
|
||||
|
|
@ -42,8 +44,6 @@ _GA = object.__getattribute__
|
|||
_SA = object.__setattr__
|
||||
_DA = object.__delattr__
|
||||
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
||||
def is_iter(iterable):
|
||||
"""
|
||||
|
|
@ -79,7 +79,7 @@ def make_iter(obj):
|
|||
return not hasattr(obj, '__iter__') and [obj] or obj
|
||||
|
||||
|
||||
def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
||||
def wrap(text, width=None, indent=0):
|
||||
"""
|
||||
Safely wrap text to a certain number of characters.
|
||||
|
||||
|
|
@ -92,6 +92,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
|||
text (str): Properly wrapped text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
if not text:
|
||||
return ""
|
||||
text = to_unicode(text)
|
||||
|
|
@ -103,7 +104,7 @@ def wrap(text, width=_DEFAULT_WIDTH, indent=0):
|
|||
fill = wrap
|
||||
|
||||
|
||||
def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
||||
def pad(text, width=None, align="c", fillchar=" "):
|
||||
"""
|
||||
Pads to a given width.
|
||||
|
||||
|
|
@ -118,6 +119,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
|||
text (str): The padded text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
align = align if align in ('c', 'l', 'r') else 'c'
|
||||
fillchar = fillchar[0] if fillchar else " "
|
||||
if align == 'l':
|
||||
|
|
@ -128,7 +130,7 @@ def pad(text, width=_DEFAULT_WIDTH, align="c", fillchar=" "):
|
|||
return text.center(width, fillchar)
|
||||
|
||||
|
||||
def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
||||
def crop(text, width=None, suffix="[...]"):
|
||||
"""
|
||||
Crop text to a certain width, throwing away text from too-long
|
||||
lines.
|
||||
|
|
@ -146,7 +148,7 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
|||
text (str): The cropped text.
|
||||
|
||||
"""
|
||||
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
utext = to_unicode(text)
|
||||
ltext = len(utext)
|
||||
if ltext <= width:
|
||||
|
|
@ -157,12 +159,16 @@ def crop(text, width=_DEFAULT_WIDTH, suffix="[...]"):
|
|||
return to_str(utext)
|
||||
|
||||
|
||||
def dedent(text):
|
||||
def dedent(text, baseline_index=None):
|
||||
"""
|
||||
Safely clean all whitespace at the left of a paragraph.
|
||||
|
||||
Args:
|
||||
text (str): The text to dedent.
|
||||
baseline_index (int or None, optional): Which row to use as a 'base'
|
||||
for the indentation. Lines will be dedented to this level but
|
||||
no further. If None, indent so as to completely deindent the
|
||||
least indented text.
|
||||
|
||||
Returns:
|
||||
text (str): Dedented string.
|
||||
|
|
@ -175,10 +181,17 @@ def dedent(text):
|
|||
"""
|
||||
if not text:
|
||||
return ""
|
||||
return textwrap.dedent(text)
|
||||
if baseline_index is None:
|
||||
return textwrap.dedent(text)
|
||||
else:
|
||||
lines = text.split('\n')
|
||||
baseline = lines[baseline_index]
|
||||
spaceremove = len(baseline) - len(baseline.lstrip(' '))
|
||||
return "\n".join(line[min(spaceremove, len(line) - len(line.lstrip(' '))):]
|
||||
for line in lines)
|
||||
|
||||
|
||||
def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
||||
def justify(text, width=None, align="f", indent=0):
|
||||
"""
|
||||
Fully justify a text so that it fits inside `width`. When using
|
||||
full justification (default) this will be done by padding between
|
||||
|
|
@ -197,6 +210,7 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
justified (str): The justified and indented block of text.
|
||||
|
||||
"""
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
def _process_line(line):
|
||||
"""
|
||||
|
|
@ -208,18 +222,27 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
gap = " " # minimum gap between words
|
||||
if line_rest > 0:
|
||||
if align == 'l':
|
||||
line[-1] += " " * line_rest
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] = " " * (line_rest-1) + "\n" + " " * width + "\n" + " " * width
|
||||
else:
|
||||
line[-1] += " " * line_rest
|
||||
elif align == 'r':
|
||||
line[0] = " " * line_rest + line[0]
|
||||
elif align == 'c':
|
||||
pad = " " * (line_rest // 2)
|
||||
line[0] = pad + line[0]
|
||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||
if line[-1] == "\n\n":
|
||||
line[-1] += pad + " " * (line_rest % 2 - 1) + \
|
||||
"\n" + " " * width + "\n" + " " * width
|
||||
else:
|
||||
line[-1] = line[-1] + pad + " " * (line_rest % 2)
|
||||
else: # align 'f'
|
||||
gap += " " * (line_rest // max(1, ngaps))
|
||||
rest_gap = line_rest % max(1, ngaps)
|
||||
for i in range(rest_gap):
|
||||
line[i] += " "
|
||||
elif not any(line):
|
||||
return [" " * width]
|
||||
return gap.join(line)
|
||||
|
||||
# split into paragraphs and words
|
||||
|
|
@ -260,6 +283,62 @@ def justify(text, width=_DEFAULT_WIDTH, align="f", indent=0):
|
|||
return "\n".join([indentstring + line for line in lines])
|
||||
|
||||
|
||||
def columnize(string, columns=2, spacing=4, align='l', width=None):
|
||||
"""
|
||||
Break a string into a number of columns, using as little
|
||||
vertical space as possible.
|
||||
|
||||
Args:
|
||||
string (str): The string to columnize.
|
||||
columns (int, optional): The number of columns to use.
|
||||
spacing (int, optional): How much space to have between columns.
|
||||
width (int, optional): The max width of the columns.
|
||||
Defaults to client's default width.
|
||||
|
||||
Returns:
|
||||
columns (str): Text divided into columns.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If given invalid values.
|
||||
|
||||
"""
|
||||
columns = max(1, columns)
|
||||
spacing = max(1, spacing)
|
||||
width = width if width else settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
w_spaces = (columns - 1) * spacing
|
||||
w_txt = max(1, width - w_spaces)
|
||||
|
||||
if w_spaces + columns > width: # require at least 1 char per column
|
||||
raise RuntimeError("Width too small to fit columns")
|
||||
|
||||
colwidth = int(w_txt / (1.0 * columns))
|
||||
|
||||
# first make a single column which we then split
|
||||
onecol = justify(string, width=colwidth, align=align)
|
||||
onecol = onecol.split("\n")
|
||||
|
||||
nrows, dangling = divmod(len(onecol), columns)
|
||||
nrows = [nrows + 1 if i < dangling else nrows for i in range(columns)]
|
||||
|
||||
height = max(nrows)
|
||||
cols = []
|
||||
istart = 0
|
||||
for irows in nrows:
|
||||
cols.append(onecol[istart:istart+irows])
|
||||
istart = istart + irows
|
||||
for col in cols:
|
||||
if len(col) < height:
|
||||
col.append(" " * colwidth)
|
||||
|
||||
sep = " " * spacing
|
||||
rows = []
|
||||
for irow in range(height):
|
||||
rows.append(sep.join(col[irow] for col in cols))
|
||||
|
||||
return "\n".join(rows)
|
||||
|
||||
|
||||
def list_to_string(inlist, endsep="and", addquote=False):
|
||||
"""
|
||||
This pretty-formats a list as string output, adding an optional
|
||||
|
|
@ -931,17 +1010,17 @@ def delay(timedelay, callback, *args, **kwargs):
|
|||
Delay the return of a value.
|
||||
|
||||
Args:
|
||||
timedelay (int or float): The delay in seconds
|
||||
callback (callable): Will be called with optional
|
||||
arguments after `timedelay` seconds.
|
||||
args (any, optional): Will be used as arguments to callback
|
||||
timedelay (int or float): The delay in seconds
|
||||
callback (callable): Will be called as `callback(*args, **kwargs)`
|
||||
after `timedelay` seconds.
|
||||
args (any, optional): Will be used as arguments to callback
|
||||
Kwargs:
|
||||
persistent (bool, optional): should make the delay persistent
|
||||
over a reboot or reload
|
||||
any (any): Will be used to call the callback.
|
||||
persistent (bool, optional): should make the delay persistent
|
||||
over a reboot or reload
|
||||
any (any): Will be used as keyword arguments to callback.
|
||||
|
||||
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
|
||||
commandhandler callback chain, the callback chain can 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:
|
||||
|
||||
```python
|
||||
ftable = format_table([[...], [...], ...])
|
||||
for ir, row in enumarate(ftable):
|
||||
if ir == 0:
|
||||
# make first row white
|
||||
|
|
@ -1879,3 +1959,29 @@ def get_game_dir_path():
|
|||
else:
|
||||
os.chdir(os.pardir)
|
||||
raise RuntimeError("server/conf/settings.py not found: Must start from inside game dir.")
|
||||
|
||||
|
||||
def get_all_typeclasses(parent=None):
|
||||
"""
|
||||
List available typeclasses from all available modules.
|
||||
|
||||
Args:
|
||||
parent (str, optional): If given, only return typeclasses inheriting (at any distance)
|
||||
from this parent.
|
||||
|
||||
Returns:
|
||||
typeclasses (dict): On the form {"typeclass.path": typeclass, ...}
|
||||
|
||||
Notes:
|
||||
This will dynamicall retrieve all abstract django models inheriting at any distance
|
||||
from the TypedObject base (aka a Typeclass) so it will work fine with any custom
|
||||
classes being added.
|
||||
|
||||
"""
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
typeclasses = {"{}.{}".format(model.__module__, model.__name__): model
|
||||
for model in apps.get_models() if TypedObject in getmro(model)}
|
||||
if parent:
|
||||
typeclasses = {name: typeclass for name, typeclass in typeclasses.items()
|
||||
if inherits_from(typeclass, parent)}
|
||||
return typeclasses
|
||||
|
|
|
|||
|
|
@ -8,10 +8,11 @@
|
|||
--- */
|
||||
|
||||
/* Overall element look */
|
||||
html, body, #clientwrapper { height: 100% }
|
||||
html, body {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: #000;
|
||||
color: #ccc;
|
||||
font-size: .9em;
|
||||
|
|
@ -19,6 +20,12 @@ body {
|
|||
line-height: 1.6em;
|
||||
overflow: hidden;
|
||||
}
|
||||
@media screen and (max-width: 480px) {
|
||||
body {
|
||||
font-size: .5rem;
|
||||
line-height: .7rem;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
a:link, a:visited { color: inherit; }
|
||||
|
|
@ -74,93 +81,109 @@ div {margin:0px;}
|
|||
}
|
||||
|
||||
/* Style specific classes corresponding to formatted, narative text. */
|
||||
|
||||
.wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Container surrounding entire client */
|
||||
#wrapper {
|
||||
position: relative;
|
||||
height: 100%
|
||||
#clientwrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* Main scrolling message area */
|
||||
|
||||
#messagewindow {
|
||||
position: absolute;
|
||||
overflow: auto;
|
||||
padding: 1em;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 70px;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Input area containing input field and button */
|
||||
#inputform {
|
||||
position: absolute;
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
bottom: 0;
|
||||
margin: 0;
|
||||
padding-bottom: 10px;
|
||||
border-top: 1px solid #555;
|
||||
}
|
||||
|
||||
#inputcontrol {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
#messagewindow {
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-wrap: break-word;
|
||||
}
|
||||
|
||||
/* Input field */
|
||||
#inputfield, #inputsend, #inputsizer {
|
||||
display: block;
|
||||
-webkit-box-sizing: border-box;
|
||||
-moz-box-sizing: border-box;
|
||||
box-sizing: border-box;
|
||||
height: 50px;
|
||||
#inputfield, #inputsizer {
|
||||
height: 100%;
|
||||
background: #000;
|
||||
color: #fff;
|
||||
padding: 0 .45em;
|
||||
font-size: 1.1em;
|
||||
padding: 0 .45rem;
|
||||
font-size: 1.1rem;
|
||||
font-family: 'DejaVu Sans Mono', Consolas, Inconsolata, 'Lucida Console', monospace;
|
||||
}
|
||||
|
||||
#inputfield, #inputsizer {
|
||||
float: left;
|
||||
width: 95%;
|
||||
border: 0;
|
||||
resize: none;
|
||||
line-height: normal;
|
||||
}
|
||||
#inputsend {
|
||||
height: 100%;
|
||||
}
|
||||
#inputcontrol {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
#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 {
|
||||
margin-top: 10px;
|
||||
padding: 0 .45em;
|
||||
.prompt {
|
||||
max-height: 3rem;
|
||||
}
|
||||
|
||||
#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 {
|
||||
width: 40px;
|
||||
font-size: 20px;
|
||||
width: 2rem;
|
||||
font-size: 2rem;
|
||||
color: #a6a6a6;
|
||||
background-color: transparent;
|
||||
border: 0px;
|
||||
|
|
@ -173,8 +196,8 @@ div {margin:0px;}
|
|||
|
||||
#toolbar {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 5px;
|
||||
top: .5rem;
|
||||
right: .5rem;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
|
|
@ -248,6 +271,52 @@ div {margin:0px;}
|
|||
text-decoration: none;
|
||||
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 */
|
||||
|
||||
|
|
|
|||
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 () {
|
||||
"use strict"
|
||||
|
||||
var num_splits = 0; //unique id counter for default split-panel names
|
||||
|
||||
var options = {};
|
||||
|
||||
var known_types = new Array();
|
||||
known_types.push('help');
|
||||
|
||||
//
|
||||
// GUI Elements
|
||||
//
|
||||
|
|
@ -106,6 +111,7 @@ function togglePopup(dialogname, content) {
|
|||
|
||||
// Grab text from inputline and send to Evennia
|
||||
function doSendText() {
|
||||
console.log("sending text");
|
||||
if (!Evennia.isConnected()) {
|
||||
var reconnect = confirm("Not currently connected. Reconnect?");
|
||||
if (reconnect) {
|
||||
|
|
@ -158,7 +164,11 @@ function onKeydown (event) {
|
|||
var code = event.which;
|
||||
var history_entry = null;
|
||||
var inputfield = $("#inputfield");
|
||||
inputfield.focus();
|
||||
if (code === 9) {
|
||||
return;
|
||||
}
|
||||
|
||||
//inputfield.focus();
|
||||
|
||||
if (code === 13) { // Enter key sends text
|
||||
doSendText();
|
||||
|
|
@ -205,74 +215,68 @@ function onKeyPress (event) {
|
|||
}
|
||||
|
||||
var resizeInputField = function () {
|
||||
var min_height = 50;
|
||||
var max_height = 300;
|
||||
var prev_text_len = 0;
|
||||
return function() {
|
||||
var wrapper = $("#inputform")
|
||||
var input = $("#inputcontrol")
|
||||
var prompt = $("#prompt")
|
||||
|
||||
// Check to see if we should change the height of the input area
|
||||
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;
|
||||
input.height(wrapper.height() - (input.offset().top - wrapper.offset().top));
|
||||
}
|
||||
}();
|
||||
|
||||
// Handle resizing of client
|
||||
function doWindowResize() {
|
||||
var formh = $('#inputform').outerHeight(true);
|
||||
var message_scrollh = $("#messagewindow").prop("scrollHeight");
|
||||
$("#messagewindow")
|
||||
.css({"bottom": formh}) // leave space for the input form
|
||||
.scrollTop(message_scrollh); // keep the output window scrolled to the bottom
|
||||
resizeInputField();
|
||||
var resizable = $("[data-update-append]");
|
||||
var parents = resizable.closest(".split")
|
||||
parents.animate({
|
||||
scrollTop: parents.prop("scrollHeight")
|
||||
}, 0);
|
||||
}
|
||||
|
||||
// Handle text coming from the server
|
||||
function onText(args, kwargs) {
|
||||
// append message to previous ones, then scroll so latest is at
|
||||
// the bottom. Send 'cls' kwarg to modify the output class.
|
||||
var renderto = "main";
|
||||
if (kwargs["type"] == "help") {
|
||||
if (("helppopup" in options) && (options["helppopup"])) {
|
||||
renderto = "#helpdialog";
|
||||
var use_default_pane = true;
|
||||
|
||||
if ( kwargs && 'type' in kwargs ) {
|
||||
var msgtype = kwargs['type'];
|
||||
if ( ! known_types.includes(msgtype) ) {
|
||||
// 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 cls = kwargs == null ? 'out' : kwargs['cls'];
|
||||
mwin.append("<div class='" + cls + "'>" + args[0] + "</div>");
|
||||
mwin.animate({
|
||||
scrollTop: document.getElementById("messagewindow").scrollHeight
|
||||
}, 0);
|
||||
var scrollHeight = mwin.parent().parent().prop("scrollHeight");
|
||||
mwin.parent().parent().animate({ scrollTop: scrollHeight }, 0);
|
||||
|
||||
onNewLine(args[0], null);
|
||||
} else {
|
||||
openPopup(renderto, args[0]);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -430,6 +434,105 @@ function doStartDragDialog(event) {
|
|||
$(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
|
||||
//
|
||||
|
|
@ -437,6 +540,18 @@ function doStartDragDialog(event) {
|
|||
// Event when client finishes loading
|
||||
$(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) {
|
||||
Notification.requestPermission();
|
||||
}
|
||||
|
|
@ -453,7 +568,7 @@ $(document).ready(function() {
|
|||
|
||||
//$(document).on("visibilitychange", onVisibilityChange);
|
||||
|
||||
$("#inputfield").bind("resize", doWindowResize)
|
||||
$("[data-role-input]").bind("resize", doWindowResize)
|
||||
.keypress(onKeyPress)
|
||||
.bind("paste", resizeInputField)
|
||||
.bind("cut", resizeInputField);
|
||||
|
|
@ -506,6 +621,7 @@ $(document).ready(function() {
|
|||
},
|
||||
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 name="author" 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" %}>
|
||||
|
||||
|
|
@ -20,7 +24,7 @@ JQuery available.
|
|||
|
||||
<!-- Import JQuery and warn if there is a problem -->
|
||||
{% 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 %}
|
||||
|
||||
<script type="text/javascript" charset="utf-8">
|
||||
|
|
@ -29,6 +33,14 @@ JQuery available.
|
|||
}
|
||||
</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-->
|
||||
<script language="javascript" type="text/javascript">
|
||||
{% if websocket_enabled %}
|
||||
|
|
@ -51,6 +63,12 @@ JQuery available.
|
|||
</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 -->
|
||||
{% block guilib_import %}
|
||||
<script src={% static "webclient/js/webclient_gui.js" %} language="javascript" type="text/javascript" charset="utf-8"></script>
|
||||
|
|
@ -63,7 +81,11 @@ JQuery available.
|
|||
}
|
||||
</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>
|
||||
<body>
|
||||
|
||||
|
|
@ -86,10 +108,9 @@ JQuery available.
|
|||
</div>
|
||||
|
||||
<!-- main client -->
|
||||
<div id=clientwrapper>
|
||||
<div id=clientwrapper class="d-none">
|
||||
{% block client %}
|
||||
{% endblock %}
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -8,20 +8,29 @@
|
|||
|
||||
|
||||
{% 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">
|
||||
<div id="toolbar">
|
||||
<button id="optionsbutton" type="button" class="hidden">⚙</button>
|
||||
</div>
|
||||
<div id="messagewindow" role="log"></div>
|
||||
<div id="inputform">
|
||||
<div id="prompt"></div>
|
||||
<div id="inputcontrol">
|
||||
<textarea id="inputfield" type="text"></textarea>
|
||||
<input id="inputsend" type="button" value=">"/>
|
||||
<!-- The "Main" Content -->
|
||||
<div id="main" class="split split-vertical" data-role-default>
|
||||
<div id="main-sub" class="split-sub">
|
||||
<div id="messagewindow"></div>
|
||||
</div>
|
||||
</div>
|
||||
<!-- The "Input" Pane -->
|
||||
<div id="input" class="split split-vertical" data-role-input data-update-append></div>
|
||||
|
||||
<!-- Basic UI Components -->
|
||||
<div id="splitdialog" class="dialog">
|
||||
<div class="dialogtitle">Split Pane<span class="dialogclose">×</span></div>
|
||||
<div class="dialogcontentparent">
|
||||
<div id="splitdialogcontent" class="dialogcontent">
|
||||
</div>
|
||||
</div>
|
||||
<div id="inputsizer"></div>
|
||||
</div>
|
||||
|
||||
<div id="optionsdialog" class="dialog">
|
||||
|
|
@ -47,4 +56,29 @@
|
|||
</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 %}
|
||||
|
|
|
|||
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
django > 1.10, < 2.0
|
||||
twisted == 16.0.0
|
||||
mock >= 1.0.1
|
||||
pillow == 2.9.0
|
||||
pytz
|
||||
future >= 0.15.2
|
||||
django-sekizai
|
||||
inflect
|
||||
|
||||
mock >= 1.0.1
|
||||
anything
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue