From b5239e18c1ab71cb6889cc888f7bfe2076f36174 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 11:44:47 -0700 Subject: [PATCH 1/5] fix cmdget help lock --- evennia/commands/default/general.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index e46fbe2cf5..66c5afc4fd 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -387,13 +387,12 @@ class CmdGet(COMMAND_DEFAULT_CLASS): Usage: get - Picks up an object from your location and puts it in - your inventory. + Picks up an object from your location and puts it in your inventory. """ key = "get" aliases = "grab" - locks = "cmd:all();view:perm(Developer);read:perm(Developer)" + locks = "cmd:all()" arg_regex = r"\s|$" def func(self): From 829f32f5736bca5fdcda666b99e97a7a6b878245 Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 13:20:05 -0700 Subject: [PATCH 2/5] add object stacking to get/drop/give --- evennia/commands/default/general.py | 176 ++++++++++++++++++++-------- 1 file changed, 126 insertions(+), 50 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 66c5afc4fd..8c031ac605 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -395,39 +395,64 @@ class CmdGet(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + def func(self): """implements the command.""" caller = self.caller if not self.args: - caller.msg("Get what?") + self.msg("Get what?") return - obj = caller.search(self.args, location=caller.location) - if not obj: + objs = caller.search(self.args, location=caller.location, stacked=self.number) + if not objs: return - if caller == obj: - caller.msg("You can't get yourself.") - return - if not obj.access(caller, "get"): - if obj.db.get_err_msg: - caller.msg(obj.db.get_err_msg) - else: - caller.msg("You can't get that.") + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + objs = utils.make_iter(objs) + + if len(objs) == 1 and caller == objs[0]: + self.msg("You can't get yourself.") return - # calling at_pre_get hook method - if not obj.at_pre_get(caller): - return + # if we aren't allowed to get any of the objects, cancel the get + for obj in objs: + # check the locks + if not obj.access(caller, "get"): + if obj.db.get_err_msg: + self.msg(obj.db.get_err_msg) + else: + self.msg("You can't get that.") + return + # calling at_pre_get hook method + if not obj.at_pre_get(caller): + return - success = obj.move_to(caller, quiet=True, move_type="get") - if not success: - caller.msg("This can't be picked up.") + moved = [] + # attempt to move all of the objects + for obj in objs: + if obj.move_to(caller, quiet=True, move_type="get"): + moved.append(obj) + # calling at_get hook method + obj.at_get(caller) + + if not moved: + # none of the objects were successfully moved + self.msg("That can't be picked up.") else: - singular, _ = obj.get_numbered_name(1, caller) - caller.location.msg_contents(f"$You() $conj(pick) up {singular}.", from_obj=caller) - # calling at_get hook method - obj.at_get(caller) + singular, plural = moved[0].get_numbered_name(len(moved), caller) + caller.location.msg_contents(f"$You() $conj(pick) up {plural if len(moved) > 1 else singular}.", from_obj=caller) class CmdDrop(COMMAND_DEFAULT_CLASS): @@ -445,6 +470,18 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + def func(self): """Implement command""" @@ -455,27 +492,39 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): # Because the DROP command by definition looks for items # in inventory, call the search function using location = caller - obj = caller.search( + objs = caller.search( self.args, location=caller, nofound_string=f"You aren't carrying {self.args}.", multimatch_string=f"You carry more than one {self.args}:", + stacked=self.number, ) - if not obj: + if not objs: return + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + objs = utils.make_iter(objs) - # Call the object script's at_pre_drop() method. - if not obj.at_pre_drop(caller): - return + # if any objects fail the drop permission check, cancel the drop + for obj in objs: + # Call the object's at_pre_drop() method. + if not obj.at_pre_drop(caller): + return - success = obj.move_to(caller.location, quiet=True, move_type="drop") - if not success: - caller.msg("This couldn't be dropped.") + # do the actual dropping + moved = [] + for obj in objs: + if obj.move_to(caller.location, quiet=True, move_type="drop"): + moved.append(obj) + # Call the object's at_drop() method. + obj.at_drop(caller) + + if not moved: + # none of the objects were successfully moved + self.msg("That can't be dropped.") else: - singular, _ = obj.get_numbered_name(1, caller) - caller.location.msg_contents(f"$You() $conj(drop) {singular}.", from_obj=caller) - # Call the object script's at_drop() method. - obj.at_drop(caller) + singular, plural = obj.get_numbered_name(len(moved), caller) + caller.location.msg_contents(f"$You() $conj(drop) {plural if len(moved) > 1 else singular}.", from_obj=caller) class CmdGive(COMMAND_DEFAULT_CLASS): @@ -494,6 +543,18 @@ class CmdGive(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" + def parse(self): + super().parse() + self.number = 0 + if self.lhs: + # check for numbering + count, *args = self.lhs.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.lhs = args[0] + def func(self): """Implement give""" @@ -501,37 +562,52 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not self.args or not self.rhs: caller.msg("Usage: give = ") return + # find the thing(s) to give away to_give = caller.search( self.lhs, location=caller, nofound_string=f"You aren't carrying {self.lhs}.", multimatch_string=f"You carry more than one {self.lhs}:", + stacked=self.number, ) + if not to_give: + return + # find the target to give to target = caller.search(self.rhs) - if not (to_give and target): + if not target: return - singular, _ = to_give.get_numbered_name(1, caller) + # the 'stacked' search sometimes returns a list, sometimes not, so we make it always a list + # NOTE: this behavior may be a bug, see issue #3432 + to_give = utils.make_iter(to_give) + + + singular, plural = to_give[0].get_numbered_name(len(to_give), caller) if target == caller: - caller.msg(f"You keep {singular} to yourself.") - return - if not to_give.location == caller: - caller.msg(f"You are not holding {singular}.") + caller.msg(f"You keep {plural if len(to_give) > 1 else singular} to yourself.") return - # calling at_pre_give hook method - if not to_give.at_pre_give(caller, target): - return + # if any of the objects aren't allowed to be given, cancel the give + for obj in to_give: + # calling at_pre_give hook method + if not obj.at_pre_give(caller, target): + return - # give object - success = to_give.move_to(target, quiet=True, move_type="give") - if not success: - caller.msg(f"You could not give {singular} to {target.key}.") + # do the actual moving + moved = [] + for obj in to_give: + if obj.move_to(target, quiet=True, move_type="give"): + moved.append(obj) + # Call the object's at_give() method. + obj.at_give(caller, target) + + if not moved: + caller.msg(f"You could not give that to {target.get_display_name(caller)}.") else: - caller.msg(f"You give {singular} to {target.key}.") - target.msg(f"{caller.key} gives you {singular}.") - # Call the object script's at_give() method. - to_give.at_give(caller, target) + singular, plural = to_give[0].get_numbered_name(len(moved), caller) + names = plural if len(moved) > 1 else singular + caller.msg(f"You give {names} to {target.get_display_name(caller)}.") + target.msg(f"{caller.get_display_name(target)} gives you {names}.") class CmdSetDesc(COMMAND_DEFAULT_CLASS): From 86701f5d2c7115925a696655fa7bf9bb13d76e8d Mon Sep 17 00:00:00 2001 From: InspectorCaracal Date: Mon, 19 Feb 2024 13:52:27 -0700 Subject: [PATCH 3/5] add stack tests --- evennia/commands/default/tests.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/evennia/commands/default/tests.py b/evennia/commands/default/tests.py index b3fe5b5fd7..6bc69f602a 100644 --- a/evennia/commands/default/tests.py +++ b/evennia/commands/default/tests.py @@ -116,8 +116,12 @@ class TestGeneral(BaseEvenniaCommandTest): self.call(general.CmdNick(), "/list", "Defined Nicks:") def test_get_and_drop(self): - self.call(general.CmdGet(), "Obj", "You pick up an Obj") - self.call(general.CmdDrop(), "Obj", "You drop an Obj") + self.call(general.CmdGet(), "Obj", "You pick up an Obj.") + self.call(general.CmdDrop(), "Obj", "You drop an Obj.") + # test stacking + self.obj2.key = "Obj" + self.call(general.CmdGet(), "2 Obj", "You pick up two Objs.") + self.call(general.CmdDrop(), "2 Obj", "You drop two Objs.") def test_give(self): self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.") @@ -125,6 +129,10 @@ class TestGeneral(BaseEvenniaCommandTest): self.call(general.CmdGet(), "Obj", "You pick up an Obj") self.call(general.CmdGive(), "Obj to Char2", "You give") self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2) + # test stacking + self.obj2.key = "Obj" + self.obj2.location = self.char1 + self.call(general.CmdGive(), "2 Obj = Char2", "You give two Objs") def test_mux_command(self): class CmdTest(MuxCommand): From f5552121cf80a7df7884dbfd29263ae3ec729273 Mon Sep 17 00:00:00 2001 From: Cal Date: Mon, 1 Apr 2024 11:24:31 -0600 Subject: [PATCH 4/5] add new NumberedTargetCommand --- evennia/commands/default/general.py | 105 ++++++++++++++++------------ 1 file changed, 60 insertions(+), 45 deletions(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 8c031ac605..ce45daadc2 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -379,8 +379,58 @@ class CmdInventory(COMMAND_DEFAULT_CLASS): string = f"|wYou are carrying:\n{table}" self.msg(text=(string, {"type": "inventory"})) +class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): + """ + A class that parses out an optional number component from the input string. This + class is intended to be inherited from to provide additional functionality, rather + than used on its own. -class CmdGet(COMMAND_DEFAULT_CLASS): + Note that the class's __doc__ string (this text) is used by Evennia to create the + automatic help entry for the command, so make sure to document consistently here. + """ + def parse(self): + """ + This method is called by the cmdhandler once the command name + has been identified. It creates a new set of member variables + that can be later accessed from self.func() (see below) + + The following variables are available for our use when entering this + method (from the command definition, and assigned on the fly by the + cmdhandler): + self.key - the name of this command ('look') + self.aliases - the aliases of this cmd ('l') + self.permissions - permission string for this command + self.help_category - overall category of command + + self.caller - the object calling this command + self.cmdstring - the actual command name used to call this + (this allows you to know which alias was used, + for example) + self.args - the raw input; everything following self.cmdstring. + self.cmdset - the cmdset from which this command was picked. Not + often used (useful for commands like 'help' or to + list all available commands etc) + self.obj - the object on which this command was defined. It is often + the same as self.caller. + + This parser does additional parsing on self.args to identify a leading number, + storing the results in the following variables: + self.number = an integer representing the amount, or 0 if none was given + self.args = the re-defined input with the leading number removed + """ + super().parse() + self.number = 0 + if self.args: + # check for numbering + count, *args = self.args.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.args = args[0] + + +class CmdGet(NumberedTargetCommand): """ pick up something @@ -395,17 +445,6 @@ class CmdGet(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.args: - # check for numbering - count, *args = self.args.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.args = args[0] def func(self): """implements the command.""" @@ -451,11 +490,11 @@ class CmdGet(COMMAND_DEFAULT_CLASS): # none of the objects were successfully moved self.msg("That can't be picked up.") else: - singular, plural = moved[0].get_numbered_name(len(moved), caller) - caller.location.msg_contents(f"$You() $conj(pick) up {plural if len(moved) > 1 else singular}.", from_obj=caller) + obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True) + caller.location.msg_contents(f"$You() $conj(pick) up {obj_name}.", from_obj=caller) -class CmdDrop(COMMAND_DEFAULT_CLASS): +class CmdDrop(NumberedTargetCommand): """ drop something @@ -470,18 +509,6 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.args: - # check for numbering - count, *args = self.args.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.args = args[0] - def func(self): """Implement command""" @@ -523,11 +550,11 @@ class CmdDrop(COMMAND_DEFAULT_CLASS): # none of the objects were successfully moved self.msg("That can't be dropped.") else: - singular, plural = obj.get_numbered_name(len(moved), caller) - caller.location.msg_contents(f"$You() $conj(drop) {plural if len(moved) > 1 else singular}.", from_obj=caller) + obj_name = moved[0].get_numbered_name(len(moved), caller, return_string=True) + caller.location.msg_contents(f"$You() $conj(drop) {obj_name}.", from_obj=caller) -class CmdGive(COMMAND_DEFAULT_CLASS): +class CmdGive(NumberedTargetCommand): """ give away something to someone @@ -543,17 +570,6 @@ class CmdGive(COMMAND_DEFAULT_CLASS): locks = "cmd:all()" arg_regex = r"\s|$" - def parse(self): - super().parse() - self.number = 0 - if self.lhs: - # check for numbering - count, *args = self.lhs.split(maxsplit=1) - # we only use the first word as a count if it's a number and - # there is more text afterwards - if args and count.isdecimal(): - self.number = int(count) - self.lhs = args[0] def func(self): """Implement give""" @@ -604,10 +620,9 @@ class CmdGive(COMMAND_DEFAULT_CLASS): if not moved: caller.msg(f"You could not give that to {target.get_display_name(caller)}.") else: - singular, plural = to_give[0].get_numbered_name(len(moved), caller) - names = plural if len(moved) > 1 else singular - caller.msg(f"You give {names} to {target.get_display_name(caller)}.") - target.msg(f"{caller.get_display_name(target)} gives you {names}.") + obj_name = to_give[0].get_numbered_name(len(moved), caller, return_string=True) + caller.msg(f"You give {obj_name} to {target.get_display_name(caller)}.") + target.msg(f"{caller.get_display_name(target)} gives you {obj_name}.") class CmdSetDesc(COMMAND_DEFAULT_CLASS): From 2b27214b52c0323a672847c9665bc6f2e9c8c965 Mon Sep 17 00:00:00 2001 From: Cal Date: Mon, 1 Apr 2024 11:38:52 -0600 Subject: [PATCH 5/5] accommodate MuxCommand splitting --- evennia/commands/default/general.py | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index ce45daadc2..d91b6b6b83 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -417,17 +417,30 @@ class NumberedTargetCommand(COMMAND_DEFAULT_CLASS): storing the results in the following variables: self.number = an integer representing the amount, or 0 if none was given self.args = the re-defined input with the leading number removed + + Optionally, if COMMAND_DEFAULT_CLASS is a MuxCommand, it applies the same + parsing to self.lhs """ super().parse() self.number = 0 + if hasattr(self, 'lhs'): + # handle self.lhs but don't require it + count, *args = self.lhs.split(maxsplit=1) + # we only use the first word as a count if it's a number and + # there is more text afterwards + if args and count.isdecimal(): + self.number = int(count) + self.lhs = args[0] if self.args: # check for numbering count, *args = self.args.split(maxsplit=1) # we only use the first word as a count if it's a number and # there is more text afterwards if args and count.isdecimal(): - self.number = int(count) self.args = args[0] + # we only re-assign self.number if it wasn't already taken from self.lhs + if not self.number: + self.number = int(count) class CmdGet(NumberedTargetCommand):