Run black reformatter on code

This commit is contained in:
Griatch 2022-02-08 13:03:52 +01:00
parent 4582eb4085
commit bd3e31bf3c
178 changed files with 4511 additions and 3385 deletions

View file

@ -11,15 +11,15 @@ from os import rename
def _rst2md(filename_rst):
with open(filename_rst, 'r') as fil:
with open(filename_rst, "r") as fil:
# read rst file, reformat and save
txt = fil.read()
with open(filename_rst, 'w') as fil:
with open(filename_rst, "w") as fil:
txt = "```{eval-rst}\n" + txt + "\n```"
fil.write(txt)
# rename .rst file to .md file
filename, _ = filename_rst.rsplit('.', 1)
filename, _ = filename_rst.rsplit(".", 1)
filename_md = filename + ".md"
rename(filename_rst, filename_md)

View file

@ -149,7 +149,7 @@ def auto_link_remapper(no_autodoc=False):
for strip_prefix in _STRIP_PREFIX:
if url.startswith(strip_prefix):
url = url[len(strip_prefix):]
url = url[len(strip_prefix) :]
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
# skip regular http/s urls etc
@ -157,10 +157,10 @@ def auto_link_remapper(no_autodoc=False):
if url.startswith("evennia."):
# api link - we want to remove legacy #reference and remove .md
if '#' in url:
_, url = url.rsplit('#', 1)
if "#" in url:
_, url = url.rsplit("#", 1)
if url.endswith(".md"):
url, _ = url.rsplit('.', 1)
url, _ = url.rsplit(".", 1)
return f"[{txt}]({url})"
fname, *part = url.rsplit("/", 1)
@ -174,7 +174,9 @@ def auto_link_remapper(no_autodoc=False):
if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
cfilename = _CURRFILE.rsplit("/", 1)[-1]
urlout = docref_map[_CURRFILE][fname] + ".md" + ("#" + anchor[0].lower() if anchor else "")
urlout = (
docref_map[_CURRFILE][fname] + ".md" + ("#" + anchor[0].lower() if anchor else "")
)
if urlout != url:
print(f" {cfilename}: [{txt}]({url}) -> [{txt}]({urlout})")
else:
@ -193,7 +195,7 @@ def auto_link_remapper(no_autodoc=False):
for strip_prefix in _STRIP_PREFIX:
if url.startswith(strip_prefix):
url = url[len(strip_prefix):]
url = url[len(strip_prefix) :]
if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
return f"[{txt}]: {url}"
@ -202,8 +204,8 @@ def auto_link_remapper(no_autodoc=False):
urlout = url
elif url.startswith("evennia."):
# api link - we want to remove legacy #reference
if '#' in url:
_, urlout = url.rsplit('#', 1)
if "#" in url:
_, urlout = url.rsplit("#", 1)
else:
fname, *part = url.rsplit("/", 1)
fname = part[0] if part else fname

View file

@ -50,15 +50,11 @@ tutorials are found here. Also the home of the Tutorial World demo adventure.
"utils": """
Miscellaneous, optional tools for manipulating text, auditing connections
and more.
"""
""",
}
_FILENAME_MAP = {
"rpsystem": "RPSystem",
"xyzgrid": "XYZGrid",
"awsstorage": "AWSStorage"
}
_FILENAME_MAP = {"rpsystem": "RPSystem", "xyzgrid": "XYZGrid", "awsstorage": "AWSStorage"}
HEADER = """# Contribs
@ -145,10 +141,14 @@ def readmes2docs(directory=_SOURCE_DIR):
pypath = f"evennia.contrib.{category}.{name}"
filename = "Contrib-" + "-".join(
_FILENAME_MAP.get(
part, part.capitalize() if part[0].islower() else part)
for part in name.split("_")) + ".md"
filename = (
"Contrib-"
+ "-".join(
_FILENAME_MAP.get(part, part.capitalize() if part[0].islower() else part)
for part in name.split("_")
)
+ ".md"
)
outfile = pathjoin(_OUT_DIR, filename)
with open(file_path) as fil:
@ -163,7 +163,7 @@ def readmes2docs(directory=_SOURCE_DIR):
except IndexError:
blurb = name
with open(outfile, 'w') as fil:
with open(outfile, "w") as fil:
fil.write(data)
categories[category].append((name, credits, blurb, filename, pypath))
@ -179,11 +179,7 @@ def readmes2docs(directory=_SOURCE_DIR):
for tup in sorted(contrib_tups, key=lambda tup: tup[0].lower()):
catlines.append(
BLURB.format(
name=tup[0],
credits=tup[1],
blurb=tup[2],
filename=tup[3],
code_location=tup[4]
name=tup[0], credits=tup[1], blurb=tup[2], filename=tup[3], code_location=tup[4]
)
)
filenames.append(f"{tup[3]}")
@ -193,17 +189,15 @@ def readmes2docs(directory=_SOURCE_DIR):
category=category,
category_desc=_CATEGORY_DESCS[category].strip(),
blurbs="\n".join(catlines),
toctree=toctree
toctree=toctree,
)
)
text = _FILE_STRUCTURE.format(
header=HEADER,
categories="\n".join(category_sections),
footer=INDEX_FOOTER
header=HEADER, categories="\n".join(category_sections), footer=INDEX_FOOTER
)
with open(_OUT_INDEX_FILE, 'w') as fil:
with open(_OUT_INDEX_FILE, "w") as fil:
fil.write(text)
print(f" -- Converted Contrib READMEs to {ncount} doc pages + index.")

View file

@ -26,7 +26,11 @@ if __name__ == "__main__":
filepaths = glob.glob(args.files, recursive=True)
width = args.width
wrapper = textwrap.TextWrapper(width=width, break_long_words=False, expand_tabs=True,)
wrapper = textwrap.TextWrapper(
width=width,
break_long_words=False,
expand_tabs=True,
)
count = 0
for filepath in filepaths:

View file

@ -6,11 +6,9 @@
#
from os.path import dirname, abspath, join as pathjoin
from evennia.utils.utils import (
mod_import, variable_from_module, callables_from_module
)
from evennia.utils.utils import mod_import, variable_from_module, callables_from_module
__all__ = ("run_update")
__all__ = "run_update"
PAGE = """
@ -33,6 +31,7 @@ with [EvEditor](EvEditor), flipping pages in [EvMore](EvMore) or using the
"""
def run_update(no_autodoc=False):
if no_autodoc:
@ -71,7 +70,8 @@ def run_update(no_autodoc=False):
for modname in cmd_modules:
module = mod_import(modname)
cmds_per_module[module] = [
cmd for cmd in callables_from_module(module).values() if cmd.__name__.startswith("Cmd")]
cmd for cmd in callables_from_module(module).values() if cmd.__name__.startswith("Cmd")
]
for cmd in cmds_per_module[module]:
cmd_to_module_map[cmd] = module
cmds_alphabetically.append(cmd)
@ -79,8 +79,9 @@ def run_update(no_autodoc=False):
cmd_infos = []
for cmd in cmds_alphabetically:
aliases = [alias[1:] if alias and alias[0] == "@" else alias
for alias in sorted(cmd.aliases)]
aliases = [
alias[1:] if alias and alias[0] == "@" else alias for alias in sorted(cmd.aliases)
]
aliases = f" [{', '.join(sorted(cmd.aliases))}]" if aliases else ""
cmdlink = f"[**{cmd.key}**{aliases}]({cmd.__module__}.{cmd.__name__})"
category = f"help-category: _{cmd.help_category.capitalize()}_"
@ -98,12 +99,13 @@ def run_update(no_autodoc=False):
txt = PAGE.format(
ncommands=len(cmd_to_cmdset_map),
nfiles=len(cmds_per_module),
alphabetical="\n".join(f"- {info}" for info in cmd_infos))
alphabetical="\n".join(f"- {info}" for info in cmd_infos),
)
outdir = pathjoin(dirname(dirname(abspath(__file__))), "source", "Components")
fname = pathjoin(outdir, "Default-Commands.md")
with open(fname, 'w') as fil:
with open(fname, "w") as fil:
fil.write(txt)
print(" -- Updated Default Command index.")

View file

@ -10,6 +10,7 @@ DOCDIR = pathjoin(ROOTDIR, "docs")
DOCSRCDIR = pathjoin(DOCDIR, "source")
EVENNIADIR = pathjoin(ROOTDIR, "evennia")
def update_changelog():
"""
Plain CHANGELOG copy
@ -22,7 +23,7 @@ def update_changelog():
with open(sourcefile) as fil:
txt = fil.read()
with open(targetfile, 'w') as fil:
with open(targetfile, "w") as fil:
fil.write(txt)
print(" -- Updated Changelog.md")
@ -62,7 +63,7 @@ if settings.SERVERNAME == "Evennia":
{txt}
```
"""
with open(targetfile, 'w') as fil:
with open(targetfile, "w") as fil:
fil.write(txt)
print(" -- Updated Settings-Default.md")

View file

@ -7,6 +7,7 @@
import os
import sys
import re
# from recommonmark.transform import AutoStructify
from sphinx.util.osutil import cd
@ -31,7 +32,7 @@ extensions = [
"sphinx.ext.viewcode",
"sphinx.ext.todo",
"sphinx.ext.githubpages",
"myst_parser"
"myst_parser",
]
source_suffix = [".md", ".rst"]
@ -145,9 +146,11 @@ _github_code_root = "https://github.com/evennia/evennia/blob/"
_github_doc_root = "https://github.com/evennia/tree/master/docs/sources/"
_github_issue_choose = "https://github.com/evennia/evennia/issues/new/choose"
_ref_regex = re.compile( # normal reference-links [txt](url)
r"\[(?P<txt>[\w -\[\]\`\n]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M)
r"\[(?P<txt>[\w -\[\]\`\n]+?)\]\((?P<url>.+?)\)", re.I + re.S + re.U + re.M
)
_ref_doc_regex = re.compile( # in-document bottom references [txt]: url
r"\[(?P<txt>[\w -\`]+?)\\n]:\s+?(?P<url>.+?)(?=$|\n)", re.I + re.S + re.U + re.M)
r"\[(?P<txt>[\w -\`]+?)\\n]:\s+?(?P<url>.+?)(?=$|\n)", re.I + re.S + re.U + re.M
)
def url_resolver(app, docname, source):
@ -165,10 +168,11 @@ def url_resolver(app, docname, source):
"""
def _url_remap(url):
# determine depth in tree of current document
docdepth = docname.count('/') + 1
docdepth = docname.count("/") + 1
relative_path = "../".join("" for _ in range(docdepth))
if url.endswith(_choose_issue):
@ -176,14 +180,14 @@ def url_resolver(app, docname, source):
return _github_issue_choose
elif _githubstart in url:
# github:develop/... shortcut
urlpath = url[url.index(_githubstart) + len(_githubstart):]
urlpath = url[url.index(_githubstart) + len(_githubstart) :]
if not (urlpath.startswith("develop/") or urlpath.startswith("master")):
urlpath = "master/" + urlpath
return _github_code_root + urlpath
elif _sourcestart in url:
ind = url.index(_sourcestart)
modpath, *inmodule = url[ind + len(_sourcestart):].rsplit("#", 1)
modpath, *inmodule = url[ind + len(_sourcestart) :].rsplit("#", 1)
modpath = "/".join(modpath.split("."))
inmodule = "#" + inmodule[0] if inmodule else ""
modpath = modpath + ".html" + inmodule
@ -194,13 +198,13 @@ def url_resolver(app, docname, source):
return url
def _re_ref_sub(match):
txt = match.group('txt')
url = _url_remap(match.group('url'))
txt = match.group("txt")
url = _url_remap(match.group("url"))
return f"[{txt}]({url})"
def _re_docref_sub(match):
txt = match.group('txt')
url = _url_remap(match.group('url'))
txt = match.group("txt")
url = _url_remap(match.group("url"))
return f"[{txt}]: {url}"
src = source[0]
@ -248,7 +252,7 @@ autodoc_default_options = {
"show-inheritance": True,
"special-members": "__init__",
"enable_eval_rst": True,
"inherited_members": True
"inherited_members": True,
}
autodoc_member_order = "bysource"
@ -345,8 +349,12 @@ def setup(app):
# build toctree file
sys.path.insert(1, os.path.dirname(os.path.dirname(os.path.dirname(__file__))))
from docs.pylib import (auto_link_remapper, update_default_cmd_index,
contrib_readmes2docs, update_dynamic_pages)
from docs.pylib import (
auto_link_remapper,
update_default_cmd_index,
contrib_readmes2docs,
update_dynamic_pages,
)
_no_autodoc = os.environ.get("NOAUTODOC")
update_default_cmd_index.run_update(no_autodoc=_no_autodoc)

View file

@ -100,6 +100,7 @@ MONITOR_HANDLER = None
GLOBAL_SCRIPTS = None
OPTION_CLASSES = None
def _create_version():
"""
Helper function for building the version string

View file

@ -54,11 +54,12 @@ _CMDHANDLER = None
# Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle(
name='creation', limit=settings.CREATION_THROTTLE_LIMIT,
timeout=settings.CREATION_THROTTLE_TIMEOUT
name="creation",
limit=settings.CREATION_THROTTLE_LIMIT,
timeout=settings.CREATION_THROTTLE_TIMEOUT,
)
LOGIN_THROTTLE = Throttle(
name='login', limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
name="login", limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
)
@ -802,8 +803,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
except Exception:
errors.append(
_("There was an error creating the Account. "
"If this problem persists, contact an admin."))
_(
"There was an error creating the Account. "
"If this problem persists, contact an admin."
)
)
logger.log_trace()
return None, errors
@ -879,7 +883,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
super().delete(*args, **kwargs)
return True
# methods inherited from database model
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
@ -968,9 +971,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
sessions = self.sessions.get()
session = sessions[0] if sessions else None
return _CMDHANDLER(
self, raw_string, callertype="account", session=session, **kwargs
)
return _CMDHANDLER(self, raw_string, callertype="account", session=session, **kwargs)
# channel receive hooks
@ -1000,11 +1001,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
"""
if senders:
sender_string = ', '.join(sender.get_display_name(self) for sender in senders)
sender_string = ", ".join(sender.get_display_name(self) for sender in senders)
message_lstrip = message.lstrip()
if message_lstrip.startswith((':', ';')):
if message_lstrip.startswith((":", ";")):
# this is a pose, should show as e.g. "User1 smiles to channel"
spacing = "" if message_lstrip[1:].startswith((':', '\'', ',')) else " "
spacing = "" if message_lstrip[1:].startswith((":", "'", ",")) else " "
message = f"{sender_string}{spacing}{message_lstrip[1:]}"
else:
# normal message
@ -1035,8 +1036,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
to customize the message for the receiver on the channel-level.
"""
self.msg(text=(message, {"from_channel": channel.id}),
from_obj=senders, options={"from_channel": channel.id})
self.msg(
text=(message, {"from_channel": channel.id}),
from_obj=senders,
options={"from_channel": channel.id},
)
def at_post_channel_msg(self, message, channel, senders=None, **kwargs):
"""
@ -1373,8 +1377,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if _MUDINFO_CHANNEL is None:
if settings.CHANNEL_MUDINFO:
try:
_MUDINFO_CHANNEL = ChannelDB.objects.get(
db_key=settings.CHANNEL_MUDINFO["key"])
_MUDINFO_CHANNEL = ChannelDB.objects.get(db_key=settings.CHANNEL_MUDINFO["key"])
except ChannelDB.DoesNotExist:
logger.log_trace()
else:
@ -1383,7 +1386,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if settings.CHANNEL_CONNECTINFO:
try:
_CONNECT_CHANNEL = ChannelDB.objects.get(
db_key=settings.CHANNEL_CONNECTINFO["key"])
db_key=settings.CHANNEL_CONNECTINFO["key"]
)
except ChannelDB.DoesNotExist:
logger.log_trace()
else:
@ -1661,7 +1665,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if sess and sid:
result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] "
f"(played by you in session {sid})")
f"(played by you in session {sid})"
)
else:
result.append(
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] "

View file

@ -329,9 +329,7 @@ class IRCBot(Bot):
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
for obj in self._nicklist_callers:
obj.msg(
"Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist)
)
obj.msg("Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist))
self._nicklist_callers = []
return

View file

@ -276,8 +276,11 @@ class AccountDBManager(TypedObjectManager, UserManager):
new_account.set_password(password)
new_account._createdict = dict(
locks=locks, permissions=permissions,
report_to=report_to, tags=tags, attributes=attributes
locks=locks,
permissions=permissions,
report_to=report_to,
tags=tags,
attributes=attributes,
)
# saving will trigger the signal that calls the
# at_first_save hook on the typeclass, where the _createdict

View file

@ -80,47 +80,63 @@ _SEARCH_AT_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit
# is the normal "production message to echo to the account.
_ERROR_UNTRAPPED = (
_("""
_(
"""
An untrapped error occurred.
"""),
_("""
"""
),
_(
"""
An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
"""),
"""
),
)
_ERROR_CMDSETS = (
_("""
_(
"""
A cmdset merger-error occurred. This is often due to a syntax
error in one of the cmdsets to merge.
"""),
_("""
"""
),
_(
"""
A cmdset merger-error occurred. Please file a bug report detailing the
steps to reproduce.
"""),
"""
),
)
_ERROR_NOCMDSETS = (
_("""
_(
"""
No command sets found! This is a critical bug that can have
multiple causes.
"""),
_("""
"""
),
_(
"""
No command sets found! This is a sign of a critical bug. If
disconnecting/reconnecting doesn't" solve the problem, try to contact
the server admin through" some other means for assistance.
"""),
"""
),
)
_ERROR_CMDHANDLER = (
_("""
_(
"""
A command handler bug occurred. If this is not due to a local change,
please file a bug report with the Evennia project, including the
traceback and steps to reproduce.
"""),
_("""
"""
),
_(
"""
A command handler bug occurred. Please notify staff - they should
likely file a bug report with the Evennia project.
"""),
"""
),
)
_ERROR_RECURSION_LIMIT = _(

View file

@ -71,7 +71,7 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
for cmdname in [cmd.key] + cmd.aliases
if cmdname
and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):]))
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
]
)
else:
@ -90,7 +90,7 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
if (
cmdname
and l_raw_string.startswith(cmdname.lower())
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname):]))
and (not cmd.arg_regex or cmd.arg_regex.match(l_raw_string[len(cmdname) :]))
):
matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception:
@ -125,7 +125,10 @@ def try_num_differentiators(raw_string):
# the user might be trying to identify the command
# with a #num-command style syntax. We expect the regex to
# contain the groups "number" and "name".
mindex, new_raw_string = (num_ref_match.group("number"), num_ref_match.group("name") + num_ref_match.group("args"))
mindex, new_raw_string = (
num_ref_match.group("number"),
num_ref_match.group("name") + num_ref_match.group("args"),
)
return int(mindex), new_raw_string
else:
return None, None
@ -182,9 +185,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if not matches and _CMD_IGNORE_PREFIXES:
# still no match. Try to strip prefixes
raw_string = (
raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
)
raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else raw_string
matches = build_matches(raw_string, cmdset, include_prefixes=False)
# only select command matches we are actually allowed to call.

View file

@ -358,14 +358,18 @@ class CmdSet(object, metaclass=_CmdSetMeta):
"""
perm = "perm" if self.persistent else "non-perm"
options = ", ".join([
"{}:{}".format(opt, "T" if getattr(self, opt) else "F")
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(self, opt) is not None
])
options = ", ".join(
[
"{}:{}".format(opt, "T" if getattr(self, opt) else "F")
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
if getattr(self, opt) is not None
]
)
options = (", " + options) if options else ""
return (f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
+ ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)]))
return (
f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
+ ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
)
def __iter__(self):
"""
@ -519,10 +523,12 @@ class CmdSet(object, metaclass=_CmdSetMeta):
try:
cmdset = self._instantiate(cmdset)
except RuntimeError:
err = ("Adding cmdset {cmdset} to {cls} lead to an "
"infinite loop. When adding a cmdset to another, "
"make sure they are not themself cyclically added to "
"the new cmdset somewhere in the chain.")
err = (
"Adding cmdset {cmdset} to {cls} lead to an "
"infinite loop. When adding a cmdset to another, "
"make sure they are not themself cyclically added to "
"the new cmdset somewhere in the chain."
)
raise RuntimeError(_(err.format(cmdset=cmdset, cls=self.__class__)))
cmds = cmdset.commands
elif is_iter(cmd):

View file

@ -423,8 +423,7 @@ class CmdSetHandler(object):
self.mergetype_stack.append(new_current.actual_mergetype)
self.current = new_current
def add(self, cmdset, emit_to_obj=None, persistent=False, default_cmdset=False,
**kwargs):
def add(self, cmdset, emit_to_obj=None, persistent=False, default_cmdset=False, **kwargs):
"""
Add a cmdset to the handler, on top of the old ones, unless it
is set as the default one (it will then end up at the bottom of the stack)
@ -451,9 +450,11 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to "
"'persistent' and now defaults to True.")
persistent = kwargs['permanent'] if persistent is None else persistent
logger.log_dep(
"obj.cmdset.add() kwarg 'permanent' has changed name to "
"'persistent' and now defaults to True."
)
persistent = kwargs["permanent"] if persistent is None else persistent
if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
string = _("Only CmdSets can be added to the cmdsethandler!")
@ -491,9 +492,10 @@ class CmdSetHandler(object):
"""
if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add_default() kwarg 'permanent' has changed name to "
"'persistent'.")
persistent = kwargs['permanent'] if persistent is None else persistent
logger.log_dep(
"obj.cmdset.add_default() kwarg 'permanent' has changed name to 'persistent'."
)
persistent = kwargs["permanent"] if persistent is None else persistent
self.add(cmdset, emit_to_obj=emit_to_obj, persistent=persistent, default_cmdset=True)
def remove(self, cmdset=None, default_cmdset=False):

View file

@ -102,16 +102,16 @@ def _init_command(cls, **kwargs):
# pre-prepare a help index entry for quicker lookup
# strip the @- etc to allow help to be agnostic
stripped_key = cls.key[1:] if cls.key and cls.key[0] in CMD_IGNORE_PREFIXES else ""
stripped_aliases = (
" ".join(al[1:] if al and al[0] in CMD_IGNORE_PREFIXES else al
for al in cls.aliases))
stripped_aliases = " ".join(
al[1:] if al and al[0] in CMD_IGNORE_PREFIXES else al for al in cls.aliases
)
cls.search_index_entry = {
"key": cls.key,
"aliases": " ".join(cls.aliases),
"no_prefix": f"{stripped_key} {stripped_aliases}",
"category": cls.help_category,
"text": cls.__doc__,
"tags": ""
"tags": "",
}
@ -562,7 +562,7 @@ Command {self} has no defined `func()` - showing on-command variables:
"""
try:
return reverse(
'help-entry-detail',
"help-entry-detail",
kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)},
)
except Exception as e:

View file

@ -217,7 +217,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
ipregex = re.compile(r"%s" % ipregex)
bantup = ("", ban, ipregex, now, reason)
ret = yield(f"Are you sure you want to {typ}-ban '|w{ban}|n' [Y]/N?")
ret = yield (f"Are you sure you want to {typ}-ban '|w{ban}|n' [Y]/N?")
if str(ret).lower() in ("no", "n"):
self.caller.msg("Aborted.")
return
@ -273,7 +273,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
ban = banlist[num - 1]
value = (" ".join([s for s in ban[:2]])).strip()
ret = yield(f"Are you sure you want to unban {num}: '|w{value}|n' [Y]/N?")
ret = yield (f"Are you sure you want to unban {num}: '|w{value}|n' [Y]/N?")
if str(ret).lower() in ("n", "no"):
self.caller.msg("Aborted.")
return

View file

@ -17,7 +17,8 @@ from evennia.utils.utils import (
class_from_module,
get_all_typeclasses,
variable_from_module,
dbref, crop,
dbref,
crop,
interactive,
list_to_string,
display_len,
@ -1498,9 +1499,11 @@ class CmdOpen(ObjManipCommand):
super().parse()
self.location = self.caller.location
if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination>")
self.caller.msg(
"Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination>"
)
raise InterruptCommand
if not self.location:
self.caller.msg("You cannot create an exit from a None-location.")
@ -1519,8 +1522,9 @@ class CmdOpen(ObjManipCommand):
as well as the self.create_exit() method.
"""
# Create exit
ok = self.create_exit(self.exit_name, self.location, self.destination,
self.exit_aliases, self.exit_typeclass)
ok = self.create_exit(
self.exit_name, self.location, self.destination, self.exit_aliases, self.exit_typeclass
)
if not ok:
# an error; the exit was not created, so we quit.
return
@ -1529,8 +1533,13 @@ class CmdOpen(ObjManipCommand):
back_exit_name = self.lhs_objs[1]["name"]
back_exit_aliases = self.lhs_objs[1]["aliases"]
back_exit_typeclass = self.lhs_objs[1]["option"]
self.create_exit(back_exit_name, self.destination, self.location, back_exit_aliases,
back_exit_typeclass)
self.create_exit(
back_exit_name,
self.destination,
self.location,
back_exit_aliases,
back_exit_typeclass,
)
def _convert_from_string(cmd, strobj):
@ -1740,8 +1749,10 @@ class CmdSetAttribute(ObjManipCommand):
obj.attributes.remove(attr, category=category)
return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
else:
return (f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] "
"was found to delete.")
return (
f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] "
"was found to delete."
)
error = f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] was found to delete."
if nested:
error += " (Nested lookups attempted)"
@ -1813,7 +1824,7 @@ class CmdSetAttribute(ObjManipCommand):
except AttributeError:
# we set empty buffer on nonexisting Attribute because otherwise
# we'd always have the string "None" in the buffer to start with
old_value = ''
old_value = ""
return str(old_value) # we already confirmed we are ok with this
def save(caller, buf):
@ -1825,11 +1836,12 @@ class CmdSetAttribute(ObjManipCommand):
try:
old_value = obj.attributes.get(attr, raise_exception=True)
if not isinstance(old_value, str):
answer = yield(
answer = yield (
f"|rWarning: Attribute |w{attr}|r is of type |w{type(old_value).__name__}|r. "
"\nTo continue editing, it must be converted to (and saved as) a string. "
"Continue? [Y]/N?")
if answer.lower() in ('n', 'no'):
"Continue? [Y]/N?"
)
if answer.lower() in ("n", "no"):
self.caller.msg("Aborted edit.")
return
except AttributeError:
@ -1903,9 +1915,11 @@ class CmdSetAttribute(ObjManipCommand):
caller.msg("The Line editor can only be applied " "to one attribute at a time.")
return
if not attrs:
caller.msg("Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo "
"edit the current room description, use `set/edit here/desc` (or "
"use the `desc` command).")
caller.msg(
"Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo "
"edit the current room description, use `set/edit here/desc` (or "
"use the `desc` command)."
)
return
self.edit_handler(obj, attrs[0], caller)
return
@ -1936,8 +1950,10 @@ class CmdSetAttribute(ObjManipCommand):
global _ATTRFUNCPARSER
if not _ATTRFUNCPARSER:
_ATTRFUNCPARSER = funcparser.FuncParser(
{"dbref": funcparser.funcparser_callable_search,
"search": funcparser.funcparser_callable_search}
{
"dbref": funcparser.funcparser_callable_search,
"search": funcparser.funcparser_callable_search,
}
)
if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
@ -1951,10 +1967,13 @@ class CmdSetAttribute(ObjManipCommand):
if hasattr(parsed_value, "access"):
# if this is an object we must have the right to read it, if so,
# we will not convert it to a string
if not (parsed_value.access(caller, "control")
or parsed_value.access(self.caller, "edit")):
caller.msg("You don't have permission to set "
f"object with identifier '{value}'.")
if not (
parsed_value.access(caller, "control")
or parsed_value.access(self.caller, "edit")
):
caller.msg(
"You don't have permission to set " f"object with identifier '{value}'."
)
continue
value = parsed_value
else:
@ -2038,7 +2057,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
obj = caller.search(query)
if not obj:
return
elif (self.account and self.account.__dbclass__ == dbclass):
elif self.account and self.account.__dbclass__ == dbclass:
# applying account while caller is object
caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
obj = self.account.search(query)
@ -2071,7 +2090,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
caller = self.caller
if "list" in self.switches or self.cmdname in ('typeclasses', '@typeclasses'):
if "list" in self.switches or self.cmdname in ("typeclasses", "@typeclasses"):
tclasses = get_all_typeclasses()
contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [
"<None loaded>"
@ -2188,8 +2207,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
is_same = obj.is_typeclass(new_typeclass, exact=True)
if is_same and "force" not in self.switches:
string = (f"{obj.name} already has the typeclass '{new_typeclass}'. "
"Use /force to override.")
string = (
f"{obj.name} already has the typeclass '{new_typeclass}'. "
"Use /force to override."
)
else:
update = "update" in self.switches
reset = "reset" in self.switches
@ -2220,7 +2241,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
if "prototype" in self.switches:
modified = spawner.batch_update_objects_with_prototype(
prototype, objects=[obj], caller=self.caller)
prototype, objects=[obj], caller=self.caller
)
prototype_success = modified > 0
if not prototype_success:
caller.msg("Prototype %s failed to apply." % prototype["key"])
@ -2543,9 +2565,7 @@ class CmdExamine(ObjManipCommand):
def format_locks(self, obj):
locks = str(obj.locks)
if locks:
return utils.fill(
"; ".join([lock for lock in locks.split(";")]), indent=2
)
return utils.fill("; ".join([lock for lock in locks.split(";")]), indent=2)
return "Default"
def format_scripts(self, obj):
@ -2572,6 +2592,7 @@ class CmdExamine(ObjManipCommand):
if value:
return f"{string}: T"
return f"{string}: F"
txt = ", ".join(
_truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
@ -2607,13 +2628,18 @@ class CmdExamine(ObjManipCommand):
# we only show the first session's cmdset here (it is -in principle- possible
# that different sessions have different cmdsets but for admins who want such
# madness it is better that they overload with their own CmdExamine to handle it).
all_cmdsets.extend([(cmdset.key, cmdset)
for cmdset in obj.account.sessions.all()[0].cmdset.all()])
all_cmdsets.extend(
[(cmdset.key, cmdset) for cmdset in obj.account.sessions.all()[0].cmdset.all()]
)
else:
try:
# we have to protect this since many objects don't have sessions.
all_cmdsets.extend([(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()])
all_cmdsets.extend(
[
(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()
]
)
except (TypeError, AttributeError):
# an error means we are merging an object without a session
pass
@ -2659,8 +2685,10 @@ class CmdExamine(ObjManipCommand):
typ = f" |B[type: {typ}]|n" if typ else ""
value = utils.to_str(value)
value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
return (f"Attribute {obj.name}/{self.header_color}{key}|n "
f"[category={category}]{typ}:\n\n{value}")
return (
f"Attribute {obj.name}/{self.header_color}{key}|n "
f"[category={category}]{typ}:\n\n{value}"
)
def format_single_attribute(self, attr):
global _FUNCPARSER
@ -2680,8 +2708,7 @@ class CmdExamine(ObjManipCommand):
def format_attributes(self, obj):
output = "\n " + "\n ".join(
sorted(self.format_single_attribute(attr)
for attr in obj.db_attributes.all())
sorted(self.format_single_attribute(attr) for attr in obj.db_attributes.all())
)
if output.strip():
# we don't want just an empty line
@ -2695,8 +2722,7 @@ class CmdExamine(ObjManipCommand):
if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join(
sorted(self.format_single_attribute(attr)
for attr, value in ndb_attr)
sorted(self.format_single_attribute(attr) for attr, value in ndb_attr)
)
def format_exits(self, obj):
@ -2706,14 +2732,16 @@ class CmdExamine(ObjManipCommand):
def format_chars(self, obj):
if hasattr(obj, "contents"):
chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents
if obj.account)
chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents if obj.account)
return chars if chars else None
def format_things(self, obj):
if hasattr(obj, "contents"):
things = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents
if not obj.account and not obj.destination)
things = ", ".join(
f"{obj.name}({obj.dbref})"
for obj in obj.contents
if not obj.account and not obj.destination
)
return things if things else None
def format_script_desc(self, obj):
@ -2736,8 +2764,10 @@ class CmdExamine(ObjManipCommand):
remaining_repeats = obj.remaining_repeats()
remaining_repeats = 0 if remaining_repeats is None else remaining_repeats
repeats = f" - {remaining_repeats}/{obj.db_repeats} remain"
return (f"{active} - interval: {interval}s "
f"(next: {next_repeat}{repeats}, start_delay: {start_delay})")
return (
f"{active} - interval: {interval}s "
f"(next: {next_repeat}{repeats}, start_delay: {start_delay})"
)
def format_channel_sub_totals(self, obj):
if hasattr(obj, "db_account_subscriptions"):
@ -2752,14 +2782,16 @@ class CmdExamine(ObjManipCommand):
account_subs = obj.db_account_subscriptions.all()
if account_subs:
return "\n " + "\n ".join(
format_grid([sub.key for sub in account_subs], sep=' ', width=_DEFAULT_WIDTH))
format_grid([sub.key for sub in account_subs], sep=" ", width=_DEFAULT_WIDTH)
)
def format_channel_object_subs(self, obj):
if hasattr(obj, "db_object_subscriptions"):
object_subs = obj.db_object_subscriptions.all()
if object_subs:
return "\n " + "\n ".join(
format_grid([sub.key for sub in object_subs], sep=' ', width=_DEFAULT_WIDTH))
format_grid([sub.key for sub in object_subs], sep=" ", width=_DEFAULT_WIDTH)
)
def get_formatted_obj_data(self, obj, current_cmdset):
"""
@ -2781,13 +2813,14 @@ class CmdExamine(ObjManipCommand):
objdata["Destination"] = self.format_destination(obj)
objdata["Permissions"] = self.format_permissions(obj)
objdata["Locks"] = self.format_locks(obj)
if (current_cmdset
and not (len(obj.cmdset.all()) == 1
and obj.cmdset.current.key == "_EMPTY_CMDSET")):
if current_cmdset and not (
len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"
):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"] = (
self.format_current_cmds(obj, current_cmdset))
objdata[
f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
objdata["Persistent"] = self.format_script_is_persistent(obj)
@ -2859,10 +2892,11 @@ class CmdExamine(ObjManipCommand):
obj = None
elif len(obj) > 1:
err = "Multiple {objtype} found with key {obj_name}:\n{matches}"
self.caller.msg(err.format(
obj_name=obj_name,
matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj)
))
self.caller.msg(
err.format(
obj_name=obj_name, matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj)
)
)
obj = None
else:
obj = obj[0]
@ -2887,13 +2921,16 @@ class CmdExamine(ObjManipCommand):
# is not so common anyway.
obj = None
obj_name = objdef["name"] # name
obj_name = objdef["name"] # name
obj_attrs = objdef["attrs"] # /attrs
# identify object type, in prio account - script - channel
object_type = "object"
if (utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
or "account" in self.switches or obj_name.startswith("*")):
if (
utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
or "account" in self.switches
or obj_name.startswith("*")
):
object_type = "account"
elif "script" in self.switches:
object_type = "script"
@ -3293,7 +3330,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
"start": "|gStarted|n",
"stop": "|RStopped|n",
"pause": "|Paused|n",
"delete": "|rDeleted|n"
"delete": "|rDeleted|n",
}
def _search_script(self, args):
@ -3307,7 +3344,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
return scripts
if "-" in args:
# may be a dbref-range
val1, val2 = (dbref(part.strip()) for part in args.split('-', 1))
val1, val2 = (dbref(part.strip()) for part in args.split("-", 1))
if val1 and val2:
scripts = ScriptDB.objects.filter(id__in=(range(val1, val2 + 1)))
if scripts:
@ -3348,11 +3385,14 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
if obj.scripts.add(self.rhs, autostart=True):
caller.msg(
f"Script |w{self.rhs}|n successfully added and "
f"started on {obj.get_display_name(caller)}.")
f"started on {obj.get_display_name(caller)}."
)
else:
caller.msg(f"Script {self.rhs} could not be added and/or started "
f"on {obj.get_display_name(caller)} (or it started and "
"immediately shut down).")
caller.msg(
f"Script {self.rhs} could not be added and/or started "
f"on {obj.get_display_name(caller)} (or it started and "
"immediately shut down)."
)
else:
# just show all scripts on object
scripts = ScriptDB.objects.filter(db_obj=obj)
@ -3374,12 +3414,15 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
new_script = None
if new_script:
caller.msg(f"Global Script Created - "
f"{new_script.key} ({new_script.typeclass_path})")
caller.msg(
f"Global Script Created - "
f"{new_script.key} ({new_script.typeclass_path})"
)
ScriptEvMore(caller, [new_script], session=self.session)
else:
caller.msg(f"Global Script |rNOT|n Created |r(see log)|n - "
f"arguments: {self.args}")
caller.msg(
f"Global Script |rNOT|n Created |r(see log)|n - " f"arguments: {self.args}"
)
elif scripts or obj:
# modification switches - must operate on existing scripts
@ -3388,9 +3431,11 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
scripts = ScriptDB.objects.filter(db_obj=obj)
if scripts.count() > 1:
ret = yield(f"Multiple scripts found: {scripts}. Are you sure you want to "
"operate on all of them? [Y]/N? ")
if ret.lower() in ('n', 'no'):
ret = yield (
f"Multiple scripts found: {scripts}. Are you sure you want to "
"operate on all of them? [Y]/N? "
)
if ret.lower() in ("n", "no"):
caller.msg("Aborted.")
return
@ -3406,11 +3451,14 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
getattr(script, switch)()
except Exception:
logger.log_trace()
msgs.append(f"{scripttype} |rNOT|n {verb} |r(see log)|n - "
f"{script_key} ({script_typeclass_path})|n")
msgs.append(
f"{scripttype} |rNOT|n {verb} |r(see log)|n - "
f"{script_key} ({script_typeclass_path})|n"
)
else:
msgs.append(f"{scripttype} {verb} - "
f"{script_key} ({script_typeclass_path})")
msgs.append(
f"{scripttype} {verb} - " f"{script_key} ({script_typeclass_path})"
)
caller.msg("\n".join(msgs))
if "delete" not in self.switches:
ScriptEvMore(caller, [script], session=self.session)
@ -3488,7 +3536,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
)
# last N table
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim): ]
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :]
latesttable = self.styled_table(
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="table"
)
@ -3620,14 +3668,18 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
# check any locks
if not (caller.permissions.check("Admin") or obj_to_teleport.access(caller, "teleport")):
caller.msg(f"{obj_to_teleport} 'teleport'-lock blocks you from teleporting "
"it anywhere.")
caller.msg(
f"{obj_to_teleport} 'teleport'-lock blocks you from teleporting " "it anywhere."
)
return
if not (caller.permissions.check("Admin")
or destination.access(obj_to_teleport, "teleport_here")):
caller.msg(f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from "
"moving there.")
if not (
caller.permissions.check("Admin")
or destination.access(obj_to_teleport, "teleport_here")
):
caller.msg(
f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from " "moving there."
)
return
# try the teleport
@ -3636,8 +3688,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
obj_to_teleport.location = destination
caller.msg(f"Teleported {obj_to_teleport} None -> {destination}")
elif obj_to_teleport.move_to(
destination, quiet="quiet" in self.switches,
emit_to_obj=caller, use_destination="intoexit" not in self.switches):
destination,
quiet="quiet" in self.switches,
emit_to_obj=caller,
use_destination="intoexit" not in self.switches,
):
if obj_to_teleport == caller:
caller.msg(f"Teleported to {destination}.")
@ -3995,7 +4050,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
self.caller.msg("No prototypes found.")
def _list_prototypes(self, key=None, tags=None):
"""Display prototypes as a list, optionally limited by key/tags. """
"""Display prototypes as a list, optionally limited by key/tags."""
protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)
@interactive
@ -4039,7 +4094,9 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
return
try:
n_updated = spawner.batch_update_objects_with_prototype(
prototype, objects=existing_objects, caller=caller,
prototype,
objects=existing_objects,
caller=caller,
)
except Exception:
logger.log_trace()

