#!/usr/bin/env python """ Object The Object is the class for general items in the game world. Use the ObjectParent class to implement common features for *all* entities with a location in the game world (like Characters, Rooms, Exits). """ from datetime import datetime from os import makedirs from os.path import dirname, exists from re import split, match, sub, IGNORECASE from shutil import copyfile from random import randint, choice from django.conf import settings from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject, parse_sdescs_and_recogs from evennia.objects.models import ObjectDB from evennia.prototypes.spawner import spawn from evennia.utils import delay, logger from evennia.utils.search import search_object from utils.scoring import Scores from utils.word_list import routput class ObjectParent: """ This is a mixin that can be used to override *all* entities inheriting at some distance from DefaultObject (Objects, Exits, Characters and Rooms). Just add any method that exists on `DefaultObject` to this class. If one of the derived classes has itself defined that same hook already, that will take precedence. """ appearance_template = """|/{desc} {exits} {characters} {things} {footer} """ def get_display_footer(self, _, **kwargs): return "\n" def has_method(self, method_name): """True if this object has a method of a particular name.""" return hasattr(self, method_name) and callable(getattr(self, method_name)) def get_name(self, alias=None): def name_and_adjs(name): """ Split a long name into its parts. """ return list(filter(lambda s: s != "", split(r"[, ]+", name))) def new_name(parts): """ Take a long name, like: fat, black cat And return _part_ of the name, like: - fat, black cat - black cat - cat """ num_adjs = len(parts)-1 lst_adjs = parts[randint(0, num_adjs):num_adjs] noun = parts[-1] adjs = ', '.join(lst_adjs) return f"{adjs} {noun}" if len(adjs) > 0 else noun parts = name_and_adjs(self.db._sdesc or self.name) # The familiar's name: if not alias or alias == parts[-1]: return new_name(parts) else: return alias def has(self, item): """ Return true if object has an item. Where item is probably a string name to match an item's key. It can also be a type, for instance: character.has(typeclasses.drinkables.TeaCup) """ for i in self.contents: if isinstance(item, str) and (i.key == item or i.aliases.get(item)): return i if item is type(i): return i if i == item: return i return None def client_height(self): """ Get the client screenheight for the session using this command. Returns: client height (int): The height (in characters) of the client window. Not sure why this isn't part of the engine. """ if self.sessions: session = self.sessions.get()[0] return session.protocol_flags.get( "SCREENHEIGHT", {0: settings.CLIENT_DEFAULT_HEIGHT} )[0] return settings.CLIENT_DEFAULT_HEIGHT def characters_here(self, puppets=False): """ Return a list of characters in the current location. """ return [char for char in self.location.contents if char != self and (char.is_typeclass("typeclasses.characters.Character") or (char.is_typeclass("typeclasses.puppets.Puppet") and puppets))] def get_search_result(self, searchdata, attribute_name=None, typeclass=None, candidates=None, exact=False, use_dbref=None, tags=None, **kwargs): """ Redefine function to address bug in `ContribRPObject` where non-builder characters need to call the system's search when looking for aliases. """ return ObjectDB.objects.search_object( searchdata, attribute_name=attribute_name, typeclass=typeclass, candidates=candidates, exact=exact, use_dbref=use_dbref, tags=tags, ) def delay_sequence(self, sequence_str, time_delay=1, *args): """Run a sequence of messages or commands with a delay. The 'sequence_str' is a number of commands separated by ';;' character sequence. The command can by standard things like 'say' or 'give', but can be a message delivered to the room location, if it begins with a # or 'gm'. The command can also be a number, in which case, the next command (and all subsequent commands), will be delayed by that number of seconds. For instance: 'say Hello there, want a drink? ;; 8 ;; # He works on shaking a cocktail. ;; 2 ;; shake whisky = avatar' Which could show: Blonde elf says, "Hello there, want a drink?" <8 seconds pass> He works on shaking a cocktail. <2 seconds pass> You now have a whisky. """ def convert(x): try: return int(x) except ValueError: return x if self.ndb.current_sequence and self.ndb.current_sequence == sequence_str: logger.info("Duplicate sequences. Ignoring.") return self.ndb.current_sequence = sequence_str lines = [convert(line) for line in split(r" *;; *", sequence_str)] pause = 0 for line in lines: if isinstance(line, int): time_delay = line else: pause = pause + time_delay m = match(r"(^# *|^gm +)(.*)", line) if m: logger.info(f"GM'd: {m.group(2)}") if self.location: delay(pause, self.location.msg_contents, routput(m.group(2), *args)) else: delay(pause, self.msg_contents, routput(m.group(2), *args)) else: cmd = routput(line, *args) delay(pause, self.do_cmd, cmd) delay(pause, self.nattributes.remove, "current_sequence") class Object(ObjectParent, ContribRPObject): """ This is the root Object typeclass, representing all entities that have an actual presence in-game. DefaultObjects generally have a location. They can also be manipulated and looked at. Game entities you define should inherit from DefaultObject at some distance. It is recommended to create children of this class using the `evennia.create_object()` function rather than to initialize the class directly - this will both set things up and efficiently save the object without `obj.save()` having to be called explicitly. Note: Check the autodocs for complete class members, this may not always be up-to date. * Base properties defined/available on all Objects key (string) - name of object name (string)- same as key dbref (int, read-only) - unique #id-number. Also "id" can be used. date_created (string) - time stamp of object creation account (Account) - controlling account (if any, only set together with sessid below) sessid (int, read-only) - session id (if any, only set together with account above). Use `sessions` handler to get the Sessions directly. location (Object) - current location. Is None if this is a room home (Object) - safety start-location has_account (bool, read-only)- will only return *connected* accounts contents (list, read only) - returns all objects inside this object exits (list of Objects, read-only) - returns all exits from this object, if any destination (Object) - only set if this object is an exit. is_superuser (bool, read-only) - True/False if this user is a superuser is_connected (bool, read-only) - True if this object is associated with an Account with any connected sessions. has_account (bool, read-only) - True is this object has an associated account. is_superuser (bool, read-only): True if this object has an account and that account is a superuser. * Handlers available aliases - alias-handler: use aliases.add/remove/get() to use. permissions - permission-handler: use permissions.add/remove() to add/remove new perms. locks - lock-handler: use locks.add() to add new lock strings scripts - script-handler. Add new scripts to object with scripts.add() cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object nicks - nick-handler. New nicks with nicks.add(). sessions - sessions-handler. Get Sessions connected to this object with sessions.get() attributes - attribute-handler. Use attributes.add/remove/get. db - attribute-handler: Shortcut for attribute-handler. Store/retrieve database attributes using self.db.myattr=val, val=self.db.myattr ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data * Helper methods (see src.objects.objects.py for full headers) get_search_query_replacement(searchdata, **kwargs) get_search_direct_match(searchdata, **kwargs) get_search_candidates(searchdata, **kwargs) get_search_result(searchdata, attribute_name=None, typeclass=None, candidates=None, exact=False, use_dbref=None, tags=None, **kwargs) get_stacked_result(results, **kwargs) handle_search_results(searchdata, results, **kwargs) search(searchdata, global_search=False, use_nicks=True, typeclass=None, location=None, attribute_name=None, quiet=False, exact=False, candidates=None, use_locks=True, nofound_string=None, multimatch_string=None, use_dbref=None, tags=None, stacked=0) search_account(searchdata, quiet=False) execute_cmd(raw_string, session=None, **kwargs)) msg(text=None, from_obj=None, session=None, options=None, **kwargs) for_contents(func, exclude=None, **kwargs) msg_contents(message, exclude=None, from_obj=None, mapping=None, raise_funcparse_errors=False, **kwargs) move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) clear_contents() create(key, account, caller, method, **kwargs) copy(new_key=None) at_object_post_copy(new_obj, **kwargs) delete() is_typeclass(typeclass, exact=False) swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) access(accessing_obj, access_type='read', default=False, no_superuser_bypass=False, **kwargs) filter_visible(obj_list, looker, **kwargs) get_default_lockstring() get_cmdsets(caller, current, **kwargs) check_permstring(permstring) get_cmdset_providers() get_display_name(looker=None, **kwargs) get_extra_display_name_info(looker=None, **kwargs) get_numbered_name(count, looker, **kwargs) get_display_header(looker, **kwargs) get_display_desc(looker, **kwargs) get_display_exits(looker, **kwargs) get_display_characters(looker, **kwargs) get_display_things(looker, **kwargs) get_display_footer(looker, **kwargs) format_appearance(appearance, looker, **kwargs) return_apperance(looker, **kwargs) * Hooks (these are class methods, so args should start with self): basetype_setup() - only called once, used for behind-the-scenes setup. Normally not modified. basetype_posthook_setup() - customization in basetype, after the object has been created; Normally not modified. at_object_creation() - only called once, when object is first created. Object customizations go here. at_object_delete() - called just before deleting an object. If returning False, deletion is aborted. Note that all objects inside a deleted object are automatically moved to their , they don't need to be removed here. at_init() - called whenever typeclass is cached from memory, at least once every server restart/reload at_first_save() at_cmdset_get(**kwargs) - this is called just before the command handler requests a cmdset from this object. The kwargs are not normally used unless the cmdset is created dynamically (see e.g. Exits). at_pre_puppet(account)- (account-controlled objects only) called just before puppeting at_post_puppet() - (account-controlled objects only) called just after completing connection account<->object at_pre_unpuppet() - (account-controlled objects only) called just before un-puppeting at_post_unpuppet(account) - (account-controlled objects only) called just after disconnecting account<->object link at_server_reload() - called before server is reloaded at_server_shutdown() - called just before server is fully shut down at_access(result, accessing_obj, access_type) - called with the result of a lock access check on this object. Return value does not affect check result. at_pre_move(destination) - called just before moving object to the destination. If returns False, move is cancelled. announce_move_from(destination) - called in old location, just before move, if obj.move_to() has quiet=False announce_move_to(source_location) - called in new location, just after move, if obj.move_to() has quiet=False at_post_move(source_location) - always called after a move has been successfully performed. at_pre_object_leave(leaving_object, destination, **kwargs) at_object_leave(obj, target_location, move_type="move", **kwargs) at_object_leave(obj, target_location) - called when an object leaves this object in any fashion at_pre_object_receive(obj, source_location) at_object_receive(obj, source_location, move_type="move", **kwargs) - called when this object receives another object at_post_move(source_location, move_type="move", **kwargs) at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only) handles all moving across the exit, including calling the other exit hooks. Use super() to retain the default functionality. at_post_traverse(traversing_object, source_location) - (exit-objects only) called just after a traversal has happened. at_failed_traverse(traversing_object) - (exit-objects only) called if traversal fails and property err_traverse is not defined. at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message (via self.msg()) is sent to this obj. If returns false, aborts send. at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects sends a message to someone via self.msg(). return_appearance(looker) - describes this object. Used by "look" command by default at_desc(looker=None) - called by 'look' whenever the appearance is requested. at_pre_get(getter, **kwargs) at_get(getter) - called after object has been picked up. Does not stop pickup. at_pre_give(giver, getter, **kwargs) at_give(giver, getter, **kwargs) at_pre_drop(dropper, **kwargs) at_drop(dropper, **kwargs) - called when this object has been dropped. at_pre_say(speaker, message, **kwargs) at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs) at_look(target, **kwargs) at_desc(looker=None) """ def at_post_move(self, source_location, move_type="move", **kwargs): """ Delete ourselves if we are living inside a pet. """ super().at_post_move(source_location, move_type) if self.location.is_typeclass("typeclasses.pets.Pet"): delay(5, self.delete) def at_give(self, giver, getter, **kwargs): """ Call a puppet's 'other_given' method, if defined. """ super().at_give(giver, getter) if hasattr(getter, 'other_given') and callable(getter.other_given): getter.other_given(giver, self) # The RPSystem overrides the `get_search_result` function to be # able to look for words in the `sdesc` field, but in the process, # dropped the ability to search for objects by `alias`. def get_search_result( self, searchdata, candidates=None, **kwargs, ): """ Override of the parent method for producing search results that understands sdescs. These are used in the main .search() method of the parent class. """ # we also want to use the default search method search_obj = super().get_search_result results = [] if candidates is not None: searched_results = parse_sdescs_and_recogs( self, candidates, "/" + searchdata, search_mode=True ) if not searched_results: results = search_obj(searchdata, candidates=candidates, **kwargs) else: # We do a default search on each result by key, here, # to apply extra filtering kwargs for searched_obj in searched_results: results.extend([ obj for obj in search_obj(searched_obj.key, candidates=[searched_obj], **kwargs) if obj not in results]) else: # no candidates means it's a global search, so we pass it back to the default results = search_obj(searchdata, **kwargs) return results class Listener: """ Mix in class with methods for being able to respond to events from characters. """ def get_character_label(self, character): cleaned = sub(r"|[A-z/]", "", character.get_display_name(self)) return cleaned.split(' ')[-1] def trigger_sequence(self, seq, character, *other_stuff): """ The actions associated with the trigger {0} - character's real name {1} - character's sdesc (from puppet's pov) {2} - character's label, last word in sdesc {3} - character object """ logger.info(f"Triggering {seq}") self.delay_sequence(seq, 1, character.key, character.get_display_name(self), self.get_character_label(character), character, *other_stuff) def get_attribute_trigger(self, label, character): """ Return the attribute where 'key' is the label, and the category can by either the 'character' or its display name. """ name = self.get_character_label(character) return (self.attributes.get(key=label, category=character.key) or self.attributes.get(key=label, category=name) or self.attributes.get(key=label)) def do_trigger(self, action, character): """ Return a list of triggers matching 'action' and 'character'. """ seq = self.get_attribute_trigger(action, character) if seq: self.trigger_sequence(seq, character) def other_arrive(self, character): """ Execute a command when a character arrives in the same location. """ self.do_trigger('arrive', character) def other_leave(self, character): """ Execute a command when a character arrives in the same location. """ self.do_trigger('leave', character) def other_sit(self, character): """ Execute a command when a character sits on something in a location. """ self.do_trigger('sit', character) def other_stand(self, character): """ Execute a command when a character stands in a location. """ self.do_trigger('stand', character) def say_triggers(self, label, character, speech): trigs = self.get_attribute_trigger(label, character) if trigs: try: for _, trigger in enumerate(dict(trigs)): seq = trigs[trigger] if match(trigger, speech, IGNORECASE): self.trigger_sequence(seq, character) except: self.trigger_sequence(trigs, character) def other_say(self, speaker, speech): self.say_triggers('say', speaker, speech) def other_sayto(self, speaker, speech): self.say_triggers('sayto', speaker, speech) def other_given(self, giver, obj): target = giver.get_display_name(self).split(' ')[-1] self.execute_cmd(f"emote /Me says to /{target}, \"Thanks for the {obj.name}.\"") def do_cmd(self, cmd): """ Like 'execute_cmd', but for objects. """ logger.info(f"Executing: {cmd}") m = match(r"teleport +(.*?) *= *(.*)", cmd) if m: o = search_object(m.group(1)).first() d = search_object(m.group(2)).first() if o and d: logger.info(f"Teleporting: {m.group(1)} {o} to {d}") o.move_to(d, quiet=True) return # Private message only a single character can hear: m = match(r"gmm +(.*?) *= *(.*)", cmd) if m: obj = search_object(m.group(1)).first() msg = m.group(2) if obj and msg: obj.msg(msg) return # Character announce_action message: m = match(r"gma +(.*?) *= *(.*)", cmd) if m: obj = search_object(m.group(1)).first() msg = m.group(2) if obj and msg: obj.announce_action(msg) return m = match(r"tag_all +(.*) *", cmd) if m: tag = m.group(1) logger.info(f"Tagging '{tag}'") for c in self.characters_here(puppets=True): c.tags.add(tag) return m = match(r"untag_all +(.*) *", cmd) if m: tag = m.group(1) logger.info(f"Tagging '{tag}'") for c in self.characters_here(puppets=True): c.tags.remove(tag) return m = match(r"gift_all ([A-z]+)( *: *([^:]+)( *: *(.*))?)?", cmd) if m: for c in self.characters_here(puppets=True): logger.info(f"Highest Gift: {m.group(1)} to {c.key}") self.do_gift(c.key, m.group(1), m.group(3), m.group(5)) return m = match(r"gift ([A-z]+) *?( to|=)? *([^:]+)( *: *([^:]+)( *: *(.*))?)?", cmd) if m: logger.info(f"Higher Gift: {m.group(1)} to {m.group(3)}") self.do_gift(m.group(3), m.group(1), m.group(5), m.group(7)) return m = match(r"coin_all ([0-9]+)", cmd) if m: for c in self.characters_here(puppets=True): coins = int(m.group(1)) logger.info(f"Giving {c.key} {coins} coins.") c.adjust_coins(coins) return m = match(r"coin ([0-9]+) *?( to|=)? *([^:]+)", cmd) if m: c = self.search(m.group(3)) if c: c.adjust_coins(int(m.group(1))) return m = match(r"scoring_all ([a-z_]+)", cmd) if m: tick = Scores[m.group(1)] for c in self.characters_here(puppets=True): c.score(tick) return m = match(r"scoring ([a-z_]+) *?( to|=)? *([a-z_]+)", cmd) if m: tick = Scores[m.group(1)] c = self.search(m.group(3)) if c: c.score(tick) return if self.is_typeclass("typeclasses.characters.Character"): self.execute_cmd(cmd) else: logger.info(f"Can't do {cmd}, yet.") def cleanup(self): """ Execute orders. @set thing/objects_here = other_object @set thing/objects_here = [one_object, two_object] """ def move_to_me(obj): o = search_object(obj).first() if o: o.move_to(self, quiet=True) objs = self.db.objects_here if objs: if isinstance(objs, str): move_to_me(objs) else: for obj in objs: move_to_me(obj) def do_gift(self, recipient, gift, name=None, desc=None): """ Give a 'gift' by name to a 'recipient'. This doesn't give the gift if the recipient has already received the gift. """ gifts = { 'purse': {"typeclass": "typeclasses.things.CoinPurse", "key": name or gift, "desc": desc or "Small leather coin purse, with a gold clasp.", }, 'ball': {"typeclass": "typeclasses.things.CrystalBall", "key": name or "crystal ball", "desc": desc or "Swirling glass ball of colored ectoplasm.", }, 'dice': {"typeclass": "typeclasses.things.Dice", "key": name or "pair of dice", "desc": desc or "Two bone knuckles with painted dots.", "attr": {"number": 2} }, 'pipe': {"typeclass": "typeclasses.things.Pipe", "key": name or gift, "desc": desc or "Smoking pipe carved with esoteric symbols.", }, 'wand': {"typeclass": "typeclasses.things.Wand", "key": name or gift, "desc": desc or "Curiously crafted wand carved with runes: ᛑ ᛒ ᚱ", }, 'bag': {"typeclass": "typeclasses.things.BagofJunk", "key": name or "sack", "desc": desc or "Small cloth bag with a leather drawstring.|/You could probably |grummage|n around in that, and maybe |gkeep|n something.", }, 'junk': {"typeclass": "typeclasses.things.BagofJunk", "key": name or "sack", "desc": desc or "Small cloth bag with a leather drawstring.|/You could probably |grummage|n around in that, and maybe |gkeep|n something.", "attr": { "stuff": "human" }, }, 'blue': {"typeclass": "typeclasses.things.Medal", "key": name or "blue medal", "desc": desc or "Gold medallion decorated with a trident and conch suspended by a blue ribbon.", }, } receiver = self.search(recipient, global_search=True) if not receiver: logger.info(f"Didn't find {recipient}.") return None if gift in gifts.keys() and \ not receiver.attributes.get(f"received_{gift}"): details = gifts[gift] logger.info(f"Giving {details['key']} to {receiver.key}") obj = spawn(details)[0] for attr, value in enumerate(details.get("attr", {})): obj.attributes.add(attr, value) obj.location = receiver receiver.attributes.add(f"received_{gift}", True) their_name = receiver.get_display_name(self) self.announce_action(f"$You() $conj(give) something to {their_name}.", exclude=receiver) my_name = self.get_display_name(receiver) receiver.msg(f"{my_name} gives you a {obj.name}.") return True self.msg(f"You can't give '{gift}' to {receiver.key}... {gifts.keys()}") return None class Recorder(Object): """Mixin for recording the events in the current room. Classes that include this mixin, need, in their msg() should call 'record_msg', for instance: def msg(self, text=None, from_obj=None, session=None, **kwargs): self.record_msg(text, from_obj) Objects should set the following properties: @set scribe/transcripts_path = "transcripts/%Y-%m-%d.html" @set scribe/header_file = "transcripts/header-template.html" The 'directory' is really just a prefix to a file that includes today's date. """ def capitalize_msg(self, text, msg_type=None): """ If text is lowercase, capitalize it. Maybe prepend a 'The' to the front. """ logger.info(f"capitalize: {text} / {msg_type}") if msg_type and msg_type in ('traverse', 'teleport'): if match(r"^(\|[A-z])?[aeiou]", text): return "An " + text else: return "A " + text # If the line is colored and lowercase, it probably assumes a # character's sdesc, so let's add a 'the' to the front: # Do we need to only do this on 'say'? elif match(r"^\|[mb][a-z]", text): return "The " + text elif match(r"^(\|[A-z])[a-z]", text) and len(text) > 2: return text[0:2] + text[2].upper() + text[3:] elif match(r"^[a-z]", text) and len(text) > 2: return text[0].upper() + text[1:] return text def to_html(self, text, msg_type=None): """ Convert the 'text' to an HTML formatted string. """ # Yellow text should be italics: text = sub(r"\|y(.*?)\|n", '\\1', text) # Bold and tag the titles: text = sub(r"\|[cb](.*?)\|n", '\\1', text) # Bold anything white: text = sub(r"\|w(.*?)\|n", '\\1', text) # Remove the rest: text = sub(r"\|[A-z]", '', text) # Remove initial or final carriage returns: text = sub(r"^\n", '', text) text = sub(r"\n$", '', text) # Convert the carriage returns: text = sub(r"\n", '
\n', text) if msg_type and msg_type == 'look': text = text[0].upper() + text[1:] return f"

{text}

\n" if msg_type and msg_type == 'note': return f"
{text}
" return f"

{text}

\n" def find_actor(self, text, default=None): """ Extract the character performing the action from the text. """ if default: return default m = match(r".*\|b(.*?)\|n.*", text) if m: return m.group(1) def record_msg(self, text, actor=None): msg_type = None if isinstance(text, tuple): if text[1] and isinstance(text[1], dict): msg_type = text[1]['type'] msg = self.capitalize_msg(text[0], msg_type) else: msg = self.capitalize_msg(str(text)) # Make things a little more interesting by substituting the # name... names = actor and actor.db.alt_names if names: logger.info("lets change this") msg = sub(f"^{actor.sdesc.get()}", choice(names), msg, flags=IGNORECASE) actor = self.find_actor(msg, actor) msg = self.to_html(msg, msg_type) filename = datetime.today().strftime(self.db.transcripts_path) if not exists(filename): makedirs(dirname(filename), exist_ok=True) copyfile(self.db.header_file, filename) with open(filename, "a") as myfile: myfile.write(msg) # Follow-up Actions ... if match(r".* arrives .*", msg) and actor and \ msg_type in ('traverse', 'teleport'): self.execute_cmd(f"look {actor}")