moss-n-puddles/typeclasses/objects.py
2025-08-06 22:40:05 -07:00

566 lines
23 KiB
Python
Executable file

#!/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 <home>, 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.
"""
results = super().search(searchdata, global_search=True, quiet=True)
if isinstance(results, list):
return results[0]
return results
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