diff --git a/CHANGELOG.md b/CHANGELOG.md index 8d3e9e4222..948e2cfc14 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -111,6 +111,19 @@ Web/Django standard initiative (@strikaco) +### Contribs +- The `extended_room` contrib saw some backwards-incompatible refactoring: + + All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now + it's `CmdExtendedRoomLook` etc. + + The `detail` command was broken out of the `desc` command and is now a new, stand-alone command + `CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail + command works in the tutorial-world. + + The `detail` command now also supports deleting details (like the tutorial-world version). + + The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way + to install the extended-room contrib. + + + ## Evennia 0.8 (2018) ### Requirements @@ -222,6 +235,15 @@ Web/Django standard initiative (@strikaco) - `tb_items` - Extends `tb_equip` with item use with conditions/status effects. - `tb_magic` - Extends `tb_equip` with spellcasting. - `tb_range` - Adds system for abstract positioning and movement. + - The `extended_room` contrib saw some backwards-incompatible refactoring: + - All commands now begin with `CmdExtendedRoom`. So before it was `CmdExtendedLook`, now + it's `CmdExtendedRoomLook` etc. + - The `detail` command was broken out of the `desc` command and is now a new, stand-alone command + `CmdExtendedRoomDetail`. This was done to make things easier to extend and to mimic how the detail + command works in the tutorial-world. + - The `detail` command now also supports deleting details (like the tutorial-world version). + - The new `ExtendedRoomCmdSet` includes all the extended-room commands and is now the recommended way + to install the extended-room contrib. - Updates and some cleanup of existing contribs. diff --git a/evennia/commands/default/building.py b/evennia/commands/default/building.py index 19c2b6f3a1..6adc59fce1 100644 --- a/evennia/commands/default/building.py +++ b/evennia/commands/default/building.py @@ -1952,8 +1952,8 @@ class CmdLock(ObjManipCommand): caller.msg("You need 'control' access to change this type of lock.") return - if not has_control_access or not obj.access(caller, "edit"): - caller.msg("You need 'edit' access to view or delete lock on this object.") + if not (has_control_access or obj.access(caller, "edit")): + caller.msg("You are not allowed to do that.") return lockdef = obj.locks.get(access_type) diff --git a/evennia/contrib/extended_room.py b/evennia/contrib/extended_room.py index a18b6acdf4..8f33e5dd5b 100644 --- a/evennia/contrib/extended_room.py +++ b/evennia/contrib/extended_room.py @@ -1,7 +1,7 @@ """ Extended Room -Evennia Contribution - Griatch 2012 +Evennia Contribution - Griatch 2012, vincent-lg 2019 This is an extended Room typeclass for Evennia. It is supported by an extended `Look` command and an extended `desc` command, also @@ -45,25 +45,41 @@ at, without there having to be a database object created for it. The Details are simply stored in a dictionary on the room and if the look command cannot find an object match for a `look ` command it will also look through the available details at the current location -if applicable. An extended `desc` command is used to set details. +if applicable. The `@detail` command is used to change details. 4) Extra commands - CmdExtendedLook - look command supporting room details - CmdExtendedDesc - desc command allowing to add seasonal descs and details, + CmdExtendedRoomLook - look command supporting room details + CmdExtendedRoomDesc - desc command allowing to add seasonal descs, + CmdExtendedRoomDetail - command allowing to manipulate details in this room as well as listing them - CmdGameTime - A simple `time` command, displaying the current + CmdExtendedRoomGameTime - A simple `time` command, displaying the current time and season. Installation/testing: -1) Add `CmdExtendedLook`, `CmdExtendedDesc` and `CmdGameTime` to the default `cmdset` - (see Wiki for how to do this). -2) `@dig` a room of type `contrib.extended_room.ExtendedRoom` (or make it the - default room type) -3) Use `desc` and `detail` to customize the room, then play around! +Adding the `ExtendedRoomCmdset` to the default character cmdset will add all +new commands for use. + +In more detail, in mygame/commands/default_cmdsets.py: + +``` +... +from evennia.contrib import extended_room # <-new + +class CharacterCmdset(default_cmds.Character_CmdSet): + ... + def at_cmdset_creation(self): + ... + self.add(extended_room.ExtendedRoomCmdSet) # <-new + +``` + +Then reload to make the bew commands available. Note that they only work +on rooms with the typeclass `ExtendedRoom`. Create new rooms with the right +typeclass or use the `typeclass` command to swap existing rooms. """ @@ -75,6 +91,7 @@ from evennia import DefaultRoom from evennia import gametime from evennia import default_cmds from evennia import utils +from evennia import CmdSet # error return function, needed by Extended Look command _AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) @@ -213,6 +230,42 @@ class ExtendedRoom(DefaultRoom): return detail return None + def set_detail(self, detailkey, description): + """ + This sets a new detail, using an Attribute "details". + + Args: + detailkey (str): The detail identifier to add (for + aliases you need to add multiple keys to the + same description). Case-insensitive. + description (str): The text to return when looking + at the given detailkey. + + """ + if self.db.details: + self.db.details[detailkey.lower()] = description + else: + self.db.details = {detailkey.lower(): description} + + def del_detail(self, detailkey, description): + """ + Delete a detail. + + The description is ignored. + + Args: + detailkey (str): the detail to remove (case-insensitive). + description (str, ignored): the description. + + The description is only included for compliance but is completely + ignored. Note that this method doesn't raise any exception if + the detail doesn't exist in this room. + + """ + if self.db.details and detailkey.lower() in self.db.details: + del self.db.details[detailkey.lower()] + + def return_appearance(self, looker, **kwargs): """ This is called when e.g. the look command wants to retrieve @@ -268,7 +321,7 @@ class ExtendedRoom(DefaultRoom): # Custom Look command supporting Room details. Add this to # the Default cmdset to use. -class CmdExtendedLook(default_cmds.CmdLook): +class CmdExtendedRoomLook(default_cmds.CmdLook): """ look @@ -329,14 +382,12 @@ class CmdExtendedLook(default_cmds.CmdLook): # Custom build commands for setting seasonal descriptions # and detailing extended rooms. -class CmdExtendedDesc(default_cmds.CmdDesc): +class CmdExtendedRoomDesc(default_cmds.CmdDesc): """ `desc` - describe an object or room. Usage: desc[/switch] [ =] - detail[/del] [ = ] - Switches for `desc`: spring - set description for in current room. @@ -344,15 +395,9 @@ class CmdExtendedDesc(default_cmds.CmdDesc): autumn winter - Switch for `detail`: - del - delete a named detail. - Sets the "desc" attribute on an object. If an object is not given, describe the current room. - The alias `detail` allows to assign a "detail" (a non-object - target for the `look` command) to the current room (only). - You can also embed special time markers in your room description, like this: ``` @@ -362,11 +407,11 @@ class CmdExtendedDesc(default_cmds.CmdDesc): Text marked this way will only display when the server is truly at the given timeslot. The available times are night, morning, afternoon and evening. - Note that `detail`, seasons and time-of-day slots only work on rooms in this + Note that seasons and time-of-day slots only work on rooms in this version of the `desc` command. """ - aliases = ["describe", "detail"] + aliases = ["describe"] switch_options = () # Inherits from default_cmds.CmdDesc, but unused here def reset_times(self, obj): @@ -378,97 +423,121 @@ class CmdExtendedDesc(default_cmds.CmdDesc): """Define extended command""" caller = self.caller location = caller.location - if self.cmdname == 'detail': - # switch to detailing mode. This operates only on current location + if not self.args: + if location: + string = "|wDescriptions on %s|n:\n" % location.key + string += " |wspring:|n %s\n" % location.db.spring_desc + string += " |wsummer:|n %s\n" % location.db.summer_desc + string += " |wautumn:|n %s\n" % location.db.autumn_desc + string += " |wwinter:|n %s\n" % location.db.winter_desc + string += " |wgeneral:|n %s" % location.db.general_desc + caller.msg(string) + return + if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"): + # a seasonal switch was given + if self.rhs: + caller.msg("Seasonal descs only work with rooms, not objects.") + return + switch = self.switches[0] if not location: - caller.msg("No location to detail!") + caller.msg("No location was found!") return - if location.db.details is None: - caller.msg("|rThis location does not support details.|n") - return - if self.switches and self.switches[0] in 'del': - # removing a detail. - if self.lhs in location.db.details: - del location.db.details[self.lhs] - caller.msg("Detail %s deleted, if it existed." % self.lhs) - self.reset_times(location) - return - if not self.args: - # No args given. Return all details on location - string = "|wDetails on %s|n:" % location - details = "\n".join(" |w%s|n: %s" - % (key, utils.crop(text)) for key, text in location.db.details.items()) - caller.msg("%s\n%s" % (string, details) if details else "%s None." % string) - return - if not self.rhs: - # no '=' used - list content of given detail - if self.args in location.db.details: - string = "|wDetail '%s' on %s:\n|n" % (self.args, location) - string += str(location.db.details[self.args]) - caller.msg(string) - else: - caller.msg("Detail '%s' not found." % self.args) - return - # setting a detail - location.db.details[self.lhs] = self.rhs - caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs)) + if switch == 'spring': + location.db.spring_desc = self.args + elif switch == 'summer': + location.db.summer_desc = self.args + elif switch == 'autumn': + location.db.autumn_desc = self.args + elif switch == 'winter': + location.db.winter_desc = self.args + # clear flag to force an update self.reset_times(location) - return + caller.msg("Seasonal description was set on %s." % location.key) else: - # we are doing a desc call - if not self.args: - if location: - string = "|wDescriptions on %s|n:\n" % location.key - string += " |wspring:|n %s\n" % location.db.spring_desc - string += " |wsummer:|n %s\n" % location.db.summer_desc - string += " |wautumn:|n %s\n" % location.db.autumn_desc - string += " |wwinter:|n %s\n" % location.db.winter_desc - string += " |wgeneral:|n %s" % location.db.general_desc - caller.msg(string) + # No seasonal desc set, maybe this is not an extended room + if self.rhs: + text = self.rhs + obj = caller.search(self.lhs) + if not obj: return - if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"): - # a seasonal switch was given - if self.rhs: - caller.msg("Seasonal descs only work with rooms, not objects.") - return - switch = self.switches[0] - if not location: - caller.msg("No location was found!") - return - if switch == 'spring': - location.db.spring_desc = self.args - elif switch == 'summer': - location.db.summer_desc = self.args - elif switch == 'autumn': - location.db.autumn_desc = self.args - elif switch == 'winter': - location.db.winter_desc = self.args - # clear flag to force an update - self.reset_times(location) - caller.msg("Seasonal description was set on %s." % location.key) else: - # No seasonal desc set, maybe this is not an extended room - if self.rhs: - text = self.rhs - obj = caller.search(self.lhs) - if not obj: - return - else: - text = self.args - obj = location - obj.db.desc = text # a compatibility fallback - if obj.attributes.has("general_desc"): - obj.db.general_desc = text - self.reset_times(obj) - caller.msg("General description was set on %s." % obj.key) - else: - # this is not an ExtendedRoom. - caller.msg("The description was set on %s." % obj.key) + text = self.args + obj = location + obj.db.desc = text # a compatibility fallback + if obj.attributes.has("general_desc"): + obj.db.general_desc = text + self.reset_times(obj) + caller.msg("General description was set on %s." % obj.key) + else: + # this is not an ExtendedRoom. + caller.msg("The description was set on %s." % obj.key) + + +class CmdExtendedRoomDetail(default_cmds.MuxCommand): + + """ + sets a detail on a room + + Usage: + @detail[/del] [= ] + @detail ;;... = description + + Example: + @detail + @detail walls = The walls are covered in ... + @detail castle;ruin;tower = The distant ruin ... + @detail/del wall + @detail/del castle;ruin;tower + + This command allows to show the current room details if you enter it + without any argument. Otherwise, sets or deletes a detail on the current + room, if this room supports details like an extended room. To add new + detail, just use the @detail command, specifying the key, an equal sign + and the description. You can assign the same description to several + details using the alias syntax (replace key by alias1;alias2;alias3;...). + To remove one or several details, use the @detail/del switch. + + """ + key = "@detail" + locks = "cmd:perm(Builder)" + help_category = "Building" + + def func(self): + location = self.caller.location + if not self.args: + details = location.db.details + if not details: + self.msg("|rThe room {} doesn't have any detail set.|n".format(location)) + else: + details = sorted(["|y{}|n: {}".format(key, desc) for key, desc in details.items()]) + self.msg("Details on Room:\n" + "\n".join(details)) + return + + if not self.rhs and "del" not in self.switches: + detail = location.return_detail(self.lhs) + if detail: + self.msg("Detail '|y{}|n' on Room:\n{}".format(self.lhs, detail)) + else: + self.msg("Detail '{}' not found.".format(self.lhs)) + return + + method = "set_detail" if "del" not in self.switches else "del_detail" + if not hasattr(location, method): + self.caller.msg("Details cannot be set on %s." % location) + return + for key in self.lhs.split(";"): + # loop over all aliases, if any (if not, this will just be + # the one key to loop over) + getattr(location, method)(key, self.rhs) + if "del" in self.switches: + self.caller.msg("Detail %s deleted, if it existed." % self.lhs) + else: + self.caller.msg("Detail set '%s': '%s'" % (self.lhs, self.rhs)) # Simple command to view the current time and season -class CmdGameTime(default_cmds.MuxCommand): +class CmdExtendedRoomGameTime(default_cmds.MuxCommand): """ Check the game time @@ -492,3 +561,17 @@ class CmdGameTime(default_cmds.MuxCommand): if season == "autumn": prep = "an" self.caller.msg("It's %s %s day, in the %s." % (prep, season, timeslot)) + + +# CmdSet for easily install all commands + +class ExtendedRoomCmdSet(CmdSet): + """ + Groups the extended-room commands. + + """ + def at_cmdset_creation(self): + self.add(CmdExtendedRoomLook) + self.add(CmdExtendedRoomDesc) + self.add(CmdExtendedRoomDetail) + self.add(CmdExtendedRoomGameTime) diff --git a/evennia/contrib/gendersub.py b/evennia/contrib/gendersub.py index 2aa4dc92a5..f4bdefbd93 100644 --- a/evennia/contrib/gendersub.py +++ b/evennia/contrib/gendersub.py @@ -64,7 +64,7 @@ class SetGender(Command): """ key = "@gender" - alias = "@sex" + aliases = "@sex" locks = "call:all()" def func(self): diff --git a/evennia/contrib/tests.py b/evennia/contrib/tests.py index 9fe28e17f7..f76581a757 100644 --- a/evennia/contrib/tests.py +++ b/evennia/contrib/tests.py @@ -252,21 +252,31 @@ class TestExtendedRoom(CommandTest): self.assertEqual(self.DETAIL_DESC, self.room1.return_detail("testdetail")) def test_cmdextendedlook(self): - self.call(extended_room.CmdExtendedLook(), "here", "Room(#1)\n%s" % self.SPRING_DESC) - self.call(extended_room.CmdExtendedLook(), "testdetail", self.DETAIL_DESC) - self.call(extended_room.CmdExtendedLook(), "nonexistent", "Could not find 'nonexistent'.") + self.call(extended_room.CmdExtendedRoomLook(), "here", "Room(#1)\n%s" % self.SPRING_DESC) + self.call(extended_room.CmdExtendedRoomLook(), "testdetail", self.DETAIL_DESC) + self.call(extended_room.CmdExtendedRoomLook(), "nonexistent", "Could not find 'nonexistent'.") - def test_cmdextendeddesc(self): - self.call(extended_room.CmdExtendedDesc(), "", "Details on Room", cmdstring="detail") - self.call(extended_room.CmdExtendedDesc(), "thingie = newdetail with spaces", - "Set Detail thingie to 'newdetail with spaces'.", cmdstring="detail") - self.call(extended_room.CmdExtendedDesc(), "thingie", "Detail 'thingie' on Room:\n", cmdstring="detail") - self.call(extended_room.CmdExtendedDesc(), "/del thingie", "Detail thingie deleted, if it existed.", cmdstring="detail") - self.call(extended_room.CmdExtendedDesc(), "thingie", "Detail 'thingie' not found.", cmdstring="detail") - self.call(extended_room.CmdExtendedDesc(), "", "Descriptions on Room:") + def test_cmdsetdetail(self): + self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") + self.call(extended_room.CmdExtendedRoomDetail(), "thingie = newdetail with spaces", + "Detail set 'thingie': 'newdetail with spaces'") + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "/del thingie", "Detail thingie deleted, if it existed.", cmdstring="detail") + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' not found.") + + # Test with aliases + self.call(extended_room.CmdExtendedRoomDetail(), "", "Details on Room") + self.call(extended_room.CmdExtendedRoomDetail(), "thingie;other;stuff = newdetail with spaces", + "Detail set 'thingie;other;stuff': 'newdetail with spaces'") + self.call(extended_room.CmdExtendedRoomDetail(), "thingie", "Detail 'thingie' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' on Room:\n") + self.call(extended_room.CmdExtendedRoomDetail(), "/del other;stuff", "Detail other;stuff deleted, if it existed.") + self.call(extended_room.CmdExtendedRoomDetail(), "other", "Detail 'other' not found.") + self.call(extended_room.CmdExtendedRoomDetail(), "stuff", "Detail 'stuff' not found.") def test_cmdgametime(self): - self.call(extended_room.CmdGameTime(), "", "It's a spring day, in the evening.") + self.call(extended_room.CmdExtendedRoomGameTime(), "", "It's a spring day, in the evening.") # Test the contrib barter system