Merge pull request #3634 from Machine-Garden-MUD/master

Contrib - Item Storage
This commit is contained in:
Griatch 2024-10-08 23:16:04 +02:00 committed by GitHub
commit 471406bbe4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 353 additions and 0 deletions

View file

@ -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.

View file

@ -0,0 +1,5 @@
"""
Item storage integration - helpme 2024
"""
from .storage import StorageCmdSet # noqa

View file

@ -0,0 +1,190 @@
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
SHARED_TAG_PREFIX = "shared"
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
Returns:
bool: True if the command is to be stopped here
"""
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_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
)
class CmdStore(StorageCommand):
"""
Store something in a storage location.
Usage:
store <obj>
"""
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 <obj>
"""
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_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}.")
class StorageCmdSet(CmdSet):
"""
CmdSet for all storage-related commands
"""
def at_cmdset_creation(self):
self.add(CmdStore)
self.add(CmdRetrieve)
self.add(CmdList)
self.add(CmdStorage)

View file

@ -0,0 +1,121 @@
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.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_storeroom(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)
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,
)