From 9eb136f59ff8720642f566d66fcfe8b13eb1cf9c Mon Sep 17 00:00:00 2001 From: Wendy Wang Date: Sun, 6 Oct 2024 13:20:46 +0200 Subject: [PATCH 1/4] Contrib - Item Storage This contrib adds room-based, tag-based item storage. Players can store, retrieve, and list items stored in a room. Rooms can be marked as storerooms with the `storage` command by builders. Storerooms can have individual storage or shared storage. --- .../contrib/game_systems/storage/README.md | 37 ++++ .../contrib/game_systems/storage/__init__.py | 5 + .../contrib/game_systems/storage/storage.py | 181 ++++++++++++++++++ evennia/contrib/game_systems/storage/tests.py | 105 ++++++++++ 4 files changed, 328 insertions(+) create mode 100644 evennia/contrib/game_systems/storage/README.md create mode 100644 evennia/contrib/game_systems/storage/__init__.py create mode 100644 evennia/contrib/game_systems/storage/storage.py create mode 100644 evennia/contrib/game_systems/storage/tests.py diff --git a/evennia/contrib/game_systems/storage/README.md b/evennia/contrib/game_systems/storage/README.md new file mode 100644 index 0000000000..5b573e2678 --- /dev/null +++ b/evennia/contrib/game_systems/storage/README.md @@ -0,0 +1,37 @@ +# Item Storage + +Contribution by helpme (2024) + +This module allows certain rooms to be marked as storage locations. + +In those rooms, players can `list`, `store`, and `retrieve` items. Storages can be shared or individual. + +## Installation + +This utility adds the storage-related commands. Import the module into your commands and add it to your command set to make it available. + +Specifically, in `mygame/commands/default_cmdsets.py`: + +```python +... +from evennia.contrib.game_systems.storage import StorageCmdSet # <--- + +class CharacterCmdset(default_cmds.Character_CmdSet): + ... + def at_cmdset_creation(self): + ... + self.add(StorageCmdSet) # <--- + +``` + +Then `reload` to make the `list`, `retrieve`, `store`, and `storage` commands available. + +## Usage + +To mark a location as having item storage, use the `storage` command. By default this is a builder-level command. Storage can be shared, which means everyone using the storage can access all items stored there, or individual, which means only the person who stores an item can retrieve it. See `help storage` for further details. + +## Technical info + +This is a tag-based system. Rooms set as storage rooms are tagged with an identifier marking them as shared or not. Items stored in those rooms are tagged with the storage room identifier and, if the storage room is not shared, the character identifier, and then they are removed from the grid i.e. their location is set to `None`. Upon retrieval, items are untagged and moved back to character inventories. + +When a room is unmarked as storage with the `storage` command, all stored objects are untagged and dropped to the room. You should use the `storage` command to create and remove storages, as otherwise stored objects may become lost. \ No newline at end of file diff --git a/evennia/contrib/game_systems/storage/__init__.py b/evennia/contrib/game_systems/storage/__init__.py new file mode 100644 index 0000000000..3eaa1043f5 --- /dev/null +++ b/evennia/contrib/game_systems/storage/__init__.py @@ -0,0 +1,5 @@ +""" +Item storage integration - helpme 2022 +""" + +from .storage import StorageCmdSet # noqa diff --git a/evennia/contrib/game_systems/storage/storage.py b/evennia/contrib/game_systems/storage/storage.py new file mode 100644 index 0000000000..e362a2f363 --- /dev/null +++ b/evennia/contrib/game_systems/storage/storage.py @@ -0,0 +1,181 @@ +from evennia import CmdSet +from evennia.utils import list_to_string +from evennia.utils.search import search_object_by_tag +from evennia.commands.default.muxcommand import MuxCommand + + +class StorageCommand(MuxCommand): + """ + Shared functionality for storage-related commands + """ + + def at_pre_cmd(self): + """ + Check if the current location is tagged as a storage location + Every stored object is tagged on storage, and untagged on retrieval + """ + success = super().at_pre_cmd() + self.storage_location_id = self.caller.location.tags.get(category="storage_location") + if not self.storage_location_id: + self.caller.msg(f"You cannot {self.cmdstring} anything here.") + return True + + self.object_tag = ( + "shared" if self.storage_location_id.startswith("shared_") else self.caller.pk + ) + self.currently_stored = search_object_by_tag( + self.object_tag, category=self.storage_location_id + ) + return success + + +class CmdStore(StorageCommand): + """ + Store something in a storage location. + + Usage: + store + """ + + key = "store" + locks = "cmd:all()" + help_category = "Storage" + + def func(self): + """ + Find the item in question to store, then store it + """ + caller = self.caller + if not self.args: + self.caller.msg("Store what?") + return + obj = caller.search(self.args.strip(), candidates=caller.contents) + if not obj: + return + + """ + We first check at_pre_move before setting the location to None, in case + anything should stymie its movement. + """ + if obj.at_pre_move(caller.location): + obj.tags.add(self.object_tag, self.storage_location_id) + obj.location = None + caller.msg(f"You store {obj.get_display_name(caller)} here.") + else: + caller.msg(f"You fail to store {obj.get_display_name(caller)} here.") + + +class CmdRetrieve(StorageCommand): + """ + Retrieve something from a storage location. + + Usage: + retrieve + """ + + key = "retrieve" + locks = "cmd:all()" + help_category = "Storage" + + def func(self): + """ + Retrieve the item in question if possible + """ + caller = self.caller + + if not self.args: + self.caller.msg("Retrieve what?") + return + + obj = caller.search(self.args.strip(), candidates=self.currently_stored) + if not obj: + return + + if obj.at_pre_move(caller): + obj.tags.remove(self.object_tag, self.storage_location_id) + caller.msg(f"You retrieve {obj.get_display_name(caller)}.") + else: + caller.msg(f"You fail to retrieve {obj.get_display_name(caller)}.") + + +class CmdList(StorageCommand): + """ + List items in the storage location. + + Usage: + list + """ + + key = "list" + locks = "cmd:all()" + help_category = "Storage" + + def func(self): + """ + List items in the storage + """ + caller = self.caller + if not self.currently_stored: + caller.msg("You find nothing stored here.") + return + caller.msg(f"Stored here:\n{list_to_string(self.currently_stored)}") + + +class CmdStorage(MuxCommand): + """ + Make the current location a storage room, or delete it as a storage and move all stored objects into the room contents. + + Shared storage locations can be used by all interchangeably. + + The default storage identifier will be its primary key in the database, but you can supply a new one in case you want linked storages. + + Usage: + storage [= [storage identifier]] + storage/shared [= [storage identifier]] + storage/delete + """ + + key = "@storage" + locks = "cmd:perm(Builder)" + + def func(self): + """Set the storage location.""" + + caller = self.caller + location = caller.location + current_storage_id = location.tags.get(category="storage_location") + storage_id = self.lhs or location.pk + + if "delete" in self.switches: + if not current_storage_id: + caller.msg("This is not tagged as a storage location.") + return + # Move the stored objects, if any, into the room + currently_stored_here = search_object_by_tag(category=current_storage_id) + for obj in currently_stored_here: + obj.tags.remove(category=current_storage_id) + obj.location = location + caller.msg("You remove the storage capabilities of the room.") + location.tags.remove(current_storage_id, category="storage_location") + return + + if current_storage_id: + caller.msg("This is already a storage location: |wstorage/delete|n to remove the tag.") + return + + new_storage_id = f"{'shared_' if 'shared' in self.switches else ''}{storage_id}" + location.tags.add(new_storage_id, category="storage_location") + caller.msg(f"This is now a storage location with id: {new_storage_id}.") + + +# CmdSet for easily install all commands +class StorageCmdSet(CmdSet): + """ + The git command. + """ + + def at_cmdset_creation(self): + self.add(CmdStore) + self.add(CmdRetrieve) + self.add(CmdList) + self.add(CmdStorage) diff --git a/evennia/contrib/game_systems/storage/tests.py b/evennia/contrib/game_systems/storage/tests.py new file mode 100644 index 0000000000..65fe763bbf --- /dev/null +++ b/evennia/contrib/game_systems/storage/tests.py @@ -0,0 +1,105 @@ +from evennia.commands.default.tests import BaseEvenniaCommandTest +from evennia.utils.create import create_object + +from . import storage + + +class TestStorage(BaseEvenniaCommandTest): + def setUp(self): + super().setUp() + self.char1.location = self.room1 + self.obj1.location = self.char1 + self.room1.tags.add("storage_1", "storage_location") + self.room2.tags.add("shared_storage_2", "storage_location") + + def test_store_and_retrieve(self): + self.call( + storage.CmdStore(), + "", + "Store what?", + caller=self.char1, + ) + self.call( + storage.CmdStore(), + "obj", + f"You store {self.obj1.get_display_name(self.char1)} here.", + caller=self.char1, + ) + self.call( + storage.CmdList(), + "", + f"Stored here:\n{self.obj1.get_display_name(self.char1)}", + caller=self.char1, + ) + self.call( + storage.CmdRetrieve(), + "obj2", + "Could not find 'obj2'.", + caller=self.char1, + ) + self.call( + storage.CmdRetrieve(), + "obj", + f"You retrieve {self.obj1.get_display_name(self.char1)}.", + caller=self.char1, + ) + + def test_store_retrieve_while_not_in_storage(self): + self.char2.location = self.char1 + self.call(storage.CmdStore(), "obj", "You cannot store anything here.", caller=self.char2) + self.call( + storage.CmdRetrieve(), "obj", "You cannot retrieve anything here.", caller=self.char2 + ) + + def test_store_retrieve_nonexistent_obj(self): + self.call(storage.CmdStore(), "asdasd", "Could not find 'asdasd'.", caller=self.char1) + self.call(storage.CmdRetrieve(), "asdasd", "Could not find 'asdasd'.", caller=self.char1) + + def test_list_nothing_stored(self): + self.call( + storage.CmdList(), + "", + "You find nothing stored here.", + caller=self.char1, + ) + + def test_shared_storage(self): + self.char1.location = self.room2 + self.char2.location = self.room2 + + self.call( + storage.CmdStore(), + "obj", + f"You store {self.obj1.get_display_name(self.char1)} here.", + caller=self.char1, + ) + self.call( + storage.CmdRetrieve(), + "obj", + f"You retrieve {self.obj1.get_display_name(self.char1)}.", + caller=self.char2, + ) + + def test_remove_add_storage(self): + self.char1.permissions.add("builder") + self.call( + storage.CmdStorage(), + "", + "This is already a storage location: storage/delete to remove the tag.", + caller=self.char1, + ) + self.call( + storage.CmdStore(), + "obj", + f"You store {self.obj1.get_display_name(self.char1)} here.", + caller=self.char1, + ) + + self.assertEqual(self.obj1.location, None) + self.call( + storage.CmdStorage(), + "/delete", + "You remove the storage capabilities of the room.", + caller=self.char1, + ) + self.assertEqual(self.obj1.location, self.room1) From 870b0cc16f06152eaedaaf7483465738b956dc04 Mon Sep 17 00:00:00 2001 From: Wendy Wang Date: Sun, 6 Oct 2024 13:32:12 +0200 Subject: [PATCH 2/4] Fixing comments --- evennia/contrib/game_systems/storage/storage.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/evennia/contrib/game_systems/storage/storage.py b/evennia/contrib/game_systems/storage/storage.py index e362a2f363..a7106710fa 100644 --- a/evennia/contrib/game_systems/storage/storage.py +++ b/evennia/contrib/game_systems/storage/storage.py @@ -168,10 +168,9 @@ class CmdStorage(MuxCommand): caller.msg(f"This is now a storage location with id: {new_storage_id}.") -# CmdSet for easily install all commands class StorageCmdSet(CmdSet): """ - The git command. + CmdSet for all storage-related commands """ def at_cmdset_creation(self): From 7bf491a517f1596df4e0d8ac77fffb9656e26b0b Mon Sep 17 00:00:00 2001 From: Wendy Wang Date: Sun, 6 Oct 2024 13:36:53 +0200 Subject: [PATCH 3/4] Added storage tests --- evennia/contrib/game_systems/storage/tests.py | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/evennia/contrib/game_systems/storage/tests.py b/evennia/contrib/game_systems/storage/tests.py index 65fe763bbf..13489d0908 100644 --- a/evennia/contrib/game_systems/storage/tests.py +++ b/evennia/contrib/game_systems/storage/tests.py @@ -44,7 +44,7 @@ class TestStorage(BaseEvenniaCommandTest): caller=self.char1, ) - def test_store_retrieve_while_not_in_storage(self): + def test_store_retrieve_while_not_in_storeroom(self): self.char2.location = self.char1 self.call(storage.CmdStore(), "obj", "You cannot store anything here.", caller=self.char2) self.call( @@ -94,7 +94,6 @@ class TestStorage(BaseEvenniaCommandTest): f"You store {self.obj1.get_display_name(self.char1)} here.", caller=self.char1, ) - self.assertEqual(self.obj1.location, None) self.call( storage.CmdStorage(), @@ -103,3 +102,21 @@ class TestStorage(BaseEvenniaCommandTest): caller=self.char1, ) self.assertEqual(self.obj1.location, self.room1) + self.call( + storage.CmdStorage(), + "", + f"This is now a storage location with id: {self.room1.id}.", + caller=self.char1, + ) + self.call( + storage.CmdStorage(), + "/delete", + "You remove the storage capabilities of the room.", + caller=self.char1, + ) + self.call( + storage.CmdStorage(), + "/shared", + f"This is now a storage location with id: shared_{self.room1.id}.", + caller=self.char1, + ) From 238a0999d9a604253a00462a98fcd22fb80c72d3 Mon Sep 17 00:00:00 2001 From: Wendy Wang Date: Tue, 8 Oct 2024 22:47:49 +0200 Subject: [PATCH 4/4] Fixes to contrib --- .../contrib/game_systems/storage/__init__.py | 2 +- .../contrib/game_systems/storage/storage.py | 18 ++++++++++++++---- evennia/contrib/game_systems/storage/tests.py | 3 +-- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/evennia/contrib/game_systems/storage/__init__.py b/evennia/contrib/game_systems/storage/__init__.py index 3eaa1043f5..335ddff65a 100644 --- a/evennia/contrib/game_systems/storage/__init__.py +++ b/evennia/contrib/game_systems/storage/__init__.py @@ -1,5 +1,5 @@ """ -Item storage integration - helpme 2022 +Item storage integration - helpme 2024 """ from .storage import StorageCmdSet # noqa diff --git a/evennia/contrib/game_systems/storage/storage.py b/evennia/contrib/game_systems/storage/storage.py index a7106710fa..fe97ecee79 100644 --- a/evennia/contrib/game_systems/storage/storage.py +++ b/evennia/contrib/game_systems/storage/storage.py @@ -3,6 +3,8 @@ from evennia.utils import list_to_string from evennia.utils.search import search_object_by_tag from evennia.commands.default.muxcommand import MuxCommand +SHARED_TAG_PREFIX = "shared" + class StorageCommand(MuxCommand): """ @@ -13,20 +15,26 @@ class StorageCommand(MuxCommand): """ Check if the current location is tagged as a storage location Every stored object is tagged on storage, and untagged on retrieval + + Returns: + bool: True if the command is to be stopped here """ - success = super().at_pre_cmd() + if super().at_pre_cmd(): + return True + self.storage_location_id = self.caller.location.tags.get(category="storage_location") if not self.storage_location_id: self.caller.msg(f"You cannot {self.cmdstring} anything here.") return True self.object_tag = ( - "shared" if self.storage_location_id.startswith("shared_") else self.caller.pk + SHARED_TAG_PREFIX + if self.storage_location_id.startswith(SHARED_TAG_PREFIX) + else self.caller.pk ) self.currently_stored = search_object_by_tag( self.object_tag, category=self.storage_location_id ) - return success class CmdStore(StorageCommand): @@ -163,7 +171,9 @@ class CmdStorage(MuxCommand): caller.msg("This is already a storage location: |wstorage/delete|n to remove the tag.") return - new_storage_id = f"{'shared_' if 'shared' in self.switches else ''}{storage_id}" + new_storage_id = ( + f"{SHARED_TAG_PREFIX if SHARED_TAG_PREFIX in self.switches else ''}{storage_id}" + ) location.tags.add(new_storage_id, category="storage_location") caller.msg(f"This is now a storage location with id: {new_storage_id}.") diff --git a/evennia/contrib/game_systems/storage/tests.py b/evennia/contrib/game_systems/storage/tests.py index 13489d0908..bb794007d7 100644 --- a/evennia/contrib/game_systems/storage/tests.py +++ b/evennia/contrib/game_systems/storage/tests.py @@ -7,7 +7,6 @@ from . import storage class TestStorage(BaseEvenniaCommandTest): def setUp(self): super().setUp() - self.char1.location = self.room1 self.obj1.location = self.char1 self.room1.tags.add("storage_1", "storage_location") self.room2.tags.add("shared_storage_2", "storage_location") @@ -117,6 +116,6 @@ class TestStorage(BaseEvenniaCommandTest): self.call( storage.CmdStorage(), "/shared", - f"This is now a storage location with id: shared_{self.room1.id}.", + f"This is now a storage location with id: shared{self.room1.id}.", caller=self.char1, )