View file

@ -20,16 +20,15 @@ from evennia.utils.evmenu import ask_yes_no
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
CHANNEL_DEFAULT_TYPECLASS = class_from_module(
settings.BASE_CHANNEL_TYPECLASS, fallback=settings.FALLBACK_CHANNEL_TYPECLASS)
settings.BASE_CHANNEL_TYPECLASS, fallback=settings.FALLBACK_CHANNEL_TYPECLASS
)
# limit symbol import for API
__all__ = (
"CmdChannel",
"CmdObjectChannel",
"CmdPage",
"CmdIRC2Chan",
"CmdIRCStatus",
"CmdRSS2Chan",
@ -207,6 +206,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
"""
key = "@channel"
aliases = ["@chan", "@channels"]
help_category = "Comms"
@ -215,8 +215,25 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# the manage: lock controls access to /create/destroy/desc/lock/unlock switches
locks = "cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
switch_options = (
"list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias",
"create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",)
"list",
"all",
"history",
"sub",
"unsub",
"mute",
"unmute",
"alias",
"unalias",
"create",
"destroy",
"desc",
"lock",
"unlock",
"boot",
"ban",
"unban",
"who",
)
# disable this in child command classes if wanting on-character channels
account_caller = True
@ -253,17 +270,24 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)
# check permissions
channels = [channel for channel in channels
if channel.access(caller, 'listen') or channel.access(caller, 'control')]
channels = [
channel
for channel in channels
if channel.access(caller, "listen") or channel.access(caller, "control")
]
if handle_errors:
if not channels:
self.msg(f"No channel found matching '{channelname}' "
"(could also be due to missing access).")
self.msg(
f"No channel found matching '{channelname}' "
"(could also be due to missing access)."
)
return None
elif len(channels) > 1:
self.msg("Multiple possible channel matches/alias for "
"'{channelname}':\n" + ", ".join(chan.key for chan in channels))
self.msg(
"Multiple possible channel matches/alias for "
"'{channelname}':\n" + ", ".join(chan.key for chan in channels)
)
return None
return channels[0]
else:
@ -312,6 +336,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
return self.msg(
"".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines)
)
# asynchronously tail the log file
tail_log_file(log_file, start_index, 20, callback=send_msg)
@ -491,7 +516,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel(
name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass)
name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass
)
self.sub_to_channel(new_chan)
return new_chan, ""
@ -514,14 +540,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channel_key = channel.key
if message is None:
message = (f"|rChannel {channel_key} is being destroyed. "
"Make sure to clean any channel aliases.|n")
message = (
f"|rChannel {channel_key} is being destroyed. "
"Make sure to clean any channel aliases.|n"
)
if message:
channel.msg(message, senders=caller, bypass_mute=True)
channel.delete()
logger.log_sec(
"Channel {} was deleted by {}".format(channel_key, caller)
)
logger.log_sec("Channel {} was deleted by {}".format(channel_key, caller))
def set_lock(self, channel, lockstring):
"""
@ -610,8 +636,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if not quiet:
channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")
logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, "
f"Reason: {reason.strip()}, Caller: {self.caller}")
logger.log_sec(
f"Channel Boot: {target} (Channel: {channel}, "
f"Reason: {reason.strip()}, Caller: {self.caller}"
)
return True, ""
def ban_user(self, channel, target, quiet=False, reason=""):
@ -684,7 +712,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
caller = self.caller
mute_list = list(channel.mutelist)
online_list = channel.subscriptions.online()
if channel.access(caller, 'control'):
if channel.access(caller, "control"):
# for those with channel control, show also offline users
all_subs = list(channel.subscriptions.all())
else:
@ -694,8 +722,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
who_list = []
for subscriber in all_subs:
name = subscriber.get_display_name(caller)
conditions = ("muting" if subscriber in mute_list else "",
"offline" if subscriber not in online_list else "")
conditions = (
"muting" if subscriber in mute_list else "",
"offline" if subscriber not in online_list else "",
)
conditions = [cond for cond in conditions if cond]
cond_text = "(" + ", ".join(conditions) + ")" if conditions else ""
who_list.append(f"{name}{cond_text}")
@ -743,7 +773,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"locks",
"description",
align="l",
maxwidth=_DEFAULT_WIDTH
maxwidth=_DEFAULT_WIDTH,
)
for chan in subscribed:
@ -756,14 +786,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row(
*(
chanid,
"{key}{aliases}".format(
key=chan.key,
aliases=";"+ ";".join(chan.aliases.all()) if chan.aliases.all() else ""
),
my_aliases,
locks,
chan.db.desc
chanid,
"{key}{aliases}".format(
key=chan.key,
aliases=";" + ";".join(chan.aliases.all()) if chan.aliases.all() else "",
),
my_aliases,
locks,
chan.db.desc,
)
)
return comtable
@ -799,11 +829,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
substatus = "|gYes|n"
my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row(
*(substatus,
chan.key,
",".join(chan.aliases.all()) if chan.aliases.all() else "",
my_aliases,
chan.db.desc))
*(
substatus,
chan.key,
",".join(chan.aliases.all()) if chan.aliases.all() else "",
my_aliases,
chan.db.desc,
)
)
comtable.reformat_column(0, width=8)
return comtable
@ -818,16 +851,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
switches = self.switches
channel_names = [name for name in self.lhslist if name]
#from evennia import set_trace;set_trace()
# from evennia import set_trace;set_trace()
if 'all' in switches:
if "all" in switches:
# show all available channels
subscribed, available = self.list_channels()
table = self.display_all_channels(subscribed, available)
self.msg(
"\n|wAvailable channels|n (use no argument to "
f"only show your subscriptions)\n{table}")
f"only show your subscriptions)\n{table}"
)
return
if not channel_names:
@ -835,15 +869,16 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
subscribed, _ = self.list_channels()
table = self.display_subbed_channels(subscribed)
self.msg("\n|wChannel subscriptions|n "
f"(use |w/all|n to see all available):\n{table}")
self.msg(
"\n|wChannel subscriptions|n " f"(use |w/all|n to see all available):\n{table}"
)
return
if not self.switches and not self.args:
self.msg("Usage[/switches]: channel [= message]")
return
if 'create' in switches:
if "create" in switches:
# create a new channel
if not self.access(caller, "manage"):
@ -865,7 +900,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err)
return
if 'unalias' in switches:
if "unalias" in switches:
# remove a personal alias (no channel needed)
alias = self.args.strip()
if not alias:
@ -884,12 +919,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# channels without a space in their name), we need to check if the
# first 'channel name' is in fact 'channelname text'
no_rhs_channel_name = self.args.split(" ", 1)[0]
possible_lhs_message = self.args[len(no_rhs_channel_name):]
if possible_lhs_message.strip() == '=':
possible_lhs_message = self.args[len(no_rhs_channel_name) :]
if possible_lhs_message.strip() == "=":
possible_lhs_message = ""
channel_names.append(no_rhs_channel_name)
channels = []
errors = []
for channel_name in channel_names:
@ -897,16 +931,20 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# 'listen/control' perms.
found_channels = self.search_channel(channel_name, exact=False, handle_errors=False)
if not found_channels:
errors.append(f"No channel found matching '{channel_name}' "
"(could also be due to missing access).")
errors.append(
f"No channel found matching '{channel_name}' "
"(could also be due to missing access)."
)
elif len(found_channels) > 1:
errors.append("Multiple possible channel matches/alias for "
"'{channel_name}':\n" + ", ".join(chan.key for chan in found_channels))
errors.append(
"Multiple possible channel matches/alias for "
"'{channel_name}':\n" + ", ".join(chan.key for chan in found_channels)
)
else:
channels.append(found_channels[0])
if not channels:
self.msg('\n'.join(errors))
self.msg("\n".join(errors))
return
# we have at least one channel at this point
@ -925,30 +963,35 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if channel in subscribed:
table = self.display_subbed_channels([channel])
header = f"Channel |w{channel.key}|n"
self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
f"to chat and the 'channel' command "
f"to customize)\n{table}")
self.msg(
f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
f"to chat and the 'channel' command "
f"to customize)\n{table}"
)
elif channel in available:
table = self.display_all_channels([], [channel])
self.msg(
"\n|wNot subscribed to this channel|n (use /list to "
f"show all subscriptions)\n{table}")
f"show all subscriptions)\n{table}"
)
return
if 'history' in switches or 'hist' in switches:
if "history" in switches or "hist" in switches:
# view channel history
index = self.rhs or 0
try:
index = max(0, int(index))
except ValueError:
self.msg("The history index (describing how many lines to go back) "
"must be an integer >= 0.")
self.msg(
"The history index (describing how many lines to go back) "
"must be an integer >= 0."
)
return
self.get_channel_history(channel, start_index=index)
return
if 'sub' in switches:
if "sub" in switches:
# subscribe to a channel
aliases = []
if self.rhs:
@ -957,26 +1000,29 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if success:
for alias in aliases:
self.add_alias(channel, alias)
alias_txt = ', '.join(aliases)
alias_txt = f" using alias(es) {alias_txt}" if aliases else ''
self.msg("You are now subscribed "
f"to the channel {channel.key}{alias_txt}. Use /alias to "
"add additional aliases for referring to the channel.")
alias_txt = ", ".join(aliases)
alias_txt = f" using alias(es) {alias_txt}" if aliases else ""
self.msg(
"You are now subscribed "
f"to the channel {channel.key}{alias_txt}. Use /alias to "
"add additional aliases for referring to the channel."
)
else:
self.msg(err)
return
if 'unsub' in switches:
if "unsub" in switches:
# un-subscribe from a channel
success, err = self.unsub_from_channel(channel)
if success:
self.msg(f"You un-subscribed from channel {channel.key}. "
"All aliases were cleared.")
self.msg(
f"You un-subscribed from channel {channel.key}. " "All aliases were cleared."
)
else:
self.msg(err)
return
if 'alias' in switches:
if "alias" in switches:
# create a new personal alias for a channel
alias = self.rhs
if not alias:
@ -986,7 +1032,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
return
if 'mute' in switches:
if "mute" in switches:
# mute a given channel
success, err = self.mute_channel(channel)
if success:
@ -995,7 +1041,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err)
return
if 'unmute' in switches:
if "unmute" in switches:
# unmute a given channel
success, err = self.unmute_channel(channel)
if success:
@ -1004,7 +1050,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err)
return
if 'destroy' in switches or 'delete' in switches:
if "destroy" in switches or "delete" in switches:
# destroy a channel we control
if not self.access(caller, "manage"):
@ -1028,10 +1074,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"remove all users' aliases. {options}?",
yes_action=_perform_delete,
no_action="Aborted.",
default="N"
default="N",
)
if 'desc' in switches:
if "desc" in switches:
# set channel description
if not self.access(caller, "manage"):
@ -1051,7 +1097,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.set_desc(channel, desc)
self.msg("Updated channel description.")
if 'lock' in switches:
if "lock" in switches:
# add a lockstring to channel
if not self.access(caller, "changelocks"):
@ -1075,7 +1121,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Could not add/update lock: {err}")
return
if 'unlock' in switches:
if "unlock" in switches:
# remove/update lockstring from channel
if not self.access(caller, "changelocks"):
@ -1099,7 +1145,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Could not remove lock: {err}")
return
if 'boot' in switches:
if "boot" in switches:
# boot a user from channel(s)
if not self.access(caller, "admin"):
@ -1134,8 +1180,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
channames = ", ".join(chan.key for chan in channels)
reasonwarn = (". Also note that your reason will be echoed to the channel"
if reason else '')
reasonwarn = (
". Also note that your reason will be echoed to the channel" if reason else ""
)
ask_yes_no(
caller,
prompt=f"Are you sure you want to boot user {target.key} from "
@ -1143,11 +1190,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"{options}?",
yes_action=_boot_user,
no_action="Aborted.",
default="Y"
default="Y",
)
return
if 'ban' in switches:
if "ban" in switches:
# ban a user from channel(s)
if not self.access(caller, "admin"):
@ -1161,8 +1208,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"You need 'control'-access to view bans on channel {channel.key}")
return
bans = ["Channel bans "
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"]
bans = [
"Channel bans "
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"
]
bans.extend(self.channel_list_bans(channel))
self.msg("\n".join(bans))
return
@ -1191,8 +1240,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
channames = ", ".join(chan.key for chan in channels)
reasonwarn = (". Also note that your reason will be echoed to the channel"
if reason else '')
reasonwarn = (
". Also note that your reason will be echoed to the channel" if reason else ""
)
ask_yes_no(
caller,
f"Are you sure you want to ban user {target.key} from "
@ -1203,7 +1253,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
)
return
if 'unban' in switches:
if "unban" in switches:
# unban a previously banned user from channel
if not self.access(caller, "admin"):
@ -1414,7 +1464,6 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
receiver=receiver,
message=page.message,
)
)
lastpages = "\n ".join(listing)
@ -1465,6 +1514,7 @@ def _list_bots(cmd):
else:
return "No irc bots found."
class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
"""
Link an evennia channel to an external IRC channel

View file

@ -382,10 +382,13 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
string = "You are not carrying anything."
else:
from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header")
for item in items:
table.add_row(f"|C{item.name}|n",
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""))
table.add_row(
f"|C{item.name}|n",
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""),
)
string = f"|wYou are carrying:\n{table}"
self.caller.msg(string)

View file

@ -19,11 +19,7 @@ from evennia.utils import create, evmore
from evennia.utils.ansi import ANSIString
from evennia.help.filehelp import FILE_HELP_ENTRIES
from evennia.utils.eveditor import EvEditor
from evennia.utils.utils import (
class_from_module,
inherits_from,
format_grid, pad
)
from evennia.utils.utils import class_from_module, inherits_from, format_grid, pad
from evennia.help.utils import help_search_with_index, parse_entry_for_subcategories
CMD_IGNORE_PREFIXES = settings.CMD_IGNORE_PREFIXES
@ -35,12 +31,14 @@ HELP_CLICKABLE_TOPICS = settings.HELP_CLICKABLE_TOPICS
# limit symbol import for API
__all__ = ("CmdHelp", "CmdSetHelp")
@dataclass
class HelpCategory:
"""
Mock 'help entry' to search categories with the same code.
"""
key: str
@property
@ -113,7 +111,10 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
if type(self).help_more:
usemore = True
if self.session and self.session.protocol_key in ("websocket", "ajax/comet",):
if self.session and self.session.protocol_key in (
"websocket",
"ajax/comet",
):
try:
options = self.account.db._saved_webclient_options
if options and options["helppopup"]:
@ -127,8 +128,15 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
self.msg(text=(text, {"type": "help"}))
def format_help_entry(self, topic="", help_text="", aliases=None, suggested=None,
subtopics=None, click_topics=True):
def format_help_entry(
self,
topic="",
help_text="",
aliases=None,
suggested=None,
subtopics=None,
click_topics=True,
):
"""This visually formats the help entry.
This method can be overriden to customize the way a help
entry is displayed.
@ -152,28 +160,24 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
title = f"|CHelp for |w{topic}|n" if topic else "|rNo help found|n"
if aliases:
aliases = (
" |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases))
)
aliases = " |C(aliases: {}|C)|n".format("|C,|n ".join(f"|w{ali}|n" for ali in aliases))
else:
aliases = ''
aliases = ""
help_text = "\n" + dedent(help_text.strip('\n')) if help_text else ""
help_text = "\n" + dedent(help_text.strip("\n")) if help_text else ""
if subtopics:
if click_topics:
subtopics = [
f"|lchelp {topic}/{subtop}|lt|w{topic}/{subtop}|n|le"
for subtop in subtopics
]
f"|lchelp {topic}/{subtop}|lt|w{topic}/{subtop}|n|le" for subtop in subtopics
]
else:
subtopics = [f"|w{topic}/{subtop}|n" for subtop in subtopics]
subtopics = (
"\n|CSubtopics:|n\n {}".format(
"\n ".join(format_grid(subtopics, width=self.client_width())))
subtopics = "\n|CSubtopics:|n\n {}".format(
"\n ".join(format_grid(subtopics, width=self.client_width()))
)
else:
subtopics = ''
subtopics = ""
if suggested:
suggested = sorted(suggested)
@ -181,12 +185,11 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
suggested = [f"|lchelp {sug}|lt|w{sug}|n|le" for sug in suggested]
else:
suggested = [f"|w{sug}|n" for sug in suggested]
suggested = (
"\n|COther topic suggestions:|n\n{}".format(
"\n ".join(format_grid(suggested, width=self.client_width())))
suggested = "\n|COther topic suggestions:|n\n{}".format(
"\n ".join(format_grid(suggested, width=self.client_width()))
)
else:
suggested = ''
suggested = ""
end = start
@ -194,8 +197,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
return "\n".join(part.rstrip() for part in partorder if part)
def format_help_index(self, cmd_help_dict=None, db_help_dict=None, title_lone_category=False,
click_topics=True):
def format_help_index(
self, cmd_help_dict=None, db_help_dict=None, title_lone_category=False, click_topics=True
):
"""Output a category-ordered g for displaying the main help, grouped by
category.
@ -219,6 +223,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
commands and topics.
"""
def _group_by_category(help_dict):
grid = []
verbatim_elements = []
@ -231,9 +236,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# make the help topics clickable
if click_topics:
entries = [
f'|lchelp {entry}|lt{entry}|le' for entry in entries
]
entries = [f"|lchelp {entry}|lt{entry}|le" for entry in entries]
# add the entries to the grid
grid.extend(entries)
@ -243,7 +246,8 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
category_str = f"-- {category.title()} "
grid.append(
ANSIString(
self.index_category_clr + category_str
self.index_category_clr
+ category_str
+ "-" * (width - len(category_str))
+ self.index_topic_clr
)
@ -255,9 +259,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# make the help topics clickable
if click_topics:
entries = [
f'|lchelp {entry}|lt{entry}|le' for entry in entries
]
entries = [f"|lchelp {entry}|lt{entry}|le" for entry in entries]
# add the entries to the grid
grid.extend(entries)
@ -272,18 +274,22 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
if any(cmd_help_dict.values()):
# get the command-help entries by-category
sep1 = (self.index_type_separator_clr
+ pad("Commands", width=width, fillchar='-')
+ self.index_topic_clr)
sep1 = (
self.index_type_separator_clr
+ pad("Commands", width=width, fillchar="-")
+ self.index_topic_clr
)
grid, verbatim_elements = _group_by_category(cmd_help_dict)
gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements)
cmd_grid = ANSIString("\n").join(gridrows) if gridrows else ""
if any(db_help_dict.values()):
# get db-based help entries by-category
sep2 = (self.index_type_separator_clr
+ pad("Game & World", width=width, fillchar='-')
+ self.index_topic_clr)
sep2 = (
self.index_type_separator_clr
+ pad("Game & World", width=width, fillchar="-")
+ self.index_topic_clr
)
grid, verbatim_elements = _group_by_category(db_help_dict)
gridrows = format_grid(grid, width, sep=" ", verbatim_elements=verbatim_elements)
db_grid = ANSIString("\n").join(gridrows) if gridrows else ""
@ -316,9 +322,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
"""
if inherits_from(cmd_or_topic, "evennia.commands.command.Command"):
return cmd_or_topic.auto_help and cmd_or_topic.access(caller, 'read', default=True)
return cmd_or_topic.auto_help and cmd_or_topic.access(caller, "read", default=True)
else:
return cmd_or_topic.access(caller, 'read', default=True)
return cmd_or_topic.access(caller, "read", default=True)
def can_list_topic(self, cmd_or_topic, caller):
"""
@ -355,12 +361,12 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
)
if has_view:
return cmd_or_topic.access(caller, 'view', default=True)
return cmd_or_topic.access(caller, "view", default=True)
else:
# no explicit 'view' lock - use the 'read' lock
return cmd_or_topic.access(caller, 'read', default=True)
return cmd_or_topic.access(caller, "read", default=True)
def collect_topics(self, caller, mode='list'):
def collect_topics(self, caller, mode="list"):
"""
Collect help topics from all sources (cmd/db/file).
@ -383,43 +389,45 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
cmdset.make_unique(caller)
# retrieve all available commands and database / file-help topics.
# also check the 'cmd:' lock here
cmd_help_topics = [cmd for cmd in cmdset if cmd and cmd.access(caller, 'cmd')]
cmd_help_topics = [cmd for cmd in cmdset if cmd and cmd.access(caller, "cmd")]
# get all file-based help entries, checking perms
file_help_topics = {
topic.key.lower().strip(): topic
for topic in FILE_HELP_ENTRIES.all()
}
file_help_topics = {topic.key.lower().strip(): topic for topic in FILE_HELP_ENTRIES.all()}
# get db-based help entries, checking perms
db_help_topics = {
topic.key.lower().strip(): topic
for topic in HelpEntry.objects.all()
}
if mode == 'list':
db_help_topics = {topic.key.lower().strip(): topic for topic in HelpEntry.objects.all()}
if mode == "list":
# check the view lock for all help entries/commands and determine key
cmd_help_topics = {
cmd.auto_help_display_key
if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
for cmd in cmd_help_topics if self.can_list_topic(cmd, caller)}
cmd.auto_help_display_key if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
for cmd in cmd_help_topics
if self.can_list_topic(cmd, caller)
}
db_help_topics = {
key: entry for key, entry in db_help_topics.items()
key: entry
for key, entry in db_help_topics.items()
if self.can_list_topic(entry, caller)
}
file_help_topics = {
key: entry for key, entry in file_help_topics.items()
if self.can_list_topic(entry, caller)}
key: entry
for key, entry in file_help_topics.items()
if self.can_list_topic(entry, caller)
}
else:
# query - check the read lock on entries
cmd_help_topics = {
cmd.auto_help_display_key
if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
for cmd in cmd_help_topics if self.can_read_topic(cmd, caller)}
cmd.auto_help_display_key if hasattr(cmd, "auto_help_display_key") else cmd.key: cmd
for cmd in cmd_help_topics
if self.can_read_topic(cmd, caller)
}
db_help_topics = {
key: entry for key, entry in db_help_topics.items()
key: entry
for key, entry in db_help_topics.items()
if self.can_read_topic(entry, caller)
}
file_help_topics = {
key: entry for key, entry in file_help_topics.items()
if self.can_read_topic(entry, caller)}
key: entry
for key, entry in file_help_topics.items()
if self.can_read_topic(entry, caller)
}
return cmd_help_topics, db_help_topics, file_help_topics
@ -452,9 +460,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# return of this will either be a HelpCategory, a Command or a
# HelpEntry/FileHelpEntry.
matches, suggestions = help_search_with_index(
match_query, entries,
suggestion_maxnum=self.suggestion_maxnum,
fields=search_fields
match_query, entries, suggestion_maxnum=self.suggestion_maxnum, fields=search_fields
)
if matches:
match = matches[0]
@ -478,8 +484,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# parse the query
if self.args:
self.subtopics = [part.strip().lower()
for part in self.args.split(self.subtopic_separator_char)]
self.subtopics = [
part.strip().lower() for part in self.args.split(self.subtopic_separator_char)
]
self.topic = self.subtopics.pop(0)
else:
self.topic = ""
@ -505,7 +512,6 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
return key[1:]
return key
def func(self):
"""
Run the dynamic help entry creator.
@ -518,8 +524,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# list all available help entries, grouped by category. We want to
# build dictionaries {category: [topic, topic, ...], ...}
cmd_help_topics, db_help_topics, file_help_topics = \
self.collect_topics(caller, mode='list')
cmd_help_topics, db_help_topics, file_help_topics = self.collect_topics(
caller, mode="list"
)
# db-topics override file-based ones
file_db_help_topics = {**file_help_topics, **db_help_topics}
@ -538,21 +545,21 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
file_db_help_by_category[entry.help_category].append(key)
# generate the index and display
output = self.format_help_index(cmd_help_by_category,
file_db_help_by_category,
click_topics=clickable_topics)
output = self.format_help_index(
cmd_help_by_category, file_db_help_by_category, click_topics=clickable_topics
)
self.msg_help(output)
return
# search for a specific entry. We need to check for 'read' access here before
# building the set of possibilities.
cmd_help_topics, db_help_topics, file_help_topics = \
self.collect_topics(caller, mode='query')
cmd_help_topics, db_help_topics, file_help_topics = self.collect_topics(
caller, mode="query"
)
# get a collection of all keys + aliases to be able to strip prefixes like @
key_and_aliases = set(
chain(*(cmd._keyaliases for cmd in cmd_help_topics.values())))
key_and_aliases = set(chain(*(cmd._keyaliases for cmd in cmd_help_topics.values())))
# db-help topics takes priority over file-help
file_db_help_topics = {**file_help_topics, **db_help_topics}
@ -561,8 +568,9 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
all_topics = {**file_db_help_topics, **cmd_help_topics}
# get all categories
all_categories = list(set(
HelpCategory(topic.help_category) for topic in all_topics.values()))
all_categories = list(
set(HelpCategory(topic.help_category) for topic in all_topics.values())
)
# all available help options - will be searched in order. We also check # the
# read-permission here.
@ -586,23 +594,26 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
for match_query in [query, f"{query}*", f"*{query}"]:
_, suggestions = help_search_with_index(
match_query, entries,
match_query,
entries,
suggestion_maxnum=self.suggestion_maxnum,
fields=search_fields
fields=search_fields,
)
if suggestions:
help_text += (
"\n... But matches where found within the help "
"texts of the suggestions below.")
suggestions = [self.strip_cmd_prefix(sugg, key_and_aliases)
for sugg in suggestions]
"texts of the suggestions below."
)
suggestions = [
self.strip_cmd_prefix(sugg, key_and_aliases) for sugg in suggestions
]
break
output = self.format_help_entry(
topic=None, # this will give a no-match style title
help_text=help_text,
suggested=suggestions,
click_topics=clickable_topics
click_topics=clickable_topics,
)
self.msg_help(output)
@ -612,14 +623,20 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
# no subtopics for categories - these are just lists of topics
category = match.key
category_lower = category.lower()
cmds_in_category = [key for key, cmd in cmd_help_topics.items()
if category_lower == cmd.help_category]
topics_in_category = [key for key, topic in file_db_help_topics.items()
if category_lower == topic.help_category]
output = self.format_help_index({category: cmds_in_category},
{category: topics_in_category},
title_lone_category=True,
click_topics=clickable_topics)
cmds_in_category = [
key for key, cmd in cmd_help_topics.items() if category_lower == cmd.help_category
]
topics_in_category = [
key
for key, topic in file_db_help_topics.items()
if category_lower == topic.help_category
]
output = self.format_help_index(
{category: cmds_in_category},
{category: topics_in_category},
title_lone_category=True,
click_topics=clickable_topics,
)
self.msg_help(output)
return
@ -674,7 +691,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
topic=topic,
help_text=f"No help entry found for '{checked_topic}'",
subtopics=subtopic_index,
click_topics=clickable_topics
click_topics=clickable_topics,
)
self.msg_help(output)
return
@ -702,7 +719,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS):
aliases=aliases,
subtopics=subtopic_index,
suggested=suggested,
click_topics=clickable_topics
click_topics=clickable_topics,
)
self.msg_help(output)
@ -829,15 +846,17 @@ class CmdSetHelp(CmdHelp):
# check if we have an old entry with the same name
cmd_help_topics, db_help_topics, file_help_topics = \
self.collect_topics(self.caller, mode='query')
cmd_help_topics, db_help_topics, file_help_topics = self.collect_topics(
self.caller, mode="query"
)
# db-help topics takes priority over file-help
file_db_help_topics = {**file_help_topics, **db_help_topics}
# commands take priority over the other types
all_topics = {**file_db_help_topics, **cmd_help_topics}
# get all categories
all_categories = list(set(
HelpCategory(topic.help_category) for topic in all_topics.values()))
all_categories = list(
set(HelpCategory(topic.help_category) for topic in all_topics.values())
)
# all available help options - will be searched in order. We also check # the
# read-permission here.
entries = list(all_topics.values()) + all_categories
@ -853,29 +872,35 @@ class CmdSetHelp(CmdHelp):
if match:
warning = None
if isinstance(match, HelpCategory):
warning = (f"'{querystr}' matches (or partially matches) the name of "
"help-category '{match.key}'. If you continue, your help entry will "
"take precedence and the category (or part of its name) *may* not "
"be usable for grouping help entries anymore.")
warning = (
f"'{querystr}' matches (or partially matches) the name of "
"help-category '{match.key}'. If you continue, your help entry will "
"take precedence and the category (or part of its name) *may* not "
"be usable for grouping help entries anymore."
)
elif inherits_from(match, "evennia.commands.command.Command"):
warning = (f"'{querystr}' matches (or partially matches) the key/alias of "
"Command '{match.key}'. Command-help take precedence over other "
"help entries so your help *may* be impossible to reach for those "
"with access to that command.")
warning = (
f"'{querystr}' matches (or partially matches) the key/alias of "
"Command '{match.key}'. Command-help take precedence over other "
"help entries so your help *may* be impossible to reach for those "
"with access to that command."
)
elif inherits_from(match, "evennia.help.filehelp.FileHelpEntry"):
warning = (f"'{querystr}' matches (or partially matches) the name/alias of the "
f"file-based help topic '{match.key}'. File-help entries cannot be "
"modified from in-game (they are files on-disk). If you continue, "
"your help entry may shadow the file-based one's name partly or "
"completely.")
warning = (
f"'{querystr}' matches (or partially matches) the name/alias of the "
f"file-based help topic '{match.key}'. File-help entries cannot be "
"modified from in-game (they are files on-disk). If you continue, "
"your help entry may shadow the file-based one's name partly or "
"completely."
)
if warning:
# show a warning for a clashing help-entry type. Even if user accepts this
# we don't break here since we may need to show warnings for other inputs.
# We don't count this as an old-entry hit because we can't edit these
# types of entries.
self.msg(f"|rWarning:\n|r{warning}|n")
repl = yield("|wDo you still want to continue? Y/[N]?|n")
if repl.lower() not in ('y', 'yes'):
repl = yield ("|wDo you still want to continue? Y/[N]?|n")
if repl.lower() not in ("y", "yes"):
self.msg("Aborted.")
return
else:
@ -897,7 +922,11 @@ class CmdSetHelp(CmdHelp):
helpentry = old_entry
else:
helpentry = create.create_help_entry(
topicstr, self.rhs, category=category, locks=lockstring, aliases=aliases,
topicstr,
self.rhs,
category=category,
locks=lockstring,
aliases=aliases,
)
self.caller.db._editing_help = helpentry
@ -976,6 +1005,4 @@ class CmdSetHelp(CmdHelp):
)
return
else:
self.msg(
f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin."
)
self.msg(f"Error when creating topic '{topicstr}'{aliastxt}! Contact an admin.")

