Merge branch 'master' into latin_il8n
Added Latin translation of Evennia's core messages as per the directions in evennia/locale/README.
This commit is contained in:
commit
ce469f2765
590 changed files with 51168 additions and 4796 deletions
|
|
@ -1 +1 @@
|
|||
0.9.0
|
||||
0.9.5
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ This is the main top-level API for Evennia. You can explore the evennia library
|
|||
by accessing evennia.<subpackage> directly. From inside the game you can read
|
||||
docs of all object by viewing its `__doc__` string, such as through
|
||||
|
||||
@py evennia.ObjectDB.__doc__
|
||||
py evennia.ObjectDB.__doc__
|
||||
|
||||
For full functionality you should explore this module via a django-
|
||||
aware shell. Go to your game directory and use the command
|
||||
|
|
@ -20,27 +20,13 @@ See www.evennia.com for full documentation.
|
|||
# docstring header
|
||||
|
||||
DOCSTRING = """
|
||||
|cEvennia|n 'flat' API (use |wevennia.<component>.__doc__|n to read doc-strings
|
||||
and |wdict(evennia.component)|n or
|
||||
|wevennia.component.__dict__ to see contents)
|
||||
|cTypeclass-bases:|n |cDatabase models|n:
|
||||
DefaultAccount DefaultObject AccountDB ObjectDB
|
||||
DefaultGuest DefaultCharacter ChannelDB
|
||||
DefaultRoom ScriptDB
|
||||
DefaultChannel DefaultExit Msg
|
||||
DefaultScript
|
||||
|cSearch functions:|n |cCommand parents and helpers:|n
|
||||
search_account search_object default_cmds
|
||||
search_script search_channel Command InterruptCommand
|
||||
search_help search_message CmdSet
|
||||
search_tag managers |cUtilities:|n
|
||||
|cCreate functions:|n settings lockfuncs
|
||||
create_account create_object logger gametime
|
||||
create_script create_channel ansi spawn
|
||||
create_help_entry create_message contrib managers
|
||||
|cGlobal handlers:|n set_trace
|
||||
TICKER_HANDLER TASK_HANDLER EvMenu EvTable
|
||||
SESSION_HANDLER CHANNEL_HANDLER EvForm EvEditor """
|
||||
Evennia MU* creation system.
|
||||
|
||||
Online manual and API docs are found at http://www.evennia.com.
|
||||
|
||||
Flat-API shortcut names:
|
||||
{}
|
||||
"""
|
||||
|
||||
# Delayed loading of properties
|
||||
|
||||
|
|
@ -231,6 +217,7 @@ def _init():
|
|||
from . import contrib
|
||||
from .utils.evmenu import EvMenu
|
||||
from .utils.evtable import EvTable
|
||||
from .utils.evmore import EvMore
|
||||
from .utils.evform import EvForm
|
||||
from .utils.eveditor import EvEditor
|
||||
from .utils.ansi import ANSIString
|
||||
|
|
@ -247,10 +234,6 @@ def _init():
|
|||
from .utils.containers import GLOBAL_SCRIPTS
|
||||
from .utils.containers import OPTION_CLASSES
|
||||
|
||||
# initialize the doc string
|
||||
global __doc__
|
||||
__doc__ = ansi.parse_ansi(DOCSTRING)
|
||||
|
||||
# API containers
|
||||
|
||||
class _EvContainer(object):
|
||||
|
|
@ -409,11 +392,15 @@ def _init():
|
|||
BASE_GUEST_TYPECLASS = class_from_module(settings.BASE_GUEST_TYPECLASS)
|
||||
del class_from_module
|
||||
|
||||
# delayed starts
|
||||
# delayed starts - important so as to not back-access evennia before it has
|
||||
# finished initializing
|
||||
GLOBAL_SCRIPTS.start()
|
||||
from .prototypes import prototypes
|
||||
prototypes.load_module_prototypes()
|
||||
del prototypes
|
||||
|
||||
|
||||
def set_trace(term_size=(140, 40), debugger="auto"):
|
||||
def set_trace(term_size=(140, 80), debugger="auto"):
|
||||
"""
|
||||
Helper function for running a debugger inside the Evennia event loop.
|
||||
|
||||
|
|
@ -460,9 +447,21 @@ def set_trace(term_size=(140, 40), debugger="auto"):
|
|||
dbg = pdb.Pdb(stdout=sys.__stdout__)
|
||||
|
||||
try:
|
||||
# Start debugger, forcing it up one stack frame (otherwise `set_trace` will start debugger
|
||||
# this point, not the actual code location)
|
||||
# Start debugger, forcing it up one stack frame (otherwise `set_trace`
|
||||
# will start debugger this point, not the actual code location)
|
||||
dbg.set_trace(sys._getframe().f_back)
|
||||
except Exception:
|
||||
# Stopped at breakpoint. Press 'n' to continue into the code.
|
||||
dbg.set_trace()
|
||||
|
||||
|
||||
# initialize the doc string
|
||||
global __doc__
|
||||
__doc__ = DOCSTRING.format(
|
||||
"\n- "
|
||||
+ "\n- ".join(
|
||||
f"evennia.{key}"
|
||||
for key in sorted(globals())
|
||||
if not key.startswith("_") and key not in ("DOCSTRING",)
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ Typeclass for Account objects
|
|||
Note that this object is primarily intended to
|
||||
store OOC information, not game info! This
|
||||
object represents the actual user (not their
|
||||
character) and has NO actual precence in the
|
||||
character) and has NO actual presence in the
|
||||
game world (this is handled by the associated
|
||||
character object, so you should customize that
|
||||
instead for most things).
|
||||
|
|
@ -37,7 +37,7 @@ from evennia.scripts.scripthandler import ScriptHandler
|
|||
from evennia.commands.cmdsethandler import CmdSetHandler
|
||||
from evennia.utils.optionhandler import OptionHandler
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from random import getrandbits
|
||||
|
||||
__all__ = ("DefaultAccount",)
|
||||
|
|
@ -51,8 +51,12 @@ _CMDSET_ACCOUNT = settings.CMDSET_ACCOUNT
|
|||
_MUDINFO_CHANNEL = None
|
||||
|
||||
# Create throttles for too many account-creations and login attempts
|
||||
CREATION_THROTTLE = Throttle(limit=2, timeout=10 * 60)
|
||||
LOGIN_THROTTLE = Throttle(limit=5, timeout=5 * 60)
|
||||
CREATION_THROTTLE = Throttle(
|
||||
limit=settings.CREATION_THROTTLE_LIMIT, timeout=settings.CREATION_THROTTLE_TIMEOUT
|
||||
)
|
||||
LOGIN_THROTTLE = Throttle(
|
||||
limit=settings.LOGIN_THROTTLE_LIMIT, timeout=settings.LOGIN_THROTTLE_TIMEOUT
|
||||
)
|
||||
|
||||
|
||||
class AccountSessionHandler(object):
|
||||
|
|
@ -180,7 +184,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
- at_server_reload()
|
||||
- at_server_shutdown()
|
||||
|
||||
"""
|
||||
"""
|
||||
|
||||
objects = AccountManager()
|
||||
|
||||
|
|
@ -216,12 +220,16 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
@property
|
||||
def characters(self):
|
||||
# Get playable characters list
|
||||
objs = self.db._playable_characters
|
||||
objs = self.db._playable_characters or []
|
||||
|
||||
# Rebuild the list if legacy code left null values after deletion
|
||||
if None in objs:
|
||||
objs = [x for x in self.db._playable_characters if x]
|
||||
self.db._playable_characters = objs
|
||||
try:
|
||||
if None in objs:
|
||||
objs = [x for x in self.db._playable_characters if x]
|
||||
self.db._playable_characters = objs
|
||||
except Exception as e:
|
||||
logger.log_trace(e)
|
||||
logger.log_err(e)
|
||||
|
||||
return objs
|
||||
|
||||
|
|
@ -267,11 +275,11 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
raise RuntimeError("Session not found")
|
||||
if self.get_puppet(session) == obj:
|
||||
# already puppeting this object
|
||||
self.msg("You are already puppeting this object.")
|
||||
self.msg(_("You are already puppeting this object."))
|
||||
return
|
||||
if not obj.access(self, "puppet"):
|
||||
# no access
|
||||
self.msg(f"You don't have permission to puppet '{obj.key}'.")
|
||||
self.msg(_("You don't have permission to puppet '{key}'.").format(key=obj.key))
|
||||
return
|
||||
if obj.account:
|
||||
# object already puppeted
|
||||
|
|
@ -287,12 +295,12 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
else:
|
||||
txt1 = f"Taking over |c{obj.name}|n from another of your sessions."
|
||||
txt2 = f"|c{obj.name}|n|R is now acted from another of your sessions.|n"
|
||||
self.msg(txt1, session=session)
|
||||
self.msg(txt2, session=obj.sessions.all())
|
||||
self.msg(_(txt1), session=session)
|
||||
self.msg(_(txt2), session=obj.sessions.all())
|
||||
self.unpuppet_object(obj.sessions.get())
|
||||
elif obj.account.is_connected:
|
||||
# controlled by another account
|
||||
self.msg(f"|c{obj.key}|R is already puppeted by another Account.")
|
||||
self.msg(_("|c{key}|R is already puppeted by another Account.").format(key=obj.key))
|
||||
return
|
||||
|
||||
# do the puppeting
|
||||
|
|
@ -365,7 +373,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
puppet (Object): The matching puppeted object, if any.
|
||||
|
||||
"""
|
||||
return session.puppet
|
||||
return session.puppet if session else None
|
||||
|
||||
def get_all_puppets(self):
|
||||
"""
|
||||
|
|
@ -402,7 +410,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
"""
|
||||
Checks if a given username or IP is banned.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
ip (str, optional): IP address.
|
||||
username (str, optional): Username.
|
||||
|
||||
|
|
@ -473,7 +481,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
password (str): Password of account
|
||||
ip (str, optional): IP address of client
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
session (Session, optional): Session requesting authentication
|
||||
|
||||
Returns:
|
||||
|
|
@ -488,7 +496,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
|
||||
# See if authentication is currently being throttled
|
||||
if ip and LOGIN_THROTTLE.check(ip):
|
||||
errors.append("Too many login failures; please try again in a few minutes.")
|
||||
errors.append(_("Too many login failures; please try again in a few minutes."))
|
||||
|
||||
# With throttle active, do not log continued hits-- it is a
|
||||
# waste of storage and can be abused to make your logs harder to
|
||||
|
|
@ -500,8 +508,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if banned:
|
||||
# this is a banned IP or name!
|
||||
errors.append(
|
||||
"|rYou have been banned and cannot continue from here."
|
||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||
_(
|
||||
"|rYou have been banned and cannot continue from here."
|
||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||
)
|
||||
)
|
||||
logger.log_sec(f"Authentication Denied (Banned): {username} (IP: {ip}).")
|
||||
LOGIN_THROTTLE.update(ip, "Too many sightings of banned artifact.")
|
||||
|
|
@ -511,7 +521,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
account = authenticate(username=username, password=password)
|
||||
if not account:
|
||||
# User-facing message
|
||||
errors.append("Username and/or password is incorrect.")
|
||||
errors.append(_("Username and/or password is incorrect."))
|
||||
|
||||
# Log auth failures while throttle is inactive
|
||||
logger.log_sec(f"Authentication Failure: {username} (IP: {ip}).")
|
||||
|
|
@ -601,7 +611,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
Args:
|
||||
password (str): Password to validate
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
account (DefaultAccount, optional): Account object to validate the
|
||||
password for. Optional, but Django includes some validators to
|
||||
do things like making sure users aren't setting passwords to the
|
||||
|
|
@ -641,6 +651,56 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
logger.log_sec(f"Password successfully changed for {self}.")
|
||||
self.at_password_change()
|
||||
|
||||
def create_character(self, *args, **kwargs):
|
||||
"""
|
||||
Create a character linked to this account.
|
||||
|
||||
Args:
|
||||
key (str, optional): If not given, use the same name as the account.
|
||||
typeclass (str, optional): Typeclass to use for this character. If
|
||||
not given, use settings.BASE_CHARACTER_TYPECLASS.
|
||||
permissions (list, optional): If not given, use the account's permissions.
|
||||
ip (str, optiona): The client IP creating this character. Will fall back to the
|
||||
one stored for the account if not given.
|
||||
kwargs (any): Other kwargs will be used in the create_call.
|
||||
Returns:
|
||||
Object: A new character of the `character_typeclass` type. None on an error.
|
||||
list or None: A list of errors, or None.
|
||||
|
||||
"""
|
||||
# parse inputs
|
||||
character_key = kwargs.pop("key", self.key)
|
||||
character_ip = kwargs.pop("ip", self.db.creator_ip)
|
||||
character_permissions = kwargs.pop("permissions", self.permissions)
|
||||
|
||||
# Load the appropriate Character class
|
||||
character_typeclass = kwargs.pop("typeclass", None)
|
||||
character_typeclass = (
|
||||
character_typeclass if character_typeclass else settings.BASE_CHARACTER_TYPECLASS
|
||||
)
|
||||
Character = class_from_module(character_typeclass)
|
||||
|
||||
if "location" not in kwargs:
|
||||
kwargs["location"] = ObjectDB.objects.get_id(settings.START_LOCATION)
|
||||
|
||||
# Create the character
|
||||
character, errs = Character.create(
|
||||
character_key,
|
||||
self,
|
||||
ip=character_ip,
|
||||
typeclass=character_typeclass,
|
||||
permissions=character_permissions,
|
||||
**kwargs,
|
||||
)
|
||||
if character:
|
||||
# Update playable character list
|
||||
if character not in self.characters:
|
||||
self.db._playable_characters.append(character)
|
||||
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
self.db._last_puppet = character
|
||||
return character, errs
|
||||
|
||||
@classmethod
|
||||
def create(cls, *args, **kwargs):
|
||||
"""
|
||||
|
|
@ -648,7 +708,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
with default (or overridden) permissions and having joined them to the
|
||||
appropriate default channels.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
username (str): Username of Account owner
|
||||
password (str): Password of Account owner
|
||||
email (str, optional): Email address of Account owner
|
||||
|
|
@ -680,7 +740,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
ip = kwargs.get("ip", "")
|
||||
if ip and CREATION_THROTTLE.check(ip):
|
||||
errors.append(
|
||||
"You are creating too many accounts. Please log into an existing account."
|
||||
_("You are creating too many accounts. Please log into an existing account.")
|
||||
)
|
||||
return None, errors
|
||||
|
||||
|
|
@ -708,7 +768,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
banned = cls.is_banned(username=username, ip=ip)
|
||||
if banned:
|
||||
# this is a banned IP or name!
|
||||
string = (
|
||||
string = _(
|
||||
"|rYou have been banned and cannot continue from here."
|
||||
"\nIf you feel this ban is in error, please email an admin.|x"
|
||||
)
|
||||
|
|
@ -725,7 +785,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
|
||||
except Exception as e:
|
||||
errors.append(
|
||||
"There was an error creating the Account. If this problem persists, contact an admin."
|
||||
_(
|
||||
"There was an error creating the Account. If this problem persists, contact an admin."
|
||||
)
|
||||
)
|
||||
logger.log_trace()
|
||||
return None, errors
|
||||
|
|
@ -747,37 +809,19 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
logger.log_err(string)
|
||||
|
||||
if account and settings.MULTISESSION_MODE < 2:
|
||||
# Load the appropriate Character class
|
||||
character_typeclass = kwargs.get(
|
||||
"character_typeclass", settings.BASE_CHARACTER_TYPECLASS
|
||||
# Auto-create a character to go with this account
|
||||
|
||||
character, errs = account.create_character(
|
||||
typeclass=kwargs.get("character_typeclass")
|
||||
)
|
||||
character_home = kwargs.get("home")
|
||||
Character = class_from_module(character_typeclass)
|
||||
|
||||
# Create the character
|
||||
character, errs = Character.create(
|
||||
account.key,
|
||||
account,
|
||||
ip=ip,
|
||||
typeclass=character_typeclass,
|
||||
permissions=permissions,
|
||||
home=character_home,
|
||||
)
|
||||
errors.extend(errs)
|
||||
|
||||
if character:
|
||||
# Update playable character list
|
||||
if character not in account.characters:
|
||||
account.db._playable_characters.append(character)
|
||||
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
account.db._last_puppet = character
|
||||
if errs:
|
||||
errors.extend(errs)
|
||||
|
||||
except Exception:
|
||||
# We are in the middle between logged in and -not, so we have
|
||||
# to handle tracebacks ourselves at this point. If we don't,
|
||||
# we won't see any errors at all.
|
||||
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||
errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
|
||||
logger.log_trace()
|
||||
|
||||
# Update the throttle to indicate a new account was created from this IP
|
||||
|
|
@ -820,7 +864,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
server.
|
||||
|
||||
Args:
|
||||
text (str, optional): text data to send
|
||||
text (str or tuple, optional): The message to send. This
|
||||
is treated internally like any send-command, so its
|
||||
value can be a tuple if sending multiple arguments to
|
||||
the `text` oob command.
|
||||
from_obj (Object or Account or list, optional): Object sending. If given, its
|
||||
at_msg_send() hook will be called. If iterable, call on all entities.
|
||||
session (Session or list, optional): Session object or a list of
|
||||
|
|
@ -828,7 +875,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
default send behavior for the current
|
||||
MULTISESSION_MODE.
|
||||
options (list): Protocol-specific options. Passed on to the protocol.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
any (dict): All other keywords are passed on to the protocol.
|
||||
|
||||
"""
|
||||
|
|
@ -851,7 +898,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
kwargs["options"] = options
|
||||
|
||||
if text is not None:
|
||||
kwargs["text"] = to_str(text)
|
||||
if not (isinstance(text, str) or isinstance(text, tuple)):
|
||||
# sanitize text before sending across the wire
|
||||
try:
|
||||
text = to_str(text)
|
||||
except Exception:
|
||||
text = repr(text)
|
||||
kwargs["text"] = text
|
||||
|
||||
# session relay
|
||||
sessions = make_iter(session) if session else self.sessions.all()
|
||||
|
|
@ -870,7 +923,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
session (Session, optional): The session to be responsible
|
||||
for the command-send
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): Other keyword arguments will be added to the
|
||||
found command object instance as variables before it
|
||||
executes. This is unused by default Evennia but may be
|
||||
|
|
@ -899,6 +952,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
nofound_string=None,
|
||||
multimatch_string=None,
|
||||
use_nicks=True,
|
||||
quiet=False,
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
|
|
@ -925,9 +979,13 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
message to echo if `searchdata` leads to multiple matches.
|
||||
If not given, will fall back to the default handler.
|
||||
use_nicks (bool, optional): Use account-level nick replacement.
|
||||
quiet (bool, optional): If set, will not show any error to the user,
|
||||
and will also lead to returning a list of matches.
|
||||
|
||||
Return:
|
||||
match (Account, Object or None): A single Account or Object match.
|
||||
list: If `quiet=True` this is a list of 0, 1 or more Account or Object matches.
|
||||
|
||||
Notes:
|
||||
Extra keywords are ignored, but are allowed in call in
|
||||
order to make API more consistent with
|
||||
|
|
@ -939,28 +997,31 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
# handle wrapping of common terms
|
||||
if searchdata.lower() in ("me", "*me", "self", "*self"):
|
||||
return self
|
||||
if search_object:
|
||||
matches = ObjectDB.objects.object_search(
|
||||
searchdata, typeclass=typeclass, use_nicks=use_nicks
|
||||
)
|
||||
else:
|
||||
searchdata = self.nicks.nickreplace(
|
||||
searchdata, categories=("account",), include_account=False
|
||||
)
|
||||
|
||||
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
|
||||
matches = _AT_SEARCH_RESULT(
|
||||
matches,
|
||||
self,
|
||||
query=searchdata,
|
||||
nofound_string=nofound_string,
|
||||
multimatch_string=multimatch_string,
|
||||
searchdata = self.nicks.nickreplace(
|
||||
searchdata, categories=("account",), include_account=False
|
||||
)
|
||||
if matches and return_puppet:
|
||||
try:
|
||||
return matches.puppet
|
||||
except AttributeError:
|
||||
return None
|
||||
if search_object:
|
||||
matches = ObjectDB.objects.object_search(searchdata, typeclass=typeclass)
|
||||
else:
|
||||
matches = AccountDB.objects.account_search(searchdata, typeclass=typeclass)
|
||||
|
||||
if quiet:
|
||||
matches = list(matches)
|
||||
if return_puppet:
|
||||
matches = [match.puppet for match in matches]
|
||||
else:
|
||||
matches = _AT_SEARCH_RESULT(
|
||||
matches,
|
||||
self,
|
||||
query=searchdata,
|
||||
nofound_string=nofound_string,
|
||||
multimatch_string=multimatch_string,
|
||||
)
|
||||
if matches and return_puppet:
|
||||
try:
|
||||
matches = matches.puppet
|
||||
except AttributeError:
|
||||
return None
|
||||
return matches
|
||||
|
||||
def access(
|
||||
|
|
@ -978,7 +1039,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
no_superuser_bypass (bool, optional): Turn off superuser
|
||||
lock bypassing. Be careful with this one.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): Passed to the at_access hook along with the result.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1122,7 +1183,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
check.
|
||||
access_type (str): The type of access checked.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): These are passed on from the access check
|
||||
and can be used to relay custom instructions from the
|
||||
check mechanism.
|
||||
|
|
@ -1204,7 +1265,10 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
]
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
now = timezone.now()
|
||||
if settings.USE_TZ:
|
||||
now = timezone.localtime()
|
||||
else:
|
||||
now = timezone.now()
|
||||
now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute)
|
||||
if _MUDINFO_CHANNEL:
|
||||
_MUDINFO_CHANNEL.tempmsg(f"[{_MUDINFO_CHANNEL.key}, {now}]: {message}")
|
||||
|
|
@ -1236,21 +1300,21 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if session:
|
||||
session.msg(logged_in={})
|
||||
|
||||
self._send_to_connect_channel(f"|G{self.key} connected|n")
|
||||
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
|
||||
if _MULTISESSION_MODE == 0:
|
||||
# in this mode we should have only one character available. We
|
||||
# try to auto-connect to our last conneted object, if any
|
||||
try:
|
||||
self.puppet_object(session, self.db._last_puppet)
|
||||
except RuntimeError:
|
||||
self.msg("The Character does not exist.")
|
||||
self.msg(_("The Character does not exist."))
|
||||
return
|
||||
elif _MULTISESSION_MODE == 1:
|
||||
# in this mode all sessions connect to the same puppet.
|
||||
try:
|
||||
self.puppet_object(session, self.db._last_puppet)
|
||||
except RuntimeError:
|
||||
self.msg("The Character does not exist.")
|
||||
self.msg(_("The Character does not exist."))
|
||||
return
|
||||
elif _MULTISESSION_MODE in (2, 3):
|
||||
# In this mode we by default end up at a character selection
|
||||
|
|
@ -1288,7 +1352,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
|
||||
"""
|
||||
reason = f" ({reason if reason else ''})"
|
||||
self._send_to_connect_channel(f"|R{self.key} disconnected{reason}|n")
|
||||
self._send_to_connect_channel(
|
||||
_("|R{key} disconnected{reason}|n").format(key=self.key, reason=reason)
|
||||
)
|
||||
|
||||
def at_post_disconnect(self, **kwargs):
|
||||
"""
|
||||
|
|
@ -1322,7 +1388,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
text (str, optional): The message received.
|
||||
from_obj (any, optional): The object sending the message.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
This includes any keywords sent to the `msg` method.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1344,7 +1410,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
text (str, optional): Text to send.
|
||||
to_obj (any, optional): The object to send to.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Keywords passed from msg()
|
||||
|
||||
Notes:
|
||||
|
|
@ -1394,7 +1460,7 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if hasattr(target, "return_appearance"):
|
||||
return target.return_appearance(self)
|
||||
else:
|
||||
return "{} has no in-game appearance.".format(target)
|
||||
return _("{target} has no in-game appearance.").format(target=target)
|
||||
else:
|
||||
# list of targets - make list to disconnect from db
|
||||
characters = list(tar for tar in target if tar) if target else []
|
||||
|
|
@ -1437,7 +1503,9 @@ class DefaultAccount(AccountDB, metaclass=TypeclassBase):
|
|||
if is_su or len(characters) < charmax:
|
||||
if not characters:
|
||||
result.append(
|
||||
"\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one."
|
||||
_(
|
||||
"\n\n You don't have any characters yet. See |whelp @charcreate|n for creating one."
|
||||
)
|
||||
)
|
||||
else:
|
||||
result.append("\n |w@charcreate <name> [=description]|n - create new character")
|
||||
|
|
@ -1501,7 +1569,7 @@ class DefaultGuest(DefaultAccount):
|
|||
"""
|
||||
Gets or creates a Guest account object.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
ip (str, optional): IP address of requestor; used for ban checking,
|
||||
throttling and logging
|
||||
|
||||
|
|
@ -1517,17 +1585,17 @@ class DefaultGuest(DefaultAccount):
|
|||
|
||||
# check if guests are enabled.
|
||||
if not settings.GUEST_ENABLED:
|
||||
errors.append("Guest accounts are not enabled on this server.")
|
||||
errors.append(_("Guest accounts are not enabled on this server."))
|
||||
return None, errors
|
||||
|
||||
try:
|
||||
# Find an available guest name.
|
||||
for name in settings.GUEST_LIST:
|
||||
if not AccountDB.objects.filter(username__iexact=name).count():
|
||||
if not AccountDB.objects.filter(username__iexact=name).exists():
|
||||
username = name
|
||||
break
|
||||
if not username:
|
||||
errors.append("All guest accounts are in use. Please try again later.")
|
||||
errors.append(_("All guest accounts are in use. Please try again later."))
|
||||
if ip:
|
||||
LOGIN_THROTTLE.update(ip, "Too many requests for Guest access.")
|
||||
return None, errors
|
||||
|
|
@ -1549,13 +1617,22 @@ class DefaultGuest(DefaultAccount):
|
|||
ip=ip,
|
||||
)
|
||||
errors.extend(errs)
|
||||
|
||||
if not account.characters:
|
||||
# this can happen for multisession_mode > 1. For guests we
|
||||
# always auto-create a character, regardless of multi-session-mode.
|
||||
character, errs = account.create_character()
|
||||
|
||||
if errs:
|
||||
errors.extend(errs)
|
||||
|
||||
return account, errors
|
||||
|
||||
except Exception as e:
|
||||
# We are in the middle between logged in and -not, so we have
|
||||
# to handle tracebacks ourselves at this point. If we don't,
|
||||
# we won't see any errors at all.
|
||||
errors.append("An error occurred. Please e-mail an admin if the problem persists.")
|
||||
errors.append(_("An error occurred. Please e-mail an admin if the problem persists."))
|
||||
logger.log_trace()
|
||||
return None, errors
|
||||
|
||||
|
|
@ -1572,7 +1649,7 @@ class DefaultGuest(DefaultAccount):
|
|||
overriding the call (unused by default).
|
||||
|
||||
"""
|
||||
self._send_to_connect_channel(f"|G{self.key} connected|n")
|
||||
self._send_to_connect_channel(_("|G{key} connected|n").format(key=self.key))
|
||||
self.puppet_object(session, self.db._last_puppet)
|
||||
|
||||
def at_server_shutdown(self):
|
||||
|
|
|
|||
|
|
@ -4,13 +4,26 @@
|
|||
#
|
||||
from django import forms
|
||||
from django.conf import settings
|
||||
from django.contrib import admin
|
||||
from django.contrib import admin, messages
|
||||
from django.contrib.admin.options import IS_POPUP_VAR
|
||||
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
|
||||
from django.contrib.auth.forms import UserChangeForm, UserCreationForm
|
||||
from django.contrib.admin.utils import unquote
|
||||
from django.template.response import TemplateResponse
|
||||
from django.http import Http404, HttpResponseRedirect
|
||||
from django.core.exceptions import PermissionDenied
|
||||
from django.views.decorators.debug import sensitive_post_parameters
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.html import escape
|
||||
from django.urls import path, reverse
|
||||
from django.contrib.auth import update_session_auth_hash
|
||||
|
||||
from evennia.accounts.models import AccountDB
|
||||
from evennia.typeclasses.admin import AttributeInline, TagInline
|
||||
from evennia.utils import create
|
||||
|
||||
sensitive_post_parameters_m = method_decorator(sensitive_post_parameters())
|
||||
|
||||
|
||||
# handle the custom User editor
|
||||
class AccountDBChangeForm(UserChangeForm):
|
||||
|
|
@ -88,6 +101,7 @@ class AccountForm(forms.ModelForm):
|
|||
class Meta(object):
|
||||
model = AccountDB
|
||||
fields = "__all__"
|
||||
app_label = "accounts"
|
||||
|
||||
db_key = forms.RegexField(
|
||||
label="Username",
|
||||
|
|
@ -259,6 +273,70 @@ class AccountDBAdmin(BaseUserAdmin):
|
|||
),
|
||||
)
|
||||
|
||||
@sensitive_post_parameters_m
|
||||
def user_change_password(self, request, id, form_url=""):
|
||||
user = self.get_object(request, unquote(id))
|
||||
if not self.has_change_permission(request, user):
|
||||
raise PermissionDenied
|
||||
if user is None:
|
||||
raise Http404("%(name)s object with primary key %(key)r does not exist.") % {
|
||||
"name": self.model._meta.verbose_name,
|
||||
"key": escape(id),
|
||||
}
|
||||
if request.method == "POST":
|
||||
form = self.change_password_form(user, request.POST)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
change_message = self.construct_change_message(request, form, None)
|
||||
self.log_change(request, user, change_message)
|
||||
msg = "Password changed successfully."
|
||||
messages.success(request, msg)
|
||||
update_session_auth_hash(request, form.user)
|
||||
return HttpResponseRedirect(
|
||||
reverse(
|
||||
"%s:%s_%s_change"
|
||||
% (
|
||||
self.admin_site.name,
|
||||
user._meta.app_label,
|
||||
# the model_name is something we need to hardcode
|
||||
# since our accountdb is a proxy:
|
||||
"accountdb",
|
||||
),
|
||||
args=(user.pk,),
|
||||
)
|
||||
)
|
||||
else:
|
||||
form = self.change_password_form(user)
|
||||
|
||||
fieldsets = [(None, {"fields": list(form.base_fields)})]
|
||||
adminForm = admin.helpers.AdminForm(form, fieldsets, {})
|
||||
|
||||
context = {
|
||||
"title": "Change password: %s" % escape(user.get_username()),
|
||||
"adminForm": adminForm,
|
||||
"form_url": form_url,
|
||||
"form": form,
|
||||
"is_popup": (IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET),
|
||||
"add": True,
|
||||
"change": False,
|
||||
"has_delete_permission": False,
|
||||
"has_change_permission": True,
|
||||
"has_absolute_url": False,
|
||||
"opts": self.model._meta,
|
||||
"original": user,
|
||||
"save_as": False,
|
||||
"show_save": True,
|
||||
**self.admin_site.each_context(request),
|
||||
}
|
||||
|
||||
request.current_app = self.admin_site.name
|
||||
|
||||
return TemplateResponse(
|
||||
request,
|
||||
self.change_user_password_template or "admin/auth/user/change_password.html",
|
||||
context,
|
||||
)
|
||||
|
||||
def save_model(self, request, obj, form, change):
|
||||
"""
|
||||
Custom save actions.
|
||||
|
|
|
|||
|
|
@ -10,6 +10,7 @@ from evennia.accounts.accounts import DefaultAccount
|
|||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.utils import search
|
||||
from evennia.utils import utils
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_IDLE_TIMEOUT = settings.IDLE_TIMEOUT
|
||||
|
||||
|
|
@ -277,7 +278,7 @@ class IRCBot(Bot):
|
|||
Args:
|
||||
text (str, optional): Incoming text from channel.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
options (dict): Options dict with the following allowed keys:
|
||||
- from_channel (str): dbid of a channel this text originated from.
|
||||
- from_obj (list): list of objects sending this text.
|
||||
|
|
@ -307,7 +308,7 @@ class IRCBot(Bot):
|
|||
session (Session, optional): Session responsible for this
|
||||
command. Note that this is the bot.
|
||||
txt (str, optional): Command string.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
user (str): The name of the user who sent the message.
|
||||
channel (str): The name of channel the message was sent to.
|
||||
type (str): Nature of message. Either 'msg', 'action', 'nicklist'
|
||||
|
|
@ -328,7 +329,9 @@ class IRCBot(Bot):
|
|||
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
|
||||
nicklist = ", ".join(sorted(kwargs["nicklist"], key=lambda n: n.lower()))
|
||||
for obj in self._nicklist_callers:
|
||||
obj.msg(f"Nicks at {chstr}:\n {nicklist}")
|
||||
obj.msg(
|
||||
_("Nicks at {chstr}:\n {nicklist}").format(chstr=chstr, nicklist=nicklist)
|
||||
)
|
||||
self._nicklist_callers = []
|
||||
return
|
||||
|
||||
|
|
@ -337,7 +340,11 @@ class IRCBot(Bot):
|
|||
if hasattr(self, "_ping_callers") and self._ping_callers:
|
||||
chstr = f"{self.db.irc_channel} ({self.db.irc_network}:{self.db.irc_port})"
|
||||
for obj in self._ping_callers:
|
||||
obj.msg(f"IRC ping return from {chstr} took {kwargs['timing']}s.")
|
||||
obj.msg(
|
||||
_("IRC ping return from {chstr} took {time}s.").format(
|
||||
chstr=chstr, time=kwargs["timing"]
|
||||
)
|
||||
)
|
||||
self._ping_callers = []
|
||||
return
|
||||
|
||||
|
|
@ -511,7 +518,7 @@ class GrapevineBot(Bot):
|
|||
Args:
|
||||
text (str, optional): Incoming text from channel.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
options (dict): Options dict with the following allowed keys:
|
||||
- from_channel (str): dbid of a channel this text originated from.
|
||||
- from_obj (list): list of objects sending this text.
|
||||
|
|
|
|||
|
|
@ -105,12 +105,12 @@ class AccountDB(TypedObject, AbstractUser):
|
|||
objects = AccountDBManager()
|
||||
|
||||
# defaults
|
||||
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
|
||||
__defaultclasspath__ = "evennia.accounts.accounts.DefaultAccount"
|
||||
__applabel__ = "accounts"
|
||||
__settingsclasspath__ = settings.BASE_SCRIPT_TYPECLASS
|
||||
|
||||
class Meta(object):
|
||||
verbose_name = "Account"
|
||||
# class Meta:
|
||||
# verbose_name = "Account"
|
||||
|
||||
# cmdset_storage property
|
||||
# This seems very sensitive to caching, so leaving it be for now /Griatch
|
||||
|
|
|
|||
|
|
@ -92,7 +92,8 @@ class TestDefaultGuest(EvenniaTest):
|
|||
@patch("evennia.accounts.accounts.ChannelDB.objects.get_channel")
|
||||
def test_create(self, get_channel):
|
||||
get_channel.connect = MagicMock(return_value=True)
|
||||
account, errors = DefaultGuest.create()
|
||||
with override_settings(GUEST_HOME=self.room1.dbref):
|
||||
account, errors = DefaultGuest.create()
|
||||
self.assertTrue(account, "Guest account should have been created.")
|
||||
self.assertFalse(errors)
|
||||
|
||||
|
|
|
|||
0
evennia/commands/_trial_temp/_trial_marker
Executable file
0
evennia/commands/_trial_temp/_trial_marker
Executable file
|
|
@ -48,7 +48,7 @@ from evennia.comms.channelhandler import CHANNELHANDLER
|
|||
from evennia.utils import logger, utils
|
||||
from evennia.utils.utils import string_suggestions
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_IN_GAME_ERRORS = settings.IN_GAME_ERRORS
|
||||
|
||||
|
|
@ -174,6 +174,27 @@ def _msg_err(receiver, stringtuple):
|
|||
)
|
||||
|
||||
|
||||
def _process_input(caller, prompt, result, cmd, generator):
|
||||
"""
|
||||
Specifically handle the get_input value to send to _progressive_cmd_run as
|
||||
part of yielding from a Command's `func`.
|
||||
|
||||
Args:
|
||||
caller (Character, Account or Session): the caller.
|
||||
prompt (str): The sent prompt.
|
||||
result (str): The unprocessed answer.
|
||||
cmd (Command): The command itself.
|
||||
generator (GeneratorType): The generator.
|
||||
|
||||
Returns:
|
||||
result (bool): Always `False` (stop processing).
|
||||
|
||||
"""
|
||||
# We call it using a Twisted deferLater to make sure the input is properly closed.
|
||||
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
|
||||
return False
|
||||
|
||||
|
||||
def _progressive_cmd_run(cmd, generator, response=None):
|
||||
"""
|
||||
Progressively call the command that was given in argument. Used
|
||||
|
|
@ -206,7 +227,15 @@ def _progressive_cmd_run(cmd, generator, response=None):
|
|||
else:
|
||||
value = generator.send(response)
|
||||
except StopIteration:
|
||||
pass
|
||||
# duplicated from cmdhandler._run_command, to have these
|
||||
# run in the right order while staying inside the deferred
|
||||
cmd.at_post_cmd()
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
cmd.caller.ndb.last_cmd = copy(cmd)
|
||||
else:
|
||||
cmd.caller.ndb.last_cmd = None
|
||||
else:
|
||||
if isinstance(value, (int, float)):
|
||||
utils.delay(value, _progressive_cmd_run, cmd, generator)
|
||||
|
|
@ -216,27 +245,6 @@ def _progressive_cmd_run(cmd, generator, response=None):
|
|||
raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
|
||||
|
||||
|
||||
def _process_input(caller, prompt, result, cmd, generator):
|
||||
"""
|
||||
Specifically handle the get_input value to send to _progressive_cmd_run as
|
||||
part of yielding from a Command's `func`.
|
||||
|
||||
Args:
|
||||
caller (Character, Account or Session): the caller.
|
||||
prompt (str): The sent prompt.
|
||||
result (str): The unprocessed answer.
|
||||
cmd (Command): The command itself.
|
||||
generator (GeneratorType): The generator.
|
||||
|
||||
Returns:
|
||||
result (bool): Always `False` (stop processing).
|
||||
|
||||
"""
|
||||
# We call it using a Twisted deferLater to make sure the input is properly closed.
|
||||
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
|
||||
return False
|
||||
|
||||
|
||||
# custom Exceptions
|
||||
|
||||
|
||||
|
|
@ -472,13 +480,13 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
|
|||
tempmergers[prio] = cmdset
|
||||
|
||||
# sort cmdsets after reverse priority (highest prio are merged in last)
|
||||
cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)
|
||||
sorted_cmdsets = yield sorted(list(tempmergers.values()), key=lambda x: x.priority)
|
||||
|
||||
# Merge all command sets into one, beginning with the lowest-prio one
|
||||
cmdset = cmdsets[0]
|
||||
for merging_cmdset in cmdsets[1:]:
|
||||
cmdset = sorted_cmdsets[0]
|
||||
for merging_cmdset in sorted_cmdsets[1:]:
|
||||
cmdset = yield cmdset + merging_cmdset
|
||||
# store the full sets for diagnosis
|
||||
# store the original, ungrouped set for diagnosis
|
||||
cmdset.merged_from = cmdsets
|
||||
# cache
|
||||
_CMDSET_MERGE_CACHE[mergehash] = cmdset
|
||||
|
|
@ -486,6 +494,11 @@ def get_and_merge_cmdsets(caller, session, account, obj, callertype, raw_string)
|
|||
cmdset = None
|
||||
for cset in (cset for cset in local_obj_cmdsets if cset):
|
||||
cset.duplicates = cset.old_duplicates
|
||||
# important - this syncs the CmdSetHandler's .current field with the
|
||||
# true current cmdset!
|
||||
if cmdset:
|
||||
caller.cmdset.current = cmdset
|
||||
|
||||
returnValue(cmdset)
|
||||
except ErrorReported:
|
||||
raise
|
||||
|
|
@ -540,7 +553,7 @@ def cmdhandler(
|
|||
is made available as `self.cmdstring` when the Command runs.
|
||||
If not given, the command will be assumed to be called as `cmdobj.key`.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): other keyword arguments will be assigned as named variables on the
|
||||
retrieved command object *before* it is executed. This is unused
|
||||
in default Evennia but may be used by code to set custom flags or
|
||||
|
|
@ -632,19 +645,23 @@ def cmdhandler(
|
|||
if isinstance(ret, types.GeneratorType):
|
||||
# cmd.func() is a generator, execute progressively
|
||||
_progressive_cmd_run(cmd, ret)
|
||||
yield None
|
||||
ret = yield ret
|
||||
# note that the _progressive_cmd_run will itself run
|
||||
# the at_post_cmd etc as it finishes; this is a bit of
|
||||
# code duplication but there seems to be no way to
|
||||
# catch the StopIteration here (it's not in the same
|
||||
# frame since this is in a deferred chain)
|
||||
else:
|
||||
ret = yield ret
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
if cmd.save_for_next:
|
||||
# store a reference to this command, possibly
|
||||
# accessible by the next command.
|
||||
caller.ndb.last_cmd = yield copy(cmd)
|
||||
else:
|
||||
caller.ndb.last_cmd = None
|
||||
|
||||
# return result to the deferred
|
||||
returnValue(ret)
|
||||
|
|
@ -733,7 +750,7 @@ def cmdhandler(
|
|||
if len(matches) == 1:
|
||||
# We have a unique command match. But it may still be invalid.
|
||||
match = matches[0]
|
||||
cmdname, args, cmd, raw_cmdname = match[0], match[1], match[2], match[5]
|
||||
cmdname, args, cmd, raw_cmdname = (match[0], match[1], match[2], match[5])
|
||||
|
||||
if not matches:
|
||||
# No commands match our entered command
|
||||
|
|
@ -743,7 +760,9 @@ def cmdhandler(
|
|||
sysarg = raw_string
|
||||
else:
|
||||
# fallback to default error text
|
||||
sysarg = _("Command '%s' is not available.") % raw_string
|
||||
sysarg = _("Command '{command}' is not available.").format(
|
||||
command=raw_string
|
||||
)
|
||||
suggestions = string_suggestions(
|
||||
raw_string,
|
||||
cmdset.get_all_cmd_keys_and_aliases(caller),
|
||||
|
|
@ -751,8 +770,8 @@ def cmdhandler(
|
|||
maxnum=3,
|
||||
)
|
||||
if suggestions:
|
||||
sysarg += _(" Maybe you meant %s?") % utils.list_to_string(
|
||||
suggestions, _("or"), addquote=True
|
||||
sysarg += _(" Maybe you meant {command}?").format(
|
||||
command=utils.list_to_string(suggestions, _("or"), addquote=True)
|
||||
)
|
||||
else:
|
||||
sysarg += _(' Type "help" for help.')
|
||||
|
|
|
|||
|
|
@ -125,7 +125,7 @@ def try_num_prefixes(raw_string):
|
|||
# the user might be trying to identify the command
|
||||
# with a #num-command style syntax. We expect the regex to
|
||||
# contain the groups "number" and "name".
|
||||
mindex, new_raw_string = num_ref_match.group("number"), num_ref_match.group("name")
|
||||
mindex, new_raw_string = (num_ref_match.group("number"), num_ref_match.group("name"))
|
||||
return mindex, new_raw_string
|
||||
else:
|
||||
return None, None
|
||||
|
|
@ -209,10 +209,15 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
|
|||
quality = [mat[4] for mat in matches]
|
||||
matches = matches[-quality.count(quality[-1]) :]
|
||||
|
||||
if len(matches) > 1 and match_index is not None and 0 < match_index <= len(matches):
|
||||
if len(matches) > 1 and match_index is not None:
|
||||
# We couldn't separate match by quality, but we have an
|
||||
# index argument to tell us which match to use.
|
||||
matches = [matches[match_index - 1]]
|
||||
if 0 < match_index <= len(matches):
|
||||
matches = [matches[match_index - 1]]
|
||||
else:
|
||||
# we tried to give an index outside of the range - this means
|
||||
# a no-match
|
||||
matches = []
|
||||
|
||||
# no matter what we have at this point, we have to return it.
|
||||
return matches
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ Set theory.
|
|||
|
||||
"""
|
||||
from weakref import WeakKeyDictionary
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.utils.utils import inherits_from, is_iter
|
||||
|
||||
__all__ = ("CmdSet",)
|
||||
|
|
@ -106,9 +106,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
commands preference.
|
||||
|
||||
duplicates - determines what happens when two sets of equal
|
||||
priority merge. Default has the first of them in the
|
||||
priority merge (only). Defaults to None and has the first of them in the
|
||||
merger (i.e. A above) automatically taking
|
||||
precedence. But if allow_duplicates is true, the
|
||||
precedence. But if `duplicates` is true, the
|
||||
result will be a merger with more than one of each
|
||||
name match. This will usually lead to the account
|
||||
receiving a multiple-match error higher up the road,
|
||||
|
|
@ -119,6 +119,16 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
select which ball to kick ... Allowing duplicates
|
||||
only makes sense for Union and Intersect, the setting
|
||||
is ignored for the other mergetypes.
|
||||
Note that the `duplicates` flag is *not* propagated in
|
||||
a cmdset merger. So `A + B = C` will result in
|
||||
a cmdset with duplicate commands, but C.duplicates will
|
||||
be `None`. For duplication to apply to a whole cmdset
|
||||
stack merge, _all_ cmdsets in the stack must have
|
||||
`.duplicates=True` set.
|
||||
Finally, if a final cmdset has `.duplicates=None` (the normal
|
||||
unless created alone with another value), the cmdhandler
|
||||
will assume True for object-based cmdsets and False for
|
||||
all other. This is usually the most intuitive outcome.
|
||||
|
||||
key_mergetype (dict) - allows the cmdset to define a unique
|
||||
mergetype for particular cmdsets. Format is
|
||||
|
|
@ -144,14 +154,27 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
mergetype = "Union"
|
||||
priority = 0
|
||||
|
||||
# These flags, if set to None, will allow "pass-through" of lower-prio settings
|
||||
# of True/False. If set to True/False, will override lower-prio settings.
|
||||
# These flags, if set to None should be interpreted as 'I don't care' and,
|
||||
# will allow "pass-through" even of lower-prio cmdsets' explicitly True/False
|
||||
# options. If this is set to True/False however, priority matters.
|
||||
no_exits = None
|
||||
no_objs = None
|
||||
no_channels = None
|
||||
# same as above, but if left at None in the final merged set, the
|
||||
# cmdhandler will auto-assume True for Objects and stay False for all
|
||||
# other entities.
|
||||
# The .duplicates setting does not propagate and since duplicates can only happen
|
||||
# on same-prio cmdsets, there is no concept of passthrough on `None`.
|
||||
# The merger of two cmdsets always return in a cmdset with `duplicates=None`
|
||||
# (even if the result may have duplicated commands).
|
||||
# If a final cmdset has `duplicates=None` (normal, unless the cmdset is
|
||||
# created on its own with the flag set), the cmdhandler will auto-assume it to be
|
||||
# True for Object-based cmdsets and stay None/False for all other entities.
|
||||
#
|
||||
# Example:
|
||||
# A and C has .duplicates=True, B has .duplicates=None (or False)
|
||||
# B + A = BA, where BA will have duplicate cmds, but BA.duplicates = None
|
||||
# BA + C = BAC, where BAC will have more duplication, but BAC.duplicates = None
|
||||
#
|
||||
# Basically, for the `.duplicate` setting to survive throughout a
|
||||
# merge-stack, every cmdset in the stack must have `duplicates` set explicitly.
|
||||
duplicates = None
|
||||
|
||||
permanent = False
|
||||
|
|
@ -334,7 +357,19 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
commands (str): Representation of commands in Cmdset.
|
||||
|
||||
"""
|
||||
return ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
|
||||
perm = "perm" if self.permanent else "non-perm"
|
||||
options = ", ".join(
|
||||
[
|
||||
"{}:{}".format(opt, "T" if getattr(self, opt) else "F")
|
||||
for opt in ("no_exits", "no_objs", "no_channels", "duplicates")
|
||||
if getattr(self, opt) is not None
|
||||
]
|
||||
)
|
||||
options = (", " + options) if options else ""
|
||||
return (
|
||||
f"<CmdSet {self.key}, {self.mergetype}, {perm}, prio {self.priority}{options}>: "
|
||||
+ ", ".join([str(cmd) for cmd in sorted(self.commands, key=lambda o: o.key)])
|
||||
)
|
||||
|
||||
def __iter__(self):
|
||||
"""
|
||||
|
|
@ -401,12 +436,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
|
||||
# pass through options whenever they are set, unless the merging or higher-prio
|
||||
# set changes the setting (i.e. has a non-None value). We don't pass through
|
||||
# the duplicates setting; that is per-merge
|
||||
# the duplicates setting; that is per-merge; the resulting .duplicates value
|
||||
# is always None (so merging cmdsets must all have explicit values if wanting
|
||||
# to cause duplicates).
|
||||
cmdset_c.no_channels = (
|
||||
self.no_channels if cmdset_a.no_channels is None else cmdset_a.no_channels
|
||||
)
|
||||
cmdset_c.no_exits = self.no_exits if cmdset_a.no_exits is None else cmdset_a.no_exits
|
||||
cmdset_c.no_objs = self.no_objs if cmdset_a.no_objs is None else cmdset_a.no_objs
|
||||
cmdset_c.duplicates = None
|
||||
|
||||
else:
|
||||
# B higher priority than A
|
||||
|
|
@ -428,12 +466,15 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
|
||||
# pass through options whenever they are set, unless the higher-prio
|
||||
# set changes the setting (i.e. has a non-None value). We don't pass through
|
||||
# the duplicates setting; that is per-merge
|
||||
# the duplicates setting; that is per-merge; the resulting .duplicates value#
|
||||
# is always None (so merging cmdsets must all have explicit values if wanting
|
||||
# to cause duplicates).
|
||||
cmdset_c.no_channels = (
|
||||
cmdset_a.no_channels if self.no_channels is None else self.no_channels
|
||||
)
|
||||
cmdset_c.no_exits = cmdset_a.no_exits if self.no_exits is None else self.no_exits
|
||||
cmdset_c.no_objs = cmdset_a.no_objs if self.no_objs is None else self.no_objs
|
||||
cmdset_c.duplicates = None
|
||||
|
||||
# we store actual_mergetype since key_mergetypes
|
||||
# might be different from the main mergetype.
|
||||
|
|
@ -443,12 +484,12 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
# print "__add__ for %s (prio %i) called with %s (prio %i)." % (self.key, self.priority, cmdset_a.key, cmdset_a.priority)
|
||||
|
||||
# return the system commands to the cmdset
|
||||
cmdset_c.add(sys_commands)
|
||||
cmdset_c.add(sys_commands, allow_duplicates=True)
|
||||
return cmdset_c
|
||||
|
||||
def add(self, cmd):
|
||||
def add(self, cmd, allow_duplicates=False):
|
||||
"""
|
||||
Add a new command or commands to this CmdSetcommand, a list of
|
||||
Add a new command or commands to this CmdSet, a list of
|
||||
commands or a cmdset to this cmdset. Note that this is *not*
|
||||
a merge operation (that is handled by the + operator).
|
||||
|
||||
|
|
@ -456,6 +497,9 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
cmd (Command, list, Cmdset): This allows for adding one or
|
||||
more commands to this Cmdset in one go. If another Cmdset
|
||||
is given, all its commands will be added.
|
||||
allow_duplicates (bool, optional): If set, will not try to remove
|
||||
duplicate cmds in the set. This is needed during the merge process
|
||||
to avoid wiping commands coming from cmdsets with duplicate=True.
|
||||
|
||||
Notes:
|
||||
If cmd already exists in set, it will replace the old one
|
||||
|
|
@ -474,15 +518,16 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
# cmd is a command set so merge all commands in that set
|
||||
# to this one. We raise a visible error if we created
|
||||
# an infinite loop (adding cmdset to itself somehow)
|
||||
cmdset = cmd
|
||||
try:
|
||||
cmd = self._instantiate(cmd)
|
||||
cmdset = self._instantiate(cmdset)
|
||||
except RuntimeError:
|
||||
string = "Adding cmdset %(cmd)s to %(class)s lead to an "
|
||||
string += "infinite loop. When adding a cmdset to another, "
|
||||
string += "make sure they are not themself cyclically added to "
|
||||
string += "the new cmdset somewhere in the chain."
|
||||
raise RuntimeError(_(string) % {"cmd": cmd, "class": self.__class__})
|
||||
cmds = cmd.commands
|
||||
err = ("Adding cmdset {cmdset} to {cls} lead to an "
|
||||
"infinite loop. When adding a cmdset to another, "
|
||||
"make sure they are not themself cyclically added to "
|
||||
"the new cmdset somewhere in the chain.")
|
||||
raise RuntimeError(_(err.format(cmdset=cmdset, cls=self.__class__)))
|
||||
cmds = cmdset.commands
|
||||
elif is_iter(cmd):
|
||||
cmds = [self._instantiate(c) for c in cmd]
|
||||
else:
|
||||
|
|
@ -491,15 +536,17 @@ class CmdSet(object, metaclass=_CmdSetMeta):
|
|||
system_commands = self.system_commands
|
||||
for cmd in cmds:
|
||||
# add all commands
|
||||
if not hasattr(cmd, "obj"):
|
||||
if not hasattr(cmd, "obj") or cmd.obj is None:
|
||||
cmd.obj = self.cmdsetobj
|
||||
try:
|
||||
ic = commands.index(cmd)
|
||||
commands[ic] = cmd # replace
|
||||
except ValueError:
|
||||
commands.append(cmd)
|
||||
# extra run to make sure to avoid doublets
|
||||
self.commands = list(set(commands))
|
||||
self.commands = commands
|
||||
if not allow_duplicates:
|
||||
# extra run to make sure to avoid doublets
|
||||
self.commands = list(set(self.commands))
|
||||
# add system_command to separate list as well,
|
||||
# for quick look-up
|
||||
if cmd.key.startswith("__"):
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@ from evennia.utils import logger, utils
|
|||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.server.models import ServerConfig
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
__all__ = ("import_cmdset", "CmdSetHandler")
|
||||
|
||||
|
|
@ -184,7 +184,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
|||
raise exc.with_traceback(tb)
|
||||
else:
|
||||
# try next suggested path
|
||||
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
|
||||
errstring += _("\n(Unsuccessfully tried '{path}').").format(
|
||||
path=python_path
|
||||
)
|
||||
continue
|
||||
try:
|
||||
cmdsetclass = getattr(module, classname)
|
||||
|
|
@ -194,7 +196,9 @@ def import_cmdset(path, cmdsetobj, emit_to_obj=None, no_logging=False):
|
|||
dum, dum, tb = sys.exc_info()
|
||||
raise exc.with_traceback(tb)
|
||||
else:
|
||||
errstring += _("\n(Unsuccessfully tried '%s')." % python_path)
|
||||
errstring += _("\n(Unsuccessfully tried '{path}').").format(
|
||||
path=python_path
|
||||
)
|
||||
continue
|
||||
_CACHED_CMDSETS[python_path] = cmdsetclass
|
||||
|
||||
|
|
@ -289,7 +293,10 @@ class CmdSetHandler(object):
|
|||
|
||||
# the id of the "merged" current cmdset for easy access.
|
||||
self.key = None
|
||||
# this holds the "merged" current command set
|
||||
# this holds the "merged" current command set. Note that while the .update
|
||||
# method updates this field in order to have it synced when operating on
|
||||
# cmdsets in-code, when the game runs, this field is kept up-to-date by
|
||||
# the cmdsethandler's get_and_merge_cmdsets!
|
||||
self.current = None
|
||||
# this holds a history of CommandSets
|
||||
self.cmdset_stack = [_EmptyCmdSet(cmdsetobj=self.obj)]
|
||||
|
|
@ -307,27 +314,13 @@ class CmdSetHandler(object):
|
|||
Display current commands
|
||||
"""
|
||||
|
||||
string = ""
|
||||
strings = ["<CmdSetHandler> stack:"]
|
||||
mergelist = []
|
||||
if len(self.cmdset_stack) > 1:
|
||||
# We have more than one cmdset in stack; list them all
|
||||
for snum, cmdset in enumerate(self.cmdset_stack):
|
||||
mergetype = self.mergetype_stack[snum]
|
||||
permstring = "non-perm"
|
||||
if cmdset.permanent:
|
||||
permstring = "perm"
|
||||
if mergetype != cmdset.mergetype:
|
||||
mergetype = "%s^" % (mergetype)
|
||||
string += "\n %i: <%s (%s, prio %i, %s)>: %s" % (
|
||||
snum,
|
||||
cmdset.key,
|
||||
mergetype,
|
||||
cmdset.priority,
|
||||
permstring,
|
||||
cmdset,
|
||||
)
|
||||
mergelist.append(str(snum))
|
||||
string += "\n"
|
||||
mergelist.append(str(snum + 1))
|
||||
strings.append(f" {snum + 1}: {cmdset}")
|
||||
|
||||
# Display the currently active cmdset, limited by self.obj's permissions
|
||||
mergetype = self.mergetype_stack[-1]
|
||||
|
|
@ -335,27 +328,15 @@ class CmdSetHandler(object):
|
|||
merged_on = self.cmdset_stack[-2].key
|
||||
mergetype = _("custom {mergetype} on cmdset '{cmdset}'")
|
||||
mergetype = mergetype.format(mergetype=mergetype, cmdset=merged_on)
|
||||
|
||||
if mergelist:
|
||||
tmpstring = _(" <Merged {mergelist} {mergetype}, prio {prio}>: {current}")
|
||||
string += tmpstring.format(
|
||||
mergelist="+".join(mergelist),
|
||||
mergetype=mergetype,
|
||||
prio=self.current.priority,
|
||||
current=self.current,
|
||||
)
|
||||
# current is a result of mergers
|
||||
mergelist = "+".join(mergelist)
|
||||
strings.append(f" <Merged {mergelist}>: {self.current}")
|
||||
else:
|
||||
permstring = "non-perm"
|
||||
if self.current.permanent:
|
||||
permstring = "perm"
|
||||
tmpstring = _(" <{key} ({mergetype}, prio {prio}, {permstring})>:\n {keylist}")
|
||||
string += tmpstring.format(
|
||||
key=self.current.key,
|
||||
mergetype=mergetype,
|
||||
prio=self.current.priority,
|
||||
permstring=permstring,
|
||||
keylist=", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key)),
|
||||
)
|
||||
return string.strip()
|
||||
# current is a single cmdset
|
||||
strings.append(" " + str(self.current))
|
||||
return "\n".join(strings).rstrip()
|
||||
|
||||
def _import_cmdset(self, cmdset_path, emit_to_obj=None):
|
||||
"""
|
||||
|
|
@ -377,12 +358,22 @@ class CmdSetHandler(object):
|
|||
def update(self, init_mode=False):
|
||||
"""
|
||||
Re-adds all sets in the handler to have an updated current
|
||||
set.
|
||||
|
||||
Args:
|
||||
init_mode (bool, optional): Used automatically right after
|
||||
this handler was created; it imports all permanent cmdsets
|
||||
from the database.
|
||||
|
||||
Notes:
|
||||
This method is necessary in order to always have a `.current`
|
||||
cmdset when working with the cmdsethandler in code. But the
|
||||
CmdSetHandler doesn't (cannot) consider external cmdsets and game
|
||||
state. This means that the .current calculated from this method
|
||||
will likely not match the true current cmdset as determined at
|
||||
run-time by `cmdhandler.get_and_merge_cmdsets()`. So in a running
|
||||
game the responsibility of keeping `.current` upt-to-date belongs
|
||||
to the central `cmdhandler.get_and_merge_cmdsets()`!
|
||||
|
||||
"""
|
||||
if init_mode:
|
||||
# reimport all permanent cmdsets
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ All commands in Evennia inherit from the 'Command' class in this module.
|
|||
"""
|
||||
import re
|
||||
import math
|
||||
import inspect
|
||||
|
||||
from django.conf import settings
|
||||
|
||||
|
|
@ -74,6 +75,13 @@ def _init_command(cls, **kwargs):
|
|||
cls.is_exit = False
|
||||
if not hasattr(cls, "help_category"):
|
||||
cls.help_category = "general"
|
||||
# make sure to pick up the parent's docstring if the child class is
|
||||
# missing one (important for auto-help)
|
||||
if cls.__doc__ is None:
|
||||
for parent_class in inspect.getmro(cls):
|
||||
if parent_class.__doc__ is not None:
|
||||
cls.__doc__ = parent_class.__doc__
|
||||
break
|
||||
cls.help_category = cls.help_category.lower()
|
||||
|
||||
|
||||
|
|
@ -130,6 +138,9 @@ class Command(object, metaclass=CommandMeta):
|
|||
arg_regex - (optional) raw string regex defining how the argument part of
|
||||
the command should look in order to match for this command
|
||||
(e.g. must it be a space between cmdname and arg?)
|
||||
auto_help_display_key - (optional) if given, this replaces the string shown
|
||||
in the auto-help listing. This is particularly useful for system-commands
|
||||
whose actual key is not really meaningful.
|
||||
|
||||
(Note that if auto_help is on, this initial string is also used by the
|
||||
system to create the help entry for the command, so it's a good idea to
|
||||
|
|
@ -332,7 +343,7 @@ class Command(object, metaclass=CommandMeta):
|
|||
session (Session, optional): Supply data only to a unique
|
||||
session (ignores the value of `self.msg_all_sessions`).
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
options (dict): Options to the protocol.
|
||||
any (any): All other keywords are interpreted as th
|
||||
name of send-instructions.
|
||||
|
|
@ -358,7 +369,7 @@ class Command(object, metaclass=CommandMeta):
|
|||
obj (Object or Account, optional): Object or Account on which to call the execute_cmd.
|
||||
If not given, self.caller will be used.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Other keyword arguments will be added to the found command
|
||||
object instace as variables before it executes. This is
|
||||
unused by default Evennia but may be used to set flags and
|
||||
|
|
@ -401,12 +412,11 @@ class Command(object, metaclass=CommandMeta):
|
|||
"""
|
||||
pass
|
||||
|
||||
def func(self):
|
||||
def get_command_info(self):
|
||||
"""
|
||||
This is the actual executing part of the command. It is
|
||||
called directly after self.parse(). See the docstring of this
|
||||
module for which object properties are available (beyond those
|
||||
set in self.parse())
|
||||
This is the default output of func() if no func() overload is done.
|
||||
Provided here as a separate method so that it can be called for debugging
|
||||
purposes when making commands.
|
||||
|
||||
"""
|
||||
variables = "\n".join(
|
||||
|
|
@ -416,11 +426,8 @@ class Command(object, metaclass=CommandMeta):
|
|||
Command {self} has no defined `func()` - showing on-command variables:
|
||||
{variables}
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
return
|
||||
|
||||
# a simple test command to show the available properties
|
||||
string = "-" * 50
|
||||
string += "-" * 50
|
||||
string += "\n|w%s|n - Command variables from evennia:\n" % self.key
|
||||
string += "-" * 50
|
||||
string += "\nname of cmd (self.key): |w%s|n\n" % self.key
|
||||
|
|
@ -438,6 +445,16 @@ Command {self} has no defined `func()` - showing on-command variables:
|
|||
|
||||
self.caller.msg(string)
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is the actual executing part of the command. It is
|
||||
called directly after self.parse(). See the docstring of this
|
||||
module for which object properties are available (beyond those
|
||||
set in self.parse())
|
||||
|
||||
"""
|
||||
self.get_command_info()
|
||||
|
||||
def get_extra_info(self, caller, **kwargs):
|
||||
"""
|
||||
Display some extra information that may help distinguish this
|
||||
|
|
@ -484,12 +501,28 @@ Command {self} has no defined `func()` - showing on-command variables:
|
|||
Get the client screenwidth for the session using this command.
|
||||
|
||||
Returns:
|
||||
client width (int or None): The width (in characters) of the client window. None
|
||||
if this command is run without a Session (such as by an NPC).
|
||||
client width (int): The width (in characters) of the client window.
|
||||
|
||||
"""
|
||||
if self.session:
|
||||
return self.session.protocol_flags["SCREENWIDTH"][0]
|
||||
return self.session.protocol_flags.get(
|
||||
"SCREENWIDTH", {0: settings.CLIENT_DEFAULT_WIDTH}
|
||||
)[0]
|
||||
return settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
def client_height(self):
|
||||
"""
|
||||
Get the client screenheight for the session using this command.
|
||||
|
||||
Returns:
|
||||
client height (int): The height (in characters) of the client window.
|
||||
|
||||
"""
|
||||
if self.session:
|
||||
return self.session.protocol_flags.get(
|
||||
"SCREENHEIGHT", {0: settings.CLIENT_DEFAULT_HEIGHT}
|
||||
)[0]
|
||||
return settings.CLIENT_DEFAULT_HEIGHT
|
||||
|
||||
def styled_table(self, *args, **kwargs):
|
||||
"""
|
||||
|
|
@ -498,7 +531,7 @@ Command {self} has no defined `func()` - showing on-command variables:
|
|||
Args:
|
||||
*args (str): Column headers. If not colored explicitly, these will get colors
|
||||
from user options.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
any (str, int or dict): EvTable options, including, optionally a `table` dict
|
||||
detailing the contents of the table.
|
||||
Returns:
|
||||
|
|
@ -551,7 +584,7 @@ Command {self} has no defined `func()` - showing on-command variables:
|
|||
"""
|
||||
Helper for formatting a string into a pretty display, for a header, separator or footer.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
header_text (str): Text to include in header.
|
||||
fill_character (str): This single character will be used to fill the width of the
|
||||
display.
|
||||
|
|
|
|||
|
|
@ -154,7 +154,8 @@ class CmdCharCreate(COMMAND_DEFAULT_CLASS):
|
|||
if not account.is_superuser and (
|
||||
account.db._playable_characters and len(account.db._playable_characters) >= charmax
|
||||
):
|
||||
self.msg("You may only create a maximum of %i characters." % charmax)
|
||||
plural = "" if charmax == 1 else "s"
|
||||
self.msg(f"You may only create a maximum of {charmax} character{plural}.")
|
||||
return
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
||||
|
|
@ -300,27 +301,68 @@ class CmdIC(COMMAND_DEFAULT_CLASS):
|
|||
session = self.session
|
||||
|
||||
new_character = None
|
||||
character_candidates = []
|
||||
|
||||
if not self.args:
|
||||
new_character = account.db._last_puppet
|
||||
if not new_character:
|
||||
character_candidates = [account.db._last_puppet] if account.db._last_puppet else []
|
||||
if not character_candidates:
|
||||
self.msg("Usage: ic <character>")
|
||||
return
|
||||
if not new_character:
|
||||
# search for a matching character
|
||||
new_character = [
|
||||
char for char in search.object_search(self.args) if char.access(account, "puppet")
|
||||
]
|
||||
if not new_character:
|
||||
self.msg("That is not a valid character choice.")
|
||||
return
|
||||
if len(new_character) > 1:
|
||||
self.msg(
|
||||
"Multiple targets with the same name:\n %s"
|
||||
% ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in new_character)
|
||||
else:
|
||||
# argument given
|
||||
|
||||
if account.db._playable_characters:
|
||||
# look at the playable_characters list first
|
||||
character_candidates.extend(
|
||||
account.search(
|
||||
self.args,
|
||||
candidates=account.db._playable_characters,
|
||||
search_object=True,
|
||||
quiet=True,
|
||||
)
|
||||
)
|
||||
return
|
||||
else:
|
||||
new_character = new_character[0]
|
||||
|
||||
if account.locks.check_lockstring(account, "perm(Builder)"):
|
||||
# builders and higher should be able to puppet more than their
|
||||
# playable characters.
|
||||
if session.puppet:
|
||||
# start by local search - this helps to avoid the user
|
||||
# getting locked into their playable characters should one
|
||||
# happen to be named the same as another. We replace the suggestion
|
||||
# from playable_characters here - this allows builders to puppet objects
|
||||
# with the same name as their playable chars should it be necessary
|
||||
# (by going to the same location).
|
||||
character_candidates = [
|
||||
char
|
||||
for char in session.puppet.search(self.args, quiet=True)
|
||||
if char.access(account, "puppet")
|
||||
]
|
||||
if not character_candidates:
|
||||
# fall back to global search only if Builder+ has no
|
||||
# playable_characers in list and is not standing in a room
|
||||
# with a matching char.
|
||||
character_candidates.extend(
|
||||
[
|
||||
char
|
||||
for char in search.object_search(self.args)
|
||||
if char.access(account, "puppet")
|
||||
]
|
||||
)
|
||||
|
||||
# handle possible candidates
|
||||
if not character_candidates:
|
||||
self.msg("That is not a valid character choice.")
|
||||
return
|
||||
if len(character_candidates) > 1:
|
||||
self.msg(
|
||||
"Multiple targets with the same name:\n %s"
|
||||
% ", ".join("%s(#%s)" % (obj.key, obj.id) for obj in character_candidates)
|
||||
)
|
||||
return
|
||||
else:
|
||||
new_character = character_candidates[0]
|
||||
|
||||
# do the puppet puppet
|
||||
try:
|
||||
account.puppet_object(session, new_character)
|
||||
account.db._last_puppet = new_character
|
||||
|
|
@ -957,7 +999,7 @@ class CmdQuell(COMMAND_DEFAULT_CLASS):
|
|||
self.msg("Already quelling Account %s permissions." % permstr)
|
||||
return
|
||||
account.attributes.add("_quell", True)
|
||||
puppet = self.session.puppet
|
||||
puppet = self.session.puppet if self.session else None
|
||||
if puppet:
|
||||
cpermstr = "(%s)" % ", ".join(puppet.permissions.all())
|
||||
cpermstr = "Quelling to current puppet's permissions %s." % cpermstr
|
||||
|
|
|
|||
|
|
@ -50,35 +50,6 @@ _UTF8_ERROR = """
|
|||
Error reported was: '%s'
|
||||
"""
|
||||
|
||||
_PROCPOOL_BATCHCMD_SOURCE = """
|
||||
from evennia.commands.default.batchprocess import batch_cmd_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = commands
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_commands"
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
for inum in range(len(commands)):
|
||||
print "command:", inum
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
if not batch_cmd_exec(caller):
|
||||
break
|
||||
step_pointer(caller, 1)
|
||||
print "leaving run ..."
|
||||
"""
|
||||
_PROCPOOL_BATCHCODE_SOURCE = """
|
||||
from evennia.commands.default.batchprocess import batch_code_exec, step_pointer, BatchSafeCmdSet
|
||||
caller.ndb.batch_stack = codes
|
||||
caller.ndb.batch_stackptr = 0
|
||||
caller.ndb.batch_batchmode = "batch_code"
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
for inum in range(len(codes)):
|
||||
print "code:", inum
|
||||
caller.cmdset.add(BatchSafeCmdSet)
|
||||
if not batch_code_exec(caller):
|
||||
break
|
||||
step_pointer(caller, 1)
|
||||
print "leaving run ..."
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Helper functions
|
||||
|
|
@ -300,42 +271,17 @@ class CmdBatchCommands(_COMMAND_DEFAULT_CLASS):
|
|||
"for %s (this might take some time) ..." % python_path
|
||||
)
|
||||
|
||||
procpool = False
|
||||
if "PythonProcPool" in utils.server_services():
|
||||
if utils.uses_database("sqlite3"):
|
||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
||||
else:
|
||||
procpool = True
|
||||
|
||||
if procpool:
|
||||
# run in parallel process
|
||||
def callback(r):
|
||||
caller.msg(" |GBatchfile '%s' applied." % python_path)
|
||||
purge_processor(caller)
|
||||
|
||||
def errback(e):
|
||||
caller.msg(" |RError from processor: '%s'" % e)
|
||||
purge_processor(caller)
|
||||
|
||||
utils.run_async(
|
||||
_PROCPOOL_BATCHCMD_SOURCE,
|
||||
commands=commands,
|
||||
caller=caller,
|
||||
at_return=callback,
|
||||
at_err=errback,
|
||||
)
|
||||
else:
|
||||
# run in-process (might block)
|
||||
for _ in range(len(commands)):
|
||||
# loop through the batch file
|
||||
if not batch_cmd_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("|G%s" % string)
|
||||
purge_processor(caller)
|
||||
# run in-process (might block)
|
||||
for _ in range(len(commands)):
|
||||
# loop through the batch file
|
||||
if not batch_cmd_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("|G%s" % string)
|
||||
purge_processor(caller)
|
||||
|
||||
|
||||
class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -420,41 +366,16 @@ class CmdBatchCode(_COMMAND_DEFAULT_CLASS):
|
|||
else:
|
||||
caller.msg("Running Batch-code processor - Automatic mode for %s ..." % python_path)
|
||||
|
||||
procpool = False
|
||||
if "PythonProcPool" in utils.server_services():
|
||||
if utils.uses_database("sqlite3"):
|
||||
caller.msg("Batchprocessor disabled ProcPool under SQLite3.")
|
||||
else:
|
||||
procpool = True
|
||||
if procpool:
|
||||
# run in parallel process
|
||||
def callback(r):
|
||||
caller.msg(" |GBatchfile '%s' applied." % python_path)
|
||||
purge_processor(caller)
|
||||
|
||||
def errback(e):
|
||||
caller.msg(" |RError from processor: '%s'" % e)
|
||||
purge_processor(caller)
|
||||
|
||||
utils.run_async(
|
||||
_PROCPOOL_BATCHCODE_SOURCE,
|
||||
codes=codes,
|
||||
caller=caller,
|
||||
at_return=callback,
|
||||
at_err=errback,
|
||||
)
|
||||
else:
|
||||
# un in-process (will block)
|
||||
for _ in range(len(codes)):
|
||||
# loop through the batch file
|
||||
if not batch_code_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("|G%s" % string)
|
||||
purge_processor(caller)
|
||||
for _ in range(len(codes)):
|
||||
# loop through the batch file
|
||||
if not batch_code_exec(caller):
|
||||
return
|
||||
step_pointer(caller, 1)
|
||||
# clean out the safety cmdset and clean out all other
|
||||
# temporary attrs.
|
||||
string = " Batchfile '%s' applied." % python_path
|
||||
caller.msg("|G%s" % string)
|
||||
purge_processor(caller)
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
|
|
|||
File diff suppressed because it is too large
Load diff
|
|
@ -37,7 +37,9 @@ __all__ = (
|
|||
"CmdCdesc",
|
||||
"CmdPage",
|
||||
"CmdIRC2Chan",
|
||||
"CmdIRCStatus",
|
||||
"CmdRSS2Chan",
|
||||
"CmdGrapevine2Chan",
|
||||
)
|
||||
_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
|
||||
|
|
@ -806,17 +808,48 @@ class CmdPage(COMMAND_DEFAULT_CLASS):
|
|||
lastpages = pages[-number:]
|
||||
else:
|
||||
lastpages = pages
|
||||
template = "|w%s|n |c%s|n to |c%s|n: %s"
|
||||
lastpages = "\n ".join(
|
||||
template
|
||||
% (
|
||||
utils.datetime_format(page.date_created),
|
||||
",".join(obj.key for obj in page.senders),
|
||||
"|n,|c ".join([obj.name for obj in page.receivers]),
|
||||
page.message,
|
||||
to_template = "|w{date}{clr} {sender}|nto{clr}{receiver}|n:> {message}"
|
||||
from_template = "|w{date}{clr} {receiver}|nfrom{clr}{sender}|n:< {message}"
|
||||
listing = []
|
||||
prev_selfsend = False
|
||||
for page in lastpages:
|
||||
multi_send = len(page.senders) > 1
|
||||
multi_recv = len(page.receivers) > 1
|
||||
sending = self.caller in page.senders
|
||||
# self-messages all look like sends, so we assume they always
|
||||
# come in close pairs and treat the second of the pair as the recv.
|
||||
selfsend = sending and self.caller in page.receivers
|
||||
if selfsend:
|
||||
if prev_selfsend:
|
||||
# this is actually a receive of a self-message
|
||||
sending = False
|
||||
prev_selfsend = False
|
||||
else:
|
||||
prev_selfsend = True
|
||||
|
||||
clr = "|c" if sending else "|g"
|
||||
|
||||
sender = f"|n,{clr}".join(obj.key for obj in page.senders)
|
||||
receiver = f"|n,{clr}".join([obj.name for obj in page.receivers])
|
||||
if sending:
|
||||
template = to_template
|
||||
sender = f"{sender} " if multi_send else ""
|
||||
receiver = f" {receiver}" if multi_recv else f" {receiver}"
|
||||
else:
|
||||
template = from_template
|
||||
receiver = f"{receiver} " if multi_recv else ""
|
||||
sender = f" {sender} " if multi_send else f" {sender}"
|
||||
|
||||
listing.append(
|
||||
template.format(
|
||||
date=utils.datetime_format(page.date_created),
|
||||
clr=clr,
|
||||
sender=sender,
|
||||
receiver=receiver,
|
||||
message=page.message,
|
||||
)
|
||||
)
|
||||
for page in lastpages
|
||||
)
|
||||
lastpages = "\n ".join(listing)
|
||||
|
||||
if lastpages:
|
||||
string = "Your latest pages:\n %s" % lastpages
|
||||
|
|
|
|||
|
|
@ -378,10 +378,15 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
|
|||
if not items:
|
||||
string = "You are not carrying anything."
|
||||
else:
|
||||
from evennia.utils.ansi import raw as raw_ansi
|
||||
|
||||
table = self.styled_table(border="header")
|
||||
for item in items:
|
||||
table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
||||
string = "|wYou are carrying:\n%s" % table
|
||||
table.add_row(
|
||||
f"|C{item.name}|n",
|
||||
"{}|n".format(utils.crop(raw_ansi(item.db.desc), width=50) or ""),
|
||||
)
|
||||
string = f"|wYou are carrying:\n{table}"
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -426,11 +431,16 @@ class CmdGet(COMMAND_DEFAULT_CLASS):
|
|||
if not obj.at_before_get(caller):
|
||||
return
|
||||
|
||||
obj.move_to(caller, quiet=True)
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents("%s picks up %s." % (caller.name, obj.name), exclude=caller)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
success = obj.move_to(caller, quiet=True)
|
||||
if not success:
|
||||
caller.msg("This can't be picked up.")
|
||||
else:
|
||||
caller.msg("You pick up %s." % obj.name)
|
||||
caller.location.msg_contents(
|
||||
"%s picks up %s." % (caller.name, obj.name), exclude=caller
|
||||
)
|
||||
# calling at_get hook method
|
||||
obj.at_get(caller)
|
||||
|
||||
|
||||
class CmdDrop(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -471,11 +481,14 @@ class CmdDrop(COMMAND_DEFAULT_CLASS):
|
|||
if not obj.at_before_drop(caller):
|
||||
return
|
||||
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." % (caller.name, obj.name), exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
success = obj.move_to(caller.location, quiet=True)
|
||||
if not success:
|
||||
caller.msg("This couldn't be dropped.")
|
||||
else:
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." % (caller.name, obj.name), exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
|
||||
class CmdGive(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -522,11 +535,14 @@ class CmdGive(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
|
||||
# give object
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
to_give.move_to(target, quiet=True)
|
||||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
# Call the object script's at_give() method.
|
||||
to_give.at_give(caller, target)
|
||||
success = to_give.move_to(target, quiet=True)
|
||||
if not success:
|
||||
caller.msg("This could not be given.")
|
||||
else:
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
# Call the object script's at_give() method.
|
||||
to_give.at_give(caller, target)
|
||||
|
||||
|
||||
class CmdSetDesc(COMMAND_DEFAULT_CLASS):
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
|||
_SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n"
|
||||
|
||||
|
||||
class CmdHelp(Command):
|
||||
class CmdHelp(COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
View help or a list of topics
|
||||
|
||||
|
|
@ -79,7 +79,7 @@ class CmdHelp(Command):
|
|||
evmore.msg(self.caller, text, session=self.session)
|
||||
return
|
||||
|
||||
self.msg((text, {"type": "help"}))
|
||||
self.msg(text=(text, {"type": "help"}))
|
||||
|
||||
@staticmethod
|
||||
def format_help_entry(title, help_text, aliases=None, suggested=None):
|
||||
|
|
@ -175,7 +175,7 @@ class CmdHelp(Command):
|
|||
False: the command shouldn't appear in the table.
|
||||
|
||||
"""
|
||||
return True
|
||||
return cmd.access(caller, "view", default=True)
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
|
|
@ -219,10 +219,15 @@ class CmdHelp(Command):
|
|||
hdict_topic = defaultdict(list)
|
||||
# create the dictionaries {category:[topic, topic ...]} required by format_help_list
|
||||
# Filter commands that should be reached by the help
|
||||
# system, but not be displayed in the table.
|
||||
# system, but not be displayed in the table, or be displayed differently.
|
||||
for cmd in all_cmds:
|
||||
if self.should_list_cmd(cmd, caller):
|
||||
hdict_cmd[cmd.help_category].append(cmd.key)
|
||||
key = (
|
||||
cmd.auto_help_display_key
|
||||
if hasattr(cmd, "auto_help_display_key")
|
||||
else cmd.key
|
||||
)
|
||||
hdict_cmd[cmd.help_category].append(key)
|
||||
[hdict_topic[topic.help_category].append(topic.key) for topic in all_topics]
|
||||
# report back
|
||||
self.msg_help(self.format_help_list(hdict_cmd, hdict_topic))
|
||||
|
|
@ -266,11 +271,10 @@ class CmdHelp(Command):
|
|||
]
|
||||
|
||||
if len(match) == 1:
|
||||
cmd = match[0]
|
||||
key = cmd.auto_help_display_key if hasattr(cmd, "auto_help_display_key") else cmd.key
|
||||
formatted = self.format_help_entry(
|
||||
match[0].key,
|
||||
match[0].get_help(caller, cmdset),
|
||||
aliases=match[0].aliases,
|
||||
suggested=suggestions,
|
||||
key, cmd.get_help(caller, cmdset), aliases=cmd.aliases, suggested=suggestions,
|
||||
)
|
||||
self.msg_help(formatted)
|
||||
return
|
||||
|
|
@ -291,7 +295,15 @@ class CmdHelp(Command):
|
|||
if query in all_categories:
|
||||
self.msg_help(
|
||||
self.format_help_list(
|
||||
{query: [cmd.key for cmd in all_cmds if cmd.help_category == query]},
|
||||
{
|
||||
query: [
|
||||
cmd.auto_help_display_key
|
||||
if hasattr(cmd, "auto_help_display_key")
|
||||
else cmd.key
|
||||
for cmd in all_cmds
|
||||
if cmd.help_category == query
|
||||
]
|
||||
},
|
||||
{query: [topic.key for topic in all_topics if topic.help_category == query]},
|
||||
)
|
||||
)
|
||||
|
|
@ -376,7 +388,7 @@ class CmdSetHelp(COMMAND_DEFAULT_CLASS):
|
|||
self.msg("You have to define a topic!")
|
||||
return
|
||||
topicstrlist = topicstr.split(";")
|
||||
topicstr, aliases = topicstrlist[0], topicstrlist[1:] if len(topicstr) > 1 else []
|
||||
topicstr, aliases = (topicstrlist[0], topicstrlist[1:] if len(topicstr) > 1 else [])
|
||||
aliastxt = ("(aliases: %s)" % ", ".join(aliases)) if aliases else ""
|
||||
old_entry = None
|
||||
|
||||
|
|
|
|||
|
|
@ -202,11 +202,9 @@ class MuxCommand(Command):
|
|||
else:
|
||||
self.character = None
|
||||
|
||||
def func(self):
|
||||
def get_command_info(self):
|
||||
"""
|
||||
This is the hook function that actually does all the work. It is called
|
||||
by the cmdhandler right after self.parser() finishes, and so has access
|
||||
to all the variables defined therein.
|
||||
Update of parent class's get_command_info() for MuxCommand.
|
||||
"""
|
||||
variables = "\n".join(
|
||||
" |w{}|n ({}): {}".format(key, type(val), val) for key, val in self.__dict__.items()
|
||||
|
|
@ -245,6 +243,14 @@ Command {self} has no defined `func()` - showing on-command variables: No child
|
|||
string += "-" * 50
|
||||
self.caller.msg(string)
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is the hook function that actually does all the work. It is called
|
||||
by the cmdhandler right after self.parser() finishes, and so has access
|
||||
to all the variables defined therein.
|
||||
"""
|
||||
self.get_command_info()
|
||||
|
||||
|
||||
class MuxAccountCommand(MuxCommand):
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import twisted
|
|||
import time
|
||||
|
||||
from django.conf import settings
|
||||
from django.core.paginator import Paginator
|
||||
from evennia.server.sessionhandler import SESSIONS
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
|
|
@ -23,6 +24,7 @@ from evennia.accounts.models import AccountDB
|
|||
from evennia.utils import logger, utils, gametime, create, search
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.utils import crop, class_from_module
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
|
@ -232,6 +234,10 @@ def _run_code_snippet(
|
|||
|
||||
if ret is None:
|
||||
return
|
||||
elif isinstance(ret, tuple):
|
||||
# we must convert here to allow msg to pass it (a tuple is confused
|
||||
# with a outputfunc structure)
|
||||
ret = str(ret)
|
||||
|
||||
for session in sessions:
|
||||
try:
|
||||
|
|
@ -284,8 +290,6 @@ class EvenniaPythonConsole(code.InteractiveConsole):
|
|||
result = None
|
||||
try:
|
||||
result = super().push(line)
|
||||
except SystemExit:
|
||||
pass
|
||||
finally:
|
||||
sys.stdout = old_stdout
|
||||
sys.stderr = old_stderr
|
||||
|
|
@ -301,6 +305,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
py/edit
|
||||
py/time <cmd>
|
||||
py/clientraw <cmd>
|
||||
py/noecho
|
||||
|
||||
Switches:
|
||||
time - output an approximate execution time for <cmd>
|
||||
|
|
@ -308,6 +313,8 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
clientraw - turn off all client-specific escaping. Note that this may
|
||||
lead to different output depending on prototocol (such as angular brackets
|
||||
being parsed as HTML in the webclient but not in telnet clients)
|
||||
noecho - in Python console mode, turn off the input echo (e.g. if your client
|
||||
does this for you already)
|
||||
|
||||
Without argument, open a Python console in-game. This is a full console,
|
||||
accepting multi-line Python code for testing and debugging. Type `exit()` to
|
||||
|
|
@ -339,7 +346,7 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
|
||||
key = "py"
|
||||
aliases = ["!"]
|
||||
switch_options = ("time", "edit", "clientraw")
|
||||
switch_options = ("time", "edit", "clientraw", "noecho")
|
||||
locks = "cmd:perm(py) or perm(Developer)"
|
||||
help_category = "System"
|
||||
|
||||
|
|
@ -349,6 +356,8 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
caller = self.caller
|
||||
pycode = self.args
|
||||
|
||||
noecho = "noecho" in self.switches
|
||||
|
||||
if "edit" in self.switches:
|
||||
caller.db._py_measure_time = "time" in self.switches
|
||||
caller.db._py_clientraw = "clientraw" in self.switches
|
||||
|
|
@ -367,15 +376,26 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
# Run in interactive mode
|
||||
console = EvenniaPythonConsole(self.caller)
|
||||
banner = (
|
||||
f"|gPython {sys.version} on {sys.platform}\n"
|
||||
"Evennia interactive console mode - type 'exit()' to leave.|n"
|
||||
"|gEvennia Interactive Python mode{echomode}\n"
|
||||
"Python {version} on {platform}".format(
|
||||
echomode=" (no echoing of prompts)" if noecho else "",
|
||||
version=sys.version,
|
||||
platform=sys.platform,
|
||||
)
|
||||
)
|
||||
self.msg(banner)
|
||||
line = ""
|
||||
prompt = ">>>"
|
||||
main_prompt = "|x[py mode - quit() to exit]|n"
|
||||
prompt = main_prompt
|
||||
while line.lower() not in ("exit", "exit()"):
|
||||
line = yield (prompt)
|
||||
prompt = "..." if console.push(line) else ">>>"
|
||||
try:
|
||||
line = yield (prompt)
|
||||
if noecho:
|
||||
prompt = "..." if console.push(line) else main_prompt
|
||||
else:
|
||||
prompt = line if console.push(line) else f"{line}\n{main_prompt}"
|
||||
except SystemExit:
|
||||
break
|
||||
self.msg("|gClosing the Python console.|n")
|
||||
return
|
||||
|
||||
|
|
@ -387,53 +407,71 @@ class CmdPy(COMMAND_DEFAULT_CLASS):
|
|||
)
|
||||
|
||||
|
||||
# helper function. Kept outside so it can be imported and run
|
||||
# by other commands.
|
||||
class ScriptEvMore(EvMore):
|
||||
"""
|
||||
Listing 1000+ Scripts can be very slow and memory-consuming. So
|
||||
we use this custom EvMore child to build en EvTable only for
|
||||
each page of the list.
|
||||
|
||||
"""
|
||||
|
||||
def format_script_list(scripts):
|
||||
"""Takes a list of scripts and formats the output."""
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
def init_pages(self, scripts):
|
||||
"""Prepare the script list pagination"""
|
||||
script_pages = Paginator(scripts, max(1, int(self.height / 2)))
|
||||
super().init_pages(script_pages)
|
||||
|
||||
table = EvTable(
|
||||
"|wdbref|n",
|
||||
"|wobj|n",
|
||||
"|wkey|n",
|
||||
"|wintval|n",
|
||||
"|wnext|n",
|
||||
"|wrept|n",
|
||||
"|wdb",
|
||||
"|wtypeclass|n",
|
||||
"|wdesc|n",
|
||||
align="r",
|
||||
border="tablecols",
|
||||
)
|
||||
for script in scripts:
|
||||
nextrep = script.time_until_next_repeat()
|
||||
if nextrep is None:
|
||||
nextrep = "PAUS" if script.db._paused_time else "--"
|
||||
else:
|
||||
nextrep = "%ss" % nextrep
|
||||
def page_formatter(self, scripts):
|
||||
"""Takes a page of scripts and formats the output
|
||||
into an EvTable."""
|
||||
|
||||
maxrepeat = script.repeats
|
||||
if maxrepeat:
|
||||
rept = "%i/%i" % (maxrepeat - script.remaining_repeats(), maxrepeat)
|
||||
else:
|
||||
rept = "-/-"
|
||||
if not scripts:
|
||||
return "<No scripts>"
|
||||
|
||||
table.add_row(
|
||||
script.id,
|
||||
script.obj.key if (hasattr(script, "obj") and script.obj) else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
"*" if script.persistent else "-",
|
||||
script.typeclass_path.rsplit(".", 1)[-1],
|
||||
crop(script.desc, width=20),
|
||||
table = EvTable(
|
||||
"|wdbref|n",
|
||||
"|wobj|n",
|
||||
"|wkey|n",
|
||||
"|wintval|n",
|
||||
"|wnext|n",
|
||||
"|wrept|n",
|
||||
"|wdb",
|
||||
"|wtypeclass|n",
|
||||
"|wdesc|n",
|
||||
align="r",
|
||||
border="tablecols",
|
||||
width=self.width,
|
||||
)
|
||||
return "%s" % table
|
||||
|
||||
for script in scripts:
|
||||
|
||||
nextrep = script.time_until_next_repeat()
|
||||
if nextrep is None:
|
||||
nextrep = "PAUSED" if script.db._paused_time else "--"
|
||||
else:
|
||||
nextrep = "%ss" % nextrep
|
||||
|
||||
maxrepeat = script.repeats
|
||||
remaining = script.remaining_repeats() or 0
|
||||
if maxrepeat:
|
||||
rept = "%i/%i" % (maxrepeat - remaining, maxrepeat)
|
||||
else:
|
||||
rept = "-/-"
|
||||
|
||||
table.add_row(
|
||||
script.id,
|
||||
f"{script.obj.key}({script.obj.dbref})"
|
||||
if (hasattr(script, "obj") and script.obj)
|
||||
else "<Global>",
|
||||
script.key,
|
||||
script.interval if script.interval > 0 else "--",
|
||||
nextrep,
|
||||
rept,
|
||||
"*" if script.persistent else "-",
|
||||
script.typeclass_path.rsplit(".", 1)[-1],
|
||||
crop(script.desc, width=20),
|
||||
)
|
||||
|
||||
return str(table)
|
||||
|
||||
|
||||
class CmdScripts(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -464,6 +502,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
locks = "cmd:perm(listscripts) or perm(Admin)"
|
||||
help_category = "System"
|
||||
|
||||
excluded_typeclass_paths = ["evennia.prototypes.prototypes.DbPrototype"]
|
||||
|
||||
def func(self):
|
||||
"""implement method"""
|
||||
|
||||
|
|
@ -496,6 +536,8 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
if not scripts:
|
||||
caller.msg("No scripts are running.")
|
||||
return
|
||||
# filter any found scripts by tag category.
|
||||
scripts = scripts.exclude(db_typeclass_path__in=self.excluded_typeclass_paths)
|
||||
|
||||
if not scripts:
|
||||
string = "No scripts found with a key '%s', or on an object named '%s'." % (args, args)
|
||||
|
|
@ -515,19 +557,20 @@ class CmdScripts(COMMAND_DEFAULT_CLASS):
|
|||
# import pdb # DEBUG
|
||||
# pdb.set_trace() # DEBUG
|
||||
ScriptDB.objects.validate() # just to be sure all is synced
|
||||
caller.msg(string)
|
||||
else:
|
||||
# multiple matches.
|
||||
string = "Multiple script matches. Please refine your search:\n"
|
||||
string += format_script_list(scripts)
|
||||
ScriptEvMore(caller, scripts, session=self.session)
|
||||
caller.msg("Multiple script matches. Please refine your search")
|
||||
elif self.switches and self.switches[0] in ("validate", "valid", "val"):
|
||||
# run validation on all found scripts
|
||||
nr_started, nr_stopped = ScriptDB.objects.validate(scripts=scripts)
|
||||
string = "Validated %s scripts. " % ScriptDB.objects.all().count()
|
||||
string += "Started %s and stopped %s scripts." % (nr_started, nr_stopped)
|
||||
caller.msg(string)
|
||||
else:
|
||||
# No stopping or validation. We just want to view things.
|
||||
string = format_script_list(scripts)
|
||||
caller.msg(string)
|
||||
ScriptEvMore(caller, scripts.order_by("id"), session=self.session)
|
||||
|
||||
|
||||
class CmdObjects(COMMAND_DEFAULT_CLASS):
|
||||
|
|
@ -592,9 +635,13 @@ class CmdObjects(COMMAND_DEFAULT_CLASS):
|
|||
"|wtypeclass|n", "|wcount|n", "|w%|n", border="table", align="l"
|
||||
)
|
||||
typetable.align = "l"
|
||||
dbtotals = ObjectDB.objects.object_totals()
|
||||
for path, count in dbtotals.items():
|
||||
typetable.add_row(path, count, "%.2f" % ((float(count) / nobjs) * 100))
|
||||
dbtotals = ObjectDB.objects.get_typeclass_totals()
|
||||
for stat in dbtotals:
|
||||
typetable.add_row(
|
||||
stat.get("typeclass", "<error>"),
|
||||
stat.get("count", -1),
|
||||
"%.2f" % stat.get("percent", -1),
|
||||
)
|
||||
|
||||
# last N table
|
||||
objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim) :]
|
||||
|
|
@ -793,21 +840,30 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
return
|
||||
if service.name[:7] == "Evennia":
|
||||
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
|
||||
string = "You seem to be shutting down a core Evennia service (named 'Evennia***'). Note that"
|
||||
string += "stopping some TCP port services will *not* disconnect users *already*"
|
||||
string += "connected on those ports, but *may* instead cause spurious errors for them. To "
|
||||
string += "safely and permanently remove ports, change settings file and restart the server."
|
||||
string = ("|RYou seem to be shutting down a core Evennia "
|
||||
"service (named 'Evennia*').\nNote that stopping "
|
||||
"some TCP port services will *not* disconnect users "
|
||||
"*already* connected on those ports, but *may* "
|
||||
"instead cause spurious errors for them.\nTo safely "
|
||||
"and permanently remove ports, change settings file "
|
||||
"and restart the server.|n\n")
|
||||
caller.msg(string)
|
||||
|
||||
if delmode:
|
||||
service.stopService()
|
||||
service_collection.removeService(service)
|
||||
caller.msg("Stopped and removed service '%s'." % self.args)
|
||||
caller.msg("|gStopped and removed service '%s'.|n" % self.args)
|
||||
else:
|
||||
service.stopService()
|
||||
caller.msg("Stopped service '%s'." % self.args)
|
||||
caller.msg(f"Stopping service '{self.args}'...")
|
||||
try:
|
||||
service.stopService()
|
||||
except Exception as err:
|
||||
caller.msg(f"|rErrors were reported when stopping this service{err}.\n"
|
||||
"If there are remaining problems, try reloading "
|
||||
"or rebooting the server.")
|
||||
caller.msg("|g... Stopped service '%s'.|n" % self.args)
|
||||
return
|
||||
|
||||
if switches[0] == "start":
|
||||
|
|
@ -815,8 +871,14 @@ class CmdService(COMMAND_DEFAULT_CLASS):
|
|||
if service.running:
|
||||
caller.msg("That service is already running.")
|
||||
return
|
||||
caller.msg("Starting service '%s'." % self.args)
|
||||
service.startService()
|
||||
caller.msg(f"Starting service '{self.args}' ...")
|
||||
try:
|
||||
service.startService()
|
||||
except Exception as err:
|
||||
caller.msg(f"|rErrors were reported when starting this service{err}.\n"
|
||||
"If there are remaining problems, try reloading the server, changing the "
|
||||
"settings if it's a non-standard service.|n")
|
||||
caller.msg("|gService started.|n")
|
||||
|
||||
|
||||
class CmdAbout(COMMAND_DEFAULT_CLASS):
|
||||
|
|
|
|||
|
|
@ -17,9 +17,9 @@ import datetime
|
|||
from anything import Anything
|
||||
|
||||
from django.conf import settings
|
||||
from mock import Mock, mock
|
||||
from unittest.mock import patch, Mock, MagicMock
|
||||
|
||||
from evennia import DefaultRoom, DefaultExit
|
||||
from evennia import DefaultRoom, DefaultExit, ObjectDB
|
||||
from evennia.commands.default.cmdset_character import CharacterCmdSet
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.commands.default import (
|
||||
|
|
@ -56,6 +56,7 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--+|\|(?:\s|$)", re.MULTILINE)
|
|||
# ------------------------------------------------------------
|
||||
|
||||
|
||||
@patch("evennia.server.portal.portal.LoopingCall", new=MagicMock())
|
||||
class CommandTest(EvenniaTest):
|
||||
"""
|
||||
Tests a command
|
||||
|
|
@ -149,11 +150,18 @@ class CommandTest(EvenniaTest):
|
|||
returned_msg = msg_sep.join(
|
||||
_RE.sub("", ansi.parse_ansi(mess, strip_ansi=noansi)) for mess in stored_msg
|
||||
).strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg.strip()):
|
||||
msg = msg.strip()
|
||||
if msg == "" and returned_msg or not returned_msg.startswith(msg):
|
||||
prt = ""
|
||||
for ic, char in enumerate(msg):
|
||||
import re
|
||||
|
||||
prt += char
|
||||
|
||||
sep1 = "\n" + "=" * 30 + "Wanted message" + "=" * 34 + "\n"
|
||||
sep2 = "\n" + "=" * 30 + "Returned message" + "=" * 32 + "\n"
|
||||
sep3 = "\n" + "=" * 78
|
||||
retval = sep1 + msg.strip() + sep2 + returned_msg + sep3
|
||||
retval = sep1 + msg + sep2 + returned_msg + sep3
|
||||
raise AssertionError(retval)
|
||||
else:
|
||||
returned_msg = "\n".join(str(msg) for msg in stored_msg)
|
||||
|
|
@ -346,11 +354,29 @@ class TestAccount(CommandTest):
|
|||
self.call(account.CmdOOC(), "", "You go OOC.", caller=self.account)
|
||||
|
||||
def test_ic(self):
|
||||
self.account.db._playable_characters = [self.char1]
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(), "Char", "You become Char.", caller=self.account, receiver=self.char1
|
||||
)
|
||||
|
||||
def test_ic__other_object(self):
|
||||
self.account.db._playable_characters = [self.obj1]
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(), "Obj", "You become Obj.", caller=self.account, receiver=self.obj1
|
||||
)
|
||||
|
||||
def test_ic__nonaccess(self):
|
||||
self.account.unpuppet_object(self.session)
|
||||
self.call(
|
||||
account.CmdIC(),
|
||||
"Nonexistent",
|
||||
"That is not a valid character choice.",
|
||||
caller=self.account,
|
||||
receiver=self.account,
|
||||
)
|
||||
|
||||
def test_password(self):
|
||||
self.call(
|
||||
account.CmdPassword(),
|
||||
|
|
@ -456,10 +482,18 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdExamine(), "*TestAccount", "Name/key: TestAccount")
|
||||
|
||||
self.char1.db.test = "testval"
|
||||
self.call(building.CmdExamine(), "self/test", "Persistent attributes:\n test = testval")
|
||||
self.call(building.CmdExamine(), "self/test", "Persistent attribute(s):\n test = testval")
|
||||
self.call(building.CmdExamine(), "NotFound", "Could not find 'NotFound'.")
|
||||
self.call(building.CmdExamine(), "out", "Name/key: out")
|
||||
|
||||
# escape inlinefuncs
|
||||
self.char1.db.test2 = "this is a $random() value."
|
||||
self.call(
|
||||
building.CmdExamine(),
|
||||
"self/test2",
|
||||
"Persistent attribute(s):\n test2 = this is a \$random() value.",
|
||||
)
|
||||
|
||||
self.room1.scripts.add(self.script.__class__)
|
||||
self.call(building.CmdExamine(), "")
|
||||
self.account.scripts.add(self.script.__class__)
|
||||
|
|
@ -505,7 +539,7 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdSetAttribute(), "Obj2/test2", "Attribute Obj2/test2 = value2")
|
||||
self.call(building.CmdSetAttribute(), "Obj2/NotFound", "Obj2 has no attribute 'notfound'.")
|
||||
|
||||
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
||||
with patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
||||
self.call(building.CmdSetAttribute(), "/edit Obj2/test3")
|
||||
mock_ed.assert_called_with(self.char1, Anything, Anything, key="Obj2/test3")
|
||||
|
||||
|
|
@ -789,7 +823,7 @@ class TestBuilding(CommandTest):
|
|||
)
|
||||
self.call(building.CmdDesc(), "", "Usage: ")
|
||||
|
||||
with mock.patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
||||
with patch("evennia.commands.default.building.EvEditor") as mock_ed:
|
||||
self.call(building.CmdDesc(), "/edit")
|
||||
mock_ed.assert_called_with(
|
||||
self.char1,
|
||||
|
|
@ -937,7 +971,11 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdSetHome(), "Obj = Room2", "Home location of Obj was set to Room")
|
||||
|
||||
def test_list_cmdsets(self):
|
||||
self.call(building.CmdListCmdSets(), "", "<DefaultCharacter (Union, prio 0, perm)>:")
|
||||
self.call(
|
||||
building.CmdListCmdSets(),
|
||||
"",
|
||||
"<CmdSetHandler> stack:\n <CmdSet DefaultCharacter, Union, perm, prio 0>:",
|
||||
)
|
||||
self.call(building.CmdListCmdSets(), "NotFound", "Could not find 'NotFound'")
|
||||
|
||||
def test_typeclass(self):
|
||||
|
|
@ -991,6 +1029,34 @@ class TestBuilding(CommandTest):
|
|||
"All object creation hooks were run. All old attributes where deleted before the swap.",
|
||||
)
|
||||
|
||||
from evennia.prototypes.prototypes import homogenize_prototype
|
||||
|
||||
test_prototype = [
|
||||
homogenize_prototype(
|
||||
{
|
||||
"prototype_key": "testkey",
|
||||
"prototype_tags": [],
|
||||
"typeclass": "typeclasses.objects.Object",
|
||||
"key": "replaced_obj",
|
||||
"attrs": [("foo", "bar", None, ""), ("desc", "protdesc", None, "")],
|
||||
}
|
||||
)
|
||||
]
|
||||
with patch(
|
||||
"evennia.commands.default.building.protlib.search_prototype",
|
||||
new=MagicMock(return_value=test_prototype),
|
||||
) as mprot:
|
||||
self.call(
|
||||
building.CmdTypeclass(),
|
||||
"/prototype Obj=testkey",
|
||||
"replaced_obj changed typeclass from "
|
||||
"evennia.objects.objects.DefaultObject to "
|
||||
"typeclasses.objects.Object.\nAll object creation hooks were "
|
||||
"run. Attributes set before swap were not removed. Prototype "
|
||||
"'replaced_obj' was successfully applied over the object type.",
|
||||
)
|
||||
assert self.obj1.db.desc == "protdesc"
|
||||
|
||||
def test_lock(self):
|
||||
self.call(building.CmdLock(), "", "Usage: ")
|
||||
self.call(building.CmdLock(), "Obj = test:all()", "Added lock 'test:all()' to Obj.")
|
||||
|
|
@ -1038,11 +1104,41 @@ class TestBuilding(CommandTest):
|
|||
self.call(building.CmdFind(), self.char1.dbref, "Exact dbref match")
|
||||
self.call(building.CmdFind(), "*TestAccount", "Match")
|
||||
|
||||
self.call(building.CmdFind(), "/char Obj")
|
||||
self.call(building.CmdFind(), "/room Obj")
|
||||
self.call(building.CmdFind(), "/exit Obj")
|
||||
self.call(building.CmdFind(), "/char Obj", "No Matches")
|
||||
self.call(building.CmdFind(), "/room Obj", "No Matches")
|
||||
self.call(building.CmdFind(), "/exit Obj", "No Matches")
|
||||
self.call(building.CmdFind(), "/exact Obj", "One Match")
|
||||
|
||||
# Test multitype filtering
|
||||
with patch(
|
||||
"evennia.commands.default.building.CHAR_TYPECLASS",
|
||||
"evennia.objects.objects.DefaultCharacter",
|
||||
):
|
||||
self.call(building.CmdFind(), "/char/room Obj", "No Matches")
|
||||
self.call(building.CmdFind(), "/char/room/exit Char", "2 Matches")
|
||||
self.call(building.CmdFind(), "/char/room/exit/startswith Cha", "2 Matches")
|
||||
|
||||
# Test null search
|
||||
self.call(building.CmdFind(), "=", "Usage: ")
|
||||
|
||||
# Test bogus dbref range with no search term
|
||||
self.call(building.CmdFind(), "= obj", "Invalid dbref range provided (not a number).")
|
||||
self.call(building.CmdFind(), "= #1a", "Invalid dbref range provided (not a number).")
|
||||
|
||||
# Test valid dbref ranges with no search term
|
||||
id1 = self.obj1.id
|
||||
id2 = self.obj2.id
|
||||
maxid = ObjectDB.objects.latest("id").id
|
||||
maxdiff = maxid - id1 + 1
|
||||
mdiff = id2 - id1 + 1
|
||||
|
||||
self.call(building.CmdFind(), f"=#{id1}", f"{maxdiff} Matches(#{id1}-#{maxid}")
|
||||
self.call(building.CmdFind(), f"={id1}-{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
|
||||
self.call(building.CmdFind(), f"={id1} - {id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
|
||||
self.call(building.CmdFind(), f"={id1}- #{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
|
||||
self.call(building.CmdFind(), f"={id1}-#{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
|
||||
self.call(building.CmdFind(), f"=#{id1}-{id2}", f"{mdiff} Matches(#{id1}-#{id2}):")
|
||||
|
||||
def test_script(self):
|
||||
self.call(building.CmdScript(), "Obj = ", "No scripts defined on Obj")
|
||||
self.call(
|
||||
|
|
@ -1054,7 +1150,7 @@ class TestBuilding(CommandTest):
|
|||
"= Obj",
|
||||
"To create a global script you need scripts/add <typeclass>.",
|
||||
)
|
||||
self.call(building.CmdScript(), "Obj = ", "dbref obj")
|
||||
self.call(building.CmdScript(), "Obj ", "dbref ")
|
||||
|
||||
self.call(
|
||||
building.CmdScript(), "/start Obj", "0 scripts started on Obj"
|
||||
|
|
@ -1147,10 +1243,10 @@ class TestBuilding(CommandTest):
|
|||
)
|
||||
|
||||
def test_spawn(self):
|
||||
def getObject(commandTest, objKeyStr):
|
||||
def get_object(commandTest, obj_key):
|
||||
# A helper function to get a spawned object and
|
||||
# check that it exists in the process.
|
||||
query = search_object(objKeyStr)
|
||||
query = search_object(obj_key)
|
||||
commandTest.assertIsNotNone(query)
|
||||
commandTest.assertTrue(bool(query))
|
||||
obj = query[0]
|
||||
|
|
@ -1170,21 +1266,30 @@ class TestBuilding(CommandTest):
|
|||
inputs=["y"],
|
||||
)
|
||||
|
||||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/save testprot2 = {'key':'Test Char', "
|
||||
"'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"(Replacing `prototype_key` in prototype with given key.)|Saved prototype: testprot2",
|
||||
inputs=["y"],
|
||||
)
|
||||
|
||||
self.call(building.CmdSpawn(), "/search ", "Key ")
|
||||
self.call(building.CmdSpawn(), "/search test;test2", "")
|
||||
self.call(building.CmdSpawn(), "/search test;test2", "No prototypes found.")
|
||||
|
||||
self.call(
|
||||
building.CmdSpawn(),
|
||||
"/save {'key':'Test Char', " "'typeclass':'evennia.objects.objects.DefaultCharacter'}",
|
||||
"To save a prototype it must have the 'prototype_key' set.",
|
||||
"A prototype_key must be given, either as `prototype_key = <prototype>` or as "
|
||||
"a key 'prototype_key' inside the prototype structure.",
|
||||
)
|
||||
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
||||
self.call(building.CmdSpawn(), "testprot", "Spawned Test Char")
|
||||
# Tests that the spawned object's location is the same as the caharacter's location, since
|
||||
|
||||
# Tests that the spawned object's location is the same as the character's location, since
|
||||
# we did not specify it.
|
||||
testchar = getObject(self, "Test Char")
|
||||
testchar = get_object(self, "Test Char")
|
||||
self.assertEqual(testchar.location, self.char1.location)
|
||||
testchar.delete()
|
||||
|
||||
|
|
@ -1201,7 +1306,7 @@ class TestBuilding(CommandTest):
|
|||
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref,
|
||||
"Spawned goblin",
|
||||
)
|
||||
goblin = getObject(self, "goblin")
|
||||
goblin = get_object(self, "goblin")
|
||||
# Tests that the spawned object's type is a DefaultCharacter.
|
||||
self.assertIsInstance(goblin, DefaultCharacter)
|
||||
self.assertEqual(goblin.location, spawnLoc)
|
||||
|
|
@ -1220,7 +1325,7 @@ class TestBuilding(CommandTest):
|
|||
# Tests "spawn <prototype_name>"
|
||||
self.call(building.CmdSpawn(), "testball", "Spawned Ball")
|
||||
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertEqual(ball.location, self.char1.location)
|
||||
self.assertIsInstance(ball, DefaultObject)
|
||||
ball.delete()
|
||||
|
|
@ -1230,7 +1335,7 @@ class TestBuilding(CommandTest):
|
|||
self.call(
|
||||
building.CmdSpawn(), "/n 'BALL'", "Spawned Ball"
|
||||
) # /n switch is abbreviated form of /noloc
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertIsNone(ball.location)
|
||||
ball.delete()
|
||||
|
||||
|
|
@ -1249,12 +1354,12 @@ class TestBuilding(CommandTest):
|
|||
% spawnLoc.dbref,
|
||||
"Spawned Ball",
|
||||
)
|
||||
ball = getObject(self, "Ball")
|
||||
ball = get_object(self, "Ball")
|
||||
self.assertEqual(ball.location, spawnLoc)
|
||||
ball.delete()
|
||||
|
||||
# test calling spawn with an invalid prototype.
|
||||
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST'")
|
||||
self.call(building.CmdSpawn(), "'NO_EXIST'", "No prototype named 'NO_EXIST' was found.")
|
||||
|
||||
# Test listing commands
|
||||
self.call(building.CmdSpawn(), "/list", "Key ")
|
||||
|
|
@ -1285,13 +1390,12 @@ class TestBuilding(CommandTest):
|
|||
|
||||
# spawn/edit with invalid prototype
|
||||
msg = self.call(
|
||||
building.CmdSpawn(), "/edit NO_EXISTS", "No prototype 'NO_EXISTS' was found."
|
||||
building.CmdSpawn(), "/edit NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
|
||||
)
|
||||
|
||||
# spawn/examine (missing prototype)
|
||||
# lists all prototypes that exist
|
||||
msg = self.call(building.CmdSpawn(), "/examine")
|
||||
assert "testball" in msg and "testprot" in msg
|
||||
self.call(building.CmdSpawn(), "/examine", "You need to specify a prototype-key to show.")
|
||||
|
||||
# spawn/examine with valid prototype
|
||||
# prints the prototype
|
||||
|
|
@ -1300,7 +1404,9 @@ class TestBuilding(CommandTest):
|
|||
|
||||
# spawn/examine with invalid prototype
|
||||
# shows error
|
||||
self.call(building.CmdSpawn(), "/examine NO_EXISTS", "No prototype 'NO_EXISTS' was found.")
|
||||
self.call(
|
||||
building.CmdSpawn(), "/examine NO_EXISTS", "No prototype named 'NO_EXISTS' was found."
|
||||
)
|
||||
|
||||
|
||||
class TestComms(CommandTest):
|
||||
|
|
@ -1472,11 +1578,11 @@ class TestSystemCommands(CommandTest):
|
|||
|
||||
self.call(multimatch, "look", "")
|
||||
|
||||
@mock.patch("evennia.commands.default.syscommands.ChannelDB")
|
||||
@patch("evennia.commands.default.syscommands.ChannelDB")
|
||||
def test_channelcommand(self, mock_channeldb):
|
||||
channel = mock.MagicMock()
|
||||
channel.msg = mock.MagicMock()
|
||||
mock_channeldb.objects.get_channel = mock.MagicMock(return_value=channel)
|
||||
channel = MagicMock()
|
||||
channel.msg = MagicMock()
|
||||
mock_channeldb.objects.get_channel = MagicMock(return_value=channel)
|
||||
|
||||
self.call(syscommands.SystemSendToChannel(), "public:Hello")
|
||||
channel.msg.assert_called()
|
||||
|
|
|
|||
|
|
@ -294,7 +294,6 @@ You are not yet logged into the game. Commands available at this point:
|
|||
|wquit|n - abort the connection
|
||||
|
||||
First create an account e.g. with |wcreate Anna c67jHL8p|n
|
||||
(If you have spaces in your name, use double quotes: |wcreate "Anna the Barbarian" c67jHL8p|n
|
||||
Next you can connect to the game: |wconnect Anna c67jHL8p|n
|
||||
|
||||
You can use the |wlook|n command if you want to see the connect screen again.
|
||||
|
|
|
|||
|
|
@ -194,27 +194,71 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
self.assertTrue(all(True for cmd in cmdset_f.commands if cmd.from_cmdset == "A"))
|
||||
|
||||
def test_option_transfer(self):
|
||||
"Test transfer of cmdset options"
|
||||
|
||||
class TestOptionTransferTrue(TestCase):
|
||||
"""
|
||||
Test cmdset-merge transfer of the cmdset-special options
|
||||
(no_exits/channels/objs/duplicates etc)
|
||||
|
||||
cmdset A has all True options
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = True
|
||||
self.cmdset_a.no_objs = True
|
||||
self.cmdset_a.no_channels = True
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_option_transfer__reverse_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge), same prio.
|
||||
The options should pass through to F since none of the other cmdsets
|
||||
care to change the setting from their default None.
|
||||
|
||||
Since A.duplicates = True, the final result is an union of duplicate
|
||||
pairs (8 commands total).
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
# the options should pass through since none of the other cmdsets care
|
||||
# to change the setting from None.
|
||||
a.no_exits = True
|
||||
a.no_objs = True
|
||||
a.no_channels = True
|
||||
a.duplicates = True
|
||||
cmdset_f = d + c + b + a # reverse, same-prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_option_transfer__forward_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge), same prio. This
|
||||
should pass those options through since the other all have options set
|
||||
to None. The exception is `duplicates` since that is determined by
|
||||
the two last mergers in the chain both being True.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = a + b + c + d # forward, same-prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge) with the
|
||||
highest prio. This should also pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
|
|
@ -223,14 +267,35 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge). This is a bit
|
||||
synthetic since it will never happen in practice, but logic should
|
||||
still make it pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertTrue(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges last (normal reverse merge) with the lowest
|
||||
prio. This never happens (it would always merge first) but logic should hold
|
||||
and pass through since the other cmdsets have None.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
|
|
@ -239,32 +304,679 @@ class TestCmdSetMergers(TestCase):
|
|||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all True options, merges first (forward merge) with lowest prio. This
|
||||
is the normal behavior for a low-prio cmdset. Passthrough should happen.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with high
|
||||
prio. A should retain its option values and override the others
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, high prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with high
|
||||
prio. This situation should never happen, but logic should hold - the highest
|
||||
prio's options should survive the merge process.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = False
|
||||
b.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, high prio, never happens
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
c.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block_partial(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False excet C which has a None
|
||||
for `no_channels`. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False
|
||||
except for no_channels which passes through.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
c.no_channels = None # passthrough
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertFalse(cmdset_f.duplicates)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
a.priority = 0
|
||||
b.priority = 0
|
||||
|
||||
def test_option_transfer__reverse_highprio_sameprio_order_last(self):
|
||||
"""
|
||||
A has all True options and highest prio, D has False and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
Since A is merged last, this should give prio to A's options
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = False
|
||||
d.no_exits = False
|
||||
d.no_objs = None
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A same prio, merged after b
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_option_transfer__reverse_highprio_sameprio_order_first(self):
|
||||
"""
|
||||
A has all True options and highest prio, D has False and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
While B, with None-values, is merged after A, A's options should have
|
||||
replaced those of D at that point, and since B has passthrough the
|
||||
final result should contain A's True options.
|
||||
|
||||
Note that despite A having duplicates=True, there is no duplication in
|
||||
the DB + A merger since they have different priorities.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = False
|
||||
d.no_exits = False
|
||||
d.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + a + b # reverse, A same prio, merged before b
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_block(self):
|
||||
"""
|
||||
A has all True options, other cmdsets has False. A merges last with low
|
||||
prio. This usually doesn't happen- it should merge last. But logic should
|
||||
hold and the low-prio cmdset's values should be blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = False
|
||||
d.no_channels = False
|
||||
b.no_objs = False
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A low prio, never happens
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
class TestOptionTransferFalse(TestCase):
|
||||
"""
|
||||
Test cmdset-merge transfer of the cmdset-special options
|
||||
(no_exits/channels/objs/duplicates etc)
|
||||
|
||||
cmdset A has all False options
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = False
|
||||
self.cmdset_a.no_objs = False
|
||||
self.cmdset_a.no_channels = False
|
||||
self.cmdset_a.duplicates = False
|
||||
|
||||
def test_option_transfer__reverse_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge), same prio.
|
||||
The options should pass through to F since none of the other cmdsets
|
||||
care to change the setting from their default None.
|
||||
|
||||
Since A has duplicates=False, the result is a unique union of 4 cmds.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = d + c + b + a # reverse, same-prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_sameprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge), same prio. This
|
||||
should pass those options through since the other all have options set
|
||||
to None. The exception is `duplicates` since that is determined by
|
||||
the two last mergers in the chain both being .
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = a + b + c + d # forward, same-prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge) with the
|
||||
highest prio. This should also pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = d + c + b + a # reverse, A top priority
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge). This is a bit
|
||||
synthetic since it will never happen in practice, but logic should
|
||||
still make it pass through.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
cmdset_f = a + b + c + d # forward, A top priority. This never happens in practice.
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges last (normal reverse merge) with the lowest
|
||||
prio. This never happens (it would always merge first) but logic should hold
|
||||
and pass through since the other cmdsets have None.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = d + c + b + a # reverse, A low prio. This never happens in practice.
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_passthrough(self):
|
||||
"""
|
||||
A has all False options, merges first (forward merge) with lowest prio. This
|
||||
is the normal behavior for a low-prio cmdset. Passthrough should happen.
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with high
|
||||
prio. A should retain its option values and override the others
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, high prio
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_highprio_block_passthrough(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with high
|
||||
prio. This situation should never happen, but logic should hold - the highest
|
||||
prio's options should survive the merge process.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 1
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
c.no_exits = True
|
||||
b.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, high prio, never happens
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with low
|
||||
prio. This should result in its values being blocked and come out False.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
c.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__forward_lowprio_block_partial(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True excet C which has a None
|
||||
for `no_channels`. A merges last with low
|
||||
prio. This should result in its values being blocked and come out True
|
||||
except for no_channels which passes through.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
c.no_channels = None # passthrough
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = a + b + c + d # forward, A low prio
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_sameprio_order_last(self):
|
||||
"""
|
||||
A has all False options and highest prio, D has True and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
Since A is merged last, this should give prio to A's False options
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = True
|
||||
d.no_exits = True
|
||||
d.no_objs = True
|
||||
d.duplicates = False
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, merged after b
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_sameprio_order_first(self):
|
||||
"""
|
||||
A has all False options and highest prio, D has True and lowest prio,
|
||||
others are passthrough. B has the same prio as A, with passthrough.
|
||||
|
||||
While B, with None-values, is merged after A, A's options should have
|
||||
replaced those of D at that point, and since B has passthrough the
|
||||
final result should contain A's False options.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
d.priority = -1
|
||||
d.no_channels = True
|
||||
d.no_exits = True
|
||||
d.no_objs = True
|
||||
d.duplicates = False
|
||||
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + a + b # reverse, A high prio, merged before b
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
def test_option_transfer__reverse_lowprio_block(self):
|
||||
"""
|
||||
A has all False options, other cmdsets has True. A merges last with low
|
||||
prio. This usually doesn't happen- it should merge last. But logic should
|
||||
hold and the low-prio cmdset's values should be blocked and come out True.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = -1
|
||||
b.priority = 0
|
||||
c.priority = 1
|
||||
d.priority = 2
|
||||
c.no_exits = True
|
||||
d.no_channels = True
|
||||
b.no_objs = True
|
||||
d.duplicates = True
|
||||
# higher-prio sets will change the option up the chain
|
||||
cmdset_f = d + c + b + a # reverse, A low prio, never happens
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
class TestDuplicateBehavior(TestCase):
|
||||
"""
|
||||
Test behavior of .duplicate option, which is a bit special in that it
|
||||
doesn't propagate.
|
||||
|
||||
`A.duplicates=True` for all tests.
|
||||
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_reverse_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate. Only
|
||||
A has duplicates=True.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (no duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
cmdset_f = d + b + c + a # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_reverse_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer, which does not propagate.
|
||||
C.duplication=True
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = True
|
||||
cmdset_f = d + b + c + a # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 10)
|
||||
|
||||
def test_forward_sameprio_duplicate(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=True, merges later than A
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + A = DBA (duplication, DBA.duplication=None)
|
||||
DBA + C = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = True
|
||||
cmdset_f = d + b + a + c # two last mergers duplicates=True
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 10)
|
||||
|
||||
def test_reverse_sameprio_duplicate_reverse(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=False (explicit), merges before A. This behavior is the
|
||||
same as if C.duplication=None, since A merges later and takes
|
||||
precedence.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + C = DBC (no duplication, DBC.duplication=None)
|
||||
DBC + A = final (duplication, final.duplication=None)
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = False
|
||||
cmdset_f = d + b + c + a # a merges last, takes precedence
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 8)
|
||||
|
||||
def test_reverse_sameprio_duplicate_forward(self):
|
||||
"""
|
||||
Test of `duplicates` transfer which does not propagate.
|
||||
C.duplication=False (explicit), merges after A. This just means
|
||||
only A causes duplicates, earlier in the chain.
|
||||
|
||||
D + B = DB (no duplication, DB.duplication=None)
|
||||
DB + A = DBA (duplication, DBA.duplication=None)
|
||||
DBA + C = final (no duplication, final.duplication=None)
|
||||
|
||||
Note that DBA has 8 cmds due to A merging onto DB with duplication,
|
||||
but since C merges onto this with no duplication, the union will hold
|
||||
6 commands, since C has two commands that replaces the 4 duplicates
|
||||
with uniques copies from C.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
c.duplicates = False
|
||||
cmdset_f = d + b + a + c # a merges before c
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 6)
|
||||
|
||||
|
||||
class TestOptionTransferReplace(TestCase):
|
||||
"""
|
||||
Test option transfer through more complex merge types.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.cmdset_a = _CmdSetA()
|
||||
self.cmdset_b = _CmdSetB()
|
||||
self.cmdset_c = _CmdSetC()
|
||||
self.cmdset_d = _CmdSetD()
|
||||
self.cmdset_a.priority = 0
|
||||
self.cmdset_b.priority = 0
|
||||
self.cmdset_c.priority = 0
|
||||
self.cmdset_d.priority = 0
|
||||
self.cmdset_a.no_exits = True
|
||||
self.cmdset_a.no_objs = True
|
||||
self.cmdset_a.no_channels = True
|
||||
self.cmdset_a.duplicates = True
|
||||
|
||||
def test_option_transfer__replace_reverse_highprio(self):
|
||||
"""
|
||||
A has all options True and highest priority. C has them False and is
|
||||
Replace-type.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
c.mergetype = "Replace"
|
||||
c.no_channels = False
|
||||
c.no_exits = False
|
||||
c.no_objs = False
|
||||
c.duplicates = False
|
||||
d.priority = -1
|
||||
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
|
||||
self.assertTrue(cmdset_f.no_exits)
|
||||
self.assertTrue(cmdset_f.no_objs)
|
||||
self.assertTrue(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 7)
|
||||
|
||||
def test_option_transfer__replace_reverse_highprio_from_false(self):
|
||||
"""
|
||||
Inverse of previous test: A has all options False and highest priority.
|
||||
C has them True and is Replace-type.
|
||||
|
||||
"""
|
||||
a, b, c, d = self.cmdset_a, self.cmdset_b, self.cmdset_c, self.cmdset_d
|
||||
a.no_exits = False
|
||||
a.no_objs = False
|
||||
a.no_channels = False
|
||||
a.duplicates = False
|
||||
|
||||
a.priority = 2
|
||||
b.priority = 2
|
||||
c.priority = 0
|
||||
c.mergetype = "Replace"
|
||||
c.no_channels = True
|
||||
c.no_exits = True
|
||||
c.no_objs = True
|
||||
c.duplicates = True
|
||||
d.priority = -1
|
||||
|
||||
cmdset_f = d + c + b + a # reverse, A high prio, C Replace
|
||||
self.assertFalse(cmdset_f.no_exits)
|
||||
self.assertFalse(cmdset_f.no_objs)
|
||||
self.assertFalse(cmdset_f.no_channels)
|
||||
self.assertIsNone(cmdset_f.duplicates)
|
||||
self.assertEqual(len(cmdset_f.commands), 4)
|
||||
|
||||
|
||||
# test cmdhandler functions
|
||||
|
||||
|
|
@ -487,3 +1199,28 @@ class TestCmdParser(TestCase):
|
|||
cmdparser.cmdparser("test1hello", a_cmdset, None),
|
||||
[("test1", "hello", bcmd, 5, 0.5, "test1")],
|
||||
)
|
||||
|
||||
|
||||
class TestCmdSetNesting(EvenniaTest):
|
||||
"""
|
||||
Test 'nesting' of cmdsets by adding
|
||||
"""
|
||||
|
||||
def test_nest(self):
|
||||
|
||||
class CmdA(Command):
|
||||
key = "a"
|
||||
|
||||
def func(self):
|
||||
self.msg(str(self.obj))
|
||||
|
||||
class CmdSetA(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdA)
|
||||
|
||||
class CmdSetB(CmdSet):
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdSetA)
|
||||
|
||||
cmd = self.char1.cmdset.cmdset_stack[-1].commands[0]
|
||||
self.assertEqual(cmd.obj, self.char1)
|
||||
|
|
|
|||
|
|
@ -24,16 +24,20 @@ does this for you.
|
|||
|
||||
"""
|
||||
from django.conf import settings
|
||||
from evennia.commands import cmdset, command
|
||||
from evennia.commands import cmdset
|
||||
from evennia.utils.logger import tail_log_file
|
||||
from evennia.utils.utils import class_from_module
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
# we must late-import these since any overloads are likely to
|
||||
# themselves be using these classes leading to a circular import.
|
||||
|
||||
_CHANNEL_HANDLER_CLASS = None
|
||||
_CHANNEL_COMMAND_CLASS = None
|
||||
_CHANNELDB = None
|
||||
_COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
|
||||
class ChannelCommand(command.Command):
|
||||
class ChannelCommand(_COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
{channelkey} channel
|
||||
|
||||
|
|
@ -115,17 +119,17 @@ class ChannelCommand(command.Command):
|
|||
caller = caller if not hasattr(caller, "account") else caller.account
|
||||
unmuted = channel.unmute(caller)
|
||||
if unmuted:
|
||||
self.msg("You start listening to %s." % channel)
|
||||
self.msg(_("You start listening to %s.") % channel)
|
||||
return
|
||||
self.msg("You were already listening to %s." % channel)
|
||||
self.msg(_("You were already listening to %s.") % channel)
|
||||
return
|
||||
if msg == "off":
|
||||
caller = caller if not hasattr(caller, "account") else caller.account
|
||||
muted = channel.mute(caller)
|
||||
if muted:
|
||||
self.msg("You stop listening to %s." % channel)
|
||||
self.msg(_("You stop listening to %s.") % channel)
|
||||
return
|
||||
self.msg("You were already not listening to %s." % channel)
|
||||
self.msg(_("You were already not listening to %s.") % channel)
|
||||
return
|
||||
if self.history_start is not None:
|
||||
# Try to view history
|
||||
|
|
@ -140,7 +144,7 @@ class ChannelCommand(command.Command):
|
|||
else:
|
||||
caller = caller if not hasattr(caller, "account") else caller.account
|
||||
if caller in channel.mutelist:
|
||||
self.msg("You currently have %s muted." % channel)
|
||||
self.msg(_("You currently have %s muted.") % channel)
|
||||
return
|
||||
channel.msg(msg, senders=self.caller, online=True)
|
||||
|
||||
|
|
@ -314,5 +318,6 @@ class ChannelHandler(object):
|
|||
return chan_cmdset
|
||||
|
||||
|
||||
CHANNEL_HANDLER = ChannelHandler()
|
||||
# set up the singleton
|
||||
CHANNEL_HANDLER = class_from_module(settings.CHANNEL_HANDLER_CLASS)()
|
||||
CHANNELHANDLER = CHANNEL_HANDLER # legacy
|
||||
|
|
|
|||
|
|
@ -51,6 +51,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
self.attributes.add("keep_log", cdict["keep_log"])
|
||||
if cdict.get("desc"):
|
||||
self.attributes.add("desc", cdict["desc"])
|
||||
if cdict.get("tags"):
|
||||
self.tags.batch_add(*cdict["tags"])
|
||||
|
||||
def basetype_setup(self):
|
||||
# delayed import of the channelhandler
|
||||
|
|
@ -253,7 +255,7 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
key (str): This must be unique.
|
||||
account (Account): Account to attribute this object to.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
aliases (list of str): List of alternative (likely shorter) keynames.
|
||||
description (str): A description of the channel, for use in listings.
|
||||
locks (str): Lockstring.
|
||||
|
|
@ -394,7 +396,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
to build senders for the message.
|
||||
sender_strings (list, optional): Name strings of senders. Used for external
|
||||
connections where the sender is not an account or object.
|
||||
When this is defined, external will be assumed.
|
||||
When this is defined, external will be assumed. The list will be
|
||||
filtered so each sender-string only occurs once.
|
||||
keep_log (bool or None, optional): This allows to temporarily change the logging status of
|
||||
this channel message. If `None`, the Channel's `keep_log` Attribute will
|
||||
be used. If `True` or `False`, that logging status will be used for this
|
||||
|
|
@ -425,6 +428,8 @@ class DefaultChannel(ChannelDB, metaclass=TypeclassBase):
|
|||
msgobj = self.pre_send_message(msgobj)
|
||||
if not msgobj:
|
||||
return False
|
||||
if sender_strings:
|
||||
sender_strings = list(set(make_iter(sender_strings)))
|
||||
msgobj = self.message_transform(
|
||||
msgobj, emit=emit, sender_strings=sender_strings, external=external
|
||||
)
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ Comm system components.
|
|||
from django.db.models import Q
|
||||
from evennia.typeclasses.managers import TypedObjectManager, TypeclassManager
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import dbref
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_AccountDB = None
|
||||
|
|
@ -31,32 +32,6 @@ class CommError(Exception):
|
|||
#
|
||||
|
||||
|
||||
def dbref(inp, reqhash=True):
|
||||
"""
|
||||
Valid forms of dbref (database reference number) are either a
|
||||
string '#N' or an integer N.
|
||||
|
||||
Args:
|
||||
inp (int or str): A possible dbref to check syntactically.
|
||||
reqhash (bool): Require an initial hash `#` to accept.
|
||||
|
||||
Returns:
|
||||
is_dbref (int or None): The dbref integer part if a valid
|
||||
dbref, otherwise `None`.
|
||||
|
||||
"""
|
||||
if reqhash and not (isinstance(inp, str) and inp.startswith("#")):
|
||||
return None
|
||||
if isinstance(inp, str):
|
||||
inp = inp.lstrip("#")
|
||||
try:
|
||||
if int(inp) < 0:
|
||||
return None
|
||||
except Exception:
|
||||
return None
|
||||
return inp
|
||||
|
||||
|
||||
def identify_object(inp):
|
||||
"""
|
||||
Helper function. Identifies if an object is an account or an object;
|
||||
|
|
|
|||
|
|
@ -400,9 +400,11 @@ class Msg(SharedMemoryModel):
|
|||
|
||||
def __str__(self):
|
||||
"This handles what is shown when e.g. printing the message"
|
||||
senders = ",".join(obj.key for obj in self.senders)
|
||||
senders = ",".join(getattr(obj, "key", str(obj)) for obj in self.senders)
|
||||
|
||||
receivers = ",".join(
|
||||
["[%s]" % obj.key for obj in self.channels] + [obj.key for obj in self.receivers]
|
||||
["[%s]" % getattr(obj, "key", str(obj)) for obj in self.channels]
|
||||
+ [getattr(obj, "key", str(obj)) for obj in self.receivers]
|
||||
)
|
||||
return "%s->%s: %s" % (senders, receivers, crop(self.message, width=40))
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia import DefaultChannel
|
||||
from evennia.utils.create import create_message
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
|
||||
|
||||
class ObjectCreationTest(EvenniaTest):
|
||||
|
|
@ -10,3 +11,43 @@ class ObjectCreationTest(EvenniaTest):
|
|||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertEqual(description, obj.db.desc)
|
||||
|
||||
def test_message_create(self):
|
||||
msg = create_message("peewee herman", "heh-heh!", header="mail time!")
|
||||
self.assertTrue(msg)
|
||||
self.assertEqual(str(msg), "peewee herman->: heh-heh!")
|
||||
|
||||
|
||||
class ChannelWholistTests(EvenniaTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.default_channel, _ = DefaultChannel.create(
|
||||
"coffeetalk", description="A place to talk about coffee."
|
||||
)
|
||||
self.default_channel.connect(self.obj1)
|
||||
|
||||
def test_wholist_shows_subscribed_objects(self):
|
||||
expected = "Obj"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_shows_none_when_empty(self):
|
||||
# No one hates dogs
|
||||
empty_channel, _ = DefaultChannel.create(
|
||||
"doghaters", description="A place where dog haters unite."
|
||||
)
|
||||
expected = "<None>"
|
||||
result = empty_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_does_not_show_muted_objects(self):
|
||||
self.default_channel.mute(self.obj2)
|
||||
expected = "Obj"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
||||
def test_wholist_shows_connected_object_as_bold(self):
|
||||
self.default_channel.connect(self.char1)
|
||||
expected = "Obj, |wChar|n"
|
||||
result = self.default_channel.wholist
|
||||
self.assertEqual(expected, result)
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ def _call_or_get(value, menu=None, choice=None, string=None, obj=None, caller=No
|
|||
Args:
|
||||
value (any): the value to obtain. It might be a callable (see note).
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
menu (BuildingMenu, optional): the building menu to pass to value
|
||||
if it is a callable.
|
||||
choice (Choice, optional): the choice to pass to value if a callable.
|
||||
|
|
|
|||
|
|
@ -160,7 +160,7 @@ def get_worn_clothes(character, exclude_covered=False):
|
|||
Args:
|
||||
character (obj): The character to get a list of worn clothes from.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
exclude_covered (bool): If True, excludes clothes covered by other
|
||||
clothing from the returned list.
|
||||
|
||||
|
|
@ -237,7 +237,7 @@ class Clothing(DefaultObject):
|
|||
wearer (obj): character object wearing this clothing object
|
||||
wearstyle (True or str): string describing the style of wear or True for none
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
quiet (bool): If false, does not message the room
|
||||
|
||||
Notes:
|
||||
|
|
@ -276,7 +276,7 @@ class Clothing(DefaultObject):
|
|||
Args:
|
||||
wearer (obj): character object wearing this clothing object
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
quiet (bool): If false, does not message the room
|
||||
"""
|
||||
self.db.worn = False
|
||||
|
|
|
|||
|
|
@ -125,14 +125,14 @@ CURLY_COLOR_ANSI_EXTRA_MAP = [
|
|||
(r"{c", _ANSI_HILITE + _ANSI_CYAN),
|
||||
(r"{w", _ANSI_HILITE + _ANSI_WHITE), # pure white
|
||||
(r"{x", _ANSI_HILITE + _ANSI_BLACK), # dark grey
|
||||
(r"{R", _ANSI_HILITE + _ANSI_RED),
|
||||
(r"{G", _ANSI_HILITE + _ANSI_GREEN),
|
||||
(r"{Y", _ANSI_HILITE + _ANSI_YELLOW),
|
||||
(r"{B", _ANSI_HILITE + _ANSI_BLUE),
|
||||
(r"{M", _ANSI_HILITE + _ANSI_MAGENTA),
|
||||
(r"{C", _ANSI_HILITE + _ANSI_CYAN),
|
||||
(r"{W", _ANSI_HILITE + _ANSI_WHITE), # light grey
|
||||
(r"{X", _ANSI_HILITE + _ANSI_BLACK), # pure black
|
||||
(r"{R", _ANSI_UNHILITE + _ANSI_RED),
|
||||
(r"{G", _ANSI_UNHILITE + _ANSI_GREEN),
|
||||
(r"{Y", _ANSI_UNHILITE + _ANSI_YELLOW),
|
||||
(r"{B", _ANSI_UNHILITE + _ANSI_BLUE),
|
||||
(r"{M", _ANSI_UNHILITE + _ANSI_MAGENTA),
|
||||
(r"{C", _ANSI_UNHILITE + _ANSI_CYAN),
|
||||
(r"{W", _ANSI_UNHILITE + _ANSI_WHITE), # light grey
|
||||
(r"{X", _ANSI_UNHILITE + _ANSI_BLACK), # pure black
|
||||
# hilight-able colors
|
||||
(r"{h", _ANSI_HILITE),
|
||||
(r"{H", _ANSI_UNHILITE),
|
||||
|
|
|
|||
|
|
@ -95,7 +95,7 @@ def gametime_to_realtime(format=False, **kwargs):
|
|||
in-game, you will be able to find the number of real-world seconds this
|
||||
corresponds to (hint: Interval events deal with real life seconds).
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
format (bool): Formatting the output.
|
||||
days, month etc (int): These are the names of time units that must
|
||||
match the `settings.TIME_UNITS` dict keys.
|
||||
|
|
@ -131,7 +131,7 @@ def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0, months=0, yrs=0
|
|||
interval would correspond to. This is usually a lot less
|
||||
interesting than the other way around.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
times (int): The various components of the time.
|
||||
format (bool): Formatting the output.
|
||||
|
||||
|
|
@ -298,6 +298,9 @@ class GametimeScript(DefaultScript):
|
|||
|
||||
def at_repeat(self):
|
||||
"""Call the callback and reset interval."""
|
||||
|
||||
from evennia.utils.utils import calledby
|
||||
|
||||
callback = self.db.callback
|
||||
if callback:
|
||||
callback()
|
||||
|
|
|
|||
|
|
@ -219,7 +219,7 @@ class BaseState(object):
|
|||
"""
|
||||
This is a convenience-wrapper for quickly building EvscapeRoom objects.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
typeclass (str): This can take just the class-name in the evscaperoom's
|
||||
objects.py module. Otherwise, a full path or the actual class
|
||||
is needed (for custom state objects, just give the class directly).
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ def create_evscaperoom_object(
|
|||
Note that for the purpose of the Evscaperoom, we only allow one instance
|
||||
of each *name*, deleting the old version if it already exists.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
typeclass (str): This can take just the class-name in the evscaperoom's
|
||||
objects.py module. Otherwise, a full path is needed.
|
||||
key (str): Name of object.
|
||||
|
|
@ -69,7 +69,7 @@ def create_fantasy_word(length=5, capitalize=True):
|
|||
"""
|
||||
Create a random semi-pronouncable 'word'.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
length (int): The desired length of the 'word'.
|
||||
capitalize (bool): If the return should be capitalized or not
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -8,26 +8,39 @@ insert custom markers in their text to indicate gender-aware
|
|||
messaging. It relies on a modified msg() and is meant as an
|
||||
inspiration and starting point to how to do stuff like this.
|
||||
|
||||
When in use, all messages being sent to the character will make use of
|
||||
the character's gender, for example the echo
|
||||
An object can have the following genders:
|
||||
- male (he/his)
|
||||
- female (her/hers)
|
||||
- neutral (it/its)
|
||||
- ambiguous (they/them/their/theirs)
|
||||
|
||||
When in use, messages can contain special tags to indicate pronouns gendered
|
||||
based on the one being addressed. Capitalization will be retained.
|
||||
|
||||
- `|s`, `|S`: Subjective form: he, she, it, He, She, It, They
|
||||
- `|o`, `|O`: Objective form: him, her, it, Him, Her, It, Them
|
||||
- `|p`, `|P`: Possessive form: his, her, its, His, Her, Its, Their
|
||||
- `|a`, `|A`: Absolute Possessive form: his, hers, its, His, Hers, Its, Theirs
|
||||
|
||||
For example,
|
||||
|
||||
```
|
||||
char.msg("%s falls on |p face with a thud." % char.key)
|
||||
"Tom falls on his face with a thud"
|
||||
```
|
||||
|
||||
will result in "Tom falls on his|her|its|their face with a thud"
|
||||
depending on the gender of the object being messaged. Default gender
|
||||
is "ambiguous" (they).
|
||||
The default gender is "ambiguous" (they/them/their/theirs).
|
||||
|
||||
To use, have DefaultCharacter inherit from this, or change
|
||||
setting.DEFAULT_CHARACTER to point to this class.
|
||||
|
||||
The `@gender` command needs to be added to the default cmdset before
|
||||
it becomes available.
|
||||
The `@gender` command is used to set the gender. It needs to be added to the
|
||||
default cmdset before it becomes available.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from evennia.utils import logger
|
||||
from evennia import DefaultCharacter
|
||||
from evennia import Command
|
||||
|
||||
|
|
@ -107,14 +120,17 @@ class GenderCharacter(DefaultCharacter):
|
|||
pronoun = _GENDER_PRONOUN_MAP[gender][typ.lower()]
|
||||
return pronoun.capitalize() if typ.isupper() else pronoun
|
||||
|
||||
def msg(self, text, from_obj=None, session=None, **kwargs):
|
||||
def msg(self, text=None, from_obj=None, session=None, **kwargs):
|
||||
"""
|
||||
Emits something to a session attached to the object.
|
||||
Overloads the default msg() implementation to include
|
||||
gender-aware markers in output.
|
||||
|
||||
Args:
|
||||
text (str, optional): The message to send
|
||||
text (str or tuple, optional): The message to send. This
|
||||
is treated internally like any send-command, so its
|
||||
value can be a tuple if sending multiple arguments to
|
||||
the `text` oob command.
|
||||
from_obj (obj, optional): object that is sending. If
|
||||
given, at_msg_send will be called
|
||||
session (Session or list, optional): session or list of
|
||||
|
|
@ -125,9 +141,17 @@ class GenderCharacter(DefaultCharacter):
|
|||
All extra kwargs will be passed on to the protocol.
|
||||
|
||||
"""
|
||||
# pre-process the text before continuing
|
||||
if text is None:
|
||||
super().msg(from_obj=from_obj, session=session, **kwargs)
|
||||
return
|
||||
|
||||
try:
|
||||
text = _RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
|
||||
if text and isinstance(text, tuple):
|
||||
text = (_RE_GENDER_PRONOUN.sub(self._get_pronoun, text[0]), *text[1:])
|
||||
else:
|
||||
text = _RE_GENDER_PRONOUN.sub(self._get_pronoun, text)
|
||||
except TypeError:
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.log_trace(e)
|
||||
super().msg(text, from_obj=from_obj, session=session, **kwargs)
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ class CallbackHandler(object):
|
|||
callback_name (str): the callback name to call.
|
||||
*args: additional variables for this callback.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
number (int, optional): call just a specific callback.
|
||||
parameters (str, optional): call a callback with parameters.
|
||||
locals (dict, optional): a locals replacement.
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ def get(**kwargs):
|
|||
"""
|
||||
Return an object with the given search option or None if None is found.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Any searchable data or property (id, db_key, db_location...).
|
||||
|
||||
Returns:
|
||||
|
|
|
|||
|
|
@ -397,7 +397,7 @@ class EventHandler(DefaultScript):
|
|||
callback_name (str): the callback name to call.
|
||||
*args: additional variables for this callback.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
number (int, optional): call just a specific callback.
|
||||
parameters (str, optional): call a callback with parameters.
|
||||
locals (dict, optional): a locals replacement.
|
||||
|
|
|
|||
|
|
@ -421,7 +421,7 @@ class EventCharacter(DefaultCharacter):
|
|||
|
||||
Args:
|
||||
message (str): The suggested say/whisper text spoken by self.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
whisper (bool): If True, this is a whisper rather than
|
||||
a say. This is sent by the whisper command by default.
|
||||
Other verbal commands could use this hook in similar
|
||||
|
|
@ -477,7 +477,7 @@ class EventCharacter(DefaultCharacter):
|
|||
(by default only used by whispers).
|
||||
msg_receiver(str, optional): Specific message for receiver only.
|
||||
mapping (dict, optional): Additional mapping in messages.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
whisper (bool): If this is a whisper rather than a say. Kwargs
|
||||
can be used by other verbal commands in a similar way.
|
||||
|
||||
|
|
|
|||
|
|
@ -238,7 +238,7 @@ class CmdMail(default_cmds.MuxAccountCommand):
|
|||
else:
|
||||
raise IndexError
|
||||
except IndexError:
|
||||
self.caller.msg("Message does not exixt.")
|
||||
self.caller.msg("Message does not exist.")
|
||||
except ValueError:
|
||||
self.caller.msg("Usage: @mail/forward <account list>=<#>[/<Message>]")
|
||||
elif "reply" in self.switches or "rep" in self.switches:
|
||||
|
|
|
|||
|
|
@ -289,7 +289,7 @@ class CmdCreatePuzzleRecipe(MuxCommand):
|
|||
proto_parts = [proto_def(obj) for obj in parts]
|
||||
proto_results = [proto_def(obj) for obj in results]
|
||||
|
||||
puzzle = create_script(PuzzleRecipe, key=puzzle_name)
|
||||
puzzle = create_script(PuzzleRecipe, key=puzzle_name, persistent=True)
|
||||
puzzle.save_recipe(puzzle_name, proto_parts, proto_results)
|
||||
puzzle.locks.add("control:id(%s) or perm(Builder)" % caller.dbref[1:])
|
||||
|
||||
|
|
@ -488,7 +488,7 @@ class CmdArmPuzzle(MuxCommand):
|
|||
|
||||
Notes:
|
||||
Create puzzles with `@puzzle`; get list of
|
||||
defined puzzles using `@lspuzlerecipies`.
|
||||
defined puzzles using `@lspuzzlerecipes`.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -588,13 +588,27 @@ def _matching_puzzles(puzzles, puzzlename_tags_dict, puzzle_ingredients):
|
|||
|
||||
|
||||
class CmdUsePuzzleParts(MuxCommand):
|
||||
"""
|
||||
Use an object, or a group of objects at once.
|
||||
|
||||
|
||||
Example:
|
||||
You look around you and see a pole, a long string, and a needle.
|
||||
|
||||
use pole, long string, needle
|
||||
|
||||
Genius! You built a fishing pole.
|
||||
|
||||
|
||||
Usage:
|
||||
use <obj1> [,obj2,...]
|
||||
"""
|
||||
|
||||
# Technical explanation
|
||||
"""
|
||||
Searches for all puzzles whose parts match the given set of objects. If there are matching
|
||||
puzzles, the result objects are spawned in their corresponding location if all parts have been
|
||||
passed in.
|
||||
|
||||
Usage:
|
||||
use <part1[,part2,...>]
|
||||
"""
|
||||
|
||||
key = "use"
|
||||
|
|
|
|||
|
|
@ -331,7 +331,7 @@ class LanguageHandler(DefaultScript):
|
|||
# find out what preceeded this word
|
||||
wpos = match.start()
|
||||
preceeding = match.string[:wpos].strip()
|
||||
start_sentence = preceeding.endswith(".") or not preceeding
|
||||
start_sentence = preceeding.endswith((".", "!", "?")) or not preceeding
|
||||
|
||||
# make up translation on the fly. Length can
|
||||
# vary from un-translated word.
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ def parse_language(speaker, emote):
|
|||
the markers and a tuple (langname, saytext), where
|
||||
langname can be None.
|
||||
Raises:
|
||||
LanguageError: If an invalid language was specified.
|
||||
rplanguage.LanguageError: If an invalid language was specified.
|
||||
|
||||
Notes:
|
||||
Note that no errors are raised if the wrong language identifier
|
||||
|
|
@ -798,6 +798,16 @@ class RecogHandler(object):
|
|||
# recog_mask log not passed, disable recog
|
||||
return obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Get a mapping of the recogs stored in handler.
|
||||
|
||||
Returns:
|
||||
recogs (dict): A mapping of {recog: obj} stored in handler.
|
||||
|
||||
"""
|
||||
return {self.obj2recog[obj]: obj for obj in self.obj2recog.keys()}
|
||||
|
||||
def remove(self, obj):
|
||||
"""
|
||||
Clear recog for a given object.
|
||||
|
|
@ -896,10 +906,9 @@ class CmdSay(RPCommand): # replaces standard say
|
|||
caller.msg("Say what?")
|
||||
return
|
||||
|
||||
# calling the speech hook on the location
|
||||
speech = caller.location.at_before_say(self.args)
|
||||
# calling the speech modifying hook
|
||||
speech = caller.at_before_say(self.args)
|
||||
# preparing the speech with sdesc/speech parsing.
|
||||
speech = '/me says, "{speech}"'.format(speech=speech)
|
||||
targets = self.caller.location.contents
|
||||
send_emote(self.caller, targets, speech, anonymous_add=None)
|
||||
|
||||
|
|
@ -932,6 +941,9 @@ class CmdSdesc(RPCommand): # set/look at own sdesc
|
|||
except SdescError as err:
|
||||
caller.msg(err)
|
||||
return
|
||||
except AttributeError:
|
||||
caller.msg(f"Cannot set sdesc on {caller.key}.")
|
||||
return
|
||||
caller.msg("%s's sdesc was set to '%s'." % (caller.key, sdesc))
|
||||
|
||||
|
||||
|
|
@ -1041,6 +1053,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
|||
Recognize another person in the same room.
|
||||
|
||||
Usage:
|
||||
recog
|
||||
recog sdesc as alias
|
||||
forget alias
|
||||
|
||||
|
|
@ -1048,8 +1061,8 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
|||
recog tall man as Griatch
|
||||
forget griatch
|
||||
|
||||
This will assign a personal alias for a person, or
|
||||
forget said alias.
|
||||
This will assign a personal alias for a person, or forget said alias.
|
||||
Using the command without arguments will list all current recogs.
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -1058,6 +1071,7 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
|||
|
||||
def parse(self):
|
||||
"Parse for the sdesc as alias structure"
|
||||
self.sdesc, self.alias = "", ""
|
||||
if " as " in self.args:
|
||||
self.sdesc, self.alias = [part.strip() for part in self.args.split(" as ", 2)]
|
||||
elif self.args:
|
||||
|
|
@ -1070,22 +1084,47 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
|||
def func(self):
|
||||
"Assign the recog"
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: recog <sdesc> as <alias> or forget <alias>")
|
||||
return
|
||||
sdesc = self.sdesc
|
||||
alias = self.alias.rstrip(".?!")
|
||||
sdesc = self.sdesc
|
||||
|
||||
recog_mode = self.cmdstring != "forget" and alias and sdesc
|
||||
forget_mode = self.cmdstring == "forget" and sdesc
|
||||
list_mode = not self.args
|
||||
|
||||
if not (recog_mode or forget_mode or list_mode):
|
||||
caller.msg("Usage: recog, recog <sdesc> as <alias> or forget <alias>")
|
||||
return
|
||||
|
||||
if list_mode:
|
||||
# list all previously set recogs
|
||||
all_recogs = caller.recog.all()
|
||||
if not all_recogs:
|
||||
caller.msg(
|
||||
"You recognize no-one. " "(Use 'recog <sdesc> as <alias>' to recognize people."
|
||||
)
|
||||
else:
|
||||
# note that we don't skip those failing enable_recog lock here,
|
||||
# because that would actually reveal more than we want.
|
||||
lst = "\n".join(
|
||||
" {} ({})".format(key, obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key)
|
||||
for key, obj in all_recogs.items()
|
||||
)
|
||||
caller.msg(
|
||||
f"Currently recognized (use 'recog <sdesc> as <alias>' to add "
|
||||
f"new and 'forget <alias>' to remove):\n{lst}"
|
||||
)
|
||||
return
|
||||
|
||||
prefixed_sdesc = sdesc if sdesc.startswith(_PREFIX) else _PREFIX + sdesc
|
||||
candidates = caller.location.contents
|
||||
matches = parse_sdescs_and_recogs(caller, candidates, prefixed_sdesc, search_mode=True)
|
||||
nmatches = len(matches)
|
||||
# handle 0, 1 and >1 matches
|
||||
# handle 0 and >1 matches
|
||||
if nmatches == 0:
|
||||
caller.msg(_EMOTE_NOMATCH_ERROR.format(ref=sdesc))
|
||||
elif nmatches > 1:
|
||||
reflist = [
|
||||
"%s%s%s (%s%s)"
|
||||
% (
|
||||
"{}{}{} ({}{})".format(
|
||||
inum + 1,
|
||||
_NUM_SEP,
|
||||
_RE_PREFIX.sub("", sdesc),
|
||||
|
|
@ -1095,17 +1134,20 @@ class CmdRecog(RPCommand): # assign personal alias to object in room
|
|||
for inum, obj in enumerate(matches)
|
||||
]
|
||||
caller.msg(_EMOTE_MULTIMATCH_ERROR.format(ref=sdesc, reflist="\n ".join(reflist)))
|
||||
|
||||
else:
|
||||
# one single match
|
||||
obj = matches[0]
|
||||
if not obj.access(self.obj, "enable_recog", default=True):
|
||||
# don't apply recog if object doesn't allow it (e.g. by being masked).
|
||||
caller.msg("Can't recognize someone who is masked.")
|
||||
caller.msg("It's impossible to recognize them.")
|
||||
return
|
||||
if self.cmdstring == "forget":
|
||||
if forget_mode:
|
||||
# remove existing recog
|
||||
caller.recog.remove(obj)
|
||||
caller.msg("%s will now know only '%s'." % (caller.key, obj.recog.get(obj)))
|
||||
caller.msg("%s will now know them only as '%s'." % (caller.key, obj.recog.get(obj)))
|
||||
else:
|
||||
# set recog
|
||||
sdesc = obj.sdesc.get() if hasattr(obj, "sdesc") else obj.key
|
||||
try:
|
||||
alias = caller.recog.add(obj, alias)
|
||||
|
|
@ -1378,7 +1420,7 @@ class ContribRPObject(DefaultObject):
|
|||
looker (TypedObject): The object or account that is looking
|
||||
at/getting inforamtion for this object.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
pose (bool): Include the pose (if available) in the return.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1466,7 +1508,7 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
|||
looker (TypedObject): The object or account that is looking
|
||||
at/getting inforamtion for this object.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
pose (bool): Include the pose (if available) in the return.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1509,6 +1551,20 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
|
|||
# initializing sdesc
|
||||
self.sdesc.add("A normal person")
|
||||
|
||||
def at_before_say(self, message, **kwargs):
|
||||
"""
|
||||
Called before the object says or whispers anything, return modified message.
|
||||
|
||||
Args:
|
||||
message (str): The suggested say/whisper text spoken by self.
|
||||
Keyword Args:
|
||||
whisper (bool): If True, this is a whisper rather than a say.
|
||||
|
||||
"""
|
||||
if kwargs.get("whisper"):
|
||||
return f'/me whispers "{message}"'
|
||||
return f'/me says, "{message}"'
|
||||
|
||||
def process_sdesc(self, sdesc, obj, **kwargs):
|
||||
"""
|
||||
Allows to customize how your sdesc is displayed (primarily by
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ class AuditedServerSession(ServerSession):
|
|||
Extracts messages and system data from a Session object upon message
|
||||
send or receive.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
src (str): Source of data; 'client' or 'server'. Indicates direction.
|
||||
text (str or list): Client sends messages to server in the form of
|
||||
lists. Server sends messages to client as string.
|
||||
|
|
@ -216,7 +216,7 @@ class AuditedServerSession(ServerSession):
|
|||
"""
|
||||
Generic hook for sending data out through the protocol.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): Other data to the protocol.
|
||||
|
||||
"""
|
||||
|
|
@ -234,7 +234,7 @@ class AuditedServerSession(ServerSession):
|
|||
"""
|
||||
Hook for protocols to send incoming data to the engine.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
kwargs (any): Other data from the protocol.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -169,6 +169,8 @@ class TestRPSystem(EvenniaTest):
|
|||
self.speaker.recog.remove(self.receiver1)
|
||||
self.assertEqual(self.speaker.recog.get(self.receiver1), sdesc1)
|
||||
|
||||
self.assertEqual(self.speaker.recog.all(), {"Mr Receiver2": self.receiver2})
|
||||
|
||||
def test_parse_language(self):
|
||||
self.assertEqual(
|
||||
rpsystem.parse_language(self.speaker, emote),
|
||||
|
|
@ -233,6 +235,49 @@ class TestRPSystem(EvenniaTest):
|
|||
self.assertEqual(self.speaker.search("colliding"), self.receiver2)
|
||||
|
||||
|
||||
class TestRPSystemCommands(CommandTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.char1.swap_typeclass(rpsystem.ContribRPCharacter)
|
||||
self.char2.swap_typeclass(rpsystem.ContribRPCharacter)
|
||||
|
||||
def test_commands(self):
|
||||
|
||||
self.call(
|
||||
rpsystem.CmdSdesc(), "Foobar Character", "Char's sdesc was set to 'Foobar Character'."
|
||||
)
|
||||
self.call(
|
||||
rpsystem.CmdSdesc(),
|
||||
"BarFoo Character",
|
||||
"Char2's sdesc was set to 'BarFoo Character'.",
|
||||
caller=self.char2,
|
||||
)
|
||||
self.call(rpsystem.CmdSay(), "Hello!", 'Char says, "Hello!"')
|
||||
self.call(rpsystem.CmdEmote(), "/me smiles to /barfoo.", "Char smiles to BarFoo Character")
|
||||
self.call(
|
||||
rpsystem.CmdPose(),
|
||||
"stands by the bar",
|
||||
"Pose will read 'Foobar Character stands by the bar.'.",
|
||||
)
|
||||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
"barfoo as friend",
|
||||
"Char will now remember BarFoo Character as friend.",
|
||||
)
|
||||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
"",
|
||||
"Currently recognized (use 'recog <sdesc> as <alias>' to add new "
|
||||
"and 'forget <alias>' to remove):\n friend (BarFoo Character)",
|
||||
)
|
||||
self.call(
|
||||
rpsystem.CmdRecog(),
|
||||
"friend",
|
||||
"Char will now know them only as 'BarFoo Character'",
|
||||
cmdstring="forget",
|
||||
)
|
||||
|
||||
|
||||
# Testing of ExtendedRoom contrib
|
||||
|
||||
from django.conf import settings
|
||||
|
|
@ -607,7 +652,7 @@ class TestWilderness(EvenniaTest):
|
|||
"west": (0, 1),
|
||||
"northwest": (0, 2),
|
||||
}
|
||||
for direction, correct_loc in directions.items(): # Not compatible with Python 3
|
||||
for (direction, correct_loc) in directions.items(): # Not compatible with Python 3
|
||||
new_loc = wilderness.get_new_coordinates(loc, direction)
|
||||
self.assertEqual(new_loc, correct_loc, direction)
|
||||
|
||||
|
|
@ -829,20 +874,17 @@ class TestCustomGameTime(EvenniaTest):
|
|||
|
||||
# Test dice module
|
||||
|
||||
from evennia.contrib import dice # noqa
|
||||
|
||||
@patch("random.randint", return_value=5)
|
||||
|
||||
@patch("evennia.contrib.dice.randint", return_value=5)
|
||||
class TestDice(CommandTest):
|
||||
def test_roll_dice(self, mocked_randint):
|
||||
# we must import dice here for the mocked randint to apply correctly.
|
||||
from evennia.contrib import dice
|
||||
|
||||
self.assertEqual(dice.roll_dice(6, 6, modifier=("+", 4)), mocked_randint() * 6 + 4)
|
||||
self.assertEqual(dice.roll_dice(6, 6, conditional=("<", 35)), True)
|
||||
self.assertEqual(dice.roll_dice(6, 6, conditional=(">", 33)), False)
|
||||
|
||||
def test_cmddice(self, mocked_randint):
|
||||
from evennia.contrib import dice
|
||||
|
||||
self.call(
|
||||
dice.CmdDice(), "3d6 + 4", "You roll 3d6 + 4.| Roll(s): 5, 5 and 5. Total result is 19."
|
||||
)
|
||||
|
|
@ -853,7 +895,7 @@ class TestDice(CommandTest):
|
|||
# Test email-login
|
||||
|
||||
|
||||
from evennia.contrib import email_login
|
||||
from evennia.contrib import email_login # noqa
|
||||
|
||||
|
||||
class TestEmailLogin(CommandTest):
|
||||
|
|
|
|||
|
|
@ -279,7 +279,7 @@ def spend_action(character, actions, action_name=None):
|
|||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
|
|
@ -441,11 +441,11 @@ class TBBasicTurnHandler(DefaultScript):
|
|||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.combat_actionsleft = (
|
||||
0
|
||||
) # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
)
|
||||
character.db.combat_turnhandler = (
|
||||
self
|
||||
) # Add a reference to this turn handler script to the character
|
||||
self # Add a reference to this turn handler script to the character
|
||||
)
|
||||
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
|
|
|
|||
|
|
@ -218,10 +218,10 @@ def apply_damage(defender, damage):
|
|||
def at_defeat(defeated):
|
||||
"""
|
||||
Announces the defeat of a fighter in combat.
|
||||
|
||||
|
||||
Args:
|
||||
defeated (obj): Fighter that's been defeated.
|
||||
|
||||
|
||||
Notes:
|
||||
All this does is announce a defeat message by default, but if you
|
||||
want anything else to happen to defeated fighters (like putting them
|
||||
|
|
@ -330,7 +330,7 @@ def spend_action(character, actions, action_name=None):
|
|||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
|
|
@ -438,11 +438,11 @@ class TBEquipTurnHandler(DefaultScript):
|
|||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.combat_actionsleft = (
|
||||
0
|
||||
) # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
)
|
||||
character.db.combat_turnhandler = (
|
||||
self
|
||||
) # Add a reference to this turn handler script to the character
|
||||
self # Add a reference to this turn handler script to the character
|
||||
)
|
||||
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
|
|
@ -553,8 +553,8 @@ class TBEWeapon(DefaultObject):
|
|||
self.db.damage_range = (15, 25) # Minimum and maximum damage on hit
|
||||
self.db.accuracy_bonus = 0 # Bonus to attack rolls (or penalty if negative)
|
||||
self.db.weapon_type_name = (
|
||||
"weapon"
|
||||
) # Single word for weapon - I.E. "dagger", "staff", "scimitar"
|
||||
"weapon" # Single word for weapon - I.E. "dagger", "staff", "scimitar"
|
||||
)
|
||||
|
||||
def at_drop(self, dropper):
|
||||
"""
|
||||
|
|
@ -903,10 +903,10 @@ class CmdCombatHelp(CmdHelp):
|
|||
class CmdWield(Command):
|
||||
"""
|
||||
Wield a weapon you are carrying
|
||||
|
||||
|
||||
Usage:
|
||||
wield <weapon>
|
||||
|
||||
|
||||
Select a weapon you are carrying to wield in combat. If
|
||||
you are already wielding another weapon, you will switch
|
||||
to the weapon you specify instead. Using this command in
|
||||
|
|
@ -933,7 +933,7 @@ class CmdWield(Command):
|
|||
weapon = self.caller.search(self.args, candidates=self.caller.contents)
|
||||
if not weapon:
|
||||
return
|
||||
if not weapon.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEWeapon"):
|
||||
if not weapon.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEWeapon", exact=True):
|
||||
self.caller.msg("That's not a weapon!")
|
||||
# Remember to update the path to the weapon typeclass if you move this module!
|
||||
return
|
||||
|
|
@ -955,10 +955,10 @@ class CmdWield(Command):
|
|||
class CmdUnwield(Command):
|
||||
"""
|
||||
Stop wielding a weapon.
|
||||
|
||||
|
||||
Usage:
|
||||
unwield
|
||||
|
||||
|
||||
After using this command, you will stop wielding any
|
||||
weapon you are currently wielding and become unarmed.
|
||||
"""
|
||||
|
|
@ -986,12 +986,12 @@ class CmdUnwield(Command):
|
|||
class CmdDon(Command):
|
||||
"""
|
||||
Don armor that you are carrying
|
||||
|
||||
|
||||
Usage:
|
||||
don <armor>
|
||||
|
||||
|
||||
Select armor to wear in combat. You can't use this
|
||||
command in the middle of a fight. Use the "doff"
|
||||
command in the middle of a fight. Use the "doff"
|
||||
command to remove any armor you are wearing.
|
||||
"""
|
||||
|
||||
|
|
@ -1012,7 +1012,7 @@ class CmdDon(Command):
|
|||
armor = self.caller.search(self.args, candidates=self.caller.contents)
|
||||
if not armor:
|
||||
return
|
||||
if not armor.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEArmor"):
|
||||
if not armor.is_typeclass("evennia.contrib.turnbattle.tb_equip.TBEArmor", exact=True):
|
||||
self.caller.msg("That's not armor!")
|
||||
# Remember to update the path to the armor typeclass if you move this module!
|
||||
return
|
||||
|
|
@ -1031,10 +1031,10 @@ class CmdDon(Command):
|
|||
class CmdDoff(Command):
|
||||
"""
|
||||
Stop wearing armor.
|
||||
|
||||
|
||||
Usage:
|
||||
doff
|
||||
|
||||
|
||||
After using this command, you will stop wearing any
|
||||
armor you are currently using and become unarmored.
|
||||
You can't use this command in combat.
|
||||
|
|
|
|||
|
|
@ -348,7 +348,7 @@ def spend_action(character, actions, action_name=None):
|
|||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
|
|
@ -718,11 +718,11 @@ class TBItemsTurnHandler(DefaultScript):
|
|||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.combat_actionsleft = (
|
||||
0
|
||||
) # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
)
|
||||
character.db.combat_turnhandler = (
|
||||
self
|
||||
) # Add a reference to this turn handler script to the character
|
||||
self # Add a reference to this turn handler script to the character
|
||||
)
|
||||
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
|
|
|
|||
|
|
@ -44,6 +44,12 @@ instead of the default:
|
|||
|
||||
class Character(TBMagicCharacter):
|
||||
|
||||
Note: If your character already existed you need to also make sure
|
||||
to re-run the creation hooks on it to set the needed Attributes.
|
||||
Use `update self` to try on yourself or use py to call `at_object_creation()`
|
||||
on all existing Characters.
|
||||
|
||||
|
||||
Next, import this module into your default_cmdsets.py module:
|
||||
|
||||
from evennia.contrib.turnbattle import tb_magic
|
||||
|
|
@ -199,10 +205,10 @@ def apply_damage(defender, damage):
|
|||
def at_defeat(defeated):
|
||||
"""
|
||||
Announces the defeat of a fighter in combat.
|
||||
|
||||
|
||||
Args:
|
||||
defeated (obj): Fighter that's been defeated.
|
||||
|
||||
|
||||
Notes:
|
||||
All this does is announce a defeat message by default, but if you
|
||||
want anything else to happen to defeated fighters (like putting them
|
||||
|
|
@ -298,7 +304,7 @@ def spend_action(character, actions, action_name=None):
|
|||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
|
|
@ -332,7 +338,7 @@ class TBMagicCharacter(DefaultCharacter):
|
|||
"""
|
||||
Called once, when this object is first created. This is the
|
||||
normal hook to overload for most object types.
|
||||
|
||||
|
||||
Adds attributes for a character's current and maximum HP.
|
||||
We're just going to set this value at '100' by default.
|
||||
|
||||
|
|
@ -464,11 +470,11 @@ class TBMagicTurnHandler(DefaultScript):
|
|||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.combat_actionsleft = (
|
||||
0
|
||||
) # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
)
|
||||
character.db.combat_turnhandler = (
|
||||
self
|
||||
) # Add a reference to this turn handler script to the character
|
||||
self # Add a reference to this turn handler script to the character
|
||||
)
|
||||
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
|
|
@ -731,26 +737,26 @@ class CmdDisengage(Command):
|
|||
class CmdLearnSpell(Command):
|
||||
"""
|
||||
Learn a magic spell.
|
||||
|
||||
|
||||
Usage:
|
||||
learnspell <spell name>
|
||||
|
||||
|
||||
Adds a spell by name to your list of spells known.
|
||||
|
||||
|
||||
The following spells are provided as examples:
|
||||
|
||||
|
||||
|wmagic missile|n (3 MP): Fires three missiles that never miss. Can target
|
||||
up to three different enemies.
|
||||
|
||||
|
||||
|wflame shot|n (3 MP): Shoots a high-damage jet of flame at one target.
|
||||
|
||||
|
||||
|wcure wounds|n (5 MP): Heals damage on one target.
|
||||
|
||||
|
||||
|wmass cure wounds|n (10 MP): Like 'cure wounds', but can heal up to 5
|
||||
targets at once.
|
||||
|
||||
|wfull heal|n (12 MP): Heals one target back to full HP.
|
||||
|
||||
|
||||
|wcactus conjuration|n (2 MP): Creates a cactus.
|
||||
"""
|
||||
|
||||
|
|
@ -803,10 +809,10 @@ class CmdCast(MuxCommand):
|
|||
"""
|
||||
Cast a magic spell that you know, provided you have the MP
|
||||
to spend on its casting.
|
||||
|
||||
|
||||
Usage:
|
||||
cast <spellname> [= <target1>, <target2>, etc...]
|
||||
|
||||
|
||||
Some spells can be cast on multiple targets, some can be cast
|
||||
on only yourself, and some don't need a target specified at all.
|
||||
Typing 'cast' by itself will give you a list of spells you know.
|
||||
|
|
@ -818,7 +824,7 @@ class CmdCast(MuxCommand):
|
|||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
|
||||
|
||||
Note: This is a quite long command, since it has to cope with all
|
||||
the different circumstances in which you may or may not be able
|
||||
to cast a spell. None of the spell's effects are handled by the
|
||||
|
|
@ -1123,7 +1129,7 @@ in the docstring for each function.
|
|||
def spell_healing(caster, spell_name, targets, cost, **kwargs):
|
||||
"""
|
||||
Spell that restores HP to a target or targets.
|
||||
|
||||
|
||||
kwargs:
|
||||
healing_range (tuple): Minimum and maximum amount healed to
|
||||
each target. (20, 40) by default.
|
||||
|
|
@ -1156,7 +1162,7 @@ def spell_healing(caster, spell_name, targets, cost, **kwargs):
|
|||
def spell_attack(caster, spell_name, targets, cost, **kwargs):
|
||||
"""
|
||||
Spell that deals damage in combat. Similar to resolve_attack.
|
||||
|
||||
|
||||
kwargs:
|
||||
attack_name (tuple): Single and plural describing the sort of
|
||||
attack or projectile that strikes each enemy.
|
||||
|
|
@ -1250,12 +1256,12 @@ def spell_attack(caster, spell_name, targets, cost, **kwargs):
|
|||
def spell_conjure(caster, spell_name, targets, cost, **kwargs):
|
||||
"""
|
||||
Spell that creates an object.
|
||||
|
||||
|
||||
kwargs:
|
||||
obj_key (str): Key of the created object.
|
||||
obj_desc (str): Desc of the created object.
|
||||
obj_typeclass (str): Typeclass path of the object.
|
||||
|
||||
|
||||
If you want to make more use of this particular spell funciton,
|
||||
you may want to modify it to use the spawner (in evennia.utils.spawner)
|
||||
instead of creating objects directly.
|
||||
|
|
@ -1300,7 +1306,7 @@ parameters, some of which are required and others which are optional.
|
|||
|
||||
Required values for spells:
|
||||
|
||||
cost (int): MP cost of casting the spell
|
||||
cost (int): MP cost of casting the spell
|
||||
target (str): Valid targets for the spell. Can be any of:
|
||||
"none" - No target needed
|
||||
"self" - Self only
|
||||
|
|
@ -1312,9 +1318,9 @@ Required values for spells:
|
|||
spellfunc (callable): Function that performs the action of the spell.
|
||||
Must take the following arguments: caster (obj), spell_name (str),
|
||||
targets (list), and cost (int), as well as **kwargs.
|
||||
|
||||
|
||||
Optional values for spells:
|
||||
|
||||
|
||||
combat_spell (bool): If the spell can be cast in combat. True by default.
|
||||
noncombat_spell (bool): If the spell can be cast out of combat. True by default.
|
||||
max_targets (int): Maximum number of objects that can be targeted by the spell.
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ And change your game's character typeclass to inherit from TBRangeCharacter
|
|||
instead of the default:
|
||||
|
||||
class Character(TBRangeCharacter):
|
||||
|
||||
|
||||
Do the same thing in your game's objects.py module for TBRangeObject:
|
||||
|
||||
from evennia.contrib.turnbattle.tb_range import TBRangeObject
|
||||
|
|
@ -246,10 +246,10 @@ def apply_damage(defender, damage):
|
|||
def at_defeat(defeated):
|
||||
"""
|
||||
Announces the defeat of a fighter in combat.
|
||||
|
||||
|
||||
Args:
|
||||
defeated (obj): Fighter that's been defeated.
|
||||
|
||||
|
||||
Notes:
|
||||
All this does is announce a defeat message by default, but if you
|
||||
want anything else to happen to defeated fighters (like putting them
|
||||
|
|
@ -300,11 +300,11 @@ def resolve_attack(attacker, defender, attack_type, attack_value=None, defense_v
|
|||
def get_range(obj1, obj2):
|
||||
"""
|
||||
Gets the combat range between two objects.
|
||||
|
||||
|
||||
Args:
|
||||
obj1 (obj): First object
|
||||
obj2 (obj): Second object
|
||||
|
||||
|
||||
Returns:
|
||||
range (int or None): Distance between two objects or None if not applicable
|
||||
"""
|
||||
|
|
@ -324,7 +324,7 @@ def get_range(obj1, obj2):
|
|||
def distance_inc(mover, target):
|
||||
"""
|
||||
Function that increases distance in range field between mover and target.
|
||||
|
||||
|
||||
Args:
|
||||
mover (obj): The object moving
|
||||
target (obj): The object to be moved away from
|
||||
|
|
@ -340,11 +340,11 @@ def distance_inc(mover, target):
|
|||
def approach(mover, target):
|
||||
"""
|
||||
Manages a character's whole approach, including changes in ranges to other characters.
|
||||
|
||||
|
||||
Args:
|
||||
mover (obj): The object moving
|
||||
target (obj): The object to be moved toward
|
||||
|
||||
|
||||
Notes:
|
||||
The mover will also automatically move toward any objects that are closer to the
|
||||
target than the mover is. The mover will also move away from anything they started
|
||||
|
|
@ -354,7 +354,7 @@ def approach(mover, target):
|
|||
def distance_dec(mover, target):
|
||||
"""
|
||||
Helper function that decreases distance in range field between mover and target.
|
||||
|
||||
|
||||
Args:
|
||||
mover (obj): The object moving
|
||||
target (obj): The object to be moved toward
|
||||
|
|
@ -388,11 +388,11 @@ def approach(mover, target):
|
|||
def withdraw(mover, target):
|
||||
"""
|
||||
Manages a character's whole withdrawal, including changes in ranges to other characters.
|
||||
|
||||
|
||||
Args:
|
||||
mover (obj): The object moving
|
||||
target (obj): The object to be moved away from
|
||||
|
||||
|
||||
Notes:
|
||||
The mover will also automatically move away from objects that are close to the target
|
||||
of their withdrawl. The mover will never inadvertently move toward anything else while
|
||||
|
|
@ -470,7 +470,7 @@ def spend_action(character, actions, action_name=None):
|
|||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
|
|
@ -540,7 +540,8 @@ class TBRangeTurnHandler(DefaultScript):
|
|||
room as its object.
|
||||
|
||||
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.
|
||||
"""
|
||||
|
||||
def at_script_creation(self):
|
||||
|
|
@ -615,7 +616,7 @@ class TBRangeTurnHandler(DefaultScript):
|
|||
def init_range(self, to_init):
|
||||
"""
|
||||
Initializes range values for an object at the start of a fight.
|
||||
|
||||
|
||||
Args:
|
||||
to_init (object): Object to initialize range field for.
|
||||
"""
|
||||
|
|
@ -638,14 +639,14 @@ class TBRangeTurnHandler(DefaultScript):
|
|||
def join_rangefield(self, to_init, anchor_obj=None, add_distance=0):
|
||||
"""
|
||||
Adds a new object to the range field of a fight in progress.
|
||||
|
||||
|
||||
Args:
|
||||
to_init (object): Object to initialize range field for.
|
||||
|
||||
Kwargs:
|
||||
|
||||
Keyword Args:
|
||||
anchor_obj (object): Object to copy range values from, or None for a random object.
|
||||
add_distance (int): Distance to put between to_init object and anchor object.
|
||||
|
||||
|
||||
"""
|
||||
# Get a list of room's contents without to_init object.
|
||||
contents = self.obj.contents
|
||||
|
|
@ -674,11 +675,11 @@ class TBRangeTurnHandler(DefaultScript):
|
|||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.combat_actionsleft = (
|
||||
0
|
||||
) # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
)
|
||||
character.db.combat_turnhandler = (
|
||||
self
|
||||
) # Add a reference to this turn handler script to the character
|
||||
self # Add a reference to this turn handler script to the character
|
||||
)
|
||||
character.db.combat_lastaction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
|
|
|
|||
|
|
@ -104,36 +104,34 @@ tutorial
|
|||
# ... and describe it.
|
||||
#
|
||||
@desc
|
||||
|gWelcome to the Evennia tutorial!|n
|
||||
|gWelcome to the Evennia tutorial-world!|n
|
||||
|
||||
This small quest shows some examples of Evennia usage.
|
||||
|
||||
|gDo you want help with how to play? Write |yintro|g to get an introduction to
|
||||
Evennia and the basics of playing!|n
|
||||
|
||||
To get into the mood of this miniature quest, imagine you are an adventurer
|
||||
out to find fame and fortune. You have heard rumours of an old castle ruin by
|
||||
the coast. In its depth a warrior princess was buried together with her
|
||||
powerful magical weapon - a valuable prize, if it's true. Of course this is a
|
||||
chance to adventure that you cannot turn down!
|
||||
|
||||
You reach the coast in the midst of a raging thunderstorm. With wind and rain
|
||||
screaming in your face you stand where the moor meet the sea along a high,
|
||||
rocky coast ...
|
||||
|
||||
Try '|yintro|n' for usage help. During the quest, write '|ytutorial|n' to get
|
||||
behind-the-scenes help anywhere, and '|ygive up|n' to abandon the quest.
|
||||
|
||||
|gwrite 'begin' to start your quest!|n
|
||||
|
||||
|
||||
The following tutorial consists of a small single-player quest
|
||||
area. The various rooms are designed to show off some of the power
|
||||
and possibilities of the Evennia mud creation system. At any time
|
||||
during this tutorial you can use the |wtutorial|n (or |wtut|n)
|
||||
command to get some background info about the room or certain objects
|
||||
to see what is going on "behind the scenes".
|
||||
|
||||
|
||||
To get into the mood of this miniature quest, imagine you are an
|
||||
adventurer out to find fame and fortune. You have heard rumours of an
|
||||
old castle ruin by the coast. In its depth a warrior princess was
|
||||
buried together with her powerful magical weapon - a valuable prize,
|
||||
if it's true. Of course this is a chance to adventure that you
|
||||
cannot turn down!
|
||||
|
||||
You reach the coast in the midst of a raging thunderstorm. With wind
|
||||
and rain screaming in your face you stand where the moor meet the sea
|
||||
along a high, rocky coast ...
|
||||
|
||||
|
||||
|g(write 'start' or 'begin' to start the tutorial. Try 'tutorial'
|
||||
to get behind-the-scenes help anywhere.)|n
|
||||
#
|
||||
# Show that the tutorial command works ...
|
||||
#
|
||||
@set here/tutorial_info =
|
||||
You just tried the tutorial command. Use it in various rooms to see
|
||||
You just tried the |wtutorial|G command. Use it in various rooms to see
|
||||
what's technically going on and what you could try in each room. The
|
||||
intro room assigns some properties to your character, like a simple
|
||||
"health" property used when fighting. Other rooms and puzzles might do
|
||||
|
|
@ -294,14 +292,14 @@ start
|
|||
on the sign.
|
||||
# Set a climbable object for discovering a hidden exit
|
||||
#
|
||||
@create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
|
||||
@create/drop gnarled old tree;tree;trees;gnarled : tutorial_world.objects.TutorialClimbable
|
||||
#
|
||||
@desc trees = Only the sturdiest of trees survive at the edge of the
|
||||
moor. A small group of huddling black things has dug in near the
|
||||
@desc tree = Only the sturdiest of trees survive at the edge of the
|
||||
moor. A small huddling black thing has dug in near the
|
||||
cliff edge, eternally pummeled by wind and salt to become an integral
|
||||
part of the gloomy scenery.
|
||||
#
|
||||
@lock trees = get:false()
|
||||
@lock tree = get:false()
|
||||
#
|
||||
@set trees/get_err_msg =
|
||||
The group of old trees have withstood the eternal wind for hundreds
|
||||
|
|
@ -475,7 +473,7 @@ north
|
|||
# regular exits back to the cliff, that is handled by the bridge
|
||||
# typeclass itself.
|
||||
#
|
||||
@dig The old bridge;bridge;tut#05
|
||||
@dig The old bridge;bridge;east;e;tut#05
|
||||
: tutorial_world.rooms.BridgeRoom
|
||||
= old bridge;east;e;bridge;hangbridge
|
||||
#
|
||||
|
|
|
|||
781
evennia/contrib/tutorial_world/intro_menu.py
Normal file
781
evennia/contrib/tutorial_world/intro_menu.py
Normal file
|
|
@ -0,0 +1,781 @@
|
|||
"""
|
||||
Intro menu / game tutor
|
||||
|
||||
Evennia contrib - Griatch 2020
|
||||
|
||||
This contrib is an intro-menu for general MUD and evennia usage using the
|
||||
EvMenu menu-templating system.
|
||||
|
||||
EvMenu templating is a way to create a menu using a string-format instead
|
||||
of creating all nodes manually. Of course, for full functionality one must
|
||||
still create the goto-callbacks.
|
||||
|
||||
"""
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import CmdSet
|
||||
from evennia.utils.evmenu import parse_menu_template, EvMenu
|
||||
|
||||
# Goto callbacks and helper resources for the menu
|
||||
|
||||
|
||||
def do_nothing(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Re-runs the current node
|
||||
"""
|
||||
return None
|
||||
|
||||
|
||||
def send_testing_tagged(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Test to send a message to a pane tagged with 'testing' in the webclient.
|
||||
|
||||
"""
|
||||
caller.msg(
|
||||
(
|
||||
"This is a message tagged with 'testing' and "
|
||||
"should appear in the pane you selected!\n "
|
||||
f"You wrote: '{raw_string}'",
|
||||
{"type": "testing"},
|
||||
)
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
# Resources for the first help-command demo
|
||||
|
||||
|
||||
class DemoCommandSetHelp(CmdSet):
|
||||
"""
|
||||
Demo the help command
|
||||
"""
|
||||
|
||||
key = "Help Demo Set"
|
||||
priority = 2
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
from evennia import default_cmds
|
||||
|
||||
self.add(default_cmds.CmdHelp())
|
||||
|
||||
|
||||
def goto_command_demo_help(caller, raw_string, **kwargs):
|
||||
"Sets things up before going to the help-demo node"
|
||||
_maintain_demo_room(caller, delete=True)
|
||||
caller.cmdset.remove(DemoCommandSetRoom)
|
||||
caller.cmdset.remove(DemoCommandSetComms)
|
||||
caller.cmdset.add(DemoCommandSetHelp) # TODO - make persistent
|
||||
return kwargs.get("gotonode") or "command_demo_help"
|
||||
|
||||
|
||||
# Resources for the comms demo
|
||||
|
||||
|
||||
class DemoCommandSetComms(CmdSet):
|
||||
"""
|
||||
Demo communications
|
||||
"""
|
||||
|
||||
key = "Color Demo Set"
|
||||
priority = 2
|
||||
no_exits = True
|
||||
no_objs = True
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
from evennia import default_cmds
|
||||
|
||||
self.add(default_cmds.CmdHelp())
|
||||
self.add(default_cmds.CmdSay())
|
||||
self.add(default_cmds.CmdPose())
|
||||
self.add(default_cmds.CmdPage())
|
||||
self.add(default_cmds.CmdColorTest())
|
||||
|
||||
|
||||
def goto_command_demo_comms(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Setup and go to the color demo node.
|
||||
"""
|
||||
caller.cmdset.remove(DemoCommandSetHelp)
|
||||
caller.cmdset.remove(DemoCommandSetRoom)
|
||||
caller.cmdset.add(DemoCommandSetComms)
|
||||
return kwargs.get("gotonode") or "comms_demo_start"
|
||||
|
||||
|
||||
# Resources for the room demo
|
||||
|
||||
_ROOM_DESC = """
|
||||
This is a small and comfortable wood cabin. Bright sunlight is shining in
|
||||
through the windows.
|
||||
|
||||
Use |ylook sign|n or |yl sign|n to examine the wooden sign nailed to the wall.
|
||||
|
||||
"""
|
||||
|
||||
_SIGN_DESC = """
|
||||
The small sign reads:
|
||||
|
||||
Good! Now try '|ylook small|n'.
|
||||
|
||||
... You'll get a multi-match error! There are two things that 'small' could
|
||||
refer to here - the 'small wooden sign' or the 'small, cozy cabin' itself. You will
|
||||
get a list of the possibilities.
|
||||
|
||||
You could either tell Evennia which one you wanted by picking a unique part
|
||||
of their name (like '|ylook cozy|n') or use the number in the list to pick
|
||||
the one you want, like this:
|
||||
|
||||
|ylook 2-small|n
|
||||
|
||||
As long as what you write is uniquely identifying you can be lazy and not
|
||||
write the full name of the thing you want to look at. Try '|ylook bo|n',
|
||||
'|yl co|n' or '|yl 1-sm|n'!
|
||||
|
||||
... Oh, and if you see database-ids like (#1245) by the name of objects,
|
||||
it's because you are playing with Builder-privileges or higher. Regular
|
||||
players will not see the numbers.
|
||||
|
||||
Next try |ylook door|n.
|
||||
|
||||
"""
|
||||
|
||||
_DOOR_DESC_OUT = """
|
||||
This is a solid wooden door leading to the outside of the cabin. Some
|
||||
text is written on it:
|
||||
|
||||
This is an |wexit|n. An exit is often named by its compass-direction like
|
||||
|weast|n, |wwest|n, |wnorthwest|n and so on, but it could be named
|
||||
anything, like this door. To use the exit, you just write its name. So by
|
||||
writing |ydoor|n you will leave the cabin.
|
||||
|
||||
"""
|
||||
|
||||
_DOOR_DESC_IN = """
|
||||
This is a solid wooden door leading to the inside of the cabin. On
|
||||
are some carved text:
|
||||
|
||||
This exit leads back into the cabin. An exit is just like any object,
|
||||
so while has a name, it can also have aliases. To get back inside
|
||||
you can both write |ydoor|n but also |yin|n.
|
||||
|
||||
"""
|
||||
|
||||
_MEADOW_DESC = """
|
||||
This is a lush meadow, just outside a cozy cabin. It's surrounded
|
||||
by trees and sunlight filters down from a clear blue sky.
|
||||
|
||||
There is a |wstone|n here. Try looking at it!
|
||||
|
||||
"""
|
||||
|
||||
_STONE_DESC = """
|
||||
This is a fist-sized stone covered in runes:
|
||||
|
||||
To pick me up, use
|
||||
|
||||
|yget stone|n
|
||||
|
||||
You can see what you carry with the |yinventory|n (|yi|n).
|
||||
|
||||
To drop me again, just write
|
||||
|
||||
|ydrop stone|n
|
||||
|
||||
Use |ynext|n when you are done exploring and want to
|
||||
continue with the tutorial.
|
||||
|
||||
"""
|
||||
|
||||
|
||||
def _maintain_demo_room(caller, delete=False):
|
||||
"""
|
||||
Handle the creation/cleanup of demo assets. We store them
|
||||
on the character and clean them when leaving the menu later.
|
||||
"""
|
||||
# this is a tuple (room, obj)
|
||||
roomdata = caller.db.tutorial_world_demo_room_data
|
||||
|
||||
if delete:
|
||||
if roomdata:
|
||||
# we delete directly for simplicity. We need to delete
|
||||
# in specific order to avoid deleting rooms moves
|
||||
# its contents to their default home-location
|
||||
prev_loc, room1, sign, room2, stone, door_out, door_in = roomdata
|
||||
caller.location = prev_loc
|
||||
sign.delete()
|
||||
stone.delete()
|
||||
door_out.delete()
|
||||
door_in.delete()
|
||||
room1.delete()
|
||||
room2.delete()
|
||||
del caller.db.tutorial_world_demo_room_data
|
||||
elif not roomdata:
|
||||
# create and describe the cabin and box
|
||||
room1 = create_object("evennia.objects.objects.DefaultRoom", key="A small, cozy cabin")
|
||||
room1.db.desc = _ROOM_DESC.lstrip()
|
||||
sign = create_object(
|
||||
"evennia.objects.objects.DefaultObject", key="small wooden sign", location=room1
|
||||
)
|
||||
sign.db.desc = _SIGN_DESC.strip()
|
||||
sign.locks.add("get:false()")
|
||||
sign.db.get_err_msg = "The sign is nailed to the wall. It's not budging."
|
||||
|
||||
# create and describe the meadow and stone
|
||||
room2 = create_object("evennia.objects.objects.DefaultRoom", key="A lush summer meadow")
|
||||
room2.db.desc = _MEADOW_DESC.lstrip()
|
||||
stone = create_object(
|
||||
"evennia.objects.objects.DefaultObject", key="carved stone", location=room2
|
||||
)
|
||||
stone.db.desc = _STONE_DESC.strip()
|
||||
|
||||
# make the linking exits
|
||||
door_out = create_object(
|
||||
"evennia.objects.objects.DefaultExit",
|
||||
key="Door",
|
||||
location=room1,
|
||||
destination=room2,
|
||||
locks=["get:false()"],
|
||||
)
|
||||
door_out.db.desc = _DOOR_DESC_OUT.strip()
|
||||
door_in = create_object(
|
||||
"evennia.objects.objects.DefaultExit",
|
||||
key="entrance to the cabin",
|
||||
aliases=["door", "in", "entrance"],
|
||||
location=room2,
|
||||
destination=room1,
|
||||
locks=["get:false()"],
|
||||
)
|
||||
door_in.db.desc = _DOOR_DESC_IN.strip()
|
||||
|
||||
# store references for easy removal later
|
||||
caller.db.tutorial_world_demo_room_data = (
|
||||
caller.location,
|
||||
room1,
|
||||
sign,
|
||||
room2,
|
||||
stone,
|
||||
door_out,
|
||||
door_in,
|
||||
)
|
||||
# move caller into room
|
||||
caller.location = room1
|
||||
|
||||
|
||||
class DemoCommandSetRoom(CmdSet):
|
||||
"""
|
||||
Demo some general in-game commands command.
|
||||
"""
|
||||
|
||||
key = "Room Demo Set"
|
||||
priority = 2
|
||||
no_exits = False
|
||||
no_objs = False
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
from evennia import default_cmds
|
||||
|
||||
self.add(default_cmds.CmdHelp())
|
||||
self.add(default_cmds.CmdLook())
|
||||
self.add(default_cmds.CmdGet())
|
||||
self.add(default_cmds.CmdDrop())
|
||||
self.add(default_cmds.CmdInventory())
|
||||
self.add(default_cmds.CmdExamine())
|
||||
self.add(default_cmds.CmdPy())
|
||||
|
||||
|
||||
def goto_command_demo_room(caller, raw_string, **kwargs):
|
||||
"""
|
||||
Setup and go to the demo-room node. Generates a little 2-room environment
|
||||
for testing out some commands.
|
||||
"""
|
||||
_maintain_demo_room(caller)
|
||||
caller.cmdset.remove(DemoCommandSetHelp)
|
||||
caller.cmdset.remove(DemoCommandSetComms)
|
||||
caller.cmdset.add(DemoCommandSetRoom)
|
||||
return "command_demo_room"
|
||||
|
||||
|
||||
def goto_cleanup_cmdsets(caller, raw_strings, **kwargs):
|
||||
"""
|
||||
Cleanup all cmdsets.
|
||||
"""
|
||||
caller.cmdset.remove(DemoCommandSetHelp)
|
||||
caller.cmdset.remove(DemoCommandSetComms)
|
||||
caller.cmdset.remove(DemoCommandSetRoom)
|
||||
return kwargs.get("gotonode")
|
||||
|
||||
|
||||
# register all callables that can be used in the menu template
|
||||
|
||||
GOTO_CALLABLES = {
|
||||
"send_testing_tagged": send_testing_tagged,
|
||||
"do_nothing": do_nothing,
|
||||
"goto_command_demo_help": goto_command_demo_help,
|
||||
"goto_command_demo_comms": goto_command_demo_comms,
|
||||
"goto_command_demo_room": goto_command_demo_room,
|
||||
"goto_cleanup_cmdsets": goto_cleanup_cmdsets,
|
||||
}
|
||||
|
||||
|
||||
# Main menu definition
|
||||
|
||||
MENU_TEMPLATE = """
|
||||
|
||||
## NODE start
|
||||
|
||||
|g** Evennia introduction wizard **|n
|
||||
|
||||
If you feel lost you can learn some of the basics of how to play a text-based
|
||||
game here. You can also learn a little about the system and how to find more
|
||||
help. You can exit this tutorial-wizard at any time by entering '|yq|n' or '|yquit|n'.
|
||||
|
||||
Press |y<return>|n or write |ynext|n to step forward. Or select a number to jump to.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
1 (next);1;next;n: What is a MUD/MU*? -> about_muds
|
||||
2: About Evennia -> about_evennia
|
||||
3: Using the webclient -> using webclient
|
||||
4: The help command -> goto_command_demo_help()
|
||||
5: Communicating with others -> goto_command_demo_help(gotonode='talk on channels')
|
||||
6: Using colors -> goto_command_demo_comms(gotonode='testing_colors')
|
||||
7: Moving and exploring -> goto_command_demo_room()
|
||||
8: Conclusions & next steps-> conclusions
|
||||
>: about_muds
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE about_muds
|
||||
|
||||
|g** About MUDs **|n
|
||||
|
||||
The term '|wMUD|n' stands for Multi-user-Dungeon or -Dimension. A MUD is
|
||||
primarily played by inserting text |wcommands|n and getting text back.
|
||||
|
||||
MUDS were the |wprecursors|n to graphical MMORPG-style games like World of
|
||||
Warcraft. While not as mainstream as they once were, comparing a text-game to a
|
||||
graphical game is like comparing a book to a movie - it's just a different
|
||||
experience altogether.
|
||||
|
||||
MUDs are |wdifferent|n from Interactive Fiction (IF) in that they are multiplayer
|
||||
and usually has a consistent game world with many stories and protagonists
|
||||
acting at the same time.
|
||||
|
||||
Like there are many different styles of graphical MMOs, there are |wmany
|
||||
variations|n of MUDs: They can be slow-paced or fast. They can cover fantasy,
|
||||
sci-fi, horror or other genres. They can allow PvP or not and be casual or
|
||||
hardcore, strategic, tactical, turn-based or play in real-time.
|
||||
|
||||
Whereas 'MUD' is arguably the most well-known term, there are other terms
|
||||
centered around particular game engines - such as MUSH, MOO, MUX, MUCK, LPMuds,
|
||||
ROMs, Diku and others. Many people that played MUDs in the past used one of
|
||||
these existing families of text game-servers, whether they knew it or not.
|
||||
|
||||
|cEvennia|n is a newer text game engine designed to emulate almost any existing
|
||||
gaming style you like and possibly any new ones you can come up with!
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: About Evennia -> about_evennia
|
||||
back to start;back;start;t: start
|
||||
>: about_evennia
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE about_evennia
|
||||
|
||||
|g** About Evennia **|n
|
||||
|
||||
|cEvennia|n is a Python game engine for creating multiplayer online text-games
|
||||
(aka MUDs, MUSHes, MUX, MOOs...). It is open-source and |wfree to use|n, also for
|
||||
commercial projects (BSD license).
|
||||
|
||||
Out of the box, Evennia provides a |wworking, if empty game|n. Whereas you can play
|
||||
via traditional telnet MUD-clients, the server runs your game's website and
|
||||
offers a |wHTML5 webclient|n so that people can play your game in their browser
|
||||
without downloading anything extra.
|
||||
|
||||
Evennia deliberately |wdoes not|n hard-code any game-specific things like
|
||||
combat-systems, races, skills, etc. They would not match what just you wanted
|
||||
anyway! Whereas we do have optional contribs with many examples, most of our
|
||||
users use them as inspiration to make their own thing.
|
||||
|
||||
Evennia is developed entirely in |wPython|n, using modern developer practices.
|
||||
The advantage of text is that even a solo developer or small team can
|
||||
realistically make a competitive multiplayer game (as compared to a graphical
|
||||
MMORPG which is one of the most expensive game types in existence to develop).
|
||||
Many also use Evennia as a |wfun way to learn Python|n!
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Using the webclient -> using webclient
|
||||
back;b: About MUDs -> about_muds
|
||||
>: using webclient
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE using webclient
|
||||
|
||||
|g** Using the Webclient **|n
|
||||
|
||||
|RNote: This is only relevant if you use Evennia's HTML5 web client. If you use a
|
||||
third-party (telnet) mud-client, you can skip this section.|n
|
||||
|
||||
Evennia's web client is (for a local install) found by pointing your browser to
|
||||
|
||||
|yhttp://localhost:4001/webclient|n
|
||||
|
||||
For a live example, the public Evennia demo can be found at
|
||||
|
||||
|yhttps://demo.evennia.com/webclient|n
|
||||
|
||||
The web client starts out having two panes - the input-pane for entering commands
|
||||
and the main window.
|
||||
|
||||
- Use |y<Return>|n (or click the arrow on the right) to send your input.
|
||||
- Use |yShift + <up/down-arrow>|n to step back and forth in your command-history.
|
||||
- Use |yShift + <Return>|n to add a new line to your input without sending.
|
||||
|
||||
There is also some |wextra|n info to learn about customizing the webclient.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
extra: Customizing the webclient -> customizing the webclient
|
||||
next;n: Playing the game -> goto_command_demo_help()
|
||||
back;b: About Evennia -> about_evennia
|
||||
back to start;start: start
|
||||
>: goto_command_demo_help()
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# this is a dead-end 'leaf' of the menu
|
||||
|
||||
## NODE customizing the webclient
|
||||
|
||||
|g** Extra hints on customizing the Webclient **|n
|
||||
|
||||
|y1)|n The panes of the webclient can be resized and you can create additional panes.
|
||||
|
||||
- Press the little plus (|w+|n) sign in the top left and a new tab will appear.
|
||||
- Click and drag the tab and pull it far to the right and release when it creates two
|
||||
panes next to each other.
|
||||
|
||||
|y2)|n You can have certain server output only appear in certain panes.
|
||||
|
||||
- In your new rightmost pane, click the diamond (⯁) symbol at the top.
|
||||
- Unselect everything and make sure to select "testing".
|
||||
- Click the diamond again so the menu closes.
|
||||
- Next, write "|ytest Hello world!|n". A test-text should appear in your rightmost pane!
|
||||
|
||||
|y3)|n You can customize general webclient settings by pressing the cogwheel in the upper
|
||||
left corner. It allows to change things like font and if the client should play sound.
|
||||
|
||||
The "message routing" allows for rerouting text matching a certain regular expression (regex)
|
||||
to a web client pane with a specific tag that you set yourself.
|
||||
|
||||
|y4)|n Close the right-hand pane with the |wX|n in the rop right corner.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
back;b: using webclient
|
||||
> test *: send tagged message to new pane -> send_testing_tagged()
|
||||
>: using webclient
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# we get here via goto_command_demo_help()
|
||||
|
||||
## NODE command_demo_help
|
||||
|
||||
|g** Playing the game **|n
|
||||
|
||||
Evennia has about |w90 default commands|n. They include useful administration/building
|
||||
commands and a few limited "in-game" commands to serve as examples. They are intended
|
||||
to be changed, extended and modified as you please.
|
||||
|
||||
First to try is |yhelp|n. This lists all commands |wcurrently|n available to you.
|
||||
|
||||
Use |yhelp <topic>|n to get specific help. Try |yhelp help|n to get help on using
|
||||
the help command. For your game you could add help about your game, lore, rules etc
|
||||
as well.
|
||||
|
||||
At the moment you only have |whelp|n and some |wChannel Names|n (the '<menu commands>'
|
||||
is just a placeholder to indicate you are using this menu).
|
||||
|
||||
We'll add more commands as we get to them in this tutorial - but we'll only
|
||||
cover a small handful. Once you exit you'll find a lot more! Now let's try
|
||||
those channels ...
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Talk on Channels -> talk on channels
|
||||
back;b: Using the webclient -> goto_cleanup_cmdsets(gotonode='using webclient')
|
||||
back to start;start: start
|
||||
>: talk on channels
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE talk on channels
|
||||
|
||||
|g** Talk on Channels **|n
|
||||
|
||||
|wChannels|n are like in-game chatrooms. The |wChannel Names|n help-category
|
||||
holds the names of the channels available to you right now. One such channel is
|
||||
|wpublic|n. Use |yhelp public|n to see how to use it. Try it:
|
||||
|
||||
|ypublic Hello World!|n
|
||||
|
||||
This will send a message to the |wpublic|n channel where everyone on that
|
||||
channel can see it. If someone else is on your server, you may get a reply!
|
||||
|
||||
Evennia can link its in-game channels to external chat networks. This allows
|
||||
you to talk with people not actually logged into the game. For
|
||||
example, the online Evennia-demo links its |wpublic|n channel to the #evennia
|
||||
IRC support channel.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Talk to people in-game -> goto_command_demo_comms()
|
||||
back;b: Finding help -> goto_command_demo_help()
|
||||
back to start;start: start
|
||||
>: goto_command_demo_comms()
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# we get here via goto_command_demo_comms()
|
||||
|
||||
## NODE comms_demo_start
|
||||
|
||||
|g** Talk to people in-game **|n
|
||||
|
||||
You can also chat with people inside the game. If you try |yhelp|n now you'll
|
||||
find you have a few more commands available for trying this out.
|
||||
|
||||
|ysay Hello there!|n
|
||||
|y'Hello there!|n
|
||||
|
||||
|wsay|n is used to talk to people in the same location you are. Everyone in the
|
||||
room will see what you have to say. A single quote |y'|n is a convenient shortcut.
|
||||
|
||||
|ypose smiles|n
|
||||
|y:smiles|n
|
||||
|
||||
|wpose|n (or |wemote|n) describes what you do to those nearby. This is a very simple
|
||||
command by default, but it can be extended to much more complex parsing in order to
|
||||
include other people/objects in the emote, reference things by a short-description etc.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Paging people -> paging_people
|
||||
back;b: Talk on Channels -> goto_command_demo_help(gotonode='talk on channels')
|
||||
back to start;start: start
|
||||
>: paging_people
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE paging_people
|
||||
|
||||
|g** Paging people **|n
|
||||
|
||||
Halfway between talking on a |wChannel|n and chatting in your current location
|
||||
with |wsay|n and |wpose|n, you can also |wpage|n people. This is like a private
|
||||
message only they can see.
|
||||
|
||||
|ypage <name> = Hello there!
|
||||
page <name1>, <name2> = Hello both of you!|n
|
||||
|
||||
If you are alone on the server, put your own name as |w<name>|n to test it and
|
||||
page yourself. Write just |ypage|n to see your latest pages. This will also show
|
||||
you if anyone paged you while you were offline.
|
||||
|
||||
(By the way - depending on which games you are used to, you may think that the
|
||||
use of |y=|n above is strange. This is a MUSH/MUX-style of syntax. For your own
|
||||
game you can change the |wpose|n command to work however you prefer).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Using colors -> testing_colors
|
||||
back;b: Talk to people in-game -> comms_demo_start
|
||||
back to start;start: start
|
||||
>: testing_colors
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE testing_colors
|
||||
|
||||
|g** U|rs|yi|gn|wg |c|yc|wo|rl|bo|gr|cs |g**|n
|
||||
|
||||
You can add color in your text by the help of tags. However, remember that not
|
||||
everyone will see your colors - it depends on their client (and some use
|
||||
screenreaders). Using color can also make text harder to read. So use it
|
||||
sparingly.
|
||||
|
||||
To start coloring something |rred|n, add a ||r (red) marker and then
|
||||
end with ||n (to go back to neutral/no-color):
|
||||
|
||||
|ysay This is a ||rred||n text!
|
||||
say This is a ||Rdark red||n text!|n
|
||||
|
||||
You can also change the background:
|
||||
|
||||
|ysay This is a ||[x||bblue text on a light-grey background!|n
|
||||
|
||||
There are 16 base colors and as many background colors (called ANSI colors). Some
|
||||
clients also supports so-called Xterm256 which gives a total of 256 colors. These are
|
||||
given as |w||rgb|n, where r, g, b are the components of red, green and blue from 0-5:
|
||||
|
||||
|ysay This is ||050solid green!|n
|
||||
|ysay This is ||520an orange color!|n
|
||||
|ysay This is ||[005||555white on bright blue background!|n
|
||||
|
||||
If you don't see the expected colors from the above examples, it's because your
|
||||
client does not support it - try out the Evennia webclient instead. To see all
|
||||
color codes printed, try
|
||||
|
||||
|ycolor ansi
|
||||
|ycolor xterm
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Moving and Exploring -> goto_command_demo_room()
|
||||
back;b: Paging people -> goto_command_demo_comms(gotonode='paging_people')
|
||||
back to start;start: start
|
||||
>: goto_command_demo_room()
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
# we get here via goto_command_demo_room()
|
||||
|
||||
## NODE command_demo_room
|
||||
|
||||
|gMoving and Exploring|n
|
||||
|
||||
For exploring the game, a very important command is '|ylook|n'. It's also
|
||||
abbreviated '|yl|n' since it's used so much. Looking displays/redisplays your
|
||||
current location. You can also use it to look closer at items in the world. So
|
||||
far in this tutorial, using 'look' would just redisplay the menu.
|
||||
|
||||
Try |ylook|n now. You have been quietly transported to a sunny cabin to look
|
||||
around in. Explore a little and use |ynext|n when you are done.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
next;n: Conclusions -> conclusions
|
||||
back;b: Channel commands -> goto_command_demo_comms(gotonode='testing_colors')
|
||||
back to start;start: start
|
||||
>: conclusions
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE conclusions
|
||||
|
||||
|gConclusions|n
|
||||
|
||||
That concludes this little quick-intro to using the base game commands of
|
||||
Evennia. With this you should be able to continue exploring and also find help
|
||||
if you get stuck!
|
||||
|
||||
Write |ynext|n to end this wizard and continue to the tutorial-world quest!
|
||||
If you want there is also some |wextra|n info for where to go beyond that.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
extra: Where to go next -> post scriptum
|
||||
next;next;n: End -> end
|
||||
back;b: Moving and Exploring -> goto_command_demo_room()
|
||||
back to start;start: start
|
||||
>: end
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## NODE post scriptum
|
||||
|
||||
|gWhere to next?|n
|
||||
|
||||
After playing through the tutorial-world quest, if you aim to make a game with
|
||||
Evennia you are wise to take a look at the |wEvennia documentation|n at
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki|n
|
||||
|
||||
- You can start by trying to build some stuff by following the |wBuilder quick-start|n:
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Building-Quickstart|n
|
||||
|
||||
- The tutorial-world may or may not be your cup of tea, but it does show off
|
||||
several |wuseful tools|n of Evennia. You may want to check out how it works:
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Tutorial-World-Introduction|n
|
||||
|
||||
- You can then continue looking through the |wTutorials|n and pick one that
|
||||
fits your level of understanding.
|
||||
|
||||
|yhttps://github.com/evennia/evennia/wiki/Tutorials|n
|
||||
|
||||
- Make sure to |wjoin our forum|n and connect to our |wsupport chat|n! The
|
||||
Evennia community is very active and friendly and no question is too simple.
|
||||
You will often quickly get help. You can everything you need linked from
|
||||
|
||||
|yhttp://www.evennia.com|n
|
||||
|
||||
# ---------------------------------------------------------------------------------
|
||||
|
||||
## OPTIONS
|
||||
|
||||
back: conclusions
|
||||
>: conclusions
|
||||
|
||||
|
||||
## NODE end
|
||||
|
||||
|gGood luck!|n
|
||||
|
||||
"""
|
||||
|
||||
|
||||
# -------------------------------------------------------------------------------------------
|
||||
#
|
||||
# EvMenu implementation and access function
|
||||
#
|
||||
# -------------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
class TutorialEvMenu(EvMenu):
|
||||
"""
|
||||
Custom EvMenu for displaying the intro-menu
|
||||
"""
|
||||
|
||||
def close_menu(self):
|
||||
"""Custom cleanup actions when closing menu"""
|
||||
self.caller.cmdset.remove(DemoCommandSetHelp)
|
||||
self.caller.cmdset.remove(DemoCommandSetRoom)
|
||||
self.caller.cmdset.remove(DemoCommandSetComms)
|
||||
_maintain_demo_room(self.caller, delete=True)
|
||||
super().close_menu()
|
||||
|
||||
def options_formatter(self, optionslist):
|
||||
|
||||
navigation_keys = ("next", "back", "back to start")
|
||||
|
||||
other = []
|
||||
navigation = []
|
||||
for key, desc in optionslist:
|
||||
if key in navigation_keys:
|
||||
desc = f" ({desc})" if desc else ""
|
||||
navigation.append(f"|lc{key}|lt|w{key}|n|le{desc}")
|
||||
else:
|
||||
other.append((key, desc))
|
||||
navigation = (
|
||||
(" " + " |W|||n ".join(navigation) + " |W|||n " + "|wQ|Wuit|n") if navigation else ""
|
||||
)
|
||||
other = super().options_formatter(other)
|
||||
sep = "\n\n" if navigation and other else ""
|
||||
|
||||
return f"{navigation}{sep}{other}"
|
||||
|
||||
|
||||
def init_menu(caller):
|
||||
"""
|
||||
Call to initialize the menu.
|
||||
|
||||
"""
|
||||
menutree = parse_menu_template(caller, MENU_TEMPLATE, GOTO_CALLABLES)
|
||||
TutorialEvMenu(caller, menutree)
|
||||
|
|
@ -377,6 +377,11 @@ class Mob(tut_objects.TutorialObject):
|
|||
attack_cmd = random.choice(("thrust", "pierce", "stab", "slash", "chop"))
|
||||
self.execute_cmd("%s %s" % (attack_cmd, target))
|
||||
|
||||
if target.db.health is None:
|
||||
# This is not an attackable target
|
||||
logger.log_err(f"{self.key} found {target} had an `health` attribute of `None`.")
|
||||
return
|
||||
|
||||
# analyze the current state
|
||||
if target.db.health <= 0:
|
||||
# we reduced the target to <= 0 health. Move them to the
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ WeaponRack
|
|||
import random
|
||||
|
||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet
|
||||
from evennia.utils import search, delay
|
||||
from evennia.utils import search, delay, dedent
|
||||
from evennia.prototypes.spawner import spawn
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -647,7 +647,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
"""called when the object is first created."""
|
||||
super().at_object_creation()
|
||||
|
||||
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret door"])
|
||||
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret"])
|
||||
|
||||
# starting root positions. H1/H2 are the horizontally hanging roots,
|
||||
# V1/V2 the vertically hanging ones. Each can have three positions:
|
||||
|
|
@ -688,6 +688,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
# start a 45 second timer before closing again. We store the deferred so it can be
|
||||
# killed in unittesting.
|
||||
self.deferred = delay(45, self.reset)
|
||||
return True
|
||||
|
||||
def _translate_position(self, root, ipos):
|
||||
"""Translates the position into words"""
|
||||
|
|
@ -740,7 +741,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
"The wall is old and covered with roots that here and there have permeated the stone. "
|
||||
"The roots (or whatever they are - some of them are covered in small nondescript flowers) "
|
||||
"crisscross the wall, making it hard to clearly see its stony surface. Maybe you could "
|
||||
"try to |wshift|n or |wmove|n them.\n"
|
||||
"try to |wshift|n or |wmove|n them (like '|wshift red up|n').\n"
|
||||
]
|
||||
# display the root positions to help with the puzzle
|
||||
for key, pos in self.db.root_pos.items():
|
||||
|
|
@ -833,6 +834,7 @@ class CmdAttack(Command):
|
|||
"stab",
|
||||
"slash",
|
||||
"chop",
|
||||
"bash",
|
||||
"parry",
|
||||
"defend",
|
||||
]
|
||||
|
|
@ -875,7 +877,7 @@ class CmdAttack(Command):
|
|||
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
|
||||
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
|
||||
self.caller.db.combat_parry_mode = False
|
||||
elif cmdstring in ("slash", "chop"):
|
||||
elif cmdstring in ("slash", "chop", "bash"):
|
||||
hit = float(self.obj.db.hit) # un modified due to slash
|
||||
damage = self.obj.db.damage # un modified due to slash
|
||||
string = "You slash with %s. " % self.obj.key
|
||||
|
|
@ -1150,7 +1152,15 @@ class WeaponRack(TutorialObject):
|
|||
self.db.rack_id = "weaponrack_1"
|
||||
# these are prototype names from the prototype
|
||||
# dictionary above.
|
||||
self.db.get_weapon_msg = "You find |c%s|n."
|
||||
self.db.get_weapon_msg = dedent(
|
||||
"""
|
||||
You find |c%s|n. While carrying this weapon, these actions are available:
|
||||
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
|
||||
|wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
|
||||
|wdefend/parry|n - protect yourself and make yourself harder to hit.)
|
||||
"""
|
||||
).strip()
|
||||
|
||||
self.db.no_more_weapons_msg = "you find nothing else of use."
|
||||
self.db.available_weapons = ["knife", "dagger", "sword", "club"]
|
||||
|
||||
|
|
|
|||
|
|
@ -69,11 +69,14 @@ class CmdTutorial(Command):
|
|||
target = caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
helptext = target.db.tutorial_info
|
||||
helptext = target.db.tutorial_info or ""
|
||||
|
||||
if helptext:
|
||||
caller.msg("|G%s|n" % helptext)
|
||||
helptext = f" |G{helptext}|n"
|
||||
else:
|
||||
caller.msg("|RSorry, there is no tutorial help available here.|n")
|
||||
helptext = " |RSorry, there is no tutorial help available here.|n"
|
||||
helptext += "\n\n (Write 'give up' if you want to abandon your quest.)"
|
||||
caller.msg(helptext)
|
||||
|
||||
|
||||
# for the @detail command we inherit from MuxCommand, since
|
||||
|
|
@ -201,6 +204,30 @@ class CmdTutorialLook(default_cmds.CmdLook):
|
|||
return
|
||||
|
||||
|
||||
class CmdTutorialGiveUp(default_cmds.MuxCommand):
|
||||
"""
|
||||
Give up the tutorial-world quest and return to Limbo, the start room of the
|
||||
server.
|
||||
|
||||
"""
|
||||
|
||||
key = "give up"
|
||||
aliases = ["abort"]
|
||||
|
||||
def func(self):
|
||||
outro_room = OutroRoom.objects.all()
|
||||
if outro_room:
|
||||
outro_room = outro_room[0]
|
||||
else:
|
||||
self.caller.msg(
|
||||
"That didn't work (seems like a bug). "
|
||||
"Try to use the |wteleport|n command instead."
|
||||
)
|
||||
return
|
||||
|
||||
self.caller.move_to(outro_room)
|
||||
|
||||
|
||||
class TutorialRoomCmdSet(CmdSet):
|
||||
"""
|
||||
Implements the simple tutorial cmdset. This will overload the look
|
||||
|
|
@ -216,6 +243,7 @@ class TutorialRoomCmdSet(CmdSet):
|
|||
self.add(CmdTutorial())
|
||||
self.add(CmdTutorialSetDetail())
|
||||
self.add(CmdTutorialLook())
|
||||
self.add(CmdTutorialGiveUp())
|
||||
|
||||
|
||||
class TutorialRoom(DefaultRoom):
|
||||
|
|
@ -363,6 +391,34 @@ SUPERUSER_WARNING = (
|
|||
# -------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdEvenniaIntro(Command):
|
||||
"""
|
||||
Start the Evennia intro wizard.
|
||||
|
||||
Usage:
|
||||
intro
|
||||
|
||||
"""
|
||||
|
||||
key = "intro"
|
||||
|
||||
def func(self):
|
||||
from .intro_menu import init_menu
|
||||
|
||||
# quell also superusers
|
||||
if self.caller.account:
|
||||
self.caller.account.execute_cmd("quell")
|
||||
self.caller.msg("(Auto-quelling)")
|
||||
init_menu(self.caller)
|
||||
|
||||
|
||||
class CmdSetEvenniaIntro(CmdSet):
|
||||
key = "Evennia Intro StartSet"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
self.add(CmdEvenniaIntro())
|
||||
|
||||
|
||||
class IntroRoom(TutorialRoom):
|
||||
"""
|
||||
Intro room
|
||||
|
|
@ -381,6 +437,7 @@ class IntroRoom(TutorialRoom):
|
|||
"This assigns the health Attribute to "
|
||||
"the account."
|
||||
)
|
||||
self.cmdset.add(CmdSetEvenniaIntro, permanent=True)
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
|
|
@ -396,7 +453,12 @@ class IntroRoom(TutorialRoom):
|
|||
|
||||
if character.is_superuser:
|
||||
string = "-" * 78 + SUPERUSER_WARNING + "-" * 78
|
||||
character.msg("|r%s|n" % string.format(name=character.key, quell="|w@quell|r"))
|
||||
character.msg("|r%s|n" % string.format(name=character.key, quell="|wquell|r"))
|
||||
else:
|
||||
# quell user
|
||||
if character.account:
|
||||
character.account.execute_cmd("quell")
|
||||
character.msg("(Auto-quelling while in tutorial-world)")
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -617,7 +679,7 @@ class BridgeCmdSet(CmdSet):
|
|||
"""This groups the bridge commands. We will store it on the room."""
|
||||
|
||||
key = "Bridge commands"
|
||||
priority = 1 # this gives it precedence over the normal look/help commands.
|
||||
priority = 2 # this gives it precedence over the normal look/help commands.
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""Called at first cmdset creation"""
|
||||
|
|
@ -679,7 +741,7 @@ class BridgeRoom(WeatherRoom):
|
|||
self.db.east_exit = "gate"
|
||||
self.db.fall_exit = "cliffledge"
|
||||
# add the cmdset on the room.
|
||||
self.cmdset.add_default(BridgeCmdSet)
|
||||
self.cmdset.add(BridgeCmdSet, permanent=True)
|
||||
# since the default Character's at_look() will access the room's
|
||||
# return_description (this skips the cmdset) when
|
||||
# first entering it, we need to explicitly turn off the room
|
||||
|
|
@ -1108,3 +1170,7 @@ class OutroRoom(TutorialRoom):
|
|||
if obj.typeclass_path.startswith("evennia.contrib.tutorial_world"):
|
||||
obj.delete()
|
||||
character.tags.clear(category="tutorial_world")
|
||||
|
||||
def at_object_leave(self, character, destination):
|
||||
if character.account:
|
||||
character.account.execute_cmd("unquell")
|
||||
|
|
|
|||
|
|
@ -98,7 +98,7 @@ class UnixCommandParser(argparse.ArgumentParser):
|
|||
epilog (str): the epilog to show below options.
|
||||
command (Command): the command calling the parser.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Additional keyword arguments are directly sent to
|
||||
`argparse.ArgumentParser`. You will find them on the
|
||||
[parser's documentation](https://docs.python.org/2/library/argparse.html).
|
||||
|
|
|
|||
|
|
@ -5,7 +5,7 @@ Commands describe the input the account can do to the game.
|
|||
|
||||
"""
|
||||
|
||||
from evennia import Command as BaseCommand
|
||||
from evennia.commands.command import Command as BaseCommand
|
||||
|
||||
# from evennia import default_cmds
|
||||
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ def at_search_result(matches, caller, query="", quiet=False, **kwargs):
|
|||
quiet (bool, optional): If `True`, no messages will be echoed to caller
|
||||
on errors.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
nofound_string (str): Replacement string to echo on a notfound error.
|
||||
multimatch_string (str): Replacement string to echo on a multimatch error.
|
||||
|
||||
|
|
|
|||
|
|
@ -32,7 +32,6 @@ CONNECTION_SCREEN = """
|
|||
If you need to create an account, type (without the <>'s):
|
||||
|wcreate <username> <password>|n
|
||||
|
||||
If you have spaces in your username, enclose it in quotes.
|
||||
Enter |whelp|n for more info. |wlook|n will re-show this screen.
|
||||
|b==============================================================|n""".format(
|
||||
settings.SERVERNAME, utils.get_evennia_version("short")
|
||||
|
|
|
|||
|
|
@ -20,11 +20,11 @@ needed on the Evennia side.
|
|||
|
||||
MSSPTable = {
|
||||
# Required fields
|
||||
"NAME": "Evennia",
|
||||
"NAME": "Mygame", # usually the same as SERVERNAME
|
||||
# Generic
|
||||
"CRAWL DELAY": "-1", # limit how often crawler updates the listing. -1 for no limit
|
||||
"HOSTNAME": "", # current or new hostname
|
||||
"PORT": ["4000"], # most important port should be *last* in list!
|
||||
"CRAWL DELAY": "-1", # limit how often crawler may update the listing. -1 for no limit
|
||||
"HOSTNAME": "", # telnet hostname
|
||||
"PORT": ["4000"], # telnet port - most important port should be *last* in list!
|
||||
"CODEBASE": "Evennia",
|
||||
"CONTACT": "", # email for contacting the mud
|
||||
"CREATED": "", # year MUD was created
|
||||
|
|
@ -33,7 +33,7 @@ MSSPTable = {
|
|||
"LANGUAGE": "", # name of language used, e.g. English
|
||||
"LOCATION": "", # full English name of server country
|
||||
"MINIMUM AGE": "0", # set to 0 if not applicable
|
||||
"WEBSITE": "www.evennia.com",
|
||||
"WEBSITE": "", # http:// address to your game website
|
||||
# Categorisation
|
||||
"FAMILY": "Custom", # evennia goes under 'Custom'
|
||||
"GENRE": "None", # Adult, Fantasy, Historical, Horror, Modern, None, or Science Fiction
|
||||
|
|
@ -41,10 +41,10 @@ MSSPTable = {
|
|||
# Player versus Player, Player versus Environment,
|
||||
# Roleplaying, Simulation, Social or Strategy
|
||||
"GAMEPLAY": "",
|
||||
"STATUS": "Open Beta", # Alpha, Closed Beta, Open Beta, Live
|
||||
"STATUS": "Open Beta", # Allowed: Alpha, Closed Beta, Open Beta, Live
|
||||
"GAMESYSTEM": "Custom", # D&D, d20 System, World of Darkness, etc. Use Custom if homebrew
|
||||
# Subgenre: LASG, Medieval Fantasy, World War II, Frankenstein,
|
||||
# Cyberpunk, Dragonlance, etc. Or None if not available.
|
||||
# Cyberpunk, Dragonlance, etc. Or None if not applicable.
|
||||
"SUBGENRE": "None",
|
||||
# World
|
||||
"AREAS": "0",
|
||||
|
|
@ -56,7 +56,7 @@ MSSPTable = {
|
|||
"LEVELS": "0", # use 0 if level-less
|
||||
"RACES": "0", # use 0 if race-less
|
||||
"SKILLS": "0", # use 0 if skill-less
|
||||
# Protocols set to 1 or 0)
|
||||
# Protocols set to 1 or 0; should usually not be changed)
|
||||
"ANSI": "1",
|
||||
"GMCP": "1",
|
||||
"MSDP": "1",
|
||||
|
|
|
|||
|
|
@ -26,3 +26,16 @@ def at_webserver_root_creation(web_root):
|
|||
|
||||
"""
|
||||
return web_root
|
||||
|
||||
|
||||
def at_webproxy_root_creation(web_root):
|
||||
"""
|
||||
This function can modify the portal proxy service.
|
||||
Args:
|
||||
web_root (evennia.server.webserver.Website): The Evennia
|
||||
Website application. Use .putChild() to add new
|
||||
subdomains that are Portal-accessible over TCP;
|
||||
primarily for new protocol development, but suitable
|
||||
for other shenanigans.
|
||||
"""
|
||||
return web_root
|
||||
|
|
|
|||
|
|
@ -2,40 +2,56 @@
|
|||
Prototypes
|
||||
|
||||
A prototype is a simple way to create individualized instances of a
|
||||
given `Typeclass`. For example, you might have a Sword typeclass that
|
||||
implements everything a Sword would need to do. The only difference
|
||||
between different individual Swords would be their key, description
|
||||
and some Attributes. The Prototype system allows to create a range of
|
||||
such Swords with only minor variations. Prototypes can also inherit
|
||||
and combine together to form entire hierarchies (such as giving all
|
||||
Sabres and all Broadswords some common properties). Note that bigger
|
||||
variations, such as custom commands or functionality belong in a
|
||||
hierarchy of typeclasses instead.
|
||||
given typeclass. It is dictionary with specific key names.
|
||||
|
||||
Example prototypes are read by the `@spawn` command but is also easily
|
||||
available to use from code via `evennia.spawn` or `evennia.utils.spawner`.
|
||||
Each prototype should be a dictionary. Use the same name as the
|
||||
variable to refer to other prototypes.
|
||||
For example, you might have a Sword typeclass that implements everything a
|
||||
Sword would need to do. The only difference between different individual Swords
|
||||
would be their key, description and some Attributes. The Prototype system
|
||||
allows to create a range of such Swords with only minor variations. Prototypes
|
||||
can also inherit and combine together to form entire hierarchies (such as
|
||||
giving all Sabres and all Broadswords some common properties). Note that bigger
|
||||
variations, such as custom commands or functionality belong in a hierarchy of
|
||||
typeclasses instead.
|
||||
|
||||
A prototype can either be a dictionary placed into a global variable in a
|
||||
python module (a 'module-prototype') or stored in the database as a dict on a
|
||||
special Script (a db-prototype). The former can be created just by adding dicts
|
||||
to modules Evennia looks at for prototypes, the latter is easiest created
|
||||
in-game via the `olc` command/menu.
|
||||
|
||||
Prototypes are read and used to create new objects with the `spawn` command
|
||||
or directly via `evennia.spawn` or the full path `evennia.prototypes.spawner.spawn`.
|
||||
|
||||
A prototype dictionary have the following keywords:
|
||||
|
||||
Possible keywords are:
|
||||
prototype_parent - string pointing to parent prototype of this structure.
|
||||
key - string, the main object identifier.
|
||||
typeclass - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
|
||||
location - this should be a valid object or #dbref.
|
||||
home - valid object or #dbref.
|
||||
destination - only valid for exits (object or dbref).
|
||||
- `prototype_key` - the name of the prototype. This is required for db-prototypes,
|
||||
for module-prototypes, the global variable name of the dict is used instead
|
||||
- `prototype_parent` - string pointing to parent prototype if any. Prototype inherits
|
||||
in a similar way as classes, with children overriding values in their partents.
|
||||
- `key` - string, the main object identifier.
|
||||
- `typeclass` - string, if not set, will use `settings.BASE_OBJECT_TYPECLASS`.
|
||||
- `location` - this should be a valid object or #dbref.
|
||||
- `home` - valid object or #dbref.
|
||||
- `destination` - only valid for exits (object or #dbref).
|
||||
- `permissions` - string or list of permission strings.
|
||||
- `locks` - a lock-string to use for the spawned object.
|
||||
- `aliases` - string or list of strings.
|
||||
- `attrs` - Attributes, expressed as a list of tuples on the form `(attrname, value)`,
|
||||
`(attrname, value, category)`, or `(attrname, value, category, locks)`. If using one
|
||||
of the shorter forms, defaults are used for the rest.
|
||||
- `tags` - Tags, as a list of tuples `(tag,)`, `(tag, category)` or `(tag, category, data)`.
|
||||
- Any other keywords are interpreted as Attributes with no category or lock.
|
||||
These will internally be added to `attrs` (eqivalent to `(attrname, value)`.
|
||||
|
||||
permissions - string or list of permission strings.
|
||||
locks - a lock-string.
|
||||
aliases - string or list of strings.
|
||||
|
||||
ndb_<name> - value of a nattribute (the "ndb_" part is ignored).
|
||||
any other keywords are interpreted as Attributes and their values.
|
||||
|
||||
See the `@spawn` command and `evennia.utils.spawner` for more info.
|
||||
See the `spawn` command and `evennia.prototypes.spawner.spawn` for more info.
|
||||
|
||||
"""
|
||||
|
||||
## example of module-based prototypes using
|
||||
## the variable name as `prototype_key` and
|
||||
## simple Attributes
|
||||
|
||||
# from random import randint
|
||||
#
|
||||
# GOBLIN = {
|
||||
|
|
@ -43,7 +59,8 @@ See the `@spawn` command and `evennia.utils.spawner` for more info.
|
|||
# "health": lambda: randint(20,30),
|
||||
# "resists": ["cold", "poison"],
|
||||
# "attacks": ["fists"],
|
||||
# "weaknesses": ["fire", "light"]
|
||||
# "weaknesses": ["fire", "light"],
|
||||
# "tags": = [("greenskin", "monster"), ("humanoid", "monster")]
|
||||
# }
|
||||
#
|
||||
# GOBLIN_WIZARD = {
|
||||
|
|
|
|||
|
|
@ -131,7 +131,9 @@ class HelpEntryManager(TypedObjectManager):
|
|||
for topic in topics:
|
||||
topic.help_category = default_category
|
||||
topic.save()
|
||||
string = "Help database moved to category %s" % default_category
|
||||
string = _("Help database moved to category {default_category}").format(
|
||||
default_category=default_category
|
||||
)
|
||||
logger.log_info(string)
|
||||
|
||||
def search_help(self, ostring, help_category=None):
|
||||
|
|
|
|||
BIN
evennia/locale/ru/LC_MESSAGES/django.mo
Normal file
BIN
evennia/locale/ru/LC_MESSAGES/django.mo
Normal file
Binary file not shown.
299
evennia/locale/ru/LC_MESSAGES/django.po
Normal file
299
evennia/locale/ru/LC_MESSAGES/django.po
Normal file
|
|
@ -0,0 +1,299 @@
|
|||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: Evennia Russian Translation v0.1\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2019-02-20 12:13+0000\n"
|
||||
"PO-Revision-Date: 2020-04-19 18:32+0000\n"
|
||||
"Last-Translator: 3eluk\n"
|
||||
"Language-Team: Russian (Russia)\n"
|
||||
"Language: ru-RU\n"
|
||||
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10 >= 2 && "
|
||||
"n%10<=4 &&(n%100<10||n%100 >= 20)? 1 : 2);\n"
|
||||
"MIME-Version: 1.0\n"
|
||||
"Content-Type: text/plain; charset=UTF-8\n"
|
||||
"Content-Transfer-Encoding: 8bit\n"
|
||||
"X-Loco-Source-Locale: ru_RU\n"
|
||||
"X-Generator: Loco https://localise.biz/\n"
|
||||
"X-Loco-Parser: loco_parse_po"
|
||||
|
||||
#: accounts/accounts.py:440
|
||||
msgid "Account being deleted."
|
||||
msgstr "Аккаунт удаляется."
|
||||
|
||||
#: commands/cmdhandler.py:681
|
||||
msgid "There were multiple matches."
|
||||
msgstr "Здесь было несколько совпадений."
|
||||
|
||||
#: commands/cmdhandler.py:704
|
||||
#, python-format
|
||||
msgid "Command '%s' is not available."
|
||||
msgstr "Команда '%s' недоступна."
|
||||
|
||||
#: commands/cmdhandler.py:709
|
||||
#, python-format
|
||||
msgid " Maybe you meant %s?"
|
||||
msgstr "Возможно, вы имели ввиду %s?"
|
||||
|
||||
#: commands/cmdhandler.py:709
|
||||
msgid "or"
|
||||
msgstr "или"
|
||||
|
||||
#: commands/cmdhandler.py:711
|
||||
msgid " Type \"help\" for help."
|
||||
msgstr " Введи \"справка\" для получения помощи."
|
||||
|
||||
#: commands/cmdsethandler.py:89
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"Error loading cmdset '{path}'\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:94
|
||||
msgid ""
|
||||
"Error loading cmdset: No cmdset class '{classname}' in '{path}'.\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:98
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"SyntaxError encountered when loading cmdset '{path}'.\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:103
|
||||
msgid ""
|
||||
"{traceback}\n"
|
||||
"Compile/Run error when loading cmdset '{path}'.\",\n"
|
||||
"(Traceback was logged {timestamp})"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:108
|
||||
msgid ""
|
||||
"\n"
|
||||
"Error encountered for cmdset at path '{path}'.\n"
|
||||
"Replacing with fallback '{fallback_path}'.\n"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:114
|
||||
msgid "Fallback path '{fallback_path}' failed to generate a cmdset."
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:182 commands/cmdsethandler.py:192
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
"(Unsuccessfully tried '%s')."
|
||||
msgstr ""
|
||||
"\n"
|
||||
"(Безуспешно пробую '%s')."
|
||||
|
||||
#: commands/cmdsethandler.py:311
|
||||
msgid "custom {mergetype} on cmdset '{cmdset}'"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:314
|
||||
msgid " <Merged {mergelist} {mergetype}, prio {prio}>: {current}"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:322
|
||||
msgid ""
|
||||
" <{key} ({mergetype}, prio {prio}, {permstring})>:\n"
|
||||
" {keylist}"
|
||||
msgstr ""
|
||||
|
||||
#: commands/cmdsethandler.py:426
|
||||
msgid "Only CmdSets can be added to the cmdsethandler!"
|
||||
msgstr ""
|
||||
|
||||
#: comms/channelhandler.py:100
|
||||
msgid "Say what?"
|
||||
msgstr "Сказать что?"
|
||||
|
||||
#: comms/channelhandler.py:105
|
||||
#, python-format
|
||||
msgid "Channel '%s' not found."
|
||||
msgstr "Канал '%s' не обнаружен."
|
||||
|
||||
#: comms/channelhandler.py:108
|
||||
#, python-format
|
||||
msgid "You are not connected to channel '%s'."
|
||||
msgstr "Ты не соединён с каналом '%s'."
|
||||
|
||||
#: comms/channelhandler.py:112
|
||||
#, python-format
|
||||
msgid "You are not permitted to send to channel '%s'."
|
||||
msgstr "У тебя нет разрешения слать в канал '%s'."
|
||||
|
||||
#: comms/channelhandler.py:155
|
||||
msgid " (channel)"
|
||||
msgstr " (канал)"
|
||||
|
||||
#: locks/lockhandler.py:236
|
||||
#, python-format
|
||||
msgid "Lock: lock-function '%s' is not available."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:249
|
||||
#, python-format
|
||||
msgid "Lock: definition '%s' has syntax errors."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:253
|
||||
#, python-format
|
||||
msgid ""
|
||||
"LockHandler on %(obj)s: access type '%(access_type)s' changed from '%(source)"
|
||||
"s' to '%(goal)s' "
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:320
|
||||
msgid "Lock: '{lockdef}' contains no colon (:)."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:328
|
||||
msgid "Lock: '{lockdef}' has no access_type (left-side of colon is empty)."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:336
|
||||
msgid "Lock: '{lockdef}' has mismatched parentheses."
|
||||
msgstr ""
|
||||
|
||||
#: locks/lockhandler.py:343
|
||||
msgid "Lock: '{lockdef}' has no valid lock functions."
|
||||
msgstr ""
|
||||
|
||||
#: objects/objects.py:732
|
||||
#, python-format
|
||||
msgid "Couldn't perform move ('%s'). Contact an admin."
|
||||
msgstr "Не удалось выполнить действие ('%s'). Свяжитесь с администратором."
|
||||
|
||||
#: objects/objects.py:742
|
||||
msgid "The destination doesn't exist."
|
||||
msgstr "Такой точки назначения нету."
|
||||
|
||||
#: objects/objects.py:833
|
||||
#, python-format
|
||||
msgid "Could not find default home '(#%d)'."
|
||||
msgstr "Не обнаружен дом по умолчанию '(#%d)'."
|
||||
|
||||
#: objects/objects.py:849
|
||||
msgid "Something went wrong! You are dumped into nowhere. Contact an admin."
|
||||
msgstr ""
|
||||
"Что-то пошло не так! Тебя выбрасывает в пустоту. Свяжитесь с администратором."
|
||||
|
||||
#: objects/objects.py:915
|
||||
#, python-format
|
||||
msgid "Your character %s has been destroyed."
|
||||
msgstr "Ваш персонаж %s был уничтожен."
|
||||
|
||||
#: scripts/scripthandler.py:53
|
||||
#, python-format
|
||||
msgid ""
|
||||
"\n"
|
||||
" '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s"
|
||||
msgstr ""
|
||||
|
||||
#: scripts/scripts.py:205
|
||||
#, python-format
|
||||
msgid ""
|
||||
"Script %(key)s(#%(dbid)s) of type '%(cname)s': at_repeat() error '%(err)s'."
|
||||
msgstr ""
|
||||
|
||||
#: server/initial_setup.py:28
|
||||
msgid ""
|
||||
"\n"
|
||||
"Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if "
|
||||
"you need\n"
|
||||
"help, want to contribute, report issues or just join the community.\n"
|
||||
"As Account #1 you can create a demo/tutorial area with |w@batchcommand "
|
||||
"tutorial_world.build|n.\n"
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
"Добро пожаловать в твою новую игру, основанную на |wEvennia|n! Посети http:"
|
||||
"//www.evennia.com\n"
|
||||
"если тебе нужна помощь, хочешь помочь, сообщить об ошибках, lили просто "
|
||||
"присоединиться к сообществу.\n"
|
||||
"Как Аккаунт №1, ты можешь создать зону для демонстрации/обучения командой "
|
||||
"|w@batchcommand tutorial_world.build|n.\n"
|
||||
" "
|
||||
|
||||
#: server/initial_setup.py:92
|
||||
msgid "This is User #1."
|
||||
msgstr "Это Пользователь №1."
|
||||
|
||||
#: server/initial_setup.py:105
|
||||
msgid "Limbo"
|
||||
msgstr "Лимб"
|
||||
|
||||
#: server/server.py:139
|
||||
msgid "idle timeout exceeded"
|
||||
msgstr "время бездействия превышено"
|
||||
|
||||
#: server/sessionhandler.py:386
|
||||
msgid " ... Server restarted."
|
||||
msgstr " ... Сервер перезапущен."
|
||||
|
||||
#: server/sessionhandler.py:606
|
||||
msgid "Logged in from elsewhere. Disconnecting."
|
||||
msgstr "Выполнено соединение в другом месте. Отключение."
|
||||
|
||||
#: server/sessionhandler.py:634
|
||||
msgid "Idle timeout exceeded, disconnecting."
|
||||
msgstr "Время бездействия превышено, отключение."
|
||||
|
||||
#: server/validators.py:50
|
||||
#, python-format
|
||||
msgid ""
|
||||
"%s From a terminal client, you can also use a phrase of multiple words if "
|
||||
"you enclose the password in double quotes."
|
||||
msgstr ""
|
||||
"%s Если вы используете терминал, вы можете использовать фразу из нескольких "
|
||||
"слов если возьмёте пароль в двойные скобки."
|
||||
|
||||
#: utils/evmenu.py:192
|
||||
msgid ""
|
||||
"Menu node '{nodename}' is either not implemented or caused an error. Make "
|
||||
"another choice."
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:194
|
||||
msgid "Error in menu node '{nodename}'."
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:195
|
||||
msgid "No description."
|
||||
msgstr "Нет описания."
|
||||
|
||||
#: utils/evmenu.py:196
|
||||
msgid "Commands: <menu option>, help, quit"
|
||||
msgstr "Команды: <menu option>, справка, выход"
|
||||
|
||||
#: utils/evmenu.py:197
|
||||
msgid "Commands: <menu option>, help"
|
||||
msgstr "Команды: <menu option>, справка"
|
||||
|
||||
#: utils/evmenu.py:198
|
||||
msgid "Commands: help, quit"
|
||||
msgstr ""
|
||||
|
||||
#: utils/evmenu.py:199
|
||||
msgid "Commands: help"
|
||||
msgstr "Команды: справка"
|
||||
|
||||
#: utils/evmenu.py:200
|
||||
msgid "Choose an option or try 'help'."
|
||||
msgstr "Выберите опцию или введите \"справка\"."
|
||||
|
||||
#: utils/utils.py:1866
|
||||
#, python-format
|
||||
msgid "Could not find '%s'."
|
||||
msgstr "Не обнаружено '%s'."
|
||||
|
||||
#: utils/utils.py:1873
|
||||
#, python-format
|
||||
msgid ""
|
||||
"More than one match for '%s' (please narrow target):\n"
|
||||
msgstr ""
|
||||
"Больше одного подходящего варианта для '%s' (уточните цель):\n"
|
||||
|
|
@ -539,7 +539,11 @@ def objtag(accessing_obj, accessed_obj, *args, **kwargs):
|
|||
Only true if accessed_obj has the specified tag and optional
|
||||
category.
|
||||
"""
|
||||
return bool(accessed_obj.tags.get(*args))
|
||||
if hasattr(accessed_obj, "obj"):
|
||||
accessed_obj = accessed_obj.obj
|
||||
tagkey = args[0] if args else None
|
||||
category = args[1] if len(args) > 1 else None
|
||||
return bool(accessed_obj.tags.get(tagkey, category=category))
|
||||
|
||||
|
||||
def inside(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
|
|
@ -547,11 +551,44 @@ def inside(accessing_obj, accessed_obj, *args, **kwargs):
|
|||
Usage:
|
||||
inside()
|
||||
|
||||
Only true if accessing_obj is "inside" accessed_obj
|
||||
True if accessing_obj is 'inside' accessing_obj. Note that this only checks
|
||||
one level down. So if if the lock is on a room, you will pass but not your
|
||||
inventory (since their location is you, not the locked object). If you
|
||||
want also nested objects to pass the lock, use the `insiderecursive`
|
||||
lockfunc.
|
||||
"""
|
||||
if hasattr(accessed_obj, "obj"):
|
||||
accessed_obj = accessed_obj.obj
|
||||
return accessing_obj.location == accessed_obj
|
||||
|
||||
|
||||
def inside_rec(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
"""
|
||||
Usage:
|
||||
inside_rec()
|
||||
|
||||
True if accessing_obj is inside the accessed obj, at up to 10 levels
|
||||
of recursion (so if this lock is on a room, then an object inside a box
|
||||
in your inventory will also pass the lock).
|
||||
"""
|
||||
|
||||
if hasattr(accessed_obj, "obj"):
|
||||
accessed_obj = accessed_obj.obj
|
||||
|
||||
def _recursive_inside(obj, accessed_obj, lvl=1):
|
||||
if obj.location:
|
||||
if obj.location == accessed_obj:
|
||||
return True
|
||||
elif lvl >= 10:
|
||||
# avoid infinite recursions
|
||||
return False
|
||||
else:
|
||||
return _recursive_inside(obj.location, accessed_obj, lvl + 1)
|
||||
return False
|
||||
|
||||
return _recursive_inside(accessing_obj, accessed_obj)
|
||||
|
||||
|
||||
def holds(accessing_obj, accessed_obj, *args, **kwargs):
|
||||
"""
|
||||
Usage:
|
||||
|
|
@ -604,7 +641,7 @@ def holds(accessing_obj, accessed_obj, *args, **kwargs):
|
|||
if len(args) == 1:
|
||||
# command is holds(dbref/key) - check if given objname/dbref is held by accessing_ob
|
||||
return check_holds(args[0])
|
||||
elif len(args=2):
|
||||
elif len(args) > 1:
|
||||
# command is holds(attrname, value) check if any held object has the given attribute and value
|
||||
for obj in contents:
|
||||
if obj.attributes.get(args[0]) == args[1]:
|
||||
|
|
|
|||
|
|
@ -107,7 +107,7 @@ to any other identifier you can use.
|
|||
import re
|
||||
from django.conf import settings
|
||||
from evennia.utils import logger, utils
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
__all__ = ("LockHandler", "LockException")
|
||||
|
||||
|
|
@ -236,7 +236,13 @@ class LockHandler(object):
|
|||
elist.append(_("Lock: lock-function '%s' is not available.") % funcstring)
|
||||
continue
|
||||
args = list(arg.strip() for arg in rest.split(",") if arg and "=" not in arg)
|
||||
kwargs = dict([arg.split("=", 1) for arg in rest.split(",") if arg and "=" in arg])
|
||||
kwargs = dict(
|
||||
[
|
||||
(part.strip() for part in arg.split("=", 1))
|
||||
for arg in rest.split(",")
|
||||
if arg and "=" in arg
|
||||
]
|
||||
)
|
||||
lock_funcs.append((func, args, kwargs))
|
||||
evalstring = evalstring.replace(funcstring, "%s")
|
||||
if len(lock_funcs) < nfuncs:
|
||||
|
|
@ -246,7 +252,11 @@ class LockHandler(object):
|
|||
evalstring = " ".join(_RE_OK.findall(evalstring))
|
||||
eval(evalstring % tuple(True for func in funclist), {}, {})
|
||||
except Exception:
|
||||
elist.append(_("Lock: definition '%s' has syntax errors.") % raw_lockstring)
|
||||
elist.append(
|
||||
_("Lock: definition '{lock_string}' has syntax errors.").format(
|
||||
lock_string=raw_lockstring
|
||||
)
|
||||
)
|
||||
continue
|
||||
if access_type in locks:
|
||||
duplicates += 1
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ except ImportError:
|
|||
|
||||
from evennia import settings_default
|
||||
from evennia.locks import lockfuncs
|
||||
from evennia.utils.create import create_object
|
||||
|
||||
# ------------------------------------------------------------
|
||||
# Lock testing
|
||||
|
|
@ -174,11 +175,28 @@ class TestLockfuncs(EvenniaTest):
|
|||
self.assertEqual(True, lockfuncs.objtag(None, self.obj2, "test2", "category1"))
|
||||
self.assertEqual(False, lockfuncs.objtag(None, self.obj2, "test2"))
|
||||
|
||||
def test_traverse_taglock(self):
|
||||
self.obj2.tags.add("test1", "category1")
|
||||
self.exit.locks.add("traverse:tag(test1,category1)")
|
||||
self.assertEqual(self.exit.access(self.obj2, "traverse"), True)
|
||||
|
||||
def test_traverse_taglock_fail(self):
|
||||
self.obj2.tags.add("test1") # missing the category
|
||||
self.exit.locks.add("traverse:tag(test1,category1)")
|
||||
self.assertEqual(self.exit.access(self.obj2, "traverse"), False)
|
||||
|
||||
def test_inside_holds(self):
|
||||
self.assertEqual(True, lockfuncs.inside(self.char1, self.room1))
|
||||
self.assertEqual(False, lockfuncs.inside(self.char1, self.room2))
|
||||
self.assertEqual(True, lockfuncs.holds(self.room1, self.char1))
|
||||
self.assertEqual(False, lockfuncs.holds(self.room2, self.char1))
|
||||
# test recursively
|
||||
self.assertEqual(True, lockfuncs.inside_rec(self.char1, self.room1))
|
||||
self.assertEqual(False, lockfuncs.inside_rec(self.char1, self.room2))
|
||||
inventory_item = create_object(key="InsideTester", location=self.char1)
|
||||
self.assertEqual(True, lockfuncs.inside_rec(inventory_item, self.room1))
|
||||
self.assertEqual(False, lockfuncs.inside_rec(inventory_item, self.room2))
|
||||
inventory_item.delete()
|
||||
|
||||
def test_has_account(self):
|
||||
self.assertEqual(True, lockfuncs.has_account(self.char1, None))
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ from django.contrib import admin
|
|||
from evennia.typeclasses.admin import AttributeInline, TagInline
|
||||
from evennia.objects.models import ObjectDB
|
||||
from django.contrib.admin.utils import flatten_fieldsets
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class ObjectAttributeInline(AttributeInline):
|
||||
|
|
|
|||
|
|
@ -154,12 +154,12 @@ class ObjectDBManager(TypedObjectManager):
|
|||
|
||||
Args:
|
||||
attribute_name (str): Attribute key to search for.
|
||||
attribute_value (str): Attribute value to search for.
|
||||
attribute_value (any): Attribute value to search for. This can also be database objects.
|
||||
candidates (list, optional): Candidate objects to limit search to.
|
||||
typeclasses (list, optional): Python pats to restrict matches with.
|
||||
|
||||
Returns:
|
||||
matches (list): Objects fullfilling both the `attribute_name` and
|
||||
matches (query): Objects fullfilling both the `attribute_name` and
|
||||
`attribute_value` criterions.
|
||||
|
||||
Notes:
|
||||
|
|
@ -175,31 +175,13 @@ class ObjectDBManager(TypedObjectManager):
|
|||
)
|
||||
type_restriction = typeclasses and Q(db_typeclass_path__in=make_iter(typeclasses)) or Q()
|
||||
|
||||
# This doesn't work if attribute_value is an object. Workaround below
|
||||
|
||||
if isinstance(attribute_value, (str, int, float, bool)):
|
||||
return self.filter(
|
||||
cand_restriction
|
||||
& type_restriction
|
||||
& Q(db_attributes__db_key=attribute_name, db_attributes__db_value=attribute_value)
|
||||
).order_by("id")
|
||||
else:
|
||||
# We must loop for safety since the referenced lookup gives deepcopy error if attribute value is an object.
|
||||
global _ATTR
|
||||
if not _ATTR:
|
||||
from evennia.typeclasses.models import Attribute as _ATTR
|
||||
cands = list(
|
||||
self.filter(
|
||||
cand_restriction & type_restriction & Q(db_attributes__db_key=attribute_name)
|
||||
)
|
||||
)
|
||||
results = [
|
||||
attr.objectdb_set.all()
|
||||
for attr in _ATTR.objects.filter(
|
||||
objectdb__in=cands, db_value=attribute_value
|
||||
).order_by("id")
|
||||
]
|
||||
return chain(*results)
|
||||
results = self.filter(
|
||||
cand_restriction
|
||||
& type_restriction
|
||||
& Q(db_attributes__db_key=attribute_name)
|
||||
& Q(db_attributes__db_value=attribute_value)
|
||||
).order_by("id")
|
||||
return results
|
||||
|
||||
def get_objs_with_db_property(self, property_name, candidates=None):
|
||||
"""
|
||||
|
|
@ -273,7 +255,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
to exclude from the match.
|
||||
|
||||
Returns:
|
||||
contents (list): Matching contents, without excludeobj, if given.
|
||||
contents (query): Matching contents, without excludeobj, if given.
|
||||
"""
|
||||
exclude_restriction = (
|
||||
Q(pk__in=[_GA(obj, "id") for obj in make_iter(excludeobj)]) if excludeobj else Q()
|
||||
|
|
@ -291,7 +273,7 @@ class ObjectDBManager(TypedObjectManager):
|
|||
typeclasses (list): Only match objects with typeclasses having thess path strings.
|
||||
|
||||
Returns:
|
||||
matches (list): A list of matches of length 0, 1 or more.
|
||||
matches (query): A list of matches of length 0, 1 or more.
|
||||
"""
|
||||
if not isinstance(ostring, str):
|
||||
if hasattr(ostring, "key"):
|
||||
|
|
@ -484,19 +466,25 @@ class ObjectDBManager(TypedObjectManager):
|
|||
# strips the number
|
||||
match_number, searchdata = match.group("number"), match.group("name")
|
||||
match_number = int(match_number) - 1
|
||||
match_number = match_number if match_number >= 0 else None
|
||||
if match_number is not None or not exact:
|
||||
# run search again, with the exactness set by call
|
||||
matches = _searcher(searchdata, candidates, typeclass, exact=exact)
|
||||
|
||||
# deal with result
|
||||
if len(matches) > 1 and match_number is not None:
|
||||
if len(matches) == 1 and match_number is not None and match_number != 0:
|
||||
# this indicates trying to get a single match with a match-number
|
||||
# targeting some higher-number match (like 2-box when there is only
|
||||
# one box in the room). This leads to a no-match.
|
||||
matches = []
|
||||
elif len(matches) > 1 and match_number is not None:
|
||||
# multiple matches, but a number was given to separate them
|
||||
try:
|
||||
if 0 <= match_number < len(matches):
|
||||
# limit to one match
|
||||
matches = [matches[match_number]]
|
||||
except IndexError:
|
||||
# match number not matching anything
|
||||
pass
|
||||
else:
|
||||
# a number was given outside of range. This means a no-match.
|
||||
matches = []
|
||||
|
||||
# return a list (possibly empty)
|
||||
return matches
|
||||
|
||||
|
|
@ -575,8 +563,10 @@ class ObjectDBManager(TypedObjectManager):
|
|||
return None
|
||||
|
||||
# copy over all attributes from old to new.
|
||||
for attr in original_object.attributes.all():
|
||||
new_object.attributes.add(attr.key, attr.value)
|
||||
attrs = (
|
||||
(a.key, a.value, a.category, a.lock_storage) for a in original_object.attributes.all()
|
||||
)
|
||||
new_object.attributes.batch_add(*attrs)
|
||||
|
||||
# copy over all cmdsets, if any
|
||||
for icmdset, cmdset in enumerate(original_object.cmdset.all()):
|
||||
|
|
@ -590,8 +580,10 @@ class ObjectDBManager(TypedObjectManager):
|
|||
ScriptDB.objects.copy_script(script, new_obj=new_object)
|
||||
|
||||
# copy over all tags, if any
|
||||
for tag in original_object.tags.get(return_tagobj=True, return_list=True):
|
||||
new_object.tags.add(tag=tag.db_key, category=tag.db_category, data=tag.db_data)
|
||||
tags = (
|
||||
(t.db_key, t.db_category, t.db_data) for t in original_object.tags.all(return_objs=True)
|
||||
)
|
||||
new_object.tags.batch_add(*tags)
|
||||
|
||||
return new_object
|
||||
|
||||
|
|
|
|||
|
|
@ -24,7 +24,7 @@ from evennia.utils import logger
|
|||
from evennia.utils.utils import make_iter, dbref, lazy_property
|
||||
|
||||
|
||||
class ContentsHandler(object):
|
||||
class ContentsHandler:
|
||||
"""
|
||||
Handles and caches the contents of an object to avoid excessive
|
||||
lookups (this is done very often due to cmdhandler needing to look
|
||||
|
|
@ -79,8 +79,9 @@ class ContentsHandler(object):
|
|||
try:
|
||||
return [self._idcache[pk] for pk in pks]
|
||||
except KeyError:
|
||||
# this means an actual failure of caching. Return real database match.
|
||||
logger.log_err("contents cache failed for %s." % self.obj.key)
|
||||
# this means the central instance_cache was totally flushed.
|
||||
# Re-fetching from database will rebuild the necessary parts of the cache
|
||||
# for next fetch.
|
||||
return list(ObjectDB.objects.filter(db_location=self.obj))
|
||||
|
||||
def add(self, obj):
|
||||
|
|
|
|||
|
|
@ -24,6 +24,7 @@ from evennia.utils import search
|
|||
from evennia.utils import logger
|
||||
from evennia.utils import ansi
|
||||
from evennia.utils.utils import (
|
||||
class_from_module,
|
||||
variable_from_module,
|
||||
lazy_property,
|
||||
make_iter,
|
||||
|
|
@ -31,7 +32,7 @@ from evennia.utils.utils import (
|
|||
list_to_string,
|
||||
to_str,
|
||||
)
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
_INFLECT = inflect.engine()
|
||||
_MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
|
|
@ -40,6 +41,7 @@ _ScriptDB = None
|
|||
_SESSIONS = None
|
||||
|
||||
_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))
|
||||
_COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
# the sessid_max is based on the length of the db_sessid csv field (excluding commas)
|
||||
_SESSID_MAX = 16 if _MULTISESSION_MODE in (1, 3) else 1
|
||||
|
||||
|
|
@ -206,7 +208,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
# lockstring of newly created objects, for easy overloading.
|
||||
# Will be formatted with the appropriate attributes.
|
||||
lockstring = "control:id({account_id}) or perm(Admin);" "delete:id({account_id}) or perm(Admin)"
|
||||
lockstring = "control:id({account_id}) or perm(Admin);delete:id({account_id}) or perm(Admin)"
|
||||
|
||||
objects = ObjectManager()
|
||||
|
||||
|
|
@ -333,24 +335,29 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
Args:
|
||||
count (int): Number of objects of this type
|
||||
looker (Object): Onlooker. Not used by default.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
key (str): Optional key to pluralize, if given, use this instead of the object's key.
|
||||
Returns:
|
||||
singular (str): The singular form to display.
|
||||
plural (str): The determined plural form of the key, including the count.
|
||||
"""
|
||||
plural_category = "plural_key"
|
||||
key = kwargs.get("key", self.key)
|
||||
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
|
||||
plural = _INFLECT.plural(key, 2)
|
||||
plural = "%s %s" % (_INFLECT.number_to_words(count, threshold=12), plural)
|
||||
try:
|
||||
plural = _INFLECT.plural(key, count)
|
||||
plural = "{} {}".format(_INFLECT.number_to_words(count, threshold=12), plural)
|
||||
except IndexError:
|
||||
# this is raised by inflect if the input is not a proper noun
|
||||
plural = key
|
||||
singular = _INFLECT.an(key)
|
||||
if not self.aliases.get(plural, category="plural_key"):
|
||||
if not self.aliases.get(plural, category=plural_category):
|
||||
# we need to wipe any old plurals/an/a in case key changed in the interrim
|
||||
self.aliases.clear(category="plural_key")
|
||||
self.aliases.add(plural, category="plural_key")
|
||||
self.aliases.clear(category=plural_category)
|
||||
self.aliases.add(plural, category=plural_category)
|
||||
# save the singular form as an alias here too so we can display "an egg" and also
|
||||
# look at 'an egg'.
|
||||
self.aliases.add(singular, category="plural_key")
|
||||
self.aliases.add(singular, category=plural_category)
|
||||
return singular, plural
|
||||
|
||||
def search(
|
||||
|
|
@ -384,9 +391,9 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
a global search.
|
||||
- `me,self`: self-reference to this object
|
||||
- `<num>-<string>` - can be used to differentiate
|
||||
between multiple same-named matches
|
||||
global_search (bool): Search all objects globally. This is overruled
|
||||
by `location` keyword.
|
||||
between multiple same-named matches. The exact form of this input
|
||||
is given by `settings.SEARCH_MULTIMATCH_REGEX`.
|
||||
global_search (bool): Search all objects globally. This overrules 'location' data.
|
||||
use_nicks (bool): Use nickname-replace (nicktype "object") on `searchdata`.
|
||||
typeclass (str or Typeclass, or list of either): Limit search only
|
||||
to `Objects` with this typeclass. May be a list of typeclasses
|
||||
|
|
@ -495,7 +502,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
)
|
||||
|
||||
if quiet:
|
||||
return results
|
||||
return list(results)
|
||||
return _AT_SEARCH_RESULT(
|
||||
results,
|
||||
self,
|
||||
|
|
@ -551,7 +558,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
session (Session, optional): Session to
|
||||
return results to
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Other keyword arguments will be added to the found command
|
||||
object instace as variables before it executes. This is
|
||||
unused by default Evennia but may be used to set flags and
|
||||
|
|
@ -597,7 +604,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
depends on the MULTISESSION_MODE.
|
||||
options (dict, optional): Message-specific option-value
|
||||
pairs. These will be applied at the protocol level.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
any (string or tuples): All kwarg keys not listed above
|
||||
will be treated as send-command names and their arguments
|
||||
(which can be a string or a tuple).
|
||||
|
|
@ -648,7 +655,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
exclude (list, optional): A list of object not to call the
|
||||
function on.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Keyword arguments will be passed to the function for all objects.
|
||||
"""
|
||||
contents = self.contents
|
||||
|
|
@ -680,7 +687,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
for every looker in contents that receives the
|
||||
message. This allows for every object to potentially
|
||||
get its own customized string.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Keyword arguments will be passed on to `obj.msg()` for all
|
||||
messaged objects.
|
||||
|
||||
|
|
@ -755,7 +762,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
(at_before/after_move etc) with quiet=True, this is as quiet a move
|
||||
as can be done.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Passed on to announce_move_to and announce_move_from hooks.
|
||||
|
||||
Returns:
|
||||
|
|
@ -929,7 +936,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
key (str): Name of the new object.
|
||||
account (Account): Account to attribute this object to.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
description (str): Brief description for this object.
|
||||
ip (str): IP address of creator (for object auditing).
|
||||
|
||||
|
|
@ -1051,7 +1058,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# See if we need to kick the account off.
|
||||
|
||||
for session in self.sessions.all():
|
||||
session.msg(_("Your character %s has been destroyed.") % self.key)
|
||||
session.msg(_("Your character {key} has been destroyed.").format(key=self.key))
|
||||
# no need to disconnect, Account just jumps to OOC mode.
|
||||
# sever the connection (important!)
|
||||
if self.account:
|
||||
|
|
@ -1095,7 +1102,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
no_superuser_bypass (bool, optional): If `True`, don't skip
|
||||
lock check for superuser (be careful with this one).
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Passed on to the at_access hook along with the result of the access check.
|
||||
|
||||
"""
|
||||
|
|
@ -1254,7 +1261,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
place to do it. This is called also if the object currently
|
||||
have no cmdsets.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
caller (Session, Object or Account): The caller requesting
|
||||
this cmdset.
|
||||
|
||||
|
|
@ -1356,7 +1363,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
accessing_obj (Object or Account): The entity trying to gain access.
|
||||
access_type (str): The type of access that was requested.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Not used by default, added for possible expandability in a
|
||||
game.
|
||||
|
||||
|
|
@ -1607,7 +1614,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
text (str, optional): The message received.
|
||||
from_obj (any, optional): The object sending the message.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
This includes any keywords sent to the `msg` method.
|
||||
|
||||
Returns:
|
||||
|
|
@ -1629,7 +1636,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
text (str, optional): Text to send.
|
||||
to_obj (any, optional): The object to send to.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
Keywords passed from msg()
|
||||
|
||||
Notes:
|
||||
|
|
@ -1865,7 +1872,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
|
||||
Args:
|
||||
message (str): The suggested say/whisper text spoken by self.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
whisper (bool): If True, this is a whisper rather than
|
||||
a say. This is sent by the whisper command by default.
|
||||
Other verbal commands could use this hook in similar
|
||||
|
|
@ -1905,7 +1912,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
(by default only used by whispers).
|
||||
msg_receivers(str): Specific message to pass to the receiver(s). This will parsed
|
||||
with the {receiver} placeholder replaced with the given receiver.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
whisper (bool): If this is a whisper rather than a say. Kwargs
|
||||
can be used by other verbal commands in a similar way.
|
||||
mapping (dict): Pass an additional mapping to the message.
|
||||
|
|
@ -1936,12 +1943,14 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
|
|||
# whisper mode
|
||||
msg_type = "whisper"
|
||||
msg_self = (
|
||||
'{self} whisper to {all_receivers}, "{speech}"' if msg_self is True else msg_self
|
||||
'{self} whisper to {all_receivers}, "|n{speech}|n"'
|
||||
if msg_self is True
|
||||
else msg_self
|
||||
)
|
||||
msg_receivers = msg_receivers or '{object} whispers: "{speech}"'
|
||||
msg_receivers = msg_receivers or '{object} whispers: "|n{speech}|n"'
|
||||
msg_location = None
|
||||
else:
|
||||
msg_self = '{self} say, "{speech}"' if msg_self is True else msg_self
|
||||
msg_self = '{self} say, "|n{speech}|n"' if msg_self is True else msg_self
|
||||
msg_location = msg_location or '{object} says, "{speech}"'
|
||||
msg_receivers = msg_receivers or message
|
||||
|
||||
|
|
@ -2025,10 +2034,13 @@ class DefaultCharacter(DefaultObject):
|
|||
|
||||
# lockstring of newly created rooms, for easy overloading.
|
||||
# Will be formatted with the appropriate attributes.
|
||||
lockstring = "puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);delete:id({account_id}) or perm(Admin)"
|
||||
lockstring = (
|
||||
"puppet:id({character_id}) or pid({account_id}) or perm(Developer) or pperm(Developer);"
|
||||
"delete:id({account_id}) or perm(Admin)"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account, **kwargs):
|
||||
def create(cls, key, account=None, **kwargs):
|
||||
"""
|
||||
Creates a basic Character with default parameters, unless otherwise
|
||||
specified or extended.
|
||||
|
|
@ -2037,11 +2049,11 @@ class DefaultCharacter(DefaultObject):
|
|||
|
||||
Args:
|
||||
key (str): Name of the new Character.
|
||||
account (obj): Account to associate this Character with. Required as
|
||||
an argument, but one can fake it out by supplying None-- it will
|
||||
account (obj, optional): Account to associate this Character with.
|
||||
If unset supplying None-- it will
|
||||
change the default lockset and skip creator attribution.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
description (str): Brief description for this object.
|
||||
ip (str): IP address of creator (for object auditing).
|
||||
All other kwargs will be passed into the create_object call.
|
||||
|
|
@ -2062,9 +2074,6 @@ class DefaultCharacter(DefaultObject):
|
|||
# Set the supplied key as the name of the intended object
|
||||
kwargs["key"] = key
|
||||
|
||||
# Get home for character
|
||||
kwargs["home"] = ObjectDB.objects.get_id(kwargs.get("home", settings.DEFAULT_HOME))
|
||||
|
||||
# Get permissions
|
||||
kwargs["permissions"] = kwargs.get("permissions", settings.PERMISSION_ACCOUNT_DEFAULT)
|
||||
|
||||
|
|
@ -2076,9 +2085,10 @@ class DefaultCharacter(DefaultObject):
|
|||
|
||||
try:
|
||||
# Check to make sure account does not have too many chars
|
||||
if len(account.characters) >= settings.MAX_NR_CHARACTERS:
|
||||
errors.append("There are too many characters associated with this account.")
|
||||
return obj, errors
|
||||
if account:
|
||||
if len(account.characters) >= settings.MAX_NR_CHARACTERS:
|
||||
errors.append("There are too many characters associated with this account.")
|
||||
return obj, errors
|
||||
|
||||
# Create the Character
|
||||
obj = create.create_object(**kwargs)
|
||||
|
|
@ -2247,7 +2257,7 @@ class DefaultRoom(DefaultObject):
|
|||
)
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account, **kwargs):
|
||||
def create(cls, key, account=None, **kwargs):
|
||||
"""
|
||||
Creates a basic Room with default parameters, unless otherwise
|
||||
specified or extended.
|
||||
|
|
@ -2256,9 +2266,11 @@ class DefaultRoom(DefaultObject):
|
|||
|
||||
Args:
|
||||
key (str): Name of the new Room.
|
||||
account (obj): Account to associate this Room with.
|
||||
account (obj, optional): Account to associate this Room with. If
|
||||
given, it will be given specific control/edit permissions to this
|
||||
object (along with normal Admin perms). If not given, default
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
description (str): Brief description for this object.
|
||||
ip (str): IP address of creator (for object auditing).
|
||||
|
||||
|
|
@ -2285,13 +2297,20 @@ class DefaultRoom(DefaultObject):
|
|||
# Get description, if provided
|
||||
description = kwargs.pop("description", "")
|
||||
|
||||
# get locks if provided
|
||||
locks = kwargs.pop("locks", "")
|
||||
|
||||
try:
|
||||
# Create the Room
|
||||
obj = create.create_object(**kwargs)
|
||||
|
||||
# Set appropriate locks
|
||||
lockstring = kwargs.get("locks", cls.lockstring.format(id=account.id))
|
||||
obj.locks.add(lockstring)
|
||||
# Add locks
|
||||
if not locks and account:
|
||||
locks = cls.lockstring.format(**{"id": account.id})
|
||||
elif not locks and not account:
|
||||
locks = cls.lockstring(**{"id": obj.id})
|
||||
|
||||
obj.locks.add(locks)
|
||||
|
||||
# Record creator id and creation IP
|
||||
if ip:
|
||||
|
|
@ -2327,8 +2346,7 @@ class DefaultRoom(DefaultObject):
|
|||
# Default Exit command, used by the base exit object
|
||||
#
|
||||
|
||||
|
||||
class ExitCommand(command.Command):
|
||||
class ExitCommand(_COMMAND_DEFAULT_CLASS):
|
||||
"""
|
||||
This is a command that simply cause the caller to traverse
|
||||
the object it is attached to.
|
||||
|
|
@ -2440,7 +2458,7 @@ class DefaultExit(DefaultObject):
|
|||
# Command hooks
|
||||
|
||||
@classmethod
|
||||
def create(cls, key, account, source, dest, **kwargs):
|
||||
def create(cls, key, source, dest, account=None, **kwargs):
|
||||
"""
|
||||
Creates a basic Exit with default parameters, unless otherwise
|
||||
specified or extended.
|
||||
|
|
@ -2454,7 +2472,7 @@ class DefaultExit(DefaultObject):
|
|||
source (Room): The room to create this exit in.
|
||||
dest (Room): The room to which this exit should go.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
description (str): Brief description for this object.
|
||||
ip (str): IP address of creator (for object auditing).
|
||||
|
||||
|
|
@ -2484,13 +2502,18 @@ class DefaultExit(DefaultObject):
|
|||
|
||||
description = kwargs.pop("description", "")
|
||||
|
||||
locks = kwargs.get("locks", "")
|
||||
|
||||
try:
|
||||
# Create the Exit
|
||||
obj = create.create_object(**kwargs)
|
||||
|
||||
# Set appropriate locks
|
||||
lockstring = kwargs.get("locks", cls.lockstring.format(id=account.id))
|
||||
obj.locks.add(lockstring)
|
||||
if not locks and account:
|
||||
locks = cls.lockstring.format(**{"id": account.id})
|
||||
elif not locks and not account:
|
||||
locks = cls.lockstring.format(**{"id": obj.id})
|
||||
obj.locks.add(locks)
|
||||
|
||||
# Record creator id and creation IP
|
||||
if ip:
|
||||
|
|
@ -2524,10 +2547,10 @@ class DefaultExit(DefaultObject):
|
|||
[
|
||||
"puppet:false()", # would be weird to puppet an exit ...
|
||||
"traverse:all()", # who can pass through exit by default
|
||||
"get:false()",
|
||||
"get:false()", # noone can pick up the exit
|
||||
]
|
||||
)
|
||||
) # noone can pick up the exit
|
||||
)
|
||||
|
||||
# an exit should have a destination (this is replaced at creation time)
|
||||
if self.location:
|
||||
|
|
@ -2541,7 +2564,7 @@ class DefaultExit(DefaultObject):
|
|||
place to do it. This is called also if the object currently
|
||||
has no cmdsets.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
force_init (bool): If `True`, force a re-build of the cmdset
|
||||
(for example to update aliases).
|
||||
|
||||
|
|
|
|||
|
|
@ -9,23 +9,35 @@ class DefaultObjectTest(EvenniaTest):
|
|||
|
||||
def test_object_create(self):
|
||||
description = "A home for a grouch."
|
||||
home = self.room1.dbref
|
||||
|
||||
obj, errors = DefaultObject.create(
|
||||
"trashcan", self.account, description=description, ip=self.ip
|
||||
"trashcan", self.account, description=description, ip=self.ip, home=home
|
||||
)
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertEqual(description, obj.db.desc)
|
||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
self.assertEqual(obj.db_home, self.room1)
|
||||
|
||||
def test_character_create(self):
|
||||
description = "A furry green monster, reeking of garbage."
|
||||
home = self.room1.dbref
|
||||
|
||||
obj, errors = DefaultCharacter.create(
|
||||
"oscar", self.account, description=description, ip=self.ip
|
||||
"oscar", self.account, description=description, ip=self.ip, home=home
|
||||
)
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertEqual(description, obj.db.desc)
|
||||
self.assertEqual(obj.db.creator_ip, self.ip)
|
||||
self.assertEqual(obj.db_home, self.room1)
|
||||
|
||||
def test_character_create_noaccount(self):
|
||||
obj, errors = DefaultCharacter.create("oscar", None, home=self.room1.dbref)
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
self.assertEqual(obj.db_home, self.room1)
|
||||
|
||||
def test_room_create(self):
|
||||
description = "A dimly-lit alley behind the local Chinese restaurant."
|
||||
|
|
@ -38,7 +50,7 @@ class DefaultObjectTest(EvenniaTest):
|
|||
def test_exit_create(self):
|
||||
description = "The steaming depths of the dumpster, ripe with refuse in various states of decomposition."
|
||||
obj, errors = DefaultExit.create(
|
||||
"in", self.account, self.room1, self.room2, description=description, ip=self.ip
|
||||
"in", self.room1, self.room2, account=self.account, description=description, ip=self.ip
|
||||
)
|
||||
self.assertTrue(obj, errors)
|
||||
self.assertFalse(errors, errors)
|
||||
|
|
@ -101,3 +113,27 @@ class TestObjectManager(EvenniaTest):
|
|||
self.assertEqual(list(query), [self.obj1])
|
||||
query = ObjectDB.objects.get_objs_with_attr("NotFound", candidates=[self.char1, self.obj1])
|
||||
self.assertFalse(query)
|
||||
|
||||
def test_copy_object(self):
|
||||
"Test that all attributes and tags properly copy across objects"
|
||||
|
||||
# Add some tags
|
||||
self.obj1.tags.add("plugh", category="adventure")
|
||||
self.obj1.tags.add("xyzzy")
|
||||
|
||||
# Add some attributes
|
||||
self.obj1.attributes.add("phrase", "plugh", category="adventure")
|
||||
self.obj1.attributes.add("phrase", "xyzzy")
|
||||
|
||||
# Create object copy
|
||||
obj2 = self.obj1.copy()
|
||||
|
||||
# Make sure each of the tags were replicated
|
||||
self.assertTrue("plugh" in obj2.tags.all())
|
||||
self.assertTrue("plugh" in obj2.tags.get(category="adventure"))
|
||||
self.assertTrue("xyzzy" in obj2.tags.all())
|
||||
|
||||
# Make sure each of the attributes were replicated
|
||||
self.assertEqual(obj2.attributes.get(key="phrase"), "xyzzy")
|
||||
self.assertEqual(self.obj1.attributes.get(key="phrase", category="adventure"), "plugh")
|
||||
self.assertEqual(obj2.attributes.get(key="phrase", category="adventure"), "plugh")
|
||||
|
|
|
|||
|
|
@ -130,7 +130,7 @@ def _set_property(caller, raw_string, **kwargs):
|
|||
caller (Object, Account): The user of the wizard.
|
||||
raw_string (str): Input from user on given node - the new value to set.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
test_parse (bool): If set (default True), parse raw_string for protfuncs and obj-refs and
|
||||
try to run result through literal_eval. The parser will be run in 'testing' mode and any
|
||||
parsing errors will shown to the user. Note that this is just for testing, the original
|
||||
|
|
@ -297,7 +297,7 @@ def _format_list_actions(*args, **kwargs):
|
|||
Args:
|
||||
actions (str): Available actions. The first letter of the action name will be assumed
|
||||
to be a shortcut.
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
prefix (str): Default prefix to use.
|
||||
Returns:
|
||||
string (str): Formatted footer for adding to the node text.
|
||||
|
|
@ -1175,7 +1175,7 @@ def _add_attr(caller, attr_string, **kwargs):
|
|||
attr = value
|
||||
attr;category = value
|
||||
attr;category;lockstring = value
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
delete (str): If this is set, attr_string is
|
||||
considered the name of the attribute to delete and
|
||||
no further parsing happens.
|
||||
|
|
@ -1235,7 +1235,7 @@ def _attr_select(caller, attrstr):
|
|||
|
||||
attr_tup = _get_tup_by_attrname(caller, attrname)
|
||||
if attr_tup:
|
||||
return "node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"}
|
||||
return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
|
||||
else:
|
||||
caller.msg("Attribute not found.")
|
||||
return "node_attrs"
|
||||
|
|
@ -1260,7 +1260,7 @@ def _attrs_actions(caller, raw_inp, **kwargs):
|
|||
|
||||
if action and attr_tup:
|
||||
if action == "examine":
|
||||
return "node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"}
|
||||
return ("node_examine_entity", {"text": _display_attribute(attr_tup), "back": "attrs"})
|
||||
elif action == "remove":
|
||||
res = _add_attr(caller, attrname, delete=True)
|
||||
caller.msg(res)
|
||||
|
|
@ -1362,7 +1362,7 @@ def _add_tag(caller, tag_string, **kwargs):
|
|||
tagname;category
|
||||
tagname;category;data
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
delete (str): If this is set, tag_string is considered
|
||||
the name of the tag to delete.
|
||||
|
||||
|
|
@ -1439,7 +1439,7 @@ def _tags_actions(caller, raw_inp, **kwargs):
|
|||
|
||||
if tag_tup:
|
||||
if action == "examine":
|
||||
return "node_examine_entity", {"text": _display_tag(tag_tup), "back": "tags"}
|
||||
return ("node_examine_entity", {"text": _display_tag(tag_tup), "back": "tags"})
|
||||
elif action == "remove":
|
||||
res = _add_tag(caller, tagname, delete=True)
|
||||
caller.msg(res)
|
||||
|
|
@ -1488,7 +1488,7 @@ def node_tags(caller):
|
|||
as the |cprototype_key|n and with a category "{tag_category}". This allows the spawner to
|
||||
optionally update previously spawned objects when their prototype changes.
|
||||
""".format(
|
||||
tag_category=protlib._PROTOTYPE_TAG_CATEGORY
|
||||
tag_category=protlib.PROTOTYPE_TAG_CATEGORY
|
||||
)
|
||||
|
||||
text = (text, helptext)
|
||||
|
|
@ -1510,7 +1510,7 @@ def _locks_display(caller, lock):
|
|||
|
||||
|
||||
def _lock_select(caller, lockstr):
|
||||
return "node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "locks"}
|
||||
return ("node_examine_entity", {"text": _locks_display(caller, lockstr), "back": "locks"})
|
||||
|
||||
|
||||
def _lock_add(caller, lock, **kwargs):
|
||||
|
|
@ -1552,7 +1552,7 @@ def _locks_actions(caller, raw_inp, **kwargs):
|
|||
|
||||
if lock:
|
||||
if action == "examine":
|
||||
return "node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"}
|
||||
return ("node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"})
|
||||
elif action == "remove":
|
||||
ret = _lock_add(caller, lock, delete=True)
|
||||
caller.msg(ret)
|
||||
|
|
@ -1645,7 +1645,10 @@ def _display_perm(caller, permission, only_hierarchy=False):
|
|||
|
||||
|
||||
def _permission_select(caller, permission, **kwargs):
|
||||
return "node_examine_entity", {"text": _display_perm(caller, permission), "back": "permissions"}
|
||||
return (
|
||||
"node_examine_entity",
|
||||
{"text": _display_perm(caller, permission), "back": "permissions"},
|
||||
)
|
||||
|
||||
|
||||
def _add_perm(caller, perm, **kwargs):
|
||||
|
|
@ -1908,7 +1911,7 @@ def _add_prototype_tag(caller, tag_string, **kwargs):
|
|||
caller (Object): Caller of menu.
|
||||
tag_string (str): Input from user - only tagname
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
delete (str): If this is set, tag_string is considered
|
||||
the name of the tag to delete.
|
||||
|
||||
|
|
@ -2051,7 +2054,7 @@ def _prototype_locks_actions(caller, raw_inp, **kwargs):
|
|||
|
||||
if lock:
|
||||
if action == "examine":
|
||||
return "node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"}
|
||||
return ("node_examine_entity", {"text": _locks_display(caller, lock), "back": "locks"})
|
||||
elif action == "remove":
|
||||
ret = _prototype_lock_add(caller, lock.strip(), delete=True)
|
||||
caller.msg(ret)
|
||||
|
|
@ -2128,14 +2131,15 @@ def _keep_diff(caller, **kwargs):
|
|||
tmp[path[-1]] = tuple(list(tmp[path[-1]][:-1]) + ["KEEP"])
|
||||
|
||||
|
||||
def _format_diff_text_and_options(diff, **kwargs):
|
||||
def _format_diff_text_and_options(diff, minimal=True, **kwargs):
|
||||
"""
|
||||
Reformat the diff in a way suitable for the olc menu.
|
||||
|
||||
Args:
|
||||
diff (dict): A diff as produced by `prototype_diff`.
|
||||
minimal (bool, optional): Don't show KEEPs.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
any (any): Forwarded into the generated options as arguments to the callable.
|
||||
|
||||
Returns:
|
||||
|
|
@ -2147,12 +2151,15 @@ def _format_diff_text_and_options(diff, **kwargs):
|
|||
|
||||
def _visualize(obj, rootname, get_name=False):
|
||||
if utils.is_iter(obj):
|
||||
if not obj:
|
||||
return str(obj)
|
||||
if get_name:
|
||||
return obj[0] if obj[0] else "<unset>"
|
||||
if rootname == "attrs":
|
||||
return "{} |W=|n {} |W(category:|n {}|W, locks:|n {}|W)|n".format(*obj)
|
||||
elif rootname == "tags":
|
||||
return "{} |W(category:|n {}|W)|n".format(obj[0], obj[1])
|
||||
|
||||
return "{}".format(obj)
|
||||
|
||||
def _parse_diffpart(diffpart, optnum, *args):
|
||||
|
|
@ -2163,17 +2170,39 @@ def _format_diff_text_and_options(diff, **kwargs):
|
|||
rootname = args[0]
|
||||
old, new, instruction = diffpart
|
||||
if instruction == "KEEP":
|
||||
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
|
||||
if not minimal:
|
||||
texts.append(" |gKEEP|W:|n {old}".format(old=_visualize(old, rootname)))
|
||||
else:
|
||||
# instructions we should be able to revert by a menu choice
|
||||
vold = _visualize(old, rootname)
|
||||
vnew = _visualize(new, rootname)
|
||||
vsep = "" if len(vold) < 78 else "\n"
|
||||
vinst = "|rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
|
||||
texts.append(
|
||||
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
|
||||
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
|
||||
|
||||
if instruction == "ADD":
|
||||
texts.append(
|
||||
" |c[{optnum}] |yADD|n: {new}".format(
|
||||
optnum=optnum, new=_visualize(new, rootname)
|
||||
)
|
||||
)
|
||||
elif instruction == "REMOVE" and not new:
|
||||
if rootname == "tags" and old[1] == protlib.PROTOTYPE_TAG_CATEGORY:
|
||||
# special exception for the prototype-tag mechanism
|
||||
# this is added post-spawn automatically and should
|
||||
# not be listed as REMOVE.
|
||||
return texts, options, optnum
|
||||
|
||||
texts.append(
|
||||
" |c[{optnum}] |rREMOVE|n: {old}".format(
|
||||
optnum=optnum, old=_visualize(old, rootname)
|
||||
)
|
||||
)
|
||||
else:
|
||||
vinst = "|y{}|n".format(instruction)
|
||||
texts.append(
|
||||
" |c[{num}] {inst}|W:|n {old} |W->|n{sep} {new}".format(
|
||||
inst=vinst, num=optnum, old=vold, sep=vsep, new=vnew
|
||||
)
|
||||
)
|
||||
)
|
||||
options.append(
|
||||
{
|
||||
"key": str(optnum),
|
||||
|
|
@ -2200,11 +2229,8 @@ def _format_diff_text_and_options(diff, **kwargs):
|
|||
for root_key in sorted(diff):
|
||||
diffpart = diff[root_key]
|
||||
text, option, optnum = _parse_diffpart(diffpart, optnum, root_key)
|
||||
|
||||
heading = "- |w{}:|n ".format(root_key)
|
||||
if root_key in ("attrs", "tags", "permissions"):
|
||||
texts.append(heading)
|
||||
elif text:
|
||||
if text:
|
||||
text = [heading + text[0]] + text[1:]
|
||||
else:
|
||||
text = [heading]
|
||||
|
|
@ -2274,7 +2300,9 @@ def node_apply_diff(caller, **kwargs):
|
|||
if not custom_location:
|
||||
diff.pop("location", None)
|
||||
|
||||
txt, options = _format_diff_text_and_options(diff, objects=update_objects, base_obj=base_obj)
|
||||
txt, options = _format_diff_text_and_options(
|
||||
diff, objects=update_objects, base_obj=base_obj, prototype=prototype
|
||||
)
|
||||
|
||||
if options:
|
||||
text = [
|
||||
|
|
|
|||
|
|
@ -9,9 +9,13 @@ import hashlib
|
|||
import time
|
||||
from ast import literal_eval
|
||||
from django.conf import settings
|
||||
from django.db.models import Q, Subquery
|
||||
from django.core.paginator import Paginator
|
||||
from evennia.scripts.scripts import DefaultScript
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.typeclasses.attributes import Attribute
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.evmore import EvMore
|
||||
from evennia.utils.utils import (
|
||||
all_from_module,
|
||||
make_iter,
|
||||
|
|
@ -52,10 +56,12 @@ _PROTOTYPE_RESERVED_KEYS = _PROTOTYPE_META_NAMES + (
|
|||
"tags",
|
||||
"attrs",
|
||||
)
|
||||
_PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
||||
PROTOTYPE_TAG_CATEGORY = "from_prototype"
|
||||
_PROTOTYPE_TAG_META_CATEGORY = "db_prototype"
|
||||
PROT_FUNCS = {}
|
||||
|
||||
_PROTOTYPE_FALLBACK_LOCK = "spawn:all();edit:all()"
|
||||
|
||||
|
||||
class PermissionError(RuntimeError):
|
||||
pass
|
||||
|
|
@ -84,8 +90,19 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
homogenizations like adding missing prototype_keys and setting a default typeclass.
|
||||
|
||||
"""
|
||||
if not prototype or not isinstance(prototype, dict):
|
||||
return {}
|
||||
|
||||
reserved = _PROTOTYPE_RESERVED_KEYS + (custom_keys or ())
|
||||
|
||||
# correct cases of setting None for certain values
|
||||
for protkey in prototype:
|
||||
if prototype[protkey] is None:
|
||||
if protkey in ("attrs", "tags", "prototype_tags"):
|
||||
prototype[protkey] = []
|
||||
elif protkey in ("prototype_key", "prototype_desc"):
|
||||
prototype[protkey] = ""
|
||||
|
||||
attrs = list(prototype.get("attrs", [])) # break reference
|
||||
tags = make_iter(prototype.get("tags", []))
|
||||
homogenized_tags = []
|
||||
|
|
@ -111,12 +128,14 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
|
||||
# add required missing parts that had defaults before
|
||||
|
||||
if "prototype_key" not in prototype:
|
||||
homogenized["prototype_key"] = homogenized.get(
|
||||
"prototype_key",
|
||||
# assign a random hash as key
|
||||
homogenized["prototype_key"] = "prototype-{}".format(
|
||||
hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]
|
||||
)
|
||||
|
||||
"prototype-{}".format(hashlib.md5(bytes(str(time.time()), "utf-8")).hexdigest()[:7]),
|
||||
)
|
||||
homogenized["prototype_tags"] = homogenized.get("prototype_tags", [])
|
||||
homogenized["prototype_locks"] = homogenized.get("prototype_lock", _PROTOTYPE_FALLBACK_LOCK)
|
||||
homogenized["prototype_desc"] = homogenized.get("prototype_desc", "")
|
||||
if "typeclass" not in prototype and "prototype_parent" not in prototype:
|
||||
homogenized["typeclass"] = settings.BASE_OBJECT_TYPECLASS
|
||||
|
||||
|
|
@ -125,33 +144,41 @@ def homogenize_prototype(prototype, custom_keys=None):
|
|||
|
||||
# module-based prototypes
|
||||
|
||||
for mod in settings.PROTOTYPE_MODULES:
|
||||
# to remove a default prototype, override it with an empty dict.
|
||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||
prots = []
|
||||
for variable_name, prot in all_from_module(mod).items():
|
||||
if isinstance(prot, dict):
|
||||
if "prototype_key" not in prot:
|
||||
prot["prototype_key"] = variable_name.lower()
|
||||
prots.append((prot["prototype_key"], homogenize_prototype(prot)))
|
||||
# assign module path to each prototype_key for easy reference
|
||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||
# make sure the prototype contains all meta info
|
||||
for prototype_key, prot in prots:
|
||||
actual_prot_key = prot.get("prototype_key", prototype_key).lower()
|
||||
prot.update(
|
||||
{
|
||||
"prototype_key": actual_prot_key,
|
||||
"prototype_desc": prot["prototype_desc"] if "prototype_desc" in prot else mod,
|
||||
"prototype_locks": (
|
||||
prot["prototype_locks"]
|
||||
if "prototype_locks" in prot
|
||||
else "use:all();edit:false()"
|
||||
),
|
||||
"prototype_tags": list(set(make_iter(prot.get("prototype_tags", [])) + ["module"])),
|
||||
}
|
||||
)
|
||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||
def load_module_prototypes():
|
||||
"""
|
||||
This is called by `evennia.__init__` as Evennia initializes. It's important
|
||||
to do this late so as to not interfere with evennia initialization.
|
||||
|
||||
"""
|
||||
for mod in settings.PROTOTYPE_MODULES:
|
||||
# to remove a default prototype, override it with an empty dict.
|
||||
# internally we store as (key, desc, locks, tags, prototype_dict)
|
||||
prots = []
|
||||
for variable_name, prot in all_from_module(mod).items():
|
||||
if isinstance(prot, dict):
|
||||
if "prototype_key" not in prot:
|
||||
prot["prototype_key"] = variable_name.lower()
|
||||
prots.append((prot["prototype_key"], homogenize_prototype(prot)))
|
||||
# assign module path to each prototype_key for easy reference
|
||||
_MODULE_PROTOTYPE_MODULES.update({prototype_key.lower(): mod for prototype_key, _ in prots})
|
||||
# make sure the prototype contains all meta info
|
||||
for prototype_key, prot in prots:
|
||||
actual_prot_key = prot.get("prototype_key", prototype_key).lower()
|
||||
prot.update(
|
||||
{
|
||||
"prototype_key": actual_prot_key,
|
||||
"prototype_desc": prot["prototype_desc"] if "prototype_desc" in prot else mod,
|
||||
"prototype_locks": (
|
||||
prot["prototype_locks"]
|
||||
if "prototype_locks" in prot
|
||||
else "use:all();edit:false()"
|
||||
),
|
||||
"prototype_tags": list(
|
||||
set(list(make_iter(prot.get("prototype_tags", []))) + ["module"])
|
||||
),
|
||||
}
|
||||
)
|
||||
_MODULE_PROTOTYPES[actual_prot_key] = prot
|
||||
|
||||
|
||||
# Db-based prototypes
|
||||
|
|
@ -223,14 +250,11 @@ def save_prototype(prototype):
|
|||
)
|
||||
|
||||
# make sure meta properties are included with defaults
|
||||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||
prototype = stored_prototype[0].prototype if stored_prototype else {}
|
||||
|
||||
in_prototype["prototype_desc"] = in_prototype.get(
|
||||
"prototype_desc", prototype.get("prototype_desc", "")
|
||||
)
|
||||
prototype_locks = in_prototype.get(
|
||||
"prototype_locks", prototype.get("prototype_locks", "spawn:all();edit:perm(Admin)")
|
||||
"prototype_locks", prototype.get("prototype_locks", _PROTOTYPE_FALLBACK_LOCK)
|
||||
)
|
||||
is_valid, err = validate_lockstring(prototype_locks)
|
||||
if not is_valid:
|
||||
|
|
@ -245,27 +269,26 @@ def save_prototype(prototype):
|
|||
]
|
||||
in_prototype["prototype_tags"] = prototype_tags
|
||||
|
||||
prototype.update(in_prototype)
|
||||
|
||||
stored_prototype = DbPrototype.objects.filter(db_key=prototype_key)
|
||||
if stored_prototype:
|
||||
# edit existing prototype
|
||||
stored_prototype = stored_prototype[0]
|
||||
stored_prototype.desc = prototype["prototype_desc"]
|
||||
stored_prototype.desc = in_prototype["prototype_desc"]
|
||||
if prototype_tags:
|
||||
stored_prototype.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
|
||||
stored_prototype.tags.batch_add(*prototype["prototype_tags"])
|
||||
stored_prototype.locks.add(prototype["prototype_locks"])
|
||||
stored_prototype.attributes.add("prototype", prototype)
|
||||
stored_prototype.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
|
||||
stored_prototype.tags.batch_add(*in_prototype["prototype_tags"])
|
||||
stored_prototype.locks.add(in_prototype["prototype_locks"])
|
||||
stored_prototype.attributes.add("prototype", in_prototype)
|
||||
else:
|
||||
# create a new prototype
|
||||
stored_prototype = create_script(
|
||||
DbPrototype,
|
||||
key=prototype_key,
|
||||
desc=prototype["prototype_desc"],
|
||||
desc=in_prototype["prototype_desc"],
|
||||
persistent=True,
|
||||
locks=prototype_locks,
|
||||
tags=prototype["prototype_tags"],
|
||||
attributes=[("prototype", prototype)],
|
||||
tags=in_prototype["prototype_tags"],
|
||||
attributes=[("prototype", in_prototype)],
|
||||
)
|
||||
return stored_prototype.prototype
|
||||
|
||||
|
|
@ -309,22 +332,28 @@ def delete_prototype(prototype_key, caller=None):
|
|||
return True
|
||||
|
||||
|
||||
def search_prototype(key=None, tags=None, require_single=False):
|
||||
def search_prototype(key=None, tags=None, require_single=False, return_iterators=False):
|
||||
"""
|
||||
Find prototypes based on key and/or tags, or all prototypes.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
key (str): An exact or partial key to query for.
|
||||
tags (str or list): Tag key or keys to query for. These
|
||||
will always be applied with the 'db_protototype'
|
||||
tag category.
|
||||
require_single (bool): If set, raise KeyError if the result
|
||||
was not found or if there are multiple matches.
|
||||
return_iterators (bool): Optimized return for large numbers of db-prototypes.
|
||||
If set, separate returns of module based prototypes and paginate
|
||||
the db-prototype return.
|
||||
|
||||
Return:
|
||||
matches (list): All found prototype dicts. Empty list if
|
||||
matches (list): Default return, all found prototype dicts. Empty list if
|
||||
no match was found. Note that if neither `key` nor `tags`
|
||||
were given, *all* available prototypes will be returned.
|
||||
list, queryset: If `return_iterators` are found, this is a list of
|
||||
module-based prototypes followed by a *paginated* queryset of
|
||||
db-prototypes.
|
||||
|
||||
Raises:
|
||||
KeyError: If `require_single` is True and there are 0 or >1 matches.
|
||||
|
|
@ -372,31 +401,37 @@ def search_prototype(key=None, tags=None, require_single=False):
|
|||
tag_categories = ["db_prototype" for _ in tags]
|
||||
db_matches = DbPrototype.objects.get_by_tag(tags, tag_categories)
|
||||
else:
|
||||
db_matches = DbPrototype.objects.all().order_by("id")
|
||||
db_matches = DbPrototype.objects.all()
|
||||
|
||||
if key:
|
||||
# exact or partial match on key
|
||||
db_matches = (
|
||||
db_matches.filter(db_key=key) or db_matches.filter(db_key__icontains=key)
|
||||
).order_by("id")
|
||||
# return prototype
|
||||
db_prototypes = [dbprot.prototype for dbprot in db_matches]
|
||||
exact_match = db_matches.filter(Q(db_key__iexact=key)).order_by("db_key")
|
||||
if not exact_match:
|
||||
# try with partial match instead
|
||||
db_matches = db_matches.filter(Q(db_key__icontains=key)).order_by("db_key")
|
||||
else:
|
||||
db_matches = exact_match
|
||||
|
||||
matches = db_prototypes + module_prototypes
|
||||
nmatches = len(matches)
|
||||
if nmatches > 1 and key:
|
||||
key = key.lower()
|
||||
# avoid duplicates if an exact match exist between the two types
|
||||
filter_matches = [
|
||||
mta for mta in matches if mta.get("prototype_key") and mta["prototype_key"] == key
|
||||
]
|
||||
if filter_matches and len(filter_matches) < nmatches:
|
||||
matches = filter_matches
|
||||
# convert to prototype
|
||||
db_ids = db_matches.values_list("id", flat=True)
|
||||
db_matches = (
|
||||
Attribute.objects.filter(scriptdb__pk__in=db_ids, db_key="prototype")
|
||||
.values_list("db_value", flat=True)
|
||||
.order_by("scriptdb__db_key")
|
||||
)
|
||||
if key and require_single:
|
||||
nmodules = len(module_prototypes)
|
||||
ndbprots = db_matches.count()
|
||||
if nmodules + ndbprots != 1:
|
||||
raise KeyError(f"Found {nmodules + ndbprots} matching prototypes.")
|
||||
|
||||
nmatches = len(matches)
|
||||
if nmatches != 1 and require_single:
|
||||
raise KeyError("Found {} matching prototypes.".format(nmatches))
|
||||
|
||||
return matches
|
||||
if return_iterators:
|
||||
# trying to get the entire set of prototypes - we must paginate
|
||||
# the result instead of trying to fetch the entire set at once
|
||||
return db_matches, module_prototypes
|
||||
else:
|
||||
# full fetch, no pagination (compatibility mode)
|
||||
return list(db_matches) + module_prototypes
|
||||
|
||||
|
||||
def search_objects_with_prototype(prototype_key):
|
||||
|
|
@ -410,10 +445,113 @@ def search_objects_with_prototype(prototype_key):
|
|||
matches (Queryset): All matching objects spawned from this prototype.
|
||||
|
||||
"""
|
||||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||
return ObjectDB.objects.get_by_tag(key=prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
|
||||
def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_edit=True):
|
||||
class PrototypeEvMore(EvMore):
|
||||
"""
|
||||
Listing 1000+ prototypes can be very slow. So we customize EvMore to
|
||||
display an EvTable per paginated page rather than to try creating an
|
||||
EvTable for the entire dataset and then paginate it.
|
||||
"""
|
||||
|
||||
def __init__(self, caller, *args, session=None, **kwargs):
|
||||
"""Store some extra properties on the EvMore class"""
|
||||
self.show_non_use = kwargs.pop("show_non_use", False)
|
||||
self.show_non_edit = kwargs.pop("show_non_edit", False)
|
||||
super().__init__(caller, *args, session=session, **kwargs)
|
||||
|
||||
def init_pages(self, inp):
|
||||
"""
|
||||
This will be initialized with a tuple (mod_prototype_list, paginated_db_query)
|
||||
and we must handle these separately since they cannot be paginated in the same
|
||||
way. We will build the prototypes so that the db-prototypes come first (they
|
||||
are likely the most volatile), followed by the mod-prototypes.
|
||||
"""
|
||||
dbprot_query, modprot_list = inp
|
||||
# set the number of entries per page to half the reported height of the screen
|
||||
# to account for long descs etc
|
||||
dbprot_paged = Paginator(dbprot_query, max(1, int(self.height / 2)))
|
||||
|
||||
# we separate the different types of data, so we track how many pages there are
|
||||
# of each.
|
||||
n_mod = len(modprot_list)
|
||||
self._npages_mod = n_mod // self.height + (0 if n_mod % self.height == 0 else 1)
|
||||
self._db_count = dbprot_paged.count
|
||||
self._npages_db = dbprot_paged.num_pages if self._db_count > 0 else 0
|
||||
# total number of pages
|
||||
self._npages = self._npages_mod + self._npages_db
|
||||
self._data = (dbprot_paged, modprot_list)
|
||||
self._paginator = self.prototype_paginator
|
||||
|
||||
def prototype_paginator(self, pageno):
|
||||
"""
|
||||
The listing is separated in db/mod prototypes, so we need to figure out which
|
||||
one to pick based on the page number. Also, pageno starts from 0.
|
||||
"""
|
||||
dbprot_pages, modprot_list = self._data
|
||||
|
||||
if self._db_count and pageno < self._npages_db:
|
||||
return dbprot_pages.page(pageno + 1)
|
||||
else:
|
||||
# get the correct slice, adjusted for the db-prototypes
|
||||
pageno = max(0, pageno - self._npages_db)
|
||||
return modprot_list[pageno * self.height : pageno * self.height + self.height]
|
||||
|
||||
def page_formatter(self, page):
|
||||
"""Input is a queryset page from django.Paginator"""
|
||||
caller = self._caller
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
|
||||
table = EvTable(
|
||||
"|wKey|n",
|
||||
"|wSpawn/Edit|n",
|
||||
"|wTags|n",
|
||||
"|wDesc|n",
|
||||
border="tablecols",
|
||||
crop=True,
|
||||
width=self.width,
|
||||
)
|
||||
|
||||
for prototype in page:
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
||||
)
|
||||
if not self.show_non_use and not lock_use:
|
||||
continue
|
||||
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
|
||||
lock_edit = False
|
||||
else:
|
||||
lock_edit = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
|
||||
)
|
||||
if not self.show_non_edit and not lock_edit:
|
||||
continue
|
||||
ptags = []
|
||||
for ptag in prototype.get("prototype_tags", []):
|
||||
if is_iter(ptag):
|
||||
if len(ptag) > 1:
|
||||
ptags.append("{}".format(ptag[0]))
|
||||
else:
|
||||
ptags.append(ptag[0])
|
||||
else:
|
||||
ptags.append(str(ptag))
|
||||
|
||||
table.add_row(
|
||||
prototype.get("prototype_key", "<unset>"),
|
||||
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
||||
", ".join(list(set(ptags))),
|
||||
prototype.get("prototype_desc", "<unset>"),
|
||||
)
|
||||
|
||||
return str(table)
|
||||
|
||||
|
||||
def list_prototypes(
|
||||
caller, key=None, tags=None, show_non_use=False, show_non_edit=True, session=None
|
||||
):
|
||||
"""
|
||||
Collate a list of found prototypes based on search criteria and access.
|
||||
|
||||
|
|
@ -423,65 +561,29 @@ def list_prototypes(caller, key=None, tags=None, show_non_use=False, show_non_ed
|
|||
tags (str or list, optional): Tag key or keys to query for.
|
||||
show_non_use (bool, optional): Show also prototypes the caller may not use.
|
||||
show_non_edit (bool, optional): Show also prototypes the caller may not edit.
|
||||
session (Session, optional): If given, this is used for display formatting.
|
||||
Returns:
|
||||
table (EvTable or None): An EvTable representation of the prototypes. None
|
||||
if no prototypes were found.
|
||||
PrototypeEvMore: An EvMore subclass optimized for prototype listings.
|
||||
None: If no matches were found. In this case the caller has already been notified.
|
||||
|
||||
"""
|
||||
# this allows us to pass lists of empty strings
|
||||
tags = [tag for tag in make_iter(tags) if tag]
|
||||
|
||||
# get prototypes for readonly and db-based prototypes
|
||||
prototypes = search_prototype(key, tags)
|
||||
dbprot_query, modprot_list = search_prototype(key, tags, return_iterators=True)
|
||||
|
||||
# get use-permissions of readonly attributes (edit is always False)
|
||||
display_tuples = []
|
||||
for prototype in sorted(prototypes, key=lambda d: d.get("prototype_key", "")):
|
||||
lock_use = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="spawn", default=True
|
||||
)
|
||||
if not show_non_use and not lock_use:
|
||||
continue
|
||||
if prototype.get("prototype_key", "") in _MODULE_PROTOTYPES:
|
||||
lock_edit = False
|
||||
else:
|
||||
lock_edit = caller.locks.check_lockstring(
|
||||
caller, prototype.get("prototype_locks", ""), access_type="edit", default=True
|
||||
)
|
||||
if not show_non_edit and not lock_edit:
|
||||
continue
|
||||
ptags = []
|
||||
for ptag in prototype.get("prototype_tags", []):
|
||||
if is_iter(ptag):
|
||||
if len(ptag) > 1:
|
||||
ptags.append("{} (category: {}".format(ptag[0], ptag[1]))
|
||||
else:
|
||||
ptags.append(ptag[0])
|
||||
else:
|
||||
ptags.append(str(ptag))
|
||||
if not dbprot_query and not modprot_list:
|
||||
caller.msg("No prototypes found.", session=session)
|
||||
return None
|
||||
|
||||
display_tuples.append(
|
||||
(
|
||||
prototype.get("prototype_key", "<unset>"),
|
||||
prototype.get("prototype_desc", "<unset>"),
|
||||
"{}/{}".format("Y" if lock_use else "N", "Y" if lock_edit else "N"),
|
||||
",".join(ptags),
|
||||
)
|
||||
)
|
||||
|
||||
if not display_tuples:
|
||||
return ""
|
||||
|
||||
table = []
|
||||
width = 78
|
||||
for i in range(len(display_tuples[0])):
|
||||
table.append([str(display_tuple[i]) for display_tuple in display_tuples])
|
||||
table = EvTable("Key", "Desc", "Spawn/Edit", "Tags", table=table, crop=True, width=width)
|
||||
table.reformat_column(0, width=22)
|
||||
table.reformat_column(1, width=29)
|
||||
table.reformat_column(2, width=11, align="c")
|
||||
table.reformat_column(3, width=16)
|
||||
return table
|
||||
# get specific prototype (one value or exception)
|
||||
return PrototypeEvMore(
|
||||
caller,
|
||||
(dbprot_query, modprot_list),
|
||||
session=session,
|
||||
show_non_use=show_non_use,
|
||||
show_non_edit=show_non_edit,
|
||||
)
|
||||
|
||||
|
||||
def validate_prototype(
|
||||
|
|
@ -558,7 +660,7 @@ def validate_prototype(
|
|||
protparent = protparents.get(protstring)
|
||||
if not protparent:
|
||||
_flags["errors"].append(
|
||||
"Prototype {}'s prototype_parent '{}' was not found.".format((protkey, protstring))
|
||||
"Prototype {}'s prototype_parent '{}' was not found.".format(protkey, protstring)
|
||||
)
|
||||
if id(prototype) in _flags["visited"]:
|
||||
_flags["errors"].append(
|
||||
|
|
@ -634,7 +736,7 @@ def protfunc_parser(value, available_functions=None, testing=False, stacktrace=F
|
|||
behave differently.
|
||||
stacktrace (bool, optional): If set, print the stack parsing process of the protfunc-parser.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
session (Session): Passed to protfunc. Session of the entity spawning the prototype.
|
||||
protototype (dict): Passed to protfunc. The dict this protfunc is a part of.
|
||||
current_key(str): Passed to protfunc. The key in the prototype that will hold this value.
|
||||
|
|
@ -713,15 +815,15 @@ def prototype_to_str(prototype):
|
|||
prototype_desc=prototype.get("prototype_desc", "|wNone|n"),
|
||||
prototype_parent=prototype.get("prototype_parent", "|wNone|n"),
|
||||
)
|
||||
|
||||
key = prototype.get("key", "")
|
||||
if key:
|
||||
key = aliases = attrs = tags = locks = permissions = location = home = destination = ""
|
||||
if "key" in prototype:
|
||||
key = prototype["key"]
|
||||
key = "|ckey:|n {key}".format(key=key)
|
||||
aliases = prototype.get("aliases", "")
|
||||
if aliases:
|
||||
if "aliases" in prototype:
|
||||
aliases = prototype["aliases"]
|
||||
aliases = "|caliases:|n {aliases}".format(aliases=", ".join(aliases))
|
||||
attrs = prototype.get("attrs", "")
|
||||
if attrs:
|
||||
if "attrs" in prototype:
|
||||
attrs = prototype["attrs"]
|
||||
out = []
|
||||
for (attrkey, value, category, locks) in attrs:
|
||||
locks = ", ".join(lock for lock in locks if lock)
|
||||
|
|
@ -740,8 +842,8 @@ def prototype_to_str(prototype):
|
|||
)
|
||||
)
|
||||
attrs = "|cattrs:|n\n {attrs}".format(attrs="\n ".join(out))
|
||||
tags = prototype.get("tags", "")
|
||||
if tags:
|
||||
if "tags" in prototype:
|
||||
tags = prototype["tags"]
|
||||
out = []
|
||||
for (tagkey, category, data) in tags:
|
||||
out.append(
|
||||
|
|
@ -750,20 +852,20 @@ def prototype_to_str(prototype):
|
|||
)
|
||||
)
|
||||
tags = "|ctags:|n\n {tags}".format(tags=", ".join(out))
|
||||
locks = prototype.get("locks", "")
|
||||
if locks:
|
||||
if "locks" in prototype:
|
||||
locks = prototype["locks"]
|
||||
locks = "|clocks:|n\n {locks}".format(locks=locks)
|
||||
permissions = prototype.get("permissions", "")
|
||||
if permissions:
|
||||
if "permissions" in prototype:
|
||||
permissions = prototype["permissions"]
|
||||
permissions = "|cpermissions:|n {perms}".format(perms=", ".join(permissions))
|
||||
location = prototype.get("location", "")
|
||||
if location:
|
||||
if "location" in prototype:
|
||||
location = prototype["location"]
|
||||
location = "|clocation:|n {location}".format(location=location)
|
||||
home = prototype.get("home", "")
|
||||
if home:
|
||||
if "home" in prototype:
|
||||
home = prototype["home"]
|
||||
home = "|chome:|n {home}".format(home=home)
|
||||
destination = prototype.get("destination", "")
|
||||
if destination:
|
||||
if "destination" in prototype:
|
||||
destination = prototype["destination"]
|
||||
destination = "|cdestination:|n {destination}".format(destination=destination)
|
||||
|
||||
body = "\n".join(
|
||||
|
|
@ -833,7 +935,10 @@ def init_spawn_value(value, validator=None):
|
|||
value = validator(value[0](*make_iter(args)))
|
||||
else:
|
||||
value = validator(value)
|
||||
return protfunc_parser(value)
|
||||
result = protfunc_parser(value)
|
||||
if result != value:
|
||||
return validator(result)
|
||||
return result
|
||||
|
||||
|
||||
def value_to_obj_or_any(value):
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ There main function is `spawn(*prototype)`, where the `prototype`
|
|||
is a dictionary like this:
|
||||
|
||||
```python
|
||||
from evennia.prototypes import prototypes
|
||||
from evennia.prototypes import prototypes, spawner
|
||||
|
||||
prot = {
|
||||
"prototype_key": "goblin",
|
||||
|
|
@ -22,7 +22,10 @@ prot = {
|
|||
"tags": ["mob", "evil", ('greenskin','mob')]
|
||||
"attrs": [("weapon", "sword")]
|
||||
}
|
||||
# spawn something with the prototype
|
||||
goblin = spawner.spawn(prot)
|
||||
|
||||
# make this into a db-saved prototype (optional)
|
||||
prot = prototypes.create_prototype(prot)
|
||||
|
||||
```
|
||||
|
|
@ -82,13 +85,13 @@ import random
|
|||
|
||||
{
|
||||
"prototype_key": "goblin_wizard",
|
||||
"prototype_parent": GOBLIN,
|
||||
"prototype_parent": "GOBLIN",
|
||||
"key": "goblin wizard",
|
||||
"spells": ["fire ball", "lighting bolt"]
|
||||
}
|
||||
|
||||
GOBLIN_ARCHER = {
|
||||
"prototype_parent": GOBLIN,
|
||||
"prototype_parent": "GOBLIN",
|
||||
"key": "goblin archer",
|
||||
"attack_skill": (random, (5, 10))"
|
||||
"attacks": ["short bow"]
|
||||
|
|
@ -104,7 +107,7 @@ ARCHWIZARD = {
|
|||
|
||||
GOBLIN_ARCHWIZARD = {
|
||||
"key" : "goblin archwizard"
|
||||
"prototype_parent": (GOBLIN_WIZARD, ARCHWIZARD),
|
||||
"prototype_parent": ("GOBLIN_WIZARD", "ARCHWIZARD"),
|
||||
}
|
||||
```
|
||||
|
||||
|
|
@ -138,13 +141,14 @@ from django.conf import settings
|
|||
|
||||
import evennia
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.utils import logger
|
||||
from evennia.utils.utils import make_iter, is_iter
|
||||
from evennia.prototypes import prototypes as protlib
|
||||
from evennia.prototypes.prototypes import (
|
||||
value_to_obj,
|
||||
value_to_obj_or_any,
|
||||
init_spawn_value,
|
||||
_PROTOTYPE_TAG_CATEGORY,
|
||||
PROTOTYPE_TAG_CATEGORY,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -165,6 +169,19 @@ _PROTOTYPE_ROOT_NAMES = (
|
|||
_NON_CREATE_KWARGS = _CREATE_OBJECT_KWARGS + _PROTOTYPE_META_NAMES
|
||||
|
||||
|
||||
class Unset:
|
||||
"""
|
||||
Helper class representing a non-set diff element.
|
||||
|
||||
"""
|
||||
|
||||
def __bool__(self):
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
return "<Unset>"
|
||||
|
||||
|
||||
# Helper
|
||||
|
||||
|
||||
|
|
@ -268,7 +285,7 @@ def prototype_from_object(obj):
|
|||
"""
|
||||
# first, check if this object already has a prototype
|
||||
|
||||
prot = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||
prot = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||
if prot:
|
||||
prot = protlib.search_prototype(prot[0])
|
||||
|
||||
|
|
@ -322,9 +339,9 @@ def prototype_from_object(obj):
|
|||
return prot
|
||||
|
||||
|
||||
def prototype_diff(prototype1, prototype2, maxdepth=2):
|
||||
def prototype_diff(prototype1, prototype2, maxdepth=2, homogenize=False, implicit_keep=False):
|
||||
"""
|
||||
A 'detailed' diff specifies differences down to individual sub-sectiions
|
||||
A 'detailed' diff specifies differences down to individual sub-sections
|
||||
of the prototype, like individual attributes, permissions etc. It is used
|
||||
by the menu to allow a user to customize what should be kept.
|
||||
|
||||
|
|
@ -334,6 +351,12 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
maxdepth (int, optional): The maximum depth into the diff we go before treating the elements
|
||||
of iterables as individual entities to compare. This is important since a single
|
||||
attr/tag (for example) are represented by a tuple.
|
||||
homogenize (bool, optional): Auto-homogenize both prototypes for the best comparison.
|
||||
This is most useful for displaying.
|
||||
implicit_keep (bool, optional): If set, the resulting diff will assume KEEP unless the new
|
||||
prototype explicitly change them. That is, if a key exists in `prototype1` and
|
||||
not in `prototype2`, it will not be REMOVEd but set to KEEP instead. This is particularly
|
||||
useful for auto-generated prototypes when updating objects.
|
||||
|
||||
Returns:
|
||||
diff (dict): A structure detailing how to convert prototype1 to prototype2. All
|
||||
|
|
@ -344,12 +367,16 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
instruction can be one of "REMOVE", "ADD", "UPDATE" or "KEEP".
|
||||
|
||||
"""
|
||||
_unset = Unset()
|
||||
|
||||
def _recursive_diff(old, new, depth=0):
|
||||
|
||||
old_type = type(old)
|
||||
new_type = type(new)
|
||||
|
||||
if old_type == new_type and not (old or new):
|
||||
# both old and new are unset, like [] or None
|
||||
return (None, None, "KEEP")
|
||||
if old_type != new_type:
|
||||
if old and not new:
|
||||
if depth < maxdepth and old_type == dict:
|
||||
|
|
@ -358,6 +385,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
return {
|
||||
part[0] if is_iter(part) else part: (part, None, "REMOVE") for part in old
|
||||
}
|
||||
if isinstance(new, Unset) and implicit_keep:
|
||||
# the new does not define any change, use implicit-keep
|
||||
return (old, None, "KEEP")
|
||||
return (old, new, "REMOVE")
|
||||
elif not old and new:
|
||||
if depth < maxdepth and new_type == dict:
|
||||
|
|
@ -371,7 +401,7 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
elif depth < maxdepth and new_type == dict:
|
||||
all_keys = set(list(old.keys()) + list(new.keys()))
|
||||
return {
|
||||
key: _recursive_diff(old.get(key), new.get(key), depth=depth + 1)
|
||||
key: _recursive_diff(old.get(key, _unset), new.get(key, _unset), depth=depth + 1)
|
||||
for key in all_keys
|
||||
}
|
||||
elif depth < maxdepth and is_iter(new):
|
||||
|
|
@ -379,7 +409,9 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
new_map = {part[0] if is_iter(part) else part: part for part in new}
|
||||
all_keys = set(list(old_map.keys()) + list(new_map.keys()))
|
||||
return {
|
||||
key: _recursive_diff(old_map.get(key), new_map.get(key), depth=depth + 1)
|
||||
key: _recursive_diff(
|
||||
old_map.get(key, _unset), new_map.get(key, _unset), depth=depth + 1
|
||||
)
|
||||
for key in all_keys
|
||||
}
|
||||
elif old != new:
|
||||
|
|
@ -387,7 +419,10 @@ def prototype_diff(prototype1, prototype2, maxdepth=2):
|
|||
else:
|
||||
return (old, new, "KEEP")
|
||||
|
||||
diff = _recursive_diff(prototype1, prototype2)
|
||||
prot1 = protlib.homogenize_prototype(prototype1) if homogenize else prototype1
|
||||
prot2 = protlib.homogenize_prototype(prototype2) if homogenize else prototype2
|
||||
|
||||
diff = _recursive_diff(prot1, prot2)
|
||||
|
||||
return diff
|
||||
|
||||
|
|
@ -460,7 +495,7 @@ def flatten_diff(diff):
|
|||
return flat_diff
|
||||
|
||||
|
||||
def prototype_diff_from_object(prototype, obj):
|
||||
def prototype_diff_from_object(prototype, obj, implicit_keep=True):
|
||||
"""
|
||||
Get a simple diff for a prototype compared to an object which may or may not already have a
|
||||
prototype (or has one but changed locally). For more complex migratations a manual diff may be
|
||||
|
|
@ -474,6 +509,11 @@ def prototype_diff_from_object(prototype, obj):
|
|||
diff (dict): Mapping for every prototype key: {"keyname": "REMOVE|UPDATE|KEEP", ...}
|
||||
obj_prototype (dict): The prototype calculated for the given object. The diff is how to
|
||||
convert this prototype into the new prototype.
|
||||
implicit_keep (bool, optional): This is usually what one wants for object updating. When
|
||||
set, this means the prototype diff will assume KEEP on differences
|
||||
between the object-generated prototype and that which is not explicitly set in the
|
||||
new prototype. This means e.g. that even though the object has a location, and the
|
||||
prototype does not specify the location, it will not be unset.
|
||||
|
||||
Notes:
|
||||
The `diff` is on the following form:
|
||||
|
|
@ -486,11 +526,88 @@ def prototype_diff_from_object(prototype, obj):
|
|||
|
||||
"""
|
||||
obj_prototype = prototype_from_object(obj)
|
||||
diff = prototype_diff(obj_prototype, protlib.homogenize_prototype(prototype))
|
||||
diff = prototype_diff(
|
||||
obj_prototype, protlib.homogenize_prototype(prototype), implicit_keep=implicit_keep
|
||||
)
|
||||
return diff, obj_prototype
|
||||
|
||||
|
||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
||||
def format_diff(diff, minimal=True):
|
||||
"""
|
||||
Reformat a diff for presentation. This is a shortened version
|
||||
of the olc _format_diff_text_and_options without the options.
|
||||
|
||||
Args:
|
||||
diff (dict): A diff as produced by `prototype_diff`.
|
||||
minimal (bool, optional): Only show changes (remove KEEPs)
|
||||
|
||||
Returns:
|
||||
texts (str): The formatted text.
|
||||
|
||||
"""
|
||||
|
||||
valid_instructions = ("KEEP", "REMOVE", "ADD", "UPDATE")
|
||||
|
||||
def _visualize(obj, rootname, get_name=False):
|
||||
if is_iter(obj):
|
||||
if not obj:
|
||||
return str(obj)
|
||||
if get_name:
|
||||
return obj[0] if obj[0] else "<unset>"
|
||||
if rootname == "attrs":
|
||||
return "{} |w=|n {} |w(category:|n |n{}|w, locks:|n {}|w)|n".format(*obj)
|
||||
elif rootname == "tags":
|
||||
return "{} |w(category:|n {}|w)|n".format(obj[0], obj[1])
|
||||
return "{}".format(obj)
|
||||
|
||||
def _parse_diffpart(diffpart, rootname):
|
||||
typ = type(diffpart)
|
||||
texts = []
|
||||
if typ == tuple and len(diffpart) == 3 and diffpart[2] in valid_instructions:
|
||||
old, new, instruction = diffpart
|
||||
if instruction == "KEEP":
|
||||
if not minimal:
|
||||
texts.append(" |gKEEP|n: {old}".format(old=_visualize(old, rootname)))
|
||||
elif instruction == "ADD":
|
||||
texts.append(" |yADD|n: {new}".format(new=_visualize(new, rootname)))
|
||||
elif instruction == "REMOVE" and not new:
|
||||
texts.append(" |rREMOVE|n: {old}".format(old=_visualize(old, rootname)))
|
||||
else:
|
||||
vold = _visualize(old, rootname)
|
||||
vnew = _visualize(new, rootname)
|
||||
vsep = "" if len(vold) < 78 else "\n"
|
||||
vinst = " |rREMOVE|n" if instruction == "REMOVE" else "|y{}|n".format(instruction)
|
||||
varrow = "|r->|n" if instruction == "REMOVE" else "|y->|n"
|
||||
texts.append(
|
||||
" {inst}|W:|n {old} |W{varrow}|n{sep} {new}".format(
|
||||
inst=vinst, old=vold, varrow=varrow, sep=vsep, new=vnew
|
||||
)
|
||||
)
|
||||
else:
|
||||
for key in sorted(list(diffpart.keys())):
|
||||
subdiffpart = diffpart[key]
|
||||
text = _parse_diffpart(subdiffpart, rootname)
|
||||
texts.extend(text)
|
||||
return texts
|
||||
|
||||
texts = []
|
||||
|
||||
for root_key in sorted(diff):
|
||||
diffpart = diff[root_key]
|
||||
text = _parse_diffpart(diffpart, root_key)
|
||||
if text or not minimal:
|
||||
heading = "- |w{}:|n\n".format(root_key)
|
||||
if text:
|
||||
text = [heading + text[0]] + text[1:]
|
||||
else:
|
||||
text = [heading]
|
||||
|
||||
texts.extend(text)
|
||||
|
||||
return "\n ".join(line for line in texts if line)
|
||||
|
||||
|
||||
def batch_update_objects_with_prototype(prototype, diff=None, objects=None, exact=False):
|
||||
"""
|
||||
Update existing objects with the latest version of the prototype.
|
||||
|
||||
|
|
@ -501,6 +618,12 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
If not given this will be constructed from the first object found.
|
||||
objects (list, optional): List of objects to update. If not given, query for these
|
||||
objects using the prototype's `prototype_key`.
|
||||
exact (bool, optional): By default (`False`), keys not explicitly in the prototype will
|
||||
not be applied to the object, but will be retained as-is. This is usually what is
|
||||
expected - for example, one usually do not want to remove the object's location even
|
||||
if it's not set in the prototype. With `exact=True`, all un-specified properties of the
|
||||
objects will be removed if they exist. This will lead to a more accurate 1:1 correlation
|
||||
between the object and the prototype but is usually impractical.
|
||||
Returns:
|
||||
changed (int): The number of objects that had changes applied to them.
|
||||
|
||||
|
|
@ -515,7 +638,7 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
prototype_key = new_prototype["prototype_key"]
|
||||
|
||||
if not objects:
|
||||
objects = ObjectDB.objects.get_by_tag(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||
objects = ObjectDB.objects.get_by_tag(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
if not objects:
|
||||
return 0
|
||||
|
|
@ -525,104 +648,117 @@ def batch_update_objects_with_prototype(prototype, diff=None, objects=None):
|
|||
|
||||
# make sure the diff is flattened
|
||||
diff = flatten_diff(diff)
|
||||
|
||||
changed = 0
|
||||
for obj in objects:
|
||||
do_save = False
|
||||
|
||||
old_prot_key = obj.tags.get(category=_PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||
old_prot_key = obj.tags.get(category=PROTOTYPE_TAG_CATEGORY, return_list=True)
|
||||
old_prot_key = old_prot_key[0] if old_prot_key else None
|
||||
if prototype_key != old_prot_key:
|
||||
obj.tags.clear(category=_PROTOTYPE_TAG_CATEGORY)
|
||||
obj.tags.add(prototype_key, category=_PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
for key, directive in diff.items():
|
||||
if directive in ("UPDATE", "REPLACE"):
|
||||
try:
|
||||
for key, directive in diff.items():
|
||||
|
||||
if key in _PROTOTYPE_META_NAMES:
|
||||
# prototype meta keys are not stored on-object
|
||||
if key not in new_prototype and not exact:
|
||||
# we don't update the object if the prototype does not actually
|
||||
# contain the key (the diff will report REMOVE but we ignore it
|
||||
# since exact=False)
|
||||
continue
|
||||
|
||||
val = new_prototype[key]
|
||||
do_save = True
|
||||
if directive in ("UPDATE", "REPLACE"):
|
||||
|
||||
if key == "key":
|
||||
obj.db_key = init_spawn_value(val, str)
|
||||
elif key == "typeclass":
|
||||
obj.db_typeclass_path = init_spawn_value(val, str)
|
||||
elif key == "location":
|
||||
obj.db_location = init_spawn_value(val, value_to_obj)
|
||||
elif key == "home":
|
||||
obj.db_home = init_spawn_value(val, value_to_obj)
|
||||
elif key == "destination":
|
||||
obj.db_destination = init_spawn_value(val, value_to_obj)
|
||||
elif key == "locks":
|
||||
if directive == "REPLACE":
|
||||
obj.locks.clear()
|
||||
obj.locks.add(init_spawn_value(val, str))
|
||||
elif key == "permissions":
|
||||
if directive == "REPLACE":
|
||||
obj.permissions.clear()
|
||||
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
|
||||
elif key == "aliases":
|
||||
if directive == "REPLACE":
|
||||
obj.aliases.clear()
|
||||
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
|
||||
elif key == "tags":
|
||||
if directive == "REPLACE":
|
||||
obj.tags.clear()
|
||||
obj.tags.batch_add(
|
||||
*(
|
||||
(init_spawn_value(ttag, str), tcategory, tdata)
|
||||
for ttag, tcategory, tdata in val
|
||||
)
|
||||
)
|
||||
elif key == "attrs":
|
||||
if directive == "REPLACE":
|
||||
obj.attributes.clear()
|
||||
obj.attributes.batch_add(
|
||||
*(
|
||||
(
|
||||
init_spawn_value(akey, str),
|
||||
init_spawn_value(aval, value_to_obj),
|
||||
acategory,
|
||||
alocks,
|
||||
if key in _PROTOTYPE_META_NAMES:
|
||||
# prototype meta keys are not stored on-object
|
||||
continue
|
||||
|
||||
val = new_prototype[key]
|
||||
do_save = True
|
||||
|
||||
if key == "key":
|
||||
obj.db_key = init_spawn_value(val, str)
|
||||
elif key == "typeclass":
|
||||
obj.db_typeclass_path = init_spawn_value(val, str)
|
||||
elif key == "location":
|
||||
obj.db_location = init_spawn_value(val, value_to_obj)
|
||||
elif key == "home":
|
||||
obj.db_home = init_spawn_value(val, value_to_obj)
|
||||
elif key == "destination":
|
||||
obj.db_destination = init_spawn_value(val, value_to_obj)
|
||||
elif key == "locks":
|
||||
if directive == "REPLACE":
|
||||
obj.locks.clear()
|
||||
obj.locks.add(init_spawn_value(val, str))
|
||||
elif key == "permissions":
|
||||
if directive == "REPLACE":
|
||||
obj.permissions.clear()
|
||||
obj.permissions.batch_add(*(init_spawn_value(perm, str) for perm in val))
|
||||
elif key == "aliases":
|
||||
if directive == "REPLACE":
|
||||
obj.aliases.clear()
|
||||
obj.aliases.batch_add(*(init_spawn_value(alias, str) for alias in val))
|
||||
elif key == "tags":
|
||||
if directive == "REPLACE":
|
||||
obj.tags.clear()
|
||||
obj.tags.batch_add(
|
||||
*(
|
||||
(init_spawn_value(ttag, str), tcategory, tdata)
|
||||
for ttag, tcategory, tdata in val
|
||||
)
|
||||
for akey, aval, acategory, alocks in val
|
||||
)
|
||||
)
|
||||
elif key == "exec":
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
|
||||
elif directive == "REMOVE":
|
||||
do_save = True
|
||||
if key == "key":
|
||||
obj.db_key = ""
|
||||
elif key == "typeclass":
|
||||
# fall back to default
|
||||
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
|
||||
elif key == "location":
|
||||
obj.db_location = None
|
||||
elif key == "home":
|
||||
obj.db_home = None
|
||||
elif key == "destination":
|
||||
obj.db_destination = None
|
||||
elif key == "locks":
|
||||
obj.locks.clear()
|
||||
elif key == "permissions":
|
||||
obj.permissions.clear()
|
||||
elif key == "aliases":
|
||||
obj.aliases.clear()
|
||||
elif key == "tags":
|
||||
obj.tags.clear()
|
||||
elif key == "attrs":
|
||||
obj.attributes.clear()
|
||||
elif key == "exec":
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.remove(key)
|
||||
elif key == "attrs":
|
||||
if directive == "REPLACE":
|
||||
obj.attributes.clear()
|
||||
obj.attributes.batch_add(
|
||||
*(
|
||||
(
|
||||
init_spawn_value(akey, str),
|
||||
init_spawn_value(aval, value_to_obj),
|
||||
acategory,
|
||||
alocks,
|
||||
)
|
||||
for akey, aval, acategory, alocks in val
|
||||
)
|
||||
)
|
||||
elif key == "exec":
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.add(key, init_spawn_value(val, value_to_obj))
|
||||
elif directive == "REMOVE":
|
||||
do_save = True
|
||||
if key == "key":
|
||||
obj.db_key = ""
|
||||
elif key == "typeclass":
|
||||
# fall back to default
|
||||
obj.db_typeclass_path = settings.BASE_OBJECT_TYPECLASS
|
||||
elif key == "location":
|
||||
obj.db_location = None
|
||||
elif key == "home":
|
||||
obj.db_home = None
|
||||
elif key == "destination":
|
||||
obj.db_destination = None
|
||||
elif key == "locks":
|
||||
obj.locks.clear()
|
||||
elif key == "permissions":
|
||||
obj.permissions.clear()
|
||||
elif key == "aliases":
|
||||
obj.aliases.clear()
|
||||
elif key == "tags":
|
||||
obj.tags.clear()
|
||||
elif key == "attrs":
|
||||
obj.attributes.clear()
|
||||
elif key == "exec":
|
||||
# we don't auto-rerun exec statements, it would be huge security risk!
|
||||
pass
|
||||
else:
|
||||
obj.attributes.remove(key)
|
||||
except Exception:
|
||||
logger.log_trace(f"Failed to apply prototype '{prototype_key}' to {obj}.")
|
||||
finally:
|
||||
# we must always make sure to re-add the prototype tag
|
||||
obj.tags.clear(category=PROTOTYPE_TAG_CATEGORY)
|
||||
obj.tags.add(prototype_key, category=PROTOTYPE_TAG_CATEGORY)
|
||||
|
||||
if do_save:
|
||||
changed += 1
|
||||
obj.save()
|
||||
|
|
@ -707,8 +843,8 @@ def spawn(*prototypes, **kwargs):
|
|||
Args:
|
||||
prototypes (str or dict): Each argument should either be a
|
||||
prototype_key (will be used to find the prototype) or a full prototype
|
||||
dictionary. These will be batched-spawned as one object each.
|
||||
Kwargs:
|
||||
dictionary. These will be batched-spawned as one object each.
|
||||
Keyword Args:
|
||||
prototype_modules (str or list): A python-path to a prototype
|
||||
module, or a list of such paths. These will be used to build
|
||||
the global protparents dictionary accessible by the input
|
||||
|
|
@ -804,7 +940,7 @@ def spawn(*prototypes, **kwargs):
|
|||
prototype_key = prototype.get("prototype_key", None)
|
||||
if prototype_key:
|
||||
# we make sure to add a tag identifying which prototype created this object
|
||||
tags.append((prototype_key, _PROTOTYPE_TAG_CATEGORY))
|
||||
tags.append((prototype_key, PROTOTYPE_TAG_CATEGORY))
|
||||
|
||||
val = prot.pop("exec", "")
|
||||
execs = init_spawn_value(val, make_iter)
|
||||
|
|
|
|||
|
|
@ -3,15 +3,17 @@ Unit tests for the prototypes and spawner
|
|||
|
||||
"""
|
||||
|
||||
from random import randint
|
||||
from random import randint, sample
|
||||
import mock
|
||||
import uuid
|
||||
from time import time
|
||||
from anything import Something
|
||||
from django.test.utils import override_settings
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.utils.tests.test_evmenu import TestEvMenu
|
||||
from evennia.prototypes import spawner, prototypes as protlib
|
||||
from evennia.prototypes import menus as olc_menus
|
||||
from evennia.prototypes import protfuncs as protofuncs
|
||||
from evennia.prototypes import protfuncs as protofuncs, spawner
|
||||
|
||||
from evennia.prototypes.prototypes import _PROTOTYPE_TAG_META_CATEGORY
|
||||
|
||||
|
|
@ -212,22 +214,21 @@ class TestUtils(EvenniaTest):
|
|||
"puppet:pperm(Developer);tell:perm(Admin);view:all()",
|
||||
"KEEP",
|
||||
),
|
||||
"prototype_tags": {},
|
||||
"prototype_tags": (None, None, "KEEP"),
|
||||
"attrs": {
|
||||
"oldtest": (
|
||||
("oldtest", "to_keep", None, ""),
|
||||
("oldtest", "to_keep", None, ""),
|
||||
"KEEP",
|
||||
),
|
||||
"test": (("test", "testval", None, ""), None, "REMOVE"),
|
||||
"desc": (("desc", "changed desc", None, ""), None, "REMOVE"),
|
||||
"fooattr": (None, ("fooattr", "fooattrval", None, ""), "ADD"),
|
||||
"desc": (("desc", "changed desc", None, ""), None, "KEEP"),
|
||||
"fooattr": (Something, ("fooattr", "fooattrval", None, ""), "ADD"),
|
||||
"test": (
|
||||
("test", "testval", None, ""),
|
||||
("test", "testval_changed", None, ""),
|
||||
"UPDATE",
|
||||
),
|
||||
"new": (None, ("new", "new_val", None, ""), "ADD"),
|
||||
"new": (Something, ("new", "new_val", None, ""), "ADD"),
|
||||
},
|
||||
"key": ("Obj", "Obj", "KEEP"),
|
||||
"typeclass": (
|
||||
|
|
@ -246,7 +247,7 @@ class TestUtils(EvenniaTest):
|
|||
spawner.flatten_diff(pdiff),
|
||||
{
|
||||
"aliases": "REMOVE",
|
||||
"attrs": "REPLACE",
|
||||
"attrs": "UPDATE",
|
||||
"home": "KEEP",
|
||||
"key": "KEEP",
|
||||
"location": "KEEP",
|
||||
|
|
@ -270,7 +271,9 @@ class TestUtils(EvenniaTest):
|
|||
new_prot = spawner.prototype_from_object(self.obj1)
|
||||
self.assertEqual(
|
||||
{
|
||||
"aliases": ["foo"],
|
||||
"attrs": [
|
||||
("desc", "changed desc", None, ""),
|
||||
("fooattr", "fooattrval", None, ""),
|
||||
("new", "new_val", None, ""),
|
||||
("oldtest", "to_keep", None, ""),
|
||||
|
|
@ -293,6 +296,7 @@ class TestUtils(EvenniaTest):
|
|||
"view:all()",
|
||||
]
|
||||
),
|
||||
"tags": [("footag", "foocategory", None), (Something, "from_prototype", None)],
|
||||
"permissions": ["builder"],
|
||||
"prototype_desc": "Built from Obj",
|
||||
"prototype_key": Something,
|
||||
|
|
@ -626,8 +630,8 @@ class TestPrototypeStorage(EvenniaTest):
|
|||
|
||||
# partial match
|
||||
with mock.patch("evennia.prototypes.prototypes._MODULE_PROTOTYPES", {}):
|
||||
self.assertEqual(list(protlib.search_prototype("prot")), [prot1b, prot2, prot3])
|
||||
self.assertEqual(list(protlib.search_prototype(tags="foo1")), [prot1b, prot2, prot3])
|
||||
self.assertCountEqual(protlib.search_prototype("prot"), [prot1b, prot2, prot3])
|
||||
self.assertCountEqual(protlib.search_prototype(tags="foo1"), [prot1b, prot2, prot3])
|
||||
|
||||
self.assertTrue(str(str(protlib.list_prototypes(self.char1))))
|
||||
|
||||
|
|
@ -851,7 +855,7 @@ class TestMenuModule(EvenniaTest):
|
|||
|
||||
self.assertEqual(obj.typeclass_path, "evennia.objects.objects.DefaultObject")
|
||||
self.assertEqual(
|
||||
obj.tags.get(category=spawner._PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"]
|
||||
obj.tags.get(category=spawner.PROTOTYPE_TAG_CATEGORY), self.test_prot["prototype_key"]
|
||||
)
|
||||
|
||||
# update helpers
|
||||
|
|
@ -912,24 +916,20 @@ class TestMenuModule(EvenniaTest):
|
|||
|
||||
texts, options = olc_menus._format_diff_text_and_options(obj_diff)
|
||||
self.assertEqual(
|
||||
"\n".join(texts),
|
||||
"- |wattrs:|n \n"
|
||||
" |gKEEP|W:|n desc |W=|n This is User #1. |W(category:|n None|W, locks:|n |W)|n\n"
|
||||
" |c[1] |yADD|n|W:|n None |W->|n foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n\n"
|
||||
" |gKEEP|W:|n prelogout_location |W=|n #2 |W(category:|n None|W, locks:|n |W)|n\n"
|
||||
"- |whome:|n |gKEEP|W:|n #2\n"
|
||||
"- |wkey:|n |gKEEP|W:|n TestChar\n"
|
||||
"- |wlocks:|n |gKEEP|W:|n boot:false();call:false();control:perm(Developer);delete:false();edit:false();examine:perm(Developer);get:false();msg:all();puppet:false();tell:perm(Admin);view:all()\n"
|
||||
"- |wpermissions:|n \n"
|
||||
" |gKEEP|W:|n developer\n"
|
||||
"- |wprototype_desc:|n |c[2] |rREMOVE|n|W:|n Testobject build |W->|n None\n"
|
||||
"- |wprototype_key:|n |gKEEP|W:|n TestDiffKey\n"
|
||||
"- |wprototype_locks:|n |gKEEP|W:|n spawn:all();edit:all()\n"
|
||||
"- |wprototype_tags:|n \n"
|
||||
"- |wtags:|n \n"
|
||||
" |c[3] |yADD|n|W:|n None |W->|n foo |W(category:|n None|W)|n\n"
|
||||
"- |wtypeclass:|n |gKEEP|W:|n typeclasses.characters.Character",
|
||||
"\n".join(txt.strip() for txt in texts),
|
||||
"- |wattrs:|n |c[1] |yADD|n: foo |W=|n bar |W(category:|n None|W, locks:|n |W)|n"
|
||||
"\n- |whome:|n"
|
||||
"\n- |wkey:|n"
|
||||
"\n- |wlocks:|n"
|
||||
"\n- |wpermissions:|n"
|
||||
"\n- |wprototype_desc:|n |c[2] |rREMOVE|n: Testobject build"
|
||||
"\n- |wprototype_key:|n"
|
||||
"\n- |wprototype_locks:|n"
|
||||
"\n- |wprototype_tags:|n"
|
||||
"\n- |wtags:|n |c[3] |yADD|n: foo |W(category:|n None|W)|n"
|
||||
"\n- |wtypeclass:|n",
|
||||
)
|
||||
|
||||
self.assertEqual(
|
||||
options,
|
||||
[
|
||||
|
|
@ -1075,3 +1075,30 @@ class TestOLCMenu(TestEvMenu):
|
|||
["node_index", "node_index", "node_index"],
|
||||
],
|
||||
]
|
||||
|
||||
|
||||
class PrototypeCrashTest(EvenniaTest):
|
||||
|
||||
# increase this to 1000 for optimization testing
|
||||
num_prototypes = 10
|
||||
|
||||
def create(self, num=None):
|
||||
if not num:
|
||||
num = self.num_prototypes
|
||||
# print(f"Creating {num} additional prototypes...")
|
||||
for x in range(num):
|
||||
prot = {
|
||||
"prototype_key": str(uuid.uuid4()),
|
||||
"some_attributes": [str(uuid.uuid4()) for x in range(10)],
|
||||
"prototype_tags": list(sample(["demo", "test", "stuff"], 2)),
|
||||
}
|
||||
protlib.save_prototype(prot)
|
||||
|
||||
def test_prototype_dos(self, *args, **kwargs):
|
||||
num_prototypes = self.num_prototypes
|
||||
for x in range(2):
|
||||
self.create(num_prototypes)
|
||||
# print("Attempting to list prototypes...")
|
||||
# start_time = time()
|
||||
self.char1.execute_cmd("spawn/list")
|
||||
# print(f"Prototypes listed in {time()-start_time} seconds.")
|
||||
|
|
|
|||
|
|
@ -73,7 +73,7 @@ class ScriptDBManager(TypedObjectManager):
|
|||
Get all scripts in the database.
|
||||
|
||||
Args:
|
||||
key (str, optional): Restrict result to only those
|
||||
key (str or int, optional): Restrict result to only those
|
||||
with matching key or dbref.
|
||||
|
||||
Returns:
|
||||
|
|
@ -83,12 +83,9 @@ class ScriptDBManager(TypedObjectManager):
|
|||
if key:
|
||||
script = []
|
||||
dbref = self.dbref(key)
|
||||
if dbref or dbref == 0:
|
||||
# return either [] or a valid list (never [None])
|
||||
script = [res for res in [self.dbref_search(dbref)] if res]
|
||||
if not script:
|
||||
script = self.filter(db_key=key)
|
||||
return script
|
||||
if dbref:
|
||||
return self.filter(id=dbref)
|
||||
return self.filter(db_key__iexact=key.strip())
|
||||
return self.all()
|
||||
|
||||
def delete_script(self, dbref):
|
||||
|
|
@ -231,7 +228,7 @@ class ScriptDBManager(TypedObjectManager):
|
|||
ostring = ostring.strip()
|
||||
|
||||
dbref = self.dbref(ostring)
|
||||
if dbref or dbref == 0:
|
||||
if dbref:
|
||||
# this is a dbref, try to find the script directly
|
||||
dbref_match = self.dbref_search(dbref)
|
||||
if dbref_match and not (
|
||||
|
|
|
|||
|
|
@ -93,7 +93,7 @@ class MonitorHandler(object):
|
|||
|
||||
def at_update(self, obj, fieldname):
|
||||
"""
|
||||
Called by the field as it saves.
|
||||
Called by the field/attribute as it saves.
|
||||
|
||||
"""
|
||||
to_delete = []
|
||||
|
|
@ -125,7 +125,7 @@ class MonitorHandler(object):
|
|||
persistent (bool, optional): If False, the monitor will survive
|
||||
a server reload but not a cold restart. This is default.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
session (Session): If this keyword is given, the monitorhandler will
|
||||
correctly analyze it and remove the monitor if after a reload/reboot
|
||||
the session is no longer valid.
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ from evennia.scripts.models import ScriptDB
|
|||
from evennia.utils import create
|
||||
from evennia.utils import logger
|
||||
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
|
||||
class ScriptHandler(object):
|
||||
|
|
@ -78,11 +78,20 @@ class ScriptHandler(object):
|
|||
scriptclass, key=key, account=self.obj, autostart=autostart
|
||||
)
|
||||
else:
|
||||
# the normal - adding to an Object
|
||||
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart)
|
||||
# the normal - adding to an Object. We wait to autostart so we can differentiate
|
||||
# a failing creation from a script that immediately starts/stops.
|
||||
script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=False)
|
||||
if not script:
|
||||
logger.log_err("Script %s could not be created and/or started." % scriptclass)
|
||||
logger.log_err("Script %s failed to be created/started." % scriptclass)
|
||||
return False
|
||||
if autostart:
|
||||
script.start()
|
||||
if not script.id:
|
||||
# this can happen if the script has repeats=1 or calls stop() in at_repeat.
|
||||
logger.log_info(
|
||||
"Script %s started and then immediately stopped; "
|
||||
"it could probably be a normal function." % scriptclass
|
||||
)
|
||||
return True
|
||||
|
||||
def start(self, key):
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ ability to run timers.
|
|||
from twisted.internet.defer import Deferred, maybeDeferred
|
||||
from twisted.internet.task import LoopingCall
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.scripts.models import ScriptDB
|
||||
from evennia.scripts.manager import ScriptManager
|
||||
|
|
@ -69,7 +69,7 @@ class ExtendedLoopingCall(LoopingCall):
|
|||
steps if we want.
|
||||
|
||||
"""
|
||||
assert not self.running, "Tried to start an already running " "ExtendedLoopingCall."
|
||||
assert not self.running, "Tried to start an already running ExtendedLoopingCall."
|
||||
if interval < 0:
|
||||
raise ValueError("interval must be >= 0")
|
||||
self.running = True
|
||||
|
|
@ -107,7 +107,8 @@ class ExtendedLoopingCall(LoopingCall):
|
|||
if self.start_delay:
|
||||
self.start_delay = None
|
||||
self.starttime = self.clock.seconds()
|
||||
LoopingCall.__call__(self)
|
||||
if self._deferred:
|
||||
LoopingCall.__call__(self)
|
||||
|
||||
def force_repeat(self):
|
||||
"""
|
||||
|
|
@ -118,7 +119,7 @@ class ExtendedLoopingCall(LoopingCall):
|
|||
running.
|
||||
|
||||
"""
|
||||
assert self.running, "Tried to fire an ExtendedLoopingCall " "that was not running."
|
||||
assert self.running, "Tried to fire an ExtendedLoopingCall that was not running."
|
||||
self.call.cancel()
|
||||
self.call = None
|
||||
self.starttime = self.clock.seconds()
|
||||
|
|
@ -135,11 +136,10 @@ class ExtendedLoopingCall(LoopingCall):
|
|||
the task is not running.
|
||||
|
||||
"""
|
||||
if self.running:
|
||||
if self.running and self.interval > 0:
|
||||
total_runtime = self.clock.seconds() - self.starttime
|
||||
interval = self.start_delay or self.interval
|
||||
return interval - (total_runtime % self.interval)
|
||||
return None
|
||||
|
||||
|
||||
class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
||||
|
|
@ -162,9 +162,8 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
Start task runner.
|
||||
|
||||
"""
|
||||
if self.ndb._task:
|
||||
return
|
||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||
if not self.ndb._task:
|
||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||
|
||||
if self.db._paused_time:
|
||||
# the script was paused; restarting
|
||||
|
|
@ -174,7 +173,8 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
)
|
||||
del self.db._paused_time
|
||||
del self.db._paused_repeats
|
||||
else:
|
||||
|
||||
elif not self.ndb._task.running:
|
||||
# starting script anew
|
||||
self.ndb._task.start(self.db_interval, now=not self.db_start_delay)
|
||||
|
||||
|
|
@ -186,6 +186,7 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
task = self.ndb._task
|
||||
if task and task.running:
|
||||
task.stop()
|
||||
self.ndb._task = None
|
||||
|
||||
def _step_errback(self, e):
|
||||
"""
|
||||
|
|
@ -208,6 +209,9 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
Step task runner. No try..except needed due to defer wrap.
|
||||
|
||||
"""
|
||||
if not self.ndb._task:
|
||||
# if there is no task, we have no business using this method
|
||||
return
|
||||
|
||||
if not self.is_valid():
|
||||
self.stop()
|
||||
|
|
@ -217,10 +221,13 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
self.at_repeat()
|
||||
|
||||
# check repeats
|
||||
callcount = self.ndb._task.callcount
|
||||
maxcount = self.db_repeats
|
||||
if maxcount > 0 and maxcount <= callcount:
|
||||
self.stop()
|
||||
if self.ndb._task:
|
||||
# we need to check for the task in case stop() was called
|
||||
# inside at_repeat() and it already went away.
|
||||
callcount = self.ndb._task.callcount
|
||||
maxcount = self.db_repeats
|
||||
if maxcount > 0 and maxcount <= callcount:
|
||||
self.stop()
|
||||
|
||||
def _step_task(self):
|
||||
"""
|
||||
|
|
@ -267,13 +274,13 @@ class ScriptBase(ScriptDB, metaclass=TypeclassBase):
|
|||
self.db_key = cdict["key"]
|
||||
updates.append("db_key")
|
||||
if cdict.get("interval") and self.interval != cdict["interval"]:
|
||||
self.db_interval = cdict["interval"]
|
||||
self.db_interval = max(0, cdict["interval"])
|
||||
updates.append("db_interval")
|
||||
if cdict.get("start_delay") and self.start_delay != cdict["start_delay"]:
|
||||
self.db_start_delay = cdict["start_delay"]
|
||||
updates.append("db_start_delay")
|
||||
if cdict.get("repeats") and self.repeats != cdict["repeats"]:
|
||||
self.db_repeats = cdict["repeats"]
|
||||
self.db_repeats = max(0, cdict["repeats"])
|
||||
updates.append("db_repeats")
|
||||
if cdict.get("persistent") and self.persistent != cdict["persistent"]:
|
||||
self.db_persistent = cdict["persistent"]
|
||||
|
|
@ -338,9 +345,9 @@ class DefaultScript(ScriptBase):
|
|||
|
||||
try:
|
||||
obj = create.create_script(**kwargs)
|
||||
except Exception as e:
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
errors.append("The script '%s' encountered errors and could not be created." % key)
|
||||
logger.log_err(e)
|
||||
|
||||
return obj, errors
|
||||
|
||||
|
|
@ -430,7 +437,7 @@ class DefaultScript(ScriptBase):
|
|||
if self.is_active and not force_restart:
|
||||
# The script is already running, but make sure we have a _task if
|
||||
# this is after a cache flush
|
||||
if not self.ndb._task and self.db_interval >= 0:
|
||||
if not self.ndb._task and self.db_interval > 0:
|
||||
self.ndb._task = ExtendedLoopingCall(self._step_task)
|
||||
try:
|
||||
start_delay, callcount = SCRIPT_FLUSH_TIMERS[self.id]
|
||||
|
|
@ -562,11 +569,9 @@ class DefaultScript(ScriptBase):
|
|||
Restarts an already existing/running Script from the
|
||||
beginning, optionally using different settings. This will
|
||||
first call the stop hooks, and then the start hooks again.
|
||||
|
||||
Args:
|
||||
interval (int, optional): Allows for changing the interval
|
||||
of the Script. Given in seconds. if `None`, will use the
|
||||
already stored interval.
|
||||
of the Script. Given in seconds. if `None`, will use the already stored interval.
|
||||
repeats (int, optional): The number of repeats. If unset, will
|
||||
use the previous setting.
|
||||
start_delay (bool, optional): If we should wait `interval` seconds
|
||||
|
|
@ -585,6 +590,7 @@ class DefaultScript(ScriptBase):
|
|||
del self.db._paused_callcount
|
||||
# set new flags and start over
|
||||
if interval is not None:
|
||||
interval = max(0, interval)
|
||||
self.interval = interval
|
||||
if repeats is not None:
|
||||
self.repeats = repeats
|
||||
|
|
|
|||
|
|
@ -100,7 +100,7 @@ class TaskHandler(object):
|
|||
callback (function or instance method): the callback itself
|
||||
any (any): any additional positional arguments to send to the callback
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
persistent (bool, optional): persist the task (store it).
|
||||
any (any): additional keyword arguments to send to the callback
|
||||
|
||||
|
|
|
|||
|
|
@ -73,16 +73,22 @@ from evennia.scripts.scripts import ExtendedLoopingCall
|
|||
from evennia.server.models import ServerConfig
|
||||
from evennia.utils.logger import log_trace, log_err
|
||||
from evennia.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj
|
||||
from evennia.utils import variable_from_module
|
||||
from evennia.utils import variable_from_module, inherits_from
|
||||
|
||||
_GA = object.__getattribute__
|
||||
_SA = object.__setattr__
|
||||
|
||||
|
||||
_ERROR_ADD_TICKER = """TickerHandler: Tried to add an invalid ticker:
|
||||
{storekey}
|
||||
{store_key}
|
||||
Ticker was not added."""
|
||||
|
||||
_ERROR_ADD_TICKER_SUB_SECOND = """You are trying to add a ticker running faster
|
||||
than once per second. This is not supported and also probably not useful:
|
||||
Spamming messages to the user faster than once per second serves no purpose in
|
||||
a text-game, and if you want to update some property, consider doing so
|
||||
on-demand rather than using a ticker.
|
||||
"""
|
||||
|
||||
class Ticker(object):
|
||||
"""
|
||||
|
|
@ -183,7 +189,7 @@ class Ticker(object):
|
|||
store_key (str): Unique storage hash for this ticker subscription.
|
||||
args (any, optional): Arguments to call the hook method with.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
_start_delay (int): If set, this will be
|
||||
used to delay the start of the trigger instead of
|
||||
`interval`.
|
||||
|
|
@ -319,7 +325,6 @@ class TickerHandler(object):
|
|||
callback (function or method): This is either a stand-alone
|
||||
function or class method on a typeclassed entitye (that is,
|
||||
an entity that can be saved to the database).
|
||||
|
||||
Returns:
|
||||
ret (tuple): This is a tuple of the form `(obj, path, callfunc)`,
|
||||
where `obj` is the database object the callback is defined on
|
||||
|
|
@ -327,6 +332,8 @@ class TickerHandler(object):
|
|||
the python-path to the stand-alone function (`None` if a method).
|
||||
The `callfunc` is either the name of the method to call or the
|
||||
callable function object itself.
|
||||
Raises:
|
||||
TypeError: If the callback is of an unsupported type.
|
||||
|
||||
"""
|
||||
outobj, outpath, outcallfunc = None, None, None
|
||||
|
|
@ -337,8 +344,17 @@ class TickerHandler(object):
|
|||
elif inspect.isfunction(callback):
|
||||
outpath = "%s.%s" % (callback.__module__, callback.__name__)
|
||||
outcallfunc = callback
|
||||
else:
|
||||
raise TypeError(f"{callback} is not a method or function.")
|
||||
else:
|
||||
raise TypeError("%s is not a callable function or method." % callback)
|
||||
raise TypeError(f"{callback} is not a callable function or method.")
|
||||
|
||||
if outobj and not inherits_from(outobj, "evennia.typeclasses.models.TypedObject"):
|
||||
raise TypeError(
|
||||
f"{callback} is a method on a normal object - it must "
|
||||
"be either a method on a typeclass, or a stand-alone function."
|
||||
)
|
||||
|
||||
return outobj, outpath, outcallfunc
|
||||
|
||||
def _store_key(self, obj, path, interval, callfunc, idstring="", persistent=True):
|
||||
|
|
@ -349,7 +365,8 @@ class TickerHandler(object):
|
|||
obj (Object, tuple or None): Subscribing object if any. If a tuple, this is
|
||||
a packed_obj tuple from dbserialize.
|
||||
path (str or None): Python-path to callable, if any.
|
||||
interval (int): Ticker interval.
|
||||
interval (int): Ticker interval. Floats will be converted to
|
||||
nearest lower integer value.
|
||||
callfunc (callable or str): This is either the callable function or
|
||||
the name of the method to call. Note that the callable is never
|
||||
stored in the key; that is uniquely identified with the python-path.
|
||||
|
|
@ -368,6 +385,9 @@ class TickerHandler(object):
|
|||
`idstring` and `persistent` are integers, strings and bools respectively.
|
||||
|
||||
"""
|
||||
if interval < 1:
|
||||
raise RuntimeError(_ERROR_ADD_TICKER_SUB_SECOND)
|
||||
|
||||
interval = int(interval)
|
||||
persistent = bool(persistent)
|
||||
packed_obj = pack_dbobj(obj)
|
||||
|
|
@ -504,12 +524,6 @@ class TickerHandler(object):
|
|||
when wanting to modify/remove the ticker later.
|
||||
|
||||
"""
|
||||
if isinstance(callback, int):
|
||||
raise RuntimeError(
|
||||
"TICKER_HANDLER.add has changed: "
|
||||
"the interval is now the first argument, callback the second."
|
||||
)
|
||||
|
||||
obj, path, callfunc = self._get_callback(callback)
|
||||
store_key = self._store_key(obj, path, interval, callfunc, idstring, persistent)
|
||||
kwargs["_obj"] = obj
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ class ConnectionWizard(object):
|
|||
resp = str(default)
|
||||
|
||||
if resp.lower() in options:
|
||||
self.display(f" Selected '{resp}'.")
|
||||
# self.display(f" Selected '{resp}'.")
|
||||
desc, callback, kwargs = options[resp.lower()]
|
||||
callback(self, **kwargs)
|
||||
elif resp.lower() in ("quit", "q"):
|
||||
|
|
@ -57,7 +57,7 @@ class ConnectionWizard(object):
|
|||
"""
|
||||
Ask a yes/no question inline.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
prompt (str): The prompt to ask.
|
||||
default (str): "yes" or "no", used if pressing return.
|
||||
Returns:
|
||||
|
|
@ -83,7 +83,7 @@ class ConnectionWizard(object):
|
|||
"""
|
||||
Ask multiple-choice question, get response inline.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
prompt (str): Input prompt.
|
||||
options (list): List of options. Will be indexable by sequence number 1...
|
||||
default (int): The list index+1 of the default choice, if any
|
||||
|
|
@ -114,7 +114,7 @@ class ConnectionWizard(object):
|
|||
"""
|
||||
Get arbitrary input inline.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
prompt (str): The display prompt.
|
||||
default (str): If empty input, use this.
|
||||
validator (callable): If given, the input will be passed
|
||||
|
|
@ -161,8 +161,10 @@ class ConnectionWizard(object):
|
|||
|
||||
def node_start(wizard):
|
||||
text = """
|
||||
This wizard helps activate external networks with Evennia. It will create
|
||||
a config that will be attached to the bottom of the game settings file.
|
||||
This wizard helps to attach your Evennia server to external networks. It
|
||||
will save to a file `server/conf/connection_settings.py` that will be
|
||||
imported from the bottom of your game settings file. Once generated you can
|
||||
also modify that file directly.
|
||||
|
||||
Make sure you have at least started the game once before continuing!
|
||||
|
||||
|
|
@ -174,11 +176,15 @@ def node_start(wizard):
|
|||
node_game_index_start,
|
||||
{},
|
||||
),
|
||||
# "2": ("Add MSSP information (for mud-list crawlers)",
|
||||
# node_mssp_start, {}),
|
||||
"2": ("MSSP setup (for mud-list crawlers)", node_mssp_start, {}),
|
||||
# "3": ("Add Grapevine listing",
|
||||
# node_grapevine_start, {}),
|
||||
"2": ("View and Save created settings", node_view_and_apply_settings, {}),
|
||||
# "4": ("Add IRC link",
|
||||
# "node_irc_start", {}),
|
||||
# "5" ("Add RSS feed",
|
||||
# "node_rss_start", {}),
|
||||
"s": ("View and (optionally) Save created settings", node_view_and_apply_settings, {}),
|
||||
"q": ("Quit", lambda *args: sys.exit(), {}),
|
||||
}
|
||||
|
||||
wizard.display(text)
|
||||
|
|
@ -189,13 +195,13 @@ def node_start(wizard):
|
|||
|
||||
|
||||
def node_game_index_start(wizard, **kwargs):
|
||||
text = f"""
|
||||
text = """
|
||||
The Evennia game index (http://games.evennia.com) lists both active Evennia
|
||||
games as well as games in various stages of development.
|
||||
|
||||
You can put up your game in the index also if you are not (yet) open for
|
||||
players. If so, put 'None' for the connection details. Just tell us you
|
||||
are out there and make us excited about your upcoming game!
|
||||
players. If so, put 'None' for the connection details - you are just telling
|
||||
us that you are out there, making us excited about your upcoming game!
|
||||
|
||||
Please check the listing online first to see that your exact game name is
|
||||
not colliding with an existing game-name in the list (be nice!).
|
||||
|
|
@ -222,9 +228,9 @@ def node_game_index_fields(wizard, status=None):
|
|||
- pre-alpha: a game in its very early stages, mostly unfinished or unstarted
|
||||
- alpha: a working concept, probably lots of bugs and incomplete features
|
||||
- beta: a working game, but expect bugs and changing features
|
||||
- launched: a full, working game that may still be expanded upon and improved later
|
||||
- launched: a full, working game (that may still be expanded upon and improved later)
|
||||
|
||||
Current value:
|
||||
Current value (return to keep):
|
||||
{status_default}
|
||||
"""
|
||||
|
||||
|
|
@ -233,6 +239,31 @@ def node_game_index_fields(wizard, status=None):
|
|||
wizard.display(text)
|
||||
wizard.game_index_listing["game_status"] = wizard.ask_choice("Select one: ", options)
|
||||
|
||||
# game name
|
||||
|
||||
name_default = settings.SERVERNAME
|
||||
text = f"""
|
||||
Your game's name should usually be the same as `settings.SERVERNAME`, but
|
||||
you can set it to something else here if you want.
|
||||
|
||||
Current value:
|
||||
{name_default}
|
||||
"""
|
||||
|
||||
def name_validator(inp):
|
||||
tmax = 80
|
||||
tlen = len(inp)
|
||||
if tlen > tmax:
|
||||
print(f"The name must be shorter than {tmax} characters (was {tlen}).")
|
||||
wizard.ask_continue()
|
||||
return False
|
||||
return True
|
||||
|
||||
wizard.display(text)
|
||||
wizard.game_index_listing["game_name"] = wizard.ask_input(
|
||||
default=name_default, validator=name_validator
|
||||
)
|
||||
|
||||
# short desc
|
||||
|
||||
sdesc_default = wizard.game_index_listing.get("short_description", None)
|
||||
|
|
@ -249,7 +280,7 @@ def node_game_index_fields(wizard, status=None):
|
|||
def sdesc_validator(inp):
|
||||
tmax = 255
|
||||
tlen = len(inp)
|
||||
if tlen > 255:
|
||||
if tlen > tmax:
|
||||
print(f"The short desc must be shorter than {tmax} characters (was {tlen}).")
|
||||
wizard.ask_continue()
|
||||
return False
|
||||
|
|
@ -341,7 +372,7 @@ def node_game_index_fields(wizard, status=None):
|
|||
Evennia is its own web server and runs your game's website. Enter the
|
||||
URL of the website here, like http://yourwebsite.com, here.
|
||||
|
||||
Wtite 'None' if you are not offering a publicly visible website at this time.
|
||||
Write 'None' if you are not offering a publicly visible website at this time.
|
||||
|
||||
Current value:
|
||||
{website_default}
|
||||
|
|
@ -359,7 +390,7 @@ def node_game_index_fields(wizard, status=None):
|
|||
your specific URL here (when clicking this link you should launch into the
|
||||
web client)
|
||||
|
||||
Wtite 'None' if you don't want to list a publicly accessible webclient.
|
||||
Write 'None' if you don't want to list a publicly accessible webclient.
|
||||
|
||||
Current value:
|
||||
{webclient_default}
|
||||
|
|
@ -388,24 +419,26 @@ def node_game_index_fields(wizard, status=None):
|
|||
|
||||
def node_mssp_start(wizard):
|
||||
|
||||
mssp_module = mod_import(settings.MSSP_META_MODULE)
|
||||
filename = mssp_module.__file__
|
||||
mssp_module = mod_import(settings.MSSP_META_MODULE or "server.conf.mssp")
|
||||
try:
|
||||
filename = mssp_module.__file__
|
||||
except AttributeError:
|
||||
filename = "server/conf/mssp.py"
|
||||
|
||||
text = f"""
|
||||
MSSP (Mud Server Status Protocol) allows online MUD-listing sites/crawlers
|
||||
to continuously monitor your game and list information about it. Some of
|
||||
this, like active player-count, Evennia will automatically add for you,
|
||||
whereas many fields are manually added info about your game.
|
||||
MSSP (Mud Server Status Protocol) has a vast amount of options so it must
|
||||
be modified outside this wizard by directly editing its config file here:
|
||||
|
||||
'{filename}'
|
||||
|
||||
MSSP allows traditional online MUD-listing sites/crawlers to continuously
|
||||
monitor your game and list information about it. Some of this, like active
|
||||
player-count, Evennia will automatically add for you, whereas most fields
|
||||
you need to set manually.
|
||||
|
||||
To use MSSP you should generally have a publicly open game that external
|
||||
players can connect to. You also need to register at a MUD listing site to
|
||||
tell them to list your game.
|
||||
|
||||
MSSP has a large number of configuration options and we found it was simply
|
||||
a lot easier to set them in a file rather than using this wizard. So to
|
||||
configure MSSP, edit the empty template listing found here:
|
||||
|
||||
'{filename}'
|
||||
tell them to crawl your game.
|
||||
"""
|
||||
|
||||
wizard.display(text)
|
||||
|
|
@ -456,25 +489,32 @@ def node_view_and_apply_settings(wizard):
|
|||
pp = pprint.PrettyPrinter(indent=4)
|
||||
saves = False
|
||||
|
||||
game_index_txt = "No changes to save for Game Index."
|
||||
if hasattr(wizard, "game_index_listing"):
|
||||
if wizard.game_index_listing != settings.GAME_INDEX_LISTING:
|
||||
game_index_txt = "No changes to save for Game Index."
|
||||
else:
|
||||
game_index_txt = "GAME_INDEX_ENABLED = True\n" "GAME_INDEX_LISTING = \\\n" + pp.pformat(
|
||||
wizard.game_index_listing
|
||||
)
|
||||
saves = True
|
||||
# game index
|
||||
game_index_save_text = ""
|
||||
game_index_listing = (
|
||||
wizard.game_index_listing if hasattr(wizard, "game_index_listing") else None
|
||||
)
|
||||
if not game_index_listing and settings.GAME_INDEX_ENABLED:
|
||||
game_index_listing = settings.GAME_INDEX_LISTING
|
||||
if game_index_listing:
|
||||
game_index_save_text = (
|
||||
"GAME_INDEX_ENABLED = True\n"
|
||||
"GAME_INDEX_LISTING = \\\n" + pp.pformat(game_index_listing)
|
||||
)
|
||||
saves = True
|
||||
else:
|
||||
game_index_save_text = "# No Game Index settings found."
|
||||
|
||||
text = game_index_txt
|
||||
# potentially add other wizards in the future
|
||||
text = game_index_save_text
|
||||
|
||||
wizard.display(f"Settings to save:\n\n{text}")
|
||||
|
||||
if saves:
|
||||
if wizard.ask_yesno("Do you want to save these settings?") == "yes":
|
||||
if wizard.ask_yesno("\nDo you want to save these settings?") == "yes":
|
||||
wizard.save_output = text
|
||||
_save_changes(wizard)
|
||||
wizard.display("... saved!")
|
||||
wizard.display("... saved!\nThe changes will apply after you reload your server.")
|
||||
else:
|
||||
wizard.display("... cancelled.")
|
||||
wizard.ask_continue()
|
||||
|
|
|
|||
|
|
@ -96,6 +96,12 @@ def check_errors(settings):
|
|||
"must now be either None or a dict "
|
||||
"specifying the properties of the channel to create."
|
||||
)
|
||||
if hasattr(settings, "CYCLE_LOGFILES"):
|
||||
raise DeprecationWarning(
|
||||
"settings.CYCLE_LOGFILES is unused and should be removed. "
|
||||
"Use PORTAL/SERVER_LOG_DAY_ROTATION and PORTAL/SERVER_LOG_MAX_SIZE "
|
||||
"to control log cycling."
|
||||
)
|
||||
|
||||
|
||||
def check_warnings(settings):
|
||||
|
|
@ -109,3 +115,10 @@ def check_warnings(settings):
|
|||
print(" [Devel: settings.IN_GAME_ERRORS is True. Turn off in production.]")
|
||||
if settings.ALLOWED_HOSTS == ["*"]:
|
||||
print(" [Devel: settings.ALLOWED_HOSTS set to '*' (all). Limit in production.]")
|
||||
for dbentry in settings.DATABASES.values():
|
||||
if "psycopg" in dbentry.get("ENGINE", ""):
|
||||
print(
|
||||
'Deprecation: postgresql_psycopg2 backend is deprecated". '
|
||||
"Switch settings.DATABASES to use "
|
||||
'"ENGINE": "django.db.backends.postgresql instead"'
|
||||
)
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ from twisted.protocols import amp
|
|||
from twisted.internet import reactor, endpoints
|
||||
import django
|
||||
from django.core.management import execute_from_command_line
|
||||
from django.db.utils import ProgrammingError
|
||||
|
||||
# Signal processing
|
||||
SIG = signal.SIGINT
|
||||
|
|
@ -92,8 +93,8 @@ SRESET = chr(19) # shutdown server in reset mode
|
|||
# requirements
|
||||
PYTHON_MIN = "3.7"
|
||||
TWISTED_MIN = "18.0.0"
|
||||
DJANGO_MIN = "2.1"
|
||||
DJANGO_REC = "2.2.5"
|
||||
DJANGO_MIN = "2.2.5"
|
||||
DJANGO_LT = "3.0"
|
||||
|
||||
try:
|
||||
sys.path[1] = EVENNIA_ROOT
|
||||
|
|
@ -373,8 +374,8 @@ ERROR_NOTWISTED = """
|
|||
"""
|
||||
|
||||
ERROR_DJANGO_MIN = """
|
||||
ERROR: Django {dversion} found. Evennia requires version {django_min}
|
||||
or higher.
|
||||
ERROR: Django {dversion} found. Evennia requires at least version {django_min} (but
|
||||
no higher than {django_lt}).
|
||||
|
||||
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
|
||||
`evennia` is the folder to where you cloned the Evennia library. If not
|
||||
|
|
@ -385,14 +386,9 @@ ERROR_DJANGO_MIN = """
|
|||
any warnings and don't run `makemigrate` even if told to.
|
||||
"""
|
||||
|
||||
NOTE_DJANGO_MIN = """
|
||||
NOTE: Django {dversion} found. This will work, but Django {django_rec} is
|
||||
recommended for production.
|
||||
"""
|
||||
|
||||
NOTE_DJANGO_NEW = """
|
||||
NOTE: Django {dversion} found. This is newer than Evennia's
|
||||
recommended version ({django_rec}). It might work, but may be new
|
||||
recommended version ({django_rec}). It might work, but is new
|
||||
enough to not be fully tested yet. Report any issues.
|
||||
"""
|
||||
|
||||
|
|
@ -1152,7 +1148,7 @@ def tail_log_files(filename1, filename2, start_lines1=20, start_lines2=20, rate=
|
|||
# this happens if the file was cycled or manually deleted/edited.
|
||||
print(
|
||||
" ** Log file {filename} has cycled or been edited. "
|
||||
"Restarting log. ".format(filehandle.name)
|
||||
"Restarting log. ".format(filename=filehandle.name)
|
||||
)
|
||||
new_linecount = 0
|
||||
old_linecount = 0
|
||||
|
|
@ -1280,14 +1276,16 @@ def check_main_evennia_dependencies():
|
|||
try:
|
||||
dversion = ".".join(str(num) for num in django.VERSION if isinstance(num, int))
|
||||
# only the main version (1.5, not 1.5.4.0)
|
||||
dversion_main = ".".join(dversion.split(".")[:3])
|
||||
dversion_main = ".".join(dversion.split(".")[:2])
|
||||
if LooseVersion(dversion) < LooseVersion(DJANGO_MIN):
|
||||
print(ERROR_DJANGO_MIN.format(dversion=dversion_main, django_min=DJANGO_MIN))
|
||||
print(
|
||||
ERROR_DJANGO_MIN.format(
|
||||
dversion=dversion_main, django_min=DJANGO_MIN, django_lt=DJANGO_LT
|
||||
)
|
||||
)
|
||||
error = True
|
||||
elif LooseVersion(DJANGO_MIN) <= LooseVersion(dversion) < LooseVersion(DJANGO_REC):
|
||||
print(NOTE_DJANGO_MIN.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
||||
elif LooseVersion(DJANGO_REC) < LooseVersion(dversion_main):
|
||||
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_REC))
|
||||
elif LooseVersion(DJANGO_LT) <= LooseVersion(dversion_main):
|
||||
print(NOTE_DJANGO_NEW.format(dversion=dversion_main, django_rec=DJANGO_LT))
|
||||
except ImportError:
|
||||
print(ERROR_NODJANGO)
|
||||
error = True
|
||||
|
|
@ -1367,10 +1365,10 @@ def create_settings_file(init=True, secret_settings=False):
|
|||
if not init:
|
||||
# if not --init mode, settings file may already exist from before
|
||||
if os.path.exists(settings_path):
|
||||
inp = eval(input("%s already exists. Do you want to reset it? y/[N]> " % settings_path))
|
||||
inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
|
||||
if not inp.lower() == "y":
|
||||
print("Aborted.")
|
||||
return
|
||||
sys.exit()
|
||||
else:
|
||||
print("Reset the settings file.")
|
||||
|
||||
|
|
@ -1428,12 +1426,17 @@ def create_superuser():
|
|||
django.core.management.call_command("createsuperuser", interactive=True)
|
||||
|
||||
|
||||
def check_database():
|
||||
def check_database(always_return=False):
|
||||
"""
|
||||
Check so the database exists.
|
||||
|
||||
Args:
|
||||
always_return (bool, optional): If set, will always return True/False
|
||||
also on critical errors. No output will be printed.
|
||||
Returns:
|
||||
exists (bool): `True` if the database exists, otherwise `False`.
|
||||
|
||||
|
||||
"""
|
||||
# Check so a database exists and is accessible
|
||||
from django.db import connection
|
||||
|
|
@ -1449,7 +1452,9 @@ def check_database():
|
|||
|
||||
try:
|
||||
AccountDB.objects.get(id=1)
|
||||
except django.db.utils.OperationalError as e:
|
||||
except (django.db.utils.OperationalError, ProgrammingError) as e:
|
||||
if always_return:
|
||||
return False
|
||||
print(ERROR_DATABASE.format(traceback=e))
|
||||
sys.exit()
|
||||
except AccountDB.DoesNotExist:
|
||||
|
|
@ -1484,7 +1489,7 @@ def check_database():
|
|||
new.save()
|
||||
else:
|
||||
create_superuser()
|
||||
check_database()
|
||||
check_database(always_return=always_return)
|
||||
return True
|
||||
|
||||
|
||||
|
|
@ -1634,7 +1639,7 @@ def error_check_python_modules(show_warnings=False):
|
|||
python source files themselves). Best they fail already here
|
||||
before we get any further.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
show_warnings (bool): If non-fatal warning messages should be shown.
|
||||
|
||||
"""
|
||||
|
|
@ -2246,14 +2251,15 @@ def main():
|
|||
# pass-through to django manager, but set things up first
|
||||
check_db = False
|
||||
need_gamedir = True
|
||||
# some commands don't require the presence of a game directory to work
|
||||
if option in ("makemessages", "compilemessages"):
|
||||
need_gamedir = False
|
||||
|
||||
# handle special django commands
|
||||
if option in ("runserver", "testserver"):
|
||||
# we don't want the django test-webserver
|
||||
print(WARNING_RUNSERVER)
|
||||
if option in ("shell", "check"):
|
||||
if option in ("makemessages", "compilemessages"):
|
||||
# some commands don't require the presence of a game directory to work
|
||||
need_gamedir = False
|
||||
if option in ("shell", "check", "makemigrations", "createsuperuser"):
|
||||
# some django commands requires the database to exist,
|
||||
# or evennia._init to have run before they work right.
|
||||
check_db = True
|
||||
|
|
@ -2263,16 +2269,17 @@ def main():
|
|||
|
||||
init_game_directory(CURRENT_DIR, check_db=check_db, need_gamedir=need_gamedir)
|
||||
|
||||
if option in ("migrate", "makemigrations"):
|
||||
# we have to launch migrate within the program to make sure migrations
|
||||
# run within the scope of the launcher (otherwise missing a db will cause errors)
|
||||
django.core.management.call_command(*([option] + unknown_args))
|
||||
else:
|
||||
# pass on to the core django manager - re-parse the entire input line
|
||||
# but keep 'evennia' as the name instead of django-admin. This is
|
||||
# an exit condition.
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(execute_from_command_line())
|
||||
if option == "migrate":
|
||||
# we need to bypass some checks here for the first db creation
|
||||
if not check_database(always_return=True):
|
||||
django.core.management.call_command(*([option] + unknown_args))
|
||||
sys.exit(0)
|
||||
|
||||
# pass on to the core django manager - re-parse the entire input line
|
||||
# but keep 'evennia' as the name instead of django-admin. This is
|
||||
# an exit condition.
|
||||
sys.argv[0] = re.sub(r"(-script\.pyw?|\.exe)?$", "", sys.argv[0])
|
||||
sys.exit(execute_from_command_line(sys.argv))
|
||||
|
||||
elif not args.tail_log:
|
||||
# no input; print evennia info (don't pring if we're tailing log)
|
||||
|
|
|
|||
|
|
@ -1,403 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
"""
|
||||
|
||||
This runner is controlled by the evennia launcher and should normally
|
||||
not be launched directly. It manages the two main Evennia processes
|
||||
(Server and Portal) and most importantly runs a passive, threaded loop
|
||||
that makes sure to restart Server whenever it shuts down.
|
||||
|
||||
Since twistd does not allow for returning an optional exit code we
|
||||
need to handle the current reload state for server and portal with
|
||||
flag-files instead. The files, one each for server and portal either
|
||||
contains True or False indicating if the process should be restarted
|
||||
upon returning, or not. A process returning != 0 will always stop, no
|
||||
matter the value of this file.
|
||||
|
||||
"""
|
||||
|
||||
import os
|
||||
import sys
|
||||
from argparse import ArgumentParser
|
||||
from subprocess import Popen
|
||||
import queue
|
||||
import _thread
|
||||
import evennia
|
||||
|
||||
try:
|
||||
# check if launched with pypy
|
||||
import __pypy__ as is_pypy
|
||||
except ImportError:
|
||||
is_pypy = False
|
||||
|
||||
SERVER = None
|
||||
PORTAL = None
|
||||
|
||||
EVENNIA_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||
EVENNIA_BIN = os.path.join(EVENNIA_ROOT, "bin")
|
||||
EVENNIA_LIB = os.path.dirname(evennia.__file__)
|
||||
|
||||
SERVER_PY_FILE = os.path.join(EVENNIA_LIB, "server", "server.py")
|
||||
PORTAL_PY_FILE = os.path.join(EVENNIA_LIB, "server", "portal", "portal.py")
|
||||
|
||||
GAMEDIR = None
|
||||
SERVERDIR = "server"
|
||||
SERVER_PIDFILE = None
|
||||
PORTAL_PIDFILE = None
|
||||
SERVER_RESTART = None
|
||||
PORTAL_RESTART = None
|
||||
SERVER_LOGFILE = None
|
||||
PORTAL_LOGFILE = None
|
||||
HTTP_LOGFILE = None
|
||||
PPROFILER_LOGFILE = None
|
||||
SPROFILER_LOGFILE = None
|
||||
|
||||
# messages
|
||||
|
||||
CMDLINE_HELP = """
|
||||
This program manages the running Evennia processes. It is called
|
||||
by evennia and should not be started manually. Its main task is to
|
||||
sit and watch the Server and restart it whenever the user reloads.
|
||||
The runner depends on four files for its operation, two PID files
|
||||
and two RESTART files for Server and Portal respectively; these
|
||||
are stored in the game's server/ directory.
|
||||
"""
|
||||
|
||||
PROCESS_ERROR = """
|
||||
{component} process error: {traceback}.
|
||||
"""
|
||||
|
||||
PROCESS_IOERROR = """
|
||||
{component} IOError: {traceback}
|
||||
One possible explanation is that 'twistd' was not found.
|
||||
"""
|
||||
|
||||
PROCESS_RESTART = "{component} restarting ..."
|
||||
|
||||
PROCESS_DOEXIT = "Deferring to external runner."
|
||||
|
||||
# Functions
|
||||
|
||||
|
||||
def set_restart_mode(restart_file, flag="reload"):
|
||||
"""
|
||||
This sets a flag file for the restart mode.
|
||||
"""
|
||||
with open(restart_file, "w") as f:
|
||||
f.write(str(flag))
|
||||
|
||||
|
||||
def getenv():
|
||||
"""
|
||||
Get current environment and add PYTHONPATH
|
||||
"""
|
||||
sep = ";" if os.name == "nt" else ":"
|
||||
env = os.environ.copy()
|
||||
sys.path.insert(0, GAMEDIR)
|
||||
env["PYTHONPATH"] = sep.join(sys.path)
|
||||
return env
|
||||
|
||||
|
||||
def get_restart_mode(restart_file):
|
||||
"""
|
||||
Parse the server/portal restart status
|
||||
"""
|
||||
if os.path.exists(restart_file):
|
||||
with open(restart_file, "r") as f:
|
||||
return f.read()
|
||||
return "shutdown"
|
||||
|
||||
|
||||
def get_pid(pidfile):
|
||||
"""
|
||||
Get the PID (Process ID) by trying to access
|
||||
an PID file.
|
||||
"""
|
||||
pid = None
|
||||
if os.path.exists(pidfile):
|
||||
with open(pidfile, "r") as f:
|
||||
pid = f.read()
|
||||
return pid
|
||||
|
||||
|
||||
def cycle_logfile(logfile):
|
||||
"""
|
||||
Rotate the old log files to <filename>.old
|
||||
"""
|
||||
logfile_old = logfile + ".old"
|
||||
if os.path.exists(logfile):
|
||||
# Cycle the old logfiles to *.old
|
||||
if os.path.exists(logfile_old):
|
||||
# E.g. Windows don't support rename-replace
|
||||
os.remove(logfile_old)
|
||||
os.rename(logfile, logfile_old)
|
||||
|
||||
|
||||
# Start program management
|
||||
|
||||
|
||||
def start_services(server_argv, portal_argv, doexit=False):
|
||||
"""
|
||||
This calls a threaded loop that launches the Portal and Server
|
||||
and then restarts them when they finish.
|
||||
"""
|
||||
global SERVER, PORTAL
|
||||
processes = queue.Queue()
|
||||
|
||||
def server_waiter(queue):
|
||||
try:
|
||||
rc = Popen(server_argv, env=getenv()).wait()
|
||||
except Exception as e:
|
||||
print(PROCESS_ERROR.format(component="Server", traceback=e))
|
||||
return
|
||||
# this signals the controller that the program finished
|
||||
queue.put(("server_stopped", rc))
|
||||
|
||||
def portal_waiter(queue):
|
||||
try:
|
||||
rc = Popen(portal_argv, env=getenv()).wait()
|
||||
except Exception as e:
|
||||
print(PROCESS_ERROR.format(component="Portal", traceback=e))
|
||||
return
|
||||
# this signals the controller that the program finished
|
||||
queue.put(("portal_stopped", rc))
|
||||
|
||||
if portal_argv:
|
||||
try:
|
||||
if not doexit and get_restart_mode(PORTAL_RESTART) == "True":
|
||||
# start portal as interactive, reloadable thread
|
||||
PORTAL = _thread.start_new_thread(portal_waiter, (processes,))
|
||||
else:
|
||||
# normal operation: start portal as a daemon;
|
||||
# we don't care to monitor it for restart
|
||||
PORTAL = Popen(portal_argv, env=getenv())
|
||||
except IOError as e:
|
||||
print(PROCESS_IOERROR.format(component="Portal", traceback=e))
|
||||
return
|
||||
|
||||
try:
|
||||
if server_argv:
|
||||
if doexit:
|
||||
SERVER = Popen(server_argv, env=getenv())
|
||||
else:
|
||||
# start server as a reloadable thread
|
||||
SERVER = _thread.start_new_thread(server_waiter, (processes,))
|
||||
except IOError as e:
|
||||
print(PROCESS_IOERROR.format(component="Server", traceback=e))
|
||||
return
|
||||
|
||||
if doexit:
|
||||
# Exit immediately
|
||||
return
|
||||
|
||||
# Reload loop
|
||||
while True:
|
||||
|
||||
# this blocks until something is actually returned.
|
||||
from twisted.internet.error import ReactorNotRunning
|
||||
|
||||
try:
|
||||
try:
|
||||
message, rc = processes.get()
|
||||
except KeyboardInterrupt:
|
||||
# this only matters in interactive mode
|
||||
break
|
||||
|
||||
# restart only if process stopped cleanly
|
||||
if (
|
||||
message == "server_stopped"
|
||||
and int(rc) == 0
|
||||
and get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")
|
||||
):
|
||||
print(PROCESS_RESTART.format(component="Server"))
|
||||
SERVER = _thread.start_new_thread(server_waiter, (processes,))
|
||||
continue
|
||||
|
||||
# normally the portal is not reloaded since it's run as a daemon.
|
||||
if (
|
||||
message == "portal_stopped"
|
||||
and int(rc) == 0
|
||||
and get_restart_mode(PORTAL_RESTART) == "True"
|
||||
):
|
||||
print(PROCESS_RESTART.format(component="Portal"))
|
||||
PORTAL = _thread.start_new_thread(portal_waiter, (processes,))
|
||||
continue
|
||||
break
|
||||
except ReactorNotRunning:
|
||||
break
|
||||
|
||||
|
||||
def main():
|
||||
"""
|
||||
This handles the command line input of the runner, usually created by
|
||||
the evennia launcher
|
||||
"""
|
||||
|
||||
parser = ArgumentParser(description=CMDLINE_HELP)
|
||||
parser.add_argument(
|
||||
"--noserver",
|
||||
action="store_true",
|
||||
dest="noserver",
|
||||
default=False,
|
||||
help="Do not start Server process",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--noportal",
|
||||
action="store_true",
|
||||
dest="noportal",
|
||||
default=False,
|
||||
help="Do not start Portal process",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--logserver",
|
||||
action="store_true",
|
||||
dest="logserver",
|
||||
default=False,
|
||||
help="Log Server output to logfile",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--iserver",
|
||||
action="store_true",
|
||||
dest="iserver",
|
||||
default=False,
|
||||
help="Server in interactive mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--iportal",
|
||||
action="store_true",
|
||||
dest="iportal",
|
||||
default=False,
|
||||
help="Portal in interactive mode",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pserver", action="store_true", dest="pserver", default=False, help="Profile Server"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pportal", action="store_true", dest="pportal", default=False, help="Profile Portal"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--nologcycle",
|
||||
action="store_false",
|
||||
dest="nologcycle",
|
||||
default=True,
|
||||
help="Do not cycle log files",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--doexit",
|
||||
action="store_true",
|
||||
dest="doexit",
|
||||
default=False,
|
||||
help="Immediately exit after processes have started.",
|
||||
)
|
||||
parser.add_argument("gamedir", help="path to game dir")
|
||||
parser.add_argument("twistdbinary", help="path to twistd binary")
|
||||
parser.add_argument("slogfile", help="path to server log file")
|
||||
parser.add_argument("plogfile", help="path to portal log file")
|
||||
parser.add_argument("hlogfile", help="path to http log file")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
global GAMEDIR
|
||||
global SERVER_LOGFILE, PORTAL_LOGFILE, HTTP_LOGFILE
|
||||
global SERVER_PIDFILE, PORTAL_PIDFILE
|
||||
global SERVER_RESTART, PORTAL_RESTART
|
||||
global SPROFILER_LOGFILE, PPROFILER_LOGFILE
|
||||
|
||||
GAMEDIR = args.gamedir
|
||||
sys.path.insert(1, os.path.join(GAMEDIR, SERVERDIR))
|
||||
|
||||
SERVER_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "server.pid")
|
||||
PORTAL_PIDFILE = os.path.join(GAMEDIR, SERVERDIR, "portal.pid")
|
||||
SERVER_RESTART = os.path.join(GAMEDIR, SERVERDIR, "server.restart")
|
||||
PORTAL_RESTART = os.path.join(GAMEDIR, SERVERDIR, "portal.restart")
|
||||
SERVER_LOGFILE = args.slogfile
|
||||
PORTAL_LOGFILE = args.plogfile
|
||||
HTTP_LOGFILE = args.hlogfile
|
||||
TWISTED_BINARY = args.twistdbinary
|
||||
SPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "server.prof")
|
||||
PPROFILER_LOGFILE = os.path.join(GAMEDIR, SERVERDIR, "logs", "portal.prof")
|
||||
|
||||
# set up default project calls
|
||||
server_argv = [
|
||||
TWISTED_BINARY,
|
||||
"--nodaemon",
|
||||
"--logfile=%s" % SERVER_LOGFILE,
|
||||
"--pidfile=%s" % SERVER_PIDFILE,
|
||||
"--python=%s" % SERVER_PY_FILE,
|
||||
]
|
||||
portal_argv = [
|
||||
TWISTED_BINARY,
|
||||
"--logfile=%s" % PORTAL_LOGFILE,
|
||||
"--pidfile=%s" % PORTAL_PIDFILE,
|
||||
"--python=%s" % PORTAL_PY_FILE,
|
||||
]
|
||||
|
||||
# Profiling settings (read file from python shell e.g with
|
||||
# p = pstats.Stats('server.prof')
|
||||
pserver_argv = ["--savestats", "--profiler=cprofile", "--profile=%s" % SPROFILER_LOGFILE]
|
||||
pportal_argv = ["--savestats", "--profiler=cprofile", "--profile=%s" % PPROFILER_LOGFILE]
|
||||
|
||||
# Server
|
||||
|
||||
pid = get_pid(SERVER_PIDFILE)
|
||||
if pid and not args.noserver:
|
||||
print(
|
||||
"\nEvennia Server is already running as process %(pid)s. Not restarted." % {"pid": pid}
|
||||
)
|
||||
args.noserver = True
|
||||
if args.noserver:
|
||||
server_argv = None
|
||||
else:
|
||||
set_restart_mode(SERVER_RESTART, "shutdown")
|
||||
if not args.logserver:
|
||||
# don't log to server logfile
|
||||
del server_argv[2]
|
||||
print("\nStarting Evennia Server (output to stdout).")
|
||||
else:
|
||||
if not args.nologcycle:
|
||||
cycle_logfile(SERVER_LOGFILE)
|
||||
print("\nStarting Evennia Server (output to server logfile).")
|
||||
if args.pserver:
|
||||
server_argv.extend(pserver_argv)
|
||||
print("\nRunning Evennia Server under cProfile.")
|
||||
|
||||
# Portal
|
||||
|
||||
pid = get_pid(PORTAL_PIDFILE)
|
||||
if pid and not args.noportal:
|
||||
print(
|
||||
"\nEvennia Portal is already running as process %(pid)s. Not restarted." % {"pid": pid}
|
||||
)
|
||||
args.noportal = True
|
||||
if args.noportal:
|
||||
portal_argv = None
|
||||
else:
|
||||
if args.iportal:
|
||||
# make portal interactive
|
||||
portal_argv[1] = "--nodaemon"
|
||||
set_restart_mode(PORTAL_RESTART, True)
|
||||
print("\nStarting Evennia Portal in non-Daemon mode (output to stdout).")
|
||||
else:
|
||||
if not args.nologcycle:
|
||||
cycle_logfile(PORTAL_LOGFILE)
|
||||
cycle_logfile(HTTP_LOGFILE)
|
||||
set_restart_mode(PORTAL_RESTART, False)
|
||||
print("\nStarting Evennia Portal in Daemon mode (output to portal logfile).")
|
||||
if args.pportal:
|
||||
portal_argv.extend(pportal_argv)
|
||||
print("\nRunning Evennia Portal under cProfile.")
|
||||
if args.doexit:
|
||||
print(PROCESS_DOEXIT)
|
||||
|
||||
# Windows fixes (Windows don't support pidfiles natively)
|
||||
if os.name == "nt":
|
||||
if server_argv:
|
||||
del server_argv[-2]
|
||||
if portal_argv:
|
||||
del portal_argv[-2]
|
||||
|
||||
# Start processes
|
||||
start_services(server_argv, portal_argv, doexit=args.doexit)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
|
@ -1,8 +1,8 @@
|
|||
# Evennia Game Index Client
|
||||
|
||||
Greg Taylor 2016
|
||||
Greg Taylor 2016, Griatch 2020
|
||||
|
||||
This contrib features a client for the [Evennia Game Index]
|
||||
This is a client for the [Evennia Game Index]
|
||||
(http://evennia-game-index.appspot.com/), a listing of games built on
|
||||
Evennia. By listing your game on the index, you make it easy for other
|
||||
people in the community to discover your creation.
|
||||
|
|
@ -14,74 +14,24 @@ on remedying this.*
|
|||
|
||||
## Listing your Game
|
||||
|
||||
To list your game, you'll need to enable the Evennia Game Index client.
|
||||
Start by `cd`'ing to your game directory. From there, open up
|
||||
`server/conf/server_services_plugins.py`. It might look something like this
|
||||
if you don't have any other optional add-ons enabled:
|
||||
To list your game, go to your game dir and run
|
||||
|
||||
```python
|
||||
"""
|
||||
Server plugin services
|
||||
evennia connections
|
||||
|
||||
This plugin module can define user-created services for the Server to
|
||||
start.
|
||||
Follow the prompts to add details to the listing. Use `evennia reload`. In your log (visible with `evennia --log`
|
||||
you should see a note that info has been sent to the game index.
|
||||
|
||||
This module must handle all imports and setups required to start a
|
||||
twisted service (see examples in evennia.server.server). It must also
|
||||
contain a function start_plugin_services(application). Evennia will
|
||||
call this function with the main Server application (so your services
|
||||
can be added to it). The function should not return anything. Plugin
|
||||
services are started last in the Server startup process.
|
||||
"""
|
||||
## Detailed settings
|
||||
|
||||
|
||||
def start_plugin_services(server):
|
||||
"""
|
||||
This hook is called by Evennia, last in the Server startup process.
|
||||
|
||||
server - a reference to the main server application.
|
||||
"""
|
||||
pass
|
||||
```
|
||||
|
||||
To enable the client, import `EvenniaGameIndexService` and fire it up after the
|
||||
Evennia server has finished starting:
|
||||
|
||||
```python
|
||||
"""
|
||||
Server plugin services
|
||||
|
||||
This plugin module can define user-created services for the Server to
|
||||
start.
|
||||
|
||||
This module must handle all imports and setups required to start a
|
||||
twisted service (see examples in evennia.server.server). It must also
|
||||
contain a function start_plugin_services(application). Evennia will
|
||||
call this function with the main Server application (so your services
|
||||
can be added to it). The function should not return anything. Plugin
|
||||
services are started last in the Server startup process.
|
||||
"""
|
||||
|
||||
from evennia.contrib.egi_client import EvenniaGameIndexService
|
||||
|
||||
def start_plugin_services(server):
|
||||
"""
|
||||
This hook is called by Evennia, last in the Server startup process.
|
||||
|
||||
server - a reference to the main server application.
|
||||
"""
|
||||
egi_service = EvenniaGameIndexService()
|
||||
server.services.addService(egi_service)
|
||||
```
|
||||
|
||||
Next, configure your game listing by opening up `server/conf/settings.py` and
|
||||
If you don't want to use the wizard you can configure your game listing by opening up `server/conf/settings.py` and
|
||||
using the following as a starting point:
|
||||
|
||||
```python
|
||||
######################################################################
|
||||
# Contrib config
|
||||
# Game index
|
||||
######################################################################
|
||||
|
||||
GAME_INDEX_ENABLED = True
|
||||
GAME_INDEX_LISTING = {
|
||||
'game_status': 'pre-alpha',
|
||||
# Optional, comment out or remove if N/A
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ Everything starts at handle_setup()
|
|||
|
||||
import time
|
||||
from django.conf import settings
|
||||
from django.utils.translation import ugettext as _
|
||||
from django.utils.translation import gettext as _
|
||||
from evennia.accounts.models import AccountDB
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.utils import create, logger
|
||||
|
|
@ -29,7 +29,7 @@ LIMBO_DESC = _(
|
|||
"""
|
||||
Welcome to your new |wEvennia|n-based game! Visit http://www.evennia.com if you need
|
||||
help, want to contribute, report issues or just join the community.
|
||||
As Account #1 you can create a demo/tutorial area with |w@batchcommand tutorial_world.build|n.
|
||||
As Account #1 you can create a demo/tutorial area with '|wbatchcommand tutorial_world.build|n'.
|
||||
"""
|
||||
)
|
||||
|
||||
|
|
@ -95,7 +95,8 @@ def create_objects():
|
|||
god_character.locks.add(
|
||||
"examine:perm(Developer);edit:false();delete:false();boot:false();msg:all();puppet:false()"
|
||||
)
|
||||
god_character.permissions.add("Developer")
|
||||
# we set this low so that quelling is more useful
|
||||
god_character.permissions.add("Player")
|
||||
|
||||
god_account.attributes.add("_first_login", True)
|
||||
god_account.attributes.add("_last_puppet", god_character)
|
||||
|
|
|
|||
|
|
@ -164,7 +164,7 @@ def client_options(session, *args, **kwargs):
|
|||
This allows the client an OOB way to inform us about its name and capabilities.
|
||||
This will be integrated into the session settings
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
get (bool): If this is true, return the settings as a dict
|
||||
(ignore all other kwargs).
|
||||
client (str): A client identifier, like "mushclient".
|
||||
|
|
@ -282,7 +282,7 @@ def login(session, *args, **kwargs):
|
|||
Peform a login. This only works if session is currently not logged
|
||||
in. This will also automatically throttle too quick attempts.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
name (str): Account name
|
||||
password (str): Plain-text password
|
||||
|
||||
|
|
@ -308,7 +308,7 @@ def get_value(session, *args, **kwargs):
|
|||
Return the value of a given attribute or db_property on the
|
||||
session's current account or character.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
name (str): Name of info value to return. Only names
|
||||
in the _gettable dictionary earlier in this module
|
||||
are accepted.
|
||||
|
|
@ -325,7 +325,7 @@ def _testrepeat(**kwargs):
|
|||
This is a test function for using with the repeat
|
||||
inputfunc.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
session (Session): Session to return to.
|
||||
"""
|
||||
import time
|
||||
|
|
@ -342,7 +342,7 @@ def repeat(session, *args, **kwargs):
|
|||
this is meant as an example of limiting the number of
|
||||
possible call functions.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
callback (str): The function to call. Only functions
|
||||
from the _repeatable dictionary earlier in this
|
||||
module are available.
|
||||
|
|
@ -403,7 +403,7 @@ def monitor(session, *args, **kwargs):
|
|||
"""
|
||||
Adds monitoring to a given property or Attribute.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
name (str): The name of the property or Attribute
|
||||
to report. No db_* prefix is needed. Only names
|
||||
in the _monitorable dict earlier in this module
|
||||
|
|
@ -485,8 +485,9 @@ def webclient_options(session, *args, **kwargs):
|
|||
If kwargs is not empty, the key/values stored in there will be persisted
|
||||
to the account object.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
<option name>: an option to save
|
||||
|
||||
"""
|
||||
account = session.account
|
||||
|
||||
|
|
@ -521,9 +522,8 @@ def webclient_options(session, *args, **kwargs):
|
|||
session=session,
|
||||
)
|
||||
else:
|
||||
# kwargs provided: persist them to the account object
|
||||
for key, value in kwargs.items():
|
||||
clientoptions[key] = value
|
||||
# kwargs provided: persist them to the account object.
|
||||
clientoptions.update(kwargs)
|
||||
|
||||
|
||||
# OOB protocol-specific aliases and wrappers
|
||||
|
|
@ -576,8 +576,7 @@ def msdp_list(session, *args, **kwargs):
|
|||
fieldnames = [tup[1] for tup in monitor_infos]
|
||||
session.msg(reported_variables=(fieldnames, {}))
|
||||
if "sendable_variables" in args_lower:
|
||||
# no default sendable variables
|
||||
session.msg(sendable_variables=([], {}))
|
||||
session.msg(sendable_variables=(_monitorable, {}))
|
||||
|
||||
|
||||
def msdp_report(session, *args, **kwargs):
|
||||
|
|
@ -597,6 +596,17 @@ def msdp_unreport(session, *args, **kwargs):
|
|||
unmonitor(session, *args, **kwargs)
|
||||
|
||||
|
||||
def msdp_send(session, *args, **kwargs):
|
||||
"""
|
||||
MSDP SEND command
|
||||
"""
|
||||
out = {}
|
||||
for varname in args:
|
||||
if varname.lower() in _monitorable:
|
||||
out[varname] = _monitorable[varname.lower()]
|
||||
session.msg(send=((), out))
|
||||
|
||||
|
||||
# client specific
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -46,6 +46,14 @@ NULNUL = b"\x00\x00"
|
|||
|
||||
AMP_MAXLEN = amp.MAX_VALUE_LENGTH # max allowed data length in AMP protocol (cannot be changed)
|
||||
|
||||
# amp internal
|
||||
ASK = b'_ask'
|
||||
ANSWER = b'_answer'
|
||||
ERROR = b'_error'
|
||||
ERROR_CODE = b'_error_code'
|
||||
ERROR_DESCRIPTION = b'_error_description'
|
||||
UNKNOWN_ERROR_CODE = b'UNKNOWN'
|
||||
|
||||
# buffers
|
||||
_SENDBATCH = defaultdict(list)
|
||||
_MSGBUFFER = defaultdict(list)
|
||||
|
|
@ -301,6 +309,47 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
# later twisted amp has its own __init__
|
||||
super(AMPMultiConnectionProtocol, self).__init__(*args, **kwargs)
|
||||
|
||||
def _commandReceived(self, box):
|
||||
"""
|
||||
This overrides the default Twisted AMP error handling which is not
|
||||
passing enough of the traceback through to the other side. Instead we
|
||||
add a specific log of the problem on the erroring side.
|
||||
|
||||
"""
|
||||
def formatAnswer(answerBox):
|
||||
answerBox[ANSWER] = box[ASK]
|
||||
return answerBox
|
||||
|
||||
def formatError(error):
|
||||
if error.check(amp.RemoteAmpError):
|
||||
code = error.value.errorCode
|
||||
desc = error.value.description
|
||||
|
||||
# Evennia extra logging
|
||||
desc += " (error logged on other side)"
|
||||
_get_logger().log_err(f"AMP caught exception ({desc}):\n{error.value}")
|
||||
|
||||
if isinstance(desc, str):
|
||||
desc = desc.encode("utf-8", "replace")
|
||||
if error.value.fatal:
|
||||
errorBox = amp.QuitBox()
|
||||
else:
|
||||
errorBox = amp.AmpBox()
|
||||
else:
|
||||
errorBox = amp.QuitBox()
|
||||
_get_logger().log_err(error) # server-side logging if unhandled error
|
||||
code = UNKNOWN_ERROR_CODE
|
||||
desc = b"Unknown Error"
|
||||
errorBox[ERROR] = box[ASK]
|
||||
errorBox[ERROR_DESCRIPTION] = desc
|
||||
errorBox[ERROR_CODE] = code
|
||||
return errorBox
|
||||
deferred = self.dispatchCommand(box)
|
||||
if ASK in box:
|
||||
deferred.addCallbacks(formatAnswer, formatError)
|
||||
deferred.addCallback(self._safeEmit)
|
||||
deferred.addErrback(self.unhandledError)
|
||||
|
||||
def dataReceived(self, data):
|
||||
"""
|
||||
Handle non-AMP messages, such as HTTP communication.
|
||||
|
|
@ -314,7 +363,9 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
try:
|
||||
super(AMPMultiConnectionProtocol, self).dataReceived(data)
|
||||
except KeyError:
|
||||
_get_logger().log_trace("Discarded incoming partial data: {}".format(to_str(data)))
|
||||
_get_logger().log_trace(
|
||||
"Discarded incoming partial (packed) data (len {})".format(len(data))
|
||||
)
|
||||
elif self.multibatches:
|
||||
# invalid AMP, but we have a pending multi-batch that is not yet complete
|
||||
if data[-2:] == NULNUL:
|
||||
|
|
@ -323,7 +374,9 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
try:
|
||||
super(AMPMultiConnectionProtocol, self).dataReceived(data)
|
||||
except KeyError:
|
||||
_get_logger().log_trace("Discarded incoming multi-batch data:".format(to_str(data)))
|
||||
_get_logger().log_trace(
|
||||
"Discarded incoming multi-batch (packed) data (len {})".format(len(data))
|
||||
)
|
||||
else:
|
||||
# not an AMP communication, return warning
|
||||
self.transport.write(_HTTP_WARNING)
|
||||
|
|
@ -381,7 +434,7 @@ class AMPMultiConnectionProtocol(amp.AMP):
|
|||
"""
|
||||
e.trap(Exception)
|
||||
_get_logger().log_err(
|
||||
"AMP Error for {info}: {trcbck} {err}".format(
|
||||
"AMP Error from {info}: {trcbck} {err}".format(
|
||||
info=info, trcbck=e.getTraceback(), err=e.getErrorMessage()
|
||||
)
|
||||
)
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class AMPServerProtocol(amp.AMPMultiConnectionProtocol):
|
|||
|
||||
"""
|
||||
# start the Server
|
||||
print("Portal starting server ... {}".format(server_twistd_cmd))
|
||||
print("Portal starting server ... ")
|
||||
process = None
|
||||
with open(settings.SERVER_LOG_FILE, "a") as logfile:
|
||||
# we link stdout to a file in order to catch
|
||||
|
|
|
|||
|
|
@ -303,7 +303,7 @@ class GrapevineClient(WebSocketClientProtocol, Session):
|
|||
"""
|
||||
Send data grapevine -> Evennia
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
data (dict): Converted json data.
|
||||
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -275,7 +275,7 @@ class IRCBot(irc.IRCClient, Session):
|
|||
"""
|
||||
Data IRC -> Server.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
text (str): Ingoing text.
|
||||
kwargs (any): Other data from protocol.
|
||||
|
||||
|
|
@ -306,7 +306,7 @@ class IRCBot(irc.IRCClient, Session):
|
|||
Args:
|
||||
text (str): Outgoing text.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
user (str): the nick to send
|
||||
privately to.
|
||||
|
||||
|
|
@ -375,7 +375,7 @@ class IRCBotFactory(protocol.ReconnectingClientFactory):
|
|||
Args:
|
||||
sessionhandler (SessionHandler): Reference to the main Sessionhandler.
|
||||
|
||||
Kwargs:
|
||||
Keyword Args:
|
||||
uid (int): Bot user id.
|
||||
botname (str): Bot name (seen in IRC channel).
|
||||
channel (str): IRC channel to connect to.
|
||||
|
|
|
|||
|
|
@ -15,9 +15,10 @@ This protocol is implemented by the telnet protocol importing
|
|||
mccp_compress and calling it from its write methods.
|
||||
"""
|
||||
import zlib
|
||||
from twisted.python.compat import _bytesChr as chr
|
||||
|
||||
# negotiations for v1 and v2 of the protocol
|
||||
MCCP = b"\x56"
|
||||
MCCP = chr(86) # b"\x56"
|
||||
FLUSH = zlib.Z_SYNC_FLUSH
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -12,10 +12,11 @@ active players and so on.
|
|||
"""
|
||||
from django.conf import settings
|
||||
from evennia.utils import utils
|
||||
from twisted.python.compat import _bytesChr as bchr
|
||||
|
||||
MSSP = b"\x46"
|
||||
MSSP_VAR = b"\x01"
|
||||
MSSP_VAL = b"\x02"
|
||||
MSSP = bchr(70) # b"\x46"
|
||||
MSSP_VAR = bchr(1) # b"\x01"
|
||||
MSSP_VAL = bchr(2) # b"\x02"
|
||||
|
||||
# try to get the customized mssp info, if it exists.
|
||||
MSSPTable_CUSTOM = utils.variable_from_module(settings.MSSP_META_MODULE, "MSSPTable", default={})
|
||||
|
|
@ -86,7 +87,7 @@ class Mssp(object):
|
|||
"PLAYERS": self.get_player_count,
|
||||
"UPTIME": self.get_uptime,
|
||||
"PORT": list(
|
||||
reversed(settings.TELNET_PORTS)
|
||||
str(port) for port in reversed(settings.TELNET_PORTS)
|
||||
), # most important port should be last in list
|
||||
# Evennia auto-filled
|
||||
"CRAWL DELAY": "-1",
|
||||
|
|
@ -119,10 +120,15 @@ class Mssp(object):
|
|||
if utils.is_iter(value):
|
||||
for partval in value:
|
||||
varlist += (
|
||||
MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(partval, "utf-8")
|
||||
MSSP_VAR
|
||||
+ bytes(str(variable), "utf-8")
|
||||
+ MSSP_VAL
|
||||
+ bytes(str(partval), "utf-8")
|
||||
)
|
||||
else:
|
||||
varlist += MSSP_VAR + bytes(variable, "utf-8") + MSSP_VAL + bytes(value, "utf-8")
|
||||
varlist += (
|
||||
MSSP_VAR + bytes(str(variable), "utf-8") + MSSP_VAL + bytes(str(value), "utf-8")
|
||||
)
|
||||
|
||||
# send to crawler by subnegotiation
|
||||
self.protocol.requestNegotiation(MSSP, varlist)
|
||||
|
|
|
|||
|
|
@ -14,11 +14,12 @@ http://www.gammon.com.au/mushclient/addingservermxp.htm
|
|||
|
||||
"""
|
||||
import re
|
||||
from twisted.python.compat import _bytesChr as bchr
|
||||
|
||||
LINKS_SUB = re.compile(r"\|lc(.*?)\|lt(.*?)\|le", re.DOTALL)
|
||||
|
||||
# MXP Telnet option
|
||||
MXP = b"\x5b"
|
||||
MXP = bchr(91) # b"\x5b"
|
||||
|
||||
MXP_TEMPSECURE = "\x1B[4z"
|
||||
MXP_SEND = MXP_TEMPSECURE + '<SEND HREF="\\1">' + "\\2" + MXP_TEMPSECURE + "</SEND>"
|
||||
|
|
|
|||
|
|
@ -11,9 +11,10 @@ client and update it when the size changes
|
|||
"""
|
||||
from codecs import encode as codecs_encode
|
||||
from django.conf import settings
|
||||
from twisted.python.compat import _bytesChr as bchr
|
||||
|
||||
NAWS = b"\x1f"
|
||||
IS = b"\x00"
|
||||
NAWS = bchr(31) # b"\x1f"
|
||||
IS = bchr(0) # b"\x00"
|
||||
# default taken from telnet specification
|
||||
DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH
|
||||
DEFAULT_HEIGHT = settings.CLIENT_DEFAULT_HEIGHT
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ import time
|
|||
|
||||
from os.path import dirname, abspath
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet import protocol, reactor
|
||||
from twisted.python.log import ILogObserver
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ import django
|
|||
|
||||
django.setup()
|
||||
from django.conf import settings
|
||||
from django.db import connection
|
||||
|
||||
import evennia
|
||||
|
||||
|
|
@ -95,6 +97,35 @@ INFO_DICT = {
|
|||
"webserver_internal": [],
|
||||
}
|
||||
|
||||
try:
|
||||
WEB_PLUGINS_MODULE = mod_import(settings.WEB_PLUGINS_MODULE)
|
||||
except ImportError:
|
||||
WEB_PLUGINS_MODULE = None
|
||||
INFO_DICT["errors"] = (
|
||||
"WARNING: settings.WEB_PLUGINS_MODULE not found - "
|
||||
"copy 'evennia/game_template/server/conf/web_plugins.py to "
|
||||
"mygame/server/conf."
|
||||
)
|
||||
|
||||
|
||||
_MAINTENANCE_COUNT = 0
|
||||
|
||||
|
||||
def _portal_maintenance():
|
||||
"""
|
||||
Repeated maintenance tasks for the portal.
|
||||
|
||||
"""
|
||||
global _MAINTENANCE_COUNT
|
||||
|
||||
_MAINTENANCE_COUNT += 1
|
||||
|
||||
if _MAINTENANCE_COUNT % (3600 * 7) == 0:
|
||||
# drop database connection every 7 hrs to avoid default timeouts on MySQL
|
||||
# (see https://github.com/evennia/evennia/issues/1376)
|
||||
connection.close()
|
||||
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# Portal Service object
|
||||
# -------------------------------------------------------------
|
||||
|
|
@ -133,6 +164,9 @@ class Portal(object):
|
|||
|
||||
self.start_time = time.time()
|
||||
|
||||
self.maintenance_task = LoopingCall(_portal_maintenance)
|
||||
self.maintenance_task.start(60, now=True) # call every minute
|
||||
|
||||
# in non-interactive portal mode, this gets overwritten by
|
||||
# cmdline sent by the evennia launcher
|
||||
self.server_twistd_cmd = self._get_backup_server_twistd_cmd()
|
||||
|
|
@ -190,7 +224,6 @@ class Portal(object):
|
|||
self.sessions.disconnect_all()
|
||||
if _stop_server:
|
||||
self.amp_protocol.stop_server(mode="shutdown")
|
||||
|
||||
if not _reactor_stopping:
|
||||
# shutting down the reactor will trigger another signal. We set
|
||||
# a flag to avoid loops.
|
||||
|
|
@ -213,7 +246,10 @@ application = service.Application("Portal")
|
|||
|
||||
if "--nodaemon" not in sys.argv:
|
||||
logfile = logger.WeeklyLogFile(
|
||||
os.path.basename(settings.PORTAL_LOG_FILE), os.path.dirname(settings.PORTAL_LOG_FILE)
|
||||
os.path.basename(settings.PORTAL_LOG_FILE),
|
||||
os.path.dirname(settings.PORTAL_LOG_FILE),
|
||||
day_rotation=settings.PORTAL_LOG_DAY_ROTATION,
|
||||
max_size=settings.PORTAL_LOG_MAX_SIZE,
|
||||
)
|
||||
application.setComponent(ILogObserver, logger.PortalLogObserver(logfile).emit)
|
||||
|
||||
|
|
@ -358,7 +394,7 @@ if WEBSERVER_ENABLED:
|
|||
w_interface = WEBSOCKET_CLIENT_INTERFACE
|
||||
w_ifacestr = ""
|
||||
if w_interface not in ("0.0.0.0", "::") or len(WEBSERVER_INTERFACES) > 1:
|
||||
w_ifacestr = "-%s" % interface
|
||||
w_ifacestr = "-%s" % w_interface
|
||||
port = WEBSOCKET_CLIENT_PORT
|
||||
|
||||
class Websocket(WebSocketServerFactory):
|
||||
|
|
@ -376,6 +412,14 @@ if WEBSERVER_ENABLED:
|
|||
webclientstr = "webclient-websocket%s: %s" % (w_ifacestr, port)
|
||||
INFO_DICT["webclient"].append(webclientstr)
|
||||
|
||||
if WEB_PLUGINS_MODULE:
|
||||
try:
|
||||
web_root = WEB_PLUGINS_MODULE.at_webproxy_root_creation(web_root)
|
||||
except Exception as e: # Legacy user has not added an at_webproxy_root_creation function in existing web plugins file
|
||||
INFO_DICT["errors"] = (
|
||||
"WARNING: WEB_PLUGINS_MODULE is enabled but at_webproxy_root_creation() not found - "
|
||||
"copy 'evennia/game_template/server/conf/web_plugins.py to mygame/server/conf."
|
||||
)
|
||||
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
web_root.is_portal = True
|
||||
proxy_service = internet.TCPServer(proxyport, web_root, interface=interface)
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue