#!/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 re import split, match, sub, IGNORECASE 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. """ 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 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): # Two tags? c.tags.add(tag) c.tags.add(tag, category="mp") 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