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): def _rst2md(filename_rst):
with open(filename_rst, 'r') as fil: with open(filename_rst, "r") as fil:
# read rst file, reformat and save # read rst file, reformat and save
txt = fil.read() txt = fil.read()
with open(filename_rst, 'w') as fil: with open(filename_rst, "w") as fil:
txt = "```{eval-rst}\n" + txt + "\n```" txt = "```{eval-rst}\n" + txt + "\n```"
fil.write(txt) fil.write(txt)
# rename .rst file to .md file # rename .rst file to .md file
filename, _ = filename_rst.rsplit('.', 1) filename, _ = filename_rst.rsplit(".", 1)
filename_md = filename + ".md" filename_md = filename + ".md"
rename(filename_rst, 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: for strip_prefix in _STRIP_PREFIX:
if url.startswith(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): if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
# skip regular http/s urls etc # skip regular http/s urls etc
@ -157,10 +157,10 @@ def auto_link_remapper(no_autodoc=False):
if url.startswith("evennia."): if url.startswith("evennia."):
# api link - we want to remove legacy #reference and remove .md # api link - we want to remove legacy #reference and remove .md
if '#' in url: if "#" in url:
_, url = url.rsplit('#', 1) _, url = url.rsplit("#", 1)
if url.endswith(".md"): if url.endswith(".md"):
url, _ = url.rsplit('.', 1) url, _ = url.rsplit(".", 1)
return f"[{txt}]({url})" return f"[{txt}]({url})"
fname, *part = url.rsplit("/", 1) 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]: if _CURRFILE in docref_map and fname in docref_map[_CURRFILE]:
cfilename = _CURRFILE.rsplit("/", 1)[-1] 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: if urlout != url:
print(f" {cfilename}: [{txt}]({url}) -> [{txt}]({urlout})") print(f" {cfilename}: [{txt}]({url}) -> [{txt}]({urlout})")
else: else:
@ -193,7 +195,7 @@ def auto_link_remapper(no_autodoc=False):
for strip_prefix in _STRIP_PREFIX: for strip_prefix in _STRIP_PREFIX:
if url.startswith(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): if any(url.startswith(noremap) for noremap in _NO_REMAP_STARTSWITH):
return f"[{txt}]: {url}" return f"[{txt}]: {url}"
@ -202,8 +204,8 @@ def auto_link_remapper(no_autodoc=False):
urlout = url urlout = url
elif url.startswith("evennia."): elif url.startswith("evennia."):
# api link - we want to remove legacy #reference # api link - we want to remove legacy #reference
if '#' in url: if "#" in url:
_, urlout = url.rsplit('#', 1) _, urlout = url.rsplit("#", 1)
else: else:
fname, *part = url.rsplit("/", 1) fname, *part = url.rsplit("/", 1)
fname = part[0] if part else fname 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": """ "utils": """
Miscellaneous, optional tools for manipulating text, auditing connections Miscellaneous, optional tools for manipulating text, auditing connections
and more. and more.
""" """,
} }
_FILENAME_MAP = { _FILENAME_MAP = {"rpsystem": "RPSystem", "xyzgrid": "XYZGrid", "awsstorage": "AWSStorage"}
"rpsystem": "RPSystem",
"xyzgrid": "XYZGrid",
"awsstorage": "AWSStorage"
}
HEADER = """# Contribs HEADER = """# Contribs
@ -145,10 +141,14 @@ def readmes2docs(directory=_SOURCE_DIR):
pypath = f"evennia.contrib.{category}.{name}" pypath = f"evennia.contrib.{category}.{name}"
filename = "Contrib-" + "-".join( filename = (
_FILENAME_MAP.get( "Contrib-"
part, part.capitalize() if part[0].islower() else part) + "-".join(
for part in name.split("_")) + ".md" _FILENAME_MAP.get(part, part.capitalize() if part[0].islower() else part)
for part in name.split("_")
)
+ ".md"
)
outfile = pathjoin(_OUT_DIR, filename) outfile = pathjoin(_OUT_DIR, filename)
with open(file_path) as fil: with open(file_path) as fil:
@ -163,7 +163,7 @@ def readmes2docs(directory=_SOURCE_DIR):
except IndexError: except IndexError:
blurb = name blurb = name
with open(outfile, 'w') as fil: with open(outfile, "w") as fil:
fil.write(data) fil.write(data)
categories[category].append((name, credits, blurb, filename, pypath)) 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()): for tup in sorted(contrib_tups, key=lambda tup: tup[0].lower()):
catlines.append( catlines.append(
BLURB.format( BLURB.format(
name=tup[0], name=tup[0], credits=tup[1], blurb=tup[2], filename=tup[3], code_location=tup[4]
credits=tup[1],
blurb=tup[2],
filename=tup[3],
code_location=tup[4]
) )
) )
filenames.append(f"{tup[3]}") filenames.append(f"{tup[3]}")
@ -193,17 +189,15 @@ def readmes2docs(directory=_SOURCE_DIR):
category=category, category=category,
category_desc=_CATEGORY_DESCS[category].strip(), category_desc=_CATEGORY_DESCS[category].strip(),
blurbs="\n".join(catlines), blurbs="\n".join(catlines),
toctree=toctree toctree=toctree,
) )
) )
text = _FILE_STRUCTURE.format( text = _FILE_STRUCTURE.format(
header=HEADER, header=HEADER, categories="\n".join(category_sections), footer=INDEX_FOOTER
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) fil.write(text)
print(f" -- Converted Contrib READMEs to {ncount} doc pages + index.") 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) filepaths = glob.glob(args.files, recursive=True)
width = args.width 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 count = 0
for filepath in filepaths: for filepath in filepaths:

View file

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

View file

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

View file

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

View file

