Resolve merge conflict

This commit is contained in:
TehomCD 2020-03-08 00:10:18 -05:00
commit 1153433a2c
55 changed files with 590 additions and 348 deletions

View file

@ -40,7 +40,7 @@ SERVERNAME = "testing_mygame"
DATABASES = {
"default": {
"ENGINE": "django.db.backends.postgresql_psycopg2",
"ENGINE": "django.db.backends.postgresql",
"NAME": "evennia",
"USER": "evennia",
"PASSWORD": "password",

View file

@ -48,6 +48,10 @@ without arguments starts a full interactive Python console.
`.get_command_info()` method for easier overloading and access. (Volund)
- Removed unused `CYCLE_LOGFILES` setting. Added `SERVER_LOG_DAY_ROTATION`
and `SERVER_LOG_MAX_SIZE` (and equivalent for PORTAL) to control log rotation.
- Addded `inside_rec` lockfunc - if room is locked, the normal `inside()` lockfunc will
fail e.g. for your inventory objs (since their loc is you), whereas this will pass.
- RPSystem contrib's CmdRecog will now list all recogs if no arg is given. Also multiple
bugfixes.
## Evennia 0.9 (2018-2019)

View file

@ -1,137 +1,5 @@
# Evennia installation
The latest and more detailed installation instructions can be found
[here](https://github.com/evennia/evennia/wiki/Getting-Started).
## Installing Python
First install [Python](https://www.python.org/). Linux users should
have it in their repositories, Windows/Mac users can get it from the
Python homepage. You need the 2.7.x version (Python 3 is not yet
supported). Windows users, make sure to select the option to make
Python available in your path - this is so you can call it everywhere
as `python`. Python 2.7.9 and later also includes the
[pip](https://pypi.python.org/pypi/pip/) installer out of the box,
otherwise install this separately (in linux it's usually found as the
`python-pip` package).
### installing virtualenv
This step is optional, but *highly* recommended. For installing
up-to-date Python packages we recommend using
[virtualenv](https://pypi.python.org/pypi/virtualenv), this makes it
easy to keep your Python packages up-to-date without interfering with
the defaults for your system.
```
pip install virtualenv
```
Go to the place where you want to make your virtual python library
storage. This does not need to be near where you plan to install
Evennia. Then do
```
virtualenv vienv
```
A new folder `vienv` will be created (you could also name it something
else if you prefer). Activate the virtual environment like this:
```
# for Linux/Unix/Mac:
source vienv/bin/activate
# for Windows:
vienv\Scripts\activate.bat
```
You should see `(vienv)` next to your prompt to show you the
environment is active. You need to activate it whenever you open a new
terminal, but you *don't* have to be inside the `vienv` folder henceforth.
## Get the developer's version of Evennia
This is currently the only Evennia version available. First download
and install [Git](http://git-scm.com/) from the homepage or via the
package manager in Linux. Next, go to the place where you want the
`evennia` folder to be created and run
```
git clone https://github.com/evennia/evennia.git
```
If you have a github account and have [set up SSH
keys](https://help.github.com/articles/generating-ssh-keys/), you want
to use this instead:
```
git clone git@github.com:evennia/evennia.git
```
In the future you just enter the new `evennia` folder and do
```
git pull
```
to get the latest Evennia updates.
## Evennia package install
Stand at the root of your new `evennia` directory and run
```
pip install -e .
```
(note the period "." at the end, this tells pip to install from the
current directory). This will install Evennia and all its dependencies
(into your virtualenv if you are using that) and make the `evennia`
command available on the command line. You can find Evennia's
dependencies in `evennia/requirements.txt`.
## Creating your game project
To create your new game you need to initialize a new game project.
This should be done somewhere *outside* of your `evennia` folder.
```
evennia --init mygame
```
This will create a new game project named "mygame" in a folder of the
same name. If you want to change the settings for your project, you
will need to edit `mygame/server/conf/settings.py`.
## Starting Evennia
Enter your new game directory and run
```
evennia migrate
evennia start
```
Follow the instructions to create your superuser account. A lot of
information will scroll past as the database is created and the server
initializes. After this Evennia will be running. Use
```
evennia -h
```
for help with starting, stopping and other operations.
Start up your MUD client of choice and point it to your server and
port *4000*. If you are just running locally the server name is
*localhost*.
Alternatively, you can find the web interface and webclient by
pointing your web browser to *http://localhost:4001*.
Finally, login with the superuser account and password you provided
earlier. Welcome to Evennia!
You can find the latest updated installation instructions and
requirements [here](https://github.com/evennia/evennia/wiki/Getting-Started).

View file

@ -37,7 +37,7 @@ from evennia.scripts.scripthandler import ScriptHandler
from evennia.commands.cmdsethandler import CmdSetHandler
from evennia.utils.optionhandler import OptionHandler
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from random import getrandbits
__all__ = ("DefaultAccount",)
@ -828,7 +828,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
server.
Args:
text (str, optional): text data to send
text (str or tuple, optional): The message to send. This
is treated internally like any send-command, so its
value can be a tuple if sending multiple arguments to
the `text` oob command.
from_obj (Object or Account or list, optional): Object sending. If given, its
at_msg_send() hook will be called. If iterable, call on all entities.
session (Session or list, optional): Session object or a list of
@ -859,7 +862,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
kwargs["options"] = options
if text is not None:
kwargs["text"] = to_str(text)
if not (isinstance(text, str) or isinstance(text, tuple)):
# sanitize text before sending across the wire
try:
text = to_str(text)
except Exception:
text = repr(text)
kwargs["text"] = text
# session relay
sessions = make_iter(session) if session else self.sessions.all()

View file

@ -48,7 +48,7 @@ from evennia.comms.channelhandler import CHANNELHANDLER
from evennia.utils import logger, utils
from evennia.utils.utils import string_suggestions
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
_IN_GAME_ERRORS = settings.IN_GAME_ERRORS

View file

@ -27,7 +27,7 @@ Set theory.
"""
from weakref import WeakKeyDictionary
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from evennia.utils.utils import inherits_from, is_iter
__all__ = ("CmdSet",)

View file

@ -72,7 +72,7 @@ from evennia.utils import logger, utils
from evennia.commands.cmdset import CmdSet
from evennia.server.models import ServerConfig
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
__all__ = ("import_cmdset", "CmdSetHandler")

View file

@ -6,6 +6,7 @@ All commands in Evennia inherit from the 'Command' class in this module.
"""
import re
import math
import inspect
from django.conf import settings
@ -74,6 +75,13 @@ def _init_command(cls, **kwargs):
cls.is_exit = False
if not hasattr(cls, "help_category"):
cls.help_category = "general"
# make sure to pick up the parent's docstring if the child class is
# missing one (important for auto-help)
if cls.__doc__ is None:
for parent_class in inspect.getmro(cls):
if parent_class.__doc__ is not None:
cls.__doc__ = parent_class.__doc__
break
cls.help_category = cls.help_category.lower()

View file

@ -19,6 +19,7 @@ from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore
from evennia.prototypes import spawner, prototypes as protlib, menus as olc_menus
from evennia.utils.ansi import raw
from evennia.prototypes.menus import _format_diff_text_and_options
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
@ -1912,8 +1913,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
Usage:
typeclass[/switch] <object> [= typeclass.path]
type ''
parent ''
typeclass/prototype <object> = prototype_key
typeclass/list/show [typeclass.path]
swap - this is a shorthand for using /force/reset flags.
update - this is a shorthand for using the /force/reload flag.
@ -1930,9 +1931,12 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
list - show available typeclasses. Only typeclasses in modules actually
imported or used from somewhere in the code will show up here
(those typeclasses are still available if you know the path)
prototype - clean and overwrite the object with the specified
prototype key - effectively making a whole new object.
Example:
type button = examples.red_button.RedButton
type/prototype button=a red button
If the typeclass_path is not given, the current object's typeclass is
assumed.
@ -1954,7 +1958,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
key = "typeclass"
aliases = ["type", "parent", "swap", "update"]
switch_options = ("show", "examine", "update", "reset", "force", "list")
switch_options = ("show", "examine", "update", "reset", "force", "list", "prototype")
locks = "cmd:perm(typeclass) or perm(Builder)"
help_category = "Building"
@ -2038,6 +2042,27 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
new_typeclass = self.rhs or obj.path
prototype = None
if "prototype" in self.switches:
key = self.rhs
prototype = protlib.search_prototype(key=key)
if len(prototype) > 1:
caller.msg(
"More than one match for {}:\n{}".format(
key, "\n".join(proto.get("prototype_key", "") for proto in prototype)
)
)
return
elif prototype:
# one match
prototype = prototype[0]
else:
# no match
caller.msg("No prototype '{}' was found.".format(key))
return
new_typeclass = prototype["typeclass"]
self.switches.append("force")
if "show" in self.switches or "examine" in self.switches:
string = "%s's current typeclass is %s." % (obj.name, obj.__class__)
caller.msg(string)
@ -2070,11 +2095,34 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
hooks = "at_object_creation" if update else "all"
old_typeclass_path = obj.typeclass_path
# special prompt for the user in cases where we want
# to confirm changes.
if "prototype" in self.switches:
diff, _ = spawner.prototype_diff_from_object(prototype, obj)
txt, options = _format_diff_text_and_options(diff, objects=[obj])
prompt = (
"Applying prototype '%s' over '%s' will cause the follow changes:\n%s\n"
% (prototype["key"], obj.name, "\n".join(txt))
)
if not reset:
prompt += "\n|yWARNING:|n Use the /reset switch to apply the prototype over a blank state."
prompt += "\nAre you sure you want to apply these changes [yes]/no?"
answer = yield (prompt)
if answer and answer in ("no", "n"):
caller.msg("Canceled: No changes were applied.")
return
# we let this raise exception if needed
obj.swap_typeclass(
new_typeclass, clean_attributes=reset, clean_cmdsets=reset, run_start_hooks=hooks
)
if "prototype" in self.switches:
modified = spawner.batch_update_objects_with_prototype(prototype, objects=[obj])
prototype_success = modified > 0
if not prototype_success:
caller.msg("Prototype %s failed to apply." % prototype["key"])
if is_same:
string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.path)
else:
@ -2091,6 +2139,11 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
string += " All old attributes where deleted before the swap."
else:
string += " Attributes set before swap were not removed."
if "prototype" in self.switches and prototype_success:
string += (
" Prototype '%s' was successfully applied over the object type."
% prototype["key"]
)
caller.msg(string)
@ -2832,8 +2885,8 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
reference. A puppeted object cannot be moved to None.
loc - teleport object to the target's location instead of its contents
Teleports an object somewhere. If no object is given, you yourself
is teleported to the target location.
Teleports an object somewhere. If no object is given, you yourself are
teleported to the target location.
"""
key = "tel"
@ -2998,7 +3051,8 @@ class CmdScript(COMMAND_DEFAULT_CLASS):
ok = obj.scripts.add(self.rhs, autostart=True)
if not ok:
result.append(
"\nScript %s could not be added and/or started on %s."
"\nScript %s could not be added and/or started on %s "
"(or it started and immediately shut down)."
% (self.rhs, obj.get_display_name(caller))
)
else:

View file

@ -79,7 +79,7 @@ class CmdHelp(Command):
evmore.msg(self.caller, text, session=self.session)
return
self.msg((text, {"type": "help"}))
self.msg(text=(text, {"type": "help"}))
@staticmethod
def format_help_entry(title, help_text, aliases=None, suggested=None):

View file

@ -991,6 +991,34 @@ class TestBuilding(CommandTest):
"All object creation hooks were run. All old attributes where deleted before the swap.",
)
from evennia.prototypes.prototypes import homogenize_prototype
test_prototype = [
homogenize_prototype(
{
"prototype_key": "testkey",
"prototype_tags": [],
"typeclass": "typeclasses.objects.Object",
"key": "replaced_obj",
"attrs": [("foo", "bar", None, ""), ("desc", "protdesc", None, "")],
}
)
]
with mock.patch(
"evennia.commands.default.building.protlib.search_prototype",
new=mock.MagicMock(return_value=test_prototype),
) as mprot:
self.call(
building.CmdTypeclass(),
"/prototype Obj=testkey",
"replaced_obj changed typeclass from "
"evennia.objects.objects.DefaultObject to "
"typeclasses.objects.Object.\nAll object creation hooks were "
"run. Attributes set before swap were not removed. Prototype "
"'replaced_obj' was successfully applied over the object type.",
)
assert self.obj1.db.desc == "protdesc"
def test_lock(self):
self.call(building.CmdLock(), "", "Usage: ")
self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")

View file

@ -27,7 +27,7 @@ from django.conf import settings
from evennia.commands import cmdset, command
from evennia.utils.logger import tail_log_file
from evennia.utils.utils import class_from_module
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
# we must late-import these since any overloads are likely to
# themselves be using these classes leading to a circular import.

View file

@ -8,6 +8,7 @@ Comm system components.
from django.db.models import Q
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
from evennia.utils import logger
from evennia.utils.utils import dbref
_GA = object.__getattribute__
_AccountDB = None
@ -31,32 +32,6 @@ class CommError(Exception):
#
def dbref(inp, reqhash=True):
"""
Valid forms of dbref (database reference number) are either a
string '#N' or an integer N.
Args:
inp (int or str): A possible dbref to check syntactically.
reqhash (bool): Require an initial hash `#` to accept.
Returns:
is_dbref (int or None): The dbref integer part if a valid
dbref, otherwise `None`.
"""
if reqhash and not (isinstance(inp, str) and inp.startswith("#")):
return None
if isinstance(inp, str):
inp = inp.lstrip("#")
try:
if int(inp) < 0:
return None
except Exception:
return None
return inp
def identify_object(inp):
"""
Helper function. Identifies if an object is an account or an object;

View file

@ -8,26 +8,39 @@ insert custom markers in their text to indicate gender-aware
messaging. It relies on a modified msg() and is meant as an
inspiration and starting point to how to do stuff like this.
When in use, all messages being sent to the character will make use of
the character's gender, for example the echo
An object can have the following genders:
- male (he/his)
- female (her/hers)
- neutral (it/its)
- ambiguous (they/them/their/theirs)
When in use, messages can contain special tags to indicate pronouns gendered
based on the one being addressed. Capitalization will be retained.
- `|s`, `|S`: Subjective form: he, she, it, He, She, It, They
- `|o`, `|O`: Objective form: him, her, it, Him, Her, It, Them
- `|p`, `|P`: Possessive form: his, her, its, His, Her, Its, Their
- `|a`, `|A`: Absolute Possessive form: his, hers, its, His, Hers, Its, Theirs
For example,
```
char.msg("%s falls on |p face with a thud." % char.key)
"Tom falls on his face with a thud"
```
will result in "Tom falls on his|her|its|their face with a thud"
depending on the gender of the object being messaged. Default gender
is "ambiguous" (they).
The default gender is "ambiguous" (they/them/their/theirs).
To use, have DefaultCharacter inherit from this, or change
setting.DEFAULT_CHARACTER to point to this class.
The `@gender` command needs to be added to the default cmdset before
it becomes available.
The `@gender` command is used to set the gender. It needs to be added to the
default cmdset before it becomes available.
"""
import re
from evennia.utils import logger
from evennia import DefaultCharacter
from evennia import Command
@ -114,7 +127,10 @@ class GenderCharacter(DefaultCharacter):
gender-aware markers in output.
Args:
text (str, optional): The message to send
text (str or tuple, optional): The message to send. This
is treated internally like any send-command, so its
value can be a tuple if sending multiple arguments to
the `text` oob command.
from_obj (obj, optional): object that is sending. If
given, at_msg_send will be called
session (Session or list, optional): session or list of
@ -125,9 +141,13 @@ class GenderCharacter(DefaultCharacter):
All extra kwargs will be passed on to the protocol.
"""
# pre-process the text before continuing
try:
text = _RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
if text and isinstance(text, tuple):
text = (self._RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
else:
text = self._RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
except TypeError:
pass
except Exception as e:
logger.log_trace(e)
super().msg(text, from_obj=from_obj, session=session, **kwargs)

View file

@ -798,6 +798,16 @@ class RecogHandler(object):
# recog_mask log not passed, disable recog
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
def all(self):
"""
Get a mapping of the recogs stored in handler.
Returns:
recogs (dict): A mapping of {recog: obj} stored in handler.
"""
return {self.obj2recog[obj]: obj for obj in self.obj2recog.keys()}
def remove(self, obj):
"""
Clear recog for a given object.
@ -896,10 +906,9 @@ class CmdSay(RPCommand): # replaces standard say
caller.msg("Say what?")
return
# calling the speech hook on the location
speech = caller.location.at_before_say(self.args)
# calling the speech modifying hook
speech = caller.at_before_say(self.args)
# preparing the speech with sdesc/speech parsing.
speech = '/me says, "{speech}"'.format(speech=speech)
targets = self.caller.location.contents
send_emote(self.caller, targets, speech, anonymous_add=None)
@ -932,6 +941,9 @@ class CmdSdesc(RPCommand): # set/look at own sdesc
except SdescError as err:
caller.msg(err)
return
except AttributeError:
caller.msg(f"Cannot set sdesc on {caller.key}.")
return
caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc))
@ -1041,6 +1053,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
Recognize another person in the same room.
Usage:
recog
recog sdesc as alias
forget alias
@ -1048,8 +1061,8 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
recog tall man as Griatch
forget griatch
This will assign a personal alias for a person, or
forget said alias.
This will assign a personal alias for a person, or forget said alias.
Using the command without arguments will list all current recogs.
"""
@ -1058,6 +1071,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
def parse(self):
"Parse for the sdesc as alias structure"
self.sdesc, self.alias = "", ""
if " as " in self.args:
self.sdesc, self.alias = [part.strip() for part in self.args.split(" as ", 2)]
elif self.args:
@ -1070,22 +1084,47 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
def func(self):
"Assign the recog"
caller = self.caller
if not self.args:
caller.msg("Usage: recog <sdesc> as <alias> or forget <alias>")
return
sdesc = self.sdesc
alias = self.alias.rstrip(".?!")
sdesc = self.sdesc
recog_mode = self.cmdstring != "forget" and alias and sdesc
forget_mode = self.cmdstring == "forget" and sdesc
list_mode = not self.args
if not (recog_mode or forget_mode or list_mode):
caller.msg("Usage: recog, recog <sdesc> as <alias> or forget <alias>")
return
if list_mode:
# list all previously set recogs
all_recogs = caller.recog.all()
if not all_recogs:
caller.msg(
"You recognize no-one. " "(Use 'recog <sdesc> as <alias>' to recognize people."
)
else:
# note that we don't skip those failing enable_recog lock here,
# because that would actually reveal more than we want.
lst = "\n".join(
" {} ({})".format(key, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
for key, obj in all_recogs.items()
)
caller.msg(
f"Currently recognized (use 'recog <sdesc> as <alias>' to add "
f"new and 'forget <alias>' to remove):\n{lst}"
)
return
prefixed_sdesc = sdesc if sdesc.startswith(_PREFIX) else _PREFIX + sdesc
candidates = caller.location.contents
matches = parse_sdescs_and_recogs(caller, candidates, prefixed_sdesc, search_mode=True)
nmatches = len(matches)
# handle 0, 1 and >1 matches
# handle 0 and >1 matches
if nmatches == 0:
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
elif nmatches > 1:
reflist = [
"%s%s%s (%s%s)"
% (
"{}{}{} ({}{})".format(
inum + 1,
_NUM_SEP,
_RE_PREFIX.sub("", sdesc),
@ -1095,17 +1134,20 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
for inum, obj in enumerate(matches)
]
caller.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=sdesc, reflist="\n ".join(reflist)))
else:
# one single match
obj = matches[0]
if not obj.access(self.obj, "enable_recog", default=True):
# don't apply recog if object doesn't allow it (e.g. by being masked).
caller.msg("Can't recognize someone who is masked.")
caller.msg("It's impossible to recognize them.")
return
if self.cmdstring == "forget":
if forget_mode:
# remove existing recog
caller.recog.remove(obj)
caller.msg("%s will now know only '%s'." % (caller.key, obj.recog.get(obj)))
caller.msg("%s will now know them only as '%s'." % (caller.key, obj.recog.get(obj)))
else:
# set recog
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
try:
alias = caller.recog.add(obj, alias)
@ -1509,6 +1551,20 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
# initializing sdesc
self.sdesc.add("A normal person")
def at_before_say(self, message, **kwargs):
"""
Called before the object says or whispers anything, return modified message.
Args:
message (str): The suggested say/whisper text spoken by self.
Kwargs:
whisper (bool): If True, this is a whisper rather than a say.
"""
if kwargs.get("whisper"):
return f'/me whispers "{message}"'
return f'/me says, "{message}"'
def process_sdesc(self, sdesc, obj, **kwargs):
"""
Allows to customize how your sdesc is displayed (primarily by

View file

@ -169,6 +169,8 @@ class TestRPSystem(EvenniaTest):
self.speaker.recog.remove(self.receiver1)
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
def test_parse_language(self):
self.assertEqual(
rpsystem.parse_language(self.speaker, emote),
@ -233,6 +235,49 @@ class TestRPSystem(EvenniaTest):
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
class TestRPSystemCommands(CommandTest):
def setUp(self):
super().setUp()
self.char1.swap_typeclass(rpsystem.ContribRPCharacter)
self.char2.swap_typeclass(rpsystem.ContribRPCharacter)
def test_commands(self):
self.call(
rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'."
)
self.call(
rpsystem.CmdSdesc(),
"BarFoo Character",
"Char2's sdesc was set to 'BarFoo Character'.",
caller=self.char2,
)
self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"')
self.call(rpsystem.CmdEmote(), "/me smiles to /barfoo.", "Char smiles to BarFoo Character")
self.call(
rpsystem.CmdPose(),
"stands by the bar",
"Pose will read 'Foobar Character stands by the bar.'.",
)
self.call(
rpsystem.CmdRecog(),
"barfoo as friend",
"Char will now remember BarFoo Character as friend.",
)
self.call(
rpsystem.CmdRecog(),
"",
"Currently recognized (use 'recog <sdesc> as <alias>' to add new "
"and 'forget <alias>' to remove):\n friend (BarFoo Character)",
)
self.call(
rpsystem.CmdRecog(),
"friend",
"Char will now know them only as 'BarFoo Character'",
cmdstring="forget",
)
# Testing of ExtendedRoom contrib
from django.conf import settings

View file

@ -26,3 +26,16 @@ def at_webserver_root_creation(web_root):
"""
return web_root
def at_webproxy_root_creation(web_root):
"""
This function can modify the portal proxy service.
Args:
web_root (evennia.server.webserver.Website): The Evennia
Website application. Use .putChild() to add new
subdomains that are Portal-accessible over TCP;
primarily for new protocol development, but suitable
for other shenanigans.
"""
return web_root

View file

@ -547,11 +547,39 @@ def inside(accessing_obj, accessed_obj, *args, **kwargs):
Usage:
inside()
Only true if accessing_obj is "inside" accessed_obj
True if accessing_obj is 'inside' accessing_obj. Note that this only checks
one level down. So if if the lock is on a room, you will pass but not your
inventory (since their location is you, not the locked object). If you
want also nested objects to pass the lock, use the `insiderecursive`
lockfunc.
"""
return accessing_obj.location == accessed_obj
def inside_rec(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:
inside_rec()
True if accessing_obj is inside the accessed obj, at up to 10 levels
of recursion (so if this lock is on a room, then an object inside a box
in your inventory will also pass the lock).
"""
def _recursive_inside(obj, accessed_obj, lvl=1):
if obj.location:
if obj.location == accessed_obj:
return True
elif lvl >= 10:
# avoid infinite recursions
return False
else:
return _recursive_inside(obj.location, accessed_obj, lvl + 1)
return False
return _recursive_inside(accessing_obj, accessed_obj)
def holds(accessing_obj, accessed_obj, *args, **kwargs):
"""
Usage:

View file

@ -107,7 +107,7 @@ to any other identifier you can use.
import re
from django.conf import settings
from evennia.utils import logger, utils
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
__all__ = ("LockHandler", "LockException")

View file

@ -17,6 +17,7 @@ except ImportError:
from evennia import settings_default
from evennia.locks import lockfuncs
from evennia.utils.create import create_object
# ------------------------------------------------------------
# Lock testing
@ -179,6 +180,13 @@ class TestLockfuncs(EvenniaTest):
self.assertEqual(False, lockfuncs.inside(self.char1, self.room2))
self.assertEqual(True, lockfuncs.holds(self.room1, self.char1))
self.assertEqual(False, lockfuncs.holds(self.room2, self.char1))
# test recursively
self.assertEqual(True, lockfuncs.inside_rec(self.char1, self.room1))
self.assertEqual(False, lockfuncs.inside_rec(self.char1, self.room2))
inventory_item = create_object(key="InsideTester", location=self.char1)
self.assertEqual(True, lockfuncs.inside_rec(inventory_item, self.room1))
self.assertEqual(False, lockfuncs.inside_rec(inventory_item, self.room2))
inventory_item.delete()
def test_has_account(self):
self.assertEqual(True, lockfuncs.has_account(self.char1, None))

View file

@ -31,7 +31,7 @@ from evennia.utils.utils import (
list_to_string,
to_str,
)
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
_INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE

View file

@ -9,7 +9,7 @@ from evennia.scripts.models import ScriptDB
from evennia.utils import create
from evennia.utils import logger
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
class ScriptHandler(object):
@ -78,11 +78,20 @@ class ScriptHandler(object):
scriptclass, key=key, account=self.obj, autostart=autostart
)
else:
# the normal - adding to an Object
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart)
# the normal - adding to an Object. We wait to autostart so we can differentiate
# a failing creation from a script that immediately starts/stops.
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
if not script:
logger.log_err("Script %s could not be created and/or started." % scriptclass)
logger.log_err("Script %s failed to be created/started." % scriptclass)
return False
if autostart:
script.start()
if not script.id:
# this can happen if the script has repeats=1 or calls stop() in at_repeat.
logger.log_info(
"Script %s started and then immediately stopped; "
"it could probably be a normal function." % scriptclass
)
return True
def start(self, key):

View file

@ -8,7 +8,7 @@ ability to run timers.
from twisted.internet.defer import Deferred, maybeDeferred
from twisted.internet.task import LoopingCall
from django.core.exceptions import ObjectDoesNotExist
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from evennia.typeclasses.models import TypeclassBase
from evennia.scripts.models import ScriptDB
from evennia.scripts.manager import ScriptManager
@ -209,6 +209,9 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
Step task runner. No try..except needed due to defer wrap.
"""
if not self.ndb._task:
# if there is no task, we have no business using this method
return
if not self.is_valid():
self.stop()
@ -218,10 +221,13 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
self.at_repeat()
# check repeats
callcount = self.ndb._task.callcount
maxcount = self.db_repeats
if maxcount > 0 and maxcount <= callcount:
self.stop()
if self.ndb._task:
# we need to check for the task in case stop() was called
# inside at_repeat() and it already went away.
callcount = self.ndb._task.callcount
maxcount = self.db_repeats
if maxcount > 0 and maxcount <= callcount:
self.stop()
def _step_task(self):
"""
@ -339,9 +345,9 @@ class DefaultScript(ScriptBase):
try:
obj = create.create_script(**kwargs)
except Exception as e:
except Exception:
logger.log_trace()
errors.append("The script '%s' encountered errors and could not be created." % key)
logger.log_err(e)
return obj, errors

View file

@ -115,3 +115,10 @@ def check_warnings(settings):
print(" [Devel: settings.IN_GAME_ERRORS is True. Turn off in production.]")
if settings.ALLOWED_HOSTS == ["*"]:
print(" [Devel: settings.ALLOWED_HOSTS set to '*' (all). Limit in production.]")
for dbentry in settings.DATABASES.values():
if "psycopg" in dbentry.get("ENGINE", ""):
print(
'Deprecation: postgresql_psycopg2 backend is deprecated". '
"Switch settings.DATABASES to use "
'"ENGINE": "django.db.backends.postgresql instead"'
)

View file

@ -94,7 +94,7 @@ SRESET = chr(19) # shutdown server in reset mode
PYTHON_MIN = "3.7"
TWISTED_MIN = "18.0.0"
DJANGO_MIN = "2.1"
DJANGO_REC = "2.2.9"
DJANGO_REC = "2.2"
try:
sys.path[1] = EVENNIA_ROOT
@ -1281,7 +1281,7 @@ def check_main_evennia_dependencies():
try:
dversion = ".".join(str(num) for num in django.VERSION if isinstance(num, int))
# only the main version (1.5, not 1.5.4.0)
dversion_main = ".".join(dversion.split(".")[:3])
dversion_main = ".".join(dversion.split(".")[:2])
if LooseVersion(dversion) < LooseVersion(DJANGO_MIN):
print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN))
error = True

View file

@ -1,8 +1,8 @@
# Evennia Game Index Client
Greg Taylor 2016
Greg Taylor 2016, Griatch 2020
This contrib features a client for the [Evennia Game Index]
This is a client for the [Evennia Game Index]
(http://evennia-game-index.appspot.com/), a listing of games built on
Evennia. By listing your game on the index, you make it easy for other
people in the community to discover your creation.
@ -14,74 +14,24 @@ on remedying this.*
## Listing your Game
To list your game, you'll need to enable the Evennia Game Index client.
Start by `cd`'ing to your game directory. From there, open up
`server/conf/server_services_plugins.py`. It might look something like this
if you don't have any other optional add-ons enabled:
To list your game, go to your game dir and run
```python
"""
Server plugin services
evennia connections
This plugin module can define user-created services for the Server to
start.
Follow the prompts to add details to the listing. Use `evennia reload`. In your log (visible with `evennia --log`
you should see a note that info has been sent to the game index.
This module must handle all imports and setups required to start a
twisted service (see examples in evennia.server.server). It must also
contain a function start_plugin_services(application). Evennia will
call this function with the main Server application (so your services
can be added to it). The function should not return anything. Plugin
services are started last in the Server startup process.
"""
## Detailed settings
def start_plugin_services(server):
"""
This hook is called by Evennia, last in the Server startup process.
server - a reference to the main server application.
"""
pass
```
To enable the client, import `EvenniaGameIndexService` and fire it up after the
Evennia server has finished starting:
```python
"""
Server plugin services
This plugin module can define user-created services for the Server to
start.
This module must handle all imports and setups required to start a
twisted service (see examples in evennia.server.server). It must also
contain a function start_plugin_services(application). Evennia will
call this function with the main Server application (so your services
can be added to it). The function should not return anything. Plugin
services are started last in the Server startup process.
"""
from evennia.contrib.egi_client import EvenniaGameIndexService
def start_plugin_services(server):
"""
This hook is called by Evennia, last in the Server startup process.
server - a reference to the main server application.
"""
egi_service = EvenniaGameIndexService()
server.services.addService(egi_service)
```
Next, configure your game listing by opening up `server/conf/settings.py` and
If you don't want to use the wizard you can configure your game listing by opening up `server/conf/settings.py` and
using the following as a starting point:
```python
######################################################################
# Contrib config
# Game index
######################################################################
GAME_INDEX_ENABLED = True
GAME_INDEX_LISTING = {
'game_status': 'pre-alpha',
# Optional, comment out or remove if N/A

View file

@ -9,7 +9,7 @@ Everything starts at handle_setup()
import time
from django.conf import settings
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from evennia.accounts.models import AccountDB
from evennia.server.models import ServerConfig
from evennia.utils import create, logger

View file

@ -576,8 +576,7 @@ def msdp_list(session, *args, **kwargs):
fieldnames = [tup[1] for tup in monitor_infos]
session.msg(reported_variables=(fieldnames, {}))
if "sendable_variables" in args_lower:
# no default sendable variables
session.msg(sendable_variables=([], {}))
session.msg(sendable_variables=(_monitorable, {}))
def msdp_report(session, *args, **kwargs):
@ -597,6 +596,17 @@ def msdp_unreport(session, *args, **kwargs):
unmonitor(session, *args, **kwargs)
def msdp_send(session, *args, **kwargs):
"""
MSDP SEND command
"""
out = {}
for varname in args:
if varname.lower() in _monitorable:
out[varname] = _monitorable[varname.lower()]
session.msg(send=((), out))
# client specific

View file

@ -314,7 +314,9 @@ class AMPMultiConnectionProtocol(amp.AMP):
try:
super(AMPMultiConnectionProtocol, self).dataReceived(data)
except KeyError:
_get_logger().log_trace("Discarded incoming partial data: {}".format(to_str(data)))
_get_logger().log_trace(
"Discarded incoming partial (packed) data (len {})".format(len(data))
)
elif self.multibatches:
# invalid AMP, but we have a pending multi-batch that is not yet complete
if data[-2:] == NULNUL:
@ -323,7 +325,9 @@ class AMPMultiConnectionProtocol(amp.AMP):
try:
super(AMPMultiConnectionProtocol, self).dataReceived(data)
except KeyError:
_get_logger().log_trace("Discarded incoming multi-batch data:".format(to_str(data)))
_get_logger().log_trace(
"Discarded incoming multi-batch (packed) data (len {})".format(len(data))
)
else:
# not an AMP communication, return warning
self.transport.write(_HTTP_WARNING)

View file

@ -15,9 +15,10 @@ This protocol is implemented by the telnet protocol importing
mccp_compress and calling it from its write methods.
"""
import zlib
from twisted.python.compat import _bytesChr as chr
# negotiations for v1 and v2 of the protocol
MCCP = b"\x56"
MCCP = chr(86) # b"\x56"
FLUSH = zlib.Z_SYNC_FLUSH

View file

@ -12,10 +12,11 @@ active players and so on.
"""
from django.conf import settings
from evennia.utils import utils
from twisted.python.compat import _bytesChr as bchr
MSSP = b"\x46"
MSSP_VAR = b"\x01"
MSSP_VAL = b"\x02"
MSSP = bchr(70) # b"\x46"
MSSP_VAR = bchr(1) # b"\x01"
MSSP_VAL = bchr(2) # b"\x02"
# try to get the customized mssp info, if it exists.
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})
@ -86,7 +87,7 @@ class Mssp(object):
"PLAYERS": self.get_player_count,
"UPTIME": self.get_uptime,
"PORT": list(
reversed(settings.TELNET_PORTS)
str(port) for port in reversed(settings.TELNET_PORTS)
), # most important port should be last in list
# Evennia auto-filled
"CRAWL DELAY": "-1",
@ -119,10 +120,15 @@ class Mssp(object):
if utils.is_iter(value):
for partval in value:
varlist += (
MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(partval, "utf-8")
MSSP_VAR
+ bytes(str(variable), "utf-8")
+ MSSP_VAL
+ bytes(str(partval), "utf-8")
)
else:
varlist += MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(value, "utf-8")
varlist += (
MSSP_VAR + bytes(str(variable), "utf-8") + MSSP_VAL + bytes(str(value), "utf-8")
)
# send to crawler by subnegotiation
self.protocol.requestNegotiation(MSSP, varlist)

View file

@ -14,11 +14,12 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
"""
import re
from twisted.python.compat import _bytesChr as bchr
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
# MXP Telnet option
MXP = b"\x5b"
MXP = bchr(91) # b"\x5b"
MXP_TEMPSECURE = "\x1B[4z"
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"

View file

@ -11,9 +11,10 @@ client and update it when the size changes
"""
from codecs import encode as codecs_encode
from django.conf import settings
from twisted.python.compat import _bytesChr as bchr
NAWS = b"\x1f"
IS = b"\x00"
NAWS = bchr(31) # b"\x1f"
IS = bchr(0) # b"\x00"
# default taken from telnet specification
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT

View file

@ -95,6 +95,16 @@ INFO_DICT = {
"webserver_internal": [],
}
try:
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
except ImportError:
WEB_PLUGINS_MODULE = None
INFO_DICT["errors"] = (
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
)
# -------------------------------------------------------------
# Portal Service object
# -------------------------------------------------------------
@ -190,7 +200,6 @@ class Portal(object):
self.sessions.disconnect_all()
if _stop_server:
self.amp_protocol.stop_server(mode="shutdown")
if not _reactor_stopping:
# shutting down the reactor will trigger another signal. We set
# a flag to avoid loops.
@ -379,6 +388,14 @@ if WEBSERVER_ENABLED:
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
INFO_DICT["webclient"].append(webclientstr)
if WEB_PLUGINS_MODULE:
try:
web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
except Exception as e: # Legacy user has not added an at_webproxy_root_creation function in existing web plugins file
INFO_DICT["errors"] = (
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() not found - "
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
)
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
web_root.is_portal = True
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)

View file

@ -13,7 +13,9 @@ It is set as the NOGOAHEAD protocol_flag option.
http://www.faqs.org/rfcs/rfc858.html
"""
SUPPRESS_GA = b"\x03"
from twisted.python.compat import _bytesChr as bchr
SUPPRESS_GA = bchr(3) # b"\x03"
# default taken from telnet specification

View file

@ -75,13 +75,21 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.protocol_key = "telnet"
super().__init__(*args, **kwargs)
def dataReceived(self, data):
"""
Unused by default, but a good place to put debug printouts
of incoming data.
"""
# print(f"telnet dataReceived: {data}")
super().dataReceived(data)
def connectionMade(self):
"""
This is called when the connection is first established.
"""
# important in order to work normally with standard telnet
self.do(LINEMODE)
self.do(LINEMODE).addErrback(self._wont_linemode)
# initialize the session
self.line_buffer = b""
client_address = self.transport.client
@ -126,6 +134,14 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
self.nop_keep_alive = None
self.toggle_nop_keepalive()
def _wont_linemode(self, *args):
"""
Client refuses do(linemode). This is common for MUD-specific
clients, but we must ask for the sake of raw telnet. We ignore
this error.
"""
pass
def _send_nop_keepalive(self):
"""Send NOP keepalive unless flag is set"""
if self.protocol_flags.get("NOPKEEPALIVE"):
@ -193,6 +209,16 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
or option == suppress_ga.SUPPRESS_GA
)
def disableRemote(self, option):
return (
option == LINEMODE
or option == ttype.TTYPE
or option == naws.NAWS
or option == MCCP
or option == mssp.MSSP
or option == suppress_ga.SUPPRESS_GA
)
def enableLocal(self, option):
"""
Call to allow the activation of options for this protocol
@ -219,13 +245,20 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
option (char): The telnet option to disable locally.
"""
if option == LINEMODE:
return True
if option == ECHO:
return True
if option == MCCP:
self.mccp.no_mccp(option)
return True
else:
return super().disableLocal(option)
try:
return super().disableLocal(option)
except Exception:
from evennia.utils import logger
logger.log_trace()
def connectionLost(self, reason):
"""

View file

@ -28,22 +28,24 @@ header where applicable.
import re
import json
from evennia.utils.utils import is_iter
# MSDP-relevant telnet cmd/opt-codes
MSDP = b"\x45"
MSDP_VAR = b"\x01" # ^A
MSDP_VAL = b"\x02" # ^B
MSDP_TABLE_OPEN = b"\x03" # ^C
MSDP_TABLE_CLOSE = b"\x04" # ^D
MSDP_ARRAY_OPEN = b"\x05" # ^E
MSDP_ARRAY_CLOSE = b"\x06" # ^F
# GMCP
GMCP = b"\xc9"
from twisted.python.compat import _bytesChr as bchr
# General Telnet
from twisted.conch.telnet import IAC, SB, SE
# MSDP-relevant telnet cmd/opt-codes
MSDP = bchr(69)
MSDP_VAR = bchr(1)
MSDP_VAL = bchr(2)
MSDP_TABLE_OPEN = bchr(3)
MSDP_TABLE_CLOSE = bchr(4)
MSDP_ARRAY_OPEN = bchr(5)
MSDP_ARRAY_CLOSE = bchr(6)
# GMCP
GMCP = bchr(201)
# pre-compiled regexes
# returns 2-tuple
@ -168,7 +170,7 @@ class TelnetOOB(object):
"""
msdp_cmdname = "{msdp_var}{msdp_cmdname}{msdp_val}".format(
msdp_var=MSDP_VAR, msdp_cmdname=cmdname, msdp_val=MSDP_VAL
msdp_var=MSDP_VAR.decode(), msdp_cmdname=cmdname, msdp_val=MSDP_VAL.decode()
)
if not (args or kwargs):
@ -186,9 +188,9 @@ class TelnetOOB(object):
"{msdp_array_open}"
"{msdp_args}"
"{msdp_array_close}".format(
msdp_array_open=MSDP_ARRAY_OPEN,
msdp_array_close=MSDP_ARRAY_CLOSE,
msdp_args="".join("%s%s" % (MSDP_VAL, json.dumps(val)) for val in args),
msdp_array_open=MSDP_ARRAY_OPEN.decode(),
msdp_array_close=MSDP_ARRAY_CLOSE.decode(),
msdp_args="".join("%s%s" % (MSDP_VAL.decode(), val) for val in args),
)
)
@ -199,10 +201,10 @@ class TelnetOOB(object):
"{msdp_table_open}"
"{msdp_kwargs}"
"{msdp_table_close}".format(
msdp_table_open=MSDP_TABLE_OPEN,
msdp_table_close=MSDP_TABLE_CLOSE,
msdp_table_open=MSDP_TABLE_OPEN.decode(),
msdp_table_close=MSDP_TABLE_CLOSE.decode(),
msdp_kwargs="".join(
"%s%s%s%s" % (MSDP_VAR, key, MSDP_VAL, json.dumps(val))
"%s%s%s%s" % (MSDP_VAR.decode(), key, MSDP_VAL.decode(), val)
for key, val in kwargs.items()
),
)

View file

@ -100,11 +100,11 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile):
keypair.generate_key(crypto.TYPE_RSA, _PRIVATE_KEY_LENGTH)
with open(_PRIVATE_KEY_FILE, "wt") as pfile:
pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair))
pfile.write(crypto.dump_privatekey(crypto.FILETYPE_PEM, keypair).decode("utf-8"))
print("Created SSL private key in '{}'.".format(_PRIVATE_KEY_FILE))
with open(_PUBLIC_KEY_FILE, "wt") as pfile:
pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair))
pfile.write(crypto.dump_publickey(crypto.FILETYPE_PEM, keypair).decode("utf-8"))
print("Created SSL public key in '{}'.".format(_PUBLIC_KEY_FILE))
except Exception as err:
@ -128,7 +128,7 @@ def verify_or_create_SSL_key_and_cert(keyfile, certfile):
cert.sign(keypair, "sha1")
with open(_CERTIFICATE_FILE, "wt") as cfile:
cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert))
cfile.write(crypto.dump_certificate(crypto.FILETYPE_PEM, cert).decode("utf-8"))
print("Created SSL certificate in '{}'.".format(_CERTIFICATE_FILE))
except Exception as err:

View file

@ -10,11 +10,12 @@ etc. If the client does not support TTYPE, this will be ignored.
All data will be stored on the protocol's protocol_flags dictionary,
under the 'TTYPE' key.
"""
from twisted.python.compat import _bytesChr as bchr
# telnet option codes
TTYPE = b"\x18"
IS = b"\x00"
SEND = b"\x01"
TTYPE = bchr(24) # b"\x18"
IS = bchr(0) # b"\x00"
SEND = bchr(1) # b"\x01"
# terminal capabilities and their codes
MTTS = [

View file

@ -38,7 +38,7 @@ from evennia.utils import logger
from evennia.comms import channelhandler
from evennia.server.sessionhandler import SESSIONS
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
_SA = object.__setattr__

View file

@ -23,7 +23,7 @@ _ObjectDB = None
_ANSI = None
# i18n
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
# Handlers for Session.db/ndb operation

View file

@ -67,7 +67,7 @@ PSTATUS = chr(18) # ping server or portal status
SRESET = chr(19) # server shutdown in reset mode
# i18n
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
_SERVERNAME = settings.SERVERNAME
_MULTISESSION_MODE = settings.MULTISESSION_MODE

View file

@ -27,7 +27,7 @@ def _drop_table(db_cursor, table_name):
db_cursor.execute("SET FOREIGN_KEY_CHECKS=0;")
db_cursor.execute("DROP TABLE {table};".format(table=table_name))
db_cursor.execute("SET FOREIGN_KEY_CHECKS=1;")
elif _ENGINE == "postgresql_psycopg2":
elif _ENGINE == "postgresql":
db_cursor.execute("ALTER TABLE {table} DISABLE TRIGGER ALL;".format(table=table_name))
db_cursor.execute("DROP TABLE {table};".format(table=table_name))
db_cursor.execute("ALTER TABLE {table} ENABLE TRIGGER ALL;".format(table=table_name))

View file

@ -674,6 +674,13 @@ class ANSIString(str, metaclass=ANSIMeta):
"""
# A compiled Regex for the format mini-language: https://docs.python.org/3/library/string.html#formatspec
re_format = re.compile(
r"(?i)(?P<just>(?P<fill>.)?(?P<align>\<|\>|\=|\^))?(?P<sign>\+|\-| )?(?P<alt>\#)?"
r"(?P<zero>0)?(?P<width>\d+)?(?P<grouping>\_|\,)?(?:\.(?P<precision>\d+))?"
r"(?P<type>b|c|d|e|E|f|F|g|G|n|o|s|x|X|%)?"
)
def __new__(cls, *args, **kwargs):
"""
When creating a new ANSIString, you may use a custom parser that has
@ -733,6 +740,47 @@ class ANSIString(str, metaclass=ANSIMeta):
def __str__(self):
return self._raw_string
def __format__(self, format_spec):
"""
This magic method covers ANSIString's behavior within a str.format() or f-string.
Current features supported: fill, align, width.
Args:
format_spec (str): The format specification passed by f-string or str.format(). This is a string such as
"0<30" which would mean "left justify to 30, filling with zeros". The full specification can be found
at https://docs.python.org/3/library/string.html#formatspec
Returns:
ansi_str (str): The formatted ANSIString's .raw() form, for display.
"""
# This calls the compiled regex stored on ANSIString's class to analyze the format spec.
# It returns a dictionary.
format_data = self.re_format.match(format_spec).groupdict()
clean = self.clean()
base_output = ANSIString(self.raw())
align = format_data.get("align", "<")
fill = format_data.get("fill", " ")
# Need to coerce width into an integer. We can be certain that it's numeric thanks to regex.
width = format_data.get("width", None)
if width is None:
width = len(clean)
else:
width = int(width)
if align == "<":
base_output = self.ljust(width, fill)
elif align == ">":
base_output = self.rjust(width, fill)
elif align == "^":
base_output = self.center(width, fill)
elif align == "=":
pass
# Return the raw string with ANSI markup, ready to be displayed.
return base_output.raw()
def __repr__(self):
"""
Let's make the repr the command that would actually be used to

View file

@ -28,7 +28,7 @@ except ImportError:
from pickle import dumps, loads
from django.core.exceptions import ObjectDoesNotExist
from django.contrib.contenttypes.models import ContentType
from django.utils.safestring import SafeString, SafeBytes
from django.utils.safestring import SafeString
from evennia.utils.utils import uses_database, is_iter, to_str, to_bytes
from evennia.utils import logger
@ -549,7 +549,7 @@ def to_pickle(data):
def process_item(item):
"""Recursive processor and identification of data"""
dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes):
if dtype in (str, int, float, bool, bytes, SafeString):
return item
elif dtype == tuple:
return tuple(process_item(val) for val in item)
@ -577,7 +577,7 @@ def to_pickle(data):
except TypeError:
return item
except Exception:
logger.log_error(f"The object {item} of type {type(item)} could not be stored.")
logger.log_err(f"The object {item} of type {type(item)} could not be stored.")
raise
return process_item(data)
@ -609,7 +609,7 @@ def from_pickle(data, db_obj=None):
def process_item(item):
"""Recursive processor and identification of data"""
dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes):
if dtype in (str, int, float, bool, bytes, SafeString):
return item
elif _IS_PACKED_DBOBJ(item):
# this must be checked before tuple
@ -638,7 +638,7 @@ def from_pickle(data, db_obj=None):
def process_tree(item, parent):
"""Recursive processor, building a parent-tree from iterable data"""
dtype = type(item)
if dtype in (str, int, float, bool, bytes, SafeString, SafeBytes):
if dtype in (str, int, float, bool, bytes, SafeString):
return item
elif _IS_PACKED_DBOBJ(item):
# this must be checked before tuple
@ -716,7 +716,7 @@ def do_pickle(data):
try:
return dumps(data, protocol=PICKLE_PROTOCOL)
except Exception:
logger.log_error(f"Could not pickle data for storage: {data}")
logger.log_err(f"Could not pickle data for storage: {data}")
raise
@ -725,7 +725,7 @@ def do_unpickle(data):
try:
return loads(to_bytes(data))
except Exception:
logger.log_error(f"Could not unpickle data from storage: {data}")
logger.log_err(f"Could not unpickle data from storage: {data}")
raise

View file

@ -187,7 +187,7 @@ _CMD_NOINPUT = cmdhandler.CMD_NOINPUT
# Return messages
# i18n
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
_ERR_NOT_IMPLEMENTED = _(
"Menu node '{nodename}' is either not implemented or " "caused an error. Make another choice."

View file

@ -43,7 +43,7 @@ from django.forms.fields import CharField
from django.forms.widgets import Textarea
from pickle import loads, dumps
from django.utils.encoding import force_text
from django.utils.encoding import force_str
DEFAULT_PROTOCOL = 4
@ -210,10 +210,10 @@ class PickledObjectField(models.Field):
"""
Returns the default value for this field.
The default implementation on models.Field calls force_text
The default implementation on models.Field calls force_str
on the default, which means you can't set arbitrary Python
objects as the default. To fix this, we just return the value
without calling force_text on it. Note that if you set a
without calling force_str on it. Note that if you set a
callable as a default, the field will still call it. It will
*not* try to pickle and encode it.
@ -267,13 +267,13 @@ class PickledObjectField(models.Field):
"""
if value is not None and not isinstance(value, PickledObject):
# We call force_text here explicitly, so that the encoded string
# isn't rejected by the postgresql_psycopg2 backend. Alternatively,
# We call force_str here explicitly, so that the encoded string
# isn't rejected by the postgresql backend. Alternatively,
# we could have just registered PickledObject with the psycopg
# marshaller (telling it to store it like it would a string), but
# since both of these methods result in the same value being stored,
# doing things this way is much easier.
value = force_text(dbsafe_encode(value, self.compress, self.protocol))
value = force_str(dbsafe_encode(value, self.compress, self.protocol))
return value
def value_to_string(self, obj):

View file

@ -98,6 +98,30 @@ class TestMLen(TestCase):
self.assertEqual(utils.m_len({"hello": True, "Goodbye": False}), 2)
class TestANSIString(TestCase):
"""
Verifies that ANSIString's string-API works as intended.
"""
def setUp(self):
self.example_raw = "|relectric |cboogaloo|n"
self.example_ansi = ANSIString(self.example_raw)
self.example_str = "electric boogaloo"
self.example_output = "\x1b[1m\x1b[31melectric \x1b[1m\x1b[36mboogaloo\x1b[0m"
def test_length(self):
self.assertEqual(len(self.example_ansi), 17)
def test_clean(self):
self.assertEqual(self.example_ansi.clean(), self.example_str)
def test_raw(self):
self.assertEqual(self.example_ansi.raw(), self.example_output)
def test_format(self):
self.assertEqual(f"{self.example_ansi:0<20}", self.example_output + "000")
class TestTimeformat(TestCase):
"""
Default function header from utils.py:

View file

@ -27,7 +27,7 @@ from collections import defaultdict, OrderedDict
from twisted.internet import threads, reactor
from django.conf import settings
from django.utils import timezone
from django.utils.translation import ugettext as _
from django.utils.translation import gettext as _
from django.apps import apps
from evennia.utils import logger
@ -1036,7 +1036,7 @@ def uses_database(name="sqlite3"):
shortcut to having to use the full backend name.
Args:
name (str): One of 'sqlite3', 'mysql', 'postgresql_psycopg2'
name (str): One of 'sqlite3', 'mysql', 'postgresql'
or 'oracle'.
Returns:

View file

@ -6,9 +6,9 @@
# http://diveintopython.org/regular_expressions/street_addresses.html#re.matching.2.3
#
from django.urls import path
from django.conf.urls import url, include
from django.conf.urls import url
from django.conf import settings
from django.urls import path, include
from django.views.generic import RedirectView
# Setup the root url tree from /

View file

@ -6,7 +6,7 @@ with evennia set up automatically and get the Evennia JS lib and
JQuery available.
-->
{% load staticfiles %}
{% load static %}
<html dir="ltr" lang="en">
<head>
<title> {{game_name}} </title>

View file

@ -2,8 +2,9 @@
This structures the (simple) structure of the
webpage 'application'.
"""
from django.conf.urls import *
from django.urls import path
from evennia.web.webclient import views as webclient_views
app_name = "webclient"
urlpatterns = [url(r"^$", webclient_views.webclient, name="index")]
urlpatterns = [path("", webclient_views.webclient, name="index")]

View file

@ -3,7 +3,7 @@ Allow to customize the menu that appears at the top of every Evennia
webpage. Copy this file to your game dir's web/template_overrides/website
folder and edit it to add/remove links to the menu.
{% endcomment %}
{% load staticfiles %}
{% load static %}
<nav class="navbar navbar-dark font-weight-bold navbar-expand-md">
<button class="navbar-toggler navbar-toggler-right" type="button" data-toggle="collapse" data-target="#menu-content" aria-controls="menu-content" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>

View file

@ -1,4 +1,4 @@
{% load staticfiles sekizai_tags %}
{% load static sekizai_tags %}
<!DOCTYPE html>
<html lang="en">
<head>

View file

@ -10,9 +10,12 @@ django-filter >= 2.2.0, < 2.3
django-sekizai
inflect
autobahn >= 17.9.3
model_mommy
# try to resolve dependency issue in py3.7
attrs >= 19.2.0
# testing and development
model_mommy
mock >= 1.0.1
anything
black