Fix merge conflicts
This commit is contained in:
commit
1290f648d5
20 changed files with 769 additions and 761 deletions
|
|
@ -166,6 +166,12 @@ Up requirements to Django 4.0+, Twisted 22+, Python 3.9 or 3.10
|
||||||
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
- New `at_pre_object_receive(obj, source_location)` method on Objects. Called on
|
||||||
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
destination, mimicking behavior of `at_pre_move` hook - returning False will abort move.
|
||||||
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
- New `at_pre_object_leave(obj, destination)` method on Objects. Called on
|
||||||
|
- The db pickle-serializer now checks for methods `__serialize_dbobjs__` and `__deserialize_dbobjs__`
|
||||||
|
to allow custom packing/unpacking of nested dbobjs, to allow storing in Attribute.
|
||||||
|
- Optimizations to rpsystem contrib performance. Breaking change: `.get_sdesc()` will
|
||||||
|
now return `None` instead of `.db.desc` if no sdesc is set; fallback in hook (inspectorCaracal)
|
||||||
|
- Reworked text2html parser to avoid problems with stateful color tags (inspectorCaracal)
|
||||||
|
- Simplified `EvMenu.options_formatter` hook to use `EvColumn` and f-strings (inspectorcaracal)
|
||||||
|
|
||||||
|
|
||||||
## Evennia 0.9.5
|
## Evennia 0.9.5
|
||||||
|
|
|
||||||
|
|
@ -193,7 +193,7 @@ or in the chat.
|
||||||
|
|
||||||
[pep8]: http://www.python.org/dev/peps/pep-0008
|
[pep8]: http://www.python.org/dev/peps/pep-0008
|
||||||
[pep8tool]: https://pypi.python.org/pypi/pep8
|
[pep8tool]: https://pypi.python.org/pypi/pep8
|
||||||
[googlestyle]: http://www.sphinx-doc.org/en/stable/ext/example_google.html
|
[googlestyle]: https://www.sphinx-doc.org/en/master/usage/extensions/example_google.html
|
||||||
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
|
[githubmarkdown]: https://help.github.com/articles/github-flavored-markdown/
|
||||||
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
|
[markdown-hilight]: https://help.github.com/articles/github-flavored-markdown/#syntax-highlighting
|
||||||
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy
|
[command-docstrings]: https://github.com/evennia/evennia/wiki/Using%20MUX%20As%20a%20Standard#documentation-policy
|
||||||
|
|
|
||||||
|
|
@ -152,6 +152,8 @@ mv-local:
|
||||||
@echo "Documentation built (multiversion + autodocs)."
|
@echo "Documentation built (multiversion + autodocs)."
|
||||||
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
|
@echo "To see result, open evennia/docs/build/html/<version>/index.html in a browser."
|
||||||
|
|
||||||
|
# note - don't run the following manually, the result will clash with the result
|
||||||
|
# of the github actions!
|
||||||
deploy:
|
deploy:
|
||||||
make _multiversion-deploy
|
make _multiversion-deploy
|
||||||
@echo "Documentation deployed."
|
@echo "Documentation deployed."
|
||||||
|
|
|
||||||
|
|
@ -328,13 +328,13 @@ values into a string representation before storing it to the database. This is d
|
||||||
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
|
With a single object, we mean anything that is *not iterable*, like numbers, strings or custom class
|
||||||
instances without the `__iter__` method.
|
instances without the `__iter__` method.
|
||||||
|
|
||||||
* You can generally store any non-iterable Python entity that can be pickled.
|
* You can generally store any non-iterable Python entity that can be _pickled_.
|
||||||
* Single database objects/typeclasses can be stored, despite them normally not being possible
|
* Single database objects/typeclasses can be stored, despite them normally not being possible
|
||||||
to pickle. Evennia wil convert them to an internal representation using their classname,
|
to pickle. Evennia will convert them to an internal representation using theihr classname,
|
||||||
database-id and creation-date with a microsecond precision. When retrieving, the object
|
database-id and creation-date with a microsecond precision. When retrieving, the object
|
||||||
instance will be re-fetched from the database using this information.
|
instance will be re-fetched from the database using this information.
|
||||||
* To convert the database object, Evennia must know it's there. If you *hide* a database object
|
* If you 'hide' a db-obj as a property on a custom class, Evennia will not be
|
||||||
inside a non-iterable class, you will run into errors - this is not supported!
|
able to find it to serialize it. For that you need to help it out (see below).
|
||||||
|
|
||||||
```{code-block} python
|
```{code-block} python
|
||||||
:caption: Valid assignments
|
:caption: Valid assignments
|
||||||
|
|
@ -345,16 +345,55 @@ obj.db.test1 = False
|
||||||
# a database object (will be stored as an internal representation)
|
# a database object (will be stored as an internal representation)
|
||||||
obj.db.test2 = myobj
|
obj.db.test2 = myobj
|
||||||
```
|
```
|
||||||
|
|
||||||
|
As mentioned, Evennia will not be able to automatically serialize db-objects
|
||||||
|
'hidden' in arbitrary properties on an object. This will lead to an error
|
||||||
|
when saving the Attribute.
|
||||||
|
|
||||||
```{code-block} python
|
```{code-block} python
|
||||||
:caption: Invalid, 'hidden' dbobject
|
:caption: Invalid, 'hidden' dbobject
|
||||||
|
# example of storing an invalid, "hidden" dbobject in Attribute
|
||||||
# example of an invalid, "hidden" dbobject
|
|
||||||
class Container:
|
class Container:
|
||||||
def __init__(self, mydbobj):
|
def __init__(self, mydbobj):
|
||||||
# no way for Evennia to know this is a database object!
|
# no way for Evennia to know this is a database object!
|
||||||
self.mydbobj = mydbobj
|
self.mydbobj = mydbobj
|
||||||
|
|
||||||
|
# let's assume myobj is a db-object
|
||||||
container = Container(myobj)
|
container = Container(myobj)
|
||||||
obj.db.invalid = container # will cause error!
|
obj.db.mydata = container # will raise error!
|
||||||
|
|
||||||
|
```
|
||||||
|
|
||||||
|
By adding two methods `__serialize_dbobjs__` and `__deserialize_dbobjs__` to the
|
||||||
|
object you want to save, you can pre-serialize and post-deserialize all 'hidden'
|
||||||
|
objects before Evennia's main serializer gets to work. Inside these methods, use Evennia's
|
||||||
|
[evennia.utils.dbserialize.dbserialize](api:evennia.utils.dbserialize.dbserialize) and
|
||||||
|
[dbunserialize](api:evennia.utils.dbserialize.dbunserialize) functions to safely
|
||||||
|
serialize the db-objects you want to store.
|
||||||
|
|
||||||
|
```{code-block} python
|
||||||
|
:caption: Fixing an invalid 'hidden' dbobj for storing in Attribute
|
||||||
|
|
||||||
|
from evennia.utils import dbserialize # important
|
||||||
|
|
||||||
|
class Container:
|
||||||
|
def __init__(self, mydbobj):
|
||||||
|
# A 'hidden' db-object
|
||||||
|
self.mydbobj = mydbobj
|
||||||
|
|
||||||
|
def __serialize_dbobjs__(self):
|
||||||
|
"""This is called before serialization and allows
|
||||||
|
us to custom-handle those 'hidden' dbobjs"""
|
||||||
|
self.mydbobj = dbserialize.dbserialize(self.mydbobj
|
||||||
|
|
||||||
|
def __deserialize_dbobjs__(self):
|
||||||
|
"""This is called after deserialization and allows you to
|
||||||
|
restore the 'hidden' dbobjs you serialized before"""
|
||||||
|
self.mydbobj = dbserialize.dbunserialize(self.mydbobj)
|
||||||
|
|
||||||
|
# let's assume myobj is a db-object
|
||||||
|
container = Container(myobj)
|
||||||
|
obj.db.mydata = container # will now work fine!
|
||||||
```
|
```
|
||||||
|
|
||||||
### Storing multiple objects
|
### Storing multiple objects
|
||||||
|
|
@ -404,6 +443,12 @@ obj.db.test8[2]["test"] = 5
|
||||||
# test8 is now [4,2,{"test":5}]
|
# test8 is now [4,2,{"test":5}]
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Note that if make some advanced iterable object, and store an db-object on it in
|
||||||
|
a way such that it is _not_ returned by iterating over it, you have created a
|
||||||
|
'hidden' db-object. See [the previous section](#storing-single-objects) for how
|
||||||
|
to tell Evennia how to serialize such hidden objects safely.
|
||||||
|
|
||||||
|
|
||||||
### Retrieving Mutable objects
|
### Retrieving Mutable objects
|
||||||
|
|
||||||
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
|
A side effect of the way Evennia stores Attributes is that *mutable* iterables (iterables that can
|
||||||
|
|
|
||||||
|
|
@ -129,10 +129,11 @@ Contains a very useful list of things to think about when starting your new MUD.
|
||||||
Essential reading for the design of any persistent game
|
Essential reading for the design of any persistent game
|
||||||
world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as
|
world, written by the co-creator of the original game *MUD*. Published in 2003 but it's still as
|
||||||
relevant now as when it came out. Covers everything you need to know and then some.
|
relevant now as when it came out. Covers everything you need to know and then some.
|
||||||
- Zed A. Shaw *Learn Python the Hard way* ([homepage](https://learnpythonthehardway.org/)) - Despite
|
|
||||||
the imposing name this book is for the absolute Python/programming beginner. One learns the language
|
When the rights to Designing Virtual Worlds returned to him, Richard Bartle
|
||||||
by gradually creating a small text game! It has been used by multiple users before moving on to
|
made the PDF of his Designing Virtual Worlds freely available through his own
|
||||||
Evennia. *Update: This used to be free to read online, this is no longer the case.*
|
website ([Designing Virtual Worlds](https://mud.co.uk/dvw/)). A direct link to
|
||||||
|
the PDF can be found [here](https://mud.co.uk/richard/DesigningVirtualWorlds.pdf).
|
||||||
- David M. Beazley *Python Essential Reference (4th ed)*
|
- David M. Beazley *Python Essential Reference (4th ed)*
|
||||||
([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) -
|
([amazon page](https://www.amazon.com/Python-Essential-Reference-David-Beazley/dp/0672329786/)) -
|
||||||
Our recommended book on Python; it not only efficiently summarizes the language but is also
|
Our recommended book on Python; it not only efficiently summarizes the language but is also
|
||||||
|
|
|
||||||
|
|
@ -328,4 +328,4 @@ class GametimeScript(DefaultScript):
|
||||||
callback()
|
callback()
|
||||||
|
|
||||||
seconds = real_seconds_until(**self.db.gametime)
|
seconds = real_seconds_until(**self.db.gametime)
|
||||||
self.restart(interval=seconds)
|
self.start(interval=seconds,force_restart=True)
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ Roleplaying emotes and language - Griatch, 2015
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
|
from .rpsystem import EmoteError, SdescError, RecogError, LanguageError # noqa
|
||||||
from .rpsystem import ordered_permutation_regex, regex_tuple_from_key_alias # noqa
|
|
||||||
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
|
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
|
||||||
from .rpsystem import SdescHandler, RecogHandler # noqa
|
from .rpsystem import SdescHandler, RecogHandler # noqa
|
||||||
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa
|
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa
|
||||||
|
|
|
||||||
|
|
@ -53,7 +53,7 @@ Add `RPSystemCmdSet` from this module to your CharacterCmdSet:
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
|
|
||||||
from evennia.contrib.rpg.rpsystem import RPSystemCmdSet <---
|
from evennia.contrib.rpg.rpsystem.rpsystem import RPSystemCmdSet <---
|
||||||
|
|
||||||
class CharacterCmdSet(default_cmds.CharacterCmdset):
|
class CharacterCmdSet(default_cmds.CharacterCmdset):
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -69,7 +69,7 @@ the typeclasses in this module:
|
||||||
```python
|
```python
|
||||||
# in mygame/typeclasses/characters.py
|
# in mygame/typeclasses/characters.py
|
||||||
|
|
||||||
from evennia.contrib.rpg import ContribRPCharacter
|
from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter
|
||||||
|
|
||||||
class Character(ContribRPCharacter):
|
class Character(ContribRPCharacter):
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -79,7 +79,7 @@ class Character(ContribRPCharacter):
|
||||||
```python
|
```python
|
||||||
# in mygame/typeclasses/objects.py
|
# in mygame/typeclasses/objects.py
|
||||||
|
|
||||||
from evennia.contrib.rpg import ContribRPObject
|
from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject
|
||||||
|
|
||||||
class Object(ContribRPObject):
|
class Object(ContribRPObject):
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -89,7 +89,7 @@ class Object(ContribRPObject):
|
||||||
```python
|
```python
|
||||||
# in mygame/typeclasses/rooms.py
|
# in mygame/typeclasses/rooms.py
|
||||||
|
|
||||||
from evennia.contrib.rpg import ContribRPRoom
|
from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom
|
||||||
|
|
||||||
class Room(ContribRPRoom):
|
class Room(ContribRPRoom):
|
||||||
# ...
|
# ...
|
||||||
|
|
@ -125,7 +125,7 @@ Extra Installation Instructions:
|
||||||
|
|
||||||
1. In typeclasses/character.py:
|
1. In typeclasses/character.py:
|
||||||
Import the `ContribRPCharacter` class:
|
Import the `ContribRPCharacter` class:
|
||||||
`from evennia.contrib.rpg.rpsystem import ContribRPCharacter`
|
`from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPCharacter`
|
||||||
Inherit ContribRPCharacter:
|
Inherit ContribRPCharacter:
|
||||||
Change "class Character(DefaultCharacter):" to
|
Change "class Character(DefaultCharacter):" to
|
||||||
`class Character(ContribRPCharacter):`
|
`class Character(ContribRPCharacter):`
|
||||||
|
|
@ -133,13 +133,13 @@ Extra Installation Instructions:
|
||||||
Add `super().at_object_creation()` as the top line.
|
Add `super().at_object_creation()` as the top line.
|
||||||
2. In `typeclasses/rooms.py`:
|
2. In `typeclasses/rooms.py`:
|
||||||
Import the `ContribRPRoom` class:
|
Import the `ContribRPRoom` class:
|
||||||
`from evennia.contrib.rpg.rpsystem import ContribRPRoom`
|
`from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPRoom`
|
||||||
Inherit `ContribRPRoom`:
|
Inherit `ContribRPRoom`:
|
||||||
Change `class Room(DefaultRoom):` to
|
Change `class Room(DefaultRoom):` to
|
||||||
`class Room(ContribRPRoom):`
|
`class Room(ContribRPRoom):`
|
||||||
3. In `typeclasses/objects.py`
|
3. In `typeclasses/objects.py`
|
||||||
Import the `ContribRPObject` class:
|
Import the `ContribRPObject` class:
|
||||||
`from evennia.contrib.rpg.rpsystem import ContribRPObject`
|
`from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject`
|
||||||
Inherit `ContribRPObject`:
|
Inherit `ContribRPObject`:
|
||||||
Change `class Object(DefaultObject):` to
|
Change `class Object(DefaultObject):` to
|
||||||
`class Object(ContribRPObject):`
|
`class Object(ContribRPObject):`
|
||||||
|
|
@ -149,18 +149,15 @@ Extra Installation Instructions:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
import re
|
import re
|
||||||
from re import escape as re_escape
|
from string import punctuation
|
||||||
import itertools
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.objects.objects import DefaultObject, DefaultCharacter
|
from evennia.objects.objects import DefaultObject, DefaultCharacter
|
||||||
from evennia.objects.models import ObjectDB
|
from evennia.objects.models import ObjectDB
|
||||||
from evennia.commands.command import Command
|
from evennia.commands.command import Command
|
||||||
from evennia.commands.cmdset import CmdSet
|
from evennia.commands.cmdset import CmdSet
|
||||||
from evennia.utils import ansi
|
from evennia.utils import ansi, logger
|
||||||
from evennia.utils.utils import lazy_property, make_iter, variable_from_module
|
from evennia.utils.utils import lazy_property, make_iter, variable_from_module
|
||||||
|
|
||||||
_REGEX_TUPLE_CACHE = {}
|
|
||||||
|
|
||||||
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
|
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# Emote parser
|
# Emote parser
|
||||||
|
|
@ -189,13 +186,13 @@ _EMOTE_MULTIMATCH_ERROR = """|RMultiple possibilities for {ref}:
|
||||||
|
|
||||||
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
|
_RE_FLAGS = re.MULTILINE + re.IGNORECASE + re.UNICODE
|
||||||
|
|
||||||
_RE_PREFIX = re.compile(r"^%s" % _PREFIX, re.UNICODE)
|
_RE_PREFIX = re.compile(rf"^{_PREFIX}", re.UNICODE)
|
||||||
|
|
||||||
# This regex will return groups (num, word), where num is an optional counter to
|
# This regex will return groups (num, word), where num is an optional counter to
|
||||||
# separate multimatches from one another and word is the first word in the
|
# separate multimatches from one another and word is the first word in the
|
||||||
# marker. So entering "/tall man" will return groups ("", "tall")
|
# marker. So entering "/tall man" will return groups ("", "tall")
|
||||||
# and "/2-tall man" will return groups ("2", "tall").
|
# and "/2-tall man" will return groups ("2", "tall").
|
||||||
_RE_OBJ_REF_START = re.compile(r"%s(?:([0-9]+)%s)*(\w+)" % (_PREFIX, _NUM_SEP), _RE_FLAGS)
|
_RE_OBJ_REF_START = re.compile(rf"{_PREFIX}(?:([0-9]+){_NUM_SEP})*(\w+)", _RE_FLAGS)
|
||||||
|
|
||||||
_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
|
_RE_LEFT_BRACKETS = re.compile(r"\{+", _RE_FLAGS)
|
||||||
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
|
_RE_RIGHT_BRACKETS = re.compile(r"\}+", _RE_FLAGS)
|
||||||
|
|
@ -239,97 +236,7 @@ class LanguageError(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def _dummy_process(text, *args, **kwargs):
|
|
||||||
"Pass-through processor"
|
|
||||||
return text
|
|
||||||
|
|
||||||
|
|
||||||
# emoting mechanisms
|
# emoting mechanisms
|
||||||
|
|
||||||
|
|
||||||
def ordered_permutation_regex(sentence):
|
|
||||||
"""
|
|
||||||
Builds a regex that matches 'ordered permutations' of a sentence's
|
|
||||||
words.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sentence (str): The sentence to build a match pattern to
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
regex (re object): Compiled regex object represented the
|
|
||||||
possible ordered permutations of the sentence, from longest to
|
|
||||||
shortest.
|
|
||||||
Example:
|
|
||||||
The sdesc_regex for an sdesc of " very tall man" will
|
|
||||||
result in the following allowed permutations,
|
|
||||||
regex-matched in inverse order of length (case-insensitive):
|
|
||||||
"the very tall man", "the very tall", "very tall man",
|
|
||||||
"very tall", "the very", "tall man", "the", "very", "tall",
|
|
||||||
and "man".
|
|
||||||
We also add regex to make sure it also accepts num-specifiers,
|
|
||||||
like /2-tall.
|
|
||||||
|
|
||||||
"""
|
|
||||||
# escape {#nnn} markers from sentence, replace with nnn
|
|
||||||
sentence = _RE_REF.sub(r"\1", sentence)
|
|
||||||
# escape {##nnn} markers, replace with nnn
|
|
||||||
sentence = _RE_REF_LANG.sub(r"\1", sentence)
|
|
||||||
# escape self-ref marker from sentence
|
|
||||||
sentence = _RE_SELF_REF.sub(r"", sentence)
|
|
||||||
|
|
||||||
# ordered permutation algorithm
|
|
||||||
words = sentence.split()
|
|
||||||
combinations = itertools.product((True, False), repeat=len(words))
|
|
||||||
solution = []
|
|
||||||
for combination in combinations:
|
|
||||||
comb = []
|
|
||||||
for iword, word in enumerate(words):
|
|
||||||
if combination[iword]:
|
|
||||||
comb.append(word)
|
|
||||||
elif comb:
|
|
||||||
break
|
|
||||||
if comb:
|
|
||||||
solution.append(
|
|
||||||
_PREFIX
|
|
||||||
+ r"[0-9]*%s*%s(?=\W|$)+" % (_NUM_SEP, re_escape(" ".join(comb)).rstrip("\\"))
|
|
||||||
)
|
|
||||||
|
|
||||||
# combine into a match regex, first matching the longest down to the shortest components
|
|
||||||
regex = r"|".join(sorted(set(solution), key=lambda item: (-len(item), item)))
|
|
||||||
return regex
|
|
||||||
|
|
||||||
|
|
||||||
def regex_tuple_from_key_alias(obj):
|
|
||||||
"""
|
|
||||||
This will build a regex tuple for any object, not just from those
|
|
||||||
with sdesc/recog handlers. It's used as a legacy mechanism for
|
|
||||||
being able to mix this contrib with objects not using sdescs, but
|
|
||||||
note that creating the ordered permutation regex dynamically for
|
|
||||||
every object will add computational overhead.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
obj (Object): This object's key and eventual aliases will
|
|
||||||
be used to build the tuple.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
regex_tuple (tuple): A tuple
|
|
||||||
(ordered_permutation_regex, obj, key/alias)
|
|
||||||
|
|
||||||
|
|
||||||
"""
|
|
||||||
global _REGEX_TUPLE_CACHE
|
|
||||||
permutation_string = " ".join([obj.key] + obj.aliases.all())
|
|
||||||
cache_key = f"{obj.id} {permutation_string}"
|
|
||||||
|
|
||||||
if cache_key not in _REGEX_TUPLE_CACHE:
|
|
||||||
_REGEX_TUPLE_CACHE[cache_key] = (
|
|
||||||
re.compile(ordered_permutation_regex(permutation_string), _RE_FLAGS),
|
|
||||||
obj,
|
|
||||||
obj.key,
|
|
||||||
)
|
|
||||||
return _REGEX_TUPLE_CACHE[cache_key]
|
|
||||||
|
|
||||||
|
|
||||||
def parse_language(speaker, emote):
|
def parse_language(speaker, emote):
|
||||||
"""
|
"""
|
||||||
Parse the emote for language. This is
|
Parse the emote for language. This is
|
||||||
|
|
@ -375,9 +282,9 @@ def parse_language(speaker, emote):
|
||||||
langname, saytext = say_match.groups()
|
langname, saytext = say_match.groups()
|
||||||
istart, iend = say_match.start(), say_match.end()
|
istart, iend = say_match.start(), say_match.end()
|
||||||
# the key is simply the running match in the emote
|
# the key is simply the running match in the emote
|
||||||
key = "##%i" % imatch
|
key = f"##{imatch}"
|
||||||
# replace say with ref markers in emote
|
# replace say with ref markers in emote
|
||||||
emote = emote[:istart] + "{%s}" % key + emote[iend:]
|
emote = "{start}{{{key}}}{end}".format( start=emote[:istart], key=key, end=emote[iend:] )
|
||||||
mapping[key] = (langname, saytext)
|
mapping[key] = (langname, saytext)
|
||||||
|
|
||||||
if errors:
|
if errors:
|
||||||
|
|
@ -430,24 +337,20 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
- says, "..." are
|
- says, "..." are
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# Load all candidate regex tuples [(regex, obj, sdesc/recog),...]
|
# build a list of candidates with all possible referrable names
|
||||||
candidate_regexes = (
|
# include 'me' keyword for self-ref
|
||||||
([(_RE_SELF_REF, sender, sender.sdesc.get())] if hasattr(sender, "sdesc") else [])
|
candidate_map = [(sender, 'me')]
|
||||||
+ (
|
for obj in candidates:
|
||||||
[sender.recog.get_regex_tuple(obj) for obj in candidates]
|
# check if sender has any recogs for obj and add
|
||||||
if hasattr(sender, "recog")
|
if hasattr(sender, "recog"):
|
||||||
else []
|
if recog := sender.recog.get(obj):
|
||||||
)
|
candidate_map.append((obj, recog))
|
||||||
+ [obj.sdesc.get_regex_tuple() for obj in candidates if hasattr(obj, "sdesc")]
|
# check if obj has an sdesc and add
|
||||||
+ [
|
if hasattr(obj, "sdesc"):
|
||||||
regex_tuple_from_key_alias(obj) # handle objects without sdescs
|
candidate_map.append((obj, obj.sdesc.get()))
|
||||||
for obj in candidates
|
# if no sdesc, include key plus aliases instead
|
||||||
if not (hasattr(obj, "recog") and hasattr(obj, "sdesc"))
|
else:
|
||||||
]
|
candidate_map.extend( [(obj, obj.key)] + [(obj, alias) for alias in obj.aliases.all()] )
|
||||||
)
|
|
||||||
|
|
||||||
# filter out non-found data
|
|
||||||
candidate_regexes = [tup for tup in candidate_regexes if tup]
|
|
||||||
|
|
||||||
# escape mapping syntax on the form {#id} if it exists already in emote,
|
# escape mapping syntax on the form {#id} if it exists already in emote,
|
||||||
# if so it is replaced with just "id".
|
# if so it is replaced with just "id".
|
||||||
|
|
@ -468,18 +371,48 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
|
|
||||||
# first see if there is a number given (e.g. 1-tall)
|
# first see if there is a number given (e.g. 1-tall)
|
||||||
num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None
|
num_identifier, _ = marker_match.groups("") # return "" if no match, rather than None
|
||||||
istart0 = marker_match.start()
|
match_index = marker_match.start()
|
||||||
istart = istart0
|
# split the emote string at the reference marker, to process everything after it
|
||||||
|
head = string[:match_index]
|
||||||
|
tail = string[match_index+1:]
|
||||||
|
|
||||||
# loop over all candidate regexes and match against the string following the match
|
if search_mode:
|
||||||
matches = ((reg.match(string[istart:]), obj, text) for reg, obj, text in candidate_regexes)
|
# match the candidates against the whole search string after the marker
|
||||||
|
rquery = "".join([r"\b(" + re.escape(word.strip(punctuation)) + r").*" for word in iter(tail.split())])
|
||||||
|
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
|
||||||
|
# filter out any non-matching candidates
|
||||||
|
bestmatches = [(obj, match.group()) for match, obj, text in matches if match]
|
||||||
|
|
||||||
# score matches by how long part of the string was matched
|
else:
|
||||||
matches = [(match.end() if match else -1, obj, text) for match, obj, text in matches]
|
# to find the longest match, we start from the marker and lengthen the
|
||||||
maxscore = max(score for score, obj, text in matches)
|
# match query one word at a time.
|
||||||
|
word_list = []
|
||||||
|
bestmatches = []
|
||||||
|
# preserve punctuation when splitting
|
||||||
|
tail = re.split('(\W)', tail)
|
||||||
|
iend = 0
|
||||||
|
for i, item in enumerate(tail):
|
||||||
|
# don't add non-word characters to the search query
|
||||||
|
if not item.isalpha():
|
||||||
|
continue
|
||||||
|
word_list.append(item)
|
||||||
|
rquery = "".join([r"\b(" + re.escape(word) + r").*" for word in word_list])
|
||||||
|
# match candidates against the current set of words
|
||||||
|
matches = ((re.search(rquery, text, _RE_FLAGS), obj, text) for obj, text in candidate_map)
|
||||||
|
matches = [(obj, match.group()) for match, obj, text in matches if match]
|
||||||
|
if len(matches) == 0:
|
||||||
|
# no matches at this length, keep previous iteration as best
|
||||||
|
break
|
||||||
|
# since this is the longest match so far, set latest match set as best matches
|
||||||
|
bestmatches = matches
|
||||||
|
# save current index as end point of matched text
|
||||||
|
iend = i
|
||||||
|
|
||||||
|
# save search string
|
||||||
|
matched_text = "".join(tail[1:iend])
|
||||||
|
# recombine remainder of emote back into a string
|
||||||
|
tail = "".join(tail[iend+1:])
|
||||||
|
|
||||||
# we have a valid maxscore, extract all matches with this value
|
|
||||||
bestmatches = [(obj, text) for score, obj, text in matches if maxscore == score != -1]
|
|
||||||
nmatches = len(bestmatches)
|
nmatches = len(bestmatches)
|
||||||
|
|
||||||
if not nmatches:
|
if not nmatches:
|
||||||
|
|
@ -488,12 +421,11 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
nmatches = 0
|
nmatches = 0
|
||||||
elif nmatches == 1:
|
elif nmatches == 1:
|
||||||
# an exact match.
|
# an exact match.
|
||||||
obj = bestmatches[0][0]
|
obj, match_str = bestmatches[0]
|
||||||
nmatches = 1
|
|
||||||
elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches):
|
elif all(bestmatches[0][0].id == obj.id for obj, text in bestmatches):
|
||||||
# multi-match but all matches actually reference the same
|
# multi-match but all matches actually reference the same
|
||||||
# obj (could happen with clashing recogs + sdescs)
|
# obj (could happen with clashing recogs + sdescs)
|
||||||
obj = bestmatches[0][0]
|
obj, match_str = bestmatches[0]
|
||||||
nmatches = 1
|
nmatches = 1
|
||||||
else:
|
else:
|
||||||
# multi-match.
|
# multi-match.
|
||||||
|
|
@ -501,7 +433,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None
|
inum = min(max(0, int(num_identifier) - 1), nmatches - 1) if num_identifier else None
|
||||||
if inum is not None:
|
if inum is not None:
|
||||||
# A valid inum is given. Use this to separate data.
|
# A valid inum is given. Use this to separate data.
|
||||||
obj = bestmatches[inum][0]
|
obj, match_str = bestmatches[inum]
|
||||||
nmatches = 1
|
nmatches = 1
|
||||||
else:
|
else:
|
||||||
# no identifier given - a real multimatch.
|
# no identifier given - a real multimatch.
|
||||||
|
|
@ -519,12 +451,9 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
# case sensitive mode
|
# case sensitive mode
|
||||||
# internal flags for the case used for the original /query
|
# internal flags for the case used for the original /query
|
||||||
# - t for titled input (like /Name)
|
# - t for titled input (like /Name)
|
||||||
# - ^ for all upercase input (likle /NAME)
|
# - ^ for all upercase input (like /NAME)
|
||||||
# - v for lower-case input (like /name)
|
# - v for lower-case input (like /name)
|
||||||
# - ~ for mixed case input (like /nAmE)
|
# - ~ for mixed case input (like /nAmE)
|
||||||
matchtext = marker_match.group()
|
|
||||||
if not _RE_SELF_REF.match(matchtext):
|
|
||||||
# self-refs are kept as-is, others are parsed by case
|
|
||||||
matchtext = marker_match.group().lstrip(_PREFIX)
|
matchtext = marker_match.group().lstrip(_PREFIX)
|
||||||
if matchtext.istitle():
|
if matchtext.istitle():
|
||||||
case = "t"
|
case = "t"
|
||||||
|
|
@ -533,21 +462,21 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
|
||||||
elif matchtext.islower():
|
elif matchtext.islower():
|
||||||
case = "v"
|
case = "v"
|
||||||
|
|
||||||
key = "#%i%s" % (obj.id, case)
|
key = f"#{obj.id}{case}"
|
||||||
string = string[:istart0] + "{%s}" % key + string[istart + maxscore :]
|
# recombine emote with matched text replaced by ref
|
||||||
|
string = f"{head}{{{key}}}{tail}"
|
||||||
mapping[key] = obj
|
mapping[key] = obj
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# multimatch error
|
# multimatch error
|
||||||
refname = marker_match.group()
|
refname = marker_match.group()
|
||||||
reflist = [
|
reflist = [
|
||||||
"%s%s%s (%s%s)"
|
"{num}{sep}{name} ({text}{key})".format(
|
||||||
% (
|
num=inum + 1,
|
||||||
inum + 1,
|
sep=_NUM_SEP,
|
||||||
_NUM_SEP,
|
name=_RE_PREFIX.sub("", refname),
|
||||||
_RE_PREFIX.sub("", refname),
|
text=text,
|
||||||
text,
|
key=f" ({sender.key})" if sender == ob else "",
|
||||||
" (%s)" % sender.key if sender == ob else "",
|
|
||||||
)
|
)
|
||||||
for inum, (ob, text) in enumerate(obj)
|
for inum, (ob, text) in enumerate(obj)
|
||||||
]
|
]
|
||||||
|
|
@ -611,7 +540,7 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
|
||||||
sender.msg(str(err))
|
sender.msg(str(err))
|
||||||
return
|
return
|
||||||
|
|
||||||
skey = "#%i" % sender.id
|
skey = f"#{sender.id}"
|
||||||
|
|
||||||
# we escape the object mappings since we'll do the language ones first
|
# we escape the object mappings since we'll do the language ones first
|
||||||
# (the text could have nested object mappings).
|
# (the text could have nested object mappings).
|
||||||
|
|
@ -619,66 +548,45 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
|
||||||
# if anonymous_add is passed as a kwarg, collect and remove it from kwargs
|
# if anonymous_add is passed as a kwarg, collect and remove it from kwargs
|
||||||
if "anonymous_add" in kwargs:
|
if "anonymous_add" in kwargs:
|
||||||
anonymous_add = kwargs.pop("anonymous_add")
|
anonymous_add = kwargs.pop("anonymous_add")
|
||||||
if anonymous_add and not any(1 for tag in obj_mapping if tag.startswith(skey)):
|
# make sure to catch all possible self-refs
|
||||||
# no self-reference in the emote - add to the end
|
self_refs = [f"{skey}{ref}" for ref in ('t','^','v','~','')]
|
||||||
obj_mapping[skey] = sender
|
if anonymous_add and not any(1 for tag in obj_mapping if tag in self_refs):
|
||||||
|
# no self-reference in the emote - add it
|
||||||
if anonymous_add == "first":
|
if anonymous_add == "first":
|
||||||
possessive = "" if emote.startswith("'") else " "
|
# add case flag for initial caps
|
||||||
emote = "%s%s%s" % ("{{%s}}" % skey, possessive, emote)
|
skey += 't'
|
||||||
|
# don't put a space after the self-ref if it's a possessive emote
|
||||||
|
femote = "{key}{emote}" if emote.startswith("'") else "{key} {emote}"
|
||||||
else:
|
else:
|
||||||
emote = "%s [%s]" % (emote, "{{%s}}" % skey)
|
# add it to the end
|
||||||
|
femote = "{emote} [{key}]"
|
||||||
|
emote = femote.format( key="{{"+ skey +"}}", emote=emote )
|
||||||
|
obj_mapping[skey] = sender
|
||||||
|
|
||||||
# broadcast emote to everyone
|
# broadcast emote to everyone
|
||||||
for receiver in receivers:
|
for receiver in receivers:
|
||||||
# first handle the language mapping, which always produce different keys ##nn
|
# first handle the language mapping, which always produce different keys ##nn
|
||||||
receiver_lang_mapping = {}
|
if hasattr(receiver, "process_language") and callable(receiver.process_language):
|
||||||
try:
|
receiver_lang_mapping = {
|
||||||
process_language = receiver.process_language
|
key: receiver.process_language(saytext, sender, langname)
|
||||||
except AttributeError:
|
for key, (langname, saytext) in language_mapping.items()
|
||||||
process_language = _dummy_process
|
}
|
||||||
for key, (langname, saytext) in language_mapping.items():
|
else:
|
||||||
# color says
|
receiver_lang_mapping = {
|
||||||
receiver_lang_mapping[key] = process_language(saytext, sender, langname)
|
key: saytext for key, (langname, saytext) in language_mapping.items()
|
||||||
|
}
|
||||||
# map the language {##num} markers. This will convert the escaped sdesc markers on
|
# map the language {##num} markers. This will convert the escaped sdesc markers on
|
||||||
# the form {{#num}} to {#num} markers ready to sdesc-map in the next step.
|
# the form {{#num}} to {#num} markers ready to sdesc-map in the next step.
|
||||||
sendemote = emote.format(**receiver_lang_mapping)
|
sendemote = emote.format(**receiver_lang_mapping)
|
||||||
|
|
||||||
# handle sdesc mappings. we make a temporary copy that we can modify
|
# map the ref keys to sdescs
|
||||||
try:
|
|
||||||
process_sdesc = receiver.process_sdesc
|
|
||||||
except AttributeError:
|
|
||||||
process_sdesc = _dummy_process
|
|
||||||
|
|
||||||
try:
|
|
||||||
process_recog = receiver.process_recog
|
|
||||||
except AttributeError:
|
|
||||||
process_recog = _dummy_process
|
|
||||||
|
|
||||||
try:
|
|
||||||
recog_get = receiver.recog.get
|
|
||||||
receiver_sdesc_mapping = dict(
|
|
||||||
(ref, process_recog(recog_get(obj), obj, ref=ref, **kwargs))
|
|
||||||
for ref, obj in obj_mapping.items()
|
|
||||||
)
|
|
||||||
except AttributeError:
|
|
||||||
receiver_sdesc_mapping = dict(
|
receiver_sdesc_mapping = dict(
|
||||||
(
|
(
|
||||||
ref,
|
ref,
|
||||||
process_sdesc(obj.sdesc.get(), obj, ref=ref)
|
obj.get_display_name(receiver, ref=ref, noid=True),
|
||||||
if hasattr(obj, "sdesc")
|
|
||||||
else process_sdesc(obj.key, obj, ref=ref),
|
|
||||||
)
|
)
|
||||||
for ref, obj in obj_mapping.items()
|
for ref, obj in obj_mapping.items()
|
||||||
)
|
)
|
||||||
# make sure receiver always sees their real name
|
|
||||||
rkey_start = "#%i" % receiver.id
|
|
||||||
rkey_keep_case = rkey_start + "~" # signifies keeping the case
|
|
||||||
for rkey in (key for key in receiver_sdesc_mapping if key.startswith(rkey_start)):
|
|
||||||
# we could have #%i^, #%it etc depending on input case - we want the
|
|
||||||
# self-reference to retain case.
|
|
||||||
receiver_sdesc_mapping[rkey] = process_sdesc(
|
|
||||||
receiver.key, receiver, ref=rkey_keep_case, **kwargs
|
|
||||||
)
|
|
||||||
|
|
||||||
# do the template replacement of the sdesc/recog {#num} markers
|
# do the template replacement of the sdesc/recog {#num} markers
|
||||||
receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs)
|
receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs)
|
||||||
|
|
@ -713,17 +621,13 @@ class SdescHandler:
|
||||||
"""
|
"""
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
self.sdesc = ""
|
self.sdesc = ""
|
||||||
self.sdesc_regex = ""
|
|
||||||
self._cache()
|
self._cache()
|
||||||
|
|
||||||
def _cache(self):
|
def _cache(self):
|
||||||
"""
|
"""
|
||||||
Cache data from storage
|
Cache data from storage
|
||||||
|
|
||||||
"""
|
"""
|
||||||
self.sdesc = self.obj.attributes.get("_sdesc", default="")
|
self.sdesc = self.obj.attributes.get("_sdesc", default=self.obj.key)
|
||||||
sdesc_regex = self.obj.attributes.get("_sdesc_regex", default="")
|
|
||||||
self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS)
|
|
||||||
|
|
||||||
def add(self, sdesc, max_length=60):
|
def add(self, sdesc, max_length=60):
|
||||||
"""
|
"""
|
||||||
|
|
@ -759,17 +663,13 @@ class SdescHandler:
|
||||||
|
|
||||||
if len(cleaned_sdesc) > max_length:
|
if len(cleaned_sdesc) > max_length:
|
||||||
raise SdescError(
|
raise SdescError(
|
||||||
"Short desc can max be %i chars long (was %i chars)."
|
"Short desc can max be {} chars long (was {} chars).".format(max_length, len(cleaned_sdesc))
|
||||||
% (max_length, len(cleaned_sdesc))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# store to attributes
|
# store to attributes
|
||||||
sdesc_regex = ordered_permutation_regex(cleaned_sdesc)
|
|
||||||
self.obj.attributes.add("_sdesc", sdesc)
|
self.obj.attributes.add("_sdesc", sdesc)
|
||||||
self.obj.attributes.add("_sdesc_regex", sdesc_regex)
|
|
||||||
# local caching
|
# local caching
|
||||||
self.sdesc = sdesc
|
self.sdesc = sdesc
|
||||||
self.sdesc_regex = re.compile(sdesc_regex, _RE_FLAGS)
|
|
||||||
|
|
||||||
return sdesc
|
return sdesc
|
||||||
|
|
||||||
|
|
@ -781,15 +681,6 @@ class SdescHandler:
|
||||||
"""
|
"""
|
||||||
return self.sdesc or self.obj.key
|
return self.sdesc or self.obj.key
|
||||||
|
|
||||||
def get_regex_tuple(self):
|
|
||||||
"""
|
|
||||||
Return data for sdesc/recog handling
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
tup (tuple): tuple (sdesc_regex, obj, sdesc)
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.sdesc_regex, self.obj, self.sdesc
|
|
||||||
|
|
||||||
|
|
||||||
class RecogHandler:
|
class RecogHandler:
|
||||||
|
|
@ -802,7 +693,6 @@ class RecogHandler:
|
||||||
|
|
||||||
_recog_ref2recog
|
_recog_ref2recog
|
||||||
_recog_obj2recog
|
_recog_obj2recog
|
||||||
_recog_obj2regex
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
@ -817,7 +707,6 @@ class RecogHandler:
|
||||||
self.obj = obj
|
self.obj = obj
|
||||||
# mappings
|
# mappings
|
||||||
self.ref2recog = {}
|
self.ref2recog = {}
|
||||||
self.obj2regex = {}
|
|
||||||
self.obj2recog = {}
|
self.obj2recog = {}
|
||||||
self._cache()
|
self._cache()
|
||||||
|
|
||||||
|
|
@ -826,11 +715,7 @@ class RecogHandler:
|
||||||
Load data to handler cache
|
Load data to handler cache
|
||||||
"""
|
"""
|
||||||
self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={})
|
self.ref2recog = self.obj.attributes.get("_recog_ref2recog", default={})
|
||||||
obj2regex = self.obj.attributes.get("_recog_obj2regex", default={})
|
|
||||||
obj2recog = self.obj.attributes.get("_recog_obj2recog", default={})
|
obj2recog = self.obj.attributes.get("_recog_obj2recog", default={})
|
||||||
self.obj2regex = dict(
|
|
||||||
(obj, re.compile(regex, _RE_FLAGS)) for obj, regex in obj2regex.items() if obj
|
|
||||||
)
|
|
||||||
self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj)
|
self.obj2recog = dict((obj, recog) for obj, recog in obj2recog.items() if obj)
|
||||||
|
|
||||||
def add(self, obj, recog, max_length=60):
|
def add(self, obj, recog, max_length=60):
|
||||||
|
|
@ -873,31 +758,27 @@ class RecogHandler:
|
||||||
|
|
||||||
if len(cleaned_recog) > max_length:
|
if len(cleaned_recog) > max_length:
|
||||||
raise RecogError(
|
raise RecogError(
|
||||||
"Recog string cannot be longer than %i chars (was %i chars)"
|
"Recog string cannot be longer than {} chars (was {} chars)".format(max_length, len(cleaned_recog))
|
||||||
% (max_length, len(cleaned_recog))
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# mapping #dbref:obj
|
# mapping #dbref:obj
|
||||||
key = "#%i" % obj.id
|
key = f"#{obj.id}"
|
||||||
self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog
|
self.obj.attributes.get("_recog_ref2recog", default={})[key] = recog
|
||||||
self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog
|
self.obj.attributes.get("_recog_obj2recog", default={})[obj] = recog
|
||||||
regex = ordered_permutation_regex(cleaned_recog)
|
|
||||||
self.obj.attributes.get("_recog_obj2regex", default={})[obj] = regex
|
|
||||||
# local caching
|
# local caching
|
||||||
self.ref2recog[key] = recog
|
self.ref2recog[key] = recog
|
||||||
self.obj2recog[obj] = recog
|
self.obj2recog[obj] = recog
|
||||||
self.obj2regex[obj] = re.compile(regex, _RE_FLAGS)
|
|
||||||
return recog
|
return recog
|
||||||
|
|
||||||
def get(self, obj):
|
def get(self, obj):
|
||||||
"""
|
"""
|
||||||
Get recog replacement string, if one exists, otherwise
|
Get recog replacement string, if one exists.
|
||||||
get sdesc and as a last resort, the object's key.
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
obj (Object): The object, whose sdesc to replace
|
obj (Object): The object, whose sdesc to replace
|
||||||
Returns:
|
Returns:
|
||||||
recog (str): The replacement string to use.
|
recog (str or None): The replacement string to use, or
|
||||||
|
None if there is no recog for this object.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
This method will respect a "enable_recog" lock set on
|
This method will respect a "enable_recog" lock set on
|
||||||
|
|
@ -908,10 +789,10 @@ class RecogHandler:
|
||||||
# check an eventual recog_masked lock on the object
|
# check an eventual recog_masked lock on the object
|
||||||
# to avoid revealing masked characters. If lock
|
# to avoid revealing masked characters. If lock
|
||||||
# does not exist, pass automatically.
|
# does not exist, pass automatically.
|
||||||
return self.obj2recog.get(obj, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
|
return self.obj2recog.get(obj, None)
|
||||||
else:
|
else:
|
||||||
# recog_mask log not passed, disable recog
|
# recog_mask lock not passed, disable recog
|
||||||
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
|
return None
|
||||||
|
|
||||||
def all(self):
|
def all(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -932,19 +813,9 @@ class RecogHandler:
|
||||||
"""
|
"""
|
||||||
if obj in self.obj2recog:
|
if obj in self.obj2recog:
|
||||||
del self.obj.db._recog_obj2recog[obj]
|
del self.obj.db._recog_obj2recog[obj]
|
||||||
del self.obj.db._recog_obj2regex[obj]
|
del self.obj.db._recog_ref2recog[f"#{obj.id}"]
|
||||||
del self.obj.db._recog_ref2recog["#%i" % obj.id]
|
|
||||||
self._cache()
|
self._cache()
|
||||||
|
|
||||||
def get_regex_tuple(self, obj):
|
|
||||||
"""
|
|
||||||
Returns:
|
|
||||||
rec (tuple): Tuple (recog_regex, obj, recog)
|
|
||||||
"""
|
|
||||||
if obj in self.obj2recog and obj.access(self.obj, "enable_recog", default=True):
|
|
||||||
return self.obj2regex[obj], obj, self.obj2regex[obj]
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
# ------------------------------------------------------------
|
# ------------------------------------------------------------
|
||||||
# RP Commands
|
# RP Commands
|
||||||
|
|
@ -994,8 +865,8 @@ class CmdEmote(RPCommand): # replaces the main emote
|
||||||
# we also include ourselves here.
|
# we also include ourselves here.
|
||||||
emote = self.args
|
emote = self.args
|
||||||
targets = self.caller.location.contents
|
targets = self.caller.location.contents
|
||||||
if not emote.endswith((".", "?", "!")): # If emote is not punctuated,
|
if not emote.endswith((".", "?", "!", '"')): # If emote is not punctuated or speech,
|
||||||
emote = "%s." % emote # add a full-stop for good measure.
|
emote += "." # add a full-stop for good measure.
|
||||||
send_emote(self.caller, targets, emote, anonymous_add="first")
|
send_emote(self.caller, targets, emote, anonymous_add="first")
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1025,7 +896,6 @@ class CmdSay(RPCommand): # replaces standard say
|
||||||
|
|
||||||
# calling the speech modifying hook
|
# calling the speech modifying hook
|
||||||
speech = caller.at_pre_say(self.args)
|
speech = caller.at_pre_say(self.args)
|
||||||
# preparing the speech with sdesc/speech parsing.
|
|
||||||
targets = self.caller.location.contents
|
targets = self.caller.location.contents
|
||||||
send_emote(self.caller, targets, speech, anonymous_add=None)
|
send_emote(self.caller, targets, speech, anonymous_add=None)
|
||||||
|
|
||||||
|
|
@ -1061,7 +931,7 @@ class CmdSdesc(RPCommand): # set/look at own sdesc
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
caller.msg(f"Cannot set sdesc on {caller.key}.")
|
caller.msg(f"Cannot set sdesc on {caller.key}.")
|
||||||
return
|
return
|
||||||
caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc))
|
caller.msg(f"{caller.key}'s sdesc was set to '{sdesc}'.")
|
||||||
|
|
||||||
|
|
||||||
class CmdPose(RPCommand): # set current pose and default pose
|
class CmdPose(RPCommand): # set current pose and default pose
|
||||||
|
|
@ -1121,8 +991,8 @@ class CmdPose(RPCommand): # set current pose and default pose
|
||||||
caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>")
|
caller.msg("Usage: pose <pose-text> OR pose obj = <pose-text>")
|
||||||
return
|
return
|
||||||
|
|
||||||
if not pose.endswith("."):
|
if not pose.endswith((".", "?", "!", '"')):
|
||||||
pose = "%s." % pose
|
pose += "."
|
||||||
if target:
|
if target:
|
||||||
# affect something else
|
# affect something else
|
||||||
target = caller.search(target)
|
target = caller.search(target)
|
||||||
|
|
@ -1134,18 +1004,18 @@ class CmdPose(RPCommand): # set current pose and default pose
|
||||||
else:
|
else:
|
||||||
target = caller
|
target = caller
|
||||||
|
|
||||||
|
target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key
|
||||||
if not target.attributes.has("pose"):
|
if not target.attributes.has("pose"):
|
||||||
caller.msg("%s cannot be posed." % target.key)
|
caller.msg(f"{target_name} cannot be posed.")
|
||||||
return
|
return
|
||||||
|
|
||||||
target_name = target.sdesc.get() if hasattr(target, "sdesc") else target.key
|
|
||||||
# set the pose
|
# set the pose
|
||||||
if self.reset:
|
if self.reset:
|
||||||
pose = target.db.pose_default
|
pose = target.db.pose_default
|
||||||
target.db.pose = pose
|
target.db.pose = pose
|
||||||
elif self.default:
|
elif self.default:
|
||||||
target.db.pose_default = pose
|
target.db.pose_default = pose
|
||||||
caller.msg("Default pose is now '%s %s'." % (target_name, pose))
|
caller.msg(f"Default pose is now '{target_name} {pose}'.")
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
# set the pose. We do one-time ref->sdesc mapping here.
|
# set the pose. We do one-time ref->sdesc mapping here.
|
||||||
|
|
@ -1157,12 +1027,12 @@ class CmdPose(RPCommand): # set current pose and default pose
|
||||||
pose = parsed.format(**mapping)
|
pose = parsed.format(**mapping)
|
||||||
|
|
||||||
if len(target_name) + len(pose) > 60:
|
if len(target_name) + len(pose) > 60:
|
||||||
caller.msg("Your pose '%s' is too long." % pose)
|
caller.msg(f"'{pose}' is too long.")
|
||||||
return
|
return
|
||||||
|
|
||||||
target.db.pose = pose
|
target.db.pose = pose
|
||||||
|
|
||||||
caller.msg("Pose will read '%s %s'." % (target_name, pose))
|
caller.msg(f"Pose will read '{target_name} {pose}'.")
|
||||||
|
|
||||||
|
|
||||||
class CmdRecog(RPCommand): # assign personal alias to object in room
|
class CmdRecog(RPCommand): # assign personal alias to object in room
|
||||||
|
|
@ -1241,12 +1111,12 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
||||||
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
|
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
|
||||||
elif nmatches > 1:
|
elif nmatches > 1:
|
||||||
reflist = [
|
reflist = [
|
||||||
"{}{}{} ({}{})".format(
|
"{num}{sep}{sdesc} ({recog}{key})".format(
|
||||||
inum + 1,
|
num=inum + 1,
|
||||||
_NUM_SEP,
|
sep=_NUM_SEP,
|
||||||
_RE_PREFIX.sub("", sdesc),
|
sdesc=_RE_PREFIX.sub("", sdesc),
|
||||||
caller.recog.get(obj),
|
recog=caller.recog.get(obj) or "no recog",
|
||||||
" (%s)" % caller.key if caller == obj else "",
|
key=f" ({caller.key})" if caller == obj else "",
|
||||||
)
|
)
|
||||||
for inum, obj in enumerate(matches)
|
for inum, obj in enumerate(matches)
|
||||||
]
|
]
|
||||||
|
|
@ -1262,7 +1132,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
||||||
if forget_mode:
|
if forget_mode:
|
||||||
# remove existing recog
|
# remove existing recog
|
||||||
caller.recog.remove(obj)
|
caller.recog.remove(obj)
|
||||||
caller.msg("%s will now know them only as '%s'." % (caller.key, obj.recog.get(obj)))
|
caller.msg("You will now know them only as '{}'.".format( obj.get_display_name(caller, noid=True) ))
|
||||||
else:
|
else:
|
||||||
# set recog
|
# set recog
|
||||||
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
|
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
|
||||||
|
|
@ -1271,7 +1141,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
||||||
except RecogError as err:
|
except RecogError as err:
|
||||||
caller.msg(err)
|
caller.msg(err)
|
||||||
return
|
return
|
||||||
caller.msg("%s will now remember |w%s|n as |w%s|n." % (caller.key, sdesc, alias))
|
caller.msg("You will now remember |w{}|n as |w{}|n.".format(sdesc, alias))
|
||||||
|
|
||||||
|
|
||||||
class CmdMask(RPCommand):
|
class CmdMask(RPCommand):
|
||||||
|
|
@ -1302,14 +1172,14 @@ class CmdMask(RPCommand):
|
||||||
caller.msg("You are already wearing a mask.")
|
caller.msg("You are already wearing a mask.")
|
||||||
return
|
return
|
||||||
sdesc = _RE_CHAREND.sub("", self.args)
|
sdesc = _RE_CHAREND.sub("", self.args)
|
||||||
sdesc = "%s |H[masked]|n" % sdesc
|
sdesc = f"{sdesc} |H[masked]|n"
|
||||||
if len(sdesc) > 60:
|
if len(sdesc) > 60:
|
||||||
caller.msg("Your masked sdesc is too long.")
|
caller.msg("Your masked sdesc is too long.")
|
||||||
return
|
return
|
||||||
caller.db.unmasked_sdesc = caller.sdesc.get()
|
caller.db.unmasked_sdesc = caller.sdesc.get()
|
||||||
caller.locks.add("enable_recog:false()")
|
caller.locks.add("enable_recog:false()")
|
||||||
caller.sdesc.add(sdesc)
|
caller.sdesc.add(sdesc)
|
||||||
caller.msg("You wear a mask as '%s'." % sdesc)
|
caller.msg(f"You wear a mask as '{sdesc}'.")
|
||||||
else:
|
else:
|
||||||
# unmask
|
# unmask
|
||||||
old_sdesc = caller.db.unmasked_sdesc
|
old_sdesc = caller.db.unmasked_sdesc
|
||||||
|
|
@ -1319,7 +1189,7 @@ class CmdMask(RPCommand):
|
||||||
del caller.db.unmasked_sdesc
|
del caller.db.unmasked_sdesc
|
||||||
caller.locks.remove("enable_recog")
|
caller.locks.remove("enable_recog")
|
||||||
caller.sdesc.add(old_sdesc)
|
caller.sdesc.add(old_sdesc)
|
||||||
caller.msg("You remove your mask and are again '%s'." % old_sdesc)
|
caller.msg(f"You remove your mask and are again '{old_sdesc}'.")
|
||||||
|
|
||||||
|
|
||||||
class RPSystemCmdSet(CmdSet):
|
class RPSystemCmdSet(CmdSet):
|
||||||
|
|
@ -1346,6 +1216,9 @@ class ContribRPObject(DefaultObject):
|
||||||
This class is meant as a mix-in or parent for objects in an
|
This class is meant as a mix-in or parent for objects in an
|
||||||
rp-heavy game. It implements the base functionality for poses.
|
rp-heavy game. It implements the base functionality for poses.
|
||||||
"""
|
"""
|
||||||
|
@lazy_property
|
||||||
|
def sdesc(self):
|
||||||
|
return SdescHandler(self)
|
||||||
|
|
||||||
def at_object_creation(self):
|
def at_object_creation(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1357,6 +1230,10 @@ class ContribRPObject(DefaultObject):
|
||||||
self.db.pose = ""
|
self.db.pose = ""
|
||||||
self.db.pose_default = "is here."
|
self.db.pose_default = "is here."
|
||||||
|
|
||||||
|
# initializing sdesc
|
||||||
|
self.db._sdesc = ""
|
||||||
|
self.sdesc.add("Something")
|
||||||
|
|
||||||
def search(
|
def search(
|
||||||
self,
|
self,
|
||||||
searchdata,
|
searchdata,
|
||||||
|
|
@ -1529,6 +1406,22 @@ class ContribRPObject(DefaultObject):
|
||||||
multimatch_string=multimatch_string,
|
multimatch_string=multimatch_string,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_posed_sdesc(self, sdesc, **kwargs):
|
||||||
|
"""
|
||||||
|
Displays the object with its current pose string.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
pose (str): A string containing the object's sdesc and
|
||||||
|
current or default pose.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# get the current pose, or default if no pose is set
|
||||||
|
pose = self.db.pose or self.db.pose_default
|
||||||
|
|
||||||
|
# return formatted string, or sdesc as fallback
|
||||||
|
return f"{sdesc} {pose}" if pose else sdesc
|
||||||
|
|
||||||
|
|
||||||
def get_display_name(self, looker, **kwargs):
|
def get_display_name(self, looker, **kwargs):
|
||||||
"""
|
"""
|
||||||
Displays the name of the object in a viewer-aware manner.
|
Displays the name of the object in a viewer-aware manner.
|
||||||
|
|
@ -1539,28 +1432,41 @@ class ContribRPObject(DefaultObject):
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
pose (bool): Include the pose (if available) in the return.
|
pose (bool): Include the pose (if available) in the return.
|
||||||
|
ref (str): The reference marker found in string to replace.
|
||||||
|
This is on the form #{num}{case}, like '#12^', where
|
||||||
|
the number is a processing location in the string and the
|
||||||
|
case symbol indicates the case of the original tag input
|
||||||
|
- `t` - input was Titled, like /Tall
|
||||||
|
- `^` - input was all uppercase, like /TALL
|
||||||
|
- `v` - input was all lowercase, like /tall
|
||||||
|
- `~` - input case should be kept, or was mixed-case
|
||||||
|
noid (bool): Don't show DBREF even if viewer has control access.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
name (str): A string of the sdesc containing the name of the object,
|
name (str): A string of the sdesc containing the name of the object,
|
||||||
if this is defined.
|
if this is defined. By default, included the DBREF if this user
|
||||||
including the DBREF if this user is privileged to control
|
is privileged to control said object.
|
||||||
said object.
|
|
||||||
|
|
||||||
Notes:
|
|
||||||
The RPObject version doesn't add color to its display.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else ""
|
ref = kwargs.get("ref","~")
|
||||||
|
|
||||||
if looker == self:
|
if looker == self:
|
||||||
|
# always show your own key
|
||||||
sdesc = self.key
|
sdesc = self.key
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
recog = looker.recog.get(self)
|
# get the sdesc looker should see
|
||||||
|
sdesc = looker.get_sdesc(self, ref=ref)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
recog = None
|
# use own sdesc as a fallback
|
||||||
sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key
|
sdesc = self.sdesc.get()
|
||||||
pose = " %s" % (self.db.pose or "") if kwargs.get("pose", False) else ""
|
|
||||||
return "%s%s%s" % (sdesc, idstr, pose)
|
# add dbref is looker has control access and `noid` is not set
|
||||||
|
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
|
||||||
|
sdesc = f"{sdesc}(#{self.id})"
|
||||||
|
|
||||||
|
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
|
||||||
|
|
||||||
|
|
||||||
def return_appearance(self, looker):
|
def return_appearance(self, looker):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1569,6 +1475,10 @@ class ContribRPObject(DefaultObject):
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
looker (Object): Object doing the looking.
|
looker (Object): Object doing the looking.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
string (str): A string containing the name, appearance and contents
|
||||||
|
of the object.
|
||||||
"""
|
"""
|
||||||
if not looker:
|
if not looker:
|
||||||
return ""
|
return ""
|
||||||
|
|
@ -1592,6 +1502,7 @@ class ContribRPObject(DefaultObject):
|
||||||
string += "\n|wExits:|n " + ", ".join(exits)
|
string += "\n|wExits:|n " + ", ".join(exits)
|
||||||
if users or things:
|
if users or things:
|
||||||
string += "\n " + "\n ".join(users + things)
|
string += "\n " + "\n ".join(users + things)
|
||||||
|
|
||||||
return string
|
return string
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -1608,11 +1519,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
This is a character class that has poses, sdesc and recog.
|
This is a character class that has poses, sdesc and recog.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
# Handlers
|
|
||||||
@lazy_property
|
|
||||||
def sdesc(self):
|
|
||||||
return SdescHandler(self)
|
|
||||||
|
|
||||||
@lazy_property
|
@lazy_property
|
||||||
def recog(self):
|
def recog(self):
|
||||||
return RecogHandler(self)
|
return RecogHandler(self)
|
||||||
|
|
@ -1627,29 +1533,45 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
|
|
||||||
Keyword Args:
|
Keyword Args:
|
||||||
pose (bool): Include the pose (if available) in the return.
|
pose (bool): Include the pose (if available) in the return.
|
||||||
|
ref (str): The reference marker found in string to replace.
|
||||||
|
This is on the form #{num}{case}, like '#12^', where
|
||||||
|
the number is a processing location in the string and the
|
||||||
|
case symbol indicates the case of the original tag input
|
||||||
|
- `t` - input was Titled, like /Tall
|
||||||
|
- `^` - input was all uppercase, like /TALL
|
||||||
|
- `v` - input was all lowercase, like /tall
|
||||||
|
- `~` - input case should be kept, or was mixed-case
|
||||||
|
noid (bool): Don't show DBREF even if viewer has control access.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
name (str): A string of the sdesc containing the name of the object,
|
name (str): A string of the sdesc containing the name of the object,
|
||||||
if this is defined.
|
if this is defined. By default, included the DBREF if this user
|
||||||
including the DBREF if this user is privileged to control
|
is privileged to control said object.
|
||||||
said object.
|
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
The RPCharacter version of this method colors its display to make
|
The RPCharacter version adds additional processing to sdescs to make
|
||||||
characters stand out from other objects.
|
characters stand out from other objects.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
idstr = "(#%s)" % self.id if self.access(looker, access_type="control") else ""
|
ref = kwargs.get("ref","~")
|
||||||
|
|
||||||
if looker == self:
|
if looker == self:
|
||||||
sdesc = self.key
|
# process your key as recog since you recognize yourself
|
||||||
|
sdesc = self.process_recog(self.key,self)
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
recog = looker.recog.get(self)
|
# get the sdesc looker should see, with formatting
|
||||||
|
sdesc = looker.get_sdesc(self, process=True, ref=ref)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
recog = None
|
# use own sdesc as a fallback
|
||||||
sdesc = recog or (hasattr(self, "sdesc") and self.sdesc.get()) or self.key
|
sdesc = self.sdesc.get()
|
||||||
pose = " %s" % (self.db.pose or "is here.") if kwargs.get("pose", False) else ""
|
|
||||||
return "|c%s|n%s%s" % (sdesc, idstr, pose)
|
# add dbref is looker has control access and `noid` is not set
|
||||||
|
if self.access(looker, access_type="control") and not kwargs.get("noid",False):
|
||||||
|
sdesc = f"{sdesc}(#{self.id})"
|
||||||
|
|
||||||
|
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
|
||||||
|
|
||||||
|
|
||||||
def at_object_creation(self):
|
def at_object_creation(self):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1658,10 +1580,8 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
super().at_object_creation()
|
super().at_object_creation()
|
||||||
|
|
||||||
self.db._sdesc = ""
|
self.db._sdesc = ""
|
||||||
self.db._sdesc_regex = ""
|
|
||||||
|
|
||||||
self.db._recog_ref2recog = {}
|
self.db._recog_ref2recog = {}
|
||||||
self.db._recog_obj2regex = {}
|
|
||||||
self.db._recog_obj2recog = {}
|
self.db._recog_obj2recog = {}
|
||||||
|
|
||||||
self.cmdset.add(RPSystemCmdSet, persistent=True)
|
self.cmdset.add(RPSystemCmdSet, persistent=True)
|
||||||
|
|
@ -1679,8 +1599,38 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if kwargs.get("whisper"):
|
if kwargs.get("whisper"):
|
||||||
return f'/me whispers "{message}"'
|
return f'/Me whispers "{message}"'
|
||||||
return f'/me says, "{message}"'
|
return f'/Me says, "{message}"'
|
||||||
|
|
||||||
|
def get_sdesc(self, obj, process=False, **kwargs):
|
||||||
|
"""
|
||||||
|
Single method to handle getting recogs with sdesc fallback in an
|
||||||
|
aware manner, to allow separate processing of recogs from sdescs.
|
||||||
|
Gets the sdesc or recog for obj from the view of self.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
obj (Object): the object whose sdesc or recog is being gotten
|
||||||
|
Keyword Args:
|
||||||
|
process (bool): If True, the sdesc/recog is run through the
|
||||||
|
appropriate process method for self - .process_sdesc or
|
||||||
|
.process_recog
|
||||||
|
"""
|
||||||
|
# always see own key
|
||||||
|
if obj == self:
|
||||||
|
recog = self.key
|
||||||
|
sdesc = self.key
|
||||||
|
else:
|
||||||
|
# first check if we have a recog for this object
|
||||||
|
recog = self.recog.get(obj)
|
||||||
|
# set sdesc to recog, using sdesc as a fallback, or the object's key if no sdesc
|
||||||
|
sdesc = recog or (hasattr(obj, "sdesc") and obj.sdesc.get()) or obj.key
|
||||||
|
|
||||||
|
if process:
|
||||||
|
# process the sdesc as a recog if a recog was found, else as an sdesc
|
||||||
|
sdesc = (self.process_recog if recog else self.process_sdesc)(sdesc, obj, **kwargs)
|
||||||
|
|
||||||
|
return sdesc
|
||||||
|
|
||||||
|
|
||||||
def process_sdesc(self, sdesc, obj, **kwargs):
|
def process_sdesc(self, sdesc, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1721,7 +1671,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
sdesc = sdesc.upper()
|
sdesc = sdesc.upper()
|
||||||
elif "v" in ref:
|
elif "v" in ref:
|
||||||
sdesc = sdesc.lower()
|
sdesc = sdesc.lower()
|
||||||
return "|b%s|n" % sdesc
|
return f"|b{sdesc}|n"
|
||||||
|
|
||||||
def process_recog(self, recog, obj, **kwargs):
|
def process_recog(self, recog, obj, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1732,14 +1682,15 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
translated from the original sdesc at this point.
|
translated from the original sdesc at this point.
|
||||||
obj (Object): The object the recog:ed string belongs to.
|
obj (Object): The object the recog:ed string belongs to.
|
||||||
This is not used by default.
|
This is not used by default.
|
||||||
Kwargs:
|
|
||||||
ref (str): See process_sdesc.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
recog (str): The modified recog string.
|
recog (str): The modified recog string.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return self.process_sdesc(recog, obj, **kwargs)
|
if not recog:
|
||||||
|
return ""
|
||||||
|
|
||||||
|
return f"|m{recog}|n"
|
||||||
|
|
||||||
def process_language(self, text, speaker, language, **kwargs):
|
def process_language(self, text, speaker, language, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
@ -1762,4 +1713,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
||||||
the evennia.contrib.rpg.rplanguage module.
|
the evennia.contrib.rpg.rplanguage module.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return "%s|w%s|n" % ("|W(%s)" % language if language else "", text)
|
return "{label}|w{text}|n".format(
|
||||||
|
label=f"|W({language})" if language else "",
|
||||||
|
text=text
|
||||||
|
)
|
||||||
|
|
|
||||||
|
|
@ -96,7 +96,7 @@ recog01 = "Mr Receiver"
|
||||||
recog02 = "Mr Receiver2"
|
recog02 = "Mr Receiver2"
|
||||||
recog10 = "Mr Sender"
|
recog10 = "Mr Sender"
|
||||||
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
|
emote = 'With a flair, /me looks at /first and /colliding sdesc-guy. She says "This is a test."'
|
||||||
case_emote = "/me looks at /first, then /FIRST, /First and /Colliding twice."
|
case_emote = "/Me looks at /first. Then, /me looks at /FIRST, /First and /Colliding twice."
|
||||||
|
|
||||||
|
|
||||||
class TestRPSystem(BaseEvenniaTest):
|
class TestRPSystem(BaseEvenniaTest):
|
||||||
|
|
@ -113,41 +113,11 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
|
rpsystem.ContribRPCharacter, key="Receiver2", location=self.room
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_ordered_permutation_regex(self):
|
|
||||||
self.assertEqual(
|
|
||||||
rpsystem.ordered_permutation_regex(sdesc0),
|
|
||||||
"/[0-9]*-*A\\ nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*nice\\ sender\\ of\\ emotes(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*A\\ nice\\ sender\\ of(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*sender\\ of\\ emotes(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*nice\\ sender\\ of(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*A\\ nice\\ sender(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*nice\\ sender(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*of\\ emotes(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*sender\\ of(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*A\\ nice(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*emotes(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*sender(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*nice(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*of(?=\\W|$)+|"
|
|
||||||
"/[0-9]*-*A(?=\\W|$)+",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_sdesc_handler(self):
|
def test_sdesc_handler(self):
|
||||||
self.speaker.sdesc.add(sdesc0)
|
self.speaker.sdesc.add(sdesc0)
|
||||||
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
|
self.assertEqual(self.speaker.sdesc.get(), sdesc0)
|
||||||
self.speaker.sdesc.add("This is {#324} ignored")
|
self.speaker.sdesc.add("This is {#324} ignored")
|
||||||
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
|
self.assertEqual(self.speaker.sdesc.get(), "This is 324 ignored")
|
||||||
self.speaker.sdesc.add("Testing three words")
|
|
||||||
self.assertEqual(
|
|
||||||
self.speaker.sdesc.get_regex_tuple()[0].pattern,
|
|
||||||
"/[0-9]*-*Testing\ three\ words(?=\W|$)+|"
|
|
||||||
"/[0-9]*-*Testing\ three(?=\W|$)+|"
|
|
||||||
"/[0-9]*-*three\ words(?=\W|$)+|"
|
|
||||||
"/[0-9]*-*Testing(?=\W|$)+|"
|
|
||||||
"/[0-9]*-*three(?=\W|$)+|"
|
|
||||||
"/[0-9]*-*words(?=\W|$)+",
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_recog_handler(self):
|
def test_recog_handler(self):
|
||||||
self.speaker.sdesc.add(sdesc0)
|
self.speaker.sdesc.add(sdesc0)
|
||||||
|
|
@ -156,12 +126,8 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
self.speaker.recog.add(self.receiver2, recog02)
|
self.speaker.recog.add(self.receiver2, recog02)
|
||||||
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
|
self.assertEqual(self.speaker.recog.get(self.receiver1), recog01)
|
||||||
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
|
self.assertEqual(self.speaker.recog.get(self.receiver2), recog02)
|
||||||
self.assertEqual(
|
|
||||||
self.speaker.recog.get_regex_tuple(self.receiver1)[0].pattern,
|
|
||||||
"/[0-9]*-*Mr\\ Receiver(?=\\W|$)+|/[0-9]*-*Receiver(?=\\W|$)+|/[0-9]*-*Mr(?=\\W|$)+",
|
|
||||||
)
|
|
||||||
self.speaker.recog.remove(self.receiver1)
|
self.speaker.recog.remove(self.receiver1)
|
||||||
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
|
self.assertEqual(self.speaker.recog.get(self.receiver1), None)
|
||||||
|
|
||||||
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
|
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
|
||||||
|
|
||||||
|
|
@ -198,6 +164,24 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
result,
|
result,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_sdesc(self):
|
||||||
|
looker = self.speaker # Sender
|
||||||
|
target = self.receiver1 # Receiver1
|
||||||
|
looker.sdesc.add(sdesc0) # A nice sender of emotes
|
||||||
|
target.sdesc.add(sdesc1) # The first receiver of emotes.
|
||||||
|
|
||||||
|
# sdesc with no processing
|
||||||
|
self.assertEqual(looker.get_sdesc(target), "The first receiver of emotes.")
|
||||||
|
# sdesc with processing
|
||||||
|
self.assertEqual(looker.get_sdesc(target, process=True), "|bThe first receiver of emotes.|n")
|
||||||
|
|
||||||
|
looker.recog.add(target, recog01) # Mr Receiver
|
||||||
|
|
||||||
|
# recog with no processing
|
||||||
|
self.assertEqual(looker.get_sdesc(target), "Mr Receiver")
|
||||||
|
# recog with processing
|
||||||
|
self.assertEqual(looker.get_sdesc(target, process=True), "|mMr Receiver|n")
|
||||||
|
|
||||||
def test_send_emote(self):
|
def test_send_emote(self):
|
||||||
speaker = self.speaker
|
speaker = self.speaker
|
||||||
receiver1 = self.receiver1
|
receiver1 = self.receiver1
|
||||||
|
|
@ -212,18 +196,18 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
|
rpsystem.send_emote(speaker, receivers, emote, case_sensitive=False)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out0,
|
self.out0,
|
||||||
"With a flair, |bSender|n looks at |bThe first receiver of emotes.|n "
|
"With a flair, |mSender|n looks at |bThe first receiver of emotes.|n "
|
||||||
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
'and |bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out1,
|
self.out1,
|
||||||
"With a flair, |bA nice sender of emotes|n looks at |bReceiver1|n and "
|
"With a flair, |bA nice sender of emotes|n looks at |mReceiver1|n and "
|
||||||
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
'|bAnother nice colliding sdesc-guy for tests|n. She says |w"This is a test."|n',
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out2,
|
self.out2,
|
||||||
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
|
"With a flair, |bA nice sender of emotes|n looks at |bThe first "
|
||||||
'receiver of emotes.|n and |bReceiver2|n. She says |w"This is a test."|n',
|
'receiver of emotes.|n and |mReceiver2|n. She says |w"This is a test."|n',
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_send_case_sensitive_emote(self):
|
def test_send_case_sensitive_emote(self):
|
||||||
|
|
@ -241,20 +225,21 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
rpsystem.send_emote(speaker, receivers, case_emote)
|
rpsystem.send_emote(speaker, receivers, case_emote)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out0,
|
self.out0,
|
||||||
"|bSender|n looks at |bthe first receiver of emotes.|n, then "
|
"|mSender|n looks at |bthe first receiver of emotes.|n. Then, |mSender|n "
|
||||||
"|bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n and "
|
"looks at |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of emotes.|n "
|
||||||
"|bAnother nice colliding sdesc-guy for tests|n twice.",
|
"and |bAnother nice colliding sdesc-guy for tests|n twice.",
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out1,
|
self.out1,
|
||||||
"|bA nice sender of emotes|n looks at |bReceiver1|n, then |bReceiver1|n, "
|
"|bA nice sender of emotes|n looks at |mReceiver1|n. Then, "
|
||||||
"|bReceiver1|n and |bAnother nice colliding sdesc-guy for tests|n twice.",
|
"|ba nice sender of emotes|n looks at |mReceiver1|n, |mReceiver1|n "
|
||||||
|
"and |bAnother nice colliding sdesc-guy for tests|n twice."
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
self.out2,
|
self.out2,
|
||||||
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n, "
|
"|bA nice sender of emotes|n looks at |bthe first receiver of emotes.|n. "
|
||||||
"then |bTHE FIRST RECEIVER OF EMOTES.|n, |bThe first receiver of "
|
"Then, |ba nice sender of emotes|n looks at |bTHE FIRST RECEIVER OF EMOTES.|n, "
|
||||||
"emotes.|n and |bReceiver2|n twice.",
|
"|bThe first receiver of emotes.|n and |mReceiver2|n twice.",
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_rpsearch(self):
|
def test_rpsearch(self):
|
||||||
|
|
@ -265,18 +250,6 @@ class TestRPSystem(BaseEvenniaTest):
|
||||||
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
|
self.assertEqual(self.speaker.search("receiver of emotes"), self.receiver1)
|
||||||
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
|
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
|
||||||
|
|
||||||
def test_regex_tuple_from_key_alias(self):
|
|
||||||
self.speaker.aliases.add("foo bar")
|
|
||||||
self.speaker.aliases.add("this thing is a long thing")
|
|
||||||
t0 = time.time()
|
|
||||||
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
|
|
||||||
t1 = time.time()
|
|
||||||
result = rpsystem.regex_tuple_from_key_alias(self.speaker)
|
|
||||||
t2 = time.time()
|
|
||||||
# print(f"t1: {t1 - t0}, t2: {t2 - t1}")
|
|
||||||
self.assertLess(t2 - t1, 10**-4)
|
|
||||||
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))
|
|
||||||
|
|
||||||
|
|
||||||
class TestRPSystemCommands(BaseEvenniaCommandTest):
|
class TestRPSystemCommands(BaseEvenniaCommandTest):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
|
|
@ -305,7 +278,7 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
||||||
self.call(
|
self.call(
|
||||||
rpsystem.CmdRecog(),
|
rpsystem.CmdRecog(),
|
||||||
"barfoo as friend",
|
"barfoo as friend",
|
||||||
"Char will now remember BarFoo Character as friend.",
|
"You will now remember BarFoo Character as friend.",
|
||||||
)
|
)
|
||||||
self.call(
|
self.call(
|
||||||
rpsystem.CmdRecog(),
|
rpsystem.CmdRecog(),
|
||||||
|
|
@ -316,6 +289,6 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
|
||||||
self.call(
|
self.call(
|
||||||
rpsystem.CmdRecog(),
|
rpsystem.CmdRecog(),
|
||||||
"friend",
|
"friend",
|
||||||
"Char will now know them only as 'BarFoo Character'",
|
"You will now know them only as 'BarFoo Character'",
|
||||||
cmdstring="forget",
|
cmdstring="forget",
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -286,7 +286,7 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager):
|
||||||
categories = make_iter(category) if category else []
|
categories = make_iter(category) if category else []
|
||||||
n_keys = len(keys)
|
n_keys = len(keys)
|
||||||
n_categories = len(categories)
|
n_categories = len(categories)
|
||||||
unique_categories = sorted(set(categories))
|
unique_categories = set(categories)
|
||||||
n_unique_categories = len(unique_categories)
|
n_unique_categories = len(unique_categories)
|
||||||
|
|
||||||
dbmodel = self.model.__dbclass__.__name__.lower()
|
dbmodel = self.model.__dbclass__.__name__.lower()
|
||||||
|
|
|
||||||
|
|
@ -142,6 +142,13 @@ class TestTypedObjectManager(BaseEvenniaTest):
|
||||||
[self.obj1],
|
[self.obj1],
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def test_get_tag_with_any_including_nones(self):
|
||||||
|
self.obj1.tags.add("tagA", "categoryA")
|
||||||
|
self.assertEqual(
|
||||||
|
self._manager("get_by_tag", ["tagA", "tagB"], ["categoryA", "categoryB", None], match="any"),
|
||||||
|
[self.obj1],
|
||||||
|
)
|
||||||
|
|
||||||
def test_get_tag_withnomatch(self):
|
def test_get_tag_withnomatch(self):
|
||||||
self.obj1.tags.add("tagC", "categoryC")
|
self.obj1.tags.add("tagC", "categoryC")
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
|
|
|
||||||
|
|
@ -12,6 +12,7 @@ evennia.OPTION_CLASSES
|
||||||
|
|
||||||
|
|
||||||
from pickle import dumps
|
from pickle import dumps
|
||||||
|
from django.db.utils import OperationalError, ProgrammingError
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia.utils.utils import class_from_module, callables_from_module
|
from evennia.utils.utils import class_from_module, callables_from_module
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
|
|
@ -167,7 +168,6 @@ class GlobalScriptContainer(Container):
|
||||||
|
|
||||||
# store a hash representation of the setup
|
# store a hash representation of the setup
|
||||||
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
|
script.attributes.add("_global_script_settings", compare_hash, category="settings_hash")
|
||||||
script.start()
|
|
||||||
|
|
||||||
return script
|
return script
|
||||||
|
|
||||||
|
|
@ -183,9 +183,16 @@ class GlobalScriptContainer(Container):
|
||||||
# populate self.typeclass_storage
|
# populate self.typeclass_storage
|
||||||
self.load_data()
|
self.load_data()
|
||||||
|
|
||||||
# start registered scripts
|
# make sure settings-defined scripts are loaded
|
||||||
for key in self.loaded_data:
|
for key in self.loaded_data:
|
||||||
self._load_script(key)
|
self._load_script(key)
|
||||||
|
# start all global scripts
|
||||||
|
try:
|
||||||
|
for script in self._get_scripts():
|
||||||
|
script.start()
|
||||||
|
except (OperationalError, ProgrammingError):
|
||||||
|
# this can happen if db is not loaded yet (such as when building docs)
|
||||||
|
pass
|
||||||
|
|
||||||
def load_data(self):
|
def load_data(self):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -239,6 +239,9 @@ class _SaverMutable(object):
|
||||||
def __gt__(self, other):
|
def __gt__(self, other):
|
||||||
return self._data > other
|
return self._data > other
|
||||||
|
|
||||||
|
def __or__(self, other):
|
||||||
|
return self._data | other
|
||||||
|
|
||||||
@_save
|
@_save
|
||||||
def __setitem__(self, key, value):
|
def __setitem__(self, key, value):
|
||||||
self._data.__setitem__(key, self._convert_mutables(value))
|
self._data.__setitem__(key, self._convert_mutables(value))
|
||||||
|
|
@ -450,7 +453,9 @@ def deserialize(obj):
|
||||||
elif tname in ("_SaverOrderedDict", "OrderedDict"):
|
elif tname in ("_SaverOrderedDict", "OrderedDict"):
|
||||||
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
|
return OrderedDict([(_iter(key), _iter(val)) for key, val in obj.items()])
|
||||||
elif tname in ("_SaverDefaultDict", "defaultdict"):
|
elif tname in ("_SaverDefaultDict", "defaultdict"):
|
||||||
return defaultdict(obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()})
|
return defaultdict(
|
||||||
|
obj.default_factory, {_iter(key): _iter(val) for key, val in obj.items()}
|
||||||
|
)
|
||||||
elif tname in _DESERIALIZE_MAPPING:
|
elif tname in _DESERIALIZE_MAPPING:
|
||||||
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
|
return _DESERIALIZE_MAPPING[tname](_iter(val) for val in obj)
|
||||||
elif is_iter(obj):
|
elif is_iter(obj):
|
||||||
|
|
@ -602,7 +607,9 @@ def to_pickle(data):
|
||||||
|
|
||||||
def process_item(item):
|
def process_item(item):
|
||||||
"""Recursive processor and identification of data"""
|
"""Recursive processor and identification of data"""
|
||||||
|
|
||||||
dtype = type(item)
|
dtype = type(item)
|
||||||
|
|
||||||
if dtype in (str, int, float, bool, bytes, SafeString):
|
if dtype in (str, int, float, bool, bytes, SafeString):
|
||||||
return item
|
return item
|
||||||
elif dtype == tuple:
|
elif dtype == tuple:
|
||||||
|
|
@ -612,7 +619,10 @@ def to_pickle(data):
|
||||||
elif dtype in (dict, _SaverDict):
|
elif dtype in (dict, _SaverDict):
|
||||||
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
||||||
elif dtype in (defaultdict, _SaverDefaultDict):
|
elif dtype in (defaultdict, _SaverDefaultDict):
|
||||||
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
|
return defaultdict(
|
||||||
|
item.default_factory,
|
||||||
|
((process_item(key), process_item(val)) for key, val in item.items()),
|
||||||
|
)
|
||||||
elif dtype in (set, _SaverSet):
|
elif dtype in (set, _SaverSet):
|
||||||
return set(process_item(val) for val in item)
|
return set(process_item(val) for val in item)
|
||||||
elif dtype in (OrderedDict, _SaverOrderedDict):
|
elif dtype in (OrderedDict, _SaverOrderedDict):
|
||||||
|
|
@ -620,7 +630,20 @@ def to_pickle(data):
|
||||||
elif dtype in (deque, _SaverDeque):
|
elif dtype in (deque, _SaverDeque):
|
||||||
return deque(process_item(val) for val in item)
|
return deque(process_item(val) for val in item)
|
||||||
|
|
||||||
elif hasattr(item, "__iter__"):
|
# not one of the base types
|
||||||
|
if hasattr(item, "__serialize_dbobjs__"):
|
||||||
|
# Allows custom serialization of any dbobjects embedded in
|
||||||
|
# the item that Evennia will otherwise not found (these would
|
||||||
|
# otherwise lead to an error). Use the dbserialize helper from
|
||||||
|
# this method.
|
||||||
|
try:
|
||||||
|
item.__serialize_dbobjs__()
|
||||||
|
except TypeError:
|
||||||
|
# we catch typerrors so we can handle both classes (requiring
|
||||||
|
# classmethods) and instances
|
||||||
|
pass
|
||||||
|
|
||||||
|
if hasattr(item, "__iter__"):
|
||||||
# we try to conserve the iterable class, if not convert to list
|
# we try to conserve the iterable class, if not convert to list
|
||||||
try:
|
try:
|
||||||
return item.__class__([process_item(val) for val in item])
|
return item.__class__([process_item(val) for val in item])
|
||||||
|
|
@ -678,7 +701,10 @@ def from_pickle(data, db_obj=None):
|
||||||
elif dtype == dict:
|
elif dtype == dict:
|
||||||
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
return dict((process_item(key), process_item(val)) for key, val in item.items())
|
||||||
elif dtype == defaultdict:
|
elif dtype == defaultdict:
|
||||||
return defaultdict(item.default_factory, ((process_item(key), process_item(val)) for key, val in item.items()))
|
return defaultdict(
|
||||||
|
item.default_factory,
|
||||||
|
((process_item(key), process_item(val)) for key, val in item.items()),
|
||||||
|
)
|
||||||
elif dtype == set:
|
elif dtype == set:
|
||||||
return set(process_item(val) for val in item)
|
return set(process_item(val) for val in item)
|
||||||
elif dtype == OrderedDict:
|
elif dtype == OrderedDict:
|
||||||
|
|
@ -692,6 +718,18 @@ def from_pickle(data, db_obj=None):
|
||||||
return item.__class__(process_item(val) for val in item)
|
return item.__class__(process_item(val) for val in item)
|
||||||
except (AttributeError, TypeError):
|
except (AttributeError, TypeError):
|
||||||
return [process_item(val) for val in item]
|
return [process_item(val) for val in item]
|
||||||
|
|
||||||
|
if hasattr(item, "__deserialize_dbobjs__"):
|
||||||
|
# this allows the object to custom-deserialize any embedded dbobjs
|
||||||
|
# that we previously serialized with __serialize_dbobjs__.
|
||||||
|
# use the dbunserialize helper in this module.
|
||||||
|
try:
|
||||||
|
item.__deserialize_dbobjs__()
|
||||||
|
except TypeError:
|
||||||
|
# handle recoveries both of classes (requiring classmethods
|
||||||
|
# or instances
|
||||||
|
pass
|
||||||
|
|
||||||
return item
|
return item
|
||||||
|
|
||||||
def process_tree(item, parent):
|
def process_tree(item, parent):
|
||||||
|
|
|
||||||
|
|
@ -274,12 +274,13 @@ import inspect
|
||||||
|
|
||||||
from ast import literal_eval
|
from ast import literal_eval
|
||||||
from fnmatch import fnmatch
|
from fnmatch import fnmatch
|
||||||
|
from math import ceil
|
||||||
|
|
||||||
from inspect import isfunction, getargspec
|
from inspect import isfunction, getargspec
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
from evennia import Command, CmdSet
|
from evennia import Command, CmdSet
|
||||||
from evennia.utils import logger
|
from evennia.utils import logger
|
||||||
from evennia.utils.evtable import EvTable
|
from evennia.utils.evtable import EvTable, EvColumn
|
||||||
from evennia.utils.ansi import strip_ansi
|
from evennia.utils.ansi import strip_ansi
|
||||||
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
from evennia.utils.utils import mod_import, make_iter, pad, to_str, m_len, is_iter, dedent, crop
|
||||||
from evennia.commands import cmdhandler
|
from evennia.commands import cmdhandler
|
||||||
|
|
@ -1210,7 +1211,6 @@ class EvMenu:
|
||||||
Args:
|
Args:
|
||||||
optionlist (list): List of (key, description) tuples for every
|
optionlist (list): List of (key, description) tuples for every
|
||||||
option related to this node.
|
option related to this node.
|
||||||
caller (Object, Account or None, optional): The caller of the node.
|
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
options (str): The formatted option display.
|
options (str): The formatted option display.
|
||||||
|
|
@ -1229,7 +1229,7 @@ class EvMenu:
|
||||||
table = []
|
table = []
|
||||||
for key, desc in optionlist:
|
for key, desc in optionlist:
|
||||||
if key or desc:
|
if key or desc:
|
||||||
desc_string = ": %s" % desc if desc else ""
|
desc_string = f": {desc}" if desc else ""
|
||||||
table_width_max = max(
|
table_width_max = max(
|
||||||
table_width_max,
|
table_width_max,
|
||||||
max(m_len(p) for p in key.split("\n"))
|
max(m_len(p) for p in key.split("\n"))
|
||||||
|
|
@ -1239,42 +1239,31 @@ class EvMenu:
|
||||||
raw_key = strip_ansi(key)
|
raw_key = strip_ansi(key)
|
||||||
if raw_key != key:
|
if raw_key != key:
|
||||||
# already decorations in key definition
|
# already decorations in key definition
|
||||||
table.append(" |lc%s|lt%s|le%s" % (raw_key, key, desc_string))
|
table.append(f" |lc{raw_key}|lt{key}|le{desc_string}")
|
||||||
else:
|
else:
|
||||||
# add a default white color to key
|
# add a default white color to key
|
||||||
table.append(" |lc%s|lt|w%s|n|le%s" % (raw_key, raw_key, desc_string))
|
table.append(f" |lc{raw_key}|lt|w{key}|n|le{desc_string}")
|
||||||
ncols = _MAX_TEXT_WIDTH // table_width_max # number of ncols
|
ncols = _MAX_TEXT_WIDTH // table_width_max # number of columns
|
||||||
|
|
||||||
if ncols < 0:
|
if ncols < 0:
|
||||||
# no visible option at all
|
# no visible options at all
|
||||||
return ""
|
return ""
|
||||||
|
|
||||||
ncols = ncols + 1 if ncols == 0 else ncols
|
ncols = 1 if ncols == 0 else ncols
|
||||||
# get the amount of rows needed (start with 4 rows)
|
|
||||||
nrows = 4
|
|
||||||
while nrows * ncols < nlist:
|
|
||||||
nrows += 1
|
|
||||||
ncols = nlist // nrows # number of full columns
|
|
||||||
nlastcol = nlist % nrows # number of elements in last column
|
|
||||||
|
|
||||||
# get the final column count
|
# minimum number of rows in a column
|
||||||
ncols = ncols + 1 if nlastcol > 0 else ncols
|
min_rows = 4
|
||||||
if ncols > 1:
|
|
||||||
# only extend if longer than one column
|
|
||||||
table.extend([" " for i in range(nrows - nlastcol)])
|
|
||||||
|
|
||||||
# build the actual table grid
|
# split the items into columns
|
||||||
table = [table[icol * nrows : (icol * nrows) + nrows] for icol in range(0, ncols)]
|
split = max(min_rows, ceil(len(table)/ncols))
|
||||||
|
max_end = len(table)
|
||||||
|
cols_list = []
|
||||||
|
for icol in range(ncols):
|
||||||
|
start = icol*split
|
||||||
|
end = min(start+split,max_end)
|
||||||
|
cols_list.append(EvColumn(*table[start:end]))
|
||||||
|
|
||||||
# adjust the width of each column
|
return str(EvTable(table=cols_list, border="none"))
|
||||||
for icol in range(len(table)):
|
|
||||||
col_width = (
|
|
||||||
max(max(m_len(p) for p in part.split("\n")) for part in table[icol]) + colsep
|
|
||||||
)
|
|
||||||
table[icol] = [pad(part, width=col_width + colsep, align="l") for part in table[icol]]
|
|
||||||
|
|
||||||
# format the table into columns
|
|
||||||
return str(EvTable(table=table, border="none"))
|
|
||||||
|
|
||||||
def node_formatter(self, nodetext, optionstext):
|
def node_formatter(self, nodetext, optionstext):
|
||||||
"""
|
"""
|
||||||
|
|
|
||||||
|
|
@ -67,7 +67,7 @@ class TimeScript(DefaultScript):
|
||||||
callback(*args, **kwargs)
|
callback(*args, **kwargs)
|
||||||
|
|
||||||
seconds = real_seconds_until(**self.db.gametime)
|
seconds = real_seconds_until(**self.db.gametime)
|
||||||
self.restart(interval=seconds)
|
self.start(interval=seconds,force_restart=True)
|
||||||
|
|
||||||
|
|
||||||
# Access functions
|
# Access functions
|
||||||
|
|
|
||||||
|
|
@ -62,10 +62,12 @@ class TestDbSerialize(TestCase):
|
||||||
self.obj.db.test.sort(key=lambda d: str(d))
|
self.obj.db.test.sort(key=lambda d: str(d))
|
||||||
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
|
self.assertEqual(self.obj.db.test, [{0: 1}, {1: 0}])
|
||||||
|
|
||||||
def test_dict(self):
|
def test_saverdict(self):
|
||||||
self.obj.db.test = {"a": True}
|
self.obj.db.test = {"a": True}
|
||||||
self.obj.db.test.update({"b": False})
|
self.obj.db.test.update({"b": False})
|
||||||
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
|
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
|
||||||
|
self.obj.db.test |= {"c": 5}
|
||||||
|
self.assertEqual(self.obj.db.test, {"a": True, "b": False, "c": 5})
|
||||||
|
|
||||||
@parameterized.expand(
|
@parameterized.expand(
|
||||||
[
|
[
|
||||||
|
|
@ -88,27 +90,30 @@ class TestDbSerialize(TestCase):
|
||||||
self.assertIsInstance(value, base_type)
|
self.assertIsInstance(value, base_type)
|
||||||
self.assertNotIsInstance(value, saver_type)
|
self.assertNotIsInstance(value, saver_type)
|
||||||
self.assertEqual(value, default_value)
|
self.assertEqual(value, default_value)
|
||||||
self.obj.db.test = {'a': True}
|
self.obj.db.test = {"a": True}
|
||||||
self.obj.db.test.update({'b': False})
|
self.obj.db.test.update({"b": False})
|
||||||
self.assertEqual(self.obj.db.test, {'a': True, 'b': False})
|
self.assertEqual(self.obj.db.test, {"a": True, "b": False})
|
||||||
|
|
||||||
def test_defaultdict(self):
|
def test_defaultdict(self):
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
# baseline behavior for a defaultdict
|
# baseline behavior for a defaultdict
|
||||||
_dd = defaultdict(list)
|
_dd = defaultdict(list)
|
||||||
_dd['a']
|
_dd["a"]
|
||||||
self.assertEqual(_dd, {'a': []})
|
self.assertEqual(_dd, {"a": []})
|
||||||
|
|
||||||
# behavior after defaultdict is set as attribute
|
# behavior after defaultdict is set as attribute
|
||||||
|
|
||||||
dd = defaultdict(list)
|
dd = defaultdict(list)
|
||||||
self.obj.db.test = dd
|
self.obj.db.test = dd
|
||||||
self.obj.db.test['a']
|
self.obj.db.test["a"]
|
||||||
self.assertEqual(self.obj.db.test, {'a': []})
|
self.assertEqual(self.obj.db.test, {"a": []})
|
||||||
|
|
||||||
self.obj.db.test['a'].append(1)
|
self.obj.db.test["a"].append(1)
|
||||||
self.assertEqual(self.obj.db.test, {'a': [1]})
|
self.assertEqual(self.obj.db.test, {"a": [1]})
|
||||||
self.obj.db.test['a'].append(2)
|
self.obj.db.test["a"].append(2)
|
||||||
self.assertEqual(self.obj.db.test, {'a': [1, 2]})
|
self.assertEqual(self.obj.db.test, {"a": [1, 2]})
|
||||||
self.obj.db.test['a'].append(3)
|
self.obj.db.test["a"].append(3)
|
||||||
self.assertEqual(self.obj.db.test, {'a': [1, 2, 3]})
|
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3]})
|
||||||
|
self.obj.db.test |= {"b": [5, 6]}
|
||||||
|
self.assertEqual(self.obj.db.test, {"a": [1, 2, 3], "b": [5, 6]})
|
||||||
|
|
|
||||||
|
|
@ -7,20 +7,20 @@ import mock
|
||||||
|
|
||||||
|
|
||||||
class TestText2Html(TestCase):
|
class TestText2Html(TestCase):
|
||||||
def test_re_color(self):
|
def test_format_styles(self):
|
||||||
parser = text2html.HTML_PARSER
|
parser = text2html.HTML_PARSER
|
||||||
self.assertEqual("foo", parser.re_color("foo"))
|
self.assertEqual("foo", parser.format_styles("foo"))
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'<span class="color-001">red</span>foo',
|
'<span class="color-001">red</span>foo',
|
||||||
parser.re_color(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
parser.format_styles(ansi.ANSI_UNHILITE + ansi.ANSI_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'<span class="bgcolor-001">red</span>foo',
|
'<span class="bgcolor-001">red</span>foo',
|
||||||
parser.re_color(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
parser.format_styles(ansi.ANSI_BACK_RED + "red" + ansi.ANSI_NORMAL + "foo"),
|
||||||
)
|
)
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'<span class="bgcolor-001"><span class="color-002">red</span></span>foo',
|
'<span class="bgcolor-001 color-002">red</span>foo',
|
||||||
parser.re_color(
|
parser.format_styles(
|
||||||
ansi.ANSI_BACK_RED
|
ansi.ANSI_BACK_RED
|
||||||
+ ansi.ANSI_UNHILITE
|
+ ansi.ANSI_UNHILITE
|
||||||
+ ansi.ANSI_GREEN
|
+ ansi.ANSI_GREEN
|
||||||
|
|
@ -29,63 +29,37 @@ class TestText2Html(TestCase):
|
||||||
+ "foo"
|
+ "foo"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_re_bold(self):
|
|
||||||
parser = text2html.HTML_PARSER
|
|
||||||
self.assertEqual("foo", parser.re_bold("foo"))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# "a <strong>red</strong>foo", # TODO: why not?
|
'a <span class="underline">red</span>foo',
|
||||||
"a <strong>redfoo</strong>",
|
parser.format_styles(
|
||||||
parser.re_bold("a " + ansi.ANSI_HILITE + "red" + ansi.ANSI_UNHILITE + "foo"),
|
|
||||||
)
|
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_re_underline(self):
|
|
||||||
parser = text2html.HTML_PARSER
|
|
||||||
self.assertEqual("foo", parser.re_underline("foo"))
|
|
||||||
self.assertEqual(
|
|
||||||
'a <span class="underline">red</span>' + ansi.ANSI_NORMAL + "foo",
|
|
||||||
parser.re_underline(
|
|
||||||
"a "
|
"a "
|
||||||
+ ansi.ANSI_UNDERLINE
|
+ ansi.ANSI_UNDERLINE
|
||||||
+ "red"
|
+ "red"
|
||||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
+ ansi.ANSI_NORMAL
|
||||||
+ "foo"
|
+ "foo"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_re_blinking(self):
|
|
||||||
parser = text2html.HTML_PARSER
|
|
||||||
self.assertEqual("foo", parser.re_blinking("foo"))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'a <span class="blink">red</span>' + ansi.ANSI_NORMAL + "foo",
|
'a <span class="blink">red</span>foo',
|
||||||
parser.re_blinking(
|
parser.format_styles(
|
||||||
"a "
|
"a "
|
||||||
+ ansi.ANSI_BLINK
|
+ ansi.ANSI_BLINK
|
||||||
+ "red"
|
+ "red"
|
||||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
+ ansi.ANSI_NORMAL
|
||||||
+ "foo"
|
+ "foo"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_re_inversing(self):
|
|
||||||
parser = text2html.HTML_PARSER
|
|
||||||
self.assertEqual("foo", parser.re_inversing("foo"))
|
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
'a <span class="inverse">red</span>' + ansi.ANSI_NORMAL + "foo",
|
'a <span class="bgcolor-007 color-000">red</span>foo',
|
||||||
parser.re_inversing(
|
parser.format_styles(
|
||||||
"a "
|
"a "
|
||||||
+ ansi.ANSI_INVERSE
|
+ ansi.ANSI_INVERSE
|
||||||
+ "red"
|
+ "red"
|
||||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
+ ansi.ANSI_NORMAL
|
||||||
+ "foo"
|
+ "foo"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_remove_bells(self):
|
def test_remove_bells(self):
|
||||||
parser = text2html.HTML_PARSER
|
parser = text2html.HTML_PARSER
|
||||||
self.assertEqual("foo", parser.remove_bells("foo"))
|
self.assertEqual("foo", parser.remove_bells("foo"))
|
||||||
|
|
@ -95,7 +69,7 @@ class TestText2Html(TestCase):
|
||||||
"a "
|
"a "
|
||||||
+ ansi.ANSI_BEEP
|
+ ansi.ANSI_BEEP
|
||||||
+ "red"
|
+ "red"
|
||||||
+ ansi.ANSI_NORMAL # TODO: why does it keep it?
|
+ ansi.ANSI_NORMAL
|
||||||
+ "foo"
|
+ "foo"
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
@ -110,7 +84,6 @@ class TestText2Html(TestCase):
|
||||||
self.assertEqual("foo", parser.convert_linebreaks("foo"))
|
self.assertEqual("foo", parser.convert_linebreaks("foo"))
|
||||||
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
|
self.assertEqual("a<br> redfoo<br>", parser.convert_linebreaks("a\n redfoo\n"))
|
||||||
|
|
||||||
@unittest.skip("parser issues")
|
|
||||||
def test_convert_urls(self):
|
def test_convert_urls(self):
|
||||||
parser = text2html.HTML_PARSER
|
parser = text2html.HTML_PARSER
|
||||||
self.assertEqual("foo", parser.convert_urls("foo"))
|
self.assertEqual("foo", parser.convert_urls("foo"))
|
||||||
|
|
@ -118,7 +91,6 @@ class TestText2Html(TestCase):
|
||||||
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
|
'a <a href="http://redfoo" target="_blank">http://redfoo</a> runs',
|
||||||
parser.convert_urls("a http://redfoo runs"),
|
parser.convert_urls("a http://redfoo runs"),
|
||||||
)
|
)
|
||||||
# TODO: doesn't URL encode correctly
|
|
||||||
|
|
||||||
def test_sub_mxp_links(self):
|
def test_sub_mxp_links(self):
|
||||||
parser = text2html.HTML_PARSER
|
parser = text2html.HTML_PARSER
|
||||||
|
|
@ -186,22 +158,22 @@ class TestText2Html(TestCase):
|
||||||
self.assertEqual("foo", text2html.parse_html("foo"))
|
self.assertEqual("foo", text2html.parse_html("foo"))
|
||||||
self.maxDiff = None
|
self.maxDiff = None
|
||||||
self.assertEqual(
|
self.assertEqual(
|
||||||
# TODO: note that the blink is currently *not* correctly aborted
|
|
||||||
# with |n here! This is probably not possible to correctly handle
|
|
||||||
# with regex - a stateful parser may be needed.
|
|
||||||
# blink back-cyan normal underline red green yellow blue magenta cyan back-green
|
|
||||||
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
|
text2html.parse_html("|^|[CHello|n|u|rW|go|yr|bl|md|c!|[G!"),
|
||||||
'<span class="blink">'
|
'<span class="blink bgcolor-006">'
|
||||||
'<span class="bgcolor-006">Hello</span>' # noqa
|
'Hello'
|
||||||
'<span class="underline">'
|
'</span><span class="underline color-009">'
|
||||||
'<span class="color-009">W</span>' # noqa
|
'W'
|
||||||
'<span class="color-010">o</span>'
|
'</span><span class="underline color-010">'
|
||||||
'<span class="color-011">r</span>'
|
'o'
|
||||||
'<span class="color-012">l</span>'
|
'</span><span class="underline color-011">'
|
||||||
'<span class="color-013">d</span>'
|
'r'
|
||||||
'<span class="color-014">!'
|
'</span><span class="underline color-012">'
|
||||||
'<span class="bgcolor-002">!</span>' # noqa
|
'l'
|
||||||
"</span>"
|
'</span><span class="underline color-013">'
|
||||||
"</span>"
|
'd'
|
||||||
"</span>",
|
'</span><span class="underline color-014">'
|
||||||
|
'!'
|
||||||
|
'</span><span class="underline bgcolor-002 color-014">'
|
||||||
|
'!'
|
||||||
|
'</span>',
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -12,11 +12,10 @@ import re
|
||||||
from html import escape as html_escape
|
from html import escape as html_escape
|
||||||
from .ansi import *
|
from .ansi import *
|
||||||
|
|
||||||
|
|
||||||
# All xterm256 RGB equivalents
|
# All xterm256 RGB equivalents
|
||||||
|
|
||||||
XTERM256_FG = "\033[38;5;%sm"
|
XTERM256_FG = "\033[38;5;{}m"
|
||||||
XTERM256_BG = "\033[48;5;%sm"
|
XTERM256_BG = "\033[48;5;{}m"
|
||||||
|
|
||||||
|
|
||||||
class TextToHTMLparser(object):
|
class TextToHTMLparser(object):
|
||||||
|
|
@ -25,77 +24,65 @@ class TextToHTMLparser(object):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
tabstop = 4
|
tabstop = 4
|
||||||
# mapping html color name <-> ansi code.
|
|
||||||
hilite = ANSI_HILITE
|
|
||||||
unhilite = ANSI_UNHILITE # this will be stripped - there is no css equivalent.
|
|
||||||
normal = ANSI_NORMAL # "
|
|
||||||
underline = ANSI_UNDERLINE
|
|
||||||
blink = ANSI_BLINK
|
|
||||||
inverse = ANSI_INVERSE # this will produce an outline; no obvious css equivalent?
|
|
||||||
colorcodes = [
|
|
||||||
("color-000", unhilite + ANSI_BLACK), # pure black
|
|
||||||
("color-001", unhilite + ANSI_RED),
|
|
||||||
("color-002", unhilite + ANSI_GREEN),
|
|
||||||
("color-003", unhilite + ANSI_YELLOW),
|
|
||||||
("color-004", unhilite + ANSI_BLUE),
|
|
||||||
("color-005", unhilite + ANSI_MAGENTA),
|
|
||||||
("color-006", unhilite + ANSI_CYAN),
|
|
||||||
("color-007", unhilite + ANSI_WHITE), # light grey
|
|
||||||
("color-008", hilite + ANSI_BLACK), # dark grey
|
|
||||||
("color-009", hilite + ANSI_RED),
|
|
||||||
("color-010", hilite + ANSI_GREEN),
|
|
||||||
("color-011", hilite + ANSI_YELLOW),
|
|
||||||
("color-012", hilite + ANSI_BLUE),
|
|
||||||
("color-013", hilite + ANSI_MAGENTA),
|
|
||||||
("color-014", hilite + ANSI_CYAN),
|
|
||||||
("color-015", hilite + ANSI_WHITE), # pure white
|
|
||||||
] + [("color-%03i" % (i + 16), XTERM256_FG % ("%i" % (i + 16))) for i in range(240)]
|
|
||||||
|
|
||||||
colorback = [
|
style_codes = [
|
||||||
("bgcolor-000", ANSI_BACK_BLACK), # pure black
|
# non-color style markers
|
||||||
("bgcolor-001", ANSI_BACK_RED),
|
ANSI_NORMAL,
|
||||||
("bgcolor-002", ANSI_BACK_GREEN),
|
ANSI_UNDERLINE,
|
||||||
("bgcolor-003", ANSI_BACK_YELLOW),
|
ANSI_HILITE,
|
||||||
("bgcolor-004", ANSI_BACK_BLUE),
|
ANSI_UNHILITE,
|
||||||
("bgcolor-005", ANSI_BACK_MAGENTA),
|
ANSI_INVERSE,
|
||||||
("bgcolor-006", ANSI_BACK_CYAN),
|
ANSI_BLINK,
|
||||||
("bgcolor-007", ANSI_BACK_WHITE), # light grey
|
ANSI_INV_HILITE,
|
||||||
("bgcolor-008", hilite + ANSI_BACK_BLACK), # dark grey
|
ANSI_BLINK_HILITE,
|
||||||
("bgcolor-009", hilite + ANSI_BACK_RED),
|
ANSI_INV_BLINK,
|
||||||
("bgcolor-010", hilite + ANSI_BACK_GREEN),
|
ANSI_INV_BLINK_HILITE,
|
||||||
("bgcolor-011", hilite + ANSI_BACK_YELLOW),
|
]
|
||||||
("bgcolor-012", hilite + ANSI_BACK_BLUE),
|
|
||||||
("bgcolor-013", hilite + ANSI_BACK_MAGENTA),
|
|
||||||
("bgcolor-014", hilite + ANSI_BACK_CYAN),
|
|
||||||
("bgcolor-015", hilite + ANSI_BACK_WHITE), # pure white
|
|
||||||
] + [("bgcolor-%03i" % (i + 16), XTERM256_BG % ("%i" % (i + 16))) for i in range(240)]
|
|
||||||
|
|
||||||
# make sure to escape [
|
ansi_color_codes = [
|
||||||
# colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes]
|
# Foreground colors
|
||||||
# colorback = [(c, code.replace("[", r"\[")) for c, code in colorback]
|
ANSI_BLACK,
|
||||||
fg_colormap = dict((code, clr) for clr, code in colorcodes)
|
ANSI_RED,
|
||||||
bg_colormap = dict((code, clr) for clr, code in colorback)
|
ANSI_GREEN,
|
||||||
|
ANSI_YELLOW,
|
||||||
|
ANSI_BLUE,
|
||||||
|
ANSI_MAGENTA,
|
||||||
|
ANSI_CYAN,
|
||||||
|
ANSI_WHITE,
|
||||||
|
]
|
||||||
|
|
||||||
# create stop markers
|
xterm_fg_codes = [XTERM256_FG.format(i + 16) for i in range(240)]
|
||||||
fgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m|\033\[0m|$"
|
|
||||||
bgstop = "(?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m|\033\[0m|$"
|
|
||||||
bgfgstop = bgstop[:-2] + fgstop
|
|
||||||
|
|
||||||
fgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[3[0-8].*?m)"
|
ansi_bg_codes = [
|
||||||
bgstart = "((?:\033\[1m|\033\[22m){0,1}\033\[4[0-8].*?m)"
|
# Background colors
|
||||||
bgfgstart = bgstart + r"((?:\033\[1m|\033\[22m){0,1}\033\[[3-4][0-8].*?m){0,1}"
|
ANSI_BACK_BLACK,
|
||||||
|
ANSI_BACK_RED,
|
||||||
|
ANSI_BACK_GREEN,
|
||||||
|
ANSI_BACK_YELLOW,
|
||||||
|
ANSI_BACK_BLUE,
|
||||||
|
ANSI_BACK_MAGENTA,
|
||||||
|
ANSI_BACK_CYAN,
|
||||||
|
ANSI_BACK_WHITE,
|
||||||
|
]
|
||||||
|
|
||||||
# extract color markers, tagging the start marker and the text marked
|
xterm_bg_codes = [XTERM256_BG.format(i + 16) for i in range(240)]
|
||||||
re_fgs = re.compile(fgstart + "(.*?)(?=" + fgstop + ")")
|
|
||||||
re_bgs = re.compile(bgstart + "(.*?)(?=" + bgstop + ")")
|
re_style = re.compile(
|
||||||
re_bgfg = re.compile(bgfgstart + "(.*?)(?=" + bgfgstop + ")")
|
r"({})".format(
|
||||||
|
"|".join(
|
||||||
|
style_codes + ansi_color_codes + xterm_fg_codes + ansi_bg_codes + xterm_bg_codes
|
||||||
|
).replace("[", r"\[")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
colorlist = (
|
||||||
|
[ANSI_UNHILITE + code for code in ansi_color_codes]
|
||||||
|
+ [ANSI_HILITE + code for code in ansi_color_codes]
|
||||||
|
+ xterm_fg_codes
|
||||||
|
)
|
||||||
|
|
||||||
|
bglist = ansi_bg_codes + [ANSI_HILITE + code for code in ansi_bg_codes] + xterm_bg_codes
|
||||||
|
|
||||||
re_normal = re.compile(normal.replace("[", r"\["))
|
|
||||||
re_hilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (hilite.replace("[", r"\["), fgstop, bgstop))
|
|
||||||
re_unhilite = re.compile("(?:%s)(.*)(?=%s|%s)" % (unhilite.replace("[", r"\["), fgstop, bgstop))
|
|
||||||
re_uline = re.compile("(?:%s)(.*?)(?=%s|%s)" % (underline.replace("[", r"\["), fgstop, bgstop))
|
|
||||||
re_blink = re.compile("(?:%s)(.*?)(?=%s|%s)" % (blink.replace("[", r"\["), fgstop, bgstop))
|
|
||||||
re_inverse = re.compile("(?:%s)(.*?)(?=%s|%s)" % (inverse.replace("[", r"\["), fgstop, bgstop))
|
|
||||||
re_string = re.compile(
|
re_string = re.compile(
|
||||||
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
|
r"(?P<htmlchars>[<&>])|(?P<tab>[\t]+)|(?P<lineend>\r\n|\r|\n)",
|
||||||
re.S | re.M | re.I,
|
re.S | re.M | re.I,
|
||||||
|
|
@ -106,100 +93,6 @@ class TextToHTMLparser(object):
|
||||||
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
re_mxplink = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||||
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
|
re_mxpurl = re.compile(r"\|lu(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||||
|
|
||||||
def _sub_bgfg(self, colormatch):
|
|
||||||
# print("colormatch.groups()", colormatch.groups())
|
|
||||||
bgcode, fgcode, text = colormatch.groups()
|
|
||||||
if not fgcode:
|
|
||||||
ret = r"""<span class="%s">%s</span>""" % (
|
|
||||||
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
ret = r"""<span class="%s"><span class="%s">%s</span></span>""" % (
|
|
||||||
self.bg_colormap.get(bgcode, self.fg_colormap.get(bgcode, "err")),
|
|
||||||
self.fg_colormap.get(fgcode, self.bg_colormap.get(fgcode, "err")),
|
|
||||||
text,
|
|
||||||
)
|
|
||||||
return ret
|
|
||||||
|
|
||||||
def _sub_fg(self, colormatch):
|
|
||||||
code, text = colormatch.groups()
|
|
||||||
return r"""<span class="%s">%s</span>""" % (self.fg_colormap.get(code, "err"), text)
|
|
||||||
|
|
||||||
def _sub_bg(self, colormatch):
|
|
||||||
code, text = colormatch.groups()
|
|
||||||
return r"""<span class="%s">%s</span>""" % (self.bg_colormap.get(code, "err"), text)
|
|
||||||
|
|
||||||
def re_color(self, text):
|
|
||||||
"""
|
|
||||||
Replace ansi colors with html color class names. Let the
|
|
||||||
client choose how it will display colors, if it wishes to.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): the string with color to replace.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
text (str): Re-colored text.
|
|
||||||
|
|
||||||
"""
|
|
||||||
text = self.re_bgfg.sub(self._sub_bgfg, text)
|
|
||||||
text = self.re_fgs.sub(self._sub_fg, text)
|
|
||||||
text = self.re_bgs.sub(self._sub_bg, text)
|
|
||||||
text = self.re_normal.sub("", text)
|
|
||||||
return text
|
|
||||||
|
|
||||||
def re_bold(self, text):
|
|
||||||
"""
|
|
||||||
Clean out superfluous hilights rather than set <strong>to make
|
|
||||||
it match the look of telnet.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
text (str): Processed text.
|
|
||||||
|
|
||||||
"""
|
|
||||||
text = self.re_hilite.sub(r"<strong>\1</strong>", text)
|
|
||||||
return self.re_unhilite.sub(r"\1", text) # strip unhilite - there is no equivalent in css.
|
|
||||||
|
|
||||||
def re_underline(self, text):
|
|
||||||
"""
|
|
||||||
Replace ansi underline with html underline class name.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
text (str): Processed text.
|
|
||||||
|
|
||||||
"""
|
|
||||||
return self.re_uline.sub(r'<span class="underline">\1</span>', text)
|
|
||||||
|
|
||||||
def re_blinking(self, text):
|
|
||||||
"""
|
|
||||||
Replace ansi blink with custom blink css class
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
text (str): Processed text.
|
|
||||||
"""
|
|
||||||
return self.re_blink.sub(r'<span class="blink">\1</span>', text)
|
|
||||||
|
|
||||||
def re_inversing(self, text):
|
|
||||||
"""
|
|
||||||
Replace ansi inverse with custom inverse css class
|
|
||||||
|
|
||||||
Args:
|
|
||||||
text (str): Text to process.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
text (str): Processed text.
|
|
||||||
"""
|
|
||||||
return self.re_inverse.sub(r'<span class="inverse">\1</span>', text)
|
|
||||||
|
|
||||||
def remove_bells(self, text):
|
def remove_bells(self, text):
|
||||||
"""
|
"""
|
||||||
Remove ansi specials
|
Remove ansi specials
|
||||||
|
|
@ -211,7 +104,7 @@ class TextToHTMLparser(object):
|
||||||
text (str): Processed text.
|
text (str): Processed text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
return text.replace("\07", "")
|
return text.replace(ANSI_BEEP, "")
|
||||||
|
|
||||||
def remove_backspaces(self, text):
|
def remove_backspaces(self, text):
|
||||||
"""
|
"""
|
||||||
|
|
@ -315,6 +208,128 @@ class TextToHTMLparser(object):
|
||||||
return text
|
return text
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
def format_styles(self, text):
|
||||||
|
"""
|
||||||
|
Takes a string with parsed ANSI codes and replaces them with
|
||||||
|
HTML spans and CSS classes.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
text (str): The string to process.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
text (str): Processed text.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# split out the ANSI codes and clean out any empty items
|
||||||
|
str_list = [substr for substr in self.re_style.split(text) if substr]
|
||||||
|
# initialize all the flags and classes
|
||||||
|
classes = []
|
||||||
|
clean = True
|
||||||
|
inverse = False
|
||||||
|
# default color is light grey - unhilite + white
|
||||||
|
hilight = ANSI_UNHILITE
|
||||||
|
fg = ANSI_WHITE
|
||||||
|
# default bg is black
|
||||||
|
bg = ANSI_BACK_BLACK
|
||||||
|
|
||||||
|
for i, substr in enumerate(str_list):
|
||||||
|
# reset all current styling
|
||||||
|
if substr == ANSI_NORMAL and not clean:
|
||||||
|
# replace with close existing tag
|
||||||
|
str_list[i] = "</span>"
|
||||||
|
# reset to defaults
|
||||||
|
classes = []
|
||||||
|
clean = True
|
||||||
|
inverse = False
|
||||||
|
hilight = ANSI_UNHILITE
|
||||||
|
fg = ANSI_WHITE
|
||||||
|
bg = ANSI_BACK_BLACK
|
||||||
|
|
||||||
|
# change color
|
||||||
|
elif substr in self.ansi_color_codes + self.xterm_fg_codes:
|
||||||
|
# erase ANSI code from output
|
||||||
|
str_list[i] = ""
|
||||||
|
# set new color
|
||||||
|
fg = substr
|
||||||
|
|
||||||
|
# change bg color
|
||||||
|
elif substr in self.ansi_bg_codes + self.xterm_bg_codes:
|
||||||
|
# erase ANSI code from output
|
||||||
|
str_list[i] = ""
|
||||||
|
# set new bg
|
||||||
|
bg = substr
|
||||||
|
|
||||||
|
# non-color codes
|
||||||
|
elif substr in self.style_codes:
|
||||||
|
# erase ANSI code from output
|
||||||
|
str_list[i] = ""
|
||||||
|
|
||||||
|
# hilight codes
|
||||||
|
if substr in (ANSI_HILITE, ANSI_UNHILITE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
|
||||||
|
# set new hilight status
|
||||||
|
hilight = ANSI_UNHILITE if substr == ANSI_UNHILITE else ANSI_HILITE
|
||||||
|
|
||||||
|
# inversion codes
|
||||||
|
if substr in (ANSI_INVERSE, ANSI_INV_HILITE, ANSI_INV_BLINK_HILITE):
|
||||||
|
inverse = True
|
||||||
|
|
||||||
|
# blink codes
|
||||||
|
if (
|
||||||
|
substr in (ANSI_BLINK, ANSI_BLINK_HILITE, ANSI_INV_BLINK_HILITE)
|
||||||
|
and "blink" not in classes
|
||||||
|
):
|
||||||
|
classes.append("blink")
|
||||||
|
|
||||||
|
# underline
|
||||||
|
if substr == ANSI_UNDERLINE and "underline" not in classes:
|
||||||
|
classes.append("underline")
|
||||||
|
|
||||||
|
else:
|
||||||
|
# normal text, add text back to list
|
||||||
|
if not str_list[i - 1]:
|
||||||
|
# prior entry was cleared, which means style change
|
||||||
|
# get indices for the fg and bg codes
|
||||||
|
bg_index = self.bglist.index(bg)
|
||||||
|
try:
|
||||||
|
color_index = self.colorlist.index(hilight + fg)
|
||||||
|
except ValueError:
|
||||||
|
# xterm256 colors don't have the hilight codes
|
||||||
|
color_index = self.colorlist.index(fg)
|
||||||
|
|
||||||
|
if inverse:
|
||||||
|
# inverse means swap fg and bg indices
|
||||||
|
bg_class = "bgcolor-{}".format(str(color_index).rjust(3, "0"))
|
||||||
|
color_class = "color-{}".format(str(bg_index).rjust(3, "0"))
|
||||||
|
else:
|
||||||
|
# use fg and bg indices for classes
|
||||||
|
bg_class = "bgcolor-{}".format(str(bg_index).rjust(3, "0"))
|
||||||
|
color_class = "color-{}".format(str(color_index).rjust(3, "0"))
|
||||||
|
|
||||||
|
# black bg is the default, don't explicitly style
|
||||||
|
if bg_class != "bgcolor-000":
|
||||||
|
classes.append(bg_class)
|
||||||
|
# light grey text is the default, don't explicitly style
|
||||||
|
if color_class != "color-007":
|
||||||
|
classes.append(color_class)
|
||||||
|
# define the new style span
|
||||||
|
prefix = '<span class="{}">'.format(" ".join(classes))
|
||||||
|
# close any prior span
|
||||||
|
if not clean:
|
||||||
|
prefix = "</span>" + prefix
|
||||||
|
# add span to output
|
||||||
|
str_list[i - 1] = prefix
|
||||||
|
|
||||||
|
# clean out color classes to easily update next time
|
||||||
|
classes = [cls for cls in classes if "color" not in cls]
|
||||||
|
# flag as currently being styled
|
||||||
|
clean = False
|
||||||
|
|
||||||
|
# close span if necessary
|
||||||
|
if not clean:
|
||||||
|
str_list.append("</span>")
|
||||||
|
# recombine back into string
|
||||||
|
return "".join(str_list)
|
||||||
|
|
||||||
def parse(self, text, strip_ansi=False):
|
def parse(self, text, strip_ansi=False):
|
||||||
"""
|
"""
|
||||||
Main access function, converts a text containing ANSI codes
|
Main access function, converts a text containing ANSI codes
|
||||||
|
|
@ -328,19 +343,14 @@ class TextToHTMLparser(object):
|
||||||
text (str): Parsed text.
|
text (str): Parsed text.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# print(f"incoming text:\n{text}")
|
|
||||||
# parse everything to ansi first
|
# parse everything to ansi first
|
||||||
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
|
text = parse_ansi(text, strip_ansi=strip_ansi, xterm256=True, mxp=True)
|
||||||
# convert all ansi to html
|
# convert all ansi to html
|
||||||
result = re.sub(self.re_string, self.sub_text, text)
|
result = re.sub(self.re_string, self.sub_text, text)
|
||||||
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
result = re.sub(self.re_mxplink, self.sub_mxp_links, result)
|
||||||
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
|
result = re.sub(self.re_mxpurl, self.sub_mxp_urls, result)
|
||||||
result = self.re_color(result)
|
|
||||||
result = self.re_bold(result)
|
|
||||||
result = self.re_underline(result)
|
|
||||||
result = self.re_blinking(result)
|
|
||||||
result = self.re_inversing(result)
|
|
||||||
result = self.remove_bells(result)
|
result = self.remove_bells(result)
|
||||||
|
result = self.format_styles(result)
|
||||||
result = self.convert_linebreaks(result)
|
result = self.convert_linebreaks(result)
|
||||||
result = self.remove_backspaces(result)
|
result = self.remove_backspaces(result)
|
||||||
result = self.convert_urls(result)
|
result = self.convert_urls(result)
|
||||||
|
|
|
||||||
|
|
@ -149,7 +149,7 @@ An "emitter" object must have a function
|
||||||
// kwargs (obj): keyword-args to listener
|
// kwargs (obj): keyword-args to listener
|
||||||
//
|
//
|
||||||
emit: function (cmdname, args, kwargs) {
|
emit: function (cmdname, args, kwargs) {
|
||||||
if (kwargs.cmdid) {
|
if (kwargs.cmdid && (kwargs.cmdid in cmdmap)) {
|
||||||
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
|
cmdmap[kwargs.cmdid].apply(this, [args, kwargs]);
|
||||||
delete cmdmap[kwargs.cmdid];
|
delete cmdmap[kwargs.cmdid];
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue