moss-n-puddles/typeclasses/objects.py
Howard Abrams 586fdadf7a Tag beginning characters as 'beginner'
This allows us to hide stuff if they are looking for alchemist stuff.
2025-08-28 21:56:15 -07:00

684 lines
28 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, 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 <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 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