diff --git a/evennia/commands/default/comms.py b/evennia/commands/default/comms.py index bcf4fdb7e1..81fef3718a 100644 --- a/evennia/commands/default/comms.py +++ b/evennia/commands/default/comms.py @@ -51,14 +51,13 @@ _DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH class CmdChannel(COMMAND_DEFAULT_CLASS): """ - Talk on and manage in-game channels. + Use and manage in-game channels. Usage: - channel channel channelname - channel channel name [= ] - channel/list - channel/all + channel channel name = + channel (show all subscription) + channel/all (show available channels) channel/alias channelname = alias[;alias...] channel/unalias alias channel/who channelname @@ -73,10 +72,10 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): channel/desc channelname = description channel/lock channelname = lockstring channel/unlock channelname = lockstring - channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason] channel/ban channelname (list bans) - channe/ban[/quiet] channelname[, channelname, ...] = subscribername [: reason] + channel/ban[/quiet] channelname[, channelname, ...] = subscribername [: logmessage] channel/unban[/quiet] channelname[, channelname, ...] = subscribername + channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason] # subtopics @@ -85,8 +84,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): Usage: channel channelname msg channel channel name = msg (with space in channel name) - This sends a message to the channel. Note that you will rarely use this command - like this; instead you can use the alias + This sends a message to the channel. Note that you will rarely use this + command like this; instead you can use the alias channelname channelalias @@ -117,8 +116,8 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): wguild Hello warchannel Hello - Note that this will not work if the alias has a space in it. So the 'warrior guild' - alias must be used with the `channel` command: + Note that this will not work if the alias has a space in it. So the + 'warrior guild' alias must be used with the `channel` command: channel warrior guild = Hello @@ -138,6 +137,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): This will display the last |c20|n lines of channel history. By supplying an index number, you will step that many lines back before viewing those 20 lines. + For example: channel/history public = 35 @@ -182,14 +182,39 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): listen - who may listen or join the channel. send - who may send messages to the channel control - who controls the channel. This is usually the one creating - the channel. + the channel. - Common lockfuncs are all() and perm(). To make a channel everyone can listen - to but only builders can talk on, use this: + Common lockfuncs are all() and perm(). To make a channel everyone can + listen to but only builders can talk on, use this: listen:all() send: perm(Builders) + ## boot and ban + + Usage: channel/ban channelname (list bans) + channel/ban channelname[, channelname, ...] = subscribername [: logmessage] + channel/unban channelname[, channelname, ...] = subscribername + channel/unban channelname + channel/boot[/quiet] channelname[,channelname,...] = subscribername [: reason] + + Booting will kick a named subscriber from a channel temporarily. It will + also remove all their aliases. They are still able to re-connect unless + they are also banned. The 'reason' given will be echoed to the user and + channel. + + Banning will block a given subscriber's ability to connect to a channel. It + will not automatically boot them. The 'logmessage' will be stored on the + channel and shown when you list your bans (so you can remember why they + were banned). + + So to permanently get rid of a user, the way to do so is to first ban them + and then boot them. + + Example: + ban mychannel1,mychannel2 = EvilUser : Was banned for spamming. + boot mychannel1,mychannel2 = EvilUser : No more spamming! + """ key = "channel" aliases = ["chan", "channels"] @@ -505,7 +530,7 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): new_chan = create.create_channel( name, aliases=aliases, desc=description, locks=lockstring, typeclass=typeclass) - new_chan.connect(caller) + self.sub_to_channel(new_chan) return new_chan, "" def destroy_channel(self, channel, message=None): @@ -831,12 +856,12 @@ class CmdChannel(COMMAND_DEFAULT_CLASS): table = self.display_all_channels(subscribed, available) self.msg( - "\n|wAvailable channels|n (use /list to " - f"only show subscriptions)\n{table}") + "\n|wAvailable channels|n (use no argument to " + f"only show your subscriptions)\n{table}") return if not channel_names: - # (empty or /list) show only subscribed channels + # empty arg show only subscribed channels subscribed, _ = self.list_channels() table = self.display_subbed_channels(subscribed) diff --git a/evennia/commands/default/help.py b/evennia/commands/default/help.py index 13e8ed36c9..7a274b3c34 100644 --- a/evennia/commands/default/help.py +++ b/evennia/commands/default/help.py @@ -31,8 +31,6 @@ DEFAULT_HELP_CATEGORY = settings.DEFAULT_HELP_CATEGORY # limit symbol import for API __all__ = ("CmdHelp", "CmdSetHelp") -_DEFAULT_WIDTH = settings.CLIENT_DEFAULT_WIDTH -_SEP = "|C" + "-" * _DEFAULT_WIDTH + "|n" @dataclass @@ -59,16 +57,16 @@ class HelpCategory: class CmdHelp(COMMAND_DEFAULT_CLASS): """ - View help or a list of topics + Get help. Usage: help help - help / - help / / ... + help / + help // ... - Use the help command alone to see an index of all help topics, organized by - category. Some long topics may offer additional sub-topics. + Use the 'help' command alone to see an index of all help topics, organized + by category.eSome big topics may offer additional sub-topics. """ @@ -123,8 +121,7 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): self.msg(text=(text, {"type": "help"})) - @staticmethod - def format_help_entry(topic="", help_text="", aliases=None, suggested=None, + def format_help_entry(self, topic="", help_text="", aliases=None, suggested=None, subtopics=None): """ This visually formats the help entry. @@ -142,7 +139,8 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): Returns the formatted string, ready to be sent. """ - start = f"{_SEP}\n" + separator = "|C" + "-" * self.client_width() + "|n" + start = f"{separator}\n" title = f"|CHelp for |w{topic}|n" if topic else "|rNo help found|n" @@ -156,26 +154,27 @@ class CmdHelp(COMMAND_DEFAULT_CLASS): help_text = "\n\n" + dedent(help_text.strip('\n')) + "\n" if help_text else "" if subtopics: + subtopics = [f"|w{topic}/{subtop}|n" for subtop in subtopics] subtopics = ( "\n|CSubtopics:|n\n {}".format( - "\n ".join(f"|w{topic}/{subtop}|n" for subtop in subtopics)) + "\n ".join(format_grid(subtopics, width=self.client_width()))) ) else: subtopics = '' if suggested: + suggested = [f"|w{sug}|n" for sug in suggested] suggested = ( "\n|CSuggestions:|n\n{}".format( - fill("|C,|n ".join(f"|w{sug}|n" for sug in suggested), indent=2)) + "\n ".join(format_grid(suggested, width=self.client_width()))) ) else: suggested = '' - end = f"\n{_SEP}" + end = f"\n{separator}" return "".join((start, title, aliases, help_text, subtopics, suggested, end)) - def format_help_index(self, cmd_help_dict=None, db_help_dict=None, title_lone_category=False): """ Output a category-ordered g for displaying the main help, grouped by diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index 268b87b046..c086916aca 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -439,8 +439,8 @@ class TestHelp(CommandTest): "Help for test\n\n" "Main help text\n\n" "Subtopics:\n" - " test/creating extra stuff\n" - " test/something else\n" + " test/creating extra stuff" + " test/something else" " test/more" ), ("test/creating extra stuff", # subtopic, full match @@ -483,14 +483,14 @@ class TestHelp(CommandTest): "Help for test/more/second-more\n\n" "The Second More text.\n\n" "Subtopics:\n" - " test/more/second-more/more again\n" + " test/more/second-more/more again" " test/more/second-more/third more" ), ("test/More/-more", # partial match "Help for test/more/second-more\n\n" "The Second More text.\n\n" "Subtopics:\n" - " test/more/second-more/more again\n" + " test/more/second-more/more again" " test/more/second-more/third more" ), ("test/more/second/more again", diff --git a/evennia/contrib/README.md b/evennia/contrib/README.md index 568e792e92..ebf08c6cca 100644 --- a/evennia/contrib/README.md +++ b/evennia/contrib/README.md @@ -59,6 +59,8 @@ things you want from here into your game folder and change them there. time to pass depending on if you are walking/running etc. * Talking NPC (Griatch 2011) - A talking NPC object that offers a menu-driven conversation tree. +* Traits (Whitenoise, Griatch 2021) - Properties for handling and tracking + changing RPG skill and stat values. * Tree Select (FlutterSprite 2017) - A simple system for creating a branching EvMenu with selection options sourced from a single multi-line string. diff --git a/evennia/help/utils.py b/evennia/help/utils.py index b0fdba9925..fce4811602 100644 --- a/evennia/help/utils.py +++ b/evennia/help/utils.py @@ -173,16 +173,9 @@ def parse_entry_for_subcategories(entry): } } - - Apart from making - sub-categories at the bottom of the entry. - - This will be applied both to command docstrings and database-based help - entries. - """ topic, *subtopics = _RE_HELP_SUBTOPICS_START.split(entry, maxsplit=1) - structure = {None: topic.strip()} + structure = {None: topic.strip('\n')} if subtopics: subtopics = subtopics[0] diff --git a/evennia/utils/tests/test_utils.py b/evennia/utils/tests/test_utils.py index 3895f0527f..2c4011c73b 100644 --- a/evennia/utils/tests/test_utils.py +++ b/evennia/utils/tests/test_utils.py @@ -321,7 +321,7 @@ class TestFormatGrid(TestCase): """Grid with small variations""" elements = self._generate_elements(3, 1, 30) rows = utils.format_grid(elements, width=78) - self.assertEqual(len(rows), 3) + self.assertEqual(len(rows), 4) self.assertTrue(all(len(row) == 78 for row in rows)) def test_disparate_grid(self): @@ -356,7 +356,7 @@ class TestFormatGrid(TestCase): "lock", ) rows = utils.format_grid(elements, width=78) - self.assertEqual(len(rows), 2) + self.assertEqual(len(rows), 3) for element in elements: self.assertTrue(element in "\n".join(rows), f"element {element} is missing.") diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index 46b3b2009d..3d2138872b 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -1846,7 +1846,7 @@ def percentile(iterable, percent, key=lambda x: x): return d0 + d1 -def format_grid(elements, width=78, sep=" ", verbatim_elements=None): +def format_grid(elements, width=78, sep=" ", verbatim_elements=None): """ This helper function makes a 'grid' output, where it distributes the given string-elements as evenly as possible to fill out the given width. @@ -1869,6 +1869,97 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None): like this to make it easier to insert decorations between rows, such as horizontal bars. """ + def _minimal_rows(elements): + """ + Minimalistic distribution with minimal spacing, good for single-line + grids but will look messy over many lines. + """ + rows = [""] + for element in elements: + rowlen = len(rows[-1]) + elen = len(element) + if rowlen + elen <= width: + rows[-1] += element + else: + rows.append(element) + return rows + + def _weighted_rows(elements): + """ + Dynamic-space, good for making even columns in a multi-line grid but + will look strange for a single line. + """ + + wls = [len(elem) for elem in elements] + wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements] + + if wls_percentile: + # get the nth percentile as a good representation of average width + averlen = int(percentile(sorted(wls_percentile), 0.9)) + 2 # include extra space + aver_per_row = width // averlen + 1 + else: + # no adjustable rows, just keep all as-is + aver_per_row = 1 + + if aver_per_row == 1: + # one line per row, output directly since this is trivial + # we use rstrip here to remove extra spaces added by sep + return [ + crop(element.rstrip(), width) + " " * max(0, width - len(element.rstrip())) + for iel, element in enumerate(elements) + ] + + indices = [averlen * ind for ind in range(aver_per_row - 1)] + + rows = [] + ic = 0 + row = "" + for ie, element in enumerate(elements): + + wl = wls[ie] + lrow = len(row) + debug = row.replace(" ", ".") + + if lrow + wl > width: + # this slot extends outside grid, move to next line + row += " " * (width - lrow) + rows.append(row) + if wl >= width: + # remove sep if this fills the entire line + element = element.rstrip() + row = crop(element, width) + ic = 0 + elif ic >= aver_per_row - 1: + # no more slots available on this line + row += " " * max(0, (width - lrow)) + rows.append(row) + row = crop(element, width) + ic = 0 + else: + try: + while lrow > max(0, indices[ic]): + # slot too wide, extend into adjacent slot + ic += 1 + row += " " * max(0, indices[ic] - lrow) + except IndexError: + # we extended past edge of grid, crop or move to next line + if ic == 0: + row = crop(element, width) + else: + row += " " * max(0, width - lrow) + rows.append(row) + ic = 0 + else: + # add a new slot + row += element + " " * max(0, averlen - wl) + ic += 1 + + if ie >= nelements - 1: + # last element, make sure to store + row += " " * max(0, width - len(row)) + rows.append(row) + return rows + if not elements: return [] if not verbatim_elements: @@ -1877,76 +1968,15 @@ def format_grid(elements, width=78, sep=" ", verbatim_elements=None): nelements = len(elements) # add sep to all but the very last element elements = [elements[ie] + sep for ie in range(nelements - 1)] + [elements[-1]] - wls = [len(elem) for elem in elements] - wls_percentile = [wl for iw, wl in enumerate(wls) if iw not in verbatim_elements] - if wls_percentile: - # get the nth percentile as a good representation of average width - averlen = int(percentile(sorted(wls_percentile), 0.9)) + 2 # include extra space - aver_per_row = width // averlen + 1 + if sum(len(element) for element in elements) <= width: + # grid fits in one line + return _minimal_rows(elements) else: - # no adjustable rows, just keep all as-is - aver_per_row = 1 + # full multi-line grid + return _weighted_rows(elements) - if aver_per_row == 1: - # one line per row, output directly since this is trivial - # we use rstrip here to remove extra spaces added by sep - return [ - crop(element.rstrip(), width) + " " * max(0, width - len(element.rstrip())) - for iel, element in enumerate(elements) - ] - indices = [averlen * ind for ind in range(aver_per_row - 1)] - - rows = [] - ic = 0 - row = "" - for ie, element in enumerate(elements): - - wl = wls[ie] - lrow = len(row) - debug = row.replace(" ", ".") - - if lrow + wl > width: - # this slot extends outside grid, move to next line - row += " " * (width - lrow) - rows.append(row) - if wl >= width: - # remove sep if this fills the entire line - element = element.rstrip() - row = crop(element, width) - ic = 0 - elif ic >= aver_per_row - 1: - # no more slots available on this line - row += " " * max(0, (width - lrow)) - rows.append(row) - row = crop(element, width) - ic = 0 - else: - try: - while lrow > max(0, indices[ic]): - # slot too wide, extend into adjacent slot - ic += 1 - row += " " * max(0, indices[ic] - lrow) - except IndexError: - # we extended past edge of grid, crop or move to next line - if ic == 0: - row = crop(element, width) - else: - row += " " * max(0, width - lrow) - rows.append(row) - ic = 0 - else: - # add a new slot - row += element + " " * max(0, averlen - wl) - ic += 1 - - if ie >= nelements - 1: - # last element, make sure to store - row += " " * max(0, width - len(row)) - rows.append(row) - - return rows def get_evennia_pids():