View file

@ -593,13 +593,15 @@ class CmdService(COMMAND_DEFAULT_CLASS):
if delmode:
caller.msg("You cannot remove a core Evennia service (named 'Evennia*').")
return
string = ("|RYou seem to be shutting down a core Evennia "
"service (named 'Evennia*').\nNote that stopping "
"some TCP port services will *not* disconnect users "
"*already* connected on those ports, but *may* "
"instead cause spurious errors for them.\nTo safely "
"and permanently remove ports, change settings file "
"and restart the server.|n\n")
string = (
"|RYou seem to be shutting down a core Evennia "
"service (named 'Evennia*').\nNote that stopping "
"some TCP port services will *not* disconnect users "
"*already* connected on those ports, but *may* "
"instead cause spurious errors for them.\nTo safely "
"and permanently remove ports, change settings file "
"and restart the server.|n\n"
)
caller.msg(string)
if delmode:
@ -611,9 +613,11 @@ class CmdService(COMMAND_DEFAULT_CLASS):
try:
service.stopService()
except Exception as err:
caller.msg(f"|rErrors were reported when stopping this service{err}.\n"
"If there are remaining problems, try reloading "
"or rebooting the server.")
caller.msg(
f"|rErrors were reported when stopping this service{err}.\n"
"If there are remaining problems, try reloading "
"or rebooting the server."
)
caller.msg("|g... Stopped service '%s'.|n" % self.args)
return
@ -626,9 +630,11 @@ class CmdService(COMMAND_DEFAULT_CLASS):
try:
service.startService()
except Exception as err:
caller.msg(f"|rErrors were reported when starting this service{err}.\n"
"If there are remaining problems, try reloading the server, changing the "
"settings if it's a non-standard service.|n")
caller.msg(
f"|rErrors were reported when starting this service{err}.\n"
"If there are remaining problems, try reloading the server, changing the "
"settings if it's a non-standard service.|n"
)
caller.msg("|gService started.|n")
@ -973,8 +979,8 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
@staticmethod
def coll_date_func(task):
"""Replace regex characters in date string and collect deferred function name."""
t_comp_date = str(task[0]).replace('-', '/')
t_func_name = str(task[1]).split(' ')
t_comp_date = str(task[0]).replace("-", "/")
t_func_name = str(task[1]).split(" ")
t_func_mem_ref = t_func_name[3] if len(t_func_name) >= 4 else None
return t_comp_date, t_func_mem_ref
@ -994,19 +1000,19 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
# verify manipulating the correct task
task_args = _TASK_HANDLER.tasks.get(task_id, False)
if not task_args: # check if the task is still active
self.msg('Task completed while waiting for input.')
self.msg("Task completed while waiting for input.")
return
else:
# make certain a task with matching IDs has not been created
t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
if self.t_comp_date != t_comp_date or self.t_func_mem_ref != t_func_mem_ref:
self.msg('Task completed while waiting for input.')
self.msg("Task completed while waiting for input.")
return
# Do the action requested by command caller
action_return = self.task_action()
self.msg(f'{self.action_request} request completed.')
self.msg(f'The task function {self.action_request} returned: {action_return}')
self.msg(f"{self.action_request} request completed.")
self.msg(f"The task function {self.action_request} returned: {action_return}")
def func(self):
# get a reference of the global task handler
@ -1015,9 +1021,9 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
from evennia.scripts.taskhandler import TASK_HANDLER as _TASK_HANDLER
# handle no tasks active.
if not _TASK_HANDLER.tasks:
self.msg('There are no active tasks.')
self.msg("There are no active tasks.")
if self.switches or self.args:
self.msg('Likely the task has completed and been removed.')
self.msg("Likely the task has completed and been removed.")
return
# handle caller's request to manipulate a task(s)
@ -1033,8 +1039,8 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
# if the argument is a task id, proccess the action on a single task
if arg_is_id:
err_arg_msg = 'Switch and task ID are required when manipulating a task.'
task_comp_msg = 'Task completed while processing request.'
err_arg_msg = "Switch and task ID are required when manipulating a task."
task_comp_msg = "Task completed while processing request."
# handle missing arguments or switches
if not self.switches and self.lhs:
@ -1047,14 +1053,16 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
# handle task no longer existing
if not task.exists():
self.msg(f'Task {task_id} does not exist.')
self.msg(f"Task {task_id} does not exist.")
return
# get a reference of the function caller requested
switch_action = getattr(task, action_request, False)
if not switch_action:
self.msg(f'{self.switches[0]}, is not an acceptable task action or ' \
f'{task_comp_msg.lower()}')
self.msg(
f"{self.switches[0]}, is not an acceptable task action or "
f"{task_comp_msg.lower()}"
)
# verify manipulating the correct task
if task_id in _TASK_HANDLER.tasks:
@ -1064,25 +1072,29 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
return
else:
t_comp_date, t_func_mem_ref = self.coll_date_func(task_args)
t_func_name = str(task_args[1]).split(' ')
t_func_name = str(task_args[1]).split(" ")
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
if task.exists(): # make certain the task has not been called yet.
prompt = (f'{action_request.capitalize()} task {task_id} with completion date '
f'{t_comp_date} ({t_func_name}) {{options}}?')
no_msg = f'No {action_request} processed.'
prompt = (
f"{action_request.capitalize()} task {task_id} with completion date "
f"{t_comp_date} ({t_func_name}) {{options}}?"
)
no_msg = f"No {action_request} processed."
# record variables for use in do_task_action method
self.task_id = task_id
self.t_comp_date = t_comp_date
self.t_func_mem_ref = t_func_mem_ref
self.task_action = switch_action
self.action_request = action_request
ask_yes_no(self.caller,
prompt=prompt,
yes_action=self.do_task_action,
no_action=no_msg,
default="Y",
allow_abort=True)
ask_yes_no(
self.caller,
prompt=prompt,
yes_action=self.do_task_action,
no_action=no_msg,
default="Y",
allow_abort=True,
)
return True
else:
self.msg(task_comp_msg)
@ -1102,7 +1114,7 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
# call requested action on all tasks with the function name
for task_id, task_args in current_tasks.items():
t_func_name = str(task_args[1]).split(' ')
t_func_name = str(task_args[1]).split(" ")
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
# skip this task if it is not for the function desired
if arg_func_name != t_func_name:
@ -1112,33 +1124,39 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
switch_action = getattr(task, action_request, False)
if switch_action:
action_return = switch_action()
self.msg(f'Task action {action_request} completed on task ID {task_id}.')
self.msg(f'The task function {action_request} returned: {action_return}')
self.msg(f"Task action {action_request} completed on task ID {task_id}.")
self.msg(f"The task function {action_request} returned: {action_return}")
# provide a message if not tasks of the function name was found
if not name_match_found:
self.msg(f'No tasks deferring function name {arg_func_name} found.')
self.msg(f"No tasks deferring function name {arg_func_name} found.")
return
return True
# check if an maleformed request was created
elif self.switches or self.lhs:
self.msg('Task command misformed.')
self.msg('Proper format tasks[/switch] [function name or task id]')
self.msg("Task command misformed.")
self.msg("Proper format tasks[/switch] [function name or task id]")
return
# No task manupilation requested, build a table of tasks and display it
# get the width of screen in characters
width = self.client_width()
# create table header and list to hold tasks data and actions
tasks_header = ('Task ID', 'Completion Date', 'Function', 'Arguments', 'KWARGS',
'persistent')
tasks_header = (
"Task ID",
"Completion Date",
"Function",
"Arguments",
"KWARGS",
"persistent",
)
# empty list of lists, the size of the header
tasks_list = [list() for i in range(len(tasks_header))]
for task_id, task in _TASK_HANDLER.tasks.items():
# collect data from the task
t_comp_date, t_func_mem_ref = self.coll_date_func(task)
t_func_name = str(task[1]).split(' ')
t_func_name = str(task[1]).split(" ")
t_func_name = t_func_name[1] if len(t_func_name) >= 2 else None
t_args = str(task[2])
t_kwargs = str(task[3])
@ -1148,8 +1166,9 @@ class CmdTasks(COMMAND_DEFAULT_CLASS):
for i in range(len(tasks_header)):
tasks_list[i].append(task_data[i])
# create and display the table
tasks_table = EvTable(*tasks_header, table=tasks_list, maxwidth=width, border='cells',
align='center')
actions = (f'/{switch}' for switch in self.switch_options)
tasks_table = EvTable(
*tasks_header, table=tasks_list, maxwidth=width, border="cells", align="center"
)
actions = (f"/{switch}" for switch in self.switch_options)
helptxt = f"\nActions: {iter_to_str(actions)}"
self.msg(str(tasks_table) + helptxt)

File diff suppressed because it is too large Load diff

View file

@ -201,13 +201,17 @@ class CmdUnconnectedCreate(COMMAND_DEFAULT_CLASS):
non_normalized_username = username
username = Account.normalize_username(username)
if non_normalized_username != username:
session.msg("Note: your username was normalized to strip spaces and remove characters "
"that could be visually confusing.")
session.msg(
"Note: your username was normalized to strip spaces and remove characters "
"that could be visually confusing."
)
# have the user verify their new account was what they intended
answer = yield(f"You want to create an account '{username}' with password '{password}'."
"\nIs this what you intended? [Y]/N?")
if answer.lower() in ('n', 'no'):
answer = yield (
f"You want to create an account '{username}' with password '{password}'."
"\nIs this what you intended? [Y]/N?"
)
if answer.lower() in ("n", "no"):
session.msg("Aborted. If your user name contains spaces, surround it by quotes.")
return
@ -344,7 +348,7 @@ class CmdUnconnectedEncoding(COMMAND_DEFAULT_CLASS):
If you don't submit an encoding, the current encoding will be displayed
instead.
"""
"""
key = "encoding"
aliases = "encode"

View file

@ -339,7 +339,7 @@ class TestOptionTransferTrue(TestCase):
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, high prio
cmdset_f = d + c + b + a # reverse, high prio
self.assertTrue(cmdset_f.no_exits)
self.assertTrue(cmdset_f.no_objs)
self.assertTrue(cmdset_f.no_channels)
@ -407,7 +407,7 @@ class TestOptionTransferTrue(TestCase):
c.priority = 1
d.priority = 2
c.no_exits = False
c.no_channels = None # passthrough
c.no_channels = None # passthrough
b.no_objs = False
d.duplicates = False
# higher-prio sets will change the option up the chain
@ -639,7 +639,7 @@ class TestOptionTransferFalse(TestCase):
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = d + c + b + a # reverse, high prio
cmdset_f = d + c + b + a # reverse, high prio
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
@ -663,7 +663,7 @@ class TestOptionTransferFalse(TestCase):
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
cmdset_f = a + b + c + d # forward, high prio, never happens
cmdset_f = a + b + c + d # forward, high prio, never happens
self.assertFalse(cmdset_f.no_exits)
self.assertFalse(cmdset_f.no_objs)
self.assertFalse(cmdset_f.no_channels)
@ -707,7 +707,7 @@ class TestOptionTransferFalse(TestCase):
c.priority = 1
d.priority = 2
c.no_exits = True
c.no_channels = None # passthrough
c.no_channels = None # passthrough
b.no_objs = True
d.duplicates = True
# higher-prio sets will change the option up the chain
@ -908,6 +908,7 @@ class TestOptionTransferReplace(TestCase):
"""
Test option transfer through more complex merge types.
"""
def setUp(self):
super().setUp()
self.cmdset_a = _CmdSetA()
@ -1182,7 +1183,6 @@ class TestCmdSetNesting(BaseEvenniaTest):
"""
def test_nest(self):
class CmdA(Command):
key = "a"

View file

@ -107,7 +107,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
"""
if not self._log_file:
self._log_file = self.attributes.get(
"log_file", self.log_file.format(channelname=self.key.lower()))
"log_file", self.log_file.format(channelname=self.key.lower())
)
return self._log_file
def set_log_filename(self, filename):
@ -455,8 +456,13 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
# needing to use the `channel` command explicitly.
msg_nick_pattern = self.channel_msg_nick_pattern.format(alias=alias)
msg_nick_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key)
user.nicks.add(msg_nick_pattern, msg_nick_replacement, category="inputline",
pattern_is_regex=True, **kwargs)
user.nicks.add(
msg_nick_pattern,
msg_nick_replacement,
category="inputline",
pattern_is_regex=True,
**kwargs,
)
if chan_key != alias:
# this allows for using the alias for general channel lookups
@ -546,7 +552,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
if not bypass_mute:
receivers = [receiver for receiver in receivers if receiver not in self.mutelist]
send_kwargs = {'senders': senders, 'bypass_mute': bypass_mute, **kwargs}
send_kwargs = {"senders": senders, "bypass_mute": bypass_mute, **kwargs}
# pre-send hook
message = self.at_pre_msg(message, **send_kwargs)
@ -826,27 +832,37 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
# TODO Evennia 1.0+ removed hooks. Remove in 1.1.
def message_transform(self, *args, **kwargs):
raise RuntimeError("Channel.message_transform is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead.")
raise RuntimeError(
"Channel.message_transform is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead."
)
def distribute_message(self, msgobj, online=False, **kwargs):
raise RuntimeError("Channel.distribute_message is no longer used in 1.0+.")
def format_senders(self, senders=None, **kwargs):
raise RuntimeError("Channel.format_senders is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead.")
raise RuntimeError(
"Channel.format_senders is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead."
)
def pose_transform(self, msgobj, sender_string, **kwargs):
raise RuntimeError("Channel.pose_transform is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead.")
raise RuntimeError(
"Channel.pose_transform is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead."
)
def format_external(self, msgobj, senders, emit=False, **kwargs):
raise RuntimeError("Channel.format_external is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead.")
raise RuntimeError(
"Channel.format_external is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead."
)
def format_message(self, msgobj, emit=False, **kwargs):
raise RuntimeError("Channel.format_message is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead.")
raise RuntimeError(
"Channel.format_message is no longer used in 1.0+. "
"Use Account/Object.at_pre_channel_msg instead."
)
def pre_send_message(self, msg, **kwargs):
raise RuntimeError("Channel.pre_send_message was renamed to Channel.at_pre_msg.")

View file

@ -215,7 +215,7 @@ class MsgManager(TypedObjectManager):
return self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)
elif typ == "object":
return self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj)
elif typ == 'script':
elif typ == "script":
return self.filter(db_receivers_scripts=obj)
else:
raise CommError
@ -257,7 +257,7 @@ class MsgManager(TypedObjectManager):
sender_restrict = Q(db_sender_accounts__pk=spk) & ~Q(db_hide_from_accounts__pk=spk)
elif styp == "object":
sender_restrict = Q(db_sender_objects__pk=spk) & ~Q(db_hide_from_objects__pk=spk)
elif styp == 'script':
elif styp == "script":
sender_restrict = Q(db_sender_scripts__pk=spk)
else:
sender_restrict = Q()
@ -266,16 +266,16 @@ class MsgManager(TypedObjectManager):
if receiver:
rpk = receiver.pk
if rtyp == "account":
receiver_restrict = (
Q(db_receivers_accounts__pk=rpk) & ~Q(db_hide_from_accounts__pk=rpk))
receiver_restrict = Q(db_receivers_accounts__pk=rpk) & ~Q(db_hide_from_accounts__pk=rpk)
elif rtyp == "object":
receiver_restrict = Q(db_receivers_objects__pk=rpk) & ~Q(db_hide_from_objects__pk=rpk)
elif rtyp == 'script':
elif rtyp == "script":
receiver_restrict = Q(db_receivers_scripts__pk=rpk)
elif rtyp == "channel":
raise DeprecationWarning(
"Msg.objects.search don't accept channel recipients since "
"Channels no longer accepts Msg objects.")
"Channels no longer accepts Msg objects."
)
else:
receiver_restrict = Q()
# filter by full text
@ -289,8 +289,9 @@ class MsgManager(TypedObjectManager):
# back-compatibility alias
message_search = search_message
def create_message(self, senderobj, message, receivers=None, locks=None, tags=None,
header=None, **kwargs):
def create_message(
self, senderobj, message, receivers=None, locks=None, tags=None, header=None, **kwargs
):
"""
Create a new communication Msg. Msgs represent a unit of
database-persistent communication between entites.
@ -315,7 +316,7 @@ class MsgManager(TypedObjectManager):
it's up to the command definitions to limit this as desired.
"""
if 'channels' in kwargs:
if "channels" in kwargs:
raise DeprecationWarning(
"create_message() does not accept 'channel' kwarg anymore "
"- channels no longer accept Msg objects."
@ -339,6 +340,7 @@ class MsgManager(TypedObjectManager):
new_message.save()
return new_message
#
# Channel manager
#

View file

@ -104,7 +104,7 @@ class Msg(SharedMemoryModel):
blank=True,
db_index=True,
help_text="Identifier for single external sender, for use with senders "
"not represented by a regular database model."
"not represented by a regular database model.",
)
db_receivers_accounts = models.ManyToManyField(
@ -137,7 +137,7 @@ class Msg(SharedMemoryModel):
blank=True,
db_index=True,
help_text="Identifier for single external receiver, for use with recievers "
"not represented by a regular database model."
"not represented by a regular database model.",
)
# header could be used for meta-info about the message if your system needs
@ -286,7 +286,7 @@ class Msg(SharedMemoryModel):
"""
if isinstance(receivers, str):
self.db_receiver_external = receivers
self.save(update_fields=['db_receiver_external'])
self.save(update_fields=["db_receiver_external"])
return
for receiver in make_iter(receivers):

View file

@ -21,7 +21,9 @@ class ObjectCreationTest(BaseEvenniaTest):
class ChannelWholistTests(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.default_channel, _ = DefaultChannel.create("coffeetalk", description="A place to talk about coffee.")
self.default_channel, _ = DefaultChannel.create(
"coffeetalk", description="A place to talk about coffee."
)
self.default_channel.connect(self.obj1)
def test_wholist_shows_subscribed_objects(self):
@ -31,7 +33,9 @@ class ChannelWholistTests(BaseEvenniaTest):
def test_wholist_shows_none_when_empty(self):
# No one hates dogs
empty_channel, _ = DefaultChannel.create("doghaters", description="A place where dog haters unite.")
empty_channel, _ = DefaultChannel.create(
"doghaters", description="A place where dog haters unite."
)
expected = "<None>"
result = empty_channel.wholist
self.assertEqual(expected, result)

View file

@ -165,7 +165,9 @@ def check_location(storage):
correct = storage.location.lstrip("/")
raise ImproperlyConfigured(
"{}.location cannot begin with a leading slash. Found '{}'. Use '{}' instead.".format(
storage.__class__.__name__, storage.location, correct,
storage.__class__.__name__,
storage.location,
correct,
)
)

View file

@ -121,7 +121,11 @@ class S3Boto3StorageTests(S3Boto3TestCase):
obj = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with(
content, ExtraArgs={"ContentType": "text/plain", "ACL": self.storage.default_acl,}
content,
ExtraArgs={
"ContentType": "text/plain",
"ACL": self.storage.default_acl,
},
)
def test_storage_save_with_acl(self):
@ -136,7 +140,11 @@ class S3Boto3StorageTests(S3Boto3TestCase):
obj = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with(
content, ExtraArgs={"ContentType": "text/plain", "ACL": "private",}
content,
ExtraArgs={
"ContentType": "text/plain",
"ACL": "private",
},
)
def test_content_type(self):
@ -151,7 +159,11 @@ class S3Boto3StorageTests(S3Boto3TestCase):
obj = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with(
content, ExtraArgs={"ContentType": "image/jpeg", "ACL": self.storage.default_acl,}
content,
ExtraArgs={
"ContentType": "image/jpeg",
"ACL": self.storage.default_acl,
},
)
def test_storage_save_gzipped(self):
@ -379,7 +391,10 @@ class S3Boto3StorageTests(S3Boto3TestCase):
self.assertEqual(uploaded_content, written_content)
multipart.complete.assert_called_once_with(
MultipartUpload={
"Parts": [{"ETag": "123", "PartNumber": 1}, {"ETag": "456", "PartNumber": 2},]
"Parts": [
{"ETag": "123", "PartNumber": 1},
{"ETag": "456", "PartNumber": 2},
]
}
)
@ -394,7 +409,10 @@ class S3Boto3StorageTests(S3Boto3TestCase):
)
self.storage._get_or_create_bucket("testbucketname")
Bucket.create.assert_called_once_with(
ACL="public-read", CreateBucketConfiguration={"LocationConstraint": "sa-east-1",}
ACL="public-read",
CreateBucketConfiguration={
"LocationConstraint": "sa-east-1",
},
)
def test_auto_creating_bucket_with_acl(self):
@ -409,22 +427,28 @@ class S3Boto3StorageTests(S3Boto3TestCase):
)
self.storage._get_or_create_bucket("testbucketname")
Bucket.create.assert_called_once_with(
ACL="public-read", CreateBucketConfiguration={"LocationConstraint": "sa-east-1",}
ACL="public-read",
CreateBucketConfiguration={
"LocationConstraint": "sa-east-1",
},
)
def test_storage_exists(self):
self.assertTrue(self.storage.exists("file.txt"))
self.storage.connection.meta.client.head_object.assert_called_with(
Bucket=self.storage.bucket_name, Key="file.txt",
Bucket=self.storage.bucket_name,
Key="file.txt",
)
def test_storage_exists_false(self):
self.storage.connection.meta.client.head_object.side_effect = ClientError(
{"Error": {"Code": "404", "Message": "Not Found"}}, "HeadObject",
{"Error": {"Code": "404", "Message": "Not Found"}},
"HeadObject",
)
self.assertFalse(self.storage.exists("file.txt"))
self.storage.connection.meta.client.head_object.assert_called_with(
Bucket=self.storage.bucket_name, Key="file.txt",
Bucket=self.storage.bucket_name,
Key="file.txt",
)
def test_storage_exists_doesnt_create_bucket(self):
@ -445,8 +469,14 @@ class S3Boto3StorageTests(S3Boto3TestCase):
# 4.txt
pages = [
{
"CommonPrefixes": [{"Prefix": "some"}, {"Prefix": "other"},],
"Contents": [{"Key": "2.txt"}, {"Key": "4.txt"},],
"CommonPrefixes": [
{"Prefix": "some"},
{"Prefix": "other"},
],
"Contents": [
{"Key": "2.txt"},
{"Key": "4.txt"},
],
},
]
@ -465,7 +495,14 @@ class S3Boto3StorageTests(S3Boto3TestCase):
# some/path/1.txt
# some/2.txt
pages = [
{"CommonPrefixes": [{"Prefix": "some/path"},], "Contents": [{"Key": "some/2.txt"},],},
{
"CommonPrefixes": [
{"Prefix": "some/path"},
],
"Contents": [
{"Key": "some/2.txt"},
],
},
]
paginator = mock.MagicMock()

View file

@ -4,7 +4,7 @@ Building menu tests.
"""
from evennia.commands.default.tests import BaseEvenniaCommandTest
from . building_menu import BuildingMenu, CmdNoMatch
from .building_menu import BuildingMenu, CmdNoMatch
class Submenu(BuildingMenu):

View file