@ -100,6 +100,7 @@ MONITOR_HANDLER = None
GLOBAL_SCRIPTS = None GLOBAL_SCRIPTS = None
OPTION_CLASSES = None OPTION_CLASSES = None
def _create_version(): def _create_version():
""" """
Helper function for building the version string 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 # Create throttles for too many account-creations and login attempts
CREATION_THROTTLE = Throttle( CREATION_THROTTLE = Throttle(
name='creation', limit=settings.CREATION_THROTTLE_LIMIT, name="creation",
timeout=settings.CREATION_THROTTLE_TIMEOUT limit=settings.CREATION_THROTTLE_LIMIT,
timeout=settings.CREATION_THROTTLE_TIMEOUT,
) )
LOGIN_THROTTLE = Throttle( 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: except Exception:
errors.append( 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() logger.log_trace()
return None, errors return None, errors
@ -879,7 +883,6 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
return True return True
# methods inherited from database model # methods inherited from database model
def msg(self, text=None, from_obj=None, session=None, options=None, **kwargs): 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() sessions = self.sessions.get()
session = sessions[0] if sessions else None session = sessions[0] if sessions else None
return _CMDHANDLER( return _CMDHANDLER(self, raw_string, callertype="account", session=session, **kwargs)
self, raw_string, callertype="account", session=session, **kwargs
)
# channel receive hooks # channel receive hooks
@ -1000,11 +1001,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
""" """
if senders: 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() 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" # 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:]}" message = f"{sender_string}{spacing}{message_lstrip[1:]}"
else: else:
# normal message # normal message
@ -1035,8 +1036,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
to customize the message for the receiver on the channel-level. to customize the message for the receiver on the channel-level.
""" """
self.msg(text=(message, {"from_channel": channel.id}), self.msg(
from_obj=senders, options={"from_channel": channel.id}) 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): 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 _MUDINFO_CHANNEL is None:
if settings.CHANNEL_MUDINFO: if settings.CHANNEL_MUDINFO:
try: try:
_MUDINFO_CHANNEL = ChannelDB.objects.get( _MUDINFO_CHANNEL = ChannelDB.objects.get(db_key=settings.CHANNEL_MUDINFO["key"])
db_key=settings.CHANNEL_MUDINFO["key"])
except ChannelDB.DoesNotExist: except ChannelDB.DoesNotExist:
logger.log_trace() logger.log_trace()
else: else:
@ -1383,7 +1386,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if settings.CHANNEL_CONNECTINFO: if settings.CHANNEL_CONNECTINFO:
try: try:
_CONNECT_CHANNEL = ChannelDB.objects.get( _CONNECT_CHANNEL = ChannelDB.objects.get(
db_key=settings.CHANNEL_CONNECTINFO["key"]) db_key=settings.CHANNEL_CONNECTINFO["key"]
)
except ChannelDB.DoesNotExist: except ChannelDB.DoesNotExist:
logger.log_trace() logger.log_trace()
else: else:
@ -1661,7 +1665,8 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
if sess and sid: if sess and sid:
result.append( result.append(
f"\n - |G{char.key}|n [{', '.join(char.permissions.all())}] " 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: else:
result.append( result.append(
f"\n - |R{char.key}|n [{', '.join(char.permissions.all())}] " 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})" chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower())) nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
for obj in self._nicklist_callers: for obj in self._nicklist_callers:
obj.msg( obj.msg("Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist))
"Nicks at {chstr}:\n {nicklist}".format(chstr=chstr, nicklist=nicklist)
)
self._nicklist_callers = [] self._nicklist_callers = []
return return

View file

@ -276,8 +276,11 @@ class AccountDBManager(TypedObjectManager, UserManager):
new_account.set_password(password) new_account.set_password(password)
new_account._createdict = dict( new_account._createdict = dict(
locks=locks, permissions=permissions, locks=locks,
report_to=report_to, tags=tags, attributes=attributes permissions=permissions,
report_to=report_to,
tags=tags,
attributes=attributes,
) )
# saving will trigger the signal that calls the # saving will trigger the signal that calls the
# at_first_save hook on the typeclass, where the _createdict # 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. # is the normal "production message to echo to the account.
_ERROR_UNTRAPPED = ( _ERROR_UNTRAPPED = (
_(""" _(
"""
An untrapped error occurred. An untrapped error occurred.
"""), """
_(""" ),
_(
"""
An untrapped error occurred. Please file a bug report detailing the steps to reproduce. An untrapped error occurred. Please file a bug report detailing the steps to reproduce.
"""), """
),
) )
_ERROR_CMDSETS = ( _ERROR_CMDSETS = (
_(""" _(
"""
A cmdset merger-error occurred. This is often due to a syntax A cmdset merger-error occurred. This is often due to a syntax
error in one of the cmdsets to merge. error in one of the cmdsets to merge.
"""), """
_(""" ),
_(
"""
A cmdset merger-error occurred. Please file a bug report detailing the A cmdset merger-error occurred. Please file a bug report detailing the
steps to reproduce. steps to reproduce.
"""), """
),
) )
_ERROR_NOCMDSETS = ( _ERROR_NOCMDSETS = (
_(""" _(
"""
No command sets found! This is a critical bug that can have No command sets found! This is a critical bug that can have
multiple causes. multiple causes.
"""), """
_(""" ),
_(
"""
No command sets found! This is a sign of a critical bug. If No command sets found! This is a sign of a critical bug. If
disconnecting/reconnecting doesn't" solve the problem, try to contact disconnecting/reconnecting doesn't" solve the problem, try to contact
the server admin through" some other means for assistance. the server admin through" some other means for assistance.
"""), """
),
) )
_ERROR_CMDHANDLER = ( _ERROR_CMDHANDLER = (
_(""" _(
"""
A command handler bug occurred. If this is not due to a local change, 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 please file a bug report with the Evennia project, including the
traceback and steps to reproduce. traceback and steps to reproduce.
"""), """
_(""" ),
_(
"""
A command handler bug occurred. Please notify staff - they should A command handler bug occurred. Please notify staff - they should
likely file a bug report with the Evennia project. likely file a bug report with the Evennia project.
"""), """
),
) )
_ERROR_RECURSION_LIMIT = _( _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 for cmdname in [cmd.key] + cmd.aliases
if cmdname if cmdname
and l_raw_string.startswith(cmdname.lower()) 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: else:
@ -90,7 +90,7 @@ def build_matches(raw_string, cmdset, include_prefixes=False):
if ( if (
cmdname cmdname
and l_raw_string.startswith(cmdname.lower()) 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)) matches.append(create_match(cmdname, raw_string, cmd, raw_cmdname))
except Exception: except Exception:
@ -125,7 +125,10 @@ def try_num_differentiators(raw_string):
# the user might be trying to identify the command # the user might be trying to identify the command
# with a #num-command style syntax. We expect the regex to # with a #num-command style syntax. We expect the regex to
# contain the groups "number" and "name". # 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 return int(mindex), new_raw_string
else: else:
return None, None return None, None
@ -182,9 +185,7 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
if not matches and _CMD_IGNORE_PREFIXES: if not matches and _CMD_IGNORE_PREFIXES:
# still no match. Try to strip prefixes # still no match. Try to strip prefixes
raw_string = ( raw_string = raw_string.lstrip(_CMD_IGNORE_PREFIXES) if len(raw_string) > 1 else 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) matches = build_matches(raw_string, cmdset, include_prefixes=False)
# only select command matches we are actually allowed to call. # 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" perm = "perm" if self.persistent else "non-perm"
options = ", ".join([ options = ", ".join(
"{}:{}".format(opt, "T" if getattr(self, opt) else "F") [
for opt in ("no_exits", "no_objs", "no_channels", "duplicates") "{}:{}".format(opt, "T" if getattr(self, opt) else "F")
if getattr(self, opt) is not None for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
]) if getattr(self, opt) is not None
]
)
options = (", " + options) if options else "" options = (", " + options) if options else ""
return (f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: " return (
+ ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])) 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): def __iter__(self):
""" """
@ -519,10 +523,12 @@ class CmdSet(object, metaclass=_CmdSetMeta):
try: try:
cmdset = self._instantiate(cmdset) cmdset = self._instantiate(cmdset)
except RuntimeError: except RuntimeError:
err = ("Adding cmdset {cmdset} to {cls} lead to an " err = (
"infinite loop. When adding a cmdset to another, " "Adding cmdset {cmdset} to {cls} lead to an "
"make sure they are not themself cyclically added to " "infinite loop. When adding a cmdset to another, "
"the new cmdset somewhere in the chain.") "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__))) raise RuntimeError(_(err.format(cmdset=cmdset, cls=self.__class__)))
cmds = cmdset.commands cmds = cmdset.commands
elif is_iter(cmd): elif is_iter(cmd):

View file

@ -423,8 +423,7 @@ class CmdSetHandler(object):
self.mergetype_stack.append(new_current.actual_mergetype) self.mergetype_stack.append(new_current.actual_mergetype)
self.current = new_current self.current = new_current
def add(self, cmdset, emit_to_obj=None, persistent=False, default_cmdset=False, def add(self, cmdset, emit_to_obj=None, persistent=False, default_cmdset=False, **kwargs):
**kwargs):
""" """
Add a cmdset to the handler, on top of the old ones, unless it 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) 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: if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add() kwarg 'permanent' has changed name to " logger.log_dep(
"'persistent' and now defaults to True.") "obj.cmdset.add() kwarg 'permanent' has changed name to "
persistent = kwargs['permanent'] if persistent is None else persistent "'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)): if not (isinstance(cmdset, str) or utils.inherits_from(cmdset, CmdSet)):
string = _("Only CmdSets can be added to the cmdsethandler!") string = _("Only CmdSets can be added to the cmdsethandler!")
@ -491,9 +492,10 @@ class CmdSetHandler(object):
""" """
if "permanent" in kwargs: if "permanent" in kwargs:
logger.log_dep("obj.cmdset.add_default() kwarg 'permanent' has changed name to " logger.log_dep(
"'persistent'.") "obj.cmdset.add_default() kwarg 'permanent' has changed name to 'persistent'."
persistent = kwargs['permanent'] if persistent is None else persistent )
persistent = kwargs["permanent"] if persistent is None else persistent
self.add(cmdset, emit_to_obj=emit_to_obj, persistent=persistent, default_cmdset=True) self.add(cmdset, emit_to_obj=emit_to_obj, persistent=persistent, default_cmdset=True)
def remove(self, cmdset=None, default_cmdset=False): 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 # pre-prepare a help index entry for quicker lookup
# strip the @- etc to allow help to be agnostic # 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_key = cls.key[1:] if cls.key and cls.key[0] in CMD_IGNORE_PREFIXES else ""
stripped_aliases = ( stripped_aliases = " ".join(
" ".join(al[1:] if al and al[0] in CMD_IGNORE_PREFIXES else al al[1:] if al and al[0] in CMD_IGNORE_PREFIXES else al for al in cls.aliases
for al in cls.aliases)) )
cls.search_index_entry = { cls.search_index_entry = {
"key": cls.key, "key": cls.key,
"aliases": " ".join(cls.aliases), "aliases": " ".join(cls.aliases),
"no_prefix": f"{stripped_key} {stripped_aliases}", "no_prefix": f"{stripped_key} {stripped_aliases}",
"category": cls.help_category, "category": cls.help_category,
"text": cls.__doc__, "text": cls.__doc__,
"tags": "" "tags": "",
} }
@ -562,7 +562,7 @@ Command {self} has no defined `func()` - showing on-command variables:
""" """
try: try:
return reverse( return reverse(
'help-entry-detail', "help-entry-detail",
kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)},
) )
except Exception as e: except Exception as e:

View file

@ -217,7 +217,7 @@ class CmdBan(COMMAND_DEFAULT_CLASS):
ipregex = re.compile(r"%s" % ipregex) ipregex = re.compile(r"%s" % ipregex)
bantup = ("", ban, ipregex, now, reason) 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"): if str(ret).lower() in ("no", "n"):
self.caller.msg("Aborted.") self.caller.msg("Aborted.")
return return
@ -273,7 +273,7 @@ class CmdUnban(COMMAND_DEFAULT_CLASS):
ban = banlist[num - 1] ban = banlist[num - 1]
value = (" ".join([s for s in ban[:2]])).strip() 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"): if str(ret).lower() in ("n", "no"):
self.caller.msg("Aborted.") self.caller.msg("Aborted.")
return return

View file

@ -17,7 +17,8 @@ from evennia.utils.utils import (
class_from_module, class_from_module,
get_all_typeclasses, get_all_typeclasses,
variable_from_module, variable_from_module,
dbref, crop, dbref,
crop,
interactive, interactive,
list_to_string, list_to_string,
display_len, display_len,
@ -1498,9 +1499,11 @@ class CmdOpen(ObjManipCommand):
super().parse() super().parse()
self.location = self.caller.location self.location = self.caller.location
if not self.args or not self.rhs: if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]" self.caller.msg(
"[,<return exit>[;alias..][:typeclass]]] " "Usage: open <new exit>[;alias...][:typeclass]"
"= <destination>") "[,<return exit>[;alias..][:typeclass]]] "
"= <destination>"
)
raise InterruptCommand raise InterruptCommand
if not self.location: if not self.location:
self.caller.msg("You cannot create an exit from a None-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. as well as the self.create_exit() method.
""" """
# Create exit # Create exit
ok = self.create_exit(self.exit_name, self.location, self.destination, ok = self.create_exit(
self.exit_aliases, self.exit_typeclass) self.exit_name, self.location, self.destination, self.exit_aliases, self.exit_typeclass
)
if not ok: if not ok:
# an error; the exit was not created, so we quit. # an error; the exit was not created, so we quit.
return return
@ -1529,8 +1533,13 @@ class CmdOpen(ObjManipCommand):
back_exit_name = self.lhs_objs[1]["name"] back_exit_name = self.lhs_objs[1]["name"]
back_exit_aliases = self.lhs_objs[1]["aliases"] back_exit_aliases = self.lhs_objs[1]["aliases"]
back_exit_typeclass = self.lhs_objs[1]["option"] back_exit_typeclass = self.lhs_objs[1]["option"]
self.create_exit(back_exit_name, self.destination, self.location, back_exit_aliases, self.create_exit(
back_exit_typeclass) back_exit_name,
self.destination,
self.location,
back_exit_aliases,
back_exit_typeclass,
)
def _convert_from_string(cmd, strobj): def _convert_from_string(cmd, strobj):
@ -1740,8 +1749,10 @@ class CmdSetAttribute(ObjManipCommand):
obj.attributes.remove(attr, category=category) obj.attributes.remove(attr, category=category)
return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]." return f"\nDeleted attribute {obj.name}/|w{attr}|n [category:{category}]."
else: else:
return (f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] " return (
"was found to delete.") 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." error = f"\nNo attribute {obj.name}/|w{attr}|n [category: {category}] was found to delete."
if nested: if nested:
error += " (Nested lookups attempted)" error += " (Nested lookups attempted)"
@ -1813,7 +1824,7 @@ class CmdSetAttribute(ObjManipCommand):
except AttributeError: except AttributeError:
# we set empty buffer on nonexisting Attribute because otherwise # we set empty buffer on nonexisting Attribute because otherwise
# we'd always have the string "None" in the buffer to start with # 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 return str(old_value) # we already confirmed we are ok with this
def save(caller, buf): def save(caller, buf):
@ -1825,11 +1836,12 @@ class CmdSetAttribute(ObjManipCommand):
try: try:
old_value = obj.attributes.get(attr, raise_exception=True) old_value = obj.attributes.get(attr, raise_exception=True)
if not isinstance(old_value, str): 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. " 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. " "\nTo continue editing, it must be converted to (and saved as) a string. "
"Continue? [Y]/N?") "Continue? [Y]/N?"
if answer.lower() in ('n', 'no'): )
if answer.lower() in ("n", "no"):
self.caller.msg("Aborted edit.") self.caller.msg("Aborted edit.")
return return
except AttributeError: except AttributeError:
@ -1903,9 +1915,11 @@ class CmdSetAttribute(ObjManipCommand):
caller.msg("The Line editor can only be applied " "to one attribute at a time.") caller.msg("The Line editor can only be applied " "to one attribute at a time.")
return return
if not attrs: if not attrs:
caller.msg("Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo " caller.msg(
"edit the current room description, use `set/edit here/desc` (or " "Use `set/edit <objname>/<attr>` to define the Attribute to edit.\nTo "
"use the `desc` command).") "edit the current room description, use `set/edit here/desc` (or "
"use the `desc` command)."
)
return return
self.edit_handler(obj, attrs[0], caller) self.edit_handler(obj, attrs[0], caller)
return return
@ -1936,8 +1950,10 @@ class CmdSetAttribute(ObjManipCommand):
global _ATTRFUNCPARSER global _ATTRFUNCPARSER
if not _ATTRFUNCPARSER: if not _ATTRFUNCPARSER:
_ATTRFUNCPARSER = funcparser.FuncParser( _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")): 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 hasattr(parsed_value, "access"):
# if this is an object we must have the right to read it, if so, # if this is an object we must have the right to read it, if so,
# we will not convert it to a string # we will not convert it to a string
if not (parsed_value.access(caller, "control") if not (
or parsed_value.access(self.caller, "edit")): parsed_value.access(caller, "control")
caller.msg("You don't have permission to set " or parsed_value.access(self.caller, "edit")
f"object with identifier '{value}'.") ):
caller.msg(
"You don't have permission to set " f"object with identifier '{value}'."
)
continue continue
value = parsed_value value = parsed_value
else: else:
@ -2038,7 +2057,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
obj = caller.search(query) obj = caller.search(query)
if not obj: if not obj:
return return
elif (self.account and self.account.__dbclass__ == dbclass): elif self.account and self.account.__dbclass__ == dbclass:
# applying account while caller is object # applying account while caller is object
caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.") caller.msg(f"Trying to search {new_typeclass} with query '{self.lhs}'.")
obj = self.account.search(query) obj = self.account.search(query)
@ -2071,7 +2090,7 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
caller = self.caller 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() tclasses = get_all_typeclasses()
contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [ contribs = [key for key in sorted(tclasses) if key.startswith("evennia.contrib")] or [
"<None loaded>" "<None loaded>"
@ -2188,8 +2207,10 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
is_same = obj.is_typeclass(new_typeclass, exact=True) is_same = obj.is_typeclass(new_typeclass, exact=True)
if is_same and "force" not in self.switches: if is_same and "force" not in self.switches:
string = (f"{obj.name} already has the typeclass '{new_typeclass}'. " string = (
"Use /force to override.") f"{obj.name} already has the typeclass '{new_typeclass}'. "
"Use /force to override."
)
else: else:
update = "update" in self.switches update = "update" in self.switches
reset = "reset" in self.switches reset = "reset" in self.switches
@ -2220,7 +2241,8 @@ class CmdTypeclass(COMMAND_DEFAULT_CLASS):
if "prototype" in self.switches: if "prototype" in self.switches:
modified = spawner.batch_update_objects_with_prototype( modified = spawner.batch_update_objects_with_prototype(
prototype, objects=[obj], caller=self.caller) prototype, objects=[obj], caller=self.caller
)
prototype_success = modified > 0 prototype_success = modified > 0
if not prototype_success: if not prototype_success:
caller.msg("Prototype %s failed to apply." % prototype["key"]) caller.msg("Prototype %s failed to apply." % prototype["key"])
@ -2543,9 +2565,7 @@ class CmdExamine(ObjManipCommand):
def format_locks(self, obj): def format_locks(self, obj):
locks = str(obj.locks) locks = str(obj.locks)
if locks: if locks:
return utils.fill( return utils.fill("; ".join([lock for lock in locks.split(";")]), indent=2)
"; ".join([lock for lock in locks.split(";")]), indent=2
)
return "Default" return "Default"
def format_scripts(self, obj): def format_scripts(self, obj):
@ -2572,6 +2592,7 @@ class CmdExamine(ObjManipCommand):
if value: if value:
return f"{string}: T" return f"{string}: T"
return f"{string}: F" return f"{string}: F"
txt = ", ".join( txt = ", ".join(
_truefalse(opt, getattr(cmdset, opt)) _truefalse(opt, getattr(cmdset, opt))
for opt in ("no_exits", "no_objs", "no_channels", "duplicates") 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 # 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 # 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). # madness it is better that they overload with their own CmdExamine to handle it).
all_cmdsets.extend([(cmdset.key, cmdset) all_cmdsets.extend(
for cmdset in obj.account.sessions.all()[0].cmdset.all()]) [(cmdset.key, cmdset) for cmdset in obj.account.sessions.all()[0].cmdset.all()]
)
else: else:
try: try:
# we have to protect this since many objects don't have sessions. # we have to protect this since many objects don't have sessions.
all_cmdsets.extend([(cmdset.key, cmdset) all_cmdsets.extend(
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()]) [
(cmdset.key, cmdset)
for cmdset in obj.get_session(obj.sessions.get()).cmdset.all()
]
)
except (TypeError, AttributeError): except (TypeError, AttributeError):
# an error means we are merging an object without a session # an error means we are merging an object without a session
pass pass
@ -2659,8 +2685,10 @@ class CmdExamine(ObjManipCommand):
typ = f" |B[type: {typ}]|n" if typ else "" typ = f" |B[type: {typ}]|n" if typ else ""
value = utils.to_str(value) value = utils.to_str(value)
value = _FUNCPARSER.parse(ansi_raw(value), escape=True) value = _FUNCPARSER.parse(ansi_raw(value), escape=True)
return (f"Attribute {obj.name}/{self.header_color}{key}|n " return (
f"[category={category}]{typ}:\n\n{value}") f"Attribute {obj.name}/{self.header_color}{key}|n "
f"[category={category}]{typ}:\n\n{value}"
)
def format_single_attribute(self, attr): def format_single_attribute(self, attr):
global _FUNCPARSER global _FUNCPARSER
@ -2680,8 +2708,7 @@ class CmdExamine(ObjManipCommand):
def format_attributes(self, obj): def format_attributes(self, obj):
output = "\n " + "\n ".join( output = "\n " + "\n ".join(
sorted(self.format_single_attribute(attr) sorted(self.format_single_attribute(attr) for attr in obj.db_attributes.all())
for attr in obj.db_attributes.all())
) )
if output.strip(): if output.strip():
# we don't want just an empty line # we don't want just an empty line
@ -2695,8 +2722,7 @@ class CmdExamine(ObjManipCommand):
if ndb_attr and ndb_attr[0]: if ndb_attr and ndb_attr[0]:
return "\n " + " \n".join( return "\n " + " \n".join(
sorted(self.format_single_attribute(attr) sorted(self.format_single_attribute(attr) for attr, value in ndb_attr)
for attr, value in ndb_attr)
) )
def format_exits(self, obj): def format_exits(self, obj):
@ -2706,14 +2732,16 @@ class CmdExamine(ObjManipCommand):
def format_chars(self, obj): def format_chars(self, obj):
if hasattr(obj, "contents"): if hasattr(obj, "contents"):
chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents chars = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents if obj.account)
if obj.account)
return chars if chars else None return chars if chars else None
def format_things(self, obj): def format_things(self, obj):
if hasattr(obj, "contents"): if hasattr(obj, "contents"):
things = ", ".join(f"{obj.name}({obj.dbref})" for obj in obj.contents things = ", ".join(
if not obj.account and not obj.destination) f"{obj.name}({obj.dbref})"
for obj in obj.contents
if not obj.account and not obj.destination
)
return things if things else None return things if things else None
def format_script_desc(self, obj): def format_script_desc(self, obj):
@ -2736,8 +2764,10 @@ class CmdExamine(ObjManipCommand):
remaining_repeats = obj.remaining_repeats() remaining_repeats = obj.remaining_repeats()
remaining_repeats = 0 if remaining_repeats is None else remaining_repeats remaining_repeats = 0 if remaining_repeats is None else remaining_repeats
repeats = f" - {remaining_repeats}/{obj.db_repeats} remain" repeats = f" - {remaining_repeats}/{obj.db_repeats} remain"
return (f"{active} - interval: {interval}s " return (
f"(next: {next_repeat}{repeats}, start_delay: {start_delay})") f"{active} - interval: {interval}s "
f"(next: {next_repeat}{repeats}, start_delay: {start_delay})"
)
def format_channel_sub_totals(self, obj): def format_channel_sub_totals(self, obj):
if hasattr(obj, "db_account_subscriptions"): if hasattr(obj, "db_account_subscriptions"):
@ -2752,14 +2782,16 @@ class CmdExamine(ObjManipCommand):
account_subs = obj.db_account_subscriptions.all() account_subs = obj.db_account_subscriptions.all()
if account_subs: if account_subs:
return "\n " + "\n ".join( 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): def format_channel_object_subs(self, obj):
if hasattr(obj, "db_object_subscriptions"): if hasattr(obj, "db_object_subscriptions"):
object_subs = obj.db_object_subscriptions.all() object_subs = obj.db_object_subscriptions.all()
if object_subs: if object_subs:
return "\n " + "\n ".join( 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): def get_formatted_obj_data(self, obj, current_cmdset):
""" """
@ -2781,13 +2813,14 @@ class CmdExamine(ObjManipCommand):
objdata["Destination"] = self.format_destination(obj) objdata["Destination"] = self.format_destination(obj)
objdata["Permissions"] = self.format_permissions(obj) objdata["Permissions"] = self.format_permissions(obj)
objdata["Locks"] = self.format_locks(obj) objdata["Locks"] = self.format_locks(obj)
if (current_cmdset if current_cmdset and not (
and not (len(obj.cmdset.all()) == 1 len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "_EMPTY_CMDSET"
and obj.cmdset.current.key == "_EMPTY_CMDSET")): ):
objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj) objdata["Stored Cmdset(s)"] = self.format_stored_cmdsets(obj)
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset) objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"] = ( objdata[
self.format_current_cmds(obj, current_cmdset)) f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"
] = self.format_current_cmds(obj, current_cmdset)
if self.object_type == "script": if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj) objdata["Description"] = self.format_script_desc(obj)
objdata["Persistent"] = self.format_script_is_persistent(obj) objdata["Persistent"] = self.format_script_is_persistent(obj)
@ -2859,10 +2892,11 @@ class CmdExamine(ObjManipCommand):
obj = None obj = None
elif len(obj) > 1: elif len(obj) > 1:
err = "Multiple {objtype} found with key {obj_name}:\n{matches}" err = "Multiple {objtype} found with key {obj_name}:\n{matches}"
self.caller.msg(err.format( self.caller.msg(
obj_name=obj_name, err.format(
matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj) obj_name=obj_name, matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj)
)) )
)
obj = None obj = None
else: else:
obj = obj[0] obj = obj[0]
@ -2887,13 +2921,16 @@ class CmdExamine(ObjManipCommand):
# is not so common anyway. # is not so common anyway.
obj = None obj = None
obj_name = objdef["name"] # name obj_name = objdef["name"] # name
obj_attrs = objdef["attrs"] # /attrs obj_attrs = objdef["attrs"] # /attrs
# identify object type, in prio account - script - channel # identify object type, in prio account - script - channel
object_type = "object" object_type = "object"
if (utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount") if (
or "account" in self.switches or obj_name.startswith("*")): utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
or "account" in self.switches
or obj_name.startswith("*")
):
object_type = "account" object_type = "account"
elif "script" in self.switches: elif "script" in self.switches:
object_type = "script" object_type = "script"
@ -3293,7 +3330,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
"start": "|gStarted|n", "start": "|gStarted|n",
"stop": "|RStopped|n", "stop": "|RStopped|n",
"pause": "|Paused|n", "pause": "|Paused|n",
"delete": "|rDeleted|n" "delete": "|rDeleted|n",
} }
def _search_script(self, args): def _search_script(self, args):
@ -3307,7 +3344,7 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
return scripts return scripts
if "-" in args: if "-" in args:
# may be a dbref-range # 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: if val1 and val2:
scripts = ScriptDB.objects.filter(id__in=(range(val1, val2 + 1))) scripts = ScriptDB.objects.filter(id__in=(range(val1, val2 + 1)))
if scripts: if scripts:
@ -3348,11 +3385,14 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
if obj.scripts.add(self.rhs, autostart=True): if obj.scripts.add(self.rhs, autostart=True):
caller.msg( caller.msg(
f"Script |w{self.rhs}|n successfully added and " 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: else:
caller.msg(f"Script {self.rhs} could not be added and/or started " caller.msg(
f"on {obj.get_display_name(caller)} (or it started and " f"Script {self.rhs} could not be added and/or started "
"immediately shut down).") f"on {obj.get_display_name(caller)} (or it started and "
"immediately shut down)."
)
else: else:
# just show all scripts on object # just show all scripts on object
scripts = ScriptDB.objects.filter(db_obj=obj) scripts = ScriptDB.objects.filter(db_obj=obj)
@ -3374,12 +3414,15 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
new_script = None new_script = None
if new_script: if new_script:
caller.msg(f"Global Script Created - " caller.msg(
f"{new_script.key} ({new_script.typeclass_path})") f"Global Script Created - "
f"{new_script.key} ({new_script.typeclass_path})"
)
ScriptEvMore(caller, [new_script], session=self.session) ScriptEvMore(caller, [new_script], session=self.session)
else: else:
caller.msg(f"Global Script |rNOT|n Created |r(see log)|n - " caller.msg(
f"arguments: {self.args}") f"Global Script |rNOT|n Created |r(see log)|n - " f"arguments: {self.args}"
)
elif scripts or obj: elif scripts or obj:
# modification switches - must operate on existing scripts # modification switches - must operate on existing scripts
@ -3388,9 +3431,11 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
scripts = ScriptDB.objects.filter(db_obj=obj) scripts = ScriptDB.objects.filter(db_obj=obj)
if scripts.count() > 1: if scripts.count() > 1:
ret = yield(f"Multiple scripts found: {scripts}. Are you sure you want to " ret = yield (
"operate on all of them? [Y]/N? ") f"Multiple scripts found: {scripts}. Are you sure you want to "
if ret.lower() in ('n', 'no'): "operate on all of them? [Y]/N? "
)
if ret.lower() in ("n", "no"):
caller.msg("Aborted.") caller.msg("Aborted.")
return return
@ -3406,11 +3451,14 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
getattr(script, switch)() getattr(script, switch)()
except Exception: except Exception:
logger.log_trace() logger.log_trace()
msgs.append(f"{scripttype} |rNOT|n {verb} |r(see log)|n - " msgs.append(
f"{script_key} ({script_typeclass_path})|n") f"{scripttype} |rNOT|n {verb} |r(see log)|n - "
f"{script_key} ({script_typeclass_path})|n"
)
else: else:
msgs.append(f"{scripttype} {verb} - " msgs.append(
f"{script_key} ({script_typeclass_path})") f"{scripttype} {verb} - " f"{script_key} ({script_typeclass_path})"
)
caller.msg("\n".join(msgs)) caller.msg("\n".join(msgs))
if "delete" not in self.switches: if "delete" not in self.switches:
ScriptEvMore(caller, [script], session=self.session) ScriptEvMore(caller, [script], session=self.session)
@ -3488,7 +3536,7 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
) )
# last N table # 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( latesttable = self.styled_table(
"|wcreated|n", "|wdbref|n", "|wname|n", "|wtypeclass|n", align="l", border="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 # check any locks
if not (caller.permissions.check("Admin") or obj_to_teleport.access(caller, "teleport")): 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 " caller.msg(
"it anywhere.") f"{obj_to_teleport} 'teleport'-lock blocks you from teleporting " "it anywhere."
)
return return
if not (caller.permissions.check("Admin") if not (
or destination.access(obj_to_teleport, "teleport_here")): caller.permissions.check("Admin")
caller.msg(f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from " or destination.access(obj_to_teleport, "teleport_here")
"moving there.") ):
caller.msg(
f"{destination} 'teleport_here'-lock blocks {obj_to_teleport} from " "moving there."
)
return return
# try the teleport # try the teleport
@ -3636,8 +3688,11 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
obj_to_teleport.location = destination obj_to_teleport.location = destination
caller.msg(f"Teleported {obj_to_teleport} None -> {destination}") caller.msg(f"Teleported {obj_to_teleport} None -> {destination}")
elif obj_to_teleport.move_to( elif obj_to_teleport.move_to(
destination, quiet="quiet" in self.switches, destination,
emit_to_obj=caller, use_destination="intoexit" not in self.switches): quiet="quiet" in self.switches,
emit_to_obj=caller,
use_destination="intoexit" not in self.switches,
):
if obj_to_teleport == caller: if obj_to_teleport == caller:
caller.msg(f"Teleported to {destination}.") caller.msg(f"Teleported to {destination}.")
@ -3995,7 +4050,7 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
self.caller.msg("No prototypes found.") self.caller.msg("No prototypes found.")
def _list_prototypes(self, key=None, tags=None): 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) protlib.list_prototypes(self.caller, key=key, tags=tags, session=self.session)
@interactive @interactive
@ -4039,7 +4094,9 @@ class CmdSpawn(COMMAND_DEFAULT_CLASS):
return return
try: try:
n_updated = spawner.batch_update_objects_with_prototype( n_updated = spawner.batch_update_objects_with_prototype(
prototype, objects=existing_objects, caller=caller, prototype,
objects=existing_objects,
caller=caller,
) )
except Exception: except Exception:
logger.log_trace() 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) COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
CHANNEL_DEFAULT_TYPECLASS = class_from_module( 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 # limit symbol import for API
__all__ = ( __all__ = (
"CmdChannel", "CmdChannel",
"CmdObjectChannel", "CmdObjectChannel",
"CmdPage", "CmdPage",
"CmdIRC2Chan", "CmdIRC2Chan",
"CmdIRCStatus", "CmdIRCStatus",
"CmdRSS2Chan", "CmdRSS2Chan",
@ -207,6 +206,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
ban mychannel1,mychannel2= EvilUser : Was banned for spamming. ban mychannel1,mychannel2= EvilUser : Was banned for spamming.
""" """
key = "@channel" key = "@channel"
aliases = ["@chan", "@channels"] aliases = ["@chan", "@channels"]
help_category = "Comms" help_category = "Comms"
@ -215,8 +215,25 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# the manage: lock controls access to /create/destroy/desc/lock/unlock switches # 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)" locks = "cmd:not pperm(channel_banned);admin:all();manage:all();changelocks:perm(Admin)"
switch_options = ( switch_options = (
"list", "all", "history", "sub", "unsub", "mute", "unmute", "alias", "unalias", "list",
"create", "destroy", "desc", "lock", "unlock", "boot", "ban", "unban", "who",) "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 # disable this in child command classes if wanting on-character channels
account_caller = True account_caller = True
@ -253,17 +270,24 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact) channels = CHANNEL_DEFAULT_TYPECLASS.objects.channel_search(channelname, exact=exact)
# check permissions # check permissions
channels = [channel for channel in channels channels = [
if channel.access(caller, 'listen') or channel.access(caller, 'control')] channel
for channel in channels
if channel.access(caller, "listen") or channel.access(caller, "control")
]
if handle_errors: if handle_errors:
if not channels: if not channels:
self.msg(f"No channel found matching '{channelname}' " self.msg(
"(could also be due to missing access).") f"No channel found matching '{channelname}' "
"(could also be due to missing access)."
)
return None return None
elif len(channels) > 1: elif len(channels) > 1:
self.msg("Multiple possible channel matches/alias for " self.msg(
"'{channelname}':\n" + ", ".join(chan.key for chan in channels)) "Multiple possible channel matches/alias for "
"'{channelname}':\n" + ", ".join(chan.key for chan in channels)
)
return None return None
return channels[0] return channels[0]
else: else:
@ -312,6 +336,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
return self.msg( return self.msg(
"".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines) "".join(line.split("[-]", 1)[1] if "[-]" in line else line for line in lines)
) )
# asynchronously tail the log file # asynchronously tail the log file
tail_log_file(log_file, start_index, 20, callback=send_msg) 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 lockstring = "send:all();listen:all();control:id(%s)" % caller.id
new_chan = create.create_channel( 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) self.sub_to_channel(new_chan)
return new_chan, "" return new_chan, ""
@ -514,14 +540,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
channel_key = channel.key channel_key = channel.key
if message is None: if message is None:
message = (f"|rChannel {channel_key} is being destroyed. " message = (
"Make sure to clean any channel aliases.|n") f"|rChannel {channel_key} is being destroyed. "
"Make sure to clean any channel aliases.|n"
)
if message: if message:
channel.msg(message, senders=caller, bypass_mute=True) channel.msg(message, senders=caller, bypass_mute=True)
channel.delete() channel.delete()
logger.log_sec( logger.log_sec("Channel {} was deleted by {}".format(channel_key, caller))
"Channel {} was deleted by {}".format(channel_key, caller)
)
def set_lock(self, channel, lockstring): def set_lock(self, channel, lockstring):
""" """
@ -610,8 +636,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if not quiet: if not quiet:
channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}") channel.msg(f"{target.key} was booted from channel by {self.caller.key}.{reason}")
logger.log_sec(f"Channel Boot: {target} (Channel: {channel}, " logger.log_sec(
f"Reason: {reason.strip()}, Caller: {self.caller}") f"Channel Boot: {target} (Channel: {channel}, "
f"Reason: {reason.strip()}, Caller: {self.caller}"
)
return True, "" return True, ""
def ban_user(self, channel, target, quiet=False, reason=""): def ban_user(self, channel, target, quiet=False, reason=""):
@ -684,7 +712,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
caller = self.caller caller = self.caller
mute_list = list(channel.mutelist) mute_list = list(channel.mutelist)
online_list = channel.subscriptions.online() online_list = channel.subscriptions.online()
if channel.access(caller, 'control'): if channel.access(caller, "control"):
# for those with channel control, show also offline users # for those with channel control, show also offline users
all_subs = list(channel.subscriptions.all()) all_subs = list(channel.subscriptions.all())
else: else:
@ -694,8 +722,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
who_list = [] who_list = []
for subscriber in all_subs: for subscriber in all_subs:
name = subscriber.get_display_name(caller) name = subscriber.get_display_name(caller)
conditions = ("muting" if subscriber in mute_list else "", conditions = (
"offline" if subscriber not in online_list else "") "muting" if subscriber in mute_list else "",
"offline" if subscriber not in online_list else "",
)
conditions = [cond for cond in conditions if cond] conditions = [cond for cond in conditions if cond]
cond_text = "(" + ", ".join(conditions) + ")" if conditions else "" cond_text = "(" + ", ".join(conditions) + ")" if conditions else ""
who_list.append(f"{name}{cond_text}") who_list.append(f"{name}{cond_text}")
@ -743,7 +773,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"locks", "locks",
"description", "description",
align="l", align="l",
maxwidth=_DEFAULT_WIDTH maxwidth=_DEFAULT_WIDTH,
) )
for chan in subscribed: for chan in subscribed:
@ -756,14 +786,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
my_aliases = ", ".join(self.get_channel_aliases(chan)) my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row( comtable.add_row(
*( *(
chanid, chanid,
"{key}{aliases}".format( "{key}{aliases}".format(
key=chan.key, key=chan.key,
aliases=";"+ ";".join(chan.aliases.all()) if chan.aliases.all() else "" aliases=";" + ";".join(chan.aliases.all()) if chan.aliases.all() else "",
), ),
my_aliases, my_aliases,
locks, locks,
chan.db.desc chan.db.desc,
) )
) )
return comtable return comtable
@ -799,11 +829,14 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
substatus = "|gYes|n" substatus = "|gYes|n"
my_aliases = ", ".join(self.get_channel_aliases(chan)) my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row( comtable.add_row(
*(substatus, *(
chan.key, substatus,
",".join(chan.aliases.all()) if chan.aliases.all() else "", chan.key,
my_aliases, ",".join(chan.aliases.all()) if chan.aliases.all() else "",
chan.db.desc)) my_aliases,
chan.db.desc,
)
)
comtable.reformat_column(0, width=8) comtable.reformat_column(0, width=8)
return comtable return comtable
@ -818,16 +851,17 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
switches = self.switches switches = self.switches
channel_names = [name for name in self.lhslist if name] 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 # show all available channels
subscribed, available = self.list_channels() subscribed, available = self.list_channels()
table = self.display_all_channels(subscribed, available) table = self.display_all_channels(subscribed, available)
self.msg( self.msg(
"\n|wAvailable channels|n (use no argument to " "\n|wAvailable channels|n (use no argument to "
f"only show your subscriptions)\n{table}") f"only show your subscriptions)\n{table}"
)
return return
if not channel_names: if not channel_names:
@ -835,15 +869,16 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
subscribed, _ = self.list_channels() subscribed, _ = self.list_channels()
table = self.display_subbed_channels(subscribed) table = self.display_subbed_channels(subscribed)
self.msg("\n|wChannel subscriptions|n " self.msg(
f"(use |w/all|n to see all available):\n{table}") "\n|wChannel subscriptions|n " f"(use |w/all|n to see all available):\n{table}"
)
return return
if not self.switches and not self.args: if not self.switches and not self.args:
self.msg("Usage[/switches]: channel [= message]") self.msg("Usage[/switches]: channel [= message]")
return return
if 'create' in switches: if "create" in switches:
# create a new channel # create a new channel
if not self.access(caller, "manage"): if not self.access(caller, "manage"):
@ -865,7 +900,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err) self.msg(err)
return return
if 'unalias' in switches: if "unalias" in switches:
# remove a personal alias (no channel needed) # remove a personal alias (no channel needed)
alias = self.args.strip() alias = self.args.strip()
if not alias: 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 # channels without a space in their name), we need to check if the
# first 'channel name' is in fact 'channelname text' # first 'channel name' is in fact 'channelname text'
no_rhs_channel_name = self.args.split(" ", 1)[0] no_rhs_channel_name = self.args.split(" ", 1)[0]
possible_lhs_message = self.args[len(no_rhs_channel_name):] possible_lhs_message = self.args[len(no_rhs_channel_name) :]
if possible_lhs_message.strip() == '=': if possible_lhs_message.strip() == "=":
possible_lhs_message = "" possible_lhs_message = ""
channel_names.append(no_rhs_channel_name) channel_names.append(no_rhs_channel_name)
channels = [] channels = []
errors = [] errors = []
for channel_name in channel_names: for channel_name in channel_names:
@ -897,16 +931,20 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
# 'listen/control' perms. # 'listen/control' perms.
found_channels = self.search_channel(channel_name, exact=False, handle_errors=False) found_channels = self.search_channel(channel_name, exact=False, handle_errors=False)
if not found_channels: if not found_channels:
errors.append(f"No channel found matching '{channel_name}' " errors.append(
"(could also be due to missing access).") f"No channel found matching '{channel_name}' "
"(could also be due to missing access)."
)
elif len(found_channels) > 1: elif len(found_channels) > 1:
errors.append("Multiple possible channel matches/alias for " errors.append(
"'{channel_name}':\n" + ", ".join(chan.key for chan in found_channels)) "Multiple possible channel matches/alias for "
"'{channel_name}':\n" + ", ".join(chan.key for chan in found_channels)
)
else: else:
channels.append(found_channels[0]) channels.append(found_channels[0])
if not channels: if not channels:
self.msg('\n'.join(errors)) self.msg("\n".join(errors))
return return
# we have at least one channel at this point # we have at least one channel at this point
@ -925,30 +963,35 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if channel in subscribed: if channel in subscribed:
table = self.display_subbed_channels([channel]) table = self.display_subbed_channels([channel])
header = f"Channel |w{channel.key}|n" header = f"Channel |w{channel.key}|n"
self.msg(f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) " self.msg(
f"to chat and the 'channel' command " f"{header}\n(use |w{channel.key} <msg>|n (or a channel-alias) "
f"to customize)\n{table}") f"to chat and the 'channel' command "
f"to customize)\n{table}"
)
elif channel in available: elif channel in available:
table = self.display_all_channels([], [channel]) table = self.display_all_channels([], [channel])
self.msg( self.msg(
"\n|wNot subscribed to this channel|n (use /list to " "\n|wNot subscribed to this channel|n (use /list to "
f"show all subscriptions)\n{table}") f"show all subscriptions)\n{table}"
)
return return
if 'history' in switches or 'hist' in switches: if "history" in switches or "hist" in switches:
# view channel history # view channel history
index = self.rhs or 0 index = self.rhs or 0
try: try:
index = max(0, int(index)) index = max(0, int(index))
except ValueError: except ValueError:
self.msg("The history index (describing how many lines to go back) " self.msg(
"must be an integer >= 0.") "The history index (describing how many lines to go back) "
"must be an integer >= 0."
)
return return
self.get_channel_history(channel, start_index=index) self.get_channel_history(channel, start_index=index)
return return
if 'sub' in switches: if "sub" in switches:
# subscribe to a channel # subscribe to a channel
aliases = [] aliases = []
if self.rhs: if self.rhs:
@ -957,26 +1000,29 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
if success: if success:
for alias in aliases: for alias in aliases:
self.add_alias(channel, alias) self.add_alias(channel, alias)
alias_txt = ', '.join(aliases) alias_txt = ", ".join(aliases)
alias_txt = f" using alias(es) {alias_txt}" if aliases else '' alias_txt = f" using alias(es) {alias_txt}" if aliases else ""
self.msg("You are now subscribed " self.msg(
f"to the channel {channel.key}{alias_txt}. Use /alias to " "You are now subscribed "
"add additional aliases for referring to the channel.") f"to the channel {channel.key}{alias_txt}. Use /alias to "
"add additional aliases for referring to the channel."
)
else: else:
self.msg(err) self.msg(err)
return return
if 'unsub' in switches: if "unsub" in switches:
# un-subscribe from a channel # un-subscribe from a channel
success, err = self.unsub_from_channel(channel) success, err = self.unsub_from_channel(channel)
if success: if success:
self.msg(f"You un-subscribed from channel {channel.key}. " self.msg(
"All aliases were cleared.") f"You un-subscribed from channel {channel.key}. " "All aliases were cleared."
)
else: else:
self.msg(err) self.msg(err)
return return
if 'alias' in switches: if "alias" in switches:
# create a new personal alias for a channel # create a new personal alias for a channel
alias = self.rhs alias = self.rhs
if not alias: if not alias:
@ -986,7 +1032,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.") self.msg(f"Added/updated your alias '{alias}' for channel {channel.key}.")
return return
if 'mute' in switches: if "mute" in switches:
# mute a given channel # mute a given channel
success, err = self.mute_channel(channel) success, err = self.mute_channel(channel)
if success: if success:
@ -995,7 +1041,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err) self.msg(err)
return return
if 'unmute' in switches: if "unmute" in switches:
# unmute a given channel # unmute a given channel
success, err = self.unmute_channel(channel) success, err = self.unmute_channel(channel)
if success: if success:
@ -1004,7 +1050,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(err) self.msg(err)
return return
if 'destroy' in switches or 'delete' in switches: if "destroy" in switches or "delete" in switches:
# destroy a channel we control # destroy a channel we control
if not self.access(caller, "manage"): if not self.access(caller, "manage"):
@ -1028,10 +1074,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"remove all users' aliases. {options}?", "remove all users' aliases. {options}?",
yes_action=_perform_delete, yes_action=_perform_delete,
no_action="Aborted.", no_action="Aborted.",
default="N" default="N",
) )
if 'desc' in switches: if "desc" in switches:
# set channel description # set channel description
if not self.access(caller, "manage"): if not self.access(caller, "manage"):
@ -1051,7 +1097,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.set_desc(channel, desc) self.set_desc(channel, desc)
self.msg("Updated channel description.") self.msg("Updated channel description.")
if 'lock' in switches: if "lock" in switches:
# add a lockstring to channel # add a lockstring to channel
if not self.access(caller, "changelocks"): if not self.access(caller, "changelocks"):
@ -1075,7 +1121,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Could not add/update lock: {err}") self.msg(f"Could not add/update lock: {err}")
return return
if 'unlock' in switches: if "unlock" in switches:
# remove/update lockstring from channel # remove/update lockstring from channel
if not self.access(caller, "changelocks"): if not self.access(caller, "changelocks"):
@ -1099,7 +1145,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Could not remove lock: {err}") self.msg(f"Could not remove lock: {err}")
return return
if 'boot' in switches: if "boot" in switches:
# boot a user from channel(s) # boot a user from channel(s)
if not self.access(caller, "admin"): 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}") self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
channames = ", ".join(chan.key for chan in channels) channames = ", ".join(chan.key for chan in channels)
reasonwarn = (". Also note that your reason will be echoed to the channel" reasonwarn = (
if reason else '') ". Also note that your reason will be echoed to the channel" if reason else ""
)
ask_yes_no( ask_yes_no(
caller, caller,
prompt=f"Are you sure you want to boot user {target.key} from " prompt=f"Are you sure you want to boot user {target.key} from "
@ -1143,11 +1190,11 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
"{options}?", "{options}?",
yes_action=_boot_user, yes_action=_boot_user,
no_action="Aborted.", no_action="Aborted.",
default="Y" default="Y",
) )
return return
if 'ban' in switches: if "ban" in switches:
# ban a user from channel(s) # ban a user from channel(s)
if not self.access(caller, "admin"): 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}") self.msg(f"You need 'control'-access to view bans on channel {channel.key}")
return return
bans = ["Channel bans " bans = [
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"] "Channel bans "
"(to ban, use channel/ban channel[,channel,...] = username [:reason]"
]
bans.extend(self.channel_list_bans(channel)) bans.extend(self.channel_list_bans(channel))
self.msg("\n".join(bans)) self.msg("\n".join(bans))
return return
@ -1191,8 +1240,9 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}") self.msg(f"Cannot boot {target.key} from channel {chan.key}: {err}")
channames = ", ".join(chan.key for chan in channels) channames = ", ".join(chan.key for chan in channels)
reasonwarn = (". Also note that your reason will be echoed to the channel" reasonwarn = (
if reason else '') ". Also note that your reason will be echoed to the channel" if reason else ""
)
ask_yes_no( ask_yes_no(
caller, caller,
f"Are you sure you want to ban user {target.key} from " f"Are you sure you want to ban user {target.key} from "
@ -1203,7 +1253,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
) )
return return
if 'unban' in switches: if "unban" in switches:
# unban a previously banned user from channel # unban a previously banned user from channel
if not self.access(caller, "admin"): if not self.access(caller, "admin"):
@ -1414,7 +1464,6 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
receiver=receiver, receiver=receiver,
message=page.message, message=page.message,
) )
) )
lastpages = "\n ".join(listing) lastpages = "\n ".join(listing)
@ -1465,6 +1514,7 @@ def _list_bots(cmd):
else: else:
return "No irc bots found." return "No irc bots found."
class CmdIRC2Chan(COMMAND_DEFAULT_CLASS): class CmdIRC2Chan(COMMAND_DEFAULT_CLASS):
""" """
Link an evennia channel to an external IRC channel 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." string = "You are not carrying anything."
else: else:
from evennia.utils.ansi import raw as raw_ansi from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header") table = self.styled_table(border="header")
for item in items: for item in items:
table.add_row(f"|C{item.name}|n", table.add_row(
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or "")) 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}" string = f"|wYou are carrying:\n{table}"
self.caller.msg(string) self.caller.msg(string)

