Make numbered_names use get_display_name; make dbref display separate method

This commit is contained in:
Griatch 2024-03-09 20:02:18 +01:00
parent d893cfd46e
commit cbe3d4c738
14 changed files with 242 additions and 122 deletions

View file

@ -2,16 +2,30 @@
## Evennia Main branch ## Evennia Main branch
- Feature: *Backwards incompatible*: `DefaultObject.get_numbered_name` now gets object's
name via `.get_display_name` for better compatibility with recog systems.
- Feature: *Backwards incompatible*: Removed the (#dbref) display from
`DefaultObject.get_display_name`, instead using new `.get_extra_display_name_info`
method for getting this info. The Object's display template was extended for
optionally adding this information. This makes showing extra object info to
admins an explicit action and opens up `get_display_name` for general use.
- Feature: Add `ON_DEMAND_HANDLER.set_dt(key, category, dt)` and - Feature: Add `ON_DEMAND_HANDLER.set_dt(key, category, dt)` and
`.set_stage(key, category, stage)` to allow manual tweaking of task timings, `.set_stage(key, category, stage)` to allow manual tweaking of task timings,
for example for a spell speeding a plant's growth (Griatch) for example for a spell speeding a plant's growth (Griatch)
- Feature: Add `use_assertequal` kwarg to the `EvenniaCommandTestMixin` testing - Feature: Add `use_assertequal` kwarg to the `EvenniaCommandTestMixin` testing
class; this uses django's `assertEqual` over the default more lenient checker, class; this uses django's `assertEqual` over the default more lenient checker,
which can be useful for testing table whitespace (Griatch) which can be useful for testing table whitespace (Griatch)
- Feature: New `utils.group_objects_by_key_and_desc` for grouping a list of
objects based on the visible key and desc. Useful for inventory listings (Griatch)
- Feature: Add `DefaultObject.get_numbered_name` `return_string` bool kwarg, for only
returning singular/plural based on count instead of a tuple with both (Griatch)
- Fix: `DefaultObject.get_numbered_name` used `.name` instead of
`.get_display_name` which broke recog systems. May lead to object's #dbref
will show for admins in some more places (Griatch)
- [Fix][pull3420]: Refactor Clothing contrib's inventory command align with - [Fix][pull3420]: Refactor Clothing contrib's inventory command align with
Evennia core's version (michaelfaith84, Griatch) Evennia core's version (michaelfaith84, Griatch)
- Fix: Resolve a bug when loading on-demand-handler data from database (Griatch) - Fix: Resolve a bug when loading on-demand-handler data from database (Griatch)
- Doc fixes (iLPdev, Griatch) - Doc fixes (iLPdev, Griatch, CloudKeeper)
[pull3420]: https://github.com/evennia/evennia/pull/3420 [pull3420]: https://github.com/evennia/evennia/pull/3420

View file

@ -3,9 +3,8 @@ General Character commands usually available to all characters
""" """
import re import re
from django.conf import settings
import evennia import evennia
from django.conf import settings
from evennia.typeclasses.attributes import NickTemplateInvalid from evennia.typeclasses.attributes import NickTemplateInvalid
from evennia.utils import utils from evennia.utils import utils
@ -370,11 +369,10 @@ class CmdInventory(COMMAND_DEFAULT_CLASS):
from evennia.utils.ansi import raw as raw_ansi from evennia.utils.ansi import raw as raw_ansi
table = self.styled_table(border="header") table = self.styled_table(border="header")
for item in items: for key, desc, _ in utils.group_objects_by_key_and_desc(items, caller=self.caller):
singular, _ = item.get_numbered_name(1, self.caller)
table.add_row( table.add_row(
f"|C{singular}|n", f"|C{key}|n",
"{}|n".format(utils.crop(raw_ansi(item.db.desc or ""), width=50) or ""), "{}|n".format(utils.crop(raw_ansi(desc or ""), width=50) or ""),
) )
string = f"|wYou are carrying:\n{table}" string = f"|wYou are carrying:\n{table}"
self.msg(text=(string, {"type": "inventory"})) self.msg(text=(string, {"type": "inventory"}))

View file

@ -14,13 +14,10 @@ main test suite started with
import datetime import datetime
from unittest.mock import MagicMock, Mock, patch from unittest.mock import MagicMock, Mock, patch
import evennia
from anything import Anything from anything import Anything
from django.conf import settings from django.conf import settings
from django.test import override_settings from django.test import override_settings
from parameterized import parameterized
from twisted.internet import task
import evennia
from evennia import ( from evennia import (
DefaultCharacter, DefaultCharacter,
DefaultExit, DefaultExit,
@ -32,14 +29,7 @@ from evennia import (
from evennia.commands import cmdparser from evennia.commands import cmdparser
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command, InterruptCommand from evennia.commands.command import Command, InterruptCommand
from evennia.commands.default import ( from evennia.commands.default import account, admin, batchprocess, building, comms, general
account,
admin,
batchprocess,
building,
comms,
general,
)
from evennia.commands.default import help as help_module from evennia.commands.default import help as help_module
from evennia.commands.default import syscommands, system, unloggedin from evennia.commands.default import syscommands, system, unloggedin
from evennia.commands.default.cmdset_character import CharacterCmdSet from evennia.commands.default.cmdset_character import CharacterCmdSet
@ -48,6 +38,8 @@ from evennia.prototypes import prototypes as protlib
from evennia.utils import create, gametime, utils from evennia.utils import create, gametime, utils
from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest from evennia.utils.test_resources import BaseEvenniaTest, EvenniaCommandTest
from parameterized import parameterized
from twisted.internet import task
# ------------------------------------------------------------ # ------------------------------------------------------------
# Command testing # Command testing
@ -116,13 +108,13 @@ class TestGeneral(BaseEvenniaCommandTest):
self.call(general.CmdNick(), "/list", "Defined Nicks:") self.call(general.CmdNick(), "/list", "Defined Nicks:")
def test_get_and_drop(self): def test_get_and_drop(self):
self.call(general.CmdGet(), "Obj", "You pick up an Obj.") self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdDrop(), "Obj", "You drop an Obj.") self.call(general.CmdDrop(), "Obj", "You drop an Obj")
def test_give(self): def test_give(self):
self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.") self.call(general.CmdGive(), "Obj to Char2", "You aren't carrying Obj.")
self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.") self.call(general.CmdGive(), "Obj = Char2", "You aren't carrying Obj.")
self.call(general.CmdGet(), "Obj", "You pick up an Obj.") self.call(general.CmdGet(), "Obj", "You pick up an Obj")
self.call(general.CmdGive(), "Obj to Char2", "You give") self.call(general.CmdGive(), "Obj to Char2", "You give")
self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2) self.call(general.CmdGive(), "Obj = Char", "You give", caller=self.char2)
@ -569,7 +561,7 @@ class TestAdmin(BaseEvenniaCommandTest):
self.call( self.call(
admin.CmdForce(), admin.CmdForce(),
"Char2=say test", "Char2=say test",
'Char2(#{}) says, "test"|You have forced Char2 to: say test'.format(cid), 'Char2 says, "test"|You have forced Char2 to: say test',
) )
@ -781,17 +773,14 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call(building.CmdExamine(), "*TestAccount") self.call(building.CmdExamine(), "*TestAccount")
def test_set_obj_alias(self): def test_set_obj_alias(self):
oid = self.obj1.id
self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj") self.call(building.CmdSetObjAlias(), "Obj =", "Cleared aliases from Obj")
self.call( self.call(
building.CmdSetObjAlias(), building.CmdSetObjAlias(), "Obj = TestObj1b", "Alias(es) for 'Obj' set to 'testobj1b'."
"Obj = TestObj1b",
"Alias(es) for 'Obj(#{})' set to 'testobj1b'.".format(oid),
) )
self.call(building.CmdSetObjAlias(), "", "Usage: ") self.call(building.CmdSetObjAlias(), "", "Usage: ")
self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.") self.call(building.CmdSetObjAlias(), "NotFound =", "Could not find 'NotFound'.")
self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj(#{}): 'testobj1b'".format(oid)) self.call(building.CmdSetObjAlias(), "Obj", "Aliases for Obj: 'testobj1b'")
self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2") self.call(building.CmdSetObjAlias(), "Obj2 =", "Cleared aliases from Obj2")
self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.") self.call(building.CmdSetObjAlias(), "Obj2 =", "No aliases to clear.")
@ -1228,9 +1217,7 @@ class TestBuilding(BaseEvenniaCommandTest):
def test_desc(self): def test_desc(self):
oid = self.obj2.id oid = self.obj2.id
self.call( self.call(building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2.")
building.CmdDesc(), "Obj2=TestDesc", "The description was set on Obj2(#{}).".format(oid)
)
self.call(building.CmdDesc(), "", "Usage: ") self.call(building.CmdDesc(), "", "Usage: ")
with patch("evennia.commands.default.building.EvEditor") as mock_ed: with patch("evennia.commands.default.building.EvEditor") as mock_ed:
@ -1251,7 +1238,7 @@ class TestBuilding(BaseEvenniaCommandTest):
oid = self.obj2.id oid = self.obj2.id
o2d = self.obj2.db.desc o2d = self.obj2.db.desc
r1d = self.room1.db.desc r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2(#{}).".format(oid)) self.call(building.CmdDesc(), "Obj2=", "The description was set on Obj2.")
assert self.obj2.db.desc == "" and self.obj2.db.desc != o2d assert self.obj2.db.desc == "" and self.obj2.db.desc != o2d
assert self.room1.db.desc == r1d assert self.room1.db.desc == r1d
@ -1260,7 +1247,7 @@ class TestBuilding(BaseEvenniaCommandTest):
rid = self.room1.id rid = self.room1.id
o2d = self.obj2.db.desc o2d = self.obj2.db.desc
r1d = self.room1.db.desc r1d = self.room1.db.desc
self.call(building.CmdDesc(), "Obj2", "The description was set on Room(#{}).".format(rid)) self.call(building.CmdDesc(), "Obj2", "The description was set on Room.")
assert self.obj2.db.desc == o2d assert self.obj2.db.desc == o2d
assert self.room1.db.desc == "Obj2" and self.room1.db.desc != r1d assert self.room1.db.desc == "Obj2" and self.room1.db.desc != r1d
@ -1283,16 +1270,11 @@ class TestBuilding(BaseEvenniaCommandTest):
building.CmdDestroy(), settings.DEFAULT_HOME, "You are trying to delete" building.CmdDestroy(), settings.DEFAULT_HOME, "You are trying to delete"
) # DEFAULT_HOME should not be deleted ) # DEFAULT_HOME should not be deleted
self.char2.location = self.room2 self.char2.location = self.room2
charid = self.char2.id
room1id = self.room1.id
room2id = self.room2.id
self.call( self.call(
building.CmdDestroy(), building.CmdDestroy(),
self.room2.dbref, self.room2.dbref,
"Char2(#{}) arrives to Room(#{}) from Room2(#{}).|Room2 was destroyed.".format( "Char2 arrives to Room from Room2.|Room2 was destroyed.",
charid, room1id, room2id ),
),
)
building.CmdDestroy.confirm = confirm building.CmdDestroy.confirm = confirm
def test_destroy_sequence(self): def test_destroy_sequence(self):
@ -1640,9 +1622,6 @@ class TestBuilding(BaseEvenniaCommandTest):
self.assertFalse(script3.pk) self.assertFalse(script3.pk)
def test_teleport(self): def test_teleport(self):
oid = self.obj1.id
rid = self.room1.id
rid2 = self.room2.id
self.call(building.CmdTeleport(), "", "Usage: ") self.call(building.CmdTeleport(), "", "Usage: ")
self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.") self.call(building.CmdTeleport(), "Obj = Room", "Obj is already at Room.")
self.call( self.call(
@ -1653,9 +1632,7 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdTeleport(), building.CmdTeleport(),
"Obj = Room2", "Obj = Room2",
"Obj(#{}) is leaving Room(#{}), heading for Room2(#{}).|Teleported Obj -> Room2.".format( "Obj is leaving Room, heading for Room2.|Teleported Obj -> Room2.",
oid, rid, rid2
),
) )
self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.") self.call(building.CmdTeleport(), "NotFound = Room", "Could not find 'NotFound'.")
self.call( self.call(
@ -1663,7 +1640,7 @@ class TestBuilding(BaseEvenniaCommandTest):
) )
self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.") self.call(building.CmdTeleport(), "/tonone Obj2", "Teleported Obj2 -> None-location.")
self.call(building.CmdTeleport(), "/quiet Room2", "Room2(#{})".format(rid2)) self.call(building.CmdTeleport(), "/quiet Room2", "Room2")
self.call( self.call(
building.CmdTeleport(), building.CmdTeleport(),
"/t", # /t switch is abbreviated form of /tonone "/t", # /t switch is abbreviated form of /tonone
@ -1777,7 +1754,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSpawn(), building.CmdSpawn(),
"{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', " "{'prototype_key':'GOBLIN', 'typeclass':'evennia.objects.objects.DefaultCharacter', "
"'key':'goblin', 'location':'%s'}" % spawnLoc.dbref, "'key':'goblin', 'location':'%s'}"
% spawnLoc.dbref,
"Spawned goblin", "Spawned goblin",
) )
goblin = get_object(self, "goblin") goblin = get_object(self, "goblin")
@ -1825,7 +1803,8 @@ class TestBuilding(BaseEvenniaCommandTest):
self.call( self.call(
building.CmdSpawn(), building.CmdSpawn(),
"/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo'," "/noloc {'prototype_parent':'TESTBALL', 'key': 'Ball', 'prototype_key': 'foo',"
" 'location':'%s'}" % spawnLoc.dbref, " 'location':'%s'}"
% spawnLoc.dbref,
"Spawned Ball", "Spawned Ball",
) )
ball = get_object(self, "Ball") ball = get_object(self, "Ball")

View file

@ -183,7 +183,7 @@ class TestEvscaperoomCommands(BaseEvenniaCommandTest):
self.call( self.call(
commands.CmdEmote(), commands.CmdEmote(),
"/me smiles to /obj", "/me smiles to /obj",
f"Char(#{self.char1.id}) smiles to Obj(#{self.obj1.id})", f"Char smiles to Obj.",
) )
def test_focus_interaction(self): def test_focus_interaction(self):

View file

@ -77,7 +77,15 @@ from collections import defaultdict
from django.conf import settings from django.conf import settings
from evennia import DefaultCharacter, DefaultObject, default_cmds from evennia import DefaultCharacter, DefaultObject, default_cmds
from evennia.commands.default.muxcommand import MuxCommand from evennia.commands.default.muxcommand import MuxCommand
from evennia.utils import at_search_result, crop, evtable, inherits_from, int2str, iter_to_str from evennia.utils import (
at_search_result,
crop,
evtable,
group_objects_by_key_and_desc,
inherits_from,
int2str,
iter_to_str,
)
from evennia.utils.ansi import raw as raw_ansi from evennia.utils.ansi import raw as raw_ansi
# Options start here. # Options start here.
@ -660,11 +668,10 @@ class CmdInventory(MuxCommand):
carried = [obj for obj in items if not obj.db.worn] carried = [obj for obj in items if not obj.db.worn]
carry_table = self.styled_table(border="header") carry_table = self.styled_table(border="header")
for item in carried: for key, desc, _ in group_objects_by_key_and_desc(carried, caller=self.caller):
singular, _ = item.get_numbered_name(1, self.caller)
carry_table.add_row( carry_table.add_row(
f"{singular}|n", f"{key}|n",
"{}|n".format(crop(raw_ansi(item.db.desc or ""), width=50) or ""), "{}|n".format(crop(raw_ansi(desc or ""), width=50) or ""),
) )
message_list.extend( message_list.extend(
["|wYou are carrying:|n", str(carry_table) if carry_table.nrows > 0 else " Nothing."] ["|wYou are carrying:|n", str(carry_table) if carry_table.nrows > 0 else " Nothing."]
@ -674,18 +681,17 @@ class CmdInventory(MuxCommand):
worn = [obj for obj in items if obj.db.worn] worn = [obj for obj in items if obj.db.worn]
wear_table = self.styled_table(border="header") wear_table = self.styled_table(border="header")
for item in worn: for key, desc, _ in group_objects_by_key_and_desc(worn, caller=self.caller):
singular, _ = item.get_numbered_name(1, self.caller)
wear_table.add_row( wear_table.add_row(
f"{singular}|n", f"{key}|n",
"{}|n".format(crop(raw_ansi(item.db.desc or ""), width=50) or ""), "{}|n".format(crop(raw_ansi(desc or ""), width=50) or ""),
) )
message_list.extend( message_list.extend(
["You are wearing:|n", str(wear_table) if wear_table.nrows > 0 else " Nothing."] ["You are wearing:|n", str(wear_table) if wear_table.nrows > 0 else " Nothing."]
) )
# return the composite message # return the composite message
self.caller.msg("\n".join(message_list)) self.caller.msg(text=("\n".join(message_list), {"type": "inventory"}))
class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet): class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):

View file

@ -19,9 +19,11 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.wearer.location = self.room self.wearer.location = self.room
# Make a test hat # Make a test hat
self.test_hat = create_object(clothing.ContribClothing, key="test hat") self.test_hat = create_object(clothing.ContribClothing, key="test hat")
self.test_hat.db.desc = "A test hat."
self.test_hat.db.clothing_type = "hat" self.test_hat.db.clothing_type = "hat"
# Make a test scarf # Make a test scarf
self.test_scarf = create_object(clothing.ContribClothing, key="test scarf") self.test_scarf = create_object(clothing.ContribClothing, key="test scarf")
self.test_scarf.db.desc = "A test scarf."
self.test_scarf.db.clothing_type = "accessory" self.test_scarf.db.clothing_type = "accessory"
def test_clothingcommands(self): def test_clothingcommands(self):
@ -40,7 +42,10 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.call( self.call(
clothing.CmdInventory(), clothing.CmdInventory(),
"", "",
"You are carrying:\n a test scarf \n a test hat \nYou are wearing:\n Nothing.", (
"You are carrying:\n a test hat A test hat. \n a test scarf A test"
" scarf. \nYou are wearing:\n Nothing."
),
caller=self.wearer, caller=self.wearer,
use_assertequal=True, use_assertequal=True,
) )
@ -71,7 +76,10 @@ class TestClothingCmd(BaseEvenniaCommandTest):
self.call( self.call(
clothing.CmdInventory(), clothing.CmdInventory(),
"", "",
"You are carrying:\n Nothing.\nYou are wearing:\n a test scarf \n a test hat ", (
"You are carrying:\n Nothing.\nYou are wearing:\n a test hat A test hat. \n"
" a test scarf A test scarf. "
),
caller=self.wearer, caller=self.wearer,
use_assertequal=True, use_assertequal=True,
) )

View file

@ -1,5 +1,6 @@
from evennia import create_object from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, BaseEvenniaTest # noqa from evennia.utils.test_resources import BaseEvenniaCommandTest # noqa
from evennia.utils.test_resources import BaseEvenniaTest
from .containers import CmdContainerGet, CmdContainerLook, CmdPut, ContribContainer from .containers import CmdContainerGet, CmdContainerLook, CmdPut, ContribContainer
@ -40,9 +41,17 @@ class TestContainerCmds(BaseEvenniaCommandTest):
# get normally # get normally
self.call(CmdContainerGet(), "Obj", "You pick up an Obj.") self.call(CmdContainerGet(), "Obj", "You pick up an Obj.")
# put in the container # put in the container
self.call(CmdPut(), "obj in box", "You put an Obj in a Box.") self.call(
CmdPut(),
"obj in box",
"You put an Obj in a Box.",
)
# get from the container # get from the container
self.call(CmdContainerGet(), "obj from box", "You get an Obj from a Box.") self.call(
CmdContainerGet(),
"obj from box",
"You get an Obj from a Box.",
)
def test_locked_get_put(self): def test_locked_get_put(self):
# lock container # lock container

View file

@ -6,11 +6,10 @@ Testing of ExtendedRoom contrib
import datetime import datetime
from django.conf import settings from django.conf import settings
from mock import Mock, patch
from parameterized import parameterized
from evennia import create_object from evennia import create_object
from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase from evennia.utils.test_resources import BaseEvenniaCommandTest, EvenniaTestCase
from mock import Mock, patch
from parameterized import parameterized
from . import extended_room from . import extended_room
@ -195,7 +194,7 @@ class TestExtendedRoomCommands(BaseEvenniaCommandTest):
extended_room.CmdExtendedRoomDesc(), extended_room.CmdExtendedRoomDesc(),
"", "",
f""" f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None Room Room Season: autumn. Time: afternoon. States: None
Room state (default) (active): Room state (default) (active):
Base room description. Base room description.
@ -218,7 +217,7 @@ Base room description.
extended_room.CmdExtendedRoomDesc(), extended_room.CmdExtendedRoomDesc(),
"", "",
f""" f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None Room Room Season: autumn. Time: afternoon. States: None
Room state burning: Room state burning:
Burning description. Burning description.
@ -235,8 +234,10 @@ Base room description.
self.call( self.call(
extended_room.CmdExtendedRoomDesc(), extended_room.CmdExtendedRoomDesc(),
"/del/burning/spring", "/del/burning/spring",
"The burning-description was deleted, if it existed.|The spring-description was" (
" deleted, if it existed", "The burning-description was deleted, if it existed.|The spring-description was"
" deleted, if it existed"
),
) )
# add autumn, which should be active # add autumn, which should be active
self.call( self.call(
@ -248,7 +249,7 @@ Base room description.
extended_room.CmdExtendedRoomDesc(), extended_room.CmdExtendedRoomDesc(),
"", "",
f""" f"""
Room Room(#{self.room1.id}) Season: autumn. Time: afternoon. States: None Room Room Season: autumn. Time: afternoon. States: None
Room state autumn (active): Room state autumn (active):
Autumn description. Autumn description.
@ -285,8 +286,8 @@ test: Test detail.
self.call( self.call(
extended_room.CmdExtendedRoomDetail(), extended_room.CmdExtendedRoomDetail(),
"", "",
f""" """
The room Room(#{self.room1.id}) doesn't have any details. The room Room doesn't have any details.
""".strip(), """.strip(),
) )
@ -306,7 +307,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call( self.call(
extended_room.CmdExtendedRoomState(), extended_room.CmdExtendedRoomState(),
"", "",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n None", "Room states (not counting automatic time/season) on Room:\n None",
) )
# add room states # add room states
@ -323,8 +324,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call( self.call(
extended_room.CmdExtendedRoomState(), extended_room.CmdExtendedRoomState(),
"", "",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n " f"Room states (not counting automatic time/season) on Room:\n 'burning' and 'windy'",
"'burning' and 'windy'",
) )
# toggle windy # toggle windy
self.call( self.call(
@ -335,8 +335,7 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call( self.call(
extended_room.CmdExtendedRoomState(), extended_room.CmdExtendedRoomState(),
"", "",
f"Room states (not counting automatic time/season) on Room(#{self.room1.id}):\n " f"Room states (not counting automatic time/season) on Room:\n 'burning'",
"'burning'",
) )
# add a autumn state and make sure we override it # add a autumn state and make sure we override it
self.room1.add_desc("Autumn description.", room_state="autumn") self.room1.add_desc("Autumn description.", room_state="autumn")
@ -387,13 +386,17 @@ The room Room(#{self.room1.id}) doesn't have any details.
self.call( self.call(
extended_room.CmdExtendedRoomLook(), extended_room.CmdExtendedRoomLook(),
"", "",
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is" (
" shining through the trees.", f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees."
),
) )
self.room1.add_room_state("burning") self.room1.add_room_state("burning")
self.call( self.call(
extended_room.CmdExtendedRoomLook(), extended_room.CmdExtendedRoomLook(),
"", "",
f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is" (
" shining through the trees and this place is on fire!", f"Room(#{self.room1.id})\nThis is a nice autumnal forest. The afternoon sun is"
" shining through the trees and this place is on fire!"
),
) )

View file

@ -154,18 +154,12 @@ from string import punctuation
import inflect import inflect
from django.conf import settings from django.conf import settings
from evennia.commands.cmdset import CmdSet from evennia.commands.cmdset import CmdSet
from evennia.commands.command import Command from evennia.commands.command import Command
from evennia.objects.models import ObjectDB from evennia.objects.models import ObjectDB
from evennia.objects.objects import DefaultCharacter, DefaultObject from evennia.objects.objects import DefaultCharacter, DefaultObject
from evennia.utils import ansi, logger from evennia.utils import ansi, logger
from evennia.utils.utils import ( from evennia.utils.utils import iter_to_str, lazy_property, make_iter, variable_from_module
iter_to_str,
lazy_property,
make_iter,
variable_from_module,
)
_INFLECT = inflect.engine() _INFLECT = inflect.engine()
@ -1343,13 +1337,15 @@ class ContribRPObject(DefaultObject):
# in eventual error reporting later (not their keys). Doing # in eventual error reporting later (not their keys). Doing
# it like this e.g. allows for use of the typeclass kwarg # it like this e.g. allows for use of the typeclass kwarg
# limiter. # limiter.
results.extend([obj for obj in search_obj(candidate.key) if obj not in results]) results.extend(
[obj for obj in search_obj(candidate.key, **kwargs) if obj not in results]
)
if not results and is_builder: if not results and is_builder:
# builders get a chance to search only by key+alias # builders get to do a global search by key+alias
results = search_obj(searchdata, candidates=candidates, **kwargs) results = search_obj(searchdata, **kwargs)
else: else:
# global searches / #drefs end up here. Global searches are # global searches with #drefs end up here. Global searches are
# only done in code, so is controlled, #dbrefs are turned off # only done in code, so is controlled, #dbrefs are turned off
# for non-Builders. # for non-Builders.
results = search_obj(searchdata, **kwargs) results = search_obj(searchdata, **kwargs)
@ -1409,10 +1405,6 @@ class ContribRPObject(DefaultObject):
# use own sdesc as a fallback # use own sdesc as a fallback
sdesc = self.sdesc.get() sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def get_display_characters(self, looker, pose=True, **kwargs): def get_display_characters(self, looker, pose=True, **kwargs):
@ -1545,10 +1537,6 @@ class ContribRPCharacter(DefaultCharacter, ContribRPObject):
# use own sdesc as a fallback # use own sdesc as a fallback
sdesc = self.sdesc.get() sdesc = self.sdesc.get()
# add dbref is looker has control access and `noid` is not set
if self.access(looker, access_type="control") and not kwargs.get("noid", False):
sdesc = f"{sdesc}(#{self.id})"
return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc return self.get_posed_sdesc(sdesc) if kwargs.get("pose", False) else sdesc
def at_object_creation(self): def at_object_creation(self):

View file

@ -5,7 +5,6 @@ Tests for RP system
import time import time
from anything import Anything from anything import Anything
from evennia import DefaultObject, create_object, default_cmds from evennia import DefaultObject, create_object, default_cmds
from evennia.commands.default.tests import BaseEvenniaCommandTest from evennia.commands.default.tests import BaseEvenniaCommandTest
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
@ -414,14 +413,14 @@ class TestRPSystemCommands(BaseEvenniaCommandTest):
expected_first_call = [ expected_first_call = [
"More than one match for 'Mushroom' (please narrow target):", "More than one match for 'Mushroom' (please narrow target):",
f" Mushroom({mushroom1.dbref})-1 []", f" Mushroom-1 []",
f" Mushroom({mushroom2.dbref})-2 []", f" Mushroom-2 []",
] ]
self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES self.call(default_cmds.CmdLook(), "Mushroom", "\n".join(expected_first_call)) # PASSES
expected_second_call = f"Mushroom({mushroom1.dbref})\nThe first mushroom is brown." expected_second_call = f"Mushroom(#{mushroom1.id})\nThe first mushroom is brown."
self.call(default_cmds.CmdLook(), "Mushroom-1", expected_second_call) # FAILS self.call(default_cmds.CmdLook(), "Mushroom-1", expected_second_call) # FAILS
expected_third_call = f"Mushroom({mushroom2.dbref})\nThe second mushroom is red." expected_third_call = f"Mushroom(#{mushroom2.id})\nThe second mushroom is red."
self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS self.call(default_cmds.CmdLook(), "Mushroom-2", expected_third_call) # FAILS

View file

@ -19,7 +19,6 @@ from django.conf import settings
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.core.validators import validate_comma_separated_integer_list from django.core.validators import validate_comma_separated_integer_list
from django.db import models from django.db import models
from evennia.objects.manager import ObjectDBManager from evennia.objects.manager import ObjectDBManager
from evennia.typeclasses.models import TypedObject from evennia.typeclasses.models import TypedObject
from evennia.utils import logger from evennia.utils import logger

View file

@ -219,7 +219,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# populated by `return_appearance` # populated by `return_appearance`
appearance_template = """ appearance_template = """
{header} {header}
|c{name}|n |c{name}{extra_name_info}|n
{desc} {desc}
{exits}{characters}{things} {exits}{characters}{things}
{footer} {footer}
@ -316,7 +316,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
"obj.location to move an object here.".format(self.__class__) "obj.location to move an object here.".format(self.__class__)
) )
contents = property(contents_get, contents_set, contents_set) contents = property(contents_get, contents_set, contents_set, contents_set)
@property @property
def exits(self): def exits(self):
@ -827,6 +827,16 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
for session in sessions: for session in sessions:
session.data_out(**kwargs) session.data_out(**kwargs)
def get_contents_unique(self, caller=None):
"""
Get a mapping of contents that are visually unique to the caller, along with
how many of each there are.
Args:
caller (Object, optional): The object to check visibility from. If not given,
the current object will be used.
"""
def for_contents(self, func, exclude=None, **kwargs): def for_contents(self, func, exclude=None, **kwargs):
""" """
Runs a function on every object contained within this one. Runs a function on every object contained within this one.
@ -1436,10 +1446,28 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
and is expected to produce something useful for builders. and is expected to produce something useful for builders.
""" """
if looker and self.locks.check_lockstring(looker, "perm(Builder)"):
return "{}(#{})".format(self.name, self.id)
return self.name return self.name
def get_extra_display_name_info(self, looker=None, **kwargs):
"""
Adds any extra display information to the object's name. By default this is is the
object's dbref in parentheses, if the looker has permission to see it.
Args:
looker (Object): The object looking at this object.
Returns:
str: The dbref of this object, if the looker has permission to see it. Otherwise, an
empty string is returned.
Notes:
By default, this becomes a string (#dbref) attached to the object's name.
"""
if looker and self.locks.check_lockstring(looker, "perm(Builder)"):
return f"(#{self.id})"
return ""
def get_numbered_name(self, count, looker, **kwargs): def get_numbered_name(self, count, looker, **kwargs):
""" """
Return the numbered (singular, plural) forms of this object's key. This is by default called Return the numbered (singular, plural) forms of this object's key. This is by default called
@ -1453,8 +1481,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
looker (Object): Onlooker. Not used by default. looker (Object): Onlooker. Not used by default.
Keyword Args: Keyword Args:
key (str): Optional key to pluralize. If not given, the object's `.name` property is key (str): Optional key to pluralize. If not given, the object's `.get_display_name()`
used. method is used.
return_string (bool): If `True`, return only the singular form if count is 0,1 or
the plural form otherwise. If `False` (default), return both forms as a tuple.
Returns: Returns:
tuple: This is a tuple `(str, str)` with the singular and plural forms of the key tuple: This is a tuple `(str, str)` with the singular and plural forms of the key
@ -1466,7 +1496,8 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
""" """
plural_category = "plural_key" plural_category = "plural_key"
key = kwargs.get("key", self.name) key = kwargs.get("key", self.get_display_name(looker))
raw_key = self.name
key = ansi.ANSIString(key) # this is needed to allow inflection of colored names key = ansi.ANSIString(key) # this is needed to allow inflection of colored names
try: try:
plural = _INFLECT.plural(key, count) plural = _INFLECT.plural(key, count)
@ -1482,6 +1513,10 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
# save the singular form as an alias here too so we can display "an egg" and also # save the singular form as an alias here too so we can display "an egg" and also
# look at 'an egg'. # look at 'an egg'.
self.aliases.add(singular, category=plural_category) self.aliases.add(singular, category=plural_category)
if kwargs.get("return_string"):
return singular if count in (0, 1) else plural
return singular, plural return singular, plural
def get_display_header(self, looker, **kwargs): def get_display_header(self, looker, **kwargs):
@ -1645,6 +1680,7 @@ class DefaultObject(ObjectDB, metaclass=TypeclassBase):
return self.format_appearance( return self.format_appearance(
self.appearance_template.format( self.appearance_template.format(
name=self.get_display_name(looker, **kwargs), name=self.get_display_name(looker, **kwargs),
extra_name_info=self.get_extra_display_name_info(looker, **kwargs),
desc=self.get_display_desc(looker, **kwargs), desc=self.get_display_desc(looker, **kwargs),
header=self.get_display_header(looker, **kwargs), header=self.get_display_header(looker, **kwargs),
footer=self.get_display_footer(looker, **kwargs), footer=self.get_display_footer(looker, **kwargs),

View file

@ -11,12 +11,11 @@ from datetime import datetime, timedelta
import mock import mock
from django.test import TestCase from django.test import TestCase
from parameterized import parameterized
from twisted.internet import task
from evennia.utils import utils from evennia.utils import utils
from evennia.utils.ansi import ANSIString from evennia.utils.ansi import ANSIString
from evennia.utils.test_resources import BaseEvenniaTest from evennia.utils.test_resources import BaseEvenniaTest
from parameterized import parameterized
from twisted.internet import task
class TestIsIter(TestCase): class TestIsIter(TestCase):
@ -775,6 +774,54 @@ class TestJustify(TestCase):
self.assertIn(ANSI_RED, str(result)) self.assertIn(ANSI_RED, str(result))
class TestGroupObjectsByKeyAndDesc(TestCase):
"""
Test the utils.group_objects_by_key_and_desc function.
"""
class MockObject:
def __init__(self, key, desc):
self.key = key
self.desc = desc
def get_display_name(self, looker, **kwargs):
return self.key + f" (looker: {looker.key})"
def get_display_desc(self, looker, **kwargs):
return self.desc + f" (looker: {looker.key})"
def get_numbered_name(self, count, looker, **kwargs):
return f"{count} {self.key} (looker: {looker.key})"
def __repr__(self):
return f"MockObject({self.key}, {self.desc})"
def test_group_by_key_and_desc(self):
ma1 = self.MockObject("itemA", "descA")
ma2 = self.MockObject("itemA", "descA")
ma3 = self.MockObject("itemA", "descA")
ma4 = self.MockObject("itemA", "descA")
mb1 = self.MockObject("itemB", "descB")
mb2 = self.MockObject("itemB", "descB")
mb3 = self.MockObject("itemB", "descB")
me = self.MockObject("Looker", "DescLooker")
result = utils.group_objects_by_key_and_desc([ma1, ma2, ma3, ma4, mb1, mb2, mb3], caller=me)
self.assertEqual(
list(result),
[
("4 itemA (looker: Looker)", "descA (looker: Looker)", [ma1, ma2, ma3, ma4]),
("3 itemB (looker: Looker)", "descB (looker: Looker)", [mb1, mb2, mb3]),
],
)
# Create a list of objects
class TestMatchIP(TestCase): class TestMatchIP(TestCase):
""" """
test utils.match_ip test utils.match_ip

View file

@ -28,6 +28,7 @@ from os.path import join as osjoin
from string import punctuation from string import punctuation
from unicodedata import east_asian_width from unicodedata import east_asian_width
import evennia
from django.apps import apps from django.apps import apps
from django.conf import settings from django.conf import settings
from django.core.exceptions import ValidationError as DjangoValidationError from django.core.exceptions import ValidationError as DjangoValidationError
@ -35,14 +36,12 @@ from django.core.validators import validate_email as django_validate_email
from django.utils import timezone from django.utils import timezone
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.utils.translation import gettext as _ from django.utils.translation import gettext as _
from evennia.utils import logger
from simpleeval import simple_eval from simpleeval import simple_eval
from twisted.internet import reactor, threads from twisted.internet import reactor, threads
from twisted.internet.defer import returnValue # noqa - used as import target from twisted.internet.defer import returnValue # noqa - used as import target
from twisted.internet.task import deferLater from twisted.internet.task import deferLater
import evennia
from evennia.utils import logger
_MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE _MULTIMATCH_TEMPLATE = settings.SEARCH_MULTIMATCH_TEMPLATE
_EVENNIA_DIR = settings.EVENNIA_DIR _EVENNIA_DIR = settings.EVENNIA_DIR
_GAME_DIR = settings.GAME_DIR _GAME_DIR = settings.GAME_DIR
@ -1767,6 +1766,41 @@ def string_partial_matching(alternatives, inp, ret_index=True):
return [] return []
def group_objects_by_key_and_desc(objects, caller=None, **kwargs):
"""
Groups a list of objects by their key and description. This is used to group
visibly identical objects together, for example for inventory listings.
Args:
objects (list): A list of objects to group. These must be DefaultObject.
caller (Object, optional): The object looking at the objects, used to get the
description and key of each object.
**kwargs: Passed into each object's `get_display_name/desc` methods.
Returns:
iterable: An iterable of tuples, where each tuple is on the form
`(numbered_name, description, [objects])`.
"""
key_descs = defaultdict(list)
return_string = kwargs.pop("return_string", True)
for obj in objects:
key_descs[
(obj.get_display_name(caller, **kwargs), obj.get_display_desc(caller, **kwargs))
].append(obj)
return (
(
objs[0].get_numbered_name(len(objs), caller, return_string=return_string, **kwargs),
desc,
objs,
)
for (key, desc), objs in sorted(key_descs.items(), key=lambda tup: tup[0][0])
)
def format_table(table, extra_space=1): def format_table(table, extra_space=1):
""" """
Format a 2D array of strings into a multi-column table. Format a 2D array of strings into a multi-column table.