@ -151,8 +151,9 @@ def realtime_to_gametime(secs=0, mins=0, hrs=0, days=1, weeks=1, months=1, yrs=0
"""
if days <= 0 or weeks <= 0 or months <= 0:
raise ValueError("realtime_to_gametime: days/weeks/months cannot be set <= 0, "
"they start from 1.")
raise ValueError(
"realtime_to_gametime: days/weeks/months cannot be set <= 0, " "they start from 1."
)
# days/weeks/months start from 1, we need to adjust them to work mathematically.
days, weeks, months = days - 1, weeks - 1, months - 1

View file

@ -31,17 +31,27 @@ class TestEventHandler(BaseEvenniaTest):
def setUp(self):
"""Create the event handler."""
super().setUp()
self.handler = create_script("evennia.contrib.base_systems.ingame_python.scripts.EventHandler")
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
def tearDown(self):
@ -253,17 +263,27 @@ class TestCmdCallback(BaseEvenniaCommandTest):
def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script("evennia.contrib.base_systems.ingame_python.scripts.EventHandler")
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
def tearDown(self):
@ -432,17 +452,27 @@ class TestDefaultCallbacks(BaseEvenniaCommandTest):
def setUp(self):
"""Create the callback handler."""
super().setUp()
self.handler = create_script("evennia.contrib.base_systems.ingame_python.scripts.EventHandler")
self.handler = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.EventHandler"
)
# Copy old events if necessary
if OLD_EVENTS:
self.handler.ndb.events = dict(OLD_EVENTS)
# Alter typeclasses
self.char1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.char2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter")
self.room1.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.room2.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom")
self.char1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.char2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventCharacter"
)
self.room1.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.room2.swap_typeclass(
"evennia.contrib.base_systems.ingame_python.typeclasses.EventRoom"
)
self.exit.swap_typeclass("evennia.contrib.base_systems.ingame_python.typeclasses.EventExit")
def tearDown(self):

View file

@ -11,7 +11,11 @@ from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
from evennia import ScriptDB
from evennia.utils.utils import delay, inherits_from, lazy_property
from evennia.contrib.base_systems.ingame_python.callbackhandler import CallbackHandler
from evennia.contrib.base_systems.ingame_python.utils import register_events, time_event, phrase_event
from evennia.contrib.base_systems.ingame_python.utils import (
register_events,
time_event,
phrase_event,
)
# Character help
CHARACTER_CAN_DELETE = """

View file

@ -173,7 +173,9 @@ def time_event(obj, event_name, number, parameters):
"""
seconds, usual, key = get_next_wait(parameters)
script = create_script(
"evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript", interval=seconds, obj=obj
"evennia.contrib.base_systems.ingame_python.scripts.TimeEventScript",
interval=seconds,
obj=obj,
)
script.key = key
script.desc = "event on {}".format(key)

View file

@ -31,8 +31,7 @@ _GUEST_ENABLED = settings.GUEST_ENABLED
_ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)
_ACCOUNT_HELP = (
"Enter a new or existing login name.")
_ACCOUNT_HELP = "Enter a new or existing login name."
_PASSWORD_HELP = (
"Password should be a minimum of 8 characters (preferably longer) and "
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."

View file

@ -140,7 +140,7 @@ class CmdDelCom(CmdChannel):
account_caller = True
def func(self):
"""Implementing the command. """
"""Implementing the command."""
caller = self.caller
@ -207,8 +207,7 @@ class CmdAllCom(CmdChannel):
args = self.args
if not args:
subscribed, available = self.list_channels()
self.msg(
"\n|wAvailable channels:\n{table}")
self.msg("\n|wAvailable channels:\n{table}")
return
return
@ -353,7 +352,7 @@ class CmdCBoot(CmdChannel):
self.msg(string)
return
success, err = self.boot_user(target, quiet='quiet' in self.switches)
success, err = self.boot_user(target, quiet="quiet" in self.switches)
if success:
self.msg(f"Booted {target.key} from {channel.key}")
logger.log_sec(

View file

@ -343,7 +343,7 @@ class EvscaperoomObject(DefaultObject):
# Evennia hooks
def return_appearance(self, looker, **kwargs):
""" Could be modified per state. We generally don't worry about the
"""Could be modified per state. We generally don't worry about the
contents of the object by default.
"""

View file

@ -667,8 +667,8 @@ class CraftingRecipe(CraftingRecipeBase):
consumable_kwargs = {}
if location:
tool_kwargs['location'] = location
consumable_kwargs['location'] = location
tool_kwargs["location"] = location
consumable_kwargs["location"] = location
tool_key = tool_kwargs.pop("key", None)
cons_key = consumable_kwargs.pop("key", None)
@ -966,6 +966,7 @@ class CmdCraft(Command):
things in the current location, like a furnace, windmill or anvil.
"""
key = "craft"
locks = "cmd:all()"
help_category = "General"

View file

@ -78,6 +78,7 @@ from .crafting import craft, CraftingRecipe, CraftingValidationError
# Sword recipe
# ------------------------------------------------------------
class PigIronRecipe(CraftingRecipe):
"""
Pig iron is a high-carbon result of melting iron in a blast furnace.
@ -331,9 +332,9 @@ class SwordRecipe(_SwordSmithingBaseRecipe):
exact_consumable_order = True
#------------------------------------------------------------
# ------------------------------------------------------------
# Recipes for spell casting
#------------------------------------------------------------
# ------------------------------------------------------------
class _MagicRecipe(CraftingRecipe):
@ -348,6 +349,7 @@ class _MagicRecipe(CraftingRecipe):
We also assume that the crafter has skills set on itself as plain Attributes.
"""
name = ""
# all spells require a spellbook and a wand (so there!)
tool_tags = ["spellbook", "wand"]
@ -390,15 +392,15 @@ class _MagicRecipe(CraftingRecipe):
skill_value = crafter.attributes.get(skill_name)
if skill_value is None or skill_value < min_value:
self.msg(self.error_too_low_skill_level.format(skill_name=skill_name,
spell=self.name))
self.msg(
self.error_too_low_skill_level.format(skill_name=skill_name, spell=self.name)
)
raise CraftingValidationError
# get the value of the skill to roll
self.skill_roll_value = self.crafter.attributes.get(self.skill_roll)
if self.skill_roll_value is None:
self.msg(self.error_no_skill_roll.format(skill_name=self.skill_roll,
spell=self.name))
self.msg(self.error_no_skill_roll.format(skill_name=self.skill_roll, spell=self.name))
raise CraftingValidationError
def do_craft(self, **kwargs):
@ -446,12 +448,13 @@ class FireballRecipe(_MagicRecipe):
need to be created to understand what they mean when used.
"""
name = "fireball"
skill_requirements = [('firemagic', 10)] # skill 'firemagic' lvl 10 or higher
skill_requirements = [("firemagic", 10)] # skill 'firemagic' lvl 10 or higher
skill_roll = "firemagic"
success_message = "A ball of flame appears!"
desired_effects = [('target_fire_damage', 25), ('ranged_attack', -2), ('mana_cost', 12)]
failure_effects = [('self_fire_damage', 5), ('mana_cost', 5)]
desired_effects = [("target_fire_damage", 25), ("ranged_attack", -2), ("mana_cost", 12)]
failure_effects = [("self_fire_damage", 5), ("mana_cost", 5)]
class HealingRecipe(_MagicRecipe):
@ -462,11 +465,12 @@ class HealingRecipe(_MagicRecipe):
need to be created to understand what they mean.
"""
name = "heal"
skill_requirements = [('bodymagic', 5), ("empathy", 10)]
skill_requirements = [("bodymagic", 5), ("empathy", 10)]
skill_roll = "bodymagic"
success_message = "You successfully extend your healing aura."
desired_effects = [('healing', 15), ('mana_cost', 5)]
desired_effects = [("healing", 15), ("mana_cost", 5)]
failure_effects = []
@ -478,7 +482,8 @@ class CmdCast(Command):
cast <spell> <target>
"""
key = 'cast'
key = "cast"
def parse(self):
"""
@ -488,8 +493,8 @@ class CmdCast(Command):
"""
args = self.args.strip().lower()
target = None
if ' ' in args:
self.spellname, *target = args.split(' ', 1)
if " " in args:
self.spellname, *target = args.split(" ", 1)
else:
self.spellname = args
@ -512,8 +517,9 @@ class CmdCast(Command):
try:
# if this completes without an exception, the caster will have
# a new magic_effect set on themselves, ready to use or apply in some way.
success, effects = craft(self.caller, self.spellname, *possible_tools,
raise_exception=True)
success, effects = craft(
self.caller, self.spellname, *possible_tools, raise_exception=True
)
except CraftingValidationError:
return
except KeyError:
@ -527,5 +533,7 @@ class CmdCast(Command):
# (which could be yourself) by a number of health points given by the recipe.
effect_txt = ", ".join(f"{eff[0]}({eff[1]})" for eff in effects)
success_txt = "|gsucceeded|n" if success else "|rfailed|n"
self.caller.msg(f"Casting the spell {self.spellname} on {self.target} {success_txt}, "
f"causing the following effects: {effect_txt}.")
self.caller.msg(
f"Casting the spell {self.spellname} on {self.target} {success_txt}, "
f"causing the following effects: {effect_txt}."
)

View file

@ -165,7 +165,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
pass
def test_error_format(self):
"""Test the automatic error formatter """
"""Test the automatic error formatter"""
recipe = _MockRecipe(
self.crafter, self.tool1, self.tool2, self.cons1, self.cons2, self.cons3
)
@ -428,7 +428,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
self.assertIsNotNone(self.tool2.pk)
def test_craft_tool_order__fail(self):
"""Strict tool-order recipe fail """
"""Strict tool-order recipe fail"""
recipe = _MockRecipe(
self.crafter, self.tool2, self.tool1, self.cons1, self.cons2, self.cons3
)
@ -451,7 +451,7 @@ class TestCraftingRecipe(BaseEvenniaTestCase):
self.assertIsNotNone(self.tool2.pk)
def test_craft_cons_order__fail(self):
"""Strict tool-order recipe fail """
"""Strict tool-order recipe fail"""
recipe = _MockRecipe(
self.crafter, self.tool1, self.tool2, self.cons3, self.cons2, self.cons1
)
@ -653,7 +653,10 @@ class TestCraftSword(BaseEvenniaTestCase):
@mock.patch("evennia.contrib.game_systems.crafting.crafting._load_recipes", new=mock.MagicMock())
@mock.patch("evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES", new={"testrecipe": _MockRecipe})
@mock.patch(
"evennia.contrib.game_systems.crafting.crafting._RECIPE_CLASSES",
new={"testrecipe": _MockRecipe},
)
@override_settings(CRAFT_RECIPE_MODULES=[])
class TestCraftCommand(BaseEvenniaCommandTest):
"""Test the crafting command"""

View file

@ -22,7 +22,9 @@ class TestGenderSub(BaseEvenniaCommandTest):
self.assertEqual(
gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender"
)
with patch("evennia.contrib.game_systems.gendersub.gendersub.DefaultCharacter.msg") as mock_msg:
with patch(
"evennia.contrib.game_systems.gendersub.gendersub.DefaultCharacter.msg"
) as mock_msg:
char.db.gender = "female"
char.msg("Test |p gender")
mock_msg.assert_called_with("Test her gender", from_obj=None, session=None)

View file

@ -304,6 +304,7 @@ class TBBasicCharacter(DefaultCharacter):
A character able to participate in turn-based combat. Has attributes for current
and maximum HP, and access to combat commands.
"""
rules = COMBAT_RULES
def at_object_creation(self):

View file

@ -243,6 +243,7 @@ class TBEquipTurnHandler(tb_basic.TBBasicTurnHandler):
Fights persist until only one participant is left with any HP or all
remaining participants choose to end the combat with the 'disengage' command.
"""
rules = COMBAT_RULES
@ -258,6 +259,7 @@ class TBEWeapon(DefaultObject):
A weapon which can be wielded in combat with the 'wield' command.
"""
rules = COMBAT_RULES
def at_object_creation(self):
@ -513,8 +515,9 @@ class CmdWield(Command):
weapon = self.caller.search(self.args, candidates=self.caller.contents)
if not weapon:
return
if not weapon.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon",
exact=True):
if not weapon.is_typeclass(
"evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", exact=True
):
self.caller.msg("That's not a weapon!")
# Remember to update the path to the weapon typeclass if you move this module!
return
@ -597,8 +600,9 @@ class CmdDon(Command):
armor = self.caller.search(self.args, candidates=self.caller.contents)
if not armor:
return
if not armor.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor",
exact=True):
if not armor.is_typeclass(
"evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", exact=True
):
self.caller.msg("That's not armor!")
# Remember to update the path to the armor typeclass if you move this module!
return

View file

@ -104,7 +104,6 @@ COMBAT FUNCTIONS START HERE
class ItemCombatRules(tb_basic.BasicCombatRules):
def get_attack(self, attacker, defender):
"""
Returns a value for an attack roll.
@ -699,7 +698,7 @@ AMULET_OF_MIGHT = {
AMULET_OF_WEAKNESS = {
"key": "The Amulet of Weakness",
"desc": "The one who holds this amulet can call upon its power to gain great weakness. "
"It's not a terribly useful artifact.",
"It's not a terribly useful artifact.",
"item_func": "add_condition",
"item_selfonly": True,
"item_kwargs": {"conditions": [("Damage Down", 3), ("Accuracy Down", 3), ("Defense Down", 3)]},

View file

@ -93,8 +93,8 @@ These functions also all accept **kwargs, and how these are used is specified
in the docstring for each function.
"""
class MagicCombatRules(tb_basic.BasicCombatRules):
class MagicCombatRules(tb_basic.BasicCombatRules):
def spell_healing(self, caster, spell_name, targets, cost, **kwargs):
"""
Spell that restores HP to a target or targets.
@ -325,11 +325,7 @@ SPELLS = {
"attack_name": ("A jet of flame", "jets of flame"),
"damage_range": (25, 35),
},
"cure wounds": {
"spellfunc": COMBAT_RULES.spell_healing,
"target": "anychar",
"cost": 5
},
"cure wounds": {"spellfunc": COMBAT_RULES.spell_healing, "target": "anychar", "cost": 5},
"mass cure wounds": {
"spellfunc": COMBAT_RULES.spell_healing,
"target": "anychar",
@ -376,6 +372,7 @@ class TBMagicCharacter(tb_basic.TBBasicCharacter):
and maximum HP, access to combat commands and magic.
"""
rules = COMBAT_RULES
def at_object_creation(self):

View file

@ -122,7 +122,6 @@ COMBAT FUNCTIONS START HERE
class RangedCombatRules(tb_basic.BasicCombatRules):
def get_attack(self, attacker, defender, attack_type):
"""
Returns a value for an attack roll.
@ -154,7 +153,7 @@ class RangedCombatRules(tb_basic.BasicCombatRules):
attack_value -= 15
return attack_value
def get_defense(self, attacker, defender, attack_type='melee'):
def get_defense(self, attacker, defender, attack_type="melee"):
"""
Returns a value for defense, which an attack roll must equal or exceed in order
for an attack to hit.
@ -284,8 +283,9 @@ class RangedCombatRules(tb_basic.BasicCombatRules):
if thing != mover and thing != target:
# Move away from each object closer to the target than you, if it's also closer to
# you than you are to the target.
if (self.get_range(mover, thing) >= self.get_range(target, thing)
and self.get_range(mover, thing) < self.get_range(mover, target)):
if self.get_range(mover, thing) >= self.get_range(target, thing) and self.get_range(
mover, thing
) < self.get_range(mover, target):
self.distance_inc(mover, thing)
# Move away from anything your target is engaged with
if self.get_range(target, thing) == 0:
@ -296,8 +296,9 @@ class RangedCombatRules(tb_basic.BasicCombatRules):
# Then, move away from your target.
self.distance_inc(mover, target)
def resolve_attack(self, attacker, defender, attack_value=None, defense_value=None,
attack_type='melee'):
def resolve_attack(
self, attacker, defender, attack_value=None, defense_value=None, attack_type="melee"
):
"""
Resolves an attack and outputs the result.
@ -496,6 +497,7 @@ class TBRangeCharacter(tb_basic.TBBasicCharacter):
A character able to participate in turn-based combat. Has attributes for current
and maximum HP, and access to combat commands.
"""
rules = COMBAT_RULES
@ -797,15 +799,17 @@ class CmdShoot(Command):
in_melee = []
for target in attacker.db.combat_range:
# Object is engaged and has HP
if (self.rules.get_range(attacker, defender) == 0
and target.db.hp and target != self.caller):
if (
self.rules.get_range(attacker, defender) == 0
and target.db.hp
and target != self.caller
):
in_melee.append(target) # Add to list of targets in melee
if len(in_melee) > 0:
self.caller.msg(
"You can't shoot because there are fighters engaged with you (%s) - you need "
"to retreat! (see: help withdraw)"
% ", ".join(obj.key for obj in in_melee)
"to retreat! (see: help withdraw)" % ", ".join(obj.key for obj in in_melee)
)
return

View file

@ -133,8 +133,9 @@ class TestTurnBattleBasicFunc(BaseEvenniaTest):
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_basic.COMBAT_RULES.resolve_attack(self.attacker, self.defender,
attack_value=20, defense_value=10)
tb_basic.COMBAT_RULES.resolve_attack(
self.attacker, self.defender, attack_value=20, defense_value=10
)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True
@ -227,7 +228,9 @@ class TestTurnBattleEquipFunc(BaseEvenniaTest):
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_equip.COMBAT_RULES.resolve_attack(self.attacker, self.defender, attack_value=20, defense_value=10)
tb_equip.COMBAT_RULES.resolve_attack(
self.attacker, self.defender, attack_value=20, defense_value=10
)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True
@ -305,12 +308,14 @@ class TestTurnBattleRangeFunc(BaseEvenniaTest):
initiative = tb_range.COMBAT_RULES.roll_init(self.attacker)
self.assertTrue(initiative >= 0 and initiative <= 1000)
# Attack roll
attack_roll = tb_range.COMBAT_RULES.get_attack(self.attacker, self.defender,
attack_type="test")
attack_roll = tb_range.COMBAT_RULES.get_attack(
self.attacker, self.defender, attack_type="test"
)
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
# Defense roll
defense_roll = tb_range.COMBAT_RULES.get_defense(self.attacker, self.defender,
attack_type="test")
defense_roll = tb_range.COMBAT_RULES.get_defense(
self.attacker, self.defender, attack_type="test"
)
self.assertTrue(defense_roll == 50)
# Damage roll
damage_roll = tb_range.COMBAT_RULES.get_damage(self.attacker, self.defender)
@ -436,8 +441,9 @@ class TestTurnBattleItemsFunc(BaseEvenniaTest):
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_items.COMBAT_RULES.resolve_attack(self.attacker, self.defender, attack_value=20,
defense_value=10)
tb_items.COMBAT_RULES.resolve_attack(
self.attacker, self.defender, attack_value=20, defense_value=10
)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True
@ -555,8 +561,9 @@ class TestTurnBattleMagicFunc(BaseEvenniaTest):
self.assertTrue(self.defender.db.hp == 7)
# Resolve attack
self.defender.db.hp = 40
tb_magic.COMBAT_RULES.resolve_attack(self.attacker, self.defender, attack_value=20,
defense_value=10)
tb_magic.COMBAT_RULES.resolve_attack(
self.attacker, self.defender, attack_value=20, defense_value=10
)
self.assertTrue(self.defender.db.hp < 40)
# Combat cleanup
self.attacker.db.Combat_attribute = True

View file

@ -2,4 +2,3 @@
Contribs related to moving in and manipulating the game world and grid.
"""

View file

@ -18,17 +18,18 @@ import random
# A map with a temple (▲) amongst mountains (n,∩) in a forest (♣,♠) on an
# island surrounded by water (≈). By giving no instructions for the water
# characters we effectively skip it and create no rooms for those squares.
EXAMPLE1_MAP = '''\
EXAMPLE1_MAP = """\
n
n
'''
"""
def example1_build_forest(x, y, **kwargs):
'''A basic example of build instructions. Make sure to include **kwargs
in the arguments and return an instance of the room for exit generation.'''
"""A basic example of build instructions. Make sure to include **kwargs
in the arguments and return an instance of the room for exit generation."""
# Create a room and provide a basic description.
room = create_object(rooms.Room, key="forest" + str(x) + str(y))
@ -42,7 +43,7 @@ def example1_build_forest(x, y, **kwargs):
def example1_build_mountains(x, y, **kwargs):
'''A room that is a little more advanced'''
"""A room that is a little more advanced"""
# Create the room.
room = create_object(rooms.Room, key="mountains" + str(x) + str(y))
@ -68,7 +69,7 @@ def example1_build_mountains(x, y, **kwargs):
def example1_build_temple(x, y, **kwargs):
'''A unique room that does not need to be as general'''
"""A unique room that does not need to be as general"""
# Create the room.
room = create_object(rooms.Room, key="temple" + str(x) + str(y))
@ -115,7 +116,7 @@ EXAMPLE1_LEGEND = {
# This is the same layout as Example 1 but included are characters for exits.
# We can use these characters to determine which rooms should be connected.
EXAMPLE2_MAP = '''\
EXAMPLE2_MAP = """\
--
@ -125,11 +126,11 @@ EXAMPLE2_MAP = '''\
--
'''
"""
def example2_build_forest(x, y, **kwargs):
'''A basic room'''
"""A basic room"""
# If on anything other than the first iteration - Do nothing.
if kwargs["iteration"] > 0:
return None
@ -143,7 +144,7 @@ def example2_build_forest(x, y, **kwargs):
def example2_build_verticle_exit(x, y, **kwargs):
'''Creates two exits to and from the two rooms north and south.'''
"""Creates two exits to and from the two rooms north and south."""
# If on the first iteration - Do nothing.
if kwargs["iteration"] == 0:
return
@ -164,7 +165,7 @@ def example2_build_verticle_exit(x, y, **kwargs):
def example2_build_horizontal_exit(x, y, **kwargs):
'''Creates two exits to and from the two rooms east and west.'''
"""Creates two exits to and from the two rooms east and west."""
# If on the first iteration - Do nothing.
if kwargs["iteration"] == 0:
return

View file

@ -5,7 +5,7 @@ Wilderness contrib - titeuf87, 2017
from .wilderness import create_wilderness # noqa
from .wilderness import enter_wilderness # noqa
from .wilderness import get_new_coordinates # noqa
from .wilderness import get_new_coordinates # noqa
from .wilderness import WildernessScript # noqa
from .wilderness import WildernessExit # noqa
from .wilderness import WildernessRoom # noqa

View file

@ -58,6 +58,7 @@ class CmdXYZTeleport(building.CmdTeleport):
are given, the target is a location on the XYZGrid.
"""
def _search_by_xyz(self, inp):
inp = inp.strip("()")
X, Y, *Z = inp.split(",", 2)
@ -69,8 +70,10 @@ class CmdXYZTeleport(building.CmdTeleport):
try:
xyz = self.caller.xyz
except AttributeError:
self.caller.msg("Z-coordinate is also required since you are not currently "
"in a room with a Z coordinate of its own.")
self.caller.msg(
"Z-coordinate is also required since you are not currently "
"in a room with a Z coordinate of its own."
)
raise InterruptCommand
else:
Z = xyz[2]
@ -134,9 +137,11 @@ class CmdXYZOpen(building.CmdOpen):
self.location = self.caller.location
if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination or (X,Y,Z)>")
self.caller.msg(
"Usage: open <new exit>[;alias...][:typeclass]"
"[,<return exit>[;alias..][:typeclass]]] "
"= <destination or (X,Y,Z)>"
)
raise InterruptCommand
if not self.location:
self.caller.msg("You cannot create an exit from a None-location.")
@ -184,6 +189,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
Builders can optionally specify a specific grid coordinate (X,Y) to go to.
"""
key = "goto"
aliases = "path"
help_category = "General"
@ -207,11 +213,19 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
def _search_by_key_and_alias(self, inp, xyz_start):
Z = xyz_start[2]
candidates = list(XYZRoom.objects.filter_xyz(xyz=('*', '*', Z)))
candidates = list(XYZRoom.objects.filter_xyz(xyz=("*", "*", Z)))
return self.caller.search(inp, candidates=candidates)
def _auto_step(self, caller, session, target=None,
xymap=None, directions=None, step_sequence=None, step=True):
def _auto_step(
self,
caller,
session,
target=None,
xymap=None,
directions=None,
step_sequence=None,
step=True,
):
path_data = caller.ndb.xy_path_data
@ -221,8 +235,12 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# stop any old task in its tracks
path_data.task.cancel()
path_data = caller.ndb.xy_path_data = PathData(
target=target, xymap=xymap, directions=directions,
step_sequence=step_sequence, task=None)
target=target,
xymap=xymap,
directions=directions,
step_sequence=step_sequence,
task=None,
)
if step and path_data:
@ -285,7 +303,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xymap=path_data.xymap,
directions=directions,
step_sequence=step_sequence,
task=None
task=None,
)
# the map can itself tell the stepper to stop the auto-step prematurely
interrupt_node_or_link = None
@ -301,7 +319,8 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# the exit name does not need to be the same as the cardinal direction!
exit_name, *_ = first_link.spawn_aliases.get(
direction, current_node.direction_spawn_defaults.get(direction, ('unknown', )))
direction, current_node.direction_spawn_defaults.get(direction, ("unknown",))
)
exit_obj = caller.search(exit_name)
if not exit_obj:
@ -315,13 +334,15 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# premature stop of pathfind-step because of map node/link of interrupt type
if hasattr(interrupt_node_or_link, "node_index"):
message = exit_obj.destination.attributes.get(
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg)
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg
)
# we move into the node/room and then stop
caller.execute_cmd(exit_name, session=session)
else:
# if the link is interrupted we don't cross it at all
message = exit_obj.attributes.get(
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg)
"xyz_path_interrupt_msg", default=self.default_xyz_path_interrupt_msg
)
caller.msg(message)
return
@ -335,7 +356,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xymap=path_data.xymap,
directions=path_data.directions,
step_sequence=path_data.step_sequence,
task=delay(self.auto_step_delay, self._auto_step, caller, session)
task=delay(self.auto_step_delay, self._auto_step, caller, session),
)
def func(self):
@ -344,7 +365,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
"""
caller = self.caller
goto_mode = self.cmdname == 'goto'
goto_mode = self.cmdname == "goto"
# check if we have an existing path
path_data = caller.ndb.xy_path_data
@ -359,8 +380,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
caller.msg(f"Aborted auto-walking to {target_name}.")
return
# goto/path-command will show current path
current_path = list_to_string(
[f"|w{step}|n" for step in path_data.directions])
current_path = list_to_string([f"|w{step}|n" for step in path_data.directions])
moving = "(moving)" if task and task.active() else ""
caller.msg(f"Path to {target_name}{moving}: {current_path}")
else:
@ -405,12 +425,21 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xy_end = xyz_end[:2]
directions, step_sequence = xymap.get_shortest_path(xy_start, xy_end)
caller.msg(f"There are {len(directions)} steps to {target.get_display_name(caller)}: "
f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n")
caller.msg(
f"There are {len(directions)} steps to {target.get_display_name(caller)}: "
f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n"
)
# create data for display and start stepping if we used goto
self._auto_step(caller, self.session, target=target, xymap=xymap,
directions=directions, step_sequence=step_sequence, step=goto_mode)
self._auto_step(
caller,
self.session,
target=target,
xymap=xymap,
directions=directions,
step_sequence=step_sequence,
step=goto_mode,
)
class CmdMap(COMMAND_DEFAULT_CLASS):
@ -424,6 +453,7 @@ class CmdMap(COMMAND_DEFAULT_CLASS):
This is a builder-command.
"""
key = "map"
locks = "cmd:perm(Builders)"
@ -453,8 +483,10 @@ class CmdMap(COMMAND_DEFAULT_CLASS):
xymap = xyzgrid.get_map(Z)
if not xymap:
self.caller.msg(f"XYMap '{Z}' is not found on the grid. Try 'map list' to see "
"available maps/Zcoords.")
self.caller.msg(
f"XYMap '{Z}' is not found on the grid. Try 'map list' to see "
"available maps/Zcoords."
)
return
self.caller.msg(ansi.raw(xymap.mapstring))
@ -465,6 +497,7 @@ class XYZGridCmdSet(CmdSet):
Cmdset for easily adding the above cmds to the character cmdset.
"""
key = "xyzgrid_cmdset"
def at_cmdset_creation(self):

View file

@ -81,14 +81,13 @@ class TransitionToCave(xymap_legend.TransitionMapNode):
into a room but only acts as a target for finding the exit's destination.
"""
symbol = 'T'
target_map_xyz = (1, 0, 'the small cave')
symbol = "T"
target_map_xyz = (1, 0, "the small cave")
# extends the default legend
LEGEND_MAP1 = {
'T': TransitionToCave
}
LEGEND_MAP1 = {"T": TransitionToCave}
# link coordinates to rooms
@ -96,70 +95,62 @@ PROTOTYPES_MAP1 = {
# node/room prototypes
(3, 0): {
"key": "Dungeon Entrance",
"desc": "To the east, a narrow opening leads into darkness."
"desc": "To the east, a narrow opening leads into darkness.",
},
(4, 1): {
"key": "Under the foilage of a giant tree",
"desc": "High above the branches of a giant tree blocks out the sunlight. A slide "
"leading down from the upper branches ends here."
"leading down from the upper branches ends here.",
},
(4, 4): {
"key": "The slide",
"desc": "A slide leads down to the ground from here. It looks like a one-way trip."
"desc": "A slide leads down to the ground from here. It looks like a one-way trip.",
},
(6, 1): {
"key": "Thorny path",
"desc": "To the east is a pathway of thorns. If you get through, you don't think you'll be "
"able to get back here the same way."
},
(8, 1): {
"key": "By a large tree",
"desc": "You are standing at the root of a great tree."
},
(8, 3): {
"key": "At the top of the tree",
"desc": "You are at the top of the tree."
"able to get back here the same way.",
},
(8, 1): {"key": "By a large tree", "desc": "You are standing at the root of a great tree."},
(8, 3): {"key": "At the top of the tree", "desc": "You are at the top of the tree."},
(3, 7): {
"key": "Dense foilage",
"desc": "The foilage to the east is extra dense. It will take forever to get through it."
"desc": "The foilage to the east is extra dense. It will take forever to get through it.",
},
(5, 6): {
"key": "On a huge branch",
"desc": "To the east is a glowing light, may be a teleporter to a higher branch."
"desc": "To the east is a glowing light, may be a teleporter to a higher branch.",
},
(9, 7): {
"key": "On an enormous branch",
"desc": "To the west is a glowing light. It may be a teleporter to a lower branch."
"desc": "To the west is a glowing light. It may be a teleporter to a lower branch.",
},
(10, 8): {
"key": "A gorgeous view",
"desc": "The view from here is breathtaking, showing the forest stretching far and wide."
"desc": "The view from here is breathtaking, showing the forest stretching far and wide.",
},
# default rooms
('*', '*'): {
("*", "*"): {
"key": "Among the branches of a giant tree",
"desc": "These branches are wide enough to easily walk on. There's green all around."
"desc": "These branches are wide enough to easily walk on. There's green all around.",
},
# directional prototypes
(3, 0, 'e'): {
"desc": "A dark passage into the underworld."
},
(3, 0, "e"): {"desc": "A dark passage into the underworld."},
}
for key, prot in PROTOTYPES_MAP1.items():
if len(key) == 2:
# we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT
prot["prototype_parent"] = ROOM_PARENT
else:
prot['prototype_parent'] = EXIT_PARENT
prot["prototype_parent"] = EXIT_PARENT
XYMAP_DATA_MAP1 = {
"zcoord": "the large tree",
"map": MAP1,
"legend": LEGEND_MAP1,
"prototypes": PROTOTYPES_MAP1
"prototypes": PROTOTYPES_MAP1,
}
# -------------------------------------- map2
@ -188,14 +179,13 @@ class TransitionToLargeTree(xymap_legend.TransitionMapNode):
into a room by only acts as a target for finding the exit's destination.
"""
symbol = 'T'
target_map_xyz = (3, 0, 'the large tree')
symbol = "T"
target_map_xyz = (3, 0, "the large tree")
# this extends the default legend (that defines #,-+ etc)
LEGEND_MAP2 = {
"T": TransitionToLargeTree
}
LEGEND_MAP2 = {"T": TransitionToLargeTree}
# prototypes for specific locations
PROTOTYPES_MAP2 = {
@ -203,64 +193,54 @@ PROTOTYPES_MAP2 = {
(1, 0): {
"key": "The entrance",
"desc": "This is the entrance to a small cave leading into the ground. "
"Light sifts in from the outside, while cavernous passages disappear "
"into darkness."
"Light sifts in from the outside, while cavernous passages disappear "
"into darkness.",
},
(2, 0): {
"key": "A gruesome sight.",
"desc": "Something was killed here recently. The smell is unbearable."
"desc": "Something was killed here recently. The smell is unbearable.",
},
(1, 1): {
"key": "A dark pathway",
"desc": "The path splits three ways here. To the north a faint light can be seen."
"desc": "The path splits three ways here. To the north a faint light can be seen.",
},
(3, 2): {
"key": "Stagnant water",
"desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest "
"a faint light can be seen."
},
(0, 2): {
"key": "A dark alcove",
"desc": "This alcove is empty."
"a faint light can be seen.",
},
(0, 2): {"key": "A dark alcove", "desc": "This alcove is empty."},
(1, 2): {
"key": "South-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
"between the stones.",
},
(2, 2): {
"key": "South-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
"between the stones.",
},
(1, 3): {
"key": "North-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones."
"between the stones.",
},
(2, 3): {
"key": "North-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones. To the east is a dark passage."
"between the stones. To the east is a dark passage.",
},
(3, 3): {
"key": "Craggy crevice",
"desc": "This is the deepest part of the dungeon. The path shrinks away and there "
"is no way to continue deeper."
"is no way to continue deeper.",
},
# default fallback for undefined nodes
('*', '*'): {
"key": "A dark room",
"desc": "A dark, but empty, room."
},
("*", "*"): {"key": "A dark room", "desc": "A dark, but empty, room."},
# directional prototypes
(1, 0, 'w'): {
"desc": "A narrow path to the fresh air of the outside world."
},
(1, 0, "w"): {"desc": "A narrow path to the fresh air of the outside world."},
# directional fallbacks for unset directions
('*', '*', '*'): {
"desc": "A dark passage"
}
("*", "*", "*"): {"desc": "A dark passage"},
}
# this is required by the prototypes, but we add it all at once so we don't
@ -268,9 +248,9 @@ PROTOTYPES_MAP2 = {
for key, prot in PROTOTYPES_MAP2.items():
if len(key) == 2:
# we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT
prot["prototype_parent"] = ROOM_PARENT
else:
prot['prototype_parent'] = EXIT_PARENT
prot["prototype_parent"] = EXIT_PARENT
XYMAP_DATA_MAP2 = {
@ -278,14 +258,8 @@ XYMAP_DATA_MAP2 = {
"zcoord": "the small cave",
"legend": LEGEND_MAP2,
"prototypes": PROTOTYPES_MAP2,
"options": {
"map_visual_range": 1,
"map_mode": 'scan'
}
"options": {"map_visual_range": 1, "map_mode": "scan"},
}
# This is read by the parser
XYMAP_DATA_LIST = [
XYMAP_DATA_MAP1,
XYMAP_DATA_MAP2
]
XYMAP_DATA_LIST = [XYMAP_DATA_MAP1, XYMAP_DATA_MAP2]

View file

@ -160,11 +160,12 @@ _TOPICS_MAP = {
"add": _HELP_ADD,
"spawn": _HELP_SPAWN,
"initpath": _HELP_INITPATH,
"delete": _HELP_DELETE
"delete": _HELP_DELETE,
}
evennia._init()
def _option_help(*suboptions):
"""
Show help <command> aid.
@ -188,6 +189,7 @@ def _option_list(*suboptions):
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
xyzgrid.log = _log
xymap_data = xyzgrid.grid
@ -210,7 +212,7 @@ def _option_list(*suboptions):
if not xymap:
print(f"No XYMap with Z='{zcoord}' was found on grid.")
else:
nrooms = xyzgrid.get_room(('*', '*', zcoord)).count()
nrooms = xyzgrid.get_room(("*", "*", zcoord)).count()
nnodes = len(xymap.node_index_map)
print("\n" + str(repr(xymap)) + ":\n")
checkwarning = True
@ -218,22 +220,29 @@ def _option_list(*suboptions):
print(f"{nrooms} / {nnodes} rooms are spawned.")
checkwarning = False
elif nrooms < nnodes:
print(f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Transitional nodes are *not* spawned (they just point \n"
"to another map), so the 'missing room(s)' may just be from such nodes.")
print(
f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Transitional nodes are *not* spawned (they just point \n"
"to another map), so the 'missing room(s)' may just be from such nodes."
)
elif nrooms > nnodes:
print(f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Maybe some rooms were removed from map. Run 'spawn' to re-sync.")
print(
f"{nrooms} / {nnodes} rooms are spawned\n"
"Note: Maybe some rooms were removed from map. Run 'spawn' to re-sync."
)
else:
print(f"{nrooms} / {nnodes} rooms are spawned\n")
if checkwarning:
print("Note: This check is not complete; it does not consider changed map "
"topology\nlike relocated nodes/rooms and new/removed links/exits - this "
"is calculated only during a spawn.")
print(
"Note: This check is not complete; it does not consider changed map "
"topology\nlike relocated nodes/rooms and new/removed links/exits - this "
"is calculated only during a spawn."
)
print("\nDisplayed map (as appearing in-game):\n\n" + ansi.parse_ansi(str(xymap)))
print("\nRaw map string (including axes and invisible nodes/links):\n"
+ str(xymap.mapstring))
print(
"\nRaw map string (including axes and invisible nodes/links):\n" + str(xymap.mapstring)
)
print(f"\nCustom map options: {xymap.options}\n")
legend = []
for key, node_or_link in xymap.legend.items():
@ -260,6 +269,7 @@ def _option_add(*suboptions):
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
xymap_data_list = []
@ -290,35 +300,44 @@ def _option_spawn(*suboptions):
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
if suboptions:
opts = ''.join(suboptions).strip('()')
opts = "".join(suboptions).strip("()")
# coordinate tuple
try:
x, y, z = (part.strip() for part in opts.split(","))
except ValueError:
print("spawn coordinate must be given as (X, Y, Z) tuple, where '*' act "
"wild cards and Z is the mapname/z-coord of the map to load.")
print(
"spawn coordinate must be given as (X, Y, Z) tuple, where '*' act "
"wild cards and Z is the mapname/z-coord of the map to load."
)
return
else:
x, y, z = '*', '*', '*'
x, y, z = "*", "*", "*"
if x == y == z == '*':
inp = input("This will (re)spawn the entire grid. If it was built before, it may spawn \n"
"new rooms or delete rooms that no longer matches the grid.\nDo you want to "
"continue? [Y]/N? ")
if x == y == z == "*":
inp = input(
"This will (re)spawn the entire grid. If it was built before, it may spawn \n"
"new rooms or delete rooms that no longer matches the grid.\nDo you want to "
"continue? [Y]/N? "
)
else:
inp = input("This will spawn/delete objects in the database matching grid coordinates \n"
f"({x},{y},{z}) (where '*' is a wildcard).\nDo you want to continue? [Y]/N? ")
if inp.lower() in ('no', 'n'):
inp = input(
"This will spawn/delete objects in the database matching grid coordinates \n"
f"({x},{y},{z}) (where '*' is a wildcard).\nDo you want to continue? [Y]/N? "
)
if inp.lower() in ("no", "n"):
print("Aborted.")
return
print("Beginner-Tutorial spawn ...")
grid.spawn(xyz=(x, y, z))
print("... spawn complete!\nIt's recommended to reload the server to refresh caches if this "
"modified an existing grid.")
print(
"... spawn complete!\nIt's recommended to reload the server to refresh caches if this "
"modified an existing grid."
)
def _option_initpath(*suboptions):
@ -331,6 +350,7 @@ def _option_initpath(*suboptions):
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
xymaps = grid.all_maps()
@ -354,19 +374,24 @@ def _option_delete(*suboptions):
# override grid's logger to echo directly to console
def _log(msg):
print(msg)
grid.log = _log
if not suboptions:
repl = input("WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]? ")
if repl.lower() not in ('yes', 'y'):
repl = input(
"WARNING: This will delete the ENTIRE Grid and wipe all rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]? "
)
if repl.lower() not in ("yes", "y"):
print("Aborted.")
return
print("Deleting grid ...")
grid.delete()
print("... done.\nPlease reload the server now; otherwise "
"removed rooms may linger in cache.")
print(
"... done.\nPlease reload the server now; otherwise "
"removed rooms may linger in cache."
)
return
zcoords = (part.strip() for part in suboptions)
@ -376,21 +401,24 @@ def _option_delete(*suboptions):
print(f"Mapname/zcoord {zcoord} is not a part of the grid.")
err = True
if err:
print("Valid mapnames/zcoords are\n:", "\n ".join(
xymap.Z for xymap in grid.all_rooms()))
print("Valid mapnames/zcoords are\n:", "\n ".join(xymap.Z for xymap in grid.all_rooms()))
return
repl = input("This will delete map(s) {', '.join(zcoords)} and wipe all corresponding\n"
"rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]? ")
if repl.lower() not in ('yes', 'y'):
repl = input(
"This will delete map(s) {', '.join(zcoords)} and wipe all corresponding\n"
"rooms/exits!"
"\nObjects/Chars inside deleted rooms will be moved to their home locations."
"\nThis can't be undone. Are you sure you want to continue? Y/[N]? "
)
if repl.lower() not in ("yes", "y"):
print("Aborted.")
return
print("Deleting selected xymaps ...")
grid.remove_map(*zcoords, remove_objects=True)
print("... done.\nPlease reload the server to refresh room caches."
"\nAlso remember to remove any links from remaining maps pointing to deleted maps.")
print(
"... done.\nPlease reload the server to refresh room caches."
"\nAlso remember to remove any links from remaining maps pointing to deleted maps."
)
def xyzcommand(*args):
@ -405,20 +433,19 @@ def xyzcommand(*args):
option, *suboptions = args
if option in ('help', 'h'):
if option in ("help", "h"):
_option_help(*suboptions)
if option in ('list', 'show'):
if option in ("list", "show"):
_option_list(*suboptions)
elif option == 'init':
elif option == "init":
_option_init(*suboptions)
elif option == 'add':
elif option == "add":
_option_add(*suboptions)
elif option == 'spawn':
elif option == "spawn":
_option_spawn(*suboptions)
elif option == 'initpath':
elif option == "initpath":
_option_initpath(*suboptions)
elif option == 'delete':
elif option == "delete":
_option_delete(*suboptions)
else:
print(f"Unknown option '{option}'. Use 'evennia xyzgrid help' for valid arguments.")

View file

@ -27,19 +27,19 @@ except AttributeError:
exit_override = {}
room_prototype = {
'prototype_key': 'xyz_room',
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom',
'prototype_tags': ("xyzroom", ),
'key': "A room",
'desc': "An empty room."
"prototype_key": "xyz_room",
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom",
"prototype_tags": ("xyzroom",),
"key": "A room",
"desc": "An empty room.",
}
room_prototype.update(room_override)
exit_prototype = {
'prototype_key': 'xyz_exit',
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit',
'prototype_tags': ("xyzexit", ),
'desc': "An exit."
"prototype_key": "xyz_exit",
"typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit",
"prototype_tags": ("xyzexit",),
"desc": "An exit.",
}
exit_prototype.update(exit_override)

File diff suppressed because it is too large Load diff

View file

@ -30,13 +30,15 @@ MAPSCAN = {
# errors for Map system
class MapError(RuntimeError):
class MapError(RuntimeError):
def __init__(self, error="", node_or_link=None):
prefix = ""
if node_or_link:
prefix = (f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' "
f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) ")
prefix = (
f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' "
f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) "
)
self.node_or_link = node_or_link
self.message = f"{prefix}{error}"
super().__init__(self.message)
@ -52,4 +54,5 @@ class MapTransition(RuntimeWarning):
leads to another map.
"""
pass

View file

@ -104,7 +104,8 @@ try:
except ImportError as err:
raise ImportError(
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
"the SciPy package. Install with `pip install scipy'."
)
from django.conf import settings
from evennia.utils.utils import variable_from_module, mod_import, is_iter
from evennia.utils import logger
@ -122,9 +123,7 @@ _CACHE_DIR = settings.CACHE_DIR
_LOADED_PROTOTYPES = None
_XYZROOMCLASS = None
MAP_DATA_KEYS = [
"zcoord", "map", "legend", "prototypes", "options", "module_path"
]
MAP_DATA_KEYS = ["zcoord", "map", "legend", "prototypes", "options", "module_path"]
DEFAULT_LEGEND = xymap_legend.LEGEND
@ -172,11 +171,11 @@ class XYMap:
but recommended for readability!
"""
mapcorner_symbol = '+'
mapcorner_symbol = "+"
max_pathfinding_length = 500
empty_symbol = ' '
empty_symbol = " "
# we normally only accept one single character for the legend key
legend_key_exceptions = ("\\")
legend_key_exceptions = "\\"
def __init__(self, map_module_or_dict, Z="map", xyzgrid=None):
"""
@ -210,7 +209,9 @@ class XYMap:
if not _LOADED_PROTOTYPES:
# inject default prototypes, but don't override prototype-keys loaded from
# settings, if they exist (that means the user wants to replace the defaults)
protlib.load_module_prototypes("evennia.contrib.grid.xyzgrid.prototypes", override=False)
protlib.load_module_prototypes(
"evennia.contrib.grid.xyzgrid.prototypes", override=False
)
_LOADED_PROTOTYPES = True
self.Z = Z
@ -264,7 +265,7 @@ class XYMap:
nnodes = 0
if self.node_index_map:
nnodes = len(self.node_index_map)
return (f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, {nnodes} nodes>")
return f"<XYMap(Z={self.Z}), {self.max_X + 1}x{self.max_Y + 1}, {nnodes} nodes>"
def log(self, msg):
if self.xyzgrid:
@ -317,34 +318,41 @@ class XYMap:
mapdata = variable_from_module(mod, "XYMAP_DATA")
if not mapdata:
raise MapError("No valid XYMAP_DATA or XYMAP_DATA_LIST could be found from "
f"{map_module_or_dict}.")
raise MapError(
"No valid XYMAP_DATA or XYMAP_DATA_LIST could be found from "
f"{map_module_or_dict}."
)
# validate
if any(key for key in mapdata if key not in MAP_DATA_KEYS):
raise MapError(f"Mapdata has keys {list(mapdata)}, but only "
f"keys {MAP_DATA_KEYS} are allowed.")
raise MapError(
f"Mapdata has keys {list(mapdata)}, but only " f"keys {MAP_DATA_KEYS} are allowed."
)
for key in mapdata.get('legend', DEFAULT_LEGEND):
for key in mapdata.get("legend", DEFAULT_LEGEND):
if not key or len(key) > 1:
if key not in self.legend_key_exceptions:
raise MapError(f"Map-legend key '{key}' is invalid: All keys must "
"be exactly one character long. Use the node/link's "
"`.display_symbol` property to change how it is "
"displayed.")
if 'map' not in mapdata or not mapdata['map']:
raise MapError(
f"Map-legend key '{key}' is invalid: All keys must "
"be exactly one character long. Use the node/link's "
"`.display_symbol` property to change how it is "
"displayed."
)
if "map" not in mapdata or not mapdata["map"]:
raise MapError("No map found. Add 'map' key to map-data dict.")
for key, prototype in mapdata.get('prototypes', {}).items():
for key, prototype in mapdata.get("prototypes", {}).items():
if not (is_iter(key) and (2 <= len(key) <= 3)):
raise MapError(f"Prototype override key {key} is malformed: It must be a "
"coordinate (X, Y) for nodes or (X, Y, direction) for links; "
"where direction is a supported direction string ('n', 'ne', etc).")
raise MapError(
f"Prototype override key {key} is malformed: It must be a "
"coordinate (X, Y) for nodes or (X, Y, direction) for links; "
"where direction is a supported direction string ('n', 'ne', etc)."
)
# store/update result
self.Z = mapdata.get('zcoord', self.Z)
self.mapstring = mapdata['map']
self.prototypes = mapdata.get('prototypes', {})
self.options = mapdata.get('options', {})
self.Z = mapdata.get("zcoord", self.Z)
self.mapstring = mapdata["map"]
self.prototypes = mapdata.get("prototypes", {})
self.options = mapdata.get("options", {})
# merge the custom legend onto the default legend to allow easily
# overriding only parts of it
@ -357,8 +365,9 @@ class XYMap:
# nothing more to do
continue
# we need to load the prototype dict onto each for ease of access. Note that
proto = protlib.search_prototype(prototype, require_single=True,
no_db=_NO_DB_PROTOTYPES)[0]
proto = protlib.search_prototype(
prototype, require_single=True, no_db=_NO_DB_PROTOTYPES
)[0]
node_or_link_class.prototype = proto
def parse(self):
@ -391,7 +400,8 @@ class XYMap:
raise MapParserError(
f"The mapstring must have at least two '{mapcorner_symbol}' "
"symbols marking the upper- and bottom-left corners of the "
"grid area.")
"grid area."
)
# find the the position (in the string as a whole) of the top-left corner-marker
maplines = mapstring.split("\n")
@ -406,13 +416,15 @@ class XYMap:
# find the position (in the string as a whole) of the bottom-left corner-marker
# this is always in a stright line down from the first marker
botleft_marker_x, botleft_marker_y = topleft_marker_x, -1
for botleft_marker_y, line in enumerate(maplines[topleft_marker_y + 1:]):
for botleft_marker_y, line in enumerate(maplines[topleft_marker_y + 1 :]):
if line.find(mapcorner_symbol) == topleft_marker_x:
break
if botleft_marker_y == -1:
raise MapParserError(f"No bottom-left corner-marker ({mapcorner_symbol}) found! "
"Make sure it lines up with the top-left corner-marker "
f"(found at column {topleft_marker_x} of the string).")
raise MapParserError(
f"No bottom-left corner-marker ({mapcorner_symbol}) found! "
"Make sure it lines up with the top-left corner-marker "
f"(found at column {topleft_marker_x} of the string)."
)
# the actual coordinate is dy below the topleft marker so we need to shift
botleft_marker_y += topleft_marker_y + 1
@ -443,8 +455,7 @@ class XYMap:
mapnode_or_link_class = self.legend.get(char)
if not mapnode_or_link_class:
raise MapParserError(
f"Symbol '{char}' on XY=({ix / 2:g},{iy / 2:g}) "
"is not found in LEGEND."
f"Symbol '{char}' on XY=({ix / 2:g},{iy / 2:g}) " "is not found in LEGEND."
)
if hasattr(mapnode_or_link_class, "node_index"):
# A mapnode. Mapnodes can only be placed on even grid positions, where
@ -454,7 +465,8 @@ class XYMap:
raise MapParserError(
f"Symbol '{char}' on XY=({ix / 2:g},{iy / 2:g}) marks a "
"MapNode but is located between integer (X,Y) positions (only "
"Links can be placed between coordinates)!")
"Links can be placed between coordinates)!"
)
# save the node to several different maps for different uses
# in both coordinate systems
@ -462,14 +474,17 @@ class XYMap:
max_X, max_Y = max(max_X, iX), max(max_Y, iY)
node_index += 1
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[node_index] = \
mapnode_or_link_class(x=ix, y=iy, Z=self.Z,
node_index=node_index, symbol=char, xymap=self)
xygrid[ix][iy] = XYgrid[iX][iY] = node_index_map[
node_index
] = mapnode_or_link_class(
x=ix, y=iy, Z=self.Z, node_index=node_index, symbol=char, xymap=self
)
else:
# we have a link at this xygrid position (this is ok everywhere)
xygrid[ix][iy] = mapnode_or_link_class(x=ix, y=iy, Z=self.Z, symbol=char,
xymap=self)
xygrid[ix][iy] = mapnode_or_link_class(
x=ix, y=iy, Z=self.Z, symbol=char, xymap=self
)
# store the symbol mapping for transition lookups
symbol_map[char].append(xygrid[ix][iy])
@ -499,20 +514,23 @@ class XYMap:
node_coord = (node.X, node.Y)
# load prototype from override, or use default
try:
node.prototype = flatten_prototype(self.prototypes.get(
node_coord,
self.prototypes.get(('*', '*'), node.prototype)),
no_db=_NO_DB_PROTOTYPES
node.prototype = flatten_prototype(
self.prototypes.get(
node_coord, self.prototypes.get(("*", "*"), node.prototype)
),
no_db=_NO_DB_PROTOTYPES,
)
except Exception as err:
raise MapParserError(f"Room prototype malformed: {err}", node)
# do the same for links (x, y, direction) coords
for direction, maplink in node.first_links.items():
try:
maplink.prototype = flatten_prototype(self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(('*', '*', '*'), maplink.prototype)),
no_db=_NO_DB_PROTOTYPES
maplink.prototype = flatten_prototype(
self.prototypes.get(
node_coord + (direction,),
self.prototypes.get(("*", "*", "*"), maplink.prototype),
),
no_db=_NO_DB_PROTOTYPES,
)
except Exception as err:
raise MapParserError(f"Exit prototype malformed: {err}", maplink)
@ -539,8 +557,10 @@ class XYMap:
This performs a depth-first pass down the the given dist.
"""
def _scan_neighbors(start_node, points, dist=2,
xmin=BIGVAL, ymin=BIGVAL, xmax=0, ymax=0, depth=0):
def _scan_neighbors(
start_node, points, dist=2, xmin=BIGVAL, ymin=BIGVAL, xmax=0, ymax=0, depth=0
):
x0, y0 = start_node.x, start_node.y
points.append((x0, y0))
@ -558,9 +578,15 @@ class XYMap:
ymin, ymax = min(ymin, y), max(ymax, y)
points, xmin, xmax, ymin, ymax = _scan_neighbors(
end_node, points, dist=dist,
xmin=xmin, ymin=ymin, xmax=xmax, ymax=ymax,
depth=depth + 1)
end_node,
points,
dist=dist,
xmin=xmin,
ymin=ymin,
xmax=xmax,
ymax=ymax,
depth=depth + 1,
)
return points, xmin, xmax, ymin, ymax
@ -581,14 +607,16 @@ class XYMap:
# check if the solution for this grid was already solved previously.
mapstr, dist_matrix, pathfinding_routes = "", None, None
with open(self.pathfinder_baked_filename, 'rb') as fil:
with open(self.pathfinder_baked_filename, "rb") as fil:
try:
mapstr, dist_matrix, pathfinding_routes = pickle.load(fil)
except Exception:
logger.log_trace()
if (mapstr == self.mapstring
and dist_matrix is not None
and pathfinding_routes is not None):
if (
mapstr == self.mapstring
and dist_matrix is not None
and pathfinding_routes is not None
):
# this is important - it means the map hasn't changed so
# we can re-use the stored data!
self.dist_matrix = dist_matrix
@ -606,16 +634,20 @@ class XYMap:
# solve using Dijkstra's algorithm
self.dist_matrix, self.pathfinding_routes = dijkstra(
pathfinding_matrix, directed=True,
return_predecessors=True, limit=self.max_pathfinding_length)
pathfinding_matrix,
directed=True,
return_predecessors=True,
limit=self.max_pathfinding_length,
)
if self.pathfinder_baked_filename:
# try to cache the results
with open(self.pathfinder_baked_filename, 'wb') as fil:
pickle.dump((self.mapstring, self.dist_matrix, self.pathfinding_routes),
fil, protocol=4)
with open(self.pathfinder_baked_filename, "wb") as fil:
pickle.dump(
(self.mapstring, self.dist_matrix, self.pathfinding_routes), fil, protocol=4
)
def spawn_nodes(self, xy=('*', '*')):
def spawn_nodes(self, xy=("*", "*")):
"""
Convert the nodes of this XYMap into actual in-world rooms by spawning their
related prototypes in the correct coordinate positions. This must be done *first*
@ -638,12 +670,14 @@ class XYMap:
if not _XYZROOMCLASS:
from evennia.contrib.grid.xyzgrid.xyzroom import XYZRoom as _XYZROOMCLASS
x, y = xy
wildcard = '*'
wildcard = "*"
spawned = []
# find existing nodes, in case some rooms need to be removed
map_coords = [(node.X, node.Y) for node in
sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X))]
map_coords = [
(node.X, node.Y)
for node in sorted(self.node_index_map.values(), key=lambda n: (n.Y, n.X))
]
for existing_room in _XYZROOMCLASS.objects.filter_xyz(xyz=(x, y, self.Z)):
roomX, roomY, _ = existing_room.xyz
if (roomX, roomY) not in map_coords:
@ -657,7 +691,7 @@ class XYMap:
spawned.append(node)
return spawned
def spawn_links(self, xy=('*', '*'), nodes=None, directions=None):
def spawn_links(self, xy=("*", "*"), nodes=None, directions=None):
"""
Convert links of this XYMap into actual in-game exits by spawning their related
prototypes. It's possible to only spawn a specic exit by specifying the node and
@ -676,7 +710,7 @@ class XYMap:
"""
x, y = xy
wildcard = '*'
wildcard = "*"
if not nodes:
nodes = sorted(self.node_index_map.values(), key=lambda n: (n.Z, n.Y, n.X))
@ -706,8 +740,10 @@ class XYMap:
iX, iY = xy
if not ((0 <= iX <= self.max_X) and (0 <= iY <= self.max_Y)):
raise MapError(f"get_node_from_coord got coordinate {xy} which is "
f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y}).")
raise MapError(
f"get_node_from_coord got coordinate {xy} which is "
f"outside the grid size of (0,0) - ({self.max_X}, {self.max_Y})."
)
try:
return self.XYgrid[iX][iY]
except KeyError:
@ -753,8 +789,10 @@ class XYMap:
istartnode = startnode.node_index
inextnode = endnode.node_index
except AttributeError:
raise MapError(f"Map.get_shortest_path received start/end nodes {startnode} and "
f"{endnode}. They must both be MapNodes (not Links)")
raise MapError(
f"Map.get_shortest_path received start/end nodes {startnode} and "
f"{endnode}. They must both be MapNodes (not Links)"
)
if self.pathfinding_routes is None:
self.calculate_path_matrix()
@ -782,13 +820,18 @@ class XYMap:
return directions, path
def get_visual_range(self, xy, dist=2, mode='nodes',
character='@',
target=None,
target_path_style="|y{display_symbol}|n",
max_size=None,
indent=0,
return_str=True):
def get_visual_range(
self,
xy,
dist=2,
mode="nodes",
character="@",
target=None,
target_path_style="|y{display_symbol}|n",
max_size=None,
indent=0,
return_str=True,
):
"""
Get a part of the grid centered on a specific point and extended a certain number
of nodes or grid points in every direction.
@ -876,7 +919,7 @@ class XYMap:
# nothing but ourselves or emptiness
return character if character else self.empty_symbol
elif mode == 'nodes':
elif mode == "nodes":
# dist measures only full, reachable nodes.
points, xmin, xmax, ymin, ymax = self._get_topology_around_coord(xy, dist=dist)
@ -888,7 +931,7 @@ class XYMap:
for (ix0, iy0) in points:
gridmap[iy0 - ymin][ix0 - xmin] = display_map[iy0][ix0]
elif mode == 'scan':
elif mode == "scan":
# scan-mode - dist measures individual grid points
xmin, xmax = max(0, ix - dist), min(width, ix + dist + 1)
@ -897,8 +940,10 @@ class XYMap:
gridmap = [line[xmin:xmax] for line in display_map[ymin:ymax]]
else:
raise MapError(f"Map.get_visual_range 'mode' was '{mode}' "
"- it must be either 'scan' or 'nodes'.")
raise MapError(
f"Map.get_visual_range 'mode' was '{mode}' "
"- it must be either 'scan' or 'nodes'."
)
if character:
gridmap[iyc][ixc] = character # correct indexing; it's a list of lines
@ -906,8 +951,7 @@ class XYMap:
# stylize path to target
def _default_callable(node):
return target_path_style.format(
display_symbol=node.get_display_symbol())
return target_path_style.format(display_symbol=node.get_display_symbol())
if callable(target_path_style):
_target_path_style = target_path_style
@ -916,7 +960,7 @@ class XYMap:
_, path = self.get_shortest_path(xy, target)
maxstep = dist if mode == 'nodes' else dist / 2
maxstep = dist if mode == "nodes" else dist / 2
nsteps = 0
for node_or_link in path[1:]:
if hasattr(node_or_link, "node_index"):

View file

@ -14,7 +14,8 @@ try:
except ImportError as err:
raise ImportError(
f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.")
"the SciPy package. Install with `pip install scipy'."
)
import uuid
from collections import defaultdict
@ -33,6 +34,7 @@ UUID_XYZ_NAMESPACE = uuid.uuid5(uuid.UUID(int=0), "xyzgrid")
# Nodes/Links
class MapNode:
"""
This represents a 'room' node on the map. Note that the map system deals with two grids, the
@ -62,8 +64,9 @@ class MapNode:
for various reasons, mostly map-transitions).
"""
# symbol used to identify this link on the map
symbol = '#'
symbol = "#"
# if printing this node should show another symbol. If set
# to the empty string, use `symbol`.
display_symbol = None
@ -79,16 +82,16 @@ class MapNode:
multilink = True
# default values to use if the exit doesn't have a 'spawn_aliases' iterable
direction_spawn_defaults = {
'n': ('north', 'n'),
'ne': ('northeast', 'ne', 'north-east'),
'e': ('east', 'e'),
'se': ('southeast', 'se', 'south-east'),
's': ('south', 's'),
'sw': ('southwest', 'sw', 'south-west'),
'w': ('west', 'w'),
'nw': ('northwest', 'nw', 'north-west'),
'd': ('down', 'd', 'do'),
'u': ('up', 'u'),
"n": ("north", "n"),
"ne": ("northeast", "ne", "north-east"),
"e": ("east", "e"),
"se": ("southeast", "se", "south-east"),
"s": ("south", "s"),
"sw": ("southwest", "sw", "south-west"),
"w": ("west", "w"),
"nw": ("northwest", "nw", "north-west"),
"d": ("down", "d", "do"),
"u": ("up", "u"),
}
def __init__(self, x, y, Z, node_index=0, symbol=None, xymap=None):
@ -202,7 +205,9 @@ class MapNode:
if first_step_name in self.closest_neighbor_names:
raise MapParserError(
f"has more than one outgoing direction '{first_step_name}'. "
"All directions out of a node must be unique.", self)
"All directions out of a node must be unique.",
self,
)
self.closest_neighbor_names[first_step_name] = direction
node_index = end_node.node_index
@ -215,8 +220,9 @@ class MapNode:
# used for building the shortest path. Note that we store the
# aliased link directions here, for quick display by the
# shortest-route solver
shortest_route = self.shortest_route_to_node.get(
node_index, ("", [], BIGVAL))[2]
shortest_route = self.shortest_route_to_node.get(node_index, ("", [], BIGVAL))[
2
]
if weight < shortest_route:
self.shortest_route_to_node[node_index] = (first_step_name, steps, weight)
@ -280,11 +286,9 @@ class MapNode:
str or tuple: The key of the spawned exit, or a tuple (key, alias, alias, ...)
"""
key, *aliases = (
self.first_links[direction]
.spawn_aliases.get(
direction, self.direction_spawn_defaults.get(
direction, ('unknown', ))))
key, *aliases = self.first_links[direction].spawn_aliases.get(
direction, self.direction_spawn_defaults.get(direction, ("unknown",))
)
if return_aliases:
return (key, *aliases)
return key
@ -313,28 +317,24 @@ class MapNode:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except django_exceptions.ObjectDoesNotExist:
# create a new entity with proper coordinates etc
tclass = self.prototype['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom'
else '')
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An empty room'),
xyz=xyz
tclass = self.prototype["typeclass"]
tclass = (
f" ({tclass})" if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom" else ""
)
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
if err:
raise RuntimeError(err)
else:
self.log(f" updating existing room (if changed) at xyz={xyz}")
if not self.prototype.get('prototype_key'):
if not self.prototype.get("prototype_key"):
# make sure there is a prototype_key in prototype
self.prototype['prototype_key'] = self.generate_prototype_key()
self.prototype["prototype_key"] = self.generate_prototype_key()
# apply prototype to node. This will not override the XYZ tags since
# these are not in the prototype and exact=False
spawner.batch_update_objects_with_prototype(
self.prototype, objects=[nodeobj], exact=False)
spawner.batch_update_objects_with_prototype(self.prototype, objects=[nodeobj], exact=False)
def spawn_links(self, directions=None):
"""
@ -364,9 +364,9 @@ class MapNode:
for direction, link in self.first_links.items():
key, *aliases = self.get_exit_spawn_name(direction)
if not link.prototype.get('prototype_key'):
if not link.prototype.get("prototype_key"):
# generate a deterministic prototype_key if it doesn't exist
link.prototype['prototype_key'] = self.generate_prototype_key()
link.prototype["prototype_key"] = self.generate_prototype_key()
maplinks[key.lower()] = (key, aliases, direction, link)
# remove duplicates
@ -380,8 +380,7 @@ class MapNode:
# we need to search for exits in all directions since some
# may have been removed since last sync
linkobjs = {exi.db_key.lower(): exi
for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}
linkobjs = {exi.db_key.lower(): exi for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}
# figure out if the topology changed between grid and map (will always
# build all exits first run)
@ -411,16 +410,19 @@ class MapNode:
raise RuntimeError(err)
linkobjs[key.lower()] = exi
prot = maplinks[key.lower()][3].prototype
tclass = prot['typeclass']
tclass = (f' ({tclass})'
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit'
else '')
tclass = prot["typeclass"]
tclass = (
f" ({tclass})"
if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit"
else ""
)
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")
# apply prototypes to catch any changes
for key, linkobj in linkobjs.items():
spawner.batch_update_objects_with_prototype(
maplinks[key.lower()][3].prototype, objects=[linkobj], exact=False)
maplinks[key.lower()][3].prototype, objects=[linkobj], exact=False
)
def unspawn(self):
"""
@ -466,8 +468,9 @@ class TransitionMapNode(MapNode):
actual rooms (`#`) on the other map (NOT to the `T`s)!
"""
symbol = 'T'
display_symbol = ' '
symbol = "T"
display_symbol = " "
# X,Y,Z coordinates of target node
taget_map_xyz = (None, None, None)
@ -477,10 +480,13 @@ class TransitionMapNode(MapNode):
the exit to this node (since the prototype is None, this node itself will not be built).
"""
if any(True for coord in self.target_map_xyz if coord in (None, 'unset')):
raise MapParserError(f"(Z={self.xymap.Z}) has not defined its "
"`.target_map_xyz` property. It must point "
"to another valid xymap (Z coordinate).", self)
if any(True for coord in self.target_map_xyz if coord in (None, "unset")):
raise MapParserError(
f"(Z={self.xymap.Z}) has not defined its "
"`.target_map_xyz` property. It must point "
"to another valid xymap (Z coordinate).",
self,
)
return self.target_map_xyz
@ -548,6 +554,7 @@ class MapLink:
`node.get_exit_spawn_name(direction)`
"""
# symbol for identifying this link on the map
symbol = ""
# if `None`, use .symbol
@ -661,7 +668,9 @@ class MapLink:
return None, 0, None
raise MapParserError(
f"was connected to from the direction {start_direction}, but "
"is not set up to link in that direction.", self)
"is not set up to link in that direction.",
self,
)
# note that if `get_direction` returns an unknown direction, this will be equivalent
# to pointing to an empty location, which makes sense
@ -674,8 +683,7 @@ class MapLink:
next_target = self.at_empty_target(start_direction, end_direction)
if not next_target:
raise MapParserError(
f"points to empty space in the direction {end_direction}!", self)
raise MapParserError(f"points to empty space in the direction {end_direction}!", self)
_weight += self.get_weight(start_direction, _weight)
if _steps is None:
@ -688,13 +696,16 @@ class MapLink:
return (
next_target,
_weight / max(1, _linklen) if self.average_long_link_weights else _weight,
_steps
_steps,
)
else:
# we hit another link. Progress recursively.
return next_target.traverse(
REVERSE_DIRECTIONS.get(end_direction, end_direction),
_weight=_weight, _linklen=_linklen + 1, _steps=_steps)
_weight=_weight,
_linklen=_linklen + 1,
_steps=_steps,
)
def get_linked_neighbors(self, directions=None):
"""
@ -720,8 +731,7 @@ class MapLink:
# there is is something there, we need to check if it is either
# a map node or a link connecting in our direction
node_or_link = xygrid[end_x][end_y]
if (node_or_link.multilink
or node_or_link.get_direction(direction)):
if node_or_link.multilink or node_or_link.get_direction(direction):
links[direction] = node_or_link
return links
@ -845,7 +855,8 @@ class SmartRerouterMapLink(MapLink):
for direction in unhandled_links_copy:
if REVERSE_DIRECTIONS[direction] in unhandled_links_copy:
directions[direction] = REVERSE_DIRECTIONS[
unhandled_links.pop(unhandled_links.index(direction))]
unhandled_links.pop(unhandled_links.index(direction))
]
# check if we have any non-cross-through paths left to handle
n_unhandled = len(unhandled_links)
@ -856,7 +867,8 @@ class SmartRerouterMapLink(MapLink):
if n_unhandled != 2:
links = ", ".join(unhandled_links)
raise MapParserError(
f"cannot determine how to connect in/out directions {links}.", self)
f"cannot determine how to connect in/out directions {links}.", self
)
directions[unhandled_links[0]] = unhandled_links[1]
directions[unhandled_links[1]] = unhandled_links[0]
@ -865,6 +877,7 @@ class SmartRerouterMapLink(MapLink):
return self.directions.get(start_direction)
class SmartTeleporterMapLink(MapLink):
"""
The teleport link works by connecting to nowhere - and will then continue
@ -889,10 +902,11 @@ class SmartTeleporterMapLink(MapLink):
-#-t-# - invalid, only one connected link is allowed.
"""
symbol = 't'
symbol = "t"
# usually invisible
display_symbol = ' '
direction_name = 'teleport'
display_symbol = " "
direction_name = "teleport"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -932,7 +946,9 @@ class SmartTeleporterMapLink(MapLink):
if len(found_teleporters) > 1:
raise MapParserError(
"found too many matching teleporters (must be exactly one more): "
f"{found_teleporters}", self)
f"{found_teleporters}",
self,
)
other_teleporter = found_teleporters[0]
# link the two so we don't need to scan again for the other one
@ -952,9 +968,10 @@ class SmartTeleporterMapLink(MapLink):
if len(neighbors) != 1:
raise MapParserError("must have exactly one link connected to it.", self)
direction, link = next(iter(neighbors.items()))
if hasattr(link, 'node_index'):
raise MapParserError("can only connect to a Link. Found {link} in "
"direction {direction}.", self)
if hasattr(link, "node_index"):
raise MapParserError(
"can only connect to a Link. Found {link} in " "direction {direction}.", self
)
# the string 'teleport' will not be understood by the traverser, leading to
# this being interpreted as an empty target and the `at_empty_target`
# hook firing when trying to traverse this link.
@ -962,12 +979,10 @@ class SmartTeleporterMapLink(MapLink):
if start_direction == direction_name:
# called while traversing another teleport
# - we must make sure we can always access/leave the teleport.
self.directions = {direction_name: direction,
direction: direction_name}
self.directions = {direction_name: direction, direction: direction_name}
else:
# called while traversing a normal link
self.directions = {start_direction: direction_name,
direction_name: direction}
self.directions = {start_direction: direction_name, direction_name: direction}
return self.directions.get(start_direction)
@ -1016,6 +1031,7 @@ class SmartMapLink(MapLink):
# |
"""
multilink = True
def get_direction(self, start_direction):
@ -1027,8 +1043,11 @@ class SmartMapLink(MapLink):
if not self.directions:
directions = {}
neighbors = self.get_linked_neighbors()
nodes = [direction for direction, neighbor in neighbors.items()
if hasattr(neighbor, 'node_index')]
nodes = [
direction
for direction, neighbor in neighbors.items()
if hasattr(neighbor, "node_index")
]
if len(nodes) == 2:
# prefer link to these two nodes
@ -1042,7 +1061,9 @@ class SmartMapLink(MapLink):
"must have exactly two connections - either directly to "
"two nodes or connecting directly to one node and with exactly one other "
f"link direction. The neighbor(s) in directions {list(neighbors.keys())} do "
"not fulfill these criteria.", self)
"not fulfill these criteria.",
self,
)
self.directions = directions
return self.directions.get(start_direction)
@ -1071,20 +1092,26 @@ class InvisibleSmartMapLink(SmartMapLink):
# this allows for normal movement directions even if the invisible-node
# is marked with a different symbol.
direction_aliases = {
'n': 'n', 'ne': 'ne', 'e': 'e', 'se': 'se',
's': 's', 'sw': 'sw', 'w': 'w', 'nw': 'nw'
"n": "n",
"ne": "ne",
"e": "e",
"se": "se",
"s": "s",
"sw": "sw",
"w": "w",
"nw": "nw",
}
# replace current link position with what the smart links "should" look like
display_symbol_aliases = {
(('n', 's'), ('s', 'n')): '|',
(('n', 's'),): 'v',
(('s', 'n')): '^',
(('e', 'w'), ('w', 'e')): '-',
(('e', 'w'),): '>',
(('w', 'e'),): '<',
(('nw', 'se'), ('sw', 'ne')): '\\',
(('ne', 'sw'), ('sw', 'ne')): '/',
(("n", "s"), ("s", "n")): "|",
(("n", "s"),): "v",
(("s", "n")): "^",
(("e", "w"), ("w", "e")): "-",
(("e", "w"),): ">",
(("w", "e"),): "<",
(("nw", "se"), ("sw", "ne")): "\\",
(("ne", "sw"), ("sw", "ne")): "/",
}
def get_display_symbol(self):
@ -1098,12 +1125,10 @@ class InvisibleSmartMapLink(SmartMapLink):
"""
if not hasattr(self, "_cached_display_symbol"):
legend = self.xymap.legend
default_symbol = (
self.symbol if self.display_symbol is None else self.display_symbol)
default_symbol = self.symbol if self.display_symbol is None else self.display_symbol
self._cached_display_symbol = default_symbol
dirtuple = tuple((key, self.directions[key])
for key in sorted(self.directions.keys()))
dirtuple = tuple((key, self.directions[key]) for key in sorted(self.directions.keys()))
replacement_symbol = self.display_symbol_aliases.get(dirtuple, default_symbol)
@ -1112,16 +1137,19 @@ class InvisibleSmartMapLink(SmartMapLink):
if node_or_link_class:
# initiate class in the current location and run get_display_symbol
# to get what it would show.
self._cached_display_symbol = (
node_or_link_class(self.x, self.y, self.Z).get_display_symbol())
self._cached_display_symbol = node_or_link_class(
self.x, self.y, self.Z
).get_display_symbol()
return self._cached_display_symbol
# ----------------------------------
# Default nodes and link classes
class BasicMapNode(MapNode):
"""A map node/room"""
symbol = "#"
prototype = "xyz_room"
@ -1129,20 +1157,25 @@ class BasicMapNode(MapNode):
class InterruptMapNode(MapNode):
"""A point of interest node/room. Pathfinder will ignore but auto-stepper will
stop here if passing through. Beginner-Tutorial from here is fine."""
symbol = "I"
display_symbol = "#"
interrupt_path = True
prototype = "xyz_room"
class MapTransitionNode(TransitionMapNode):
"""Transition-target node to other map. This is not actually spawned in-game."""
symbol = "T"
display_symbol = " "
prototype = None # important to leave None!
target_map_xyz = (None, None, None) # must be set manually
class NSMapLink(MapLink):
"""Two-way, North-South link"""
symbol = "|"
display_symbol = "||"
directions = {"n": "s", "s": "n"}
@ -1151,6 +1184,7 @@ class NSMapLink(MapLink):
class EWMapLink(MapLink):
"""Two-way, East-West link"""
symbol = "-"
directions = {"e": "w", "w": "e"}
prototype = "xyz_exit"
@ -1158,6 +1192,7 @@ class EWMapLink(MapLink):
class NESWMapLink(MapLink):
"""Two-way, NorthWest-SouthWest link"""
symbol = "/"
directions = {"ne": "sw", "sw": "ne"}
prototype = "xyz_exit"
@ -1165,6 +1200,7 @@ class NESWMapLink(MapLink):
class SENWMapLink(MapLink):
"""Two-way, SouthEast-NorthWest link"""
symbol = "\\"
directions = {"se": "nw", "nw": "se"}
prototype = "xyz_exit"
@ -1172,22 +1208,23 @@ class SENWMapLink(MapLink):
class PlusMapLink(MapLink):
"""Two-way, crossing North-South and East-West links"""
symbol = "+"
directions = {"s": "n", "n": "s",
"e": "w", "w": "e"}
directions = {"s": "n", "n": "s", "e": "w", "w": "e"}
prototype = "xyz_exit"
class CrossMapLink(MapLink):
"""Two-way, crossing NorthEast-SouthWest and SouthEast-NorthWest links"""
symbol = "x"
directions = {"ne": "sw", "sw": "ne",
"se": "nw", "nw": "se"}
directions = {"ne": "sw", "sw": "ne", "se": "nw", "nw": "se"}
prototype = "xyz_exit"
class NSOneWayMapLink(MapLink):
"""One-way North-South link"""
symbol = "v"
directions = {"n": "s"}
prototype = "xyz_exit"
@ -1195,6 +1232,7 @@ class NSOneWayMapLink(MapLink):
class SNOneWayMapLink(MapLink):
"""One-way South-North link"""
symbol = "^"
directions = {"s": "n"}
prototype = "xyz_exit"
@ -1202,6 +1240,7 @@ class SNOneWayMapLink(MapLink):
class EWOneWayMapLink(MapLink):
"""One-way East-West link"""
symbol = "<"
directions = {"e": "w"}
prototype = "xyz_exit"
@ -1209,6 +1248,7 @@ class EWOneWayMapLink(MapLink):
class WEOneWayMapLink(MapLink):
"""One-way West-East link"""
symbol = ">"
directions = {"w": "e"}
prototype = "xyz_exit"
@ -1216,21 +1256,39 @@ class WEOneWayMapLink(MapLink):
class UpMapLink(SmartMapLink):
"""Up direction. Note that this stays on the same z-coord so it's a 'fake' up."""
symbol = 'u'
symbol = "u"
# all movement over this link is 'up', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
direction_aliases = {
"n": symbol,
"ne": symbol,
"e": symbol,
"se": symbol,
"s": symbol,
"sw": symbol,
"w": symbol,
"nw": symbol,
}
spawn_aliases = {direction: ("up", "u") for direction in direction_aliases}
prototype = "xyz_exit"
class DownMapLink(UpMapLink):
"""Down direction. Note that this stays on the same z-coord, so it's a 'fake' down."""
symbol = 'd'
symbol = "d"
# all movement over this link is 'down', regardless of where on the xygrid we move.
direction_aliases = {'n': symbol, 'ne': symbol, 'e': symbol, 'se': symbol,
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol}
direction_aliases = {
"n": symbol,
"ne": symbol,
"e": symbol,
"se": symbol,
"s": symbol,
"sw": symbol,
"w": symbol,
"nw": symbol,
}
spawn_aliases = {direction: ("down", "d") for direction in direction_aliases}
prototype = "xyz_exit"
@ -1238,6 +1296,7 @@ class DownMapLink(UpMapLink):
class InterruptMapLink(InvisibleSmartMapLink):
"""A (still passable) link. Pathfinder will treat this as any link, but auto-stepper
will always abort before crossing this link - so this must be crossed manually."""
symbol = "i"
interrupt_path = True
prototype = "xyz_exit"
@ -1250,14 +1309,24 @@ class BlockedMapLink(InvisibleSmartMapLink):
link in any paths.
"""
symbol = 'b'
weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL,
's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL}
symbol = "b"
weights = {
"n": BIGVAL,
"ne": BIGVAL,
"e": BIGVAL,
"se": BIGVAL,
"s": BIGVAL,
"sw": BIGVAL,
"w": BIGVAL,
"nw": BIGVAL,
}
prototype = "xyz_exit"
class RouterMapLink(SmartRerouterMapLink):
"""A link that connects other links to build 'knees', pass-throughs etc."""
symbol = "o"
@ -1266,7 +1335,8 @@ class TeleporterMapLink(SmartTeleporterMapLink):
Teleporter links. Must appear in pairs on the same xy map. To make it one-way, add additional
one-way link out of the teleporter on one side.
"""
symbol = 't'
symbol = "t"
# all map components; used as base if not overridden
@ -1291,5 +1361,5 @@ LEGEND = {
"d": DownMapLink,
"b": BlockedMapLink,
"i": InterruptMapLink,
't': TeleporterMapLink,
"t": TeleporterMapLink,
}

View file

@ -28,6 +28,7 @@ class XYZGrid(DefaultScript):
Main grid class. This organizes the Maps based on their name/Z-coordinate.
"""
def at_script_creation(self):
"""
What we store persistently is data used to create each map (the legends, names etc)
@ -88,7 +89,7 @@ class XYZGrid(DefaultScript):
"""
return XYZRoom.objects.filter_xyz(xyz=xyz, **kwargs)
def get_exit(self, xyz, name='north', **kwargs):
def get_exit(self, xyz, name="north", **kwargs):
"""
Get one or more exit object at coordinate.
@ -102,7 +103,7 @@ class XYZGrid(DefaultScript):
Queryset: A queryset of XYZExit(s) found.
"""
kwargs['db_key'] = name
kwargs["db_key"] = name
return XYZExit.objects.filter_xyz_exit(xyz=xyz, **kwargs)
def maps_from_module(self, module_path):
@ -127,7 +128,7 @@ class XYZGrid(DefaultScript):
if not mapdata:
self.log(f"Could not find or load map from {module_path}.")
return
mapdata['module_path'] = module_path
mapdata["module_path"] = module_path
return map_data_list
def reload(self):
@ -154,9 +155,9 @@ class XYZGrid(DefaultScript):
# we reload the map from module
new_mapdata = loaded_mapdata.get(zcoord)
if not new_mapdata:
if 'module_path' in old_mapdata:
for mapdata in self.maps_from_module(old_mapdata['module_path']):
loaded_mapdata[mapdata['zcoord']] = mapdata
if "module_path" in old_mapdata:
for mapdata in self.maps_from_module(old_mapdata["module_path"]):
loaded_mapdata[mapdata["zcoord"]] = mapdata
else:
# nowhere to reload from - use what we have
loaded_mapdata[zcoord] = old_mapdata
@ -198,7 +199,7 @@ class XYZGrid(DefaultScript):
"""
for mapdata in mapdatas:
zcoord = mapdata.get('zcoord')
zcoord = mapdata.get("zcoord")
if not zcoord:
raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.")
@ -220,7 +221,7 @@ class XYZGrid(DefaultScript):
if remove_objects:
# we can't batch-delete because we want to run the .delete
# method that also wipes exits and moves content to save locations
for xyzroom in XYZRoom.objects.filter_xyz(xyz=('*', '*', zcoord)):
for xyzroom in XYZRoom.objects.filter_xyz(xyz=("*", "*", zcoord)):
xyzroom.delete()
self.reload()
@ -234,7 +235,7 @@ class XYZGrid(DefaultScript):
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)
super().delete()
def spawn(self, xyz=('*', '*', '*'), directions=None):
def spawn(self, xyz=("*", "*", "*"), directions=None):
"""
Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
or coordinate.
@ -255,7 +256,7 @@ class XYZGrid(DefaultScript):
"""
x, y, z = xyz
wildcard = '*'
wildcard = "*"
if z == wildcard:
xymaps = self.grid
@ -293,8 +294,10 @@ def get_xyzgrid(print_errors=True):
xyzgrid.reload()
return xyzgrid
elif len(xyzgrid) > 1:
("Warning: More than one XYZGrid instances were found. This is an error and "
"only the first one will be used. Delete the other one(s) manually.")
(
"Warning: More than one XYZGrid instances were found. This is an error and "
"only the first one will be used. Delete the other one(s) manually."
)
xyzgrid = xyzgrid[0]
try:
if not xyzgrid.ndb.loaded:

