#!/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 from evennia.prototypes.spawner import spawn from evennia.utils import delay, logger 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)) 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 global_search(self, searchdata): """ Search for something globally. """ return super().search(searchdata, global_search=True) 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 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.db.current_sequence and self.db.current_sequence == sequence_str: logger.info("Duplicate sequences. Ignoring.") return self.db.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)}") delay(pause, self.location.msg_contents, routput(m.group(2), *args)) else: cmd = routput(line, *args) delay(pause, self.do_cmd, cmd) delay(pause, self.attributes.remove, "current_sequence") 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) 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 move_triggers(self, label, character): """ Return a list of triggers matching 'label' and 'character'. """ seq = self.get_attribute_trigger(label, 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.move_triggers('arrive', character) def other_leave(self, character): """ Execute a command when a character arrives in the same location. """ self.move_triggers('leave', 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 = self.global_search(m.group(1)) d = self.global_search(m.group(2)) logger.info(f"Teleporting: {m.group(1)} {o} to {d}") if o and d: o.move_to(d, quiet=True) 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 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 = self.global_search(obj) 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