Examine-cmd support for script/channels. Resolve #2375.

This commit is contained in:
Griatch 2022-01-09 12:16:32 +01:00
parent 97009037a5
commit 01af303457
5 changed files with 172 additions and 60 deletions

View file

@ -138,6 +138,7 @@ Up requirements to Django 3.2+, Twisted 21+
wrapper functions (consistent with `utils.search`). No change of api otherwise. wrapper functions (consistent with `utils.search`). No change of api otherwise.
- Add support for `$dbref()` and `$search` when assigning an Attribute value - Add support for `$dbref()` and `$search` when assigning an Attribute value
with the `set` command. This allows assigning real objects from in-game. with the `set` command. This allows assigning real objects from in-game.
- Add ability to examine `/script` and `/channel` entities with `examine` command.
### Evennia 0.9.5 (2019-2020) ### Evennia 0.9.5 (2019-2020)

View file

@ -21,6 +21,7 @@ from evennia.utils.utils import (
interactive, interactive,
list_to_string, list_to_string,
display_len, display_len,
format_grid,
) )
from evennia.utils.eveditor import EvEditor from evennia.utils.eveditor import EvEditor
from evennia.utils.evmore import EvMore from evennia.utils.evmore import EvMore
@ -2445,6 +2446,8 @@ class CmdExamine(ObjManipCommand):
Switch: Switch:
account - examine an Account (same as adding *) account - examine an Account (same as adding *)
object - examine an Object (useful when OOC) object - examine an Object (useful when OOC)
script - examine a Script
channel - examine a Channel
The examine command shows detailed game info about an The examine command shows detailed game info about an
object and optionally a specific attribute on it. object and optionally a specific attribute on it.
@ -2459,8 +2462,10 @@ class CmdExamine(ObjManipCommand):
locks = "cmd:perm(examine) or perm(Builder)" locks = "cmd:perm(examine) or perm(Builder)"
help_category = "Building" help_category = "Building"
arg_regex = r"(/\w+?(\s|$))|\s|$" arg_regex = r"(/\w+?(\s|$))|\s|$"
switch_options = ["account", "object", "script", "channel"]
object_type = "object"
account_mode = False
detail_color = "|c" detail_color = "|c"
header_color = "|w" header_color = "|w"
quell_color = "|r" quell_color = "|r"
@ -2485,7 +2490,7 @@ class CmdExamine(ObjManipCommand):
return ", ".join(utils.make_iter(str(obj.aliases))) return ", ".join(utils.make_iter(str(obj.aliases)))
def format_typeclass(self, obj): def format_typeclass(self, obj):
if hasattr(obj, "typeclass"): if hasattr(obj, "typeclass_path"):
return f"{obj.typename} ({obj.typeclass_path})" return f"{obj.typename} ({obj.typeclass_path})"
def format_sessions(self, obj): def format_sessions(self, obj):
@ -2584,7 +2589,7 @@ class CmdExamine(ObjManipCommand):
for cmdset in stored_cmdsets: for cmdset in stored_cmdsets:
if cmdset.key != "_EMPTY_CMDSET": if cmdset.key != "_EMPTY_CMDSET":
stored_cmdset_strings.append(self.format_single_cmdset(cmdset)) stored_cmdset_strings.append(self.format_single_cmdset(cmdset))
return "\n ".join(stored_cmdset_strings) return "\n " + "\n ".join(stored_cmdset_strings)
def format_merged_cmdsets(self, obj, current_cmdset): def format_merged_cmdsets(self, obj, current_cmdset):
if not hasattr(obj, "cmdset"): if not hasattr(obj, "cmdset"):
@ -2618,7 +2623,7 @@ class CmdExamine(ObjManipCommand):
for cmdset in all_cmdsets: for cmdset in all_cmdsets:
if cmdset.key != "_EMPTY_CMDSET": if cmdset.key != "_EMPTY_CMDSET":
merged_cmdset_strings.append(self.format_single_cmdset(cmdset)) merged_cmdset_strings.append(self.format_single_cmdset(cmdset))
return "\n ".join(merged_cmdset_strings) return "\n " + "\n ".join(merged_cmdset_strings)
def format_current_cmds(self, obj, current_cmdset): def format_current_cmds(self, obj, current_cmdset):
current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")]) current_commands = sorted([cmd.key for cmd in current_cmdset if cmd.access(obj, "cmd")])
@ -2673,10 +2678,13 @@ class CmdExamine(ObjManipCommand):
return f"{self.header_color}{key}|n={value}{typ}" return f"{self.header_color}{key}|n={value}{typ}"
def format_attributes(self, obj): def format_attributes(self, obj):
return "\n " + "\n ".join( output = "\n " + "\n ".join(
sorted(self.format_single_attribute(attr) sorted(self.format_single_attribute(attr)
for attr in obj.db_attributes.all()) for attr in obj.db_attributes.all())
) )
if output.strip():
# we don't want just an empty line
return output
def format_nattributes(self, obj): def format_nattributes(self, obj):
try: try:
@ -2707,6 +2715,51 @@ class CmdExamine(ObjManipCommand):
if not obj.account and not obj.destination) if not obj.account and not obj.destination)
return things if things else None return things if things else None
def format_script_desc(self, obj):
if hasattr(obj, "db_desc") and obj.db_desc:
return crop(obj.db_desc, 20)
def format_script_is_persistent(self, obj):
if hasattr(obj, "db_persistent"):
return "T" if obj.db_persistent else "F"
def format_script_timer_data(self, obj):
if hasattr(obj, "db_interval") and obj.db_interval > 0:
start_delay = "T" if obj.db_start_delay else "F"
next_repeat = obj.time_until_next_repeat()
active = "|grunning|n" if obj.db_is_active and next_repeat else "|rinactive|n"
interval = obj.db_interval
next_repeat = "N/A" if next_repeat is None else f"{next_repeat}s"
repeats = ""
if obj.db_repeats:
remaining_repeats = obj.remaining_repeats()
remaining_repeats = 0 if remaining_repeats is None else remaining_repeats
repeats = f" - {remaining_repeats}/{obj.db_repeats} remain"
return (f"{active} - interval: {interval}s "
f"(next: {next_repeat}{repeats}, start_delay: {start_delay})")
def format_channel_sub_totals(self, obj):
if hasattr(obj, "db_account_subscriptions"):
account_subs = obj.db_account_subscriptions.all()
object_subs = obj.db_object_subscriptions.all()
online = len(obj.subscriptions.online())
ntotal = account_subs.count() + object_subs.count()
return f"{ntotal} ({online} online)"
def format_channel_account_subs(self, obj):
if hasattr(obj, "db_account_subscriptions"):
account_subs = obj.db_account_subscriptions.all()
if account_subs:
return "\n " + "\n ".join(
format_grid([sub.key for sub in account_subs], sep=' ', width=_DEFAULT_WIDTH))
def format_channel_object_subs(self, obj):
if hasattr(obj, "db_object_subscriptions"):
object_subs = obj.db_object_subscriptions.all()
if object_subs:
return "\n " + "\n ".join(
format_grid([sub.key for sub in object_subs], sep=' ', width=_DEFAULT_WIDTH))
def get_formatted_obj_data(self, obj, current_cmdset): def get_formatted_obj_data(self, obj, current_cmdset):
""" """
Calls all other `format_*` methods. Calls all other `format_*` methods.
@ -2734,6 +2787,10 @@ class CmdExamine(ObjManipCommand):
objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset) objdata["Merged Cmdset(s)"] = self.format_merged_cmdsets(obj, current_cmdset)
objdata[f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"] = ( objdata[f"Commands vailable to {obj.key} (result of Merged Cmdset(s))"] = (
self.format_current_cmds(obj, current_cmdset)) self.format_current_cmds(obj, current_cmdset))
if self.object_type == "script":
objdata["Description"] = self.format_script_desc(obj)
objdata["Persistent"] = self.format_script_is_persistent(obj)
objdata["Script Repeat"] = self.format_script_timer_data(obj)
objdata["Scripts"] = self.format_scripts(obj) objdata["Scripts"] = self.format_scripts(obj)
objdata["Tags"] = self.format_tags(obj) objdata["Tags"] = self.format_tags(obj)
objdata["Persistent Attributes"] = self.format_attributes(obj) objdata["Persistent Attributes"] = self.format_attributes(obj)
@ -2741,6 +2798,11 @@ class CmdExamine(ObjManipCommand):
objdata["Exits"] = self.format_exits(obj) objdata["Exits"] = self.format_exits(obj)
objdata["Characters"] = self.format_chars(obj) objdata["Characters"] = self.format_chars(obj)
objdata["Content"] = self.format_things(obj) objdata["Content"] = self.format_things(obj)
if self.object_type == "channel":
objdata["Subscription Totals"] = self.format_channel_sub_totals(obj)
objdata["Account Subscriptions"] = self.format_channel_account_subs(obj)
objdata["Object Subscriptions"] = self.format_channel_object_subs(obj)
return objdata return objdata
def format_output(self, obj, current_cmdset): def format_output(self, obj, current_cmdset):
@ -2765,6 +2827,46 @@ class CmdExamine(ObjManipCommand):
return f"{sep}\n{main_str}\n{sep}" return f"{sep}\n{main_str}\n{sep}"
def _search_by_object_type(self, obj_name, objtype):
"""
Route to different search functions depending on the object type being
examined. This also handles error reporting for multimatches/no matches.
Args:
obj_name (str): The search query.
objtype (str): One of 'object', 'account', 'script' or 'channel'.
Returns:
any: `None` if no match or multimatch, otherwise a single result.
"""
obj = None
if objtype == "object":
obj = self.caller.search(obj_name)
elif objtype == "account":
try:
obj = self.caller.search_account(obj_name.lstrip("*"))
except AttributeError:
# this means we are calling examine from an account object
obj = self.caller.search(
obj_name.lstrip("*"), search_object="object" in self.switches
)
else:
obj = getattr(search, f"search_{objtype}")(obj_name)
if not obj:
self.caller.msg(f"No {objtype} found with key {obj_name}.")
obj = None
elif len(obj) > 1:
err = "Multiple {objtype} found with key {obj_name}:\n{matches}"
self.caller.msg(err.format(
obj_name=obj_name,
matches=", ".join(f"{ob.key}(#{ob.id})" for ob in obj)
))
obj = None
else:
obj = obj[0]
return obj
def parse(self): def parse(self):
super().parse() super().parse()
@ -2779,42 +2881,32 @@ class CmdExamine(ObjManipCommand):
raise InterruptCommand raise InterruptCommand
else: else:
for objdef in self.lhs_objattr: for objdef in self.lhs_objattr:
# note that we check the objtype for every repeat; this will always
# be the same result, but it makes for a cleaner code and multi-examine
# is not so common anyway.
obj = None obj = None
obj_name = objdef["name"] # name obj_name = objdef["name"] # name
obj_attrs = objdef["attrs"] # /attrs obj_attrs = objdef["attrs"] # /attrs
self.account_mode = ( # identify object type, in prio account - script - channel
utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount") object_type = "object"
or "account" in self.switches if (utils.inherits_from(self.caller, "evennia.accounts.accounts.DefaultAccount")
or obj_name.startswith("*") or "account" in self.switches or obj_name.startswith("*")):
) object_type = "account"
if self.account_mode: elif "script" in self.switches:
try: object_type = "script"
obj = self.caller.search_account(obj_name.lstrip("*")) elif "channel" in self.switches:
except AttributeError: object_type = "channel"
# this means we are calling examine from an account object
obj = self.caller.search( self.object_type = object_type
obj_name.lstrip("*"), search_object="object" in self.switches obj = self._search_by_object_type(obj_name, object_type)
)
else:
obj = self.caller.search(obj_name)
if obj: if obj:
self.examine_objs.append((obj, obj_attrs)) self.examine_objs.append((obj, obj_attrs))
def func(self): def func(self):
"""Process command""" """Process command"""
def get_cmdset_callback(current_cmdset):
"""
We make use of the cmdhandler.get_and_merge_cmdsets below. This
is an asynchronous function, returning a Twisted deferred.
So in order to properly use this we need use this callback;
it is called with the result of get_and_merge_cmdsets, whenever
that function finishes. Taking the resulting cmdset, we continue
to format and output the result.
"""
self.msg(self.format_output(obj, current_cmdset).strip())
for obj, obj_attrs in self.examine_objs: for obj, obj_attrs in self.examine_objs:
# these are parsed out in .parse already # these are parsed out in .parse already
@ -2842,19 +2934,22 @@ class CmdExamine(ObjManipCommand):
# examine the obj itself # examine the obj itself
# get the cmdset status if self.object_type in ("object", "account"):
# for objects and accounts we need to set up an asynchronous
# fetch of the cmdset and not proceed with the examine display
# until the fetch is complete
session = None session = None
if obj.sessions.count(): if obj.sessions.count():
mergemode = "session" mergemode = "session"
session = obj.sessions.get()[0] session = obj.sessions.get()[0]
elif self.account_mode: elif self.object_type == "account":
mergemode = "account" mergemode = "account"
else: else:
mergemode = "object" mergemode = "object"
account = None account = None
objct = None objct = None
if self.account_mode: if self.object_type == "account":
account = obj account = obj
else: else:
account = obj.account account = obj.account
@ -2864,9 +2959,17 @@ class CmdExamine(ObjManipCommand):
# we may have leftover inherited cmdsets directly after a move etc. # we may have leftover inherited cmdsets directly after a move etc.
obj.cmdset.update() obj.cmdset.update()
# using callback to print results whenever function returns. # using callback to print results whenever function returns.
def _get_cmdset_callback(current_cmdset):
self.msg(self.format_output(obj, current_cmdset).strip())
get_and_merge_cmdsets( get_and_merge_cmdsets(
obj, session, account, objct, mergemode, self.raw_string obj, session, account, objct, mergemode, self.raw_string
).addCallback(get_cmdset_callback) ).addCallback(_get_cmdset_callback)
else:
# for objects without cmdsets we can proceed to examine immediately
self.msg(self.format_output(obj, None).strip())
class CmdFind(COMMAND_DEFAULT_CLASS): class CmdFind(COMMAND_DEFAULT_CLASS):