View file

@ -31,7 +31,8 @@ class XYZManager(ObjectManager):
efficiently querying the room in the database based on XY coordinates.
"""
def filter_xyz(self, xyz=('*', '*', '*'), **kwargs):
def filter_xyz(self, xyz=("*", "*", "*"), **kwargs):
"""
Filter queryset based on XYZ position on the grid. The Z-position is the name of the XYMap
Set a coordinate to `'*'` to act as a wildcard (setting all coords to `*` will thus find
@ -49,23 +50,28 @@ class XYZManager(ObjectManager):
"""
x, y, z = xyz
wildcard = '*'
wildcard = "*"
return (
self
.filter_family(**kwargs)
self.filter_family(**kwargs)
.filter(
Q() if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY))
Q()
if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
)
.filter(
Q() if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY))
Q()
if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
)
.filter(
Q() if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY))
Q()
if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
)
)
def get_xyz(self, xyz=(0, 0, 'map'), **kwargs):
def get_xyz(self, xyz=(0, 0, "map"), **kwargs):
"""
Always return a single matched entity directly. This accepts no `*`-wildcards.
This will also find children of XYZRooms on the given coordinates.
@ -93,8 +99,9 @@ class XYZManager(ObjectManager):
# error - mimic default get() behavior but with a little more info
x, y, z = xyz
inp = (f"Query: xyz=({x},{y},{z}), " +
",".join(f"{key}={val}" for key, val in kwargs.items()))
inp = f"Query: xyz=({x},{y},{z}), " + ",".join(
f"{key}={val}" for key, val in kwargs.items()
)
if ncount > 1:
raise self.model.MultipleObjectsReturned(inp)
else:
@ -108,8 +115,7 @@ class XYZExitManager(XYZManager):
"""
def filter_xyz_exit(self, xyz=('*', '*', '*'),
xyz_destination=('*', '*', '*'), **kwargs):
def filter_xyz_exit(self, xyz=("*", "*", "*"), xyz_destination=("*", "*", "*"), **kwargs):
"""
Used by exits (objects with a source and -destination property).
Find all exits out of a source or to a particular destination. This will also find
@ -138,32 +144,43 @@ class XYZExitManager(XYZManager):
"""
x, y, z = xyz
xdest, ydest, zdest = xyz_destination
wildcard = '*'
wildcard = "*"
return (
self
.filter_family(**kwargs)
self.filter_family(**kwargs)
.filter(
Q() if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY))
Q()
if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
)
.filter(
Q() if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY))
Q()
if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
)
.filter(
Q() if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY))
Q()
if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
)
.filter(
Q() if xdest == wildcard
else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY))
Q()
if xdest == wildcard
else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
)
.filter(
Q() if ydest == wildcard
else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY))
Q()
if ydest == wildcard
else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)
)
.filter(
Q() if zdest == wildcard
else Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY))
Q()
if zdest == wildcard
else Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)
)
)
def get_xyz_exit(self, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'), **kwargs):
def get_xyz_exit(self, xyz=(0, 0, "map"), xyz_destination=(0, 0, "map"), **kwargs):
"""
Used by exits (objects with a source and -destination property). Get a single
exit. All source/destination coordinates (as well as the map's name) are required.
@ -199,8 +216,7 @@ class XYZExitManager(XYZManager):
try:
return (
self
.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
self.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
.filter(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
.filter(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
.filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
@ -209,10 +225,12 @@ class XYZExitManager(XYZManager):
.get(**kwargs)
)
except self.model.DoesNotExist:
inp = (f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," +
",".join(f"{key}={val}" for key, val in kwargs.items()))
raise self.model.DoesNotExist(f"{self.model.__name__} "
f"matching query {inp} does not exist.")
inp = f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," + ",".join(
f"{key}={val}" for key, val in kwargs.items()
)
raise self.model.DoesNotExist(
f"{self.model.__name__} " f"matching query {inp} does not exist."
)
class XYZRoom(DefaultRoom):
@ -244,10 +262,10 @@ class XYZRoom(DefaultRoom):
# default settings for map visualization
map_display = True
map_mode = 'nodes' # or 'scan'
map_mode = "nodes" # or 'scan'
map_visual_range = 2
map_character_symbol = "|g@|n"
map_align = 'c'
map_align = "c"
map_target_path_style = "|y{display_symbol}|n"
map_fill_all = True
map_separator_char = "|x~|n"
@ -267,8 +285,10 @@ class XYZRoom(DefaultRoom):
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
if x is None or y is None or z is None:
# don't cache unfinished coordinate (probably tags have not finished saving)
return tuple(int(coord) if coord is not None and coord.isdigit() else coord
for coord in (x, y, z))
return tuple(
int(coord) if coord is not None and coord.isdigit() else coord
for coord in (x, y, z)
)
# cache result, convert to correct types (tags are strings)
self._xyz = tuple(int(coord) if coord.isdigit() else coord for coord in (x, y, z))
@ -290,7 +310,7 @@ class XYZRoom(DefaultRoom):
return self._xymap
@classmethod
def create(cls, key, account=None, xyz=(0, 0, 'map'), **kwargs):
def create(cls, key, account=None, xyz=(0, 0, "map"), **kwargs):
"""
Creation method aware of XYZ coordinates.
@ -316,14 +336,18 @@ class XYZRoom(DefaultRoom):
try:
x, y, z = xyz
except ValueError:
return None, [f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) "
"coordinate of ints/strings."]
return None, [
f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) "
"coordinate of ints/strings."
]
existing_query = cls.objects.filter_xyz(xyz=(x, y, z))
if existing_query.exists():
existing_room = existing_query.first()
return None, [f"XYRoom XYZ=({x},{y},{z}) already exists "
f"(existing room is named '{existing_room.db_key}')!"]
return None, [
f"XYRoom XYZ=({x},{y},{z}) already exists "
f"(existing room is named '{existing_room.db_key}')!"
]
tags = (
(str(x), MAP_X_TAG_CATEGORY),
@ -410,26 +434,29 @@ class XYZRoom(DefaultRoom):
xyz = self.xyz
xymap = self.xyzgrid.get_map(xyz[2])
if xymap and kwargs.get('map_display', xymap.options.get("map_display", self.map_display)):
if xymap and kwargs.get("map_display", xymap.options.get("map_display", self.map_display)):
# show the near-area map.
map_character_symbol = kwargs.get(
'map_character_symbol',
xymap.options.get("map_character_symbol", self.map_character_symbol))
"map_character_symbol",
xymap.options.get("map_character_symbol", self.map_character_symbol),
)
map_visual_range = kwargs.get(
"map_visual_range", xymap.options.get("map_visual_range", self.map_visual_range))
map_mode = kwargs.get(
"map_mode", xymap.options.get("map_mode", self.map_mode))
map_align = kwargs.get(
"map_align", xymap.options.get("map_align", self.map_align))
"map_visual_range", xymap.options.get("map_visual_range", self.map_visual_range)
)
map_mode = kwargs.get("map_mode", xymap.options.get("map_mode", self.map_mode))
map_align = kwargs.get("map_align", xymap.options.get("map_align", self.map_align))
map_target_path_style = kwargs.get(
"map_target_path_style",
xymap.options.get("map_target_path_style", self.map_target_path_style))
xymap.options.get("map_target_path_style", self.map_target_path_style),
)
map_area_client = kwargs.get(
"map_fill_all", xymap.options.get("map_fill_all", self.map_fill_all))
"map_fill_all", xymap.options.get("map_fill_all", self.map_fill_all)
)
map_separator_char = kwargs.get(
"map_separator_char",
xymap.options.get("map_separator_char", self.map_separator_char))
xymap.options.get("map_separator_char", self.map_separator_char),
)
client_width, _ = looker.sessions.get()[0].get_client_size()
@ -438,15 +465,14 @@ class XYZRoom(DefaultRoom):
if map_area_client:
display_width = client_width
else:
display_width = max(map_width,
max(len(line) for line in room_desc.split("\n")))
display_width = max(map_width, max(len(line) for line in room_desc.split("\n")))
# align map
map_indent = 0
sep_width = display_width
if map_align == 'r':
if map_align == "r":
map_indent = max(0, display_width - map_width)
elif map_align == 'c':
elif map_align == "c":
map_indent = max(0, (display_width - map_width) // 2)
# data set by the goto/path-command, for displaying the shortest path
@ -462,7 +488,7 @@ class XYZRoom(DefaultRoom):
target_path_style=map_target_path_style,
character=map_character_symbol,
max_size=(display_width, None),
indent=map_indent
indent=map_indent,
)
sep = map_separator_char * sep_width
map_display = f"{sep}|n\n{map_display}\n{sep}"
@ -523,8 +549,16 @@ class XYZExit(DefaultExit):
return self._xyz_destination
@classmethod
def create(cls, key, account=None, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'),
location=None, destination=None, **kwargs):
def create(
cls,
key,
account=None,
xyz=(0, 0, "map"),
xyz_destination=(0, 0, "map"),
location=None,
destination=None,
**kwargs,
):
"""
Creation method aware of coordinates.
@ -559,23 +593,33 @@ class XYZExit(DefaultExit):
return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."]
else:
source = XYZRoom.objects.get_xyz(xyz=(x, y, z))
tags.extend(((str(x), MAP_X_TAG_CATEGORY),
(str(y), MAP_Y_TAG_CATEGORY),
(str(z), MAP_Z_TAG_CATEGORY)))
tags.extend(
(
(str(x), MAP_X_TAG_CATEGORY),
(str(y), MAP_Y_TAG_CATEGORY),
(str(z), MAP_Z_TAG_CATEGORY),
)
)
if destination:
dest = destination
else:
try:
xdest, ydest, zdest = xyz_destination
except ValueError:
return None, ["XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
"or a `destination`."]
return None, [
"XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
"or a `destination`."
]
else:
dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest))
tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY),
(str(ydest), MAP_YDEST_TAG_CATEGORY),
(str(zdest), MAP_ZDEST_TAG_CATEGORY)))
tags.extend(
(
(str(xdest), MAP_XDEST_TAG_CATEGORY),
(str(ydest), MAP_YDEST_TAG_CATEGORY),
(str(zdest), MAP_ZDEST_TAG_CATEGORY),
)
)
return DefaultExit.create(
key, source, dest,
account=account, tags=tags, typeclass=cls, **kwargs)
key, source, dest, account=account, tags=tags, typeclass=cls, **kwargs
)

