#!/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 evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject 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" 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 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 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) logger.info(f"Executing: {cmd}") 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: for _, trigger in enumerate(dict(trigs)): seq = trigs[trigger] if match(trigger, speech, IGNORECASE): self.trigger_sequence(seq, 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. """ 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 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)