diff --git a/contrib/extended_room.py b/contrib/extended_room.py
new file mode 100644
index 000000000..c141f9c9d
--- /dev/null
+++ b/contrib/extended_room.py
@@ -0,0 +1,348 @@
+"""
+Extended Room
+
+Evennia Contribution - Griatch 2012
+
+This is an extended Room typeclass for Evennia. It is supported
+by an extended Look command and an extended @desc command, also
+in this module.
+
+
+Features:
+
+1) Time-changing description slots
+
+This allows to change the full description text the room shows
+depending on larger time variations. Four seasons - spring, summer,
+autumn and winter are used by default). The season is calculated
+on-demand (no Script or timer needed) and updates the full text block.
+
+There is also a general description which is used as fallback if
+one or more of the seasonal descriptions are not set when their
+time comes.
+
+An updated @desc command allows for setting seasonal descriptions.
+
+
+2) In-description changing tags
+
+Within each seasonal (or general) description text, you can also embed
+time-of-day dependent sections. Text inside such a tag will only show
+during that particular time of day. The tags looks like ...
+. By default there are four timeslots per day - morning,
+afternoon, evening and night.
+
+
+3) Details
+
+The Extended Room can be "detailed" with special keywords. This makes
+use of a special Look command. Details are "virtual" targets to look
+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 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.
+
+
+Installation:
+
+1) Add CmdExtendedLook and CmdExtendedDesc from this module to the default cmdset (see wiki 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!
+
+"""
+
+import re
+from django.conf import settings
+from ev import Room
+from ev import gametime
+from ev import default_cmds
+from ev import utils
+
+# error return function, needed by Extended Look command
+_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
+
+# regexes for in-desc replacements
+RE_MORNING = re.compile(r"(.*?)", re.IGNORECASE)
+RE_AFTERNOON = re.compile(r"(.*?)", re.IGNORECASE)
+RE_EVENING = re.compile(r"(.*?)", re.IGNORECASE)
+RE_NIGHT = re.compile(r"(.*?)", re.IGNORECASE)
+# this map is just a faster way to select the right regexes (the first
+# regex in each tuple will be parsed, the following will always be weeded out)
+REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
+ "afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
+ "evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
+ "night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING)}
+
+# set up the seasons and time slots. This assumes gametime started at the
+# beginning of the year (so month 1 is equivalent to January), and that
+# one CAN divive the game's year into four seasons in the first place ...
+MONTHS_PER_YEAR = settings.TIME_MONTH_PER_YEAR
+SEASONAL_BOUNDARIES = (3/12.0, 6/12.0, 9/12.0)
+HOURS_PER_DAY = settings.TIME_HOUR_PER_DAY
+DAY_BOUNDARIES = (0, 6/24.0, 12/24.0, 18/24.0)
+
+# implements the Extended Room
+
+class ExtendedRoom(Room):
+ """
+ This room implements a more advanced look functionality depending on time. It also
+ allows for "details", together with a slightly modified look command.
+ """
+ def at_object_creation(self):
+ "Called when room is first created only."
+ self.db.spring_desc = ""
+ self.db.summer_desc = ""
+ self.db.autumn_desc = ""
+ self.db.winter_desc = ""
+ # the general desc is used as a fallback if a given seasonal one is not set
+ self.db.general_desc = "This is an extended room."
+ self.db.raw_desc = "" # will be set dynamically. Can contain raw timeslot codes
+ self.db.desc = "" # this will be set dynamically at first look. Parsed for timeslot codes
+ # these will be filled later
+ self.ndb.last_season = None
+ self.ndb.last_timeslot = None
+ # detail storage
+ self.db.details = {}
+
+ def get_time_and_season(self):
+ """
+ Calcualte the current time and season ids
+ """
+ # get the current time as parts of year and parts of day
+ time = gametime.gametime(format=True) # returns a tuple (years,months,weeks,days,hours,minutes,sec)
+ month, hour = time[1], time[4]
+ season = float(month) / MONTHS_PER_YEAR
+ timeslot = float(hour) / HOURS_PER_DAY
+
+ # figure out which slots these represent
+ if SEASONAL_BOUNDARIES[0] <= season < SEASONAL_BOUNDARIES[1]: curr_season = "spring"
+ elif SEASONAL_BOUNDARIES[1] <= season < SEASONAL_BOUNDARIES[2]: curr_season = "summer"
+ elif SEASONAL_BOUNDARIES[2] <= season < SEASONAL_BOUNDARIES[3]: curr_season = "autumn"
+ else: curr_season = "winter"
+
+ if DAY_BOUNDARIES[0] <= timeslot < DAY_BOUNDARIES[1]: curr_timeslot = "night"
+ elif DAY_BOUNDARIES[1] <= timeslot < DAY_BOUNDARIES[2]: curr_timeslot = "morning"
+ elif DAY_BOUNDARIES[2] <= timeslot < DAY_BOUNDARIES[3]: curr_timeslot = "afternoon"
+ else: curr_timeslot = "evening"
+
+ print "season:%s, timeslot:%s" % (curr_season, curr_timeslot)
+ return curr_season, curr_timeslot
+
+ def replace_timeslots(self, raw_desc, curr_time):
+ """
+ Filter so that only time markers ... of the correct timeslot
+ remains in the description.
+ """
+ regextuple = REGEXMAP[curr_time]
+ raw_desc = regextuple[0].sub(r"\1", raw_desc)
+ raw_desc = regextuple[1].sub("", raw_desc)
+ raw_desc = regextuple[2].sub("", raw_desc)
+ return regextuple[3].sub("", raw_desc)
+
+ def return_detail(self, key):
+ """
+ This will attempt to match a "detail" to look for in the room. 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 " - the look command should defer to this method
+ on the current location (if it exists) before giving up on finding the target.
+
+ Details are not season-sensitive, but are parsed for timeslot markers.
+ """
+ detail = self.db.details.get(key.lower(), None)
+ if detail:
+ season, timeslot = self.get_time_and_season()
+ detail = self.replace_timeslots(detail, timeslot)
+ return detail
+ return None
+
+ def return_appearance(self, looker):
+ "This is called when e.g. the look command wants to retrieve the description of this object."
+ raw_desc = self.db.raw_desc
+ update = False
+
+ # get current time and season
+ curr_season, curr_timeslot = self.get_time_and_season()
+
+ # compare with previously stored slots
+ last_season = self.ndb.last_season
+ last_timeslot = self.ndb.last_timeslot
+
+ if curr_season != last_season:
+ # season changed. Load new desc, or a fallback.
+ if curr_season == 'spring': new_raw_desc = self.db.spring_desc
+ elif curr_season == 'summer': new_raw_desc = self.db.summer_desc
+ elif curr_season == 'autumn': new_raw_desc = self.db.autumn_desc
+ else: new_raw_desc = self.db.winter_desc
+ if new_raw_desc:
+ raw_desc = new_raw_desc
+ else:
+ # no seasonal desc set. Use fallback
+ raw_desc = self.db.general_desc
+ self.db.raw_desc = raw_desc
+ self.ndb.last_season = curr_season
+ update = True
+
+ if curr_timeslot != last_timeslot:
+ # timeslot changed. Set update flag.
+ self.ndb.last_timeslot = curr_timeslot
+ update = True
+
+ if update:
+ # if anything changed we have to re-parse the raw_desc for time markers
+ # and re-save the description again.
+ self.db.desc = self.replace_timeslots(raw_desc, curr_timeslot)
+ # run the normal return_appearance method, now that desc is updated.
+ return super(ExtendedRoom, self).return_appearance(looker)
+
+
+# Custom Look command supporting Room details. Add this to the Default cmdset to use.
+
+class CmdExtendedLook(default_cmds.CmdLook):
+ """
+ look
+
+ Usage:
+ look
+ look
+ look
+ look *
+
+ Observes your location, details at your location or objects in your vicinity.
+ """
+ def func(self):
+ """
+ Handle the looking - add fallback to details.
+ """
+ caller = self.caller
+ args = self.args
+ if args:
+ looking_at_obj = caller.search(args, use_nicks=True, ignore_errors=True)
+ if not looking_at_obj:
+ # no object found. Check if there is a matching detail at location.
+ location = caller.location
+ if location and hasattr(location, "return_detail") and callable(location.return_detail):
+ detail = location.return_detail(args)
+ if detail:
+ # we found a detail instead. Show that.
+ caller.msg(detail)
+ return
+ # no detail found. Trigger delayed error messages
+ _AT_SEARCH_RESULT(caller, args, looking_at_obj, False)
+ return
+ else:
+ # we need to extract the match manually.
+ looking_at_obj = looking_at_obj[0]
+ else:
+ looking_at_obj = caller.location
+ if not looking_at_obj:
+ caller.msg("You have no location to look at!")
+ return
+
+ if not hasattr(looking_at_obj, 'return_appearance'):
+ # this is likely due to us having a player instead
+ looking_at_obj = looking_at_obj.character
+ if not looking_at_obj.access(caller, "view"):
+ caller.msg("Could not find '%s'." % args)
+ return
+ # get object's appearance
+ caller.msg(looking_at_obj.return_appearance(caller))
+ # the object's at_desc() method.
+ looking_at_obj.at_desc(looker=caller)
+
+
+# Custom build commands for setting seasonal descriptions and detailing extended rooms.
+
+class CmdExtendedDesc(default_cmds.CmdDesc):
+ """
+ @desc - describe an object or room
+
+ Usage:
+ @desc[/switch] [ =]
+ @detail[/del] [ = ]
+
+
+ Switches for @desc:
+ spring - set description for in current room
+ summer
+ 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.
+ """
+ aliases = ["@describe", "@detail"]
+
+ def func(self):
+ "Define extended command"
+ caller = self.caller
+ if self.cmdstring == '@detail':
+ # switch to detailing mode. This operates only on current location
+ location = self.caller.location
+ if not location:
+ caller.msg("No location to detail!")
+ 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 += location.db.details[self.args]
+ caller.msg(string)
+ return
+ if not self.args:
+ # No args given. Return all details on location
+ string = "{wDetails on %s{n:\n" % location
+ string += "\n".join(" {w%s{n: %s" % (key, utils.crop(text)) for key, text in location.db.details.items())
+ caller.msg(string)
+ return
+ if self.switches and self.switches[0] in 'del':
+ # removing a detail.
+ if self.lhs in location.db.details:
+ del location.db.detail
+ caller.msg("Detail %s deleted, if it existed." % self.lhs)
+ return
+
+ # setting a detail
+ location.db.details[self.lhs] = self.rhs
+ caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs))
+ return
+ else:
+ # we are doing a @desc call
+ 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 works with rooms, not objects.")
+ return
+ switch = self.switches[0]
+ location = caller.location
+ 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
+ location.ndb.last_season = None
+ caller.msg("Seasonal description was set on %s." % location.key)
+ elif self.rhs:
+ # Not a seasonal desc, and we have an =
+ obj = caller.search(self.lhs)
+ if not obj:
+ return
+ obj.db.desc = self.rhs
+ caller.msg("The description was set on %s." % obj.key)
+ else:
+ # set a normal non-seasonal description (fallback) on room
+ obj = caller.location
+ obj.db.general_desc = self.args
+ obj.db.desc = self.args # compatability
+ caller.msg("General description was set on %s." % obj.key)
+