View file

@ -6,4 +6,4 @@ Rolling dice - Griatch, 2012
from .dice import roll # noqa
from .dice import roll_dice # noqa
from .dice import CmdDice # noqa
from .dice import DiceCmdSet # noqa
from .dice import DiceCmdSet # noqa

View file

@ -58,8 +58,7 @@ from random import randint
from evennia import default_cmds, CmdSet
def roll(dicenum, dicetype, modifier=None,
conditional=None, return_tuple=False):
def roll(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
"""
This is a standard dice roller.
@ -141,6 +140,7 @@ def roll(dicenum, dicetype, modifier=None,
else:
return result
# legacy alias
roll_dice = roll

View file

@ -7,7 +7,7 @@ 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 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
from .rpsystem import RPSystemCmdSet # noqa
from .rpsystem import ContribRPObject # noqa
from .rpsystem import ContribRPRoom # noqa

View file

@ -315,7 +315,8 @@ class LanguageHandler(DefaultScript):
raise IndexError(
"Could not find a matching phoneme for the grammar "
f"'{match.group()}'. Make there is at least one phoneme matching this "
"combination of consonants and vowels.")
"combination of consonants and vowels."
)
translation[word.lower()] = new_word.lower()
if manual_translations:

View file

@ -513,7 +513,7 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
errors.append(_EMOTE_NOMATCH_ERROR.format(ref=marker_match.group()))
elif nmatches == 1:
# a unique match - parse into intermediary representation
case = '~' # retain original case of sdesc
case = "~" # retain original case of sdesc
if case_sensitive:
# case sensitive mode
# internal flags for the case used for the original /query
@ -526,14 +526,14 @@ def parse_sdescs_and_recogs(sender, candidates, string, search_mode=False, case_
# self-refs are kept as-is, others are parsed by case
matchtext = marker_match.group().lstrip(_PREFIX)
if matchtext.istitle():
case = 't'
case = "t"
elif matchtext.isupper():
case = '^'
case = "^"
elif matchtext.islower():
case = 'v'
case = "v"
key = "#%i%s" % (obj.id, case)
string = string[:istart0] + "{%s}" % key + string[istart + maxscore:]
string = string[:istart0] + "{%s}" % key + string[istart + maxscore :]
mapping[key] = obj
else:
@ -601,8 +601,9 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
"""
case_sensitive = kwargs.pop("case_sensitive", True)
try:
emote, obj_mapping = parse_sdescs_and_recogs(sender, receivers, emote,
case_sensitive=case_sensitive)
emote, obj_mapping = parse_sdescs_and_recogs(
sender, receivers, emote, case_sensitive=case_sensitive
)
emote, language_mapping = parse_language(sender, emote)
except (EmoteError, LanguageError) as err:
# handle all error messages, don't hide actual coding errors
@ -615,8 +616,8 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
# (the text could have nested object mappings).
emote = _RE_REF.sub(r"{{#\1}}", emote)
# if anonymous_add is passed as a kwarg, collect and remove it from kwargs
if 'anonymous_add' in kwargs:
anonymous_add = kwargs.pop('anonymous_add')
if "anonymous_add" in kwargs:
anonymous_add = kwargs.pop("anonymous_add")
if anonymous_add and not any(1 for tag in obj_mapping if tag.startswith(skey)):
# no self-reference in the emote - add to the end
obj_mapping[skey] = sender
@ -670,12 +671,13 @@ def send_emote(sender, receivers, emote, anonymous_add="first", **kwargs):
)
# make sure receiver always sees their real name
rkey_start = "#%i" % receiver.id
rkey_keep_case = rkey_start + '~' # signifies keeping the case
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)
receiver.key, receiver, ref=rkey_keep_case, **kwargs
)
# do the template replacement of the sdesc/recog {#num} markers
receiver.msg(sendemote.format(**receiver_sdesc_mapping), from_obj=sender, **kwargs)
@ -1709,14 +1711,14 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
if not sdesc:
return ""
ref = kwargs.get('ref', '~') # ~ to keep sdesc unchanged
if 't' in ref:
ref = kwargs.get("ref", "~") # ~ to keep sdesc unchanged
if "t" in ref:
# we only want to capitalize the first letter if there are many words
sdesc = sdesc.lower()
sdesc = sdesc[0].upper() + sdesc[1:] if len(sdesc) > 1 else sdesc.upper()
elif '^' in ref:
elif "^" in ref:
sdesc = sdesc.upper()
elif 'v' in ref:
elif "v" in ref:
sdesc = sdesc.lower()
return "|b%s|n" % sdesc

