Note

You are reading an old version of the Evennia documentation. The latest version is here

.

# Source code for evennia.contrib.grid.extended_room.extended_room

    """
    Extended Room

    Evennia Contribution - Griatch 2012, vincent-lg 2019, Griatch 2023

    This is an extended Room typeclass for Evennia, supporting descriptions that vary
    by season, time-of-day or arbitrary states (like burning). It has details, embedded
    state tags, support for repeating random messages as well as a few extra commands.

    - The room description can be set to change depending on the season or time of day.
    - Parts of the room description can be set to change depending on arbitrary states (like burning).
    - Details can be added to the room, which can be looked at like objects.
    - Alternative text sections can be added to the room description, which will only show if
      the room is in a given state.
    - Random messages can be set to repeat at a given rate.

    Installation/testing:

    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   # <---

    class CharacterCmdset(default_cmds.Character_CmdSet):
        ...
        def at_cmdset_creation(self):
            ...
            self.add(extended_room.ExtendedRoomCmdSet)  # <---

    ```

    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.

    """

    import datetime
    import random
    import re
    from collections import deque

    from django.conf import settings
    from django.db.models import Q
    from evennia import (
        CmdSet,
        DefaultRoom,
        EvEditor,
        FuncParser,
        InterruptCommand,
        default_cmds,
        gametime,
        utils,
    )
    from evennia.typeclasses.attributes import AttributeProperty
    from evennia.utils.utils import list_to_string, repeat

    # error return function, needed by Extended Look command
    _AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit(".", 1))

    # funcparser callable for the ExtendedRoom

    def func_state(roomstate, *args, looker=None, room=None, **kwargs):
        """
        Usage: $state(roomstate, text)

        Funcparser callable for ExtendedRoom. This is called by the FuncParser when it
        returns the description of the room. Use 'default' for a default text when no
        other states are set.

        Args:
            roomstate (str): A roomstate, like "morning", "raining". This is case insensitive.
            *args: All these will be combined into one string separated by commas.

        Keyword Args:
            looker (Object): The object looking at the room. Unused by default.
            room (ExtendedRoom): The room being looked at.

        Example:

            $state(morning, It is a beautiful morning!)

        Notes:
            We try to merge all args into one text, since this function doesn't require more than one
            argument. That way, one may be able to get away without using quotes.

        """
        roomstate = str(roomstate).lower()
        text = ", ".join(args)
        # make sure we have a room and a caller and not something parsed from the string
        if not (roomstate and looker and room) or isinstance(looker, str) or isinstance(room, str):
            return ""

        try:
            if roomstate in room.room_states or roomstate == room.get_time_of_day():
                return text
            if roomstate == "default" and not room.room_states:
                # return this if no roomstate is set
                return text
        except AttributeError:
            # maybe used on a non-ExtendedRoom?
            pass
        return ""

    class ExtendedRoom(DefaultRoom):
        """
        An Extended Room

        Room states:
          A room state is set as a Tag with category "roomstate" and tagkey "on_fire" or "flooded"
          etc).

        Alternative descriptions:
        - Add an Attribute `desc_<roomstate>` to the room, where <roomstate> is the name of the
          roomstate to use this for, like `desc_on_fire` or `desc_flooded`. If not given, seasonal
          descriptions given in desc_spring/summer/autumn/winter will be used, and last the
          regular `desc` Attribute.

        Alternative text sections
        - Used to add alternative text sections to the room description. These are embedded in the
          description by adding `$state(roomstate, txt)`. They will show only if the room is in the
          given roomstate. These are managed via the add/remove/get_alt_text methods.

        Details:
        - This is set as an Attribute `details` (a dict) on the room, with the detail name as key.
          When looking at this room, the detail name can be used as a target to look at without having
          to add an actual database object for it. The `detail` command is used to add/remove details.

        Room messages
        - Set `room_message_rate > 0` and add a list of `room_messages`. These will be randomly
          echoed to the room at the given rate.

        """

        # fallback description if nothing else is set
        fallback_desc = "You see nothing special."

        # tag room_state category
        room_state_tag_category = "room_state"

        # time setup
        months_per_year = 12
        hours_per_day = 24

        # seasons per year, given as (start, end) boundaries, each a fraction of a year. These
        # will change the description. The last entry should wrap around to the first.
        seasons_per_year = {
            "spring": (3 / months_per_year, 6 / months_per_year),  # March - May
            "summer": (6 / months_per_year, 9 / months_per_year),  # June - August
            "autumn": (9 / months_per_year, 12 / months_per_year),  # September - November
            "winter": (12 / months_per_year, 3 / months_per_year),  # December - February
        }

        # time-dependent room descriptions (these must match the `seasons_per_year` above).
        desc_spring = AttributeProperty("", autocreate=False)
        desc_summer = AttributeProperty("", autocreate=False)
        desc_autumn = AttributeProperty("", autocreate=False)
        desc_winter = AttributeProperty("", autocreate=False)

        # time-dependent embedded descriptions, usable as $timeofday(morning, text)
        # (start, end) boundaries, each a fraction of a day. The last one should
        # end at 0 (not 24) to wrap around to midnight.
        times_of_day = {
            "night": (0, 6 / hours_per_day),  # midnight - 6AM
            "morning": (6 / hours_per_day, 12 / hours_per_day),  # 6AM - noon
            "afternoon": (12 / hours_per_day, 18 / hours_per_day),  # noon - 6PM
            "evening": (18 / hours_per_day, 0),  # 6PM - midnight
        }

        # normal vanilla description if no other `*_desc` matches or are set.
        desc = AttributeProperty("", autocreate=False)

        # look-targets without database objects
        details = AttributeProperty(dict, autocreate=False)

        # messages to send to the room
        room_message_rate = 0  # set >0s to enable
        room_messages = AttributeProperty(list, autocreate=False)

        # Broadcast message

        def _get_funcparser(self, looker):
            return FuncParser(
                {"state": func_state},
                looker=looker,
                room=self,
            )

        def _start_broadcast_repeat_task(self):
            if self.room_message_rate and self.room_messages and not self.ndb.broadcast_repeat_task:
                self.ndb.broadcast_repeat_task = repeat(
                    self.room_message_rate, self.repeat_broadcast_msg_to_room, persistent=False
                )

        def at_init(self):
            """Evennia hook. Start up repeating function whenever object loads into memory."""
            self._start_broadcast_repeat_task()

        def start_repeat_broadcast_messages(self):
            """
            Start repeating the broadcast messages. Only needs to be called if adding messages
            and not having reloaded the server.

            """
            self._start_broadcast_repeat_task()

        def repeat_broadcast_message_to_room(self):
            """
            Send a message to the room at room_message_rate. By default
            we will randomize which one to send.

            """
            self.msg_contents(random.choice(self.room_messages))

        def get_time_of_day(self):
            """
            Get the current time of day.

            Override to customize.

            Returns:
                str: The time of day, such as 'morning', 'afternoon', 'evening' or 'night'.

            """
            timestamp = gametime.gametime(absolute=True)
            datestamp = datetime.datetime.fromtimestamp(timestamp)
            timeslot = float(datestamp.hour) / self.hours_per_day

            for time_of_day, (start, end) in self.times_of_day.items():
                if start < end and start <= timeslot < end:
                    return time_of_day
            return time_of_day  # final back to the beginning

        def get_season(self):
            """
            Get the current season.

            Override to customize.

            Returns:
                str: The season, such as 'spring', 'summer', 'autumn' or 'winter'.

            """
            timestamp = gametime.gametime(absolute=True)
            datestamp = datetime.datetime.fromtimestamp(timestamp)
            timeslot = float(datestamp.month) / self.months_per_year

            for season_of_year, (start, end) in self.seasons_per_year.items():
                if start < end and start <= timeslot < end:
                    return season_of_year
            return season_of_year  # final step is back to beginning

        # manipulate room states

        @property
        def room_states(self):
            """
            Get all room_states set on this room.

            """
            return list(sorted(self.tags.get(category=self.room_state_tag_category, return_list=True)))

        def add_room_state(self, *room_states):
            """
            Set a room-state or room-states to the room.

            Args:
                *room_state (str): A room state like 'on_fire' or 'flooded'. This will affect
                    what `desc_*` and `roomstate_*` descriptions/inlines are used. You can add
                    more than one at a time.

            Notes:
                You can also set time-based room_states this way, like 'morning' or 'spring'. This
                can be useful to force a particular description, but while this state is
                set this way, that state will be unaffected by the passage of time. Remove
                the state to let the current game time determine this type of states.

            """
            self.tags.batch_add(*((state, self.room_state_tag_category) for state in room_states))

        def remove_room_state(self, *room_states):
            """
            Remove a roomstate from the room.

            Args:
                *room_state (str): A roomstate like 'on_fire' or 'flooded'. If the
                room did not have this state, nothing happens.You can remove more than one at a time.

            """
            for room_state in room_states:
                self.tags.remove(room_state, category=self.room_state_tag_category)

        def clear_room_state(self):
            """
            Clear all room states.

            Note that fallback time-of-day and seasonal states are not affected by this, only
            custom states added with `.add_room_state()`.

            """
            self.tags.clear(category="room_state")

        # control the available room descriptions

        def add_desc(self, desc, room_state=None):
            """
            Add a custom description, matching a particular room state.

            Args:
                desc (str): The description to use when this roomstate is active.
                roomstate (str, None): The roomstate to match, like 'on_fire', 'flooded', or "spring".
                    If `None`, set the default `desc` fallback.

            """
            if room_state is None:
                self.attributes.add("desc", desc)
            else:
                self.attributes.add(f"desc_{room_state}", desc)

        def remove_desc(self, room_state):
            """
            Remove a custom description.

            Args:
                room_state (str): The room-state description to remove.

            """
            self.attributes.remove(f"desc_{room_state}")

        def all_desc(self):
            """
            Get all available descriptions.

            Returns:
                dict: A mapping of roomstate to description. The `None` key indicates the
                    base subscription (stored in the `desc` Attribute).

            """
            return {
                **{None: self.db.desc or ""},
                **{
                    attr.key[5:]: attr.value
                    for attr in self.db_attributes.filter(db_key__startswith="desc_").order_by("db_key")
                },
            }

        def get_stateful_desc(self):
            """
            Get the currently active room description based on the current roomstate.

            Returns:
                str: The current description.

            Note:
                Only one description can be active at a time. Priority order is as follows:

                Priority order is as follows:

                    1. Room-states set by `add_roomstate()` that are not seasons.
                       If multiple room_states are set, the first one is used, sorted alphabetically.
                    2. Seasons set by `add_room_state()`. This allows to 'pin' a season.
                    3. Time-based seasons based on the current in-game time.
                    4. None, if no seasons are defined in `.seasons_per_year`.

                If either of the above is found, but doesn't have a matching `desc_<roomstate>`
                description, we move on to the next priority. If no matches are found, the `desc`
                Attribute is used.

            """

            room_states = self.room_states
            seasons = self.seasons_per_year.keys()
            seasonal_room_states = []

            # get all available descriptions on this room
            # note: *_desc is the old form, we support it for legacy
            descriptions = dict(
                self.db_attributes.filter(
                    Q(db_key__startswith="desc_") | Q(db_key__endswith="_desc")
                ).values_list("db_key", "db_value")
            )

            for roomstate in sorted(room_states):
                if roomstate not in seasons:
                    # if we have a roomstate that is not a season, use it
                    if desc := descriptions.get(f"desc_{roomstate}") or descriptions.get(
                        "{roomstate}_desc"
                    ):
                        return desc
                else:
                    seasonal_room_states.append(roomstate)

            if not seasons:
                # no seasons defined, so just return the default desc
                return self.attributes.get("desc")

            for seasonal_roomstate in seasonal_room_states:
                # explicit setting of season outside of automatic time keeping
                if desc := descriptions.get(f"desc_{seasonal_roomstate}"):
                    return desc

            # no matching room_states, use time-based seasons. Also support legacy *_desc form
            season = self.get_season()
            if desc := descriptions.get(f"desc_{season}") or descriptions.get(f"{season}_desc"):
                return desc

            # fallback to normal desc Attribute
            return self.attributes.get("desc", self.fallback_desc)

        def replace_legacy_time_of_day_markup(self, desc):
            """
            Filter description by legacy markup like `<morning>...</morning>`. Filter
            out all such markings that does not match the current time. Supports
            'morning', 'afternoon', 'evening' and 'night'.

            Args:
                desc (str): The unmodified description.

            Returns:
                str: A possibly modified description.

            Notes:
                This is legacy. Use the $state markup for new rooms instead.

            """
            desc = desc or ""
            time_of_day = self.get_time_of_day()

            # regexes for in-desc replacements (gets cached)
            if not hasattr(self, "legacy_timeofday_regex_map"):
                timeslots = deque()
                for time_of_day in self.times_of_day:
                    timeslots.append(
                        (
                            time_of_day,
                            re.compile(rf"<{time_of_day}>(.*?)</{time_of_day}>", re.IGNORECASE),
                        )
                    )

                # map the regexes cyclically, so each one is first once
                self.legacy_timeofday_regex_map = {}
                for i in range(len(timeslots)):
                    # mapping {"morning": [morning_regex, ...], ...}
                    self.legacy_timeofday_regex_map[timeslots[0][0]] = [tup[1] for tup in timeslots]
                    timeslots.rotate(-1)

            # do the replacement
            regextuple = self.legacy_timeofday_regex_map[time_of_day]
            desc = regextuple[0].sub(r"\1", desc)
            desc = regextuple[1].sub("", desc)
            desc = regextuple[2].sub("", desc)
            return regextuple[3].sub("", desc)

        def get_display_desc(self, looker, **kwargs):
            """
            Evennia standard hook. Dynamically get the 'desc' component of the object description. This
            is called by the return_appearance method and in turn by the 'look' command.

            Args:
                looker (Object): Object doing the looking (unused by default).
                **kwargs: Arbitrary data for use when overriding.
            Returns:
                str: The desc display string.

            """
            # get the current description based on the roomstate
            desc = self.get_stateful_desc()
            # parse for legacy <morning>...</morning> markers
            desc = self.replace_legacy_time_of_day_markup(desc)
            # apply funcparser
            desc = self._get_funcparser(looker).parse(desc, **kwargs)
            return desc

        # manipulate details

        def add_detail(self, key, 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. This can contain funcparser directives.

            """
            if not self.details:
                self.details = {}  # causes it to be created as real attribute
            self.details[key.lower()] = description

        set_detail = add_detail  # legacy name

        def remove_detail(self, key, *args):
            """
            Delete a detail.

            Args:
                key (str): the detail to remove (case-insensitive).
                *args: Unused (backwards compatibility)

            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.

            """
            self.details.pop(key.lower(), None)

        del_detail = remove_detail  # legacy alias

        def get_detail(self, key, looker=None):
            """
            This will attempt to match a "detail" to look for in the room.
            This will do a lower-case match followed by a startsby match. This
            is called by the new `look` Command.

            Args:
                key (str): A detail identifier.
                looker (Object, optional): The one looking.

            Returns:
                detail (str or None): A detail matching the given key, or `None` if
                it was not found.

            Notes:
                A detail is a way to offer more things to look at in a room
                without having to add new objects. For this to work, we
                require a custom `look` command that allows for `look <detail>`
                - the look command should defer to this method on
                the current location (if it exists) before giving up on
                finding the target.

            """
            key = key.lower()
            detail_keys = tuple(self.details.keys())

            detail = None
            if key in detail_keys:
                # exact match
                detail = self.details[key]
            else:
                # find closest match starting with key (shortest difference in length)
                lkey = len(key)
                startswith_matches = sorted(
                    (
                        (detail_key, abs(lkey - len(detail_key)))
                        for detail_key in detail_keys
                        if detail_key.startswith(key)
                    ),
                    key=lambda tup: tup[1],
                )
                if startswith_matches:
                    # use the matching startswith-detail with the shortest difference in length
                    detail = self.details[startswith_matches[0][0]]

            if detail:
                detail = self._get_funcparser(looker).parse(detail)

            return detail

        return_detail = get_detail  # legacy name

    # Custom Look command supporting Room details. Add this to
    # the Default cmdset to use.

    class CmdExtendedRoomLook(default_cmds.CmdLook):
        """
        look

        Usage:
          look
          look <obj>
          look <room detail>
          look *<account>

        Observes your location, details at your location or objects in your vicinity.
        """

        def look_detail(self):
            """
            Look for detail on room.
            """
            caller = self.caller
            if hasattr(self.caller.location, "get_detail"):
                detail = self.caller.location.get_detail(self.args, looker=self.caller)
                if detail:
                    caller.location.msg_contents(
                        f"$You() $conj(look) closely at {self.args}.\n",
                        from_obj=caller,
                        exclude=caller,
                    )
                    caller.msg(detail)
                    return True
            return False

        def func(self):
            """
            Handle the looking.
            """
            caller = self.caller
            if not self.args:
                target = caller.location
                if not target:
                    caller.msg("You have no location to look at!")
                    return
            else:
                # search, waiting to return errors so we can also check details
                target = caller.search(self.args, quiet=True)
                # if there's no target, check details
                if not target:
                    # no target AND no detail means run the normal no-results message
                    if not self.look_detail():
                        _AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
                    return
                # otherwise, run normal search result handling
                target = _AT_SEARCH_RESULT(target, caller, self.args, quiet=False)
                if not target:
                    return
            desc = caller.at_look(target)
            # add the type=look to the outputfunc to make it
            # easy to separate this output in client.
            self.msg(text=(desc, {"type": "look"}), options=None)

    # Custom build commands for setting seasonal descriptions
    # and detailing extended rooms.

    def _desc_load(caller):
        return caller.db.eveditor_target.db.desc or ""

    def _desc_save(caller, buf):
        """
        Save line buffer to the desc prop. This should
        return True if successful and also report its status to the user.
        """
        roomstates = caller.db.eveditor_roomstates
        target = caller.db.eveditor_target

        if not roomstates or not hasattr(target, "add_desc"):
            # normal description
            target.db.desc = buf
        elif roomstates:
            for roomstate in roomstates:
                target.add_desc(buf, room_state=roomstate)
        else:
            target.db.desc = buf

        caller.msg("Saved.")
        return True

    def _desc_quit(caller):
        caller.attributes.remove("eveditor_target")
        caller.msg("Exited editor.")

    class CmdExtendedRoomDesc(default_cmds.CmdDesc):
        """
        describe an object or the current room.

        Usage:
          @desc[/switch] [<obj> =] <description>

        Switches:
          edit - Open up a line editor for more advanced editing.
          del - Delete the description of an object. If another state is given, its description
            will be deleted.
          spring|summer|autumn|winter - room description to use in respective in-game season
          <other> - room description to use with an arbitrary room state.

        Sets the description an object. If an object is not given,
        describe the current room, potentially showing any additional stateful descriptions. The room
        states only work with rooms.

        Examples:
            @desc/winter A cold winter scene.
            @desc/edit/summer
            @desc/burning This room is burning!
            @desc A normal room with no state.
            @desc/del/burning

        Rooms will automatically change season as the in-game time changes. You can
        set a specific room-state with the |wroomstate|n command.

        """

        key = "@desc"
        switch_options = None
        locks = "cmd:perm(desc) or perm(Builder)"
        help_category = "Building"

        def parse(self):
            super().parse()

            self.delete_mode = "del" in self.switches
            self.edit_mode = not self.delete_mode and "edit" in self.switches

            self.object_mode = "=" in self.args

            # all other switches are names of room-states
            self.roomstates = [state for state in self.switches if state not in ("edit", "del")]

        def edit_handler(self):
            if self.rhs:
                self.msg("|rYou may specify a value, or use the edit switch, but not both.|n")
                return
            if self.args:
                obj = self.caller.search(self.args)
            else:
                obj = self.caller.location or self.msg("|rYou can't describe oblivion.|n")
            if not obj:
                return

            if not (obj.access(self.caller, "control") or obj.access(self.caller, "edit")):
                self.caller.msg(f"You don't have permission to edit the description of {obj.key}.")
                return

            self.caller.db.eveditor_target = obj
            self.caller.db.eveditor_roomstates = self.roomstates
            # launch the editor
            EvEditor(
                self.caller,
                loadfunc=_desc_load,
                savefunc=_desc_save,
                quitfunc=_desc_quit,
                key="desc",
                persistent=True,
            )
            return

        def show_stateful_descriptions(self):
            location = self.caller.location
            room_states = location.room_states
            season = location.get_season()
            time_of_day = location.get_time_of_day()
            stateful_descs = location.all_desc()

            output = [
                f"Room {location.get_display_name(self.caller)} "
                f"Season: {season}. Time: {time_of_day}. "
                f"States: {', '.join(room_states) if room_states else 'None'}"
            ]
            other_active = False
            for state, desc in stateful_descs.items():
                if state is None:
                    continue
                if state == season or state in room_states:
                    output.append(f"Room state |w{state}|n |g(active)|n:\n{desc}")
                    other_active = True
                else:
                    output.append(f"Room state |w{state}|n:\n{desc}")

            active = " |g(active)|n" if not other_active else ""
            output.append(f"Room state |w(default)|n{active}:\n{location.db.desc}")

            sep = "\n" + "-" * 78 + "\n"
            self.caller.msg(sep.join(output))

        def func(self):
            caller = self.caller
            if not self.args and "edit" not in self.switches and "del" not in self.switches:
                if caller.location:
                    # show stateful descs on the room
                    self.show_stateful_descriptions()
                    return
                else:
                    caller.msg("You have no location to describe!")
                    return

            if self.edit_mode:
                self.edit_handler()
                return

            if self.object_mode:
                # We are describing an object
                target = caller.search(self.lhs)
                if not target:
                    return
                desc = self.rhs or ""
            else:
                # we are describing the current room
                target = caller.location or self.msg("|rYou don't have a location to describe.|n")
                if not target:
                    return
                desc = self.args

            roomstates = self.roomstates
            if target.access(self.caller, "control") or target.access(self.caller, "edit"):
                if not roomstates or not hasattr(target, "add_desc"):
                    # normal description
                    target.db.desc = desc
                elif roomstates:
                    for roomstate in roomstates:
                        if self.delete_mode:
                            target.remove_desc(roomstate)
                            caller.msg(f"The {roomstate}-description was deleted, if it existed.")
                        else:
                            target.add_desc(desc, room_state=roomstate)
                            caller.msg(
                                f"The {roomstate}-description was set on"
                                f" {target.get_display_name(caller)}."
                            )
                else:
                    target.db.desc = desc
                    caller.msg(f"The description was set on {target.get_display_name(caller)}.")
            else:
                caller.msg(
                    "You don't have permission to edit the description "
                    f"of {target.get_display_name(caller)}."
                )

    class CmdExtendedRoomDetail(default_cmds.MuxCommand):

        """
        sets a detail on a room

        Usage:
            @detail[/del] <key> [= <description>]
            @detail <key>;<alias>;... = 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(
                        f"|rThe room {location.get_display_name(self.caller)} doesn't have any"
                        " details.|n"
                    )
                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 = "add_detail" if "del" not in self.switches else "remove_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(f"Deleted detail '{self.lhs}', if it existed.")
            else:
                self.caller.msg(f"Set detail '{self.lhs}': '{self.rhs}'")

    class CmdExtendedRoomState(default_cmds.MuxCommand):
        """
        Toggle and view room state for the current room.

        Usage:
            @roomstate [<roomstate>]

        Examples:
            @roomstate spring
            @roomstate burning
            @roomstate burning      (a second time toggles it off)

        If the roomstate was already set, it will be disabled. Use
        without arguments to see the roomstates on the current room.

        """

        key = "@roomstate"
        locks = "cmd:perm(Builder)"
        help_category = "Building"

        def parse(self):
            super().parse()
            self.room = self.caller.location
            if not self.room or not hasattr(self.room, "room_states"):
                self.caller.msg("You have no current location, or it doesn't support room states.")
                raise InterruptCommand()

            self.room_state = self.args.strip().lower()

        def func(self):
            caller = self.caller
            room = self.room
            room_state = self.room_state

            if room_state:
                # toggle room state
                if room_state in room.room_states:
                    room.remove_room_state(room_state)
                    caller.msg(f"Cleared room state '{room_state}' from this room.")
                else:
                    room.add_room_state(room_state)
                    caller.msg(f"Added room state '{room_state}' to this room.")
            else:
                # view room states
                room_states = list_to_string(
                    [f"'{state}'" for state in room.room_states] if room.room_states else ("None",)
                )
                caller.msg(
                    "Room states (not counting automatic time/season) on"
                    f" {room.get_display_name(caller)}:\n {room_states}"
                )

    class CmdExtendedRoomGameTime(default_cmds.MuxCommand):
        """
        Check the game time.

        Usage:
            time

        Shows the current in-game time and season.

        """

        key = "time"
        locks = "cmd:all()"
        help_category = "General"

        def parse(self):
            location = self.caller.location
            if (
                not location
                or not hasattr(location, "get_time_of_day")
                or not hasattr(location, "get_season")
            ):
                self.caller.msg("No location available - you are outside time.")
                raise InterruptCommand()
            self.location = location

        def func(self):
            location = self.location

            season = location.get_season()
            timeslot = location.get_time_of_day()

            prep = "an" if season == "autumn" else "a"
            self.caller.msg(f"It's {prep} {season} day, in the {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(CmdExtendedRoomState())
            self.add(CmdExtendedRoomGameTime())

Note

You are reading an old version of the Evennia documentation. The latest version is here

.