View file

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

View file

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

View file

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

View file

@ -107,7 +107,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
""" """
if not self._log_file: if not self._log_file:
self._log_file = self.attributes.get( 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 return self._log_file
def set_log_filename(self, filename): def set_log_filename(self, filename):
@ -455,8 +456,13 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
# needing to use the `channel` command explicitly. # needing to use the `channel` command explicitly.
msg_nick_pattern = self.channel_msg_nick_pattern.format(alias=alias) msg_nick_pattern = self.channel_msg_nick_pattern.format(alias=alias)
msg_nick_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key) msg_nick_replacement = self.channel_msg_nick_replacement.format(channelname=chan_key)
user.nicks.add(msg_nick_pattern, msg_nick_replacement, category="inputline", user.nicks.add(
pattern_is_regex=True, **kwargs) msg_nick_pattern,
msg_nick_replacement,
category="inputline",
pattern_is_regex=True,
**kwargs,
)
if chan_key != alias: if chan_key != alias:
# this allows for using the alias for general channel lookups # this allows for using the alias for general channel lookups
@ -546,7 +552,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
if not bypass_mute: if not bypass_mute:
receivers = [receiver for receiver in receivers if receiver not in self.mutelist] 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 # pre-send hook
message = self.at_pre_msg(message, **send_kwargs) 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. # TODO Evennia 1.0+ removed hooks. Remove in 1.1.
def message_transform(self, *args, **kwargs): def message_transform(self, *args, **kwargs):
raise RuntimeError("Channel.message_transform is no longer used in 1.0+. " raise RuntimeError(
"Use Account/Object.at_pre_channel_msg instead.") "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): def distribute_message(self, msgobj, online=False, **kwargs):
raise RuntimeError("Channel.distribute_message is no longer used in 1.0+.") raise RuntimeError("Channel.distribute_message is no longer used in 1.0+.")
def format_senders(self, senders=None, **kwargs): def format_senders(self, senders=None, **kwargs):
raise RuntimeError("Channel.format_senders is no longer used in 1.0+. " raise RuntimeError(
"Use Account/Object.at_pre_channel_msg instead.") "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): def pose_transform(self, msgobj, sender_string, **kwargs):
raise RuntimeError("Channel.pose_transform is no longer used in 1.0+. " raise RuntimeError(
"Use Account/Object.at_pre_channel_msg instead.") "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): def format_external(self, msgobj, senders, emit=False, **kwargs):
raise RuntimeError("Channel.format_external is no longer used in 1.0+. " raise RuntimeError(
"Use Account/Object.at_pre_channel_msg instead.") "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): def format_message(self, msgobj, emit=False, **kwargs):
raise RuntimeError("Channel.format_message is no longer used in 1.0+. " raise RuntimeError(
"Use Account/Object.at_pre_channel_msg instead.") "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): def pre_send_message(self, msg, **kwargs):
raise RuntimeError("Channel.pre_send_message was renamed to Channel.at_pre_msg.") 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) return self.filter(db_receivers_accounts=obj).exclude(db_hide_from_accounts=obj)
elif typ == "object": elif typ == "object":
return self.filter(db_receivers_objects=obj).exclude(db_hide_from_objects=obj) 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) return self.filter(db_receivers_scripts=obj)
else: else:
raise CommError raise CommError
@ -257,7 +257,7 @@ class MsgManager(TypedObjectManager):
sender_restrict = Q(db_sender_accounts__pk=spk) & ~Q(db_hide_from_accounts__pk=spk) sender_restrict = Q(db_sender_accounts__pk=spk) & ~Q(db_hide_from_accounts__pk=spk)
elif styp == "object": elif styp == "object":
sender_restrict = Q(db_sender_objects__pk=spk) & ~Q(db_hide_from_objects__pk=spk) 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) sender_restrict = Q(db_sender_scripts__pk=spk)
else: else:
sender_restrict = Q() sender_restrict = Q()
@ -266,16 +266,16 @@ class MsgManager(TypedObjectManager):
if receiver: if receiver:
rpk = receiver.pk rpk = receiver.pk
if rtyp == "account": if rtyp == "account":
receiver_restrict = ( receiver_restrict = Q(db_receivers_accounts__pk=rpk) & ~Q(db_hide_from_accounts__pk=rpk)
Q(db_receivers_accounts__pk=rpk) & ~Q(db_hide_from_accounts__pk=rpk))
elif rtyp == "object": elif rtyp == "object":
receiver_restrict = Q(db_receivers_objects__pk=rpk) & ~Q(db_hide_from_objects__pk=rpk) 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) receiver_restrict = Q(db_receivers_scripts__pk=rpk)
elif rtyp == "channel": elif rtyp == "channel":
raise DeprecationWarning( raise DeprecationWarning(
"Msg.objects.search don't accept channel recipients since " "Msg.objects.search don't accept channel recipients since "
"Channels no longer accepts Msg objects.") "Channels no longer accepts Msg objects."
)
else: else:
receiver_restrict = Q() receiver_restrict = Q()
# filter by full text # filter by full text
@ -289,8 +289,9 @@ class MsgManager(TypedObjectManager):
# back-compatibility alias # back-compatibility alias
message_search = search_message message_search = search_message
def create_message(self, senderobj, message, receivers=None, locks=None, tags=None, def create_message(
header=None, **kwargs): self, senderobj, message, receivers=None, locks=None, tags=None, header=None, **kwargs
):
""" """
Create a new communication Msg. Msgs represent a unit of Create a new communication Msg. Msgs represent a unit of
database-persistent communication between entites. database-persistent communication between entites.
@ -315,7 +316,7 @@ class MsgManager(TypedObjectManager):
it's up to the command definitions to limit this as desired. it's up to the command definitions to limit this as desired.
""" """
if 'channels' in kwargs: if "channels" in kwargs:
raise DeprecationWarning( raise DeprecationWarning(
"create_message() does not accept 'channel' kwarg anymore " "create_message() does not accept 'channel' kwarg anymore "
"- channels no longer accept Msg objects." "- channels no longer accept Msg objects."
@ -339,6 +340,7 @@ class MsgManager(TypedObjectManager):
new_message.save() new_message.save()
return new_message return new_message
# #
# Channel manager # Channel manager
# #

View file

@ -104,7 +104,7 @@ class Msg(SharedMemoryModel):
blank=True, blank=True,
db_index=True, db_index=True,
help_text="Identifier for single external sender, for use with senders " 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( db_receivers_accounts = models.ManyToManyField(
@ -137,7 +137,7 @@ class Msg(SharedMemoryModel):
blank=True, blank=True,
db_index=True, db_index=True,
help_text="Identifier for single external receiver, for use with recievers " 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 # 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): if isinstance(receivers, str):
self.db_receiver_external = receivers self.db_receiver_external = receivers
self.save(update_fields=['db_receiver_external']) self.save(update_fields=["db_receiver_external"])
return return
for receiver in make_iter(receivers): for receiver in make_iter(receivers):

View file

@ -21,7 +21,9 @@ class ObjectCreationTest(BaseEvenniaTest):
class ChannelWholistTests(BaseEvenniaTest): class ChannelWholistTests(BaseEvenniaTest):
def setUp(self): def setUp(self):
super().setUp() 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) self.default_channel.connect(self.obj1)
def test_wholist_shows_subscribed_objects(self): def test_wholist_shows_subscribed_objects(self):
@ -31,7 +33,9 @@ class ChannelWholistTests(BaseEvenniaTest):
def test_wholist_shows_none_when_empty(self): def test_wholist_shows_none_when_empty(self):
# No one hates dogs # 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>" expected = "<None>"
result = empty_channel.wholist result = empty_channel.wholist
self.assertEqual(expected, result) self.assertEqual(expected, result)

View file

@ -165,7 +165,9 @@ def check_location(storage):
correct = storage.location.lstrip("/") correct = storage.location.lstrip("/")
raise ImproperlyConfigured( raise ImproperlyConfigured(
"{}.location cannot begin with a leading slash. Found '{}'. Use '{}' instead.".format( "{}.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 = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with( 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): def test_storage_save_with_acl(self):
@ -136,7 +140,11 @@ class S3Boto3StorageTests(S3Boto3TestCase):
obj = self.storage.bucket.Object.return_value obj = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with( 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): def test_content_type(self):
@ -151,7 +159,11 @@ class S3Boto3StorageTests(S3Boto3TestCase):
obj = self.storage.bucket.Object.return_value obj = self.storage.bucket.Object.return_value
obj.upload_fileobj.assert_called_with( 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): def test_storage_save_gzipped(self):
@ -379,7 +391,10 @@ class S3Boto3StorageTests(S3Boto3TestCase):
self.assertEqual(uploaded_content, written_content) self.assertEqual(uploaded_content, written_content)
multipart.complete.assert_called_once_with( multipart.complete.assert_called_once_with(
MultipartUpload={ 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") self.storage._get_or_create_bucket("testbucketname")
Bucket.create.assert_called_once_with( 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): def test_auto_creating_bucket_with_acl(self):
@ -409,22 +427,28 @@ class S3Boto3StorageTests(S3Boto3TestCase):
) )
self.storage._get_or_create_bucket("testbucketname") self.storage._get_or_create_bucket("testbucketname")
Bucket.create.assert_called_once_with( 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): def test_storage_exists(self):
self.assertTrue(self.storage.exists("file.txt")) self.assertTrue(self.storage.exists("file.txt"))
self.storage.connection.meta.client.head_object.assert_called_with( 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): def test_storage_exists_false(self):
self.storage.connection.meta.client.head_object.side_effect = ClientError( 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.assertFalse(self.storage.exists("file.txt"))
self.storage.connection.meta.client.head_object.assert_called_with( 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): def test_storage_exists_doesnt_create_bucket(self):
@ -445,8 +469,14 @@ class S3Boto3StorageTests(S3Boto3TestCase):
# 4.txt # 4.txt
pages = [ pages = [
{ {
"CommonPrefixes": [{"Prefix": "some"}, {"Prefix": "other"},], "CommonPrefixes": [
"Contents": [{"Key": "2.txt"}, {"Key": "4.txt"},], {"Prefix": "some"},
{"Prefix": "other"},
],
"Contents": [
{"Key": "2.txt"},
{"Key": "4.txt"},
],
}, },
] ]
@ -465,7 +495,14 @@ class S3Boto3StorageTests(S3Boto3TestCase):
# some/path/1.txt # some/path/1.txt
# some/2.txt # some/2.txt
pages = [ pages = [
{"CommonPrefixes": [{"Prefix": "some/path"},], "Contents": [{"Key": "some/2.txt"},],}, {
"CommonPrefixes": [
{"Prefix": "some/path"},
],
"Contents": [
{"Key": "some/2.txt"},
],
},
] ]
paginator = mock.MagicMock() paginator = mock.MagicMock()

View file

@ -4,7 +4,7 @@ Building menu tests.
""" """
from evennia.commands.default.tests import BaseEvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from . building_menu import BuildingMenu, CmdNoMatch from .building_menu import BuildingMenu, CmdNoMatch
class Submenu(BuildingMenu): 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: if days <= 0 or weeks <= 0 or months <= 0:
raise ValueError("realtime_to_gametime: days/weeks/months cannot be set <= 0, " raise ValueError(
"they start from 1.") "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 start from 1, we need to adjust them to work mathematically.
days, weeks, months = days - 1, weeks - 1, months - 1 days, weeks, months = days - 1, weeks - 1, months - 1

View file

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

View file

@ -11,7 +11,11 @@ from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
from evennia import ScriptDB from evennia import ScriptDB
from evennia.utils.utils import delay, inherits_from, lazy_property 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.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 help
CHARACTER_CAN_DELETE = """ CHARACTER_CAN_DELETE = """

View file

@ -173,7 +173,9 @@ def time_event(obj, event_name, number, parameters):
""" """
seconds, usual, key = get_next_wait(parameters) seconds, usual, key = get_next_wait(parameters)
script = create_script( 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.key = key
script.desc = "event on {}".format(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) _ACCOUNT = class_from_module(settings.BASE_ACCOUNT_TYPECLASS)
_GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS) _GUEST = class_from_module(settings.BASE_GUEST_TYPECLASS)
_ACCOUNT_HELP = ( _ACCOUNT_HELP = "Enter a new or existing login name."
"Enter a new or existing login name.")
_PASSWORD_HELP = ( _PASSWORD_HELP = (
"Password should be a minimum of 8 characters (preferably longer) and " "Password should be a minimum of 8 characters (preferably longer) and "
"can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only." "can contain a mix of letters, spaces, digits and @/./+/-/_/'/, only."

View file

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

View file

@ -343,7 +343,7 @@ class EvscaperoomObject(DefaultObject):
# Evennia hooks # Evennia hooks
def return_appearance(self, looker, **kwargs): 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. contents of the object by default.
""" """

View file

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

View file

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

View file

@ -22,7 +22,9 @@ class TestGenderSub(BaseEvenniaCommandTest):
self.assertEqual( self.assertEqual(
gendersub._RE_GENDER_PRONOUN.sub(char._get_pronoun, txt), "Test their gender" 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.db.gender = "female"
char.msg("Test |p gender") char.msg("Test |p gender")
mock_msg.assert_called_with("Test her gender", from_obj=None, session=None) 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 A character able to participate in turn-based combat. Has attributes for current
and maximum HP, and access to combat commands. and maximum HP, and access to combat commands.
""" """
rules = COMBAT_RULES rules = COMBAT_RULES
def at_object_creation(self): 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 Fights persist until only one participant is left with any HP or all
remaining participants choose to end the combat with the 'disengage' command. remaining participants choose to end the combat with the 'disengage' command.
""" """
rules = COMBAT_RULES rules = COMBAT_RULES
@ -258,6 +259,7 @@ class TBEWeapon(DefaultObject):
A weapon which can be wielded in combat with the 'wield' command. A weapon which can be wielded in combat with the 'wield' command.
""" """
rules = COMBAT_RULES rules = COMBAT_RULES
def at_object_creation(self): def at_object_creation(self):
@ -513,8 +515,9 @@ class CmdWield(Command):
weapon = self.caller.search(self.args, candidates=self.caller.contents) weapon = self.caller.search(self.args, candidates=self.caller.contents)
if not weapon: if not weapon:
return return
if not weapon.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", if not weapon.is_typeclass(
exact=True): "evennia.contrib.game_systems.turnbattle.tb_equip.TBEWeapon", exact=True
):
self.caller.msg("That's not a weapon!") self.caller.msg("That's not a weapon!")
# Remember to update the path to the weapon typeclass if you move this module! # Remember to update the path to the weapon typeclass if you move this module!
return return
@ -597,8 +600,9 @@ class CmdDon(Command):
armor = self.caller.search(self.args, candidates=self.caller.contents) armor = self.caller.search(self.args, candidates=self.caller.contents)
if not armor: if not armor:
return return
if not armor.is_typeclass("evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", if not armor.is_typeclass(
exact=True): "evennia.contrib.game_systems.turnbattle.tb_equip.TBEArmor", exact=True
):
self.caller.msg("That's not armor!") self.caller.msg("That's not armor!")
# Remember to update the path to the armor typeclass if you move this module! # Remember to update the path to the armor typeclass if you move this module!
return return

View file

@ -104,7 +104,6 @@ COMBAT FUNCTIONS START HERE
class ItemCombatRules(tb_basic.BasicCombatRules): class ItemCombatRules(tb_basic.BasicCombatRules):
def get_attack(self, attacker, defender): def get_attack(self, attacker, defender):
""" """
Returns a value for an attack roll. Returns a value for an attack roll.
@ -699,7 +698,7 @@ AMULET_OF_MIGHT = {
AMULET_OF_WEAKNESS = { AMULET_OF_WEAKNESS = {
"key": "The Amulet of Weakness", "key": "The Amulet of Weakness",
"desc": "The one who holds this amulet can call upon its power to gain great 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_func": "add_condition",
"item_selfonly": True, "item_selfonly": True,
"item_kwargs": {"conditions": [("Damage Down", 3), ("Accuracy Down", 3), ("Defense Down", 3)]}, "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. 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): def spell_healing(self, caster, spell_name, targets, cost, **kwargs):
""" """
Spell that restores HP to a target or targets. Spell that restores HP to a target or targets.
@ -325,11 +325,7 @@ SPELLS = {
"attack_name": ("A jet of flame", "jets of flame"), "attack_name": ("A jet of flame", "jets of flame"),
"damage_range": (25, 35), "damage_range": (25, 35),
}, },
"cure wounds": { "cure wounds": {"spellfunc": COMBAT_RULES.spell_healing, "target": "anychar", "cost": 5},
"spellfunc": COMBAT_RULES.spell_healing,
"target": "anychar",
"cost": 5
},
"mass cure wounds": { "mass cure wounds": {
"spellfunc": COMBAT_RULES.spell_healing, "spellfunc": COMBAT_RULES.spell_healing,
"target": "anychar", "target": "anychar",
@ -376,6 +372,7 @@ class TBMagicCharacter(tb_basic.TBBasicCharacter):
and maximum HP, access to combat commands and magic. and maximum HP, access to combat commands and magic.
""" """
rules = COMBAT_RULES rules = COMBAT_RULES
def at_object_creation(self): def at_object_creation(self):

View file

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

View file

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

View file

@ -2,4 +2,3 @@
Contribs related to moving in and manipulating the game world and grid. 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 # A map with a temple (▲) amongst mountains (n,∩) in a forest (♣,♠) on an
# island surrounded by water (≈). By giving no instructions for the water # island surrounded by water (≈). By giving no instructions for the water
# characters we effectively skip it and create no rooms for those squares. # characters we effectively skip it and create no rooms for those squares.
EXAMPLE1_MAP = '''\ EXAMPLE1_MAP = """\
n n
n n
''' """
def example1_build_forest(x, y, **kwargs): def example1_build_forest(x, y, **kwargs):
'''A basic example of build instructions. Make sure to include **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.''' in the arguments and return an instance of the room for exit generation."""
# Create a room and provide a basic description. # Create a room and provide a basic description.
room = create_object(rooms.Room, key="forest" + str(x) + str(y)) 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): 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. # Create the room.
room = create_object(rooms.Room, key="mountains" + str(x) + str(y)) 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): 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. # Create the room.
room = create_object(rooms.Room, key="temple" + str(x) + str(y)) 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. # 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. # 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): def example2_build_forest(x, y, **kwargs):
'''A basic room''' """A basic room"""
# If on anything other than the first iteration - Do nothing. # If on anything other than the first iteration - Do nothing.
if kwargs["iteration"] > 0: if kwargs["iteration"] > 0:
return None return None
@ -143,7 +144,7 @@ def example2_build_forest(x, y, **kwargs):
def example2_build_verticle_exit(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 on the first iteration - Do nothing.
if kwargs["iteration"] == 0: if kwargs["iteration"] == 0:
return return
@ -164,7 +165,7 @@ def example2_build_verticle_exit(x, y, **kwargs):
def example2_build_horizontal_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 on the first iteration - Do nothing.
if kwargs["iteration"] == 0: if kwargs["iteration"] == 0:
return return

View file

@ -5,7 +5,7 @@ Wilderness contrib - titeuf87, 2017
from .wilderness import create_wilderness # noqa from .wilderness import create_wilderness # noqa
from .wilderness import enter_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 WildernessScript # noqa
from .wilderness import WildernessExit # noqa from .wilderness import WildernessExit # noqa
from .wilderness import WildernessRoom # 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. are given, the target is a location on the XYZGrid.
""" """
def _search_by_xyz(self, inp): def _search_by_xyz(self, inp):
inp = inp.strip("()") inp = inp.strip("()")
X, Y, *Z = inp.split(",", 2) X, Y, *Z = inp.split(",", 2)
@ -69,8 +70,10 @@ class CmdXYZTeleport(building.CmdTeleport):
try: try:
xyz = self.caller.xyz xyz = self.caller.xyz
except AttributeError: except AttributeError:
self.caller.msg("Z-coordinate is also required since you are not currently " self.caller.msg(
"in a room with a Z coordinate of its own.") "Z-coordinate is also required since you are not currently "
"in a room with a Z coordinate of its own."
)
raise InterruptCommand raise InterruptCommand
else: else:
Z = xyz[2] Z = xyz[2]
@ -134,9 +137,11 @@ class CmdXYZOpen(building.CmdOpen):
self.location = self.caller.location self.location = self.caller.location
if not self.args or not self.rhs: if not self.args or not self.rhs:
self.caller.msg("Usage: open <new exit>[;alias...][:typeclass]" self.caller.msg(
"[,<return exit>[;alias..][:typeclass]]] " "Usage: open <new exit>[;alias...][:typeclass]"
"= <destination or (X,Y,Z)>") "[,<return exit>[;alias..][:typeclass]]] "
"= <destination or (X,Y,Z)>"
)
raise InterruptCommand raise InterruptCommand
if not self.location: if not self.location:
self.caller.msg("You cannot create an exit from a None-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. Builders can optionally specify a specific grid coordinate (X,Y) to go to.
""" """
key = "goto" key = "goto"
aliases = "path" aliases = "path"
help_category = "General" help_category = "General"
@ -207,11 +213,19 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
def _search_by_key_and_alias(self, inp, xyz_start): def _search_by_key_and_alias(self, inp, xyz_start):
Z = xyz_start[2] 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) return self.caller.search(inp, candidates=candidates)
def _auto_step(self, caller, session, target=None, def _auto_step(
xymap=None, directions=None, step_sequence=None, step=True): self,
caller,
session,
target=None,
xymap=None,
directions=None,
step_sequence=None,
step=True,
):
path_data = caller.ndb.xy_path_data path_data = caller.ndb.xy_path_data
@ -221,8 +235,12 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
# stop any old task in its tracks # stop any old task in its tracks
path_data.task.cancel() path_data.task.cancel()
path_data = caller.ndb.xy_path_data = PathData( path_data = caller.ndb.xy_path_data = PathData(
target=target, xymap=xymap, directions=directions, target=target,
step_sequence=step_sequence, task=None) xymap=xymap,
directions=directions,
step_sequence=step_sequence,
task=None,
)
if step and path_data: if step and path_data:
@ -285,7 +303,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xymap=path_data.xymap, xymap=path_data.xymap,
directions=directions, directions=directions,
step_sequence=step_sequence, step_sequence=step_sequence,
task=None task=None,
) )
# the map can itself tell the stepper to stop the auto-step prematurely # the map can itself tell the stepper to stop the auto-step prematurely
interrupt_node_or_link = None 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! # the exit name does not need to be the same as the cardinal direction!
exit_name, *_ = first_link.spawn_aliases.get( 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) exit_obj = caller.search(exit_name)
if not exit_obj: 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 # premature stop of pathfind-step because of map node/link of interrupt type
if hasattr(interrupt_node_or_link, "node_index"): if hasattr(interrupt_node_or_link, "node_index"):
message = exit_obj.destination.attributes.get( 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 # we move into the node/room and then stop
caller.execute_cmd(exit_name, session=session) caller.execute_cmd(exit_name, session=session)
else: else:
# if the link is interrupted we don't cross it at all # if the link is interrupted we don't cross it at all
message = exit_obj.attributes.get( 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) caller.msg(message)
return return
@ -335,7 +356,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xymap=path_data.xymap, xymap=path_data.xymap,
directions=path_data.directions, directions=path_data.directions,
step_sequence=path_data.step_sequence, 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): def func(self):
@ -344,7 +365,7 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
""" """
caller = self.caller caller = self.caller
goto_mode = self.cmdname == 'goto' goto_mode = self.cmdname == "goto"
# check if we have an existing path # check if we have an existing path
path_data = caller.ndb.xy_path_data 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}.") caller.msg(f"Aborted auto-walking to {target_name}.")
return return
# goto/path-command will show current path # goto/path-command will show current path
current_path = list_to_string( current_path = list_to_string([f"|w{step}|n" for step in path_data.directions])
[f"|w{step}|n" for step in path_data.directions])
moving = "(moving)" if task and task.active() else "" moving = "(moving)" if task and task.active() else ""
caller.msg(f"Path to {target_name}{moving}: {current_path}") caller.msg(f"Path to {target_name}{moving}: {current_path}")
else: else:
@ -405,12 +425,21 @@ class CmdGoto(COMMAND_DEFAULT_CLASS):
xy_end = xyz_end[:2] xy_end = xyz_end[:2]
directions, step_sequence = xymap.get_shortest_path(xy_start, xy_end) 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)}: " caller.msg(
f"|w{list_to_string(directions, endsep='|n, and finally|w')}|n") 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 # create data for display and start stepping if we used goto
self._auto_step(caller, self.session, target=target, xymap=xymap, self._auto_step(
directions=directions, step_sequence=step_sequence, step=goto_mode) caller,
self.session,
target=target,
xymap=xymap,
directions=directions,
step_sequence=step_sequence,
step=goto_mode,
)
class CmdMap(COMMAND_DEFAULT_CLASS): class CmdMap(COMMAND_DEFAULT_CLASS):
@ -424,6 +453,7 @@ class CmdMap(COMMAND_DEFAULT_CLASS):
This is a builder-command. This is a builder-command.
""" """
key = "map" key = "map"
locks = "cmd:perm(Builders)" locks = "cmd:perm(Builders)"
@ -453,8 +483,10 @@ class CmdMap(COMMAND_DEFAULT_CLASS):
xymap = xyzgrid.get_map(Z) xymap = xyzgrid.get_map(Z)
if not xymap: if not xymap:
self.caller.msg(f"XYMap '{Z}' is not found on the grid. Try 'map list' to see " self.caller.msg(
"available maps/Zcoords.") f"XYMap '{Z}' is not found on the grid. Try 'map list' to see "
"available maps/Zcoords."
)
return return
self.caller.msg(ansi.raw(xymap.mapstring)) 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. Cmdset for easily adding the above cmds to the character cmdset.
""" """
key = "xyzgrid_cmdset" key = "xyzgrid_cmdset"
def at_cmdset_creation(self): 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. 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 # extends the default legend
LEGEND_MAP1 = { LEGEND_MAP1 = {"T": TransitionToCave}
'T': TransitionToCave
}
# link coordinates to rooms # link coordinates to rooms
@ -96,70 +95,62 @@ PROTOTYPES_MAP1 = {
# node/room prototypes # node/room prototypes
(3, 0): { (3, 0): {
"key": "Dungeon Entrance", "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): { (4, 1): {
"key": "Under the foilage of a giant tree", "key": "Under the foilage of a giant tree",
"desc": "High above the branches of a giant tree blocks out the sunlight. A slide " "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): { (4, 4): {
"key": "The slide", "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): { (6, 1): {
"key": "Thorny path", "key": "Thorny path",
"desc": "To the east is a pathway of thorns. If you get through, you don't think you'll be " "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." "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."
}, },
(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): { (3, 7): {
"key": "Dense foilage", "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): { (5, 6): {
"key": "On a huge branch", "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): { (9, 7): {
"key": "On an enormous branch", "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): { (10, 8): {
"key": "A gorgeous view", "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 # default rooms
('*', '*'): { ("*", "*"): {
"key": "Among the branches of a giant tree", "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 # directional prototypes
(3, 0, 'e'): { (3, 0, "e"): {"desc": "A dark passage into the underworld."},
"desc": "A dark passage into the underworld."
},
} }
for key, prot in PROTOTYPES_MAP1.items(): for key, prot in PROTOTYPES_MAP1.items():
if len(key) == 2: if len(key) == 2:
# we don't want to give exits the room typeclass! # we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT prot["prototype_parent"] = ROOM_PARENT
else: else:
prot['prototype_parent'] = EXIT_PARENT prot["prototype_parent"] = EXIT_PARENT
XYMAP_DATA_MAP1 = { XYMAP_DATA_MAP1 = {
"zcoord": "the large tree", "zcoord": "the large tree",
"map": MAP1, "map": MAP1,
"legend": LEGEND_MAP1, "legend": LEGEND_MAP1,
"prototypes": PROTOTYPES_MAP1 "prototypes": PROTOTYPES_MAP1,
} }
# -------------------------------------- map2 # -------------------------------------- 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. 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) # this extends the default legend (that defines #,-+ etc)
LEGEND_MAP2 = { LEGEND_MAP2 = {"T": TransitionToLargeTree}
"T": TransitionToLargeTree
}
# prototypes for specific locations # prototypes for specific locations
PROTOTYPES_MAP2 = { PROTOTYPES_MAP2 = {
@ -203,64 +193,54 @@ PROTOTYPES_MAP2 = {
(1, 0): { (1, 0): {
"key": "The entrance", "key": "The entrance",
"desc": "This is the entrance to a small cave leading into the ground. " "desc": "This is the entrance to a small cave leading into the ground. "
"Light sifts in from the outside, while cavernous passages disappear " "Light sifts in from the outside, while cavernous passages disappear "
"into darkness." "into darkness.",
}, },
(2, 0): { (2, 0): {
"key": "A gruesome sight.", "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): { (1, 1): {
"key": "A dark pathway", "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): { (3, 2): {
"key": "Stagnant water", "key": "Stagnant water",
"desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest " "desc": "A pool of stagnant, black water dominates this small chamber. To the nortwest "
"a faint light can be seen." "a faint light can be seen.",
},
(0, 2): {
"key": "A dark alcove",
"desc": "This alcove is empty."
}, },
(0, 2): {"key": "A dark alcove", "desc": "This alcove is empty."},
(1, 2): { (1, 2): {
"key": "South-west corner of the atrium", "key": "South-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout " "desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones." "between the stones.",
}, },
(2, 2): { (2, 2): {
"key": "South-east corner of the atrium", "key": "South-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout " "desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones." "between the stones.",
}, },
(1, 3): { (1, 3): {
"key": "North-west corner of the atrium", "key": "North-west corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout " "desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout "
"between the stones." "between the stones.",
}, },
(2, 3): { (2, 3): {
"key": "North-east corner of the atrium", "key": "North-east corner of the atrium",
"desc": "Sunlight sifts down into a large underground chamber. Weeds and grass sprout " "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): { (3, 3): {
"key": "Craggy crevice", "key": "Craggy crevice",
"desc": "This is the deepest part of the dungeon. The path shrinks away and there " "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 # 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 # directional prototypes
(1, 0, 'w'): { (1, 0, "w"): {"desc": "A narrow path to the fresh air of the outside world."},
"desc": "A narrow path to the fresh air of the outside world."
},
# directional fallbacks for unset directions # 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 # 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(): for key, prot in PROTOTYPES_MAP2.items():
if len(key) == 2: if len(key) == 2:
# we don't want to give exits the room typeclass! # we don't want to give exits the room typeclass!
prot['prototype_parent'] = ROOM_PARENT prot["prototype_parent"] = ROOM_PARENT
else: else:
prot['prototype_parent'] = EXIT_PARENT prot["prototype_parent"] = EXIT_PARENT
XYMAP_DATA_MAP2 = { XYMAP_DATA_MAP2 = {
@ -278,14 +258,8 @@ XYMAP_DATA_MAP2 = {
"zcoord": "the small cave", "zcoord": "the small cave",
"legend": LEGEND_MAP2, "legend": LEGEND_MAP2,
"prototypes": PROTOTYPES_MAP2, "prototypes": PROTOTYPES_MAP2,
"options": { "options": {"map_visual_range": 1, "map_mode": "scan"},
"map_visual_range": 1,
"map_mode": 'scan'
}
} }
# This is read by the parser # This is read by the parser
XYMAP_DATA_LIST = [ XYMAP_DATA_LIST = [XYMAP_DATA_MAP1, XYMAP_DATA_MAP2]
XYMAP_DATA_MAP1,
XYMAP_DATA_MAP2
]

View file

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

View file

@ -27,19 +27,19 @@ except AttributeError:
exit_override = {} exit_override = {}
room_prototype = { room_prototype = {
'prototype_key': 'xyz_room', "prototype_key": "xyz_room",
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom', "typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom",
'prototype_tags': ("xyzroom", ), "prototype_tags": ("xyzroom",),
'key': "A room", "key": "A room",
'desc': "An empty room." "desc": "An empty room.",
} }
room_prototype.update(room_override) room_prototype.update(room_override)
exit_prototype = { exit_prototype = {
'prototype_key': 'xyz_exit', "prototype_key": "xyz_exit",
'typeclass': 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit', "typeclass": "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit",
'prototype_tags': ("xyzexit", ), "prototype_tags": ("xyzexit",),
'desc': "An exit." "desc": "An exit.",
} }
exit_prototype.update(exit_override) 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 # errors for Map system
class MapError(RuntimeError):
class MapError(RuntimeError):
def __init__(self, error="", node_or_link=None): def __init__(self, error="", node_or_link=None):
prefix = "" prefix = ""
if node_or_link: if node_or_link:
prefix = (f"{node_or_link.__class__.__name__} '{node_or_link.symbol}' " prefix = (
f"at XYZ=({node_or_link.X:g},{node_or_link.Y:g},{node_or_link.Z}) ") 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.node_or_link = node_or_link
self.message = f"{prefix}{error}" self.message = f"{prefix}{error}"
super().__init__(self.message) super().__init__(self.message)
@ -52,4 +54,5 @@ class MapTransition(RuntimeWarning):
leads to another map. leads to another map.
""" """
pass pass

View file

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

View file

@ -14,7 +14,8 @@ try:
except ImportError as err: except ImportError as err:
raise ImportError( raise ImportError(
f"{err}\nThe XYZgrid contrib requires " f"{err}\nThe XYZgrid contrib requires "
"the SciPy package. Install with `pip install scipy'.") "the SciPy package. Install with `pip install scipy'."
)
import uuid import uuid
from collections import defaultdict from collections import defaultdict
@ -33,6 +34,7 @@ UUID_XYZ_NAMESPACE = uuid.uuid5(uuid.UUID(int=0), "xyzgrid")
# Nodes/Links # Nodes/Links
class MapNode: class MapNode:
""" """
This represents a 'room' node on the map. Note that the map system deals with two grids, the 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). for various reasons, mostly map-transitions).
""" """
# symbol used to identify this link on the map # symbol used to identify this link on the map
symbol = '#' symbol = "#"
# if printing this node should show another symbol. If set # if printing this node should show another symbol. If set
# to the empty string, use `symbol`. # to the empty string, use `symbol`.
display_symbol = None display_symbol = None
@ -79,16 +82,16 @@ class MapNode:
multilink = True multilink = True
# default values to use if the exit doesn't have a 'spawn_aliases' iterable # default values to use if the exit doesn't have a 'spawn_aliases' iterable
direction_spawn_defaults = { direction_spawn_defaults = {
'n': ('north', 'n'), "n": ("north", "n"),
'ne': ('northeast', 'ne', 'north-east'), "ne": ("northeast", "ne", "north-east"),
'e': ('east', 'e'), "e": ("east", "e"),
'se': ('southeast', 'se', 'south-east'), "se": ("southeast", "se", "south-east"),
's': ('south', 's'), "s": ("south", "s"),
'sw': ('southwest', 'sw', 'south-west'), "sw": ("southwest", "sw", "south-west"),
'w': ('west', 'w'), "w": ("west", "w"),
'nw': ('northwest', 'nw', 'north-west'), "nw": ("northwest", "nw", "north-west"),
'd': ('down', 'd', 'do'), "d": ("down", "d", "do"),
'u': ('up', 'u'), "u": ("up", "u"),
} }
def __init__(self, x, y, Z, node_index=0, symbol=None, xymap=None): 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: if first_step_name in self.closest_neighbor_names:
raise MapParserError( raise MapParserError(
f"has more than one outgoing direction '{first_step_name}'. " 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 self.closest_neighbor_names[first_step_name] = direction
node_index = end_node.node_index node_index = end_node.node_index
@ -215,8 +220,9 @@ class MapNode:
# used for building the shortest path. Note that we store the # used for building the shortest path. Note that we store the
# aliased link directions here, for quick display by the # aliased link directions here, for quick display by the
# shortest-route solver # shortest-route solver
shortest_route = self.shortest_route_to_node.get( shortest_route = self.shortest_route_to_node.get(node_index, ("", [], BIGVAL))[
node_index, ("", [], BIGVAL))[2] 2
]
if weight < shortest_route: if weight < shortest_route:
self.shortest_route_to_node[node_index] = (first_step_name, steps, weight) 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, ...) str or tuple: The key of the spawned exit, or a tuple (key, alias, alias, ...)
""" """
key, *aliases = ( key, *aliases = self.first_links[direction].spawn_aliases.get(
self.first_links[direction] direction, self.direction_spawn_defaults.get(direction, ("unknown",))
.spawn_aliases.get( )
direction, self.direction_spawn_defaults.get(
direction, ('unknown', ))))
if return_aliases: if return_aliases:
return (key, *aliases) return (key, *aliases)
return key return key
@ -313,28 +317,24 @@ class MapNode:
nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz) nodeobj = NodeTypeclass.objects.get_xyz(xyz=xyz)
except django_exceptions.ObjectDoesNotExist: except django_exceptions.ObjectDoesNotExist:
# create a new entity with proper coordinates etc # create a new entity with proper coordinates etc
tclass = self.prototype['typeclass'] tclass = self.prototype["typeclass"]
tclass = (f' ({tclass})' tclass = (
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom' f" ({tclass})" if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZRoom" else ""
else '')
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create(
self.prototype.get('key', 'An empty room'),
xyz=xyz
) )
self.log(f" spawning room at xyz={xyz}{tclass}")
nodeobj, err = NodeTypeclass.create(self.prototype.get("key", "An empty room"), xyz=xyz)
if err: if err:
raise RuntimeError(err) raise RuntimeError(err)
else: else:
self.log(f" updating existing room (if changed) at xyz={xyz}") 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 # 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 # apply prototype to node. This will not override the XYZ tags since
# these are not in the prototype and exact=False # these are not in the prototype and exact=False
spawner.batch_update_objects_with_prototype( spawner.batch_update_objects_with_prototype(self.prototype, objects=[nodeobj], exact=False)
self.prototype, objects=[nodeobj], exact=False)
def spawn_links(self, directions=None): def spawn_links(self, directions=None):
""" """
@ -364,9 +364,9 @@ class MapNode:
for direction, link in self.first_links.items(): for direction, link in self.first_links.items():
key, *aliases = self.get_exit_spawn_name(direction) 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 # 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) maplinks[key.lower()] = (key, aliases, direction, link)
# remove duplicates # remove duplicates
@ -380,8 +380,7 @@ class MapNode:
# we need to search for exits in all directions since some # we need to search for exits in all directions since some
# may have been removed since last sync # may have been removed since last sync
linkobjs = {exi.db_key.lower(): exi linkobjs = {exi.db_key.lower(): exi for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}
for exi in ExitTypeclass.objects.filter_xyz(xyz=xyz)}
# figure out if the topology changed between grid and map (will always # figure out if the topology changed between grid and map (will always
# build all exits first run) # build all exits first run)
@ -411,16 +410,19 @@ class MapNode:
raise RuntimeError(err) raise RuntimeError(err)
linkobjs[key.lower()] = exi linkobjs[key.lower()] = exi
prot = maplinks[key.lower()][3].prototype prot = maplinks[key.lower()][3].prototype
tclass = prot['typeclass'] tclass = prot["typeclass"]
tclass = (f' ({tclass})' tclass = (
if tclass != 'evennia.contrib.grid.xyzgrid.xyzroom.XYZExit' f" ({tclass})"
else '') if tclass != "evennia.contrib.grid.xyzgrid.xyzroom.XYZExit"
else ""
)
self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}") self.log(f" spawning/updating exit xyz={xyz}, direction={key}{tclass}")
# apply prototypes to catch any changes # apply prototypes to catch any changes
for key, linkobj in linkobjs.items(): for key, linkobj in linkobjs.items():
spawner.batch_update_objects_with_prototype( 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): def unspawn(self):
""" """
@ -466,8 +468,9 @@ class TransitionMapNode(MapNode):
actual rooms (`#`) on the other map (NOT to the `T`s)! 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 # X,Y,Z coordinates of target node
taget_map_xyz = (None, None, None) 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). 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')): 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 " raise MapParserError(
"`.target_map_xyz` property. It must point " f"(Z={self.xymap.Z}) has not defined its "
"to another valid xymap (Z coordinate).", self) "`.target_map_xyz` property. It must point "
"to another valid xymap (Z coordinate).",
self,
)
return self.target_map_xyz return self.target_map_xyz
@ -548,6 +554,7 @@ class MapLink:
`node.get_exit_spawn_name(direction)` `node.get_exit_spawn_name(direction)`
""" """
# symbol for identifying this link on the map # symbol for identifying this link on the map
symbol = "" symbol = ""
# if `None`, use .symbol # if `None`, use .symbol
@ -661,7 +668,9 @@ class MapLink:
return None, 0, None return None, 0, None
raise MapParserError( raise MapParserError(
f"was connected to from the direction {start_direction}, but " 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 # note that if `get_direction` returns an unknown direction, this will be equivalent
# to pointing to an empty location, which makes sense # 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) next_target = self.at_empty_target(start_direction, end_direction)
if not next_target: if not next_target:
raise MapParserError( raise MapParserError(f"points to empty space in the direction {end_direction}!", self)
f"points to empty space in the direction {end_direction}!", self)
_weight += self.get_weight(start_direction, _weight) _weight += self.get_weight(start_direction, _weight)
if _steps is None: if _steps is None:
@ -688,13 +696,16 @@ class MapLink:
return ( return (
next_target, next_target,
_weight / max(1, _linklen) if self.average_long_link_weights else _weight, _weight / max(1, _linklen) if self.average_long_link_weights else _weight,
_steps _steps,
) )
else: else:
# we hit another link. Progress recursively. # we hit another link. Progress recursively.
return next_target.traverse( return next_target.traverse(
REVERSE_DIRECTIONS.get(end_direction, end_direction), 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): 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 # there is is something there, we need to check if it is either
# a map node or a link connecting in our direction # a map node or a link connecting in our direction
node_or_link = xygrid[end_x][end_y] node_or_link = xygrid[end_x][end_y]
if (node_or_link.multilink if node_or_link.multilink or node_or_link.get_direction(direction):
or node_or_link.get_direction(direction)):
links[direction] = node_or_link links[direction] = node_or_link
return links return links
@ -845,7 +855,8 @@ class SmartRerouterMapLink(MapLink):
for direction in unhandled_links_copy: for direction in unhandled_links_copy:
if REVERSE_DIRECTIONS[direction] in unhandled_links_copy: if REVERSE_DIRECTIONS[direction] in unhandled_links_copy:
directions[direction] = REVERSE_DIRECTIONS[ 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 # check if we have any non-cross-through paths left to handle
n_unhandled = len(unhandled_links) n_unhandled = len(unhandled_links)
@ -856,7 +867,8 @@ class SmartRerouterMapLink(MapLink):
if n_unhandled != 2: if n_unhandled != 2:
links = ", ".join(unhandled_links) links = ", ".join(unhandled_links)
raise MapParserError( 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[0]] = unhandled_links[1]
directions[unhandled_links[1]] = unhandled_links[0] directions[unhandled_links[1]] = unhandled_links[0]
@ -865,6 +877,7 @@ class SmartRerouterMapLink(MapLink):
return self.directions.get(start_direction) return self.directions.get(start_direction)
class SmartTeleporterMapLink(MapLink): class SmartTeleporterMapLink(MapLink):
""" """
The teleport link works by connecting to nowhere - and will then continue 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. -#-t-# - invalid, only one connected link is allowed.
""" """
symbol = 't'
symbol = "t"
# usually invisible # usually invisible
display_symbol = ' ' display_symbol = " "
direction_name = 'teleport' direction_name = "teleport"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@ -932,7 +946,9 @@ class SmartTeleporterMapLink(MapLink):
if len(found_teleporters) > 1: if len(found_teleporters) > 1:
raise MapParserError( raise MapParserError(
"found too many matching teleporters (must be exactly one more): " "found too many matching teleporters (must be exactly one more): "
f"{found_teleporters}", self) f"{found_teleporters}",
self,
)
other_teleporter = found_teleporters[0] other_teleporter = found_teleporters[0]
# link the two so we don't need to scan again for the other one # 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: if len(neighbors) != 1:
raise MapParserError("must have exactly one link connected to it.", self) raise MapParserError("must have exactly one link connected to it.", self)
direction, link = next(iter(neighbors.items())) direction, link = next(iter(neighbors.items()))
if hasattr(link, 'node_index'): if hasattr(link, "node_index"):
raise MapParserError("can only connect to a Link. Found {link} in " raise MapParserError(
"direction {direction}.", self) "can only connect to a Link. Found {link} in " "direction {direction}.", self
)
# the string 'teleport' will not be understood by the traverser, leading to # the string 'teleport' will not be understood by the traverser, leading to
# this being interpreted as an empty target and the `at_empty_target` # this being interpreted as an empty target and the `at_empty_target`
# hook firing when trying to traverse this link. # hook firing when trying to traverse this link.
@ -962,12 +979,10 @@ class SmartTeleporterMapLink(MapLink):
if start_direction == direction_name: if start_direction == direction_name:
# called while traversing another teleport # called while traversing another teleport
# - we must make sure we can always access/leave the teleport. # - we must make sure we can always access/leave the teleport.
self.directions = {direction_name: direction, self.directions = {direction_name: direction, direction: direction_name}
direction: direction_name}
else: else:
# called while traversing a normal link # called while traversing a normal link
self.directions = {start_direction: direction_name, self.directions = {start_direction: direction_name, direction_name: direction}
direction_name: direction}
return self.directions.get(start_direction) return self.directions.get(start_direction)
@ -1016,6 +1031,7 @@ class SmartMapLink(MapLink):
# | # |
""" """
multilink = True multilink = True
def get_direction(self, start_direction): def get_direction(self, start_direction):
@ -1027,8 +1043,11 @@ class SmartMapLink(MapLink):
if not self.directions: if not self.directions:
directions = {} directions = {}
neighbors = self.get_linked_neighbors() neighbors = self.get_linked_neighbors()
nodes = [direction for direction, neighbor in neighbors.items() nodes = [
if hasattr(neighbor, 'node_index')] direction
for direction, neighbor in neighbors.items()
if hasattr(neighbor, "node_index")
]
if len(nodes) == 2: if len(nodes) == 2:
# prefer link to these two nodes # prefer link to these two nodes
@ -1042,7 +1061,9 @@ class SmartMapLink(MapLink):
"must have exactly two connections - either directly to " "must have exactly two connections - either directly to "
"two nodes or connecting directly to one node and with exactly one other " "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 " 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 self.directions = directions
return self.directions.get(start_direction) return self.directions.get(start_direction)
@ -1071,20 +1092,26 @@ class InvisibleSmartMapLink(SmartMapLink):
# this allows for normal movement directions even if the invisible-node # this allows for normal movement directions even if the invisible-node
# is marked with a different symbol. # is marked with a different symbol.
direction_aliases = { direction_aliases = {
'n': 'n', 'ne': 'ne', 'e': 'e', 'se': 'se', "n": "n",
's': 's', 'sw': 'sw', 'w': 'w', 'nw': 'nw' "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 # replace current link position with what the smart links "should" look like
display_symbol_aliases = { display_symbol_aliases = {
(('n', 's'), ('s', 'n')): '|', (("n", "s"), ("s", "n")): "|",
(('n', 's'),): 'v', (("n", "s"),): "v",
(('s', 'n')): '^', (("s", "n")): "^",
(('e', 'w'), ('w', 'e')): '-', (("e", "w"), ("w", "e")): "-",
(('e', 'w'),): '>', (("e", "w"),): ">",
(('w', 'e'),): '<', (("w", "e"),): "<",
(('nw', 'se'), ('sw', 'ne')): '\\', (("nw", "se"), ("sw", "ne")): "\\",
(('ne', 'sw'), ('sw', 'ne')): '/', (("ne", "sw"), ("sw", "ne")): "/",
} }
def get_display_symbol(self): def get_display_symbol(self):
@ -1098,12 +1125,10 @@ class InvisibleSmartMapLink(SmartMapLink):
""" """
if not hasattr(self, "_cached_display_symbol"): if not hasattr(self, "_cached_display_symbol"):
legend = self.xymap.legend legend = self.xymap.legend
default_symbol = ( default_symbol = self.symbol if self.display_symbol is None else self.display_symbol
self.symbol if self.display_symbol is None else self.display_symbol)
self._cached_display_symbol = default_symbol self._cached_display_symbol = default_symbol
dirtuple = tuple((key, self.directions[key]) dirtuple = tuple((key, self.directions[key]) for key in sorted(self.directions.keys()))
for key in sorted(self.directions.keys()))
replacement_symbol = self.display_symbol_aliases.get(dirtuple, default_symbol) replacement_symbol = self.display_symbol_aliases.get(dirtuple, default_symbol)
@ -1112,16 +1137,19 @@ class InvisibleSmartMapLink(SmartMapLink):
if node_or_link_class: if node_or_link_class:
# initiate class in the current location and run get_display_symbol # initiate class in the current location and run get_display_symbol
# to get what it would show. # to get what it would show.
self._cached_display_symbol = ( self._cached_display_symbol = node_or_link_class(
node_or_link_class(self.x, self.y, self.Z).get_display_symbol()) self.x, self.y, self.Z
).get_display_symbol()
return self._cached_display_symbol return self._cached_display_symbol
# ---------------------------------- # ----------------------------------
# Default nodes and link classes # Default nodes and link classes
class BasicMapNode(MapNode): class BasicMapNode(MapNode):
"""A map node/room""" """A map node/room"""
symbol = "#" symbol = "#"
prototype = "xyz_room" prototype = "xyz_room"
@ -1129,20 +1157,25 @@ class BasicMapNode(MapNode):
class InterruptMapNode(MapNode): class InterruptMapNode(MapNode):
"""A point of interest node/room. Pathfinder will ignore but auto-stepper will """A point of interest node/room. Pathfinder will ignore but auto-stepper will
stop here if passing through. Beginner-Tutorial from here is fine.""" stop here if passing through. Beginner-Tutorial from here is fine."""
symbol = "I" symbol = "I"
display_symbol = "#" display_symbol = "#"
interrupt_path = True interrupt_path = True
prototype = "xyz_room" prototype = "xyz_room"
class MapTransitionNode(TransitionMapNode): class MapTransitionNode(TransitionMapNode):
"""Transition-target node to other map. This is not actually spawned in-game.""" """Transition-target node to other map. This is not actually spawned in-game."""
symbol = "T" symbol = "T"
display_symbol = " " display_symbol = " "
prototype = None # important to leave None! prototype = None # important to leave None!
target_map_xyz = (None, None, None) # must be set manually target_map_xyz = (None, None, None) # must be set manually
class NSMapLink(MapLink): class NSMapLink(MapLink):
"""Two-way, North-South link""" """Two-way, North-South link"""
symbol = "|" symbol = "|"
display_symbol = "||" display_symbol = "||"
directions = {"n": "s", "s": "n"} directions = {"n": "s", "s": "n"}
@ -1151,6 +1184,7 @@ class NSMapLink(MapLink):
class EWMapLink(MapLink): class EWMapLink(MapLink):
"""Two-way, East-West link""" """Two-way, East-West link"""
symbol = "-" symbol = "-"
directions = {"e": "w", "w": "e"} directions = {"e": "w", "w": "e"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1158,6 +1192,7 @@ class EWMapLink(MapLink):
class NESWMapLink(MapLink): class NESWMapLink(MapLink):
"""Two-way, NorthWest-SouthWest link""" """Two-way, NorthWest-SouthWest link"""
symbol = "/" symbol = "/"
directions = {"ne": "sw", "sw": "ne"} directions = {"ne": "sw", "sw": "ne"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1165,6 +1200,7 @@ class NESWMapLink(MapLink):
class SENWMapLink(MapLink): class SENWMapLink(MapLink):
"""Two-way, SouthEast-NorthWest link""" """Two-way, SouthEast-NorthWest link"""
symbol = "\\" symbol = "\\"
directions = {"se": "nw", "nw": "se"} directions = {"se": "nw", "nw": "se"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1172,22 +1208,23 @@ class SENWMapLink(MapLink):
class PlusMapLink(MapLink): class PlusMapLink(MapLink):
"""Two-way, crossing North-South and East-West links""" """Two-way, crossing North-South and East-West links"""
symbol = "+" symbol = "+"
directions = {"s": "n", "n": "s", directions = {"s": "n", "n": "s", "e": "w", "w": "e"}
"e": "w", "w": "e"}
prototype = "xyz_exit" prototype = "xyz_exit"
class CrossMapLink(MapLink): class CrossMapLink(MapLink):
"""Two-way, crossing NorthEast-SouthWest and SouthEast-NorthWest links""" """Two-way, crossing NorthEast-SouthWest and SouthEast-NorthWest links"""
symbol = "x" symbol = "x"
directions = {"ne": "sw", "sw": "ne", directions = {"ne": "sw", "sw": "ne", "se": "nw", "nw": "se"}
"se": "nw", "nw": "se"}
prototype = "xyz_exit" prototype = "xyz_exit"
class NSOneWayMapLink(MapLink): class NSOneWayMapLink(MapLink):
"""One-way North-South link""" """One-way North-South link"""
symbol = "v" symbol = "v"
directions = {"n": "s"} directions = {"n": "s"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1195,6 +1232,7 @@ class NSOneWayMapLink(MapLink):
class SNOneWayMapLink(MapLink): class SNOneWayMapLink(MapLink):
"""One-way South-North link""" """One-way South-North link"""
symbol = "^" symbol = "^"
directions = {"s": "n"} directions = {"s": "n"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1202,6 +1240,7 @@ class SNOneWayMapLink(MapLink):
class EWOneWayMapLink(MapLink): class EWOneWayMapLink(MapLink):
"""One-way East-West link""" """One-way East-West link"""
symbol = "<" symbol = "<"
directions = {"e": "w"} directions = {"e": "w"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1209,6 +1248,7 @@ class EWOneWayMapLink(MapLink):
class WEOneWayMapLink(MapLink): class WEOneWayMapLink(MapLink):
"""One-way West-East link""" """One-way West-East link"""
symbol = ">" symbol = ">"
directions = {"w": "e"} directions = {"w": "e"}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1216,21 +1256,39 @@ class WEOneWayMapLink(MapLink):
class UpMapLink(SmartMapLink): class UpMapLink(SmartMapLink):
"""Up direction. Note that this stays on the same z-coord so it's a 'fake' up.""" """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. # 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, direction_aliases = {
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} "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} spawn_aliases = {direction: ("up", "u") for direction in direction_aliases}
prototype = "xyz_exit" prototype = "xyz_exit"
class DownMapLink(UpMapLink): class DownMapLink(UpMapLink):
"""Down direction. Note that this stays on the same z-coord, so it's a 'fake' down.""" """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. # 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, direction_aliases = {
's': symbol, 'sw': symbol, 'w': symbol, 'nw': symbol} "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} spawn_aliases = {direction: ("down", "d") for direction in direction_aliases}
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1238,6 +1296,7 @@ class DownMapLink(UpMapLink):
class InterruptMapLink(InvisibleSmartMapLink): class InterruptMapLink(InvisibleSmartMapLink):
"""A (still passable) link. Pathfinder will treat this as any link, but auto-stepper """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.""" will always abort before crossing this link - so this must be crossed manually."""
symbol = "i" symbol = "i"
interrupt_path = True interrupt_path = True
prototype = "xyz_exit" prototype = "xyz_exit"
@ -1250,14 +1309,24 @@ class BlockedMapLink(InvisibleSmartMapLink):
link in any paths. link in any paths.
""" """
symbol = 'b'
weights = {'n': BIGVAL, 'ne': BIGVAL, 'e': BIGVAL, 'se': BIGVAL, symbol = "b"
's': BIGVAL, 'sw': BIGVAL, 'w': BIGVAL, 'nw': BIGVAL} weights = {
"n": BIGVAL,
"ne": BIGVAL,
"e": BIGVAL,
"se": BIGVAL,
"s": BIGVAL,
"sw": BIGVAL,
"w": BIGVAL,
"nw": BIGVAL,
}
prototype = "xyz_exit" prototype = "xyz_exit"
class RouterMapLink(SmartRerouterMapLink): class RouterMapLink(SmartRerouterMapLink):
"""A link that connects other links to build 'knees', pass-throughs etc.""" """A link that connects other links to build 'knees', pass-throughs etc."""
symbol = "o" 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 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. one-way link out of the teleporter on one side.
""" """
symbol = 't'
symbol = "t"
# all map components; used as base if not overridden # all map components; used as base if not overridden
@ -1291,5 +1361,5 @@ LEGEND = {
"d": DownMapLink, "d": DownMapLink,
"b": BlockedMapLink, "b": BlockedMapLink,
"i": InterruptMapLink, "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. Main grid class. This organizes the Maps based on their name/Z-coordinate.
""" """
def at_script_creation(self): def at_script_creation(self):
""" """
What we store persistently is data used to create each map (the legends, names etc) 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) 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. Get one or more exit object at coordinate.
@ -102,7 +103,7 @@ class XYZGrid(DefaultScript):
Queryset: A queryset of XYZExit(s) found. Queryset: A queryset of XYZExit(s) found.
""" """
kwargs['db_key'] = name kwargs["db_key"] = name
return XYZExit.objects.filter_xyz_exit(xyz=xyz, **kwargs) return XYZExit.objects.filter_xyz_exit(xyz=xyz, **kwargs)
def maps_from_module(self, module_path): def maps_from_module(self, module_path):
@ -127,7 +128,7 @@ class XYZGrid(DefaultScript):
if not mapdata: if not mapdata:
self.log(f"Could not find or load map from {module_path}.") self.log(f"Could not find or load map from {module_path}.")
return return
mapdata['module_path'] = module_path mapdata["module_path"] = module_path
return map_data_list return map_data_list
def reload(self): def reload(self):
@ -154,9 +155,9 @@ class XYZGrid(DefaultScript):
# we reload the map from module # we reload the map from module
new_mapdata = loaded_mapdata.get(zcoord) new_mapdata = loaded_mapdata.get(zcoord)
if not new_mapdata: if not new_mapdata:
if 'module_path' in old_mapdata: if "module_path" in old_mapdata:
for mapdata in self.maps_from_module(old_mapdata['module_path']): for mapdata in self.maps_from_module(old_mapdata["module_path"]):
loaded_mapdata[mapdata['zcoord']] = mapdata loaded_mapdata[mapdata["zcoord"]] = mapdata
else: else:
# nowhere to reload from - use what we have # nowhere to reload from - use what we have
loaded_mapdata[zcoord] = old_mapdata loaded_mapdata[zcoord] = old_mapdata
@ -198,7 +199,7 @@ class XYZGrid(DefaultScript):
""" """
for mapdata in mapdatas: for mapdata in mapdatas:
zcoord = mapdata.get('zcoord') zcoord = mapdata.get("zcoord")
if not zcoord: if not zcoord:
raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.") raise RuntimeError("XYZGrid.add_map data must contain 'zcoord'.")
@ -220,7 +221,7 @@ class XYZGrid(DefaultScript):
if remove_objects: if remove_objects:
# we can't batch-delete because we want to run the .delete # we can't batch-delete because we want to run the .delete
# method that also wipes exits and moves content to save locations # 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() xyzroom.delete()
self.reload() self.reload()
@ -234,7 +235,7 @@ class XYZGrid(DefaultScript):
self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True) self.remove_map(*(zcoord for zcoord in self.db.map_data), remove_objects=True)
super().delete() 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 Create/recreate/update the in-game grid based on the stored Maps or for a specific Map
or coordinate. or coordinate.
@ -255,7 +256,7 @@ class XYZGrid(DefaultScript):
""" """
x, y, z = xyz x, y, z = xyz
wildcard = '*' wildcard = "*"
if z == wildcard: if z == wildcard:
xymaps = self.grid xymaps = self.grid
@ -293,8 +294,10 @@ def get_xyzgrid(print_errors=True):
xyzgrid.reload() xyzgrid.reload()
return xyzgrid return xyzgrid
elif len(xyzgrid) > 1: 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] xyzgrid = xyzgrid[0]
try: try:
if not xyzgrid.ndb.loaded: 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. 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 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 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 x, y, z = xyz
wildcard = '*' wildcard = "*"
return ( return (
self self.filter_family(**kwargs)
.filter_family(**kwargs)
.filter( .filter(
Q() if x == wildcard Q()
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)) if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
)
.filter( .filter(
Q() if y == wildcard Q()
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)) if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
)
.filter( .filter(
Q() if z == wildcard Q()
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)) 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. Always return a single matched entity directly. This accepts no `*`-wildcards.
This will also find children of XYZRooms on the given coordinates. 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 # error - mimic default get() behavior but with a little more info
x, y, z = xyz x, y, z = xyz
inp = (f"Query: xyz=({x},{y},{z}), " + inp = f"Query: xyz=({x},{y},{z}), " + ",".join(
",".join(f"{key}={val}" for key, val in kwargs.items())) f"{key}={val}" for key, val in kwargs.items()
)
if ncount > 1: if ncount > 1:
raise self.model.MultipleObjectsReturned(inp) raise self.model.MultipleObjectsReturned(inp)
else: else:
@ -108,8 +115,7 @@ class XYZExitManager(XYZManager):
""" """
def filter_xyz_exit(self, xyz=('*', '*', '*'), def filter_xyz_exit(self, xyz=("*", "*", "*"), xyz_destination=("*", "*", "*"), **kwargs):
xyz_destination=('*', '*', '*'), **kwargs):
""" """
Used by exits (objects with a source and -destination property). 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 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 x, y, z = xyz
xdest, ydest, zdest = xyz_destination xdest, ydest, zdest = xyz_destination
wildcard = '*' wildcard = "*"
return ( return (
self self.filter_family(**kwargs)
.filter_family(**kwargs)
.filter( .filter(
Q() if x == wildcard Q()
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)) if x == wildcard
else Q(db_tags__db_key=str(x), db_tags__db_category=MAP_X_TAG_CATEGORY)
)
.filter( .filter(
Q() if y == wildcard Q()
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)) if y == wildcard
else Q(db_tags__db_key=str(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
)
.filter( .filter(
Q() if z == wildcard Q()
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)) if z == wildcard
else Q(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
)
.filter( .filter(
Q() if xdest == wildcard Q()
else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)) if xdest == wildcard
else Q(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_TAG_CATEGORY)
)
.filter( .filter(
Q() if ydest == wildcard Q()
else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)) if ydest == wildcard
else Q(db_tags__db_key=str(ydest), db_tags__db_category=MAP_YDEST_TAG_CATEGORY)
)
.filter( .filter(
Q() if zdest == wildcard Q()
else Q(db_tags__db_key=str(zdest), db_tags__db_category=MAP_ZDEST_TAG_CATEGORY)) 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 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. exit. All source/destination coordinates (as well as the map's name) are required.
@ -199,8 +216,7 @@ class XYZExitManager(XYZManager):
try: try:
return ( return (
self self.filter(db_tags__db_key=str(z), db_tags__db_category=MAP_Z_TAG_CATEGORY)
.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(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(y), db_tags__db_category=MAP_Y_TAG_CATEGORY)
.filter(db_tags__db_key=str(xdest), db_tags__db_category=MAP_XDEST_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) .get(**kwargs)
) )
except self.model.DoesNotExist: except self.model.DoesNotExist:
inp = (f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," + inp = f"xyz=({x},{y},{z}),xyz_destination=({xdest},{ydest},{zdest})," + ",".join(
",".join(f"{key}={val}" for key, val in kwargs.items())) f"{key}={val}" for key, val in kwargs.items()
raise self.model.DoesNotExist(f"{self.model.__name__} " )
f"matching query {inp} does not exist.") raise self.model.DoesNotExist(
f"{self.model.__name__} " f"matching query {inp} does not exist."
)
class XYZRoom(DefaultRoom): class XYZRoom(DefaultRoom):
@ -244,10 +262,10 @@ class XYZRoom(DefaultRoom):
# default settings for map visualization # default settings for map visualization
map_display = True map_display = True
map_mode = 'nodes' # or 'scan' map_mode = "nodes" # or 'scan'
map_visual_range = 2 map_visual_range = 2
map_character_symbol = "|g@|n" map_character_symbol = "|g@|n"
map_align = 'c' map_align = "c"
map_target_path_style = "|y{display_symbol}|n" map_target_path_style = "|y{display_symbol}|n"
map_fill_all = True map_fill_all = True
map_separator_char = "|x~|n" map_separator_char = "|x~|n"
@ -267,8 +285,10 @@ class XYZRoom(DefaultRoom):
z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False) z = self.tags.get(category=MAP_Z_TAG_CATEGORY, return_list=False)
if x is None or y is None or z is None: if x is None or y is None or z is None:
# don't cache unfinished coordinate (probably tags have not finished saving) # 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 return tuple(
for coord in (x, y, z)) 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) # 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)) 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 return self._xymap
@classmethod @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. Creation method aware of XYZ coordinates.
@ -316,14 +336,18 @@ class XYZRoom(DefaultRoom):
try: try:
x, y, z = xyz x, y, z = xyz
except ValueError: except ValueError:
return None, [f"XYRroom.create got `xyz={xyz}` - needs a valid (X,Y,Z) " return None, [
"coordinate of ints/strings."] 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)) existing_query = cls.objects.filter_xyz(xyz=(x, y, z))
if existing_query.exists(): if existing_query.exists():
existing_room = existing_query.first() existing_room = existing_query.first()
return None, [f"XYRoom XYZ=({x},{y},{z}) already exists " return None, [
f"(existing room is named '{existing_room.db_key}')!"] f"XYRoom XYZ=({x},{y},{z}) already exists "
f"(existing room is named '{existing_room.db_key}')!"
]
tags = ( tags = (
(str(x), MAP_X_TAG_CATEGORY), (str(x), MAP_X_TAG_CATEGORY),
@ -410,26 +434,29 @@ class XYZRoom(DefaultRoom):
xyz = self.xyz xyz = self.xyz
xymap = self.xyzgrid.get_map(xyz[2]) 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. # show the near-area map.
map_character_symbol = kwargs.get( map_character_symbol = kwargs.get(
'map_character_symbol', "map_character_symbol",
xymap.options.get("map_character_symbol", self.map_character_symbol)) xymap.options.get("map_character_symbol", self.map_character_symbol),
)
map_visual_range = kwargs.get( map_visual_range = kwargs.get(
"map_visual_range", xymap.options.get("map_visual_range", self.map_visual_range)) "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_mode = kwargs.get("map_mode", xymap.options.get("map_mode", self.map_mode))
map_align = kwargs.get( map_align = kwargs.get("map_align", xymap.options.get("map_align", self.map_align))
"map_align", xymap.options.get("map_align", self.map_align))
map_target_path_style = kwargs.get( map_target_path_style = kwargs.get(
"map_target_path_style", "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_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 = kwargs.get(
"map_separator_char", "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() client_width, _ = looker.sessions.get()[0].get_client_size()
@ -438,15 +465,14 @@ class XYZRoom(DefaultRoom):
if map_area_client: if map_area_client:
display_width = client_width display_width = client_width
else: else:
display_width = max(map_width, display_width = max(map_width, max(len(line) for line in room_desc.split("\n")))
max(len(line) for line in room_desc.split("\n")))
# align map # align map
map_indent = 0 map_indent = 0
sep_width = display_width sep_width = display_width
if map_align == 'r': if map_align == "r":
map_indent = max(0, display_width - map_width) 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) map_indent = max(0, (display_width - map_width) // 2)
# data set by the goto/path-command, for displaying the shortest path # 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, target_path_style=map_target_path_style,
character=map_character_symbol, character=map_character_symbol,
max_size=(display_width, None), max_size=(display_width, None),
indent=map_indent indent=map_indent,
) )
sep = map_separator_char * sep_width sep = map_separator_char * sep_width
map_display = f"{sep}|n\n{map_display}\n{sep}" map_display = f"{sep}|n\n{map_display}\n{sep}"
@ -523,8 +549,16 @@ class XYZExit(DefaultExit):
return self._xyz_destination return self._xyz_destination
@classmethod @classmethod
def create(cls, key, account=None, xyz=(0, 0, 'map'), xyz_destination=(0, 0, 'map'), def create(
location=None, destination=None, **kwargs): cls,
key,
account=None,
xyz=(0, 0, "map"),
xyz_destination=(0, 0, "map"),
location=None,
destination=None,
**kwargs,
):
""" """
Creation method aware of coordinates. 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`."] return None, ["XYExit.create need either `xyz=(X,Y,Z)` coordinate or a `location`."]
else: else:
source = XYZRoom.objects.get_xyz(xyz=(x, y, z)) source = XYZRoom.objects.get_xyz(xyz=(x, y, z))
tags.extend(((str(x), MAP_X_TAG_CATEGORY), tags.extend(
(str(y), MAP_Y_TAG_CATEGORY), (
(str(z), MAP_Z_TAG_CATEGORY))) (str(x), MAP_X_TAG_CATEGORY),
(str(y), MAP_Y_TAG_CATEGORY),
(str(z), MAP_Z_TAG_CATEGORY),
)
)
if destination: if destination:
dest = destination dest = destination
else: else:
try: try:
xdest, ydest, zdest = xyz_destination xdest, ydest, zdest = xyz_destination
except ValueError: except ValueError:
return None, ["XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate " return None, [
"or a `destination`."] "XYExit.create need either `xyz_destination=(X,Y,Z)` coordinate "
"or a `destination`."
]
else: else:
dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest)) dest = XYZRoom.objects.get_xyz(xyz=(xdest, ydest, zdest))
tags.extend(((str(xdest), MAP_XDEST_TAG_CATEGORY), tags.extend(
(str(ydest), MAP_YDEST_TAG_CATEGORY), (
(str(zdest), MAP_ZDEST_TAG_CATEGORY))) (str(xdest), MAP_XDEST_TAG_CATEGORY),
(str(ydest), MAP_YDEST_TAG_CATEGORY),
(str(zdest), MAP_ZDEST_TAG_CATEGORY),
)
)
return DefaultExit.create( return DefaultExit.create(
key, source, dest, key, source, dest, account=account, tags=tags, typeclass=cls, **kwargs
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 # noqa
from .dice import roll_dice # noqa from .dice import roll_dice # noqa
from .dice import CmdDice # 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 from evennia import default_cmds, CmdSet
def roll(dicenum, dicetype, modifier=None, def roll(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
conditional=None, return_tuple=False):
""" """
This is a standard dice roller. This is a standard dice roller.
@ -141,6 +140,7 @@ def roll(dicenum, dicetype, modifier=None,
else: else:
return result return result
# legacy alias # legacy alias
roll_dice = roll 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 ordered_permutation_regex, regex_tuple_from_key_alias # noqa
from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa from .rpsystem import parse_language, parse_sdescs_and_recogs, send_emote # noqa
from .rpsystem import SdescHandler, RecogHandler # noqa from .rpsystem import SdescHandler, RecogHandler # noqa
from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa from .rpsystem import RPCommand, CmdEmote, CmdSay, CmdSdesc, CmdPose, CmdRecog, CmdMask # noqa
from .rpsystem import RPSystemCmdSet # noqa from .rpsystem import RPSystemCmdSet # noqa
from .rpsystem import ContribRPObject # noqa from .rpsystem import ContribRPObject # noqa
from .rpsystem import ContribRPRoom # noqa from .rpsystem import ContribRPRoom # noqa

View file

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

View file

@ -274,7 +274,7 @@ class TestRPSystem(BaseEvenniaTest):
result = rpsystem.regex_tuple_from_key_alias(self.speaker) result = rpsystem.regex_tuple_from_key_alias(self.speaker)
t2 = time.time() t2 = time.time()
# print(f"t1: {t1 - t0}, t2: {t2 - t1}") # 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)) self.assertEqual(result, (Anything, self.speaker, self.speaker.key))

View file

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

View file

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

View file

@ -65,8 +65,10 @@ def info2(caller):
def info3(caller): def info3(caller):
text = ("'Well ... I'm sort of busy so, have to go. NPC business. " text = (
"Important stuff. You wouldn't understand.'") "'Well ... I'm sort of busy so, have to go. NPC business. "
"Important stuff. You wouldn't understand.'"
)
options = ( options = (
{"desc": "Oookay ... I won't keep you. Bye.", "goto": "END"}, {"desc": "Oookay ... I won't keep you. Bye.", "goto": "END"},
@ -91,15 +93,15 @@ def END(caller):
class CmdTalk(default_cmds.MuxCommand): class CmdTalk(default_cmds.MuxCommand):
""" """
Talks to an npc Talks to an npc
Usage: Usage:
talk talk
This command is only available if a talkative non-player-character This command is only available if a talkative non-player-character
(NPC) is actually present. It will strike up a conversation with (NPC) is actually present. It will strike up a conversation with
that NPC and give you options on what to talk about. that NPC and give you options on what to talk about.
""" """
key = "talk" key = "talk"
locks = "cmd:all()" locks = "cmd:all()"
@ -113,8 +115,11 @@ class CmdTalk(default_cmds.MuxCommand):
# Initiate the menu. Change this if you are putting this on # Initiate the menu. Change this if you are putting this on
# some other custom NPC class. # some other custom NPC class.
EvMenu(self.caller, "evennia.contrib.tutorials.talking_npc.talking_npc", EvMenu(
startnode="menu_start_node") self.caller,
"evennia.contrib.tutorials.talking_npc.talking_npc",
startnode="menu_start_node",
)
class TalkingCmdSet(CmdSet): 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. |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. |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.) |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.no_more_weapons_msg = "you find nothing else of use."
self.db.available_weapons = ["knife", "dagger", "sword", "club"] 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.)" helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
caller.msg(helptext) caller.msg(helptext)
# for the @detail command we inherit from MuxCommand, since # for the @detail command we inherit from MuxCommand, since
# we want to make use of MuxCommand's pre-parsing of '=' in the # we want to make use of MuxCommand's pre-parsing of '=' in the
# argument. # argument.
@ -202,22 +203,26 @@ class CmdTutorialLook(default_cmds.CmdLook):
looking_at_obj.at_desc(looker=caller) looking_at_obj.at_desc(looker=caller)
return return
class CmdTutorialGiveUp(default_cmds.MuxCommand): class CmdTutorialGiveUp(default_cmds.MuxCommand):
""" """
Give up the tutorial-world quest and return to Limbo, the start room of the Give up the tutorial-world quest and return to Limbo, the start room of the
server. server.
""" """
key = "give up" key = "give up"
aliases = ['abort'] aliases = ["abort"]
def func(self): def func(self):
outro_room = OutroRoom.objects.all() outro_room = OutroRoom.objects.all()
if outro_room: if outro_room:
outro_room = outro_room[0] outro_room = outro_room[0]
else: else:
self.caller.msg("That didn't work (seems like a bug). " self.caller.msg(
"Try to use the |wteleport|n command instead.") "That didn't work (seems like a bug). "
"Try to use the |wteleport|n command instead."
)
return return
self.caller.move_to(outro_room) self.caller.move_to(outro_room)
@ -312,6 +317,7 @@ class TutorialStartExit(DefaultExit):
will also clean up the intro command. will also clean up the intro command.
""" """
def at_object_creation(self): def at_object_creation(self):
self.cmdset.add(CmdSetEvenniaIntro, persistent=True) self.cmdset.add(CmdSetEvenniaIntro, persistent=True)
@ -397,6 +403,7 @@ SUPERUSER_WARNING = (
# #
# ------------------------------------------------------------- # -------------------------------------------------------------
class CmdEvenniaIntro(Command): class CmdEvenniaIntro(Command):
""" """
Start the Evennia intro wizard. Start the Evennia intro wizard.
@ -405,10 +412,12 @@ class CmdEvenniaIntro(Command):
intro intro
""" """
key = "intro" key = "intro"
def func(self): def func(self):
from .intro_menu import init_menu from .intro_menu import init_menu
# quell also superusers # quell also superusers
if self.caller.account: if self.caller.account:
self.caller.msg("Auto-quelling permissions while in intro ...") self.caller.msg("Auto-quelling permissions while in intro ...")
@ -463,6 +472,7 @@ class IntroRoom(TutorialRoom):
character.account.execute_cmd("quell") character.account.execute_cmd("quell")
character.msg("(Auto-quelling while in tutorial-world)") character.msg("(Auto-quelling while in tutorial-world)")
# ------------------------------------------------------------- # -------------------------------------------------------------
# #
# Bridge - unique room # Bridge - unique room
@ -1176,4 +1186,3 @@ class OutroRoom(TutorialRoom):
def at_object_leave(self, character, destination): def at_object_leave(self, character, destination):
if character.account: if character.account:
character.account.execute_cmd("unquell") character.account.execute_cmd("unquell")

View file

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

View file

@ -5,6 +5,6 @@ FieldFill contrib - Tim Ashley Jenkins 2018
from .fieldfill import FieldEvMenu # noqa from .fieldfill import FieldEvMenu # noqa
from .fieldfill import CmdTestMenu # noqa from .fieldfill import CmdTestMenu # noqa
from .fieldfill import init_fill_field # noqa from .fieldfill import init_fill_field # noqa
from .fieldfill import form_template_to_dict # noqa from .fieldfill import form_template_to_dict # noqa
from .fieldfill import display_formdata # 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 RandomStringGenerator # noqa
from .random_string_generator import RandomStringGeneratorScript # 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) self._find_elements(regex)
def __repr__(self): def __repr__(self):
return "<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format( return (
self.name "<evennia.contrib.utils.random_string_generator.RandomStringGenerator for {}>".format(
self.name
)
) )
def _get_script(self): def _get_script(self):
@ -169,7 +171,8 @@ class RandomStringGenerator:
script = ScriptDB.objects.get(db_key="generator_script") script = ScriptDB.objects.get(db_key="generator_script")
except ScriptDB.DoesNotExist: except ScriptDB.DoesNotExist:
script = create_script( script = create_script(
"evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript") "evennia.contrib.utils.random_string_generator.RandomStringGeneratorScript"
)
type(self).script = script type(self).script = script
return script return script

View file

@ -19,6 +19,7 @@ class Command(BaseCommand):
here. Without setting one, the parent's docstring will show (like now). here. Without setting one, the parent's docstring will show (like now).
""" """
# Each Command class implements the following methods, called in this order # Each Command class implements the following methods, called in this order
# (only func() is actually required): # (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 at_say(speaker, message) - by default, called if an object inside this
object speaks object speaks
""" """
pass pass

View file

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

View file

@ -27,7 +27,7 @@ Each dict is on the form
HELP_ENTRY_DICTS = [ HELP_ENTRY_DICTS = [
{ {
"key": "evennia", "key": "evennia",
"aliases": ['ev'], "aliases": ["ev"],
"category": "General", "category": "General",
"locks": "read:perm(Developer)", "locks": "read:perm(Developer)",
"text": """ "text": """
@ -51,7 +51,7 @@ HELP_ENTRY_DICTS = [
There is also a discord channel you can find from the sidebard on evennia.com. There is also a discord channel you can find from the sidebard on evennia.com.
""" """,
}, },
{ {
"key": "building", "key": "building",
@ -60,6 +60,6 @@ HELP_ENTRY_DICTS = [
Evennia comes with a bunch of default building commands. You can Evennia comes with a bunch of default building commands. You can
find a building tutorial in the evennia documentation. 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.conf import settings
from django.urls import reverse from django.urls import reverse
from django.utils.text import slugify from django.utils.text import slugify
from evennia.utils.utils import ( from evennia.utils.utils import variable_from_module, make_iter, all_from_module
variable_from_module, make_iter, all_from_module)
from evennia.utils import logger from evennia.utils import logger
from evennia.utils.utils import lazy_property from evennia.utils.utils import lazy_property
from evennia.locks.lockhandler import LockHandler from evennia.locks.lockhandler import LockHandler
@ -86,6 +85,7 @@ class FileHelpEntry:
help command. help command.
""" """
key: str key: str
aliases: list aliases: list
help_category: str help_category: str
@ -147,7 +147,7 @@ class FileHelpEntry:
""" """
try: try:
return reverse( return reverse(
'help-entry-detail', "help-entry-detail",
kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)}, kwargs={"category": slugify(self.help_category), "topic": slugify(self.key)},
) )
except Exception: except Exception:
@ -192,8 +192,7 @@ class FileHelpStorageHandler:
""" """
Initialize the storage. Initialize the storage.
""" """
self.help_file_modules = [str(part).strip() self.help_file_modules = [str(part).strip() for part in make_iter(help_file_modules)]
for part in make_iter(help_file_modules)]
self.help_entries = [] self.help_entries = []
self.help_entries_dict = {} self.help_entries_dict = {}
self.load() self.load()
@ -206,13 +205,11 @@ class FileHelpStorageHandler:
loaded_help_dicts = [] loaded_help_dicts = []
for module_or_path in self.help_file_modules: for module_or_path in self.help_file_modules:
help_dict_list = variable_from_module( help_dict_list = variable_from_module(module_or_path, variable="HELP_ENTRY_DICTS")
module_or_path, variable="HELP_ENTRY_DICTS"
)
if not help_dict_list: if not help_dict_list:
help_dict_list = [ help_dict_list = [
dct for dct in all_from_module(module_or_path).values() dct for dct in all_from_module(module_or_path).values() if isinstance(dct, dict)
if isinstance(dct, dict)] ]
if help_dict_list: if help_dict_list:
loaded_help_dicts.extend(help_dict_list) loaded_help_dicts.extend(help_dict_list)
else: else:
@ -223,19 +220,23 @@ class FileHelpStorageHandler:
unique_help_entries = {} unique_help_entries = {}
for dct in loaded_help_dicts: for dct in loaded_help_dicts:
key = dct.get('key').lower().strip() key = dct.get("key").lower().strip()
category = dct.get('category', _DEFAULT_HELP_CATEGORY).strip() category = dct.get("category", _DEFAULT_HELP_CATEGORY).strip()
aliases = list(dct.get('aliases', [])) aliases = list(dct.get("aliases", []))
entrytext = dct.get('text', '') entrytext = dct.get("text", "")
locks = dct.get('locks', '') locks = dct.get("locks", "")
if not key and entrytext: if not key and entrytext:
logger.error(f"Cannot load file-help-entry (missing key or text): {dct}") logger.error(f"Cannot load file-help-entry (missing key or text): {dct}")
continue continue
unique_help_entries[key] = FileHelpEntry( unique_help_entries[key] = FileHelpEntry(
key=key, help_category=category, aliases=aliases, lock_storage=locks, key=key,
entrytext=entrytext) help_category=category,
aliases=aliases,
lock_storage=locks,
entrytext=entrytext,
)
self.help_entries_dict = unique_help_entries self.help_entries_dict = unique_help_entries
self.help_entries = list(unique_help_entries.values()) self.help_entries = list(unique_help_entries.values())

View file

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

View file

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

View file

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

View file

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

View file

@ -215,12 +215,11 @@ class TestPermissionCheck(BaseEvenniaTest):
Test the PermissionHandler.check method Test the PermissionHandler.check method
""" """
def test_check__success(self): def test_check__success(self):
"""Test combinations that should pass the check""" """Test combinations that should pass the check"""
self.assertEqual( self.assertEqual(
[perm for perm in self.char1.account.permissions.all()], [perm for perm in self.char1.account.permissions.all()], ["developer", "player"]
['developer', 'player']
) )
self.assertTrue(self.char1.permissions.check("Builder")) self.assertTrue(self.char1.permissions.check("Builder"))
self.assertTrue(self.char1.permissions.check("Builder", "Player")) 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("Builder", "dummy", require_all=True))
self.assertFalse(self.char1.permissions.check("Developer", "foobar", 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.char1.account.permissions.add("Builder")
self.assertEqual( self.assertEqual(
[perm for perm in self.char1.account.permissions.all()], [perm for perm in self.char1.account.permissions.all()], ["builder", "player"]
['builder', 'player']
) )
self.assertFalse(self.char1.permissions.check("Developer")) self.assertFalse(self.char1.permissions.check("Developer"))
self.assertFalse(self.char1.permissions.check("Developer", "Player", require_all=True)) 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() type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()
try: try:
return self.filter( return self.filter(cand_restriction & type_restriction & Q(**querykwargs)).order_by(
cand_restriction & type_restriction & Q(**querykwargs)).order_by("id") "id"
)
except exceptions.FieldError: except exceptions.FieldError:
return self.none() return self.none()
except ValueError: except ValueError:
@ -335,8 +336,9 @@ class ObjectDBManager(TypedObjectManager):
index_matches = string_partial_matching(key_strings, ostring, ret_index=True) index_matches = string_partial_matching(key_strings, ostring, ret_index=True)
if index_matches: if index_matches:
# a match by key # a match by key
match_ids = [obj.id for ind, obj in enumerate(search_candidates) match_ids = [
if ind in index_matches] obj.id for ind, obj in enumerate(search_candidates) if ind in index_matches
]
else: else:
# match by alias rather than by key # match by alias rather than by key
search_candidates = search_candidates.filter( 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.attributes import ModelAttributeBackend, NickHandler
from evennia.typeclasses.models import TypeclassBase from evennia.typeclasses.models import TypeclassBase
from evennia.utils import ansi, create, funcparser, logger, search from evennia.utils import ansi, create, funcparser, logger, search
from evennia.utils.utils import (class_from_module, is_iter, lazy_property, from evennia.utils.utils import (
list_to_string, make_iter, to_str, class_from_module,
variable_from_module) is_iter,
lazy_property,
list_to_string,
make_iter,
to_str,
variable_from_module,
)
_INFLECT = inflect.engine() _INFLECT = inflect.engine()
_MULTISESSION_MODE = settings.MULTISESSION_MODE _MULTISESSION_MODE = settings.MULTISESSION_MODE
@ -212,13 +218,13 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
objects = ObjectManager() objects = ObjectManager()
# populated by `return_apperance` # populated by `return_apperance`
appearance_template = ''' appearance_template = """
{header} {header}
|c{name}|n |c{name}|n
{desc} {desc}
{exits}{characters}{things} {exits}{characters}{things}
{footer} {footer}
''' """
# on-object properties # on-object properties
@ -532,12 +538,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# we re-run exact match agains one of the matches to # we re-run exact match agains one of the matches to
# make sure we were not catching partial matches not belonging # make sure we were not catching partial matches not belonging
# to the stack # to the stack
nstack = len(ObjectDB.objects.get_objs_with_key_or_alias( nstack = len(
results[0].key, ObjectDB.objects.get_objs_with_key_or_alias(
exact=True, results[0].key,
candidates=list(results), exact=True,
typeclasses=[typeclass] if typeclass else None candidates=list(results),
)) typeclasses=[typeclass] if typeclass else None,
)
)
if nstack == nresults: if nstack == nresults:
# a valid stack, return multiple results # a valid stack, return multiple results
return list(results)[:stacked] return list(results)[:stacked]
@ -630,9 +638,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
raw_string = self.nicks.nickreplace( raw_string = self.nicks.nickreplace(
raw_string, categories=("inputline", "channel"), include_account=True raw_string, categories=("inputline", "channel"), include_account=True
) )
return _CMDHANDLER( return _CMDHANDLER(self, raw_string, callertype="object", session=session, **kwargs)
self, raw_string, callertype="object", session=session, **kwargs
)
def msg(self, text=None, from_obj=None, session=None, options=None, **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 {} mapping = mapping or {}
you = from_obj or self you = from_obj or self
if 'you' not in mapping: if "you" not in mapping:
mapping[you] = you mapping[you] = you
contents = self.contents contents = self.contents
@ -802,14 +808,23 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# actor-stance replacements # actor-stance replacements
inmessage = _MSG_CONTENTS_PARSER.parse( inmessage = _MSG_CONTENTS_PARSER.parse(
inmessage, raise_errors=True, return_string=True, inmessage,
caller=you, receiver=receiver, mapping=mapping) raise_errors=True,
return_string=True,
caller=you,
receiver=receiver,
mapping=mapping,
)
# director-stance replacements # director-stance replacements
outmessage = inmessage.format( outmessage = inmessage.format(
**{key: obj.get_display_name(looker=receiver) **{
if hasattr(obj, "get_display_name") else str(obj) key: obj.get_display_name(looker=receiver)
for key, obj in mapping.items()}) 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) 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)` 7. `self.at_post_move(source_location)`
""" """
def logerr(string="", err=None): def logerr(string="", err=None):
"""Simple log helper method""" """Simple log helper method"""
logger.log_trace() logger.log_trace()
@ -989,8 +1005,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
if not home: if not home:
obj.location = None obj.location = None
obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin.")) obj.msg(_("Something went wrong! You are dumped into nowhere. Contact an admin."))
logger.log_err("Missing default home - '{name}(#{dbid})' now " logger.log_err(
"has a null location.".format(name=obj.name, dbid=obj.dbid)) "Missing default home - '{name}(#{dbid})' now "
"has a null location.".format(name=obj.name, dbid=obj.dbid)
)
return return
if obj.has_account: if obj.has_account:
@ -1550,7 +1568,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# This was created from nowhere and added to an account's # This was created from nowhere and added to an account's
# inventory; it's probably the result of a create command. # inventory; it's probably the result of a create command.
string = _("You now have {name} in your possession.").format( 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) self.location.msg(string)
return return
@ -1754,13 +1773,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
lists are the actual objects. lists are the actual objects.
""" """
def filter_visible(obj_list): def filter_visible(obj_list):
return [obj for obj in obj_list if obj != looker and obj.access(looker, "view")] return [obj for obj in obj_list if obj != looker and obj.access(looker, "view")]
return { return {
"exits": filter_visible(self.contents_get(content_type="exit")), "exits": filter_visible(self.contents_get(content_type="exit")),
"characters": filter_visible(self.contents_get(content_type="character")), "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): def get_content_names(self, looker, **kwargs):
@ -1789,13 +1809,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# a mapping {'exits': [...], 'characters': [...], 'things': [...]} # a mapping {'exits': [...], 'characters': [...], 'things': [...]}
contents_map = self.get_visible_contents(looker, **kwargs) contents_map = self.get_visible_contents(looker, **kwargs)
character_names = [char.get_display_name(looker, **kwargs) character_names = [
for char in contents_map['characters']] 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']] ]
exit_names = [exi.get_display_name(looker, **kwargs) for exi in contents_map["exits"]]
# group all same-named things under one name # group all same-named things under one name
things = defaultdict(list) things = defaultdict(list)
for thing in contents_map['things']: for thing in contents_map["things"]:
things[thing.get_display_name(looker, **kwargs)].append(thing) things[thing.get_display_name(looker, **kwargs)].append(thing)
# pluralize same-named things # pluralize same-named things
@ -1806,11 +1827,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
singular, plural = thing.get_numbered_name(nthings, looker, key=thingname) singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
thing_names.append(singular if nthings == 1 else plural) thing_names.append(singular if nthings == 1 else plural)
return { return {"exits": exit_names, "characters": character_names, "things": thing_names}
"exits": exit_names,
"characters": character_names,
"things": thing_names
}
def return_appearance(self, looker, **kwargs): def return_appearance(self, looker, **kwargs):
""" """
@ -1840,7 +1857,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
""" """
if not looker: if not looker:
return '' return ""
# ourselves # ourselves
name = self.get_display_name(looker, **kwargs) name = self.get_display_name(looker, **kwargs)
@ -1848,20 +1865,20 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# contents # contents
content_names_map = self.get_content_names(looker, **kwargs) content_names_map = self.get_content_names(looker, **kwargs)
exits = list_to_string(content_names_map['exits']) exits = list_to_string(content_names_map["exits"])
characters = list_to_string(content_names_map['characters']) characters = list_to_string(content_names_map["characters"])
things = list_to_string(content_names_map['things']) things = list_to_string(content_names_map["things"])
# populate the appearance_template string. It's a good idea to strip it and # populate the appearance_template string. It's a good idea to strip it and
# let the client add any extra spaces instead. # let the client add any extra spaces instead.
return self.appearance_template.format( return self.appearance_template.format(
header='', header="",
name=name, name=name,
desc=desc, desc=desc,
exits=f"|wExits:|n {exits}" if exits else '', exits=f"|wExits:|n {exits}" if exits else "",
characters=f"\n|wCharacters:|n {characters}" if characters else '', characters=f"\n|wCharacters:|n {characters}" if characters else "",
things=f"\n|wYou see:|n {things}" if things else '', things=f"\n|wYou see:|n {things}" if things else "",
footer='' footer="",
).strip() ).strip()
def at_look(self, target, **kwargs): def at_look(self, target, **kwargs):
@ -2124,7 +2141,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
msg_type = "whisper" msg_type = "whisper"
msg_self = ( msg_self = (
'{self} whisper to {all_receivers}, "|n{speech}|n"' '{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_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
msg_location = None msg_location = None
@ -2332,7 +2350,7 @@ class DefaultCharacter(DefaultObject):
@classmethod @classmethod
def validate_name(cls, name): 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: Args:
name (str) : The name of the character 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. self.db.prelogout_location = self.location # save location again to be sure.
else: else:
account.msg( account.msg(
_("|r{obj} has no location and no home is set.|n").format(obj=self), _("|r{obj} has no location and no home is set.|n").format(obj=self), session=session
session=session
) # Note to set home. ) # Note to set home.
def at_post_puppet(self, **kwargs): def at_post_puppet(self, **kwargs):
@ -2414,8 +2431,10 @@ class DefaultCharacter(DefaultObject):
self.msg((self.at_look(self.location), {"type": "look"}), options=None) self.msg((self.at_look(self.location), {"type": "look"}), options=None)
def message(obj, from_obj): def message(obj, from_obj):
obj.msg(_("{name} has entered the game.").format(name=self.get_display_name(obj)), obj.msg(
from_obj=from_obj) _("{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) self.location.for_contents(message, exclude=[self], from_obj=self)
@ -2438,8 +2457,10 @@ class DefaultCharacter(DefaultObject):
if self.location: if self.location:
def message(obj, from_obj): def message(obj, from_obj):
obj.msg(_("{name} has left the game.").format(name=self.get_display_name(obj)), obj.msg(
from_obj=from_obj) _("{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.location.for_contents(message, exclude=[self], from_obj=self)
self.db.prelogout_location = self.location self.db.prelogout_location = self.location
@ -2582,6 +2603,7 @@ class DefaultRoom(DefaultObject):
# Default Exit command, used by the base exit object # Default Exit command, used by the base exit object
# #
class ExitCommand(_COMMAND_DEFAULT_CLASS): class ExitCommand(_COMMAND_DEFAULT_CLASS):
""" """
This is a command that simply cause the caller to traverse 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): def test_contents_order(self):
"""Move object from room to room in various ways""" """Move object from room to room in various ways"""
self.assertEqual(self.room1.contents, [ self.assertEqual(
self.exit, self.obj1, self.obj2, self.char1, self.char2]) self.room1.contents, [self.exit, self.obj1, self.obj2, self.char1, self.char2]
)
self.assertEqual(self.room2.contents, []) self.assertEqual(self.room2.contents, [])
# use move_to hook to move obj1 # use move_to hook to move obj1
self.obj1.move_to(self.room2) 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]) self.assertEqual(self.room2.contents, [self.obj1])
# move obj2 # move obj2
self.obj2.move_to(self.room2) 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]) self.assertEqual(self.room2.contents, [self.obj1, self.obj2])
# move back and forth - it should # move back and forth - it should

View file

@ -262,7 +262,8 @@ def _validate_prototype(prototype):
def _format_protfuncs(): def _format_protfuncs():
out = [] out = []
sorted_funcs = [ 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: for protfunc_name, protfunc in sorted_funcs:
out.append( out.append(
@ -2113,8 +2114,9 @@ def _apply_diff(caller, **kwargs):
objects = kwargs["objects"] objects = kwargs["objects"]
back_node = kwargs["back_node"] back_node = kwargs["back_node"]
diff = kwargs.get("diff", None) diff = kwargs.get("diff", None)
num_changed = spawner.batch_update_objects_with_prototype(prototype, diff=diff, objects=objects, num_changed = spawner.batch_update_objects_with_prototype(
caller=caller) prototype, diff=diff, objects=objects, caller=caller
)
caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed)) caller.msg("|g{num} objects were updated successfully.|n".format(num=num_changed))
return back_node return back_node
@ -2354,7 +2356,7 @@ def node_apply_diff(caller, **kwargs):
def node_prototype_save(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 # these are only set if we selected 'yes' to save on a previous pass
prototype = kwargs.get("prototype", None) prototype = kwargs.get("prototype", None)
# set to True/False if answered, None if first pass # 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] prot_value = prototype[fieldname]
else: else:
# check if it's an attribute # check if it's an attribute
for attrtuple in prototype.get('attrs', []): for attrtuple in prototype.get("attrs", []):
if attrtuple[0] == fieldname: if attrtuple[0] == fieldname:
prot_value = attrtuple[1] prot_value = attrtuple[1]
break break
else: else:
raise AttributeError(f"{fieldname} not found in prototype\n{prototype}\n" raise AttributeError(
"(neither as prototype-field or as an Attribute") f"{fieldname} not found in prototype\n{prototype}\n"
"(neither as prototype-field or as an Attribute"
)
if callable(prot_value): if callable(prot_value):
raise RuntimeError(f"Error in prototype\n{prototype}\n$protkey can only reference static " raise RuntimeError(
f"values/attributes (found {prot_value})") f"Error in prototype\n{prototype}\n$protkey can only reference static "
f"values/attributes (found {prot_value})"
)
try: try:
return funcparser.funcparser_callable_eval(prot_value, **kwargs) return funcparser.funcparser_callable_eval(prot_value, **kwargs)
except funcparser.ParsingError: except funcparser.ParsingError:

View file

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

View file

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

View file

@ -45,6 +45,7 @@ _PROTPARENTS = {
}, },
} }
class TestSpawner(BaseEvenniaTest): class TestSpawner(BaseEvenniaTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
@ -340,7 +341,6 @@ class TestProtLib(BaseEvenniaTest):
class TestProtFuncs(BaseEvenniaTest): class TestProtFuncs(BaseEvenniaTest):
@override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"]) @override_settings(PROT_FUNC_MODULES=["evennia.prototypes.protfuncs"])
def test_protkey_protfunc(self): def test_protkey_protfunc(self):
test_prot = {"key1": "value1", "key2": 2} test_prot = {"key1": "value1", "key2": 2}
@ -350,8 +350,7 @@ class TestProtFuncs(BaseEvenniaTest):
"value1", "value1",
) )
self.assertEqual( self.assertEqual(
protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), protlib.protfunc_parser("$protkey(key2)", testing=True, prototype=test_prot), 2
2
) )
@ -908,6 +907,7 @@ class Test2474(BaseEvenniaTest):
that of its prototype_parent. that of its prototype_parent.
""" """
prototypes = { prototypes = {
"WEAPON": { "WEAPON": {
"typeclass": "evennia.objects.objects.DefaultObject", "typeclass": "evennia.objects.objects.DefaultObject",
@ -951,14 +951,14 @@ class TestPartialTagAttributes(BaseEvenniaTest):
def setUp(self): def setUp(self):
super().setUp() super().setUp()
self.prot = { self.prot = {
'prototype_key': 'rock', "prototype_key": "rock",
'typeclass': 'evennia.objects.objects.DefaultObject', "typeclass": "evennia.objects.objects.DefaultObject",
'key': 'a rock', "key": "a rock",
'tags': [('quantity', 'groupable')], # missing data field "tags": [("quantity", "groupable")], # missing data field
'attrs': [('quantity', 1)], # missing category and lock fields "attrs": [("quantity", 1)], # missing category and lock fields
'desc': 'A good way to get stoned.' "desc": "A good way to get stoned.",
} }
def test_partial_spawn(self): def test_partial_spawn(self):
obj = spawner.spawn(self.prot) 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 # if this an Attribute with a category we should differentiate
fieldname = self._attr_category_fieldname( fieldname = self._attr_category_fieldname(
fieldname, obj.db_category fieldname,
if fieldname == "db_value" and hasattr(obj, "db_category") else None obj.db_category if fieldname == "db_value" and hasattr(obj, "db_category") else None,
) )
to_delete = [] to_delete = []
@ -124,8 +124,7 @@ class MonitorHandler(object):
for (obj, fieldname, idstring) in to_delete: for (obj, fieldname, idstring) in to_delete:
del self.monitors[obj][fieldname][idstring] del self.monitors[obj][fieldname][idstring]
def add(self, obj, fieldname, callback, idstring="", persistent=False, def add(self, obj, fieldname, callback, idstring="", persistent=False, category=None, **kwargs):
category=None, **kwargs):
""" """
Add monitoring to a given field or Attribute. A field must Add monitoring to a given field or Attribute. A field must
be specified with the full db_* name or it will be assumed be specified with the full db_* name or it will be assumed

View file

@ -49,10 +49,12 @@ class ScriptHandler(object):
except Exception: except Exception:
next_repeat = "?" next_repeat = "?"
string += _("\n '{key}' ({next_repeat}/{interval}, {repeats} repeats): {desc}").format( 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, interval=interval,
repeats=repeats, repeats=repeats,
desc=script.desc) desc=script.desc,
)
return string.strip() return string.strip()
def add(self, scriptclass, key=None, autostart=True): 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.start(20, now=False, start_delay=10, count_start=1)
loopcall.__call__.assert_not_called() loopcall.__call__.assert_not_called()
self.assertEqual(loopcall.interval , 20) self.assertEqual(loopcall.interval, 20)
loopcall._scheduleFrom.assert_called_with(121) 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. on-demand rather than using a ticker.
""" """
class Ticker(object): class Ticker(object):
""" """
Represents a repeatedly running task that calls Represents a repeatedly running task that calls

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