View file

@ -274,7 +274,7 @@ class TestRPSystem(BaseEvenniaTest):
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.assertLess(t2 - t1, 10**-4)
self.assertEqual(result, (Anything, self.speaker, self.speaker.key))

View file

@ -59,16 +59,28 @@ class TraitHandlerTest(_TraitHandlerBase):
super().setUp()
self.traithandler.add("test1", name="Test1", trait_type="trait")
self.traithandler.add(
"test2", name="Test2", trait_type="trait", value=["foo", {"1": [1, 2, 3]}, 4],
"test2",
name="Test2",
trait_type="trait",
value=["foo", {"1": [1, 2, 3]}, 4],
)
def test_add_trait(self):
self.assertEqual(
self._get_dbstore("test1"), {"name": "Test1", "trait_type": "trait", "value": None,}
self._get_dbstore("test1"),
{
"name": "Test1",
"trait_type": "trait",
"value": None,
},
)
self.assertEqual(
self._get_dbstore("test2"),
{"name": "Test2", "trait_type": "trait", "value": ["foo", {"1": [1, 2, 3]}, 4],},
{
"name": "Test2",
"trait_type": "trait",
"value": ["foo", {"1": [1, 2, 3]}, 4],
},
)
self.assertEqual(len(self.traithandler), 2)
@ -328,7 +340,12 @@ class TestTraitCounter(_TraitHandlerBase):
max=10,
extra_val1="xvalue1",
extra_val2="xvalue2",
descs={0: "range0", 2: "range1", 5: "range2", 7: "range3",},
descs={
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
)
self.trait = self.traithandler.get("test1")
@ -348,7 +365,12 @@ class TestTraitCounter(_TraitHandlerBase):
"max": 10,
"extra_val1": "xvalue1",
"extra_val2": "xvalue2",
"descs": {0: "range0", 2: "range1", 5: "range2", 7: "range3",},
"descs": {
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
"rate": 0,
"ratetarget": None,
"last_update": None,
@ -507,7 +529,12 @@ class TestTraitCounterTimed(_TraitHandlerBase):
max=100,
extra_val1="xvalue1",
extra_val2="xvalue2",
descs={0: "range0", 2: "range1", 5: "range2", 7: "range3",},
descs={
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
rate=1,
ratetarget=None,
)
@ -579,7 +606,12 @@ class TestTraitGauge(_TraitHandlerBase):
mod=2,
extra_val1="xvalue1",
extra_val2="xvalue2",
descs={0: "range0", 2: "range1", 5: "range2", 7: "range3",},
descs={
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
)
self.trait = self.traithandler.get("test1")
@ -598,7 +630,12 @@ class TestTraitGauge(_TraitHandlerBase):
"min": 0,
"extra_val1": "xvalue1",
"extra_val2": "xvalue2",
"descs": {0: "range0", 2: "range1", 5: "range2", 7: "range3",},
"descs": {
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
"rate": 0,
"ratetarget": None,
"last_update": None,
@ -763,7 +800,12 @@ class TestTraitGaugeTimed(_TraitHandlerBase):
min=0,
extra_val1="xvalue1",
extra_val2="xvalue2",
descs={0: "range0", 2: "range1", 5: "range2", 7: "range3",},
descs={
0: "range0",
2: "range1",
5: "range2",
7: "range3",
},
rate=1,
ratetarget=None,
)
@ -831,8 +873,20 @@ class TestNumericTraitOperators(BaseEvenniaTestCase):
def setUp(self):
# direct instantiation for testing only; use TraitHandler in production
self.st = traits.Trait({"name": "Strength", "trait_type": "trait", "value": 8,})
self.at = traits.Trait({"name": "Attack", "trait_type": "trait", "value": 4,})
self.st = traits.Trait(
{
"name": "Strength",
"trait_type": "trait",
"value": 8,
}
)
self.at = traits.Trait(
{
"name": "Attack",
"trait_type": "trait",
"value": 4,
}
)
def tearDown(self):
self.st, self.at = None, None

View file

@ -526,6 +526,7 @@ class MandatoryTraitKey:
"""
class TraitHandler:
"""
Factory class that instantiates Trait objects. Must be assigned as a property
@ -794,10 +795,7 @@ class TraitProperty:
trait = traithandler.get(self._trait_key)
if trait is None:
# initialize the trait
traithandler.add(
self._trait_key,
**self._trait_properties
)
traithandler.add(self._trait_key, **self._trait_properties)
trait = traithandler.get(self._trait_key) # caches it in the traithandler
self._cache[instance] = trait
return self._cache[instance]
@ -809,6 +807,7 @@ class TraitProperty:
"""
pass
# Parent Trait class

View file

@ -42,6 +42,7 @@ from evennia.utils.utils import delay, repeat, interactive
# Commands for the state when the lid covering the button is closed.
class CmdPushLidClosed(Command):
"""
Push the red button (lid closed)
@ -121,23 +122,27 @@ class CmdSmashGlass(Command):
"""
rand = random.random()
self.caller.location.msg_contents(
f"{self.caller.name} tries to smash the glass of the button.",
exclude=self.caller)
f"{self.caller.name} tries to smash the glass of the button.", exclude=self.caller
)
if rand < 0.2:
string = ("You smash your hand against the glass"
" with all your might. The lid won't budge"
" but you cause quite the tremor through the button's mount."
"\nIt looks like the button's lamp stopped working for the time being, "
"but the lid is still as closed as ever.")
string = (
"You smash your hand against the glass"
" with all your might. The lid won't budge"
" but you cause quite the tremor through the button's mount."
"\nIt looks like the button's lamp stopped working for the time being, "
"but the lid is still as closed as ever."
)
# self.obj is the button itself
self.obj.break_lamp()
elif rand < 0.6:
string = "You hit the lid hard. It doesn't move an inch."
else:
string = ("You place a well-aimed fist against the glass of the lid."
" Unfortunately all you get is a pain in your hand. Maybe"
" you should just try to just ... open the lid instead?")
string = (
"You place a well-aimed fist against the glass of the lid."
" Unfortunately all you get is a pain in your hand. Maybe"
" you should just try to just ... open the lid instead?"
)
self.caller.msg(string)
@ -165,8 +170,8 @@ class CmdOpenLid(Command):
string += "the lid will soon close again."
self.caller.msg(string)
self.caller.location.msg_contents(
f"{self.caller.name} opens the lid of the button.",
exclude=self.caller)
f"{self.caller.name} opens the lid of the button.", exclude=self.caller
)
self.obj.to_open_state()
@ -200,6 +205,7 @@ class LidClosedCmdSet(CmdSet):
# Commands for the state when the button's protective cover is open - now the
# push command will work. You can also close the lid again.
class CmdPushLidOpen(Command):
"""
Push the red button
@ -225,15 +231,15 @@ class CmdPushLidOpen(Command):
"""
# pause a little between each message.
self.caller.msg("You reach out to press the big red button ...")
yield(2) # pause 2s before next message
yield (2) # pause 2s before next message
self.caller.msg("\n\n|wBOOOOM! A bright light blinds you!|n")
yield(1) # pause 1s before next message
yield (1) # pause 1s before next message
self.caller.msg("\n\n|xThe world goes dark ...|n")
name = self.caller.name
self.caller.location.msg_contents(
f"{name} presses the button. BOOM! {name} is blinded by a flash!",
exclude=self.caller)
f"{name} presses the button. BOOM! {name} is blinded by a flash!", exclude=self.caller
)
self.obj.blind_target(self.caller)
@ -259,8 +265,8 @@ class CmdCloseLid(Command):
# this will clean out scripts dependent on lid being open.
self.caller.msg("You close the button's lid. It clicks back into place.")
self.caller.location.msg_contents(
f"{self.caller.name} closes the button's lid.",
exclude=self.caller)
f"{self.caller.name} closes the button's lid.", exclude=self.caller
)
class LidOpenCmdSet(CmdSet):
@ -286,6 +292,7 @@ class LidOpenCmdSet(CmdSet):
# Commands for when the button has been pushed and the player is blinded. This
# replaces commands on the player making them 'blind' for a while.
class CmdBlindLook(Command):
"""
Looking around in darkness
@ -317,12 +324,14 @@ class CmdBlindLook(Command):
string = "You fumble around, hands outstretched. You bump your knee."
else:
# trying to look
string = ("You are temporarily blinded by the flash. "
"Until it wears off, all you can do is feel around blindly.")
string = (
"You are temporarily blinded by the flash. "
"Until it wears off, all you can do is feel around blindly."
)
self.caller.msg(string)
self.caller.location.msg_contents(
f"{self.caller.name} stumbles around, blinded.",
exclude=self.caller)
f"{self.caller.name} stumbles around, blinded.", exclude=self.caller
)
class CmdBlindHelp(Command):
@ -420,20 +429,26 @@ class RedButton(DefaultObject):
`button = create_object(RedButton, ..., attributes=[('desc', 'my desc')])`.
"""
# these are the pre-set descriptions. Setting attributes will override
# these on the fly.
desc_closed_lid = ("This is a large red button, inviting yet evil-looking. "
"A closed glass lid protects it.")
desc_open_lid = ("This is a large red button, inviting yet evil-looking. "
"Its glass cover is open and the button exposed.")
desc_closed_lid = (
"This is a large red button, inviting yet evil-looking. " "A closed glass lid protects it."
)
desc_open_lid = (
"This is a large red button, inviting yet evil-looking. "
"Its glass cover is open and the button exposed."
)
auto_close_msg = "The button's glass lid silently slides back in place."
lamp_breaks_msg = "The lamp flickers, the button going dark."
desc_add_lamp_broken = "\nThe big red button has stopped blinking for the time being."
# note that this is a list. A random message will display each time
blink_msgs = ["The red button flashes briefly.",
"The red button blinks invitingly.",
"The red button flashes. You know you wanna push it!"]
blink_msgs = [
"The red button flashes briefly.",
"The red button blinks invitingly.",
"The red button flashes. You know you wanna push it!",
]
def at_object_creation(self):
"""
@ -523,9 +538,9 @@ class RedButton(DefaultObject):
self.cmdset.add(LidOpenCmdSet)
# wait 20s then call self.to_closed_state with a message as argument
delay(35, self.to_closed_state,
self.db.auto_close_msg or self.auto_close_msg,
persistent=True)
delay(
35, self.to_closed_state, self.db.auto_close_msg or self.auto_close_msg, persistent=True
)
def _unblind_target(self, caller):
"""
@ -536,7 +551,8 @@ class RedButton(DefaultObject):
caller.msg("You blink feverishly as your eyesight slowly returns.")
self.location.msg_contents(
f"{caller.name} seems to be recovering their eyesight, blinking feverishly.",
exclude=caller)
exclude=caller,
)
def blind_target(self, caller):
"""
@ -554,8 +570,7 @@ class RedButton(DefaultObject):
# wait 20s then call self._unblind to remove blindness effect. The
# persistent=True means the delay should survive a server reload.
delay(20, self._unblind_target, caller,
persistent=True)
delay(20, self._unblind_target, caller, persistent=True)
def _unbreak_lamp(self):
"""

View file

@ -65,8 +65,10 @@ def info2(caller):
def info3(caller):
text = ("'Well ... I'm sort of busy so, have to go. NPC business. "
"Important stuff. You wouldn't understand.'")
text = (
"'Well ... I'm sort of busy so, have to go. NPC business. "
"Important stuff. You wouldn't understand.'"
)
options = (
{"desc": "Oookay ... I won't keep you. Bye.", "goto": "END"},
@ -91,15 +93,15 @@ def END(caller):
class CmdTalk(default_cmds.MuxCommand):
"""
Talks to an npc
Talks to an npc
Usage:
talk
Usage:
talk
This command is only available if a talkative non-player-character
(NPC) is actually present. It will strike up a conversation with
that NPC and give you options on what to talk about.
"""
This command is only available if a talkative non-player-character
(NPC) is actually present. It will strike up a conversation with
that NPC and give you options on what to talk about.
"""
key = "talk"
locks = "cmd:all()"
@ -113,8 +115,11 @@ class CmdTalk(default_cmds.MuxCommand):
# Initiate the menu. Change this if you are putting this on
# some other custom NPC class.
EvMenu(self.caller, "evennia.contrib.tutorials.talking_npc.talking_npc",
startnode="menu_start_node")
EvMenu(
self.caller,
"evennia.contrib.tutorials.talking_npc.talking_npc",
startnode="menu_start_node",
)
class TalkingCmdSet(CmdSet):

View file

@ -1158,7 +1158,8 @@ class TutorialWeaponRack(TutorialObject):
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
|wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
|wdefend/parry|n - protect yourself and make yourself harder to hit.)
""").strip()
"""
).strip()
self.db.no_more_weapons_msg = "you find nothing else of use."
self.db.available_weapons = ["knife", "dagger", "sword", "club"]

View file

@ -78,6 +78,7 @@ class CmdTutorial(Command):
helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
caller.msg(helptext)
# for the @detail command we inherit from MuxCommand, since
# we want to make use of MuxCommand's pre-parsing of '=' in the
# argument.
@ -202,22 +203,26 @@ class CmdTutorialLook(default_cmds.CmdLook):
looking_at_obj.at_desc(looker=caller)
return
class CmdTutorialGiveUp(default_cmds.MuxCommand):
"""
Give up the tutorial-world quest and return to Limbo, the start room of the
server.
"""
key = "give up"
aliases = ['abort']
aliases = ["abort"]
def func(self):
outro_room = OutroRoom.objects.all()
if outro_room:
outro_room = outro_room[0]
else:
self.caller.msg("That didn't work (seems like a bug). "
"Try to use the |wteleport|n command instead.")
self.caller.msg(
"That didn't work (seems like a bug). "
"Try to use the |wteleport|n command instead."
)
return
self.caller.move_to(outro_room)
@ -312,6 +317,7 @@ class TutorialStartExit(DefaultExit):
will also clean up the intro command.
"""
def at_object_creation(self):
self.cmdset.add(CmdSetEvenniaIntro, persistent=True)
@ -397,6 +403,7 @@ SUPERUSER_WARNING = (
#
# -------------------------------------------------------------
class CmdEvenniaIntro(Command):
"""
Start the Evennia intro wizard.
@ -405,10 +412,12 @@ class CmdEvenniaIntro(Command):
intro
"""
key = "intro"
def func(self):
from .intro_menu import init_menu
# quell also superusers
if self.caller.account:
self.caller.msg("Auto-quelling permissions while in intro ...")
@ -463,6 +472,7 @@ class IntroRoom(TutorialRoom):
character.account.execute_cmd("quell")
character.msg("(Auto-quelling while in tutorial-world)")
# -------------------------------------------------------------
#
# Bridge - unique room
@ -1176,4 +1186,3 @@ class OutroRoom(TutorialRoom):
def at_object_leave(self, character, destination):
if character.account:
character.account.execute_cmd("unquell")

View file

@ -12,10 +12,8 @@ from .server import AuditedServerSession
from evennia.server.sessionhandler import SESSIONS
@override_settings(
AUDIT_MASKS=[])
@override_settings(AUDIT_MASKS=[])
class AuditingTest(BaseEvenniaTest):
@patch("evennia.server.sessionhandler._ServerSession", AuditedServerSession)
def setup_session(self):
"""Overrides default one in EvenniaTest"""
@ -29,8 +27,10 @@ class AuditingTest(BaseEvenniaTest):
SESSIONS.login(session, self.account, testmode=True)
self.session = session
@patch("evennia.contrib.utils.auditing.server.AUDIT_CALLBACK",
"evennia.contrib.utils.auditing.outputs.to_syslog")
@patch(
"evennia.contrib.utils.auditing.server.AUDIT_CALLBACK",
"evennia.contrib.utils.auditing.outputs.to_syslog",
)
@patch("evennia.contrib.utils.auditing.server.AUDIT_IN", True)
@patch("evennia.contrib.utils.auditing.server.AUDIT_OUT", True)
def test_mask(self):
@ -100,8 +100,10 @@ class AuditingTest(BaseEvenniaTest):
for secret in secrets:
self.assertEqual(self.session.mask(secret), secret)
@patch("evennia.contrib.utils.auditing.server.AUDIT_CALLBACK",
"evennia.contrib.utils.auditing.outputs.to_syslog")
@patch(
"evennia.contrib.utils.auditing.server.AUDIT_CALLBACK",
"evennia.contrib.utils.auditing.outputs.to_syslog",
)
@patch("evennia.contrib.utils.auditing.server.AUDIT_IN", True)
@patch("evennia.contrib.utils.auditing.server.AUDIT_OUT", True)
def test_audit(self):

View file

@ -5,6 +5,6 @@ FieldFill contrib - Tim Ashley Jenkins 2018
from .fieldfill import FieldEvMenu # noqa
from .fieldfill import CmdTestMenu # noqa
from .fieldfill import init_fill_field # noqa
from .fieldfill import form_template_to_dict # noqa
from .fieldfill import display_formdata # noqa
from .fieldfill import init_fill_field # noqa
from .fieldfill import form_template_to_dict # noqa
from .fieldfill import display_formdata # noqa

View file

@ -5,4 +5,4 @@ Pseudo-random generator - vlgeoff 2017
from .random_string_generator import RandomStringGenerator # noqa
from .random_string_generator import RandomStringGeneratorScript # noqa
from .random_string_generator import RejectedRegex, ExhaustedGenerator # noqa
from .random_string_generator import RejectedRegex, ExhaustedGenerator # noqa

View file

@ -156,8 +156,10 @@ class RandomStringGenerator:
self._find_elements(regex)
def __repr__(self):
return "<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format(
self.name
return (
"<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format(
self.name
)
)
def _get_script(self):
@ -169,7 +171,8 @@ class RandomStringGenerator:
script = ScriptDB.objects.get(db_key="generator_script")
except ScriptDB.DoesNotExist:
script = create_script(
"evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript")
"evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript"
)
type(self).script = script
return script

View file

@ -19,6 +19,7 @@ class Command(BaseCommand):
here. Without setting one, the parent's docstring will show (like now).
"""
# Each Command class implements the following methods, called in this order
# (only func() is actually required):
#

View file

@ -157,6 +157,6 @@ class Object(DefaultObject):
at_say(speaker, message) - by default, called if an object inside this
object speaks
"""
"""
pass

View file

@ -13,6 +13,7 @@ Search the Django documentation for "URL dispatcher" for more help.
"""
from django.urls import path, include
# default evennia patterns
from evennia.web.urls import urlpatterns as evennia_default_urlpatterns
@ -24,7 +25,6 @@ urlpatterns = [
path("webclient/", include("web.webclient.urls")),
# web admin
path("admin/", include("web.admin.urls")),
# add any extra urls here:
# path("mypath/", include("path.to.my.urls.file")),
]

View file

@ -27,7 +27,7 @@ Each dict is on the form
HELP_ENTRY_DICTS = [
{
"key": "evennia",
"aliases": ['ev'],
"aliases": ["ev"],
"category": "General",
"locks": "read:perm(Developer)",
"text": """
@ -51,7 +51,7 @@ HELP_ENTRY_DICTS = [
There is also a discord channel you can find from the sidebard on evennia.com.
"""
""",
},
{
"key": "building",
@ -60,6 +60,6 @@ HELP_ENTRY_DICTS = [
Evennia comes with a bunch of default building commands. You can
find a building tutorial in the evennia documentation.
"""
}
""",
},
]

View file

@ -69,8 +69,7 @@ from dataclasses import dataclass
from django.conf import settings
from django.urls import reverse
from django.utils.text import slugify
from evennia.utils.utils import (
variable_from_module, make_iter, all_from_module)
from evennia.utils.utils import variable_from_module, make_iter, all_from_module
from evennia.utils import logger
from evennia.utils.utils import lazy_property
from evennia.locks.lockhandler import LockHandler
@ -86,6 +85,7 @@ class FileHelpEntry:
help command.
"""
key: str
aliases: list
help_category: str
@ -147,7 +147,7 @@ class FileHelpEntry:
"""
try:
return reverse(
'help-entry-detail',
"help-entry-detail",
kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)},
)
except Exception:
@ -192,8 +192,7 @@ class FileHelpStorageHandler:
"""
Initialize the storage.
"""
self.help_file_modules = [str(part).strip()
for part in make_iter(help_file_modules)]
self.help_file_modules = [str(part).strip() for part in make_iter(help_file_modules)]
self.help_entries = []
self.help_entries_dict = {}
self.load()
@ -206,13 +205,11 @@ class FileHelpStorageHandler:
loaded_help_dicts = []
for module_or_path in self.help_file_modules:
help_dict_list = variable_from_module(
module_or_path, variable="HELP_ENTRY_DICTS"
)
help_dict_list = variable_from_module(module_or_path, variable="HELP_ENTRY_DICTS")
if not help_dict_list:
help_dict_list = [
dct for dct in all_from_module(module_or_path).values()
if isinstance(dct, dict)]
dct for dct in all_from_module(module_or_path).values() if isinstance(dct, dict)
]
if help_dict_list:
loaded_help_dicts.extend(help_dict_list)
else:
@ -223,19 +220,23 @@ class FileHelpStorageHandler:
unique_help_entries = {}
for dct in loaded_help_dicts:
key = dct.get('key').lower().strip()
category = dct.get('category', _DEFAULT_HELP_CATEGORY).strip()
aliases = list(dct.get('aliases', []))
entrytext = dct.get('text', '')
locks = dct.get('locks', '')
key = dct.get("key").lower().strip()
category = dct.get("category", _DEFAULT_HELP_CATEGORY).strip()
aliases = list(dct.get("aliases", []))
entrytext = dct.get("text", "")
locks = dct.get("locks", "")
if not key and entrytext:
logger.error(f"Cannot load file-help-entry (missing key or text): {dct}")
continue
unique_help_entries[key] = FileHelpEntry(
key=key, help_category=category, aliases=aliases, lock_storage=locks,
entrytext=entrytext)
key=key,
help_category=category,
aliases=aliases,
lock_storage=locks,
entrytext=entrytext,
)
self.help_entries_dict = unique_help_entries
self.help_entries = list(unique_help_entries.values())

View file

@ -23,7 +23,8 @@ class TestParseSubtopics(TestCase):
"""
self.maxDiff = None
entry = dedent("""
entry = dedent(
"""
Main topic text
# subtopics
## foo
@ -36,7 +37,9 @@ class TestParseSubtopics(TestCase):
Bar subcategory
### moo
Bar/Moo subcategory
""", indent=0)
""",
indent=0,
)
expected = {
None: "Main topic text",
"foo": {
@ -45,15 +48,10 @@ class TestParseSubtopics(TestCase):
None: "\nFoo/Moo subsub-category\n",
"dum": {
None: "\nFoo/Moo/Dum subsubsub-category\n",
}
},
},
},
"bar": {
None: "\nBar subcategory\n",
"moo": {
None: "\nBar/Moo subcategory"
}
}
"bar": {None: "\nBar subcategory\n", "moo": {None: "\nBar/Moo subcategory"}},
}
actual_result = help_utils.parse_entry_for_subcategories(entry)
@ -65,28 +63,30 @@ class TestParseSubtopics(TestCase):
"""
entry = dedent("""
entry = dedent(
"""
Main topic text
# SUBTOPICS
## creating extra stuff
Help on creating extra stuff.
""", indent=0)
""",
indent=0,
)
expected = {
None: "Main topic text",
"creating extra stuff": {
None: "\nHelp on creating extra stuff."
}
"creating extra stuff": {None: "\nHelp on creating extra stuff."},
}
actual_result = help_utils.parse_entry_for_subcategories(entry)
self.assertEqual(expected, actual_result)
# test filehelp system
HELP_ENTRY_DICTS = [
{
"key": "evennia",
"aliases": ['ev'],
"aliases": ["ev"],
"category": "General",
"text": """
Evennia is a MUD game server in Python.
@ -105,7 +105,7 @@ HELP_ENTRY_DICTS = [
There is also a discord channel you can find from the sidebard on evennia.com.
"""
""",
},
{
"key": "building",
@ -114,12 +114,11 @@ HELP_ENTRY_DICTS = [
Evennia comes with a bunch of default building commands. You can
find a building tutorial in the evennia documentation.
"""
}
""",
},
]
class TestFileHelp(TestCase):
"""
Test the File-help system
@ -135,7 +134,7 @@ class TestFileHelp(TestCase):
result = storage.all()
for inum, helpentry in enumerate(result):
self.assertEqual(HELP_ENTRY_DICTS[inum]['key'], helpentry.key)
self.assertEqual(HELP_ENTRY_DICTS[inum].get('aliases', []), helpentry.aliases)
self.assertEqual(HELP_ENTRY_DICTS[inum]['category'], helpentry.help_category)
self.assertEqual(HELP_ENTRY_DICTS[inum]['text'], helpentry.entrytext)
self.assertEqual(HELP_ENTRY_DICTS[inum]["key"], helpentry.key)
self.assertEqual(HELP_ENTRY_DICTS[inum].get("aliases", []), helpentry.aliases)
self.assertEqual(HELP_ENTRY_DICTS[inum]["category"], helpentry.help_category)
self.assertEqual(HELP_ENTRY_DICTS[inum]["text"], helpentry.entrytext)

View file

@ -19,11 +19,9 @@ _LUNR_EXCEPTION = None
_LUNR_GET_BUILDER = None
_LUNR_BUILDER_PIPELINE = None
_RE_HELP_SUBTOPICS_START = re.compile(
r"^\s*?#\s*?subtopics\s*?$", re.I + re.M)
_RE_HELP_SUBTOPICS_START = re.compile(r"^\s*?#\s*?subtopics\s*?$", re.I + re.M)
_RE_HELP_SUBTOPIC_SPLIT = re.compile(r"^\s*?(\#{2,6}\s*?\w+?[a-z0-9 \-\?!,\.]*?)$", re.M + re.I)
_RE_HELP_SUBTOPIC_PARSE = re.compile(
r"^(?P<nesting>\#{2,6})\s*?(?P<name>.*?)$", re.I + re.M)
_RE_HELP_SUBTOPIC_PARSE = re.compile(r"^(?P<nesting>\#{2,6})\s*?(?P<name>.*?)$", re.I + re.M)
MAX_SUBTOPIC_NESTING = 5
@ -57,6 +55,7 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields
from lunr import get_default_builder as _LUNR_GET_BUILDER
from lunr import stop_word_filter
from lunr.stemmer import stemmer
# from lunr.trimmer import trimmer
# pre-create a lunr index-builder pipeline where we've removed some of
@ -90,12 +89,7 @@ def help_search_with_index(query, candidate_entries, suggestion_maxnum=5, fields
builder.pipeline.reset()
builder.pipeline.add(*_LUNR_BUILDER_PIPELINE)
search_index = _LUNR(
ref="key",
fields=fields,
documents=indx,
builder=builder
)
search_index = _LUNR(ref="key", fields=fields, documents=indx, builder=builder)
try:
matches = search_index.search(query)[:suggestion_maxnum]
@ -175,7 +169,7 @@ def parse_entry_for_subcategories(entry):
"""
topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1)
structure = {None: topic.strip('\n')}
structure = {None: topic.strip("\n")}
if subtopics:
subtopics = subtopics[0]
@ -193,12 +187,13 @@ def parse_entry_for_subcategories(entry):
if subtopic_match:
# a new sub(-sub..) category starts.
mdict = subtopic_match.groupdict()
subtopic = mdict['name'].lower().strip()
new_nesting = len(mdict['nesting']) - 1
subtopic = mdict["name"].lower().strip()
new_nesting = len(mdict["nesting"]) - 1
if new_nesting > MAX_SUBTOPIC_NESTING:
raise RuntimeError(
f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics.")
f"Can have max {MAX_SUBTOPIC_NESTING} levels of nested help subtopics."
)
nestdiff = new_nesting - current_nesting
if nestdiff < 0:
@ -226,7 +221,5 @@ def parse_entry_for_subcategories(entry):
if key in dct:
dct = dct[key]
else:
dct[key] = {
None: part
}
dct[key] = {None: part}
return structure

View file

@ -43,6 +43,7 @@ def true(*args, **kwargs):
"""
return True
def all(*args, **kwargs):
return True

View file

@ -235,8 +235,11 @@ class LockHandler:
funcname, rest = (part.strip().strip(")") for part in funcstring.split("(", 1))
func = _LOCKFUNCS.get(funcname, None)
if not callable(func):
elist.append(_("Lock: lock-function '{lockfunc}' is not available.").format(
lockfunc=funcstring))
elist.append(
_("Lock: lock-function '{lockfunc}' is not available.").format(
lockfunc=funcstring
)
)
continue
args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
kwargs = dict(
@ -263,13 +266,16 @@ class LockHandler:
continue
if access_type in locks:
duplicates += 1
wlist.append(_(
"LockHandler on {obj}: access type '{access_type}' "
"changed from '{source}' to '{goal}' ".format(
obj=self.obj,
access_type=access_type,
source=locks[access_type][2],
goal=raw_lockstring))
wlist.append(
_(
"LockHandler on {obj}: access type '{access_type}' "
"changed from '{source}' to '{goal}' ".format(
obj=self.obj,
access_type=access_type,
source=locks[access_type][2],
goal=raw_lockstring,
)
)
)
locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring)
if wlist and WARNING_LOG:
@ -695,6 +701,7 @@ def check_lockstring(
access_type=access_type,
)
def check_perm(obj, permission, no_superuser_bypass=False):
"""
Shortcut for checking if an object has the given `permission`. If the
@ -713,6 +720,7 @@ def check_perm(obj, permission, no_superuser_bypass=False):
"""
from evennia.locks.lockfuncs import perm
if not no_superuser_bypass and obj.is_superuser:
return True
return perm(obj, None, permission)

View file

@ -215,12 +215,11 @@ class TestPermissionCheck(BaseEvenniaTest):
Test the PermissionHandler.check method
"""
def test_check__success(self):
"""Test combinations that should pass the check"""
self.assertEqual(
[perm for perm in self.char1.account.permissions.all()],
['developer', 'player']
[perm for perm in self.char1.account.permissions.all()], ["developer", "player"]
)
self.assertTrue(self.char1.permissions.check("Builder"))
self.assertTrue(self.char1.permissions.check("Builder", "Player"))
@ -234,12 +233,11 @@ class TestPermissionCheck(BaseEvenniaTest):
self.assertFalse(self.char1.permissions.check("Builder", "dummy", require_all=True))
self.assertFalse(self.char1.permissions.check("Developer", "foobar", require_all=True))
self.char1.account.permissions.remove('developer')
self.char1.account.permissions.remove("developer")
self.char1.account.permissions.add("Builder")
self.assertEqual(
[perm for perm in self.char1.account.permissions.all()],
['builder', 'player']
[perm for perm in self.char1.account.permissions.all()], ["builder", "player"]
)
self.assertFalse(self.char1.permissions.check("Developer"))
self.assertFalse(self.char1.permissions.check("Developer", "Player", require_all=True))

View file

@ -237,8 +237,9 @@ class ObjectDBManager(TypedObjectManager):
)
type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()
try:
return self.filter(
cand_restriction & type_restriction & Q(**querykwargs)).order_by("id")
return self.filter(cand_restriction & type_restriction & Q(**querykwargs)).order_by(
"id"
)
except exceptions.FieldError:
return self.none()
except ValueError:
@ -335,8 +336,9 @@ class ObjectDBManager(TypedObjectManager):
index_matches = string_partial_matching(key_strings, ostring, ret_index=True)
if index_matches:
# a match by key
match_ids = [obj.id for ind, obj in enumerate(search_candidates)
if ind in index_matches]
match_ids = [
obj.id for ind, obj in enumerate(search_candidates) if ind in index_matches
]
else:
# match by alias rather than by key
search_candidates = search_candidates.filter(

View file

@ -22,9 +22,15 @@ from evennia.scripts.scripthandler import ScriptHandler
from evennia.typeclasses.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase
from evennia.utils import ansi, create, funcparser, logger, search
from evennia.utils.utils import (class_from_module, is_iter, lazy_property,
list_to_string, make_iter, to_str,
variable_from_module)
from evennia.utils.utils import (
class_from_module,
is_iter,
lazy_property,
list_to_string,
make_iter,
to_str,
variable_from_module,
)
_INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE
@ -212,13 +218,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
objects = ObjectManager()
# populated by `return_apperance`
appearance_template = '''
appearance_template = """
{header}
|c{name}|n
{desc}
{exits}{characters}{things}
{footer}
'''
"""
# on-object properties
@ -532,12 +538,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# we re-run exact match agains one of the matches to
# make sure we were not catching partial matches not belonging
# to the stack
nstack = len(ObjectDB.objects.get_objs_with_key_or_alias(
results[0].key,
exact=True,
candidates=list(results),
typeclasses=[typeclass] if typeclass else None
))
nstack = len(
ObjectDB.objects.get_objs_with_key_or_alias(
results[0].key,
exact=True,
candidates=list(results),
typeclasses=[typeclass] if typeclass else None,
)
)
if nstack == nresults:
# a valid stack, return multiple results
return list(results)[:stacked]
@ -630,9 +638,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
raw_string = self.nicks.nickreplace(
raw_string, categories=("inputline", "channel"), include_account=True
)
return _CMDHANDLER(
self, raw_string, callertype="object", session=session, **kwargs
)
return _CMDHANDLER(self, raw_string, callertype="object", session=session, **kwargs)
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs):
"""
@ -790,7 +796,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
mapping = mapping or {}
you = from_obj or self
if 'you' not in mapping:
if "you" not in mapping:
mapping[you] = you
contents = self.contents
@ -802,14 +808,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# actor-stance replacements
inmessage = _MSG_CONTENTS_PARSER.parse(
inmessage, raise_errors=True, return_string=True,
caller=you, receiver=receiver, mapping=mapping)
inmessage,
raise_errors=True,
return_string=True,
caller=you,
receiver=receiver,
mapping=mapping,
)
# director-stance replacements
outmessage = inmessage.format(
**{key: obj.get_display_name(looker=receiver)
if hasattr(obj, "get_display_name") else str(obj)
for key, obj in mapping.items()})
**{
key: obj.get_display_name(looker=receiver)
if hasattr(obj, "get_display_name")
else str(obj)
for key, obj in mapping.items()
}
)
receiver.msg(text=(outmessage, outkwargs), from_obj=from_obj, **kwargs)
@ -866,6 +881,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
7. `self.at_post_move(source_location)`
"""
def logerr(string="", err=None):
"""Simple log helper method"""
logger.log_trace()
@ -989,8 +1005,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
if not home:
obj.location = None
obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
logger.log_err("Missing default home - '{name}(#{dbid})' now "
"has a null location.".format(name=obj.name, dbid=obj.dbid))
logger.log_err(
"Missing default home - '{name}(#{dbid})' now "
"has a null location.".format(name=obj.name, dbid=obj.dbid)
)
return
if obj.has_account:
@ -1550,7 +1568,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# This was created from nowhere and added to an account's
# inventory; it's probably the result of a create command.
string = _("You now have {name} in your possession.").format(
name=self.get_display_name(self.location))
name=self.get_display_name(self.location)
)
self.location.msg(string)
return
@ -1754,13 +1773,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
lists are the actual objects.
"""
def filter_visible(obj_list):
return [obj for obj in obj_list if obj != looker and obj.access(looker, "view")]
return {
"exits": filter_visible(self.contents_get(content_type="exit")),
"characters": filter_visible(self.contents_get(content_type="character")),
"things": filter_visible(self.contents_get(content_type="object"))
"things": filter_visible(self.contents_get(content_type="object")),
}
def get_content_names(self, looker, **kwargs):
@ -1789,13 +1809,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# a mapping {'exits': [...], 'characters': [...], 'things': [...]}
contents_map = self.get_visible_contents(looker, **kwargs)
character_names = [char.get_display_name(looker, **kwargs)
for char in contents_map['characters']]
exit_names = [exi.get_display_name(looker, **kwargs) for exi in contents_map['exits']]
character_names = [
char.get_display_name(looker, **kwargs) for char in contents_map["characters"]
]
exit_names = [exi.get_display_name(looker, **kwargs) for exi in contents_map["exits"]]
# group all same-named things under one name
things = defaultdict(list)
for thing in contents_map['things']:
for thing in contents_map["things"]:
things[thing.get_display_name(looker, **kwargs)].append(thing)
# pluralize same-named things
@ -1806,11 +1827,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
thing_names.append(singular if nthings == 1 else plural)
return {
"exits": exit_names,
"characters": character_names,
"things": thing_names
}
return {"exits": exit_names, "characters": character_names, "things": thing_names}
def return_appearance(self, looker, **kwargs):
"""
@ -1840,7 +1857,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"""
if not looker:
return ''
return ""
# ourselves
name = self.get_display_name(looker, **kwargs)
@ -1848,20 +1865,20 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# contents
content_names_map = self.get_content_names(looker, **kwargs)
exits = list_to_string(content_names_map['exits'])
characters = list_to_string(content_names_map['characters'])
things = list_to_string(content_names_map['things'])
exits = list_to_string(content_names_map["exits"])
characters = list_to_string(content_names_map["characters"])
things = list_to_string(content_names_map["things"])
# populate the appearance_template string. It's a good idea to strip it and
# let the client add any extra spaces instead.
return self.appearance_template.format(
header='',
header="",
name=name,
desc=desc,
exits=f"|wExits:|n {exits}" if exits else '',
characters=f"\n|wCharacters:|n {characters}" if characters else '',
things=f"\n|wYou see:|n {things}" if things else '',
footer=''
exits=f"|wExits:|n {exits}" if exits else "",
characters=f"\n|wCharacters:|n {characters}" if characters else "",
things=f"\n|wYou see:|n {things}" if things else "",
footer="",
).strip()
def at_look(self, target, **kwargs):
@ -2124,7 +2141,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
msg_type = "whisper"
msg_self = (
'{self} whisper to {all_receivers}, "|n{speech}|n"'
if msg_self is True else msg_self
if msg_self is True
else msg_self
)
msg_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
msg_location = None
@ -2332,7 +2350,7 @@ class DefaultCharacter(DefaultObject):
@classmethod
def validate_name(cls, name):
""" Validate the character name prior to creating. Overload this function to add custom validators
"""Validate the character name prior to creating. Overload this function to add custom validators
Args:
name (str) : The name of the character
@ -2391,8 +2409,7 @@ class DefaultCharacter(DefaultObject):
self.db.prelogout_location = self.location # save location again to be sure.
else:
account.msg(
_("|r{obj} has no location and no home is set.|n").format(obj=self),
session=session
_("|r{obj} has no location and no home is set.|n").format(obj=self), session=session
) # Note to set home.
def at_post_puppet(self, **kwargs):
@ -2414,8 +2431,10 @@ class DefaultCharacter(DefaultObject):
self.msg((self.at_look(self.location), {"type": "look"}), options=None)
def message(obj, from_obj):
obj.msg(_("{name} has entered the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj)
obj.msg(
_("{name} has entered the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj,
)
self.location.for_contents(message, exclude=[self], from_obj=self)
@ -2438,8 +2457,10 @@ class DefaultCharacter(DefaultObject):
if self.location:
def message(obj, from_obj):
obj.msg(_("{name} has left the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj)
obj.msg(
_("{name} has left the game.").format(name=self.get_display_name(obj)),
from_obj=from_obj,
)
self.location.for_contents(message, exclude=[self], from_obj=self)
self.db.prelogout_location = self.location
@ -2582,6 +2603,7 @@ class DefaultRoom(DefaultObject):
# Default Exit command, used by the base exit object
#
class ExitCommand(_COMMAND_DEFAULT_CLASS):
"""
This is a command that simply cause the caller to traverse

View file

@ -202,18 +202,19 @@ class TestContentHandler(BaseEvenniaTest):
def test_contents_order(self):
"""Move object from room to room in various ways"""
self.assertEqual(self.room1.contents, [
self.exit, self.obj1, self.obj2, self.char1, self.char2])
self.assertEqual(
self.room1.contents, [self.exit, self.obj1, self.obj2, self.char1, self.char2]
)
self.assertEqual(self.room2.contents, [])
# use move_to hook to move obj1
self.obj1.move_to(self.room2)
self.assertEqual(self.room1.contents, [self.exit, self.obj2, self.char1, self.char2 ])
self.assertEqual(self.room1.contents, [self.exit, self.obj2, self.char1, self.char2])
self.assertEqual(self.room2.contents, [self.obj1])
# move obj2
self.obj2.move_to(self.room2)
self.assertEqual(self.room1.contents, [self.exit, self.char1, self.char2 ])
self.assertEqual(self.room1.contents, [self.exit, self.char1, self.char2])
self.assertEqual(self.room2.contents, [self.obj1, self.obj2])
# move back and forth - it should

View file

@ -262,7 +262,8 @@ def _validate_prototype(prototype):
def _format_protfuncs():
out = []
sorted_funcs = [
(key, func) for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0])
(key, func)
for key, func in sorted(protlib.FUNC_PARSER.callables.items(), key=lambda tup: tup[0])
]
for protfunc_name, protfunc in sorted_funcs:
out.append(
@ -2113,8 +2114,9 @@ def _apply_diff(caller, **kwargs):
objects = kwargs["objects"]
back_node = kwargs["back_node"]
diff = kwargs.get("diff", None)
num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects,
caller=caller)
num_changed = spawner.batch_update_objects_with_prototype(
prototype, diff=diff, objects=objects, caller=caller
)
caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
return back_node
@ -2354,7 +2356,7 @@ def node_apply_diff(caller, **kwargs):
def node_prototype_save(caller, **kwargs):
"""Save prototype to disk """
"""Save prototype to disk"""
# these are only set if we selected 'yes' to save on a previous pass
prototype = kwargs.get("prototype", None)
# set to True/False if answered, None if first pass

View file

@ -50,16 +50,20 @@ def protfunc_callable_protkey(*args, **kwargs):
prot_value = prototype[fieldname]
else:
# check if it's an attribute
for attrtuple in prototype.get('attrs', []):
for attrtuple in prototype.get("attrs", []):
if attrtuple[0] == fieldname:
prot_value = attrtuple[1]
break
else:
raise AttributeError(f"{fieldname} not found in prototype\n{prototype}\n"
"(neither as prototype-field or as an Attribute")
raise AttributeError(
f"{fieldname} not found in prototype\n{prototype}\n"
"(neither as prototype-field or as an Attribute"
)
if callable(prot_value):
raise RuntimeError(f"Error in prototype\n{prototype}\n$protkey can only reference static "
f"values/attributes (found {prot_value})")
raise RuntimeError(
f"Error in prototype\n{prototype}\n$protkey can only reference static "
f"values/attributes (found {prot_value})"
)
try:
return funcparser.funcparser_callable_eval(prot_value, **kwargs)
except funcparser.ParsingError:

View file

@ -131,8 +131,9 @@ def homogenize_prototype(prototype, custom_keys=None):
for attr in attrs:
# attrs must be on form [(key, value, category, lockstr)]
if not is_iter(attr):
logger.log_error("Prototype's 'attr' field must "
f"be a list of tuples: {prototype}")
logger.log_error(
"Prototype's 'attr' field must " f"be a list of tuples: {prototype}"
)
elif attr:
nattr = len(attr)
if nattr == 1:
@ -147,14 +148,15 @@ def homogenize_prototype(prototype, custom_keys=None):
elif key == "prototype_parent":
# homogenize any prototype-parents embedded directly as dicts
protparents = prototype.get('prototype_parent', [])
protparents = prototype.get("prototype_parent", [])
if isinstance(protparents, dict):
protparents = [protparents]
for parent in make_iter(protparents):
if isinstance(parent, dict):
# recursively homogenize directly embedded prototype parents
homogenized_parents.append(
homogenize_prototype(parent, custom_keys=custom_keys))
homogenize_prototype(parent, custom_keys=custom_keys)
)
else:
# normal prototype-parent names are added as-is
homogenized_parents.append(parent)
@ -170,7 +172,7 @@ def homogenize_prototype(prototype, custom_keys=None):
if homogenized_tags:
homogenized["tags"] = homogenized_tags
if homogenized_parents:
homogenized['prototype_parent'] = homogenized_parents
homogenized["prototype_parent"] = homogenized_parents
# add required missing parts that had defaults before
@ -190,6 +192,7 @@ def homogenize_prototype(prototype, custom_keys=None):
# module/dict-based prototypes
def load_module_prototypes(*mod_or_prototypes, override=True):
"""
Load module prototypes. Also prototype-dicts passed directly to this function are considered
@ -233,12 +236,16 @@ def load_module_prototypes(*mod_or_prototypes, override=True):
# prototype dicts that must have 'prototype_key' set.
for prot in prototype_list:
if not isinstance(prot, dict):
logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
f"is not a dict (skipping): {prot}")
logger.log_err(
f"Prototype read from {mod}.PROTOTYPE_LIST "
f"is not a dict (skipping): {prot}"
)
continue
elif "prototype_key" not in prot:
logger.log_err(f"Prototype read from {mod}.PROTOTYPE_LIST "
f"is missing the 'prototype_key' (skipping): {prot}")
logger.log_err(
f"Prototype read from {mod}.PROTOTYPE_LIST "
f"is missing the 'prototype_key' (skipping): {prot}"
)
continue
prots.append((prot["prototype_key"], homogenize_prototype(prot)))
else:
@ -270,7 +277,8 @@ def load_module_prototypes(*mod_or_prototypes, override=True):
{
"prototype_key": actual_prot_key,
"prototype_desc": (
prototype["prototype_desc"] if "prototype_desc" in prototype else (mod or "N/A")),
prototype["prototype_desc"] if "prototype_desc" in prototype else (mod or "N/A")
),
"prototype_locks": (
prototype["prototype_locks"]
if "prototype_locks" in prototype
@ -292,9 +300,11 @@ def load_module_prototypes(*mod_or_prototypes, override=True):
if isinstance(mod_or_dict, dict):
# a single prototype; we must make sure it has its key
prototype_key = mod_or_dict.get('prototype_key')
prototype_key = mod_or_dict.get("prototype_key")
if not prototype_key:
raise ValidationError(f"The prototype {mod_or_prototype} does not contain a 'prototype_key'")
raise ValidationError(
f"The prototype {mod_or_prototype} does not contain a 'prototype_key'"
)
prots = [(prototype_key, mod_or_dict)]
mod = None
else:
@ -307,7 +317,7 @@ def load_module_prototypes(*mod_or_prototypes, override=True):
for prototype_key, prot in prots:
prototype = _cleanup_prototype(prototype_key, prot, mod=mod)
# the key can change since in-proto key is given prio over variable-name-based keys
actual_prototype_key = prototype['prototype_key']
actual_prototype_key = prototype["prototype_key"]
if actual_prototype_key in _MODULE_PROTOTYPES and not override:
# don't override - useful to still let settings replace dynamic inserts
@ -462,23 +472,26 @@ def delete_prototype(prototype_key, caller=None):
stored_prototype = DbPrototype.objects.filter(db_key__iexact=prototype_key)
if not stored_prototype:
raise PermissionError(_("Prototype {prototype_key} was not found.").format(
prototype_key=prototype_key))
raise PermissionError(
_("Prototype {prototype_key} was not found.").format(prototype_key=prototype_key)
)
stored_prototype = stored_prototype[0]
if caller:
if not stored_prototype.access(caller, "edit"):
raise PermissionError(
_("{caller} needs explicit 'edit' permissions to "
"delete prototype {prototype_key}.").format(
caller=caller, prototype_key=prototype_key)
_(
"{caller} needs explicit 'edit' permissions to "
"delete prototype {prototype_key}."
).format(caller=caller, prototype_key=prototype_key)
)
stored_prototype.delete()
return True
def search_prototype(key=None, tags=None, require_single=False, return_iterators=False,
no_db=False):
def search_prototype(
key=None, tags=None, require_single=False, return_iterators=False, no_db=False
):
"""
Find prototypes based on key and/or tags, or all prototypes.
@ -520,7 +533,7 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
# prototype keys are always in lowecase
if key:
key = key.lower()
key = key.lower()
# search module prototypes
@ -587,10 +600,10 @@ def search_prototype(key=None, tags=None, require_single=False, return_iterators
nmodules = len(module_prototypes)
ndbprots = db_matches.count() if db_matches else 0
if nmodules + ndbprots != 1:
raise KeyError(_(
"Found {num} matching prototypes among {module_prototypes}.").format(
num=nmodules + ndbprots,
module_prototypes=module_prototypes)
raise KeyError(
_("Found {num} matching prototypes among {module_prototypes}.").format(
num=nmodules + ndbprots, module_prototypes=module_prototypes
)
)
if return_iterators:
@ -670,7 +683,7 @@ class PrototypeEvMore(EvMore):
else:
# get the correct slice, adjusted for the db-prototypes
pageno = max(0, pageno - self._npages_db)
return modprot_list[pageno * self.height: pageno * self.height + self.height]
return modprot_list[pageno * self.height : pageno * self.height + self.height]
def page_formatter(self, page):
"""
@ -809,12 +822,15 @@ def validate_prototype(
if is_prototype_base:
_flags["errors"].append(
_("Prototype {protkey} requires `typeclass` " "or 'prototype_parent'.").format(
protkey=protkey)
protkey=protkey
)
)
else:
_flags["warnings"].append(
_("Prototype {protkey} can only be used as a mixin since it lacks "
"'typeclass' or 'prototype_parent' keys.").format(protkey=protkey)
_(
"Prototype {protkey} can only be used as a mixin since it lacks "
"'typeclass' or 'prototype_parent' keys."
).format(protkey=protkey)
)
if strict and typeclass:
@ -822,9 +838,10 @@ def validate_prototype(
class_from_module(typeclass)
except ImportError as err:
_flags["errors"].append(
_("{err}: Prototype {protkey} is based on typeclass {typeclass}, "
"which could not be imported!").format(
err=err, protkey=protkey, typeclass=typeclass)
_(
"{err}: Prototype {protkey} is based on typeclass {typeclass}, "
"which could not be imported!"
).format(err=err, protkey=protkey, typeclass=typeclass)
)
if prototype_parent and isinstance(prototype_parent, dict):
@ -840,20 +857,24 @@ def validate_prototype(
else:
protstring = protstring.lower()
if protkey is not None and protstring == protkey:
_flags["errors"].append(_("Prototype {protkey} tries to parent itself.").format(
protkey=protkey))
_flags["errors"].append(
_("Prototype {protkey} tries to parent itself.").format(protkey=protkey)
)
protparent = protparents.get(protstring)
if not protparent:
_flags["errors"].append(
_("Prototype {protkey}'s `prototype_parent` (named '{parent}') "
"was not found.").format(protkey=protkey, parent=protstring)
_(
"Prototype {protkey}'s `prototype_parent` (named '{parent}') "
"was not found."
).format(protkey=protkey, parent=protstring)
)
# check for infinite recursion
if id(prototype) in _flags["visited"]:
_flags["errors"].append(
_("{protkey} has infinite nesting of prototypes.").format(
protkey=protkey or prototype)
protkey=protkey or prototype
)
)
if _flags["errors"]:
@ -875,9 +896,11 @@ def validate_prototype(
# if we get back to the current level without a typeclass it's an error.
if strict and is_prototype_base and _flags["depth"] <= 0 and not _flags["typeclass"]:
_flags["errors"].append(
_("Prototype {protkey} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass.").format(protkey=protkey)
_(
"Prototype {protkey} has no `typeclass` defined anywhere in its parent\n "
"chain. Add `typeclass`, or a `prototype_parent` pointing to a "
"prototype with a typeclass."
).format(protkey=protkey)
)
if _flags["depth"] <= 0:
@ -901,8 +924,9 @@ def validate_prototype(
prototype["prototype_locks"] = prototype_locks
def protfunc_parser(value, available_functions=None, testing=False, stacktrace=False,
caller=None, **kwargs):
def protfunc_parser(
value, available_functions=None, testing=False, stacktrace=False, caller=None, **kwargs
):
"""
Parse a prototype value string for a protfunc and process it.
@ -1129,8 +1153,9 @@ def value_to_obj(value, force=True):
stype = type(value)
if is_iter(value):
if stype == dict:
return {value_to_obj_or_any(key): value_to_obj_or_any(val)
for key, val in value.items()}
return {
value_to_obj_or_any(key): value_to_obj_or_any(val) for key, val in value.items()
}
else:
return stype([value_to_obj_or_any(val) for val in value])
return dbid_to_obj(value, ObjectDB)

View file

@ -154,8 +154,13 @@ from evennia.prototypes.prototypes import (
_CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination")
_PROTOTYPE_META_NAMES = ("prototype_key", "prototype_desc", "prototype_tags",
"prototype_locks", "prototype_parent")
_PROTOTYPE_META_NAMES = (
"prototype_key",
"prototype_desc",
"prototype_tags",
"prototype_locks",
"prototype_parent",
)
_PROTOTYPE_ROOT_NAMES = (
"typeclass",
"key",
@ -235,9 +240,7 @@ def _get_prototype(inprot, protparents, uninherited=None, _workprot=None):
parent_prototype = protparents.get(prototype.lower(), {})
# Build the prot dictionary in reverse order, overloading
new_prot = _get_prototype(
parent_prototype, protparents, _workprot=_workprot
)
new_prot = _get_prototype(parent_prototype, protparents, _workprot=_workprot)
# attrs, tags have internal structure that should be inherited separately
new_prot["attrs"] = _inherit_attrs(
@ -276,8 +279,9 @@ def flatten_prototype(prototype, validate=False, no_db=False):
if prototype:
prototype = protlib.homogenize_prototype(prototype)
protparents = {prot["prototype_key"].lower(): prot
for prot in protlib.search_prototype(no_db=no_db)}
protparents = {
prot["prototype_key"].lower(): prot for prot in protlib.search_prototype(no_db=no_db)
}
protlib.validate_prototype(
prototype, None, protparents, is_prototype_base=validate, strict=validate
)
@ -342,7 +346,7 @@ def prototype_from_object(obj):
prot["aliases"] = aliases
tags = sorted(
[(tag.db_key, tag.db_category, tag.db_data) for tag in obj.tags.all(return_objs=True)],
key=lambda tup: (str(tup[0]), tup[1] or '', tup[2] or '')
key=lambda tup: (str(tup[0]), tup[1] or "", tup[2] or ""),
)
if tags:
prot["tags"] = tags
@ -351,7 +355,7 @@ def prototype_from_object(obj):
(attr.key, attr.value, attr.category, ";".join(attr.locks.all()))
for attr in obj.attributes.all()
],
key=lambda tup: (str(tup[0]), tup[1] or '', tup[2] or '', tup[3])
key=lambda tup: (str(tup[0]), tup[1] or "", tup[2] or "", tup[3]),
)
if attrs:
prot["attrs"] = attrs
@ -489,8 +493,10 @@ def flatten_diff(diff):
out.extend(_get_all_nested_diff_instructions(val))
else:
raise RuntimeError(
_("Diff contains non-dicts that are not on the "
"form (old, new, action_to_take): {diffpart}").format(diffpart)
_(
"Diff contains non-dicts that are not on the "
"form (old, new, action_to_take): {diffpart}"
).format(diffpart)
)
return out
@ -627,8 +633,9 @@ def format_diff(diff, minimal=True):
return "\n ".join(line for line in texts if line)
def batch_update_objects_with_prototype(prototype, diff=None, objects=None,
exact=False, caller=None):
def batch_update_objects_with_prototype(
prototype, diff=None, objects=None, exact=False, caller=None
):
"""
Update existing objects with the latest version of the prototype.
@ -941,27 +948,32 @@ def spawn(*prototypes, caller=None, **kwargs):
val = prot.pop("location", None)
create_kwargs["db_location"] = init_spawn_value(
val, value_to_obj, caller=caller, prototype=prototype)
val, value_to_obj, caller=caller, prototype=prototype
)
val = prot.pop("home", None)
if val:
create_kwargs["db_home"] = init_spawn_value(val, value_to_obj, caller=caller,
prototype=prototype)
create_kwargs["db_home"] = init_spawn_value(
val, value_to_obj, caller=caller, prototype=prototype
)
else:
try:
create_kwargs["db_home"] = init_spawn_value(
settings.DEFAULT_HOME, value_to_obj, caller=caller, prototype=prototype)
settings.DEFAULT_HOME, value_to_obj, caller=caller, prototype=prototype
)
except ObjectDB.DoesNotExist:
# settings.DEFAULT_HOME not existing is common for unittests
pass
val = prot.pop("destination", None)
create_kwargs["db_destination"] = init_spawn_value(val, value_to_obj, caller=caller,
prototype=prototype)
create_kwargs["db_destination"] = init_spawn_value(
val, value_to_obj, caller=caller, prototype=prototype
)
val = prot.pop("typeclass", settings.BASE_OBJECT_TYPECLASS)
create_kwargs["db_typeclass_path"] = init_spawn_value(val, str, caller=caller,
prototype=prototype)
create_kwargs["db_typeclass_path"] = init_spawn_value(
val, str, caller=caller, prototype=prototype
)
# extract calls to handlers
val = prot.pop("permissions", [])
@ -974,8 +986,13 @@ def spawn(*prototypes, caller=None, **kwargs):
val = prot.pop("tags", [])
tags = []
for (tag, category, *data) in val:
tags.append((init_spawn_value(tag, str, caller=caller, prototype=prototype),
category, data[0] if data else None))
tags.append(
(
init_spawn_value(tag, str, caller=caller, prototype=prototype),
category,
data[0] if data else None,
)
)
prototype_key = prototype.get("prototype_key", None)
if prototype_key:
@ -987,8 +1004,10 @@ def spawn(*prototypes, caller=None, **kwargs):
# extract ndb assignments
nattributes = dict(
(key.split("_", 1)[1], init_spawn_value(val, value_to_obj, caller=caller,
prototype=prototype))
(
key.split("_", 1)[1],
init_spawn_value(val, value_to_obj, caller=caller, prototype=prototype),
)
for key, val in prot.items()
if key.startswith("ndb_")
)
@ -998,8 +1017,13 @@ def spawn(*prototypes, caller=None, **kwargs):
attributes = []
for (attrname, value, *rest) in val:
attributes.append(
(attrname, init_spawn_value(value, caller=caller, prototype=prototype),
rest[0] if rest else None, rest[1] if len(rest) > 1 else None))
(
attrname,
init_spawn_value(value, caller=caller, prototype=prototype),
rest[0] if rest else None,
rest[1] if len(rest) > 1 else None,
)
)
simple_attributes = []
for key, value in (
@ -1010,8 +1034,14 @@ def spawn(*prototypes, caller=None, **kwargs):
continue
else:
simple_attributes.append(
(key, init_spawn_value(value, value_to_obj_or_any, caller=caller,
prototype=prototype), None, None)
(
key,
init_spawn_value(
value, value_to_obj_or_any, caller=caller, prototype=prototype
),
None,
None,
)
)
attributes = attributes + simple_attributes

View file

@ -45,6 +45,7 @@ _PROTPARENTS = {
},
}
class TestSpawner(BaseEvenniaTest):
def setUp(self):
super().setUp()
@ -340,7 +341,6 @@ class TestProtLib(BaseEvenniaTest):
class TestProtFuncs(BaseEvenniaTest):
@override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"])
def test_protkey_protfunc(self):
test_prot = {"key1": "value1", "key2": 2}
@ -350,8 +350,7 @@ class TestProtFuncs(BaseEvenniaTest):
"value1",
)
self.assertEqual(
protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot),
2
protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), 2
)
@ -908,6 +907,7 @@ class Test2474(BaseEvenniaTest):
that of its prototype_parent.
"""
prototypes = {
"WEAPON": {
"typeclass": "evennia.objects.objects.DefaultObject",
@ -951,14 +951,14 @@ class TestPartialTagAttributes(BaseEvenniaTest):
def setUp(self):
super().setUp()
self.prot = {
'prototype_key': 'rock',
'typeclass': 'evennia.objects.objects.DefaultObject',
'key': 'a rock',
'tags': [('quantity', 'groupable')], # missing data field
'attrs': [('quantity', 1)], # missing category and lock fields
'desc': 'A good way to get stoned.'
"prototype_key": "rock",
"typeclass": "evennia.objects.objects.DefaultObject",
"key": "a rock",
"tags": [("quantity", "groupable")], # missing data field
"attrs": [("quantity", 1)], # missing category and lock fields
"desc": "A good way to get stoned.",
}
def test_partial_spawn(self):
obj = spawner.spawn(self.prot)
self.assertEqual(obj[0].key, self.prot['key'])
self.assertEqual(obj[0].key, self.prot["key"])

View file

@ -108,8 +108,8 @@ class MonitorHandler(object):
"""
# if this an Attribute with a category we should differentiate
fieldname = self._attr_category_fieldname(
fieldname, obj.db_category
if fieldname == "db_value" and hasattr(obj, "db_category") else None
fieldname,
obj.db_category if fieldname == "db_value" and hasattr(obj, "db_category") else None,
)
to_delete = []
@ -124,8 +124,7 @@ class MonitorHandler(object):
for (obj, fieldname, idstring) in to_delete:
del self.monitors[obj][fieldname][idstring]
def add(self, obj, fieldname, callback, idstring="", persistent=False,
category=None, **kwargs):
def add(self, obj, fieldname, callback, idstring="", persistent=False, category=None, **kwargs):
"""
Add monitoring to a given field or Attribute. A field must
be specified with the full db_* name or it will be assumed

View file

@ -49,10 +49,12 @@ class ScriptHandler(object):
except Exception:
next_repeat = "?"
string += _("\n '{key}' ({next_repeat}/{interval}, {repeats} repeats): {desc}").format(
key=script.key, next_repeat=next_repeat,
key=script.key,
next_repeat=next_repeat,
interval=interval,
repeats=repeats,
desc=script.desc)
desc=script.desc,
)
return string.strip()
def add(self, scriptclass, key=None, autostart=True):

View file

@ -85,5 +85,5 @@ class TestExtendedLoopingCall(TestCase):
loopcall.start(20, now=False, start_delay=10, count_start=1)
loopcall.__call__.assert_not_called()
self.assertEqual(loopcall.interval , 20)
self.assertEqual(loopcall.interval, 20)
loopcall._scheduleFrom.assert_called_with(121)

View file

@ -90,6 +90,7 @@ a text-game, and if you want to update some property, consider doing so
on-demand rather than using a ticker.
"""
class Ticker(object):
"""
Represents a repeatedly running task that calls

Some files were not shown because too many files have changed in this diff Show more