View file

@ -737,6 +737,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
""" """
comtable = self.styled_table( comtable = self.styled_table(
"id",
"channel", "channel",
"my aliases", "my aliases",
"locks", "locks",
@ -747,17 +748,24 @@ class CmdChannel(COMMAND_DEFAULT_CLASS):
for chan in subscribed: for chan in subscribed:
locks = "-" locks = "-"
chanid = "-"
if chan.access(self.caller, "control"): if chan.access(self.caller, "control"):
locks = chan.locks locks = chan.locks
chanid = chan.id
my_aliases = ", ".join(self.get_channel_aliases(chan)) my_aliases = ", ".join(self.get_channel_aliases(chan))
comtable.add_row( comtable.add_row(
*("{}{}".format( *(
chan.key, chanid,
"({})".format(",".join(chan.aliases.all())) if chan.aliases.all() else ""), "{key}{aliases}".format(
key=chan.key,
aliases=";"+ ";".join(chan.aliases.all()) if chan.aliases.all() else ""
),
my_aliases, my_aliases,
locks, locks,
chan.db.desc)) chan.db.desc
)
)
return comtable return comtable
def display_all_channels(self, subscribed, available): def display_all_channels(self, subscribed, available):

View file

@ -424,7 +424,7 @@ class ChannelDBManager(TypedObjectManager):
dbref = self.dbref(ostring) dbref = self.dbref(ostring)
if dbref: if dbref:
try: try:
return self.get(id=dbref) return [self.get(id=dbref)]
except self.model.DoesNotExist: except self.model.DoesNotExist:
pass pass
if exact: if exact:

View file

@ -413,7 +413,7 @@ class Msg(SharedMemoryModel):
# ------------------------------------------------------------ # ------------------------------------------------------------
class TempMsg(object): class TempMsg:
""" """
This is a non-persistent object for sending temporary messages that will not be stored. It This is a non-persistent object for sending temporary messages that will not be stored. It
mimics the "real" Msg object, but doesn't require sender to be given. mimics the "real" Msg object, but doesn't require sender to be given.