Resolve django 1.11 migration errors.
This commit is contained in:
commit
0ff1718437
54 changed files with 6444 additions and 574 deletions
|
|
@ -10,6 +10,6 @@ before_script:
|
|||
- cd dummy
|
||||
- evennia migrate
|
||||
script:
|
||||
- coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py ../bin/unix/evennia test evennia
|
||||
- coverage run --source=../evennia --omit=*/migrations/*,*/urls.py,*/test*.py,*.sh,*.txt,*.md,*.pyc,*.service ../bin/unix/evennia test evennia
|
||||
after_success:
|
||||
- coveralls
|
||||
|
|
|
|||
|
|
@ -38,6 +38,9 @@ from weakref import WeakValueDictionary
|
|||
from traceback import format_exc
|
||||
from itertools import chain
|
||||
from copy import copy
|
||||
import types
|
||||
from twisted.internet import reactor
|
||||
from twisted.internet.task import deferLater
|
||||
from twisted.internet.defer import inlineCallbacks, returnValue
|
||||
from django.conf import settings
|
||||
from evennia.comms.channelhandler import CHANNELHANDLER
|
||||
|
|
@ -127,6 +130,12 @@ _ERROR_RECURSION_LIMIT = "Command recursion limit ({recursion_limit}) " \
|
|||
"reached for '{raw_cmdname}' ({cmdclass})."
|
||||
|
||||
|
||||
# delayed imports
|
||||
_GET_INPUT = None
|
||||
|
||||
|
||||
# helper functions
|
||||
|
||||
def _msg_err(receiver, stringtuple):
|
||||
"""
|
||||
Helper function for returning an error to the caller.
|
||||
|
|
@ -134,8 +143,8 @@ def _msg_err(receiver, stringtuple):
|
|||
Args:
|
||||
receiver (Object): object to get the error message.
|
||||
stringtuple (tuple): tuple with two strings - one for the
|
||||
_IN_GAME_ERRORS mode (with the traceback) and one with the
|
||||
production string (with a timestamp) to be shown to the user.
|
||||
_IN_GAME_ERRORS mode (with the traceback) and one with the
|
||||
production string (with a timestamp) to be shown to the user.
|
||||
|
||||
"""
|
||||
string = "{traceback}\n{errmsg}\n(Traceback was logged {timestamp})."
|
||||
|
|
@ -151,12 +160,77 @@ def _msg_err(receiver, stringtuple):
|
|||
errmsg=stringtuple[1].strip(),
|
||||
timestamp=timestamp).strip())
|
||||
|
||||
|
||||
def _progressive_cmd_run(cmd, generator, response=None):
|
||||
"""
|
||||
Progressively call the command that was given in argument. Used
|
||||
when `yield` is present in the Command's `func()` method.
|
||||
|
||||
Args:
|
||||
cmd (Command): the command itself.
|
||||
generator (GeneratorType): the generator describing the processing.
|
||||
reponse (str, optional): the response to send to the generator.
|
||||
|
||||
Raises:
|
||||
ValueError: If the func call yields something not identifiable as a
|
||||
time-delay or a string prompt.
|
||||
|
||||
Note:
|
||||
This function is responsible for executing the command, if
|
||||
the func() method contains 'yield' instructions. The yielded
|
||||
value will be accessible at each step and will affect the
|
||||
process. If the value is a number, just delay the execution
|
||||
of the command. If it's a string, wait for the user input.
|
||||
|
||||
"""
|
||||
global _GET_INPUT
|
||||
if not _GET_INPUT:
|
||||
from evennia.utils.evmenu import get_input as _GET_INPUT
|
||||
|
||||
try:
|
||||
if response is None:
|
||||
value = generator.next()
|
||||
else:
|
||||
value = generator.send(response)
|
||||
except StopIteration:
|
||||
pass
|
||||
else:
|
||||
if isinstance(value, (int, float)):
|
||||
utils.delay(value, _progressive_cmd_run, cmd, generator)
|
||||
elif isinstance(value, basestring):
|
||||
_GET_INPUT(cmd.caller, value, _process_input, cmd=cmd, generator=generator)
|
||||
else:
|
||||
raise ValueError("unknown type for a yielded value in command: {}".format(type(value)))
|
||||
|
||||
|
||||
def _process_input(caller, prompt, result, cmd, generator):
|
||||
"""
|
||||
Specifically handle the get_input value to send to _progressive_cmd_run as
|
||||
part of yielding from a Command's `func`.
|
||||
|
||||
Args:
|
||||
caller (Character, Player or Session): the caller.
|
||||
prompt (basestring): The sent prompt.
|
||||
result (basestring): The unprocessed answer.
|
||||
cmd (Command): The command itself.
|
||||
generator (GeneratorType): The generator.
|
||||
|
||||
Returns:
|
||||
result (bool): Always `False` (stop processing).
|
||||
|
||||
"""
|
||||
# We call it using a Twisted deferLater to make sure the input is properly closed.
|
||||
deferLater(reactor, 0, _progressive_cmd_run, cmd, generator, response=result)
|
||||
return False
|
||||
|
||||
|
||||
# custom Exceptions
|
||||
|
||||
class NoCmdSets(Exception):
|
||||
"No cmdsets found. Critical error."
|
||||
pass
|
||||
|
||||
|
||||
class ExecSystemCommand(Exception):
|
||||
"Run a system command"
|
||||
def __init__(self, syscmd, sysarg):
|
||||
|
|
@ -510,7 +584,13 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
|
||||
# main command code
|
||||
# (return value is normally None)
|
||||
ret = yield cmd.func()
|
||||
ret = cmd.func()
|
||||
if isinstance(ret, types.GeneratorType):
|
||||
# cmd.func() is a generator, execute progressively
|
||||
_progressive_cmd_run(cmd, ret)
|
||||
yield None
|
||||
else:
|
||||
ret = yield ret
|
||||
|
||||
# post-command hook
|
||||
yield cmd.at_post_cmd()
|
||||
|
|
@ -672,3 +752,4 @@ def cmdhandler(called_by, raw_string, _testing=False, callertype="session", sess
|
|||
except Exception:
|
||||
# This catches exceptions in cmdhandler exceptions themselves
|
||||
_msg_err(error_to, _ERROR_CMDHANDLER)
|
||||
|
||||
|
|
|
|||
|
|
@ -429,7 +429,7 @@ class Command(with_metaclass(CommandMeta, object)):
|
|||
object, conventionally with a preceding space.
|
||||
|
||||
"""
|
||||
if hasattr(self, 'obj') and self.obj != caller:
|
||||
if hasattr(self, 'obj') and self.obj and self.obj != caller:
|
||||
return " (%s)" % self.obj.get_display_name(caller).strip()
|
||||
return ""
|
||||
|
||||
|
|
|
|||
|
|
@ -1926,6 +1926,10 @@ class CmdExamine(ObjManipCommand):
|
|||
string += "\n|wLocation|n: %s" % obj.location
|
||||
if obj.location:
|
||||
string += " (#%s)" % obj.location.id
|
||||
if hasattr(obj, "home"):
|
||||
string += "\n|wHome|n: %s" % obj.home
|
||||
if obj.home:
|
||||
string += " (#%s)" % obj.home.id
|
||||
if hasattr(obj, "destination") and obj.destination:
|
||||
string += "\n|wDestination|n: %s" % obj.destination
|
||||
if obj.destination:
|
||||
|
|
@ -2240,7 +2244,7 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
switch is set, <target location> is ignored.
|
||||
Note that the only way to retrieve
|
||||
an object from a None location is by direct #dbref
|
||||
reference.
|
||||
reference. A puppeted object cannot be moved to None.
|
||||
|
||||
Teleports an object somewhere. If no object is given, you yourself
|
||||
is teleported to the target location. """
|
||||
|
|
@ -2265,19 +2269,21 @@ class CmdTeleport(COMMAND_DEFAULT_CLASS):
|
|||
# teleporting to None
|
||||
if not args:
|
||||
obj_to_teleport = caller
|
||||
caller.msg("Teleported to None-location.")
|
||||
if caller.location and not tel_quietly:
|
||||
caller.location.msg_contents("%s teleported into nothingness." % caller, exclude=caller)
|
||||
else:
|
||||
obj_to_teleport = caller.search(lhs, global_search=True)
|
||||
if not obj_to_teleport:
|
||||
caller.msg("Did not find object to teleport.")
|
||||
return
|
||||
caller.msg("Teleported %s -> None-location." % obj_to_teleport)
|
||||
if obj_to_teleport.location and not tel_quietly:
|
||||
obj_to_teleport.location.msg_contents("%s teleported %s into nothingness."
|
||||
% (caller, obj_to_teleport),
|
||||
exclude=caller)
|
||||
if obj_to_teleport.has_player:
|
||||
caller.msg("Cannot teleport a puppeted object "
|
||||
"(%s, puppeted by %s) to a None-location." % (
|
||||
obj_to_teleport.key, obj_to_teleport.player))
|
||||
return
|
||||
caller.msg("Teleported %s -> None-location." % obj_to_teleport)
|
||||
if obj_to_teleport.location and not tel_quietly:
|
||||
obj_to_teleport.location.msg_contents("%s teleported %s into nothingness."
|
||||
% (caller, obj_to_teleport),
|
||||
exclude=caller)
|
||||
obj_to_teleport.location=None
|
||||
return
|
||||
|
||||
|
|
|
|||
|
|
@ -560,6 +560,7 @@ class CmdOption(COMMAND_DEFAULT_CLASS):
|
|||
"CLIENTNAME": utils.to_str,
|
||||
"ENCODING": validate_encoding,
|
||||
"MCCP": validate_bool,
|
||||
"NOGOAHEAD": validate_bool,
|
||||
"MXP": validate_bool,
|
||||
"NOCOLOR": validate_bool,
|
||||
"NOPKEEPALIVE": validate_bool,
|
||||
|
|
|
|||
|
|
@ -2,7 +2,6 @@
|
|||
Base typeclass for in-game Channels.
|
||||
|
||||
"""
|
||||
|
||||
from evennia.typeclasses.models import TypeclassBase
|
||||
from evennia.comms.models import TempMsg, ChannelDB
|
||||
from evennia.comms.managers import ChannelManager
|
||||
|
|
@ -267,7 +266,11 @@ class DefaultChannel(with_metaclass(TypeclassBase, ChannelDB)):
|
|||
|
||||
"""
|
||||
# get all players or objects connected to this channel and send to them
|
||||
for entity in self.subscriptions.all():
|
||||
if online:
|
||||
subs = self.subscriptions.online()
|
||||
else:
|
||||
subs = self.subscriptions.all()
|
||||
for entity in subs:
|
||||
# if the entity is muted, we don't send them a message
|
||||
if entity in self.mutelist:
|
||||
continue
|
||||
|
|
|
|||
81
evennia/comms/migrations/0011_auto_20170606_1731.py
Normal file
81
evennia/comms/migrations/0011_auto_20170606_1731.py
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-06-06 17:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('comms', '0010_auto_20161206_1912'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='channeldb',
|
||||
name='db_attributes',
|
||||
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='channeldb',
|
||||
name='db_object_subscriptions',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, related_name='object_subscription_set', to='objects.ObjectDB', verbose_name=b'subscriptions'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='channeldb',
|
||||
name='db_subscriptions',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, related_name='subscription_set', to=settings.AUTH_USER_MODEL, verbose_name=b'subscriptions'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='channeldb',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_hide_from_channels',
|
||||
field=models.ManyToManyField(blank=True, related_name='hide_from_channels_set', to='comms.ChannelDB'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_hide_from_objects',
|
||||
field=models.ManyToManyField(blank=True, related_name='hide_from_objects_set', to='objects.ObjectDB'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_hide_from_players',
|
||||
field=models.ManyToManyField(blank=True, related_name='hide_from_players_set', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_receivers_channels',
|
||||
field=models.ManyToManyField(blank=True, help_text=b'channel recievers', related_name='channel_set', to='comms.ChannelDB'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_receivers_objects',
|
||||
field=models.ManyToManyField(blank=True, help_text=b'object receivers', related_name='receiver_object_set', to='objects.ObjectDB'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_receivers_players',
|
||||
field=models.ManyToManyField(blank=True, help_text=b'player receivers', related_name='receiver_player_set', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_sender_objects',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_object_set', to='objects.ObjectDB', verbose_name=b'sender(object)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_sender_players',
|
||||
field=models.ManyToManyField(blank=True, db_index=True, related_name='sender_player_set', to=settings.AUTH_USER_MODEL, verbose_name=b'sender(player)'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='msg',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(blank=True, help_text=b'tags on this message. Tags are simple string markers to identify, group and alias messages.', to='typeclasses.Tag'),
|
||||
),
|
||||
]
|
||||
|
|
@ -81,11 +81,11 @@ class Msg(SharedMemoryModel):
|
|||
# an IRC channel; normally there is only one, but if co-modification of
|
||||
# a message is allowed, there may be more than one "author"
|
||||
db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set',
|
||||
null=True, blank=True, verbose_name='sender(player)', db_index=True)
|
||||
blank=True, verbose_name='sender(player)', db_index=True)
|
||||
db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set',
|
||||
null=True, blank=True, verbose_name='sender(object)', db_index=True)
|
||||
blank=True, verbose_name='sender(object)', db_index=True)
|
||||
db_sender_scripts = models.ManyToManyField("scripts.ScriptDB", related_name='sender_script_set',
|
||||
null=True, blank=True, verbose_name='sender(script)', db_index=True)
|
||||
blank=True, verbose_name='sender(script)', db_index=True)
|
||||
db_sender_external = models.CharField('external sender', max_length=255, null=True, blank=True, db_index=True,
|
||||
help_text="identifier for external sender, for example a sender over an "
|
||||
"IRC connection (i.e. someone who doesn't have an exixtence in-game).")
|
||||
|
|
@ -93,13 +93,13 @@ class Msg(SharedMemoryModel):
|
|||
# comma-separated string of object dbrefs. Can be defined along
|
||||
# with channels below.
|
||||
db_receivers_players = models.ManyToManyField('players.PlayerDB', related_name='receiver_player_set',
|
||||
null=True, blank=True, help_text="player receivers")
|
||||
blank=True, help_text="player receivers")
|
||||
db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set',
|
||||
null=True, blank=True, help_text="object receivers")
|
||||
blank=True, help_text="object receivers")
|
||||
db_receivers_scripts = models.ManyToManyField('scripts.ScriptDB', related_name='receiver_script_set',
|
||||
null=True, blank=True, help_text="script_receivers")
|
||||
blank=True, help_text="script_receivers")
|
||||
db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set',
|
||||
null=True, blank=True, help_text="channel recievers")
|
||||
blank=True, help_text="channel recievers")
|
||||
|
||||
# header could be used for meta-info about the message if your system needs
|
||||
# it, or as a separate store for the mail subject line maybe.
|
||||
|
|
@ -113,11 +113,11 @@ class Msg(SharedMemoryModel):
|
|||
help_text='access locks on this message.')
|
||||
|
||||
# these can be used to filter/hide a given message from supplied objects/players/channels
|
||||
db_hide_from_players = models.ManyToManyField("players.PlayerDB", related_name='hide_from_players_set', null=True, blank=True)
|
||||
db_hide_from_objects = models.ManyToManyField("objects.ObjectDB", related_name='hide_from_objects_set', null=True, blank=True)
|
||||
db_hide_from_channels = models.ManyToManyField("ChannelDB", related_name='hide_from_channels_set', null=True, blank=True)
|
||||
db_hide_from_players = models.ManyToManyField("players.PlayerDB", related_name='hide_from_players_set', blank=True)
|
||||
db_hide_from_objects = models.ManyToManyField("objects.ObjectDB", related_name='hide_from_objects_set', blank=True)
|
||||
db_hide_from_channels = models.ManyToManyField("ChannelDB", related_name='hide_from_channels_set', blank=True)
|
||||
|
||||
db_tags = models.ManyToManyField(Tag, null=True, blank=True,
|
||||
db_tags = models.ManyToManyField(Tag, blank=True,
|
||||
help_text='tags on this message. Tags are simple string markers to identify, group and alias messages.')
|
||||
|
||||
# Database manager
|
||||
|
|
@ -564,6 +564,24 @@ class SubscriptionHandler(object):
|
|||
self._recache()
|
||||
return self._cache
|
||||
|
||||
def online(self):
|
||||
"""
|
||||
Get all online players from our cache
|
||||
Returns:
|
||||
subscribers (list): Subscribers who are online or
|
||||
are puppeted by an online player.
|
||||
"""
|
||||
subs = []
|
||||
for obj in self.all():
|
||||
if hasattr(obj, 'player'):
|
||||
if not obj.player:
|
||||
continue
|
||||
obj = obj.player
|
||||
if not obj.is_connected:
|
||||
continue
|
||||
subs.append(obj)
|
||||
return subs
|
||||
|
||||
def clear(self):
|
||||
"""
|
||||
Remove all subscribers from channel.
|
||||
|
|
@ -588,10 +606,10 @@ class ChannelDB(TypedObject):
|
|||
|
||||
"""
|
||||
db_subscriptions = models.ManyToManyField("players.PlayerDB",
|
||||
related_name="subscription_set", null=True, blank=True, verbose_name='subscriptions', db_index=True)
|
||||
related_name="subscription_set", blank=True, verbose_name='subscriptions', db_index=True)
|
||||
|
||||
db_object_subscriptions = models.ManyToManyField("objects.ObjectDB",
|
||||
related_name="object_subscription_set", null=True, blank=True, verbose_name='subscriptions', db_index=True)
|
||||
related_name="object_subscription_set", blank=True, verbose_name='subscriptions', db_index=True)
|
||||
|
||||
# Database manager
|
||||
objects = managers.ChannelDBManager()
|
||||
|
|
|
|||
|
|
@ -19,6 +19,8 @@ things you want from here into your game folder and change them there.
|
|||
for any game. Allows safe trading of any godds (including coin)
|
||||
* CharGen (Griatch 2011) - A simple Character creator for OOC mode.
|
||||
Meant as a starting point for a more fleshed-out system.
|
||||
* Clothing (BattleJenkins 2017) - A layered clothing system with
|
||||
slots for different types of garments auto-showing in description.
|
||||
* Custom gametime (Griatch, vlgeoff 2017) - Implements Evennia's
|
||||
gametime module but for custom game world-specific calendars.
|
||||
* Dice (Griatch 2012) - A fully featured dice rolling system.
|
||||
|
|
@ -45,6 +47,8 @@ things you want from here into your game folder and change them there.
|
|||
time to pass depending on if you are walking/running etc.
|
||||
* Talking NPC (Griatch 2011) - A talking NPC object that offers a
|
||||
menu-driven conversation tree.
|
||||
* Turnbattle (BattleJenkins 2017) - A turn-based combat engine meant
|
||||
as a start to build from. Has attack/disengage and turn timeouts.
|
||||
* Wilderness (titeuf87 2017) - Make infinitely large wilderness areas
|
||||
with dynamically created locations.
|
||||
|
||||
|
|
|
|||
696
evennia/contrib/clothing.py
Normal file
696
evennia/contrib/clothing.py
Normal file
|
|
@ -0,0 +1,696 @@
|
|||
"""
|
||||
Clothing - Provides a typeclass and commands for wearable clothing,
|
||||
which is appended to a character's description when worn.
|
||||
|
||||
Evennia contribution - Tim Ashley Jenkins 2017
|
||||
|
||||
Clothing items, when worn, are added to the character's description
|
||||
in a list. For example, if wearing the following clothing items:
|
||||
|
||||
a thin and delicate necklace
|
||||
a pair of regular ol' shoes
|
||||
one nice hat
|
||||
a very pretty dress
|
||||
|
||||
A character's description may look like this:
|
||||
|
||||
Superuser(#1)
|
||||
This is User #1.
|
||||
|
||||
Superuser is wearing one nice hat, a thin and delicate necklace,
|
||||
a very pretty dress and a pair of regular ol' shoes.
|
||||
|
||||
Characters can also specify the style of wear for their clothing - I.E.
|
||||
to wear a scarf 'tied into a tight knot around the neck' or 'draped
|
||||
loosely across the shoulders' - to add an easy avenue of customization.
|
||||
For example, after entering:
|
||||
|
||||
wear scarf draped loosely across the shoulders
|
||||
|
||||
The garment appears like so in the description:
|
||||
|
||||
Superuser(#1)
|
||||
This is User #1.
|
||||
|
||||
Superuser is wearing a fanciful-looking scarf draped loosely
|
||||
across the shoulders.
|
||||
|
||||
Items of clothing can be used to cover other items, and many options
|
||||
are provided to define your own clothing types and their limits and
|
||||
behaviors. For example, to have undergarments automatically covered
|
||||
by outerwear, or to put a limit on the number of each type of item
|
||||
that can be worn. The system as-is is fairly freeform - you
|
||||
can cover any garment with almost any other, for example - but it
|
||||
can easily be made more restrictive, and can even be tied into a
|
||||
system for armor or other equipment.
|
||||
|
||||
To install, import this module and have your default character
|
||||
inherit from ClothedCharacter in your game's characters.py file:
|
||||
|
||||
from evennia.contrib.clothing import ClothedCharacter
|
||||
|
||||
class Character(ClothedCharacter):
|
||||
|
||||
And do the same with the ClothedCharacterCmdSet in your game's
|
||||
default_cmdsets.py:
|
||||
|
||||
from evennia.contrib.clothing import ClothedCharacterCmdSet
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
|
||||
From here, you can use the default builder commands to create clothes
|
||||
with which to test the system:
|
||||
|
||||
@create a pretty shirt : evennia.contrib.clothing.Clothing
|
||||
@set shirt/clothing_type = 'top'
|
||||
|
||||
"""
|
||||
|
||||
from evennia import DefaultObject
|
||||
from evennia import DefaultCharacter
|
||||
from evennia import default_cmds
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.utils import list_to_string
|
||||
from evennia.utils import evtable
|
||||
|
||||
# Options start here.
|
||||
# Maximum character length of 'wear style' strings, or None for unlimited.
|
||||
WEARSTYLE_MAXLENGTH = 50
|
||||
|
||||
# The rest of these options have to do with clothing types. Clothing types are optional,
|
||||
# but can be used to give better control over how different items of clothing behave. You
|
||||
# can freely add, remove, or change clothing types to suit the needs of your game and use
|
||||
# the options below to affect their behavior.
|
||||
|
||||
# The order in which clothing types appear on the description. Untyped clothing or clothing
|
||||
# with a type not given in this list goes last.
|
||||
CLOTHING_TYPE_ORDER = ['hat', 'jewelry', 'top', 'undershirt', 'gloves', 'fullbody', 'bottom',
|
||||
'underpants', 'socks', 'shoes', 'accessory']
|
||||
# The maximum number of each type of clothes that can be worn. Unlimited if untyped or not specified.
|
||||
CLOTHING_TYPE_LIMIT = {
|
||||
'hat': 1,
|
||||
'gloves': 1,
|
||||
'socks': 1,
|
||||
'shoes': 1
|
||||
}
|
||||
# The maximum number of clothing items that can be worn, or None for unlimited.
|
||||
CLOTHING_OVERALL_LIMIT = 20
|
||||
# What types of clothes will automatically cover what other types of clothes when worn.
|
||||
# Note that clothing only gets auto-covered if it's already worn when you put something
|
||||
# on that auto-covers it - for example, it's perfectly possible to have your underpants
|
||||
# showing if you put them on after your pants!
|
||||
CLOTHING_TYPE_AUTOCOVER = {
|
||||
'top': ['undershirt'],
|
||||
'bottom': ['underpants'],
|
||||
'fullbody': ['undershirt', 'underpants'],
|
||||
'shoes': ['socks']
|
||||
}
|
||||
# Types of clothes that can't be used to cover other clothes.
|
||||
CLOTHING_TYPE_CANT_COVER_WITH = ['jewelry']
|
||||
|
||||
|
||||
# HELPER FUNCTIONS START HERE
|
||||
|
||||
def order_clothes_list(clothes_list):
|
||||
"""
|
||||
Orders a given clothes list by the order specified in CLOTHING_TYPE_ORDER.
|
||||
|
||||
Args:
|
||||
clothes_list (list): List of clothing items to put in order
|
||||
|
||||
Returns:
|
||||
ordered_clothes_list (list): The same list as passed, but re-ordered
|
||||
according to the hierarchy of clothing types
|
||||
specified in CLOTHING_TYPE_ORDER.
|
||||
"""
|
||||
ordered_clothes_list = clothes_list
|
||||
# For each type of clothing that exists...
|
||||
for current_type in reversed(CLOTHING_TYPE_ORDER):
|
||||
# Check each item in the given clothes list.
|
||||
for clothes in clothes_list:
|
||||
# If the item has a clothing type...
|
||||
if clothes.db.clothing_type:
|
||||
item_type = clothes.db.clothing_type
|
||||
# And the clothing type matches the current type...
|
||||
if item_type == current_type:
|
||||
# Move it to the front of the list!
|
||||
ordered_clothes_list.remove(clothes)
|
||||
ordered_clothes_list.insert(0, clothes)
|
||||
return ordered_clothes_list
|
||||
|
||||
|
||||
def get_worn_clothes(character, exclude_covered=False):
|
||||
"""
|
||||
Get a list of clothes worn by a given character.
|
||||
|
||||
Args:
|
||||
character (obj): The character to get a list of worn clothes from.
|
||||
|
||||
Kwargs:
|
||||
exclude_covered (bool): If True, excludes clothes covered by other
|
||||
clothing from the returned list.
|
||||
|
||||
Returns:
|
||||
ordered_clothes_list (list): A list of clothing items worn by the
|
||||
given character, ordered according to
|
||||
the CLOTHING_TYPE_ORDER option specified
|
||||
in this module.
|
||||
"""
|
||||
clothes_list = []
|
||||
for thing in character.contents:
|
||||
# If uncovered or not excluding covered items
|
||||
if not thing.db.covered_by or exclude_covered is False:
|
||||
# If 'worn' is True, add to the list
|
||||
if thing.db.worn:
|
||||
clothes_list.append(thing)
|
||||
# Might as well put them in order here too.
|
||||
ordered_clothes_list = order_clothes_list(clothes_list)
|
||||
return ordered_clothes_list
|
||||
|
||||
|
||||
def clothing_type_count(clothes_list):
|
||||
"""
|
||||
Returns a dictionary of the number of each clothing type
|
||||
in a given list of clothing objects.
|
||||
|
||||
Args:
|
||||
clothes_list (list): A list of clothing items from which
|
||||
to count the number of clothing types
|
||||
represented among them.
|
||||
|
||||
Returns:
|
||||
types_count (dict): A dictionary of clothing types represented
|
||||
in the given list and the number of each
|
||||
clothing type represented.
|
||||
"""
|
||||
types_count = {}
|
||||
for garment in clothes_list:
|
||||
if garment.db.clothing_type:
|
||||
type = garment.db.clothing_type
|
||||
if type not in types_count.keys():
|
||||
types_count[type] = 1
|
||||
else:
|
||||
types_count[type] += 1
|
||||
return types_count
|
||||
|
||||
|
||||
def single_type_count(clothes_list, type):
|
||||
"""
|
||||
Returns an integer value of the number of a given type of clothing in a list.
|
||||
|
||||
Args:
|
||||
clothes_list (list): List of clothing objects to count from
|
||||
type (str): Clothing type to count
|
||||
|
||||
Returns:
|
||||
type_count (int): Number of garments of the specified type in the given
|
||||
list of clothing objects
|
||||
"""
|
||||
type_count = 0
|
||||
for garment in clothes_list:
|
||||
if garment.db.clothing_type:
|
||||
if garment.db.clothing_type == type:
|
||||
type_count += 1
|
||||
return type_count
|
||||
|
||||
|
||||
class Clothing(DefaultObject):
|
||||
|
||||
def wear(self, wearer, wearstyle, quiet=False):
|
||||
"""
|
||||
Sets clothes to 'worn' and optionally echoes to the room.
|
||||
|
||||
Args:
|
||||
wearer (obj): character object wearing this clothing object
|
||||
wearstyle (True or str): string describing the style of wear or True for none
|
||||
|
||||
Kwargs:
|
||||
quiet (bool): If false, does not message the room
|
||||
|
||||
Notes:
|
||||
Optionally sets db.worn with a 'wearstyle' that appends a short passage to
|
||||
the end of the name of the clothing to describe how it's worn that shows
|
||||
up in the wearer's desc - I.E. 'around his neck' or 'tied loosely around
|
||||
her waist'. If db.worn is set to 'True' then just the name will be shown.
|
||||
"""
|
||||
# Set clothing as worn
|
||||
self.db.worn = wearstyle
|
||||
# Auto-cover appropirate clothing types, as specified above
|
||||
to_cover = []
|
||||
if self.db.clothing_type and self.db.clothing_type in CLOTHING_TYPE_AUTOCOVER:
|
||||
for garment in get_worn_clothes(wearer):
|
||||
if garment.db.clothing_type and garment.db.clothing_type \
|
||||
in CLOTHING_TYPE_AUTOCOVER[self.db.clothing_type]:
|
||||
to_cover.append(garment)
|
||||
garment.db.covered_by = self
|
||||
# Return if quiet
|
||||
if quiet:
|
||||
return
|
||||
# Echo a message to the room
|
||||
message = "%s puts on %s" % (wearer, self.name)
|
||||
if wearstyle is not True:
|
||||
message = "%s wears %s %s" % (wearer, self.name, wearstyle)
|
||||
if to_cover:
|
||||
message = message + ", covering %s" % list_to_string(to_cover)
|
||||
wearer.location.msg_contents(message + ".")
|
||||
|
||||
def remove(self, wearer, quiet=False):
|
||||
"""
|
||||
Removes worn clothes and optionally echoes to the room.
|
||||
|
||||
Args:
|
||||
wearer (obj): character object wearing this clothing object
|
||||
|
||||
Kwargs:
|
||||
quiet (bool): If false, does not message the room
|
||||
"""
|
||||
self.db.worn = False
|
||||
remove_message = "%s removes %s." % (wearer, self.name)
|
||||
uncovered_list = []
|
||||
|
||||
# Check to see if any other clothes are covered by this object.
|
||||
for thing in wearer.contents:
|
||||
# If anything is covered by
|
||||
if thing.db.covered_by == self:
|
||||
thing.db.covered_by = False
|
||||
uncovered_list.append(thing.name)
|
||||
if len(uncovered_list) > 0:
|
||||
remove_message = "%s removes %s, revealing %s." % (wearer, self.name, list_to_string(uncovered_list))
|
||||
# Echo a message to the room
|
||||
if not quiet:
|
||||
wearer.location.msg_contents(remove_message)
|
||||
|
||||
def at_get(self, getter):
|
||||
"""
|
||||
Makes absolutely sure clothes aren't already set as 'worn'
|
||||
when they're picked up, in case they've somehow had their
|
||||
location changed without getting removed.
|
||||
"""
|
||||
self.db.worn = False
|
||||
|
||||
|
||||
class ClothedCharacter(DefaultCharacter):
|
||||
"""
|
||||
Character that displays worn clothing when looked at. You can also
|
||||
just copy the return_appearance hook defined below to your own game's
|
||||
character typeclass.
|
||||
"""
|
||||
def return_appearance(self, looker):
|
||||
"""
|
||||
This formats a description. It is the hook a 'look' command
|
||||
should call.
|
||||
|
||||
Args:
|
||||
looker (Object): Object doing the looking.
|
||||
|
||||
Notes:
|
||||
The name of every clothing item carried and worn by the character
|
||||
is appended to their description. If the clothing's db.worn value
|
||||
is set to True, only the name is appended, but if the value is a
|
||||
string, the string is appended to the end of the name, to allow
|
||||
characters to specify how clothing is worn.
|
||||
"""
|
||||
if not looker:
|
||||
return ""
|
||||
# get description, build string
|
||||
string = "|c%s|n\n" % self.get_display_name(looker)
|
||||
desc = self.db.desc
|
||||
worn_string_list = []
|
||||
clothes_list = get_worn_clothes(self, exclude_covered=True)
|
||||
# Append worn, uncovered clothing to the description
|
||||
for garment in clothes_list:
|
||||
# If 'worn' is True, just append the name
|
||||
if garment.db.worn is True:
|
||||
worn_string_list.append(garment.name)
|
||||
# Otherwise, append the name and the string value of 'worn'
|
||||
elif garment.db.worn:
|
||||
worn_string_list.append("%s %s" % (garment.name, garment.db.worn))
|
||||
if desc:
|
||||
string += "%s" % desc
|
||||
# Append worn clothes.
|
||||
if worn_string_list:
|
||||
string += "|/|/%s is wearing %s." % (self, list_to_string(worn_string_list))
|
||||
else:
|
||||
string += "|/|/%s is not wearing anything." % self
|
||||
return string
|
||||
|
||||
|
||||
# COMMANDS START HERE
|
||||
|
||||
class CmdWear(MuxCommand):
|
||||
"""
|
||||
Puts on an item of clothing you are holding.
|
||||
|
||||
Usage:
|
||||
wear <obj> [wear style]
|
||||
|
||||
Examples:
|
||||
wear shirt
|
||||
wear scarf wrapped loosely about the shoulders
|
||||
|
||||
All the clothes you are wearing are appended to your description.
|
||||
If you provide a 'wear style' after the command, the message you
|
||||
provide will be displayed after the clothing's name.
|
||||
"""
|
||||
|
||||
key = "wear"
|
||||
help_category = "clothing"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: wear <obj> [wear style]")
|
||||
return
|
||||
clothing = self.caller.search(self.arglist[0], candidates=self.caller.contents)
|
||||
wearstyle = True
|
||||
if not clothing:
|
||||
return
|
||||
if not clothing.is_typeclass("evennia.contrib.clothing.Clothing"):
|
||||
self.caller.msg("That's not clothes!")
|
||||
return
|
||||
|
||||
# Enforce overall clothing limit.
|
||||
if CLOTHING_OVERALL_LIMIT and len(get_worn_clothes(self.caller)) >= CLOTHING_OVERALL_LIMIT:
|
||||
self.caller.msg("You can't wear any more clothes.")
|
||||
return
|
||||
|
||||
# Apply individual clothing type limits.
|
||||
if clothing.db.clothing_type and not clothing.db.worn:
|
||||
type_count = single_type_count(get_worn_clothes(self.caller), clothing.db.clothing_type)
|
||||
if clothing.db.clothing_type in CLOTHING_TYPE_LIMIT.keys():
|
||||
if type_count >= CLOTHING_TYPE_LIMIT[clothing.db.clothing_type]:
|
||||
self.caller.msg("You can't wear any more clothes of the type '%s'." % clothing.db.clothing_type)
|
||||
return
|
||||
|
||||
if clothing.db.worn and len(self.arglist) == 1:
|
||||
self.caller.msg("You're already wearing %s!" % clothing.name)
|
||||
return
|
||||
if len(self.arglist) > 1: # If wearstyle arguments given
|
||||
wearstyle_list = self.arglist # Split arguments into a list of words
|
||||
del wearstyle_list[0] # Leave first argument (the clothing item) out of the wearstyle
|
||||
wearstring = ' '.join(str(e) for e in wearstyle_list) # Join list of args back into one string
|
||||
if WEARSTYLE_MAXLENGTH and len(wearstring) > WEARSTYLE_MAXLENGTH: # If length of wearstyle exceeds limit
|
||||
self.caller.msg("Please keep your wear style message to less than %i characters." % WEARSTYLE_MAXLENGTH)
|
||||
else:
|
||||
wearstyle = wearstring
|
||||
clothing.wear(self.caller, wearstyle)
|
||||
|
||||
|
||||
class CmdRemove(MuxCommand):
|
||||
"""
|
||||
Takes off an item of clothing.
|
||||
|
||||
Usage:
|
||||
remove <obj>
|
||||
|
||||
Removes an item of clothing you are wearing. You can't remove
|
||||
clothes that are covered up by something else - you must take
|
||||
off the covering item first.
|
||||
"""
|
||||
|
||||
key = "remove"
|
||||
help_category = "clothing"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
clothing = self.caller.search(self.args, candidates=self.caller.contents)
|
||||
if not clothing:
|
||||
return
|
||||
if not clothing.db.worn:
|
||||
self.caller.msg("You're not wearing that!")
|
||||
return
|
||||
if clothing.db.covered_by:
|
||||
self.caller.msg("You have to take off %s first." % clothing.db.covered_by.name)
|
||||
return
|
||||
clothing.remove(self.caller)
|
||||
|
||||
|
||||
class CmdCover(MuxCommand):
|
||||
"""
|
||||
Covers a worn item of clothing with another you're holding or wearing.
|
||||
|
||||
Usage:
|
||||
cover <obj> [with] <obj>
|
||||
|
||||
When you cover a clothing item, it is hidden and no longer appears in
|
||||
your description until it's uncovered or the item covering it is removed.
|
||||
You can't remove an item of clothing if it's covered.
|
||||
"""
|
||||
|
||||
key = "cover"
|
||||
help_category = "clothing"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
|
||||
if len(self.arglist) < 2:
|
||||
self.caller.msg("Usage: cover <worn clothing> [with] <clothing object>")
|
||||
return
|
||||
# Get rid of optional 'with' syntax
|
||||
if self.arglist[1].lower() == "with" and len(self.arglist) > 2:
|
||||
del self.arglist[1]
|
||||
to_cover = self.caller.search(self.arglist[0], candidates=self.caller.contents)
|
||||
cover_with = self.caller.search(self.arglist[1], candidates=self.caller.contents)
|
||||
if not to_cover or not cover_with:
|
||||
return
|
||||
if not to_cover.is_typeclass("evennia.contrib.clothing.Clothing"):
|
||||
self.caller.msg("%s isn't clothes!" % to_cover.name)
|
||||
return
|
||||
if not cover_with.is_typeclass("evennia.contrib.clothing.Clothing"):
|
||||
self.caller.msg("%s isn't clothes!" % cover_with.name)
|
||||
return
|
||||
if cover_with.db.clothing_type:
|
||||
if cover_with.db.clothing_type in CLOTHING_TYPE_CANT_COVER_WITH:
|
||||
self.caller.msg("You can't cover anything with that!")
|
||||
return
|
||||
if not to_cover.db.worn:
|
||||
self.caller.msg("You're not wearing %s!" % to_cover.name)
|
||||
return
|
||||
if to_cover == cover_with:
|
||||
self.caller.msg("You can't cover an item with itself!")
|
||||
return
|
||||
if cover_with.db.covered_by:
|
||||
self.caller.msg("%s is covered by something else!" % cover_with.name)
|
||||
return
|
||||
if to_cover.db.covered_by:
|
||||
self.caller.msg("%s is already covered by %s." % (cover_with.name, to_cover.db.covered_by.name))
|
||||
return
|
||||
if not cover_with.db.worn:
|
||||
cover_with.wear(self.caller, True) # Put on the item to cover with if it's not on already
|
||||
self.caller.location.msg_contents("%s covers %s with %s." % (self.caller, to_cover.name, cover_with.name))
|
||||
to_cover.db.covered_by = cover_with
|
||||
|
||||
|
||||
class CmdUncover(MuxCommand):
|
||||
"""
|
||||
Reveals a worn item of clothing that's currently covered up.
|
||||
|
||||
Usage:
|
||||
uncover <obj>
|
||||
|
||||
When you uncover an item of clothing, you allow it to appear in your
|
||||
description without having to take off the garment that's currently
|
||||
covering it. You can't uncover an item of clothing if the item covering
|
||||
it is also covered by something else.
|
||||
"""
|
||||
|
||||
key = "uncover"
|
||||
help_category = "clothing"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: uncover <worn clothing object>")
|
||||
return
|
||||
|
||||
to_uncover = self.caller.search(self.args, candidates=self.caller.contents)
|
||||
if not to_uncover:
|
||||
return
|
||||
if not to_uncover.db.worn:
|
||||
self.caller.msg("You're not wearing %s!" % to_uncover.name)
|
||||
return
|
||||
if not to_uncover.db.covered_by:
|
||||
self.caller.msg("%s isn't covered by anything!" % to_uncover.name)
|
||||
return
|
||||
covered_by = to_uncover.db.covered_by
|
||||
if covered_by.db.covered_by:
|
||||
self.caller.msg("%s is under too many layers to uncover." % (to_uncover.name))
|
||||
return
|
||||
self.caller.location.msg_contents("%s uncovers %s." % (self.caller, to_uncover.name))
|
||||
to_uncover.db.covered_by = None
|
||||
|
||||
|
||||
class CmdDrop(MuxCommand):
|
||||
"""
|
||||
drop something
|
||||
|
||||
Usage:
|
||||
drop <obj>
|
||||
|
||||
Lets you drop an object from your inventory into the
|
||||
location you are currently in.
|
||||
"""
|
||||
|
||||
key = "drop"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"""Implement command"""
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Drop what?")
|
||||
return
|
||||
|
||||
# Because the DROP command by definition looks for items
|
||||
# in inventory, call the search function using location = caller
|
||||
obj = caller.search(self.args, location=caller,
|
||||
nofound_string="You aren't carrying %s." % self.args,
|
||||
multimatch_string="You carry more than one %s:" % self.args)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
# This part is new!
|
||||
# You can't drop clothing items that are covered.
|
||||
if obj.db.covered_by:
|
||||
caller.msg("You can't drop that because it's covered by %s." % obj.db.covered_by)
|
||||
return
|
||||
# Remove clothes if they're dropped.
|
||||
if obj.db.worn:
|
||||
obj.remove(caller, quiet=True)
|
||||
|
||||
obj.move_to(caller.location, quiet=True)
|
||||
caller.msg("You drop %s." % (obj.name,))
|
||||
caller.location.msg_contents("%s drops %s." %
|
||||
(caller.name, obj.name),
|
||||
exclude=caller)
|
||||
# Call the object script's at_drop() method.
|
||||
obj.at_drop(caller)
|
||||
|
||||
|
||||
class CmdGive(MuxCommand):
|
||||
"""
|
||||
give away something to someone
|
||||
|
||||
Usage:
|
||||
give <inventory obj> = <target>
|
||||
|
||||
Gives an items from your inventory to another character,
|
||||
placing it in their inventory.
|
||||
"""
|
||||
key = "give"
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"\s|$"
|
||||
|
||||
def func(self):
|
||||
"""Implement give"""
|
||||
|
||||
caller = self.caller
|
||||
if not self.args or not self.rhs:
|
||||
caller.msg("Usage: give <inventory object> = <target>")
|
||||
return
|
||||
to_give = caller.search(self.lhs, location=caller,
|
||||
nofound_string="You aren't carrying %s." % self.lhs,
|
||||
multimatch_string="You carry more than one %s:" % self.lhs)
|
||||
target = caller.search(self.rhs)
|
||||
if not (to_give and target):
|
||||
return
|
||||
if target == caller:
|
||||
caller.msg("You keep %s to yourself." % to_give.key)
|
||||
return
|
||||
if not to_give.location == caller:
|
||||
caller.msg("You are not holding %s." % to_give.key)
|
||||
return
|
||||
# This is new! Can't give away something that's worn.
|
||||
if to_give.db.covered_by:
|
||||
caller.msg("You can't give that away because it's covered by %s." % to_give.db.covered_by)
|
||||
return
|
||||
# Remove clothes if they're given.
|
||||
if to_give.db.worn:
|
||||
to_give.remove(caller)
|
||||
to_give.move_to(caller.location, quiet=True)
|
||||
# give object
|
||||
caller.msg("You give %s to %s." % (to_give.key, target.key))
|
||||
to_give.move_to(target, quiet=True)
|
||||
target.msg("%s gives you %s." % (caller.key, to_give.key))
|
||||
# Call the object script's at_give() method.
|
||||
to_give.at_give(caller, target)
|
||||
|
||||
|
||||
class CmdInventory(MuxCommand):
|
||||
"""
|
||||
view inventory
|
||||
|
||||
Usage:
|
||||
inventory
|
||||
inv
|
||||
|
||||
Shows your inventory.
|
||||
"""
|
||||
# Alternate version of the inventory command which separates
|
||||
# worn and carried items.
|
||||
|
||||
key = "inventory"
|
||||
aliases = ["inv", "i"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"$"
|
||||
|
||||
def func(self):
|
||||
"""check inventory"""
|
||||
if not self.caller.contents:
|
||||
self.caller.msg("You are not carrying or wearing anything.")
|
||||
return
|
||||
|
||||
items = self.caller.contents
|
||||
|
||||
carry_table = evtable.EvTable(border="header")
|
||||
wear_table = evtable.EvTable(border="header")
|
||||
for item in items:
|
||||
if not item.db.worn:
|
||||
carry_table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
||||
if carry_table.nrows == 0:
|
||||
carry_table.add_row("|CNothing.|n", "")
|
||||
string = "|wYou are carrying:\n%s" % carry_table
|
||||
for item in items:
|
||||
if item.db.worn:
|
||||
wear_table.add_row("|C%s|n" % item.name, item.db.desc or "")
|
||||
if wear_table.nrows == 0:
|
||||
wear_table.add_row("|CNothing.|n", "")
|
||||
string += "|/|wYou are wearing:\n%s" % wear_table
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class ClothedCharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
"""
|
||||
Command set for clothing, including new versions of 'give' and 'drop'
|
||||
that take worn and covered clothing into account, as well as a new
|
||||
version of 'inventory' that differentiates between carried and worn
|
||||
items.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super(ClothedCharacterCmdSet, self).at_cmdset_creation()
|
||||
#
|
||||
# any commands you add below will overload the default ones.
|
||||
#
|
||||
self.add(CmdWear())
|
||||
self.add(CmdRemove())
|
||||
self.add(CmdCover())
|
||||
self.add(CmdUncover())
|
||||
self.add(CmdGive())
|
||||
self.add(CmdDrop())
|
||||
self.add(CmdInventory())
|
||||
869
evennia/contrib/events/README.md
Normal file
869
evennia/contrib/events/README.md
Normal file
|
|
@ -0,0 +1,869 @@
|
|||
# Evennia event system
|
||||
|
||||
Vincent Le Goff 2017
|
||||
|
||||
This contrib adds the system of events in Evennia, allowing immortals (or other trusted builders) to
|
||||
dynamically add features to individual objects. Using events, every immortal or privileged users
|
||||
could have a specific room, exit, character, object or something else behave differently from its
|
||||
"cousins". For these familiar with the use of softcode in MU`*`, like SMAUG MudProgs, the ability to
|
||||
add arbitrary behavior to individual objects is a step toward freedom. Keep in mind, however, the
|
||||
warning below, and read it carefully before the rest of the documentation.
|
||||
|
||||
## A WARNING REGARDING SECURITY
|
||||
|
||||
Evennia's event system will run arbitrary Python code without much restriction. Such a system is as
|
||||
powerful as potentially dangerous, and you will have to keep in mind these points before deciding to
|
||||
install it:
|
||||
|
||||
1. Untrusted people can run Python code on your game server with this system. Be careful about who
|
||||
can use this system (see the permissions below).
|
||||
2. You can do all of this in Python outside the game. The event system is not to replace all your
|
||||
game feature.
|
||||
|
||||
## Basic structure and vocabulary
|
||||
|
||||
- At the basis of the event system are **events**. An **event** defines the context in which we
|
||||
would like to call some arbitrary code. For instance, one event is defined on exits and will fire
|
||||
every time a character traverses through this exit. Events are described on a
|
||||
[typeclass](https://github.com/evennia/evennia/wiki/Typeclasses) (like
|
||||
[exits](https://github.com/evennia/evennia/wiki/Objects#exits) in our example). All objects
|
||||
inheriting from this typeclass will have access to this event.
|
||||
- **Callbacks** can be set on individual objects, on events defined in code. These **callbacks**
|
||||
can contain arbitrary code and describe a specific behavior for an object. When the event fires,
|
||||
all callbacks connected to this object's event are executed.
|
||||
|
||||
To see the system in context, when an object is picked up (using the default `get` command), a
|
||||
specific event is fired:
|
||||
|
||||
1. The event "get" is set on objects (on the `Object` typeclass).
|
||||
2. When using the "get" command to pick up an object, this object's `at_get` hook is called.
|
||||
3. A modified hook of DefaultObject is set by the event system. This hook will execute (or call)
|
||||
the "get" event on this object.
|
||||
4. All callbacks tied to this object's "get" event will be executed in order. These callbacks act
|
||||
as functions containing Python code that you can write in-game, using specific variables that
|
||||
will be listed when you edit the callback itself.
|
||||
5. In individual callbacks, you can add multiple lines of Python code that will be fired at this
|
||||
point. In this example, the `character` variable will contain the character who has picked up
|
||||
the object, while `obj` will contain the object that was picked up.
|
||||
|
||||
Following this example, if you create a callback "get" on the object "a sword", and put in it:
|
||||
|
||||
```python
|
||||
character.msg("You have picked up {} and have completed this quest!".format(obj.get_display_name(character)))
|
||||
```
|
||||
|
||||
When you pick up this object you should see something like:
|
||||
|
||||
You pick up a sword.
|
||||
You have picked up a sword and have completed this quest!
|
||||
|
||||
## Installation
|
||||
|
||||
Being in a separate contrib, the event system isn't installed by default. You need to do it
|
||||
manually, following these steps:
|
||||
|
||||
1. Launch the main script (important!):
|
||||
```@py evennia.create_script("evennia.contrib.events.scripts.EventHandler")```
|
||||
2. Set the permissions (optional):
|
||||
- `EVENTS_WITH_VALIDATION`: a group that can edit callbacks, but will need approval (default to
|
||||
`None`).
|
||||
- `EVENTS_WITHOUT_VALIDATION`: a group with permission to edit callbacks without need of
|
||||
validation (default to `"immortals"`).
|
||||
- `EVENTS_VALIDATING`: a group that can validate callbacks (default to `"immortals"`).
|
||||
- `EVENTS_CALENDAR`: type of the calendar to be used (either `None`, `"standard"` or `"custom"`,
|
||||
default to `None`).
|
||||
3. Add the `@call` command.
|
||||
4. Inherit from the custom typeclasses of the event system.
|
||||
- `evennia.contrib.events.typeclasses.EventCharacter`: to replace `DefaultCharacter`.
|
||||
- `evennia.contrib.events.typeclasses.EventExit`: to replace `DefaultExit`.
|
||||
- `evennia.contrib.events.typeclasses.EventObject`: to replace `DefaultObject`.
|
||||
- `evennia.contrib.events.typeclasses.EventRoom`: to replace `DefaultRoom`.
|
||||
|
||||
The following sections describe in details each step of the installation.
|
||||
|
||||
> Note: If you were to start the game without having started the main script (such as when
|
||||
resetting your database) you will most likely face a traceback when logging in, telling you
|
||||
that a 'callback' property is not defined. After performing step `1` the error will go away.
|
||||
|
||||
### Starting the event script
|
||||
|
||||
To start the event script, you only need a single command, using `@py`.
|
||||
|
||||
@py evennia.create_script("evennia.contrib.events.scripts.EventHandler")
|
||||
|
||||
This command will create a global script (that is, a script independent from any object). This
|
||||
script will hold basic configuration, individual callbacks and so on. You may access it directly,
|
||||
but you will probably use the callback handler. Creating this script will also create a `callback`
|
||||
handler on all objects (see below for details).
|
||||
|
||||
### Editing permissions
|
||||
|
||||
This contrib comes with its own set of permissions. They define who can edit callbacks without
|
||||
validation, and who can edit callbacks but needs validation. Validation is a process in which an
|
||||
administrator (or somebody trusted as such) will check the callbacks produced by others and will
|
||||
accept or reject them. If accepted, the callbacks are connected, otherwise they are never run.
|
||||
|
||||
By default, callbacks can only be created by immortals: no one except the immortals can edit
|
||||
callbacks, and immortals don't need validation. It can easily be changed, either through settings
|
||||
or dynamically by changing permissions of users.
|
||||
|
||||
The events contrib adds three
|
||||
[permissions](https://github.com/evennia/evennia/wiki/Locks#permissions) in the settings. You can
|
||||
override them by changing the settings into your `server/conf/settings.py` file (see below for an
|
||||
example). The settings defined in the events contrib are:
|
||||
|
||||
- `EVENTS_WITH_VALIDATION`: this defines a permission that can edit callbacks, but will need
|
||||
approval. If you set this to `"wizards"`, for instance, users with the permission `"wizards"`
|
||||
will be able to edit callbacks. These callbacks will not be connected, though, and will need to be
|
||||
checked and approved by an administrator. This setting can contain `None`, meaning that no user is
|
||||
allowed to edit callbacks with validation.
|
||||
- `EVENTS_WITHOUT_VALIDATION`: this setting defines a permission allowing editing of callbacks
|
||||
without needing validation. By default, this setting is set to `"immortals"`. It means that
|
||||
immortals can edit callbacks, and they will be connected when they leave the editor, without needing
|
||||
approval.
|
||||
- `EVENTS_VALIDATING`: this last setting defines who can validate callbacks. By default, this is
|
||||
set to `"immortals"`, meaning only immortals can see callbacks needing validation, accept or
|
||||
reject them.
|
||||
|
||||
You can override all these settings in your `server/conf/settings.py` file. For instance:
|
||||
|
||||
```python
|
||||
# ... other settings ...
|
||||
|
||||
# Event settings
|
||||
EVENTS_WITH_VALIDATION = "wizards"
|
||||
EVENTS_WITHOUT_VALIDATION = "immortals"
|
||||
EVENTS_VALIDATING = "immortals"
|
||||
```
|
||||
|
||||
In addition, there is another setting that must be set if you plan on using the time-related events
|
||||
(events that are scheduled at specific, in-game times). You would need to specify the type of
|
||||
calendar you are using. By default, time-related events are disabled. You can change the
|
||||
`EVENTS_CALENDAR` to set it to:
|
||||
|
||||
- `"standard"`: the standard calendar, with standard days, months, years and so on.
|
||||
- `"custom"`: a custom calendar that will use the
|
||||
[custom_gametime](https://github.com/evennia/evennia/blob/master/evennia/contrib/custom_gametime.py)
|
||||
contrib to schedule events.
|
||||
|
||||
This contrib defines two additional permissions that can be set on individual users:
|
||||
|
||||
- `events_without_validation`: this would give this user the rights to edit callbacks but not
|
||||
require validation before they are connected.
|
||||
- `events_validating`: this permission allows this user to run validation checks on callbacks
|
||||
needing to be validated.
|
||||
|
||||
For instance, to give the right to edit callbacks without needing approval to the player 'kaldara',
|
||||
you might do something like:
|
||||
|
||||
@perm *kaldara = events_without_validation
|
||||
|
||||
To remove this same permission, just use the `/del` switch:
|
||||
|
||||
@perm/del *kaldara = events_without_validation
|
||||
|
||||
The rights to use the `@call` command are directly related to these permissions: by default, only
|
||||
users who have the `events_without_validation` permission or are in (or above) the group defined in
|
||||
the `EVENTS_WITH_VALIDATION` setting will be able to call the command (with different switches).
|
||||
|
||||
### Adding the `@call` command
|
||||
|
||||
You also have to add the `@call` command to your Character CmdSet. This command allows your users
|
||||
to add, edit and delete callbacks in-game. In your `commands/default_cmdsets, it might look like
|
||||
this:
|
||||
|
||||
```python
|
||||
from evennia import default_cmds
|
||||
from evennia.contrib.events.commands import CmdCallback
|
||||
|
||||
class CharacterCmdSet(default_cmds.CharacterCmdSet):
|
||||
"""
|
||||
The `CharacterCmdSet` contains general in-game commands like `look`,
|
||||
`get`, etc available on in-game Character objects. It is merged with
|
||||
the `PlayerCmdSet` when a Player puppets a Character.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
super(CharacterCmdSet, self).at_cmdset_creation()
|
||||
self.add(CmdCallback())
|
||||
```
|
||||
|
||||
### Changing parent classes of typeclasses
|
||||
|
||||
Finally, to use the event system, you need to have your typeclasses inherit from the modified event
|
||||
classes. For instance, in your `typeclasses/characters.py` module, you should change inheritance
|
||||
like this:
|
||||
|
||||
```python
|
||||
from evennia.contrib.events.typeclasses import EventCharacter
|
||||
|
||||
class Character(EventCharacter):
|
||||
|
||||
# ...
|
||||
```
|
||||
|
||||
You should do the same thing for your rooms, exits and objects. Note that the event system works by
|
||||
overriding some hooks. Some of these features might not be accessible in your game if you don't
|
||||
call the parent methods when overriding hooks.
|
||||
|
||||
## Using the `@call` command
|
||||
|
||||
The event system relies, to a great extent, on its `@call` command. Who can execute this command,
|
||||
and who can do what with it, will depend on your set of permissions.
|
||||
|
||||
The `@call` command allows to add, edit and delete callbacks on specific objects' events. The event
|
||||
system can be used on most Evennia objects, mostly typeclassed objects (excluding players). The
|
||||
first argument of the `@call` command is the name of the object you want to edit. It can also be
|
||||
used to know what events are available for this specific object.
|
||||
|
||||
### Examining callbacks and events
|
||||
|
||||
To see the events connected to an object, use the `@call` command and give the name or ID of the
|
||||
object to examine. For instance, @call here` to examine the events on your current location. Or
|
||||
`@call self` to see the events on yourself.
|
||||
|
||||
This command will display a table, containing:
|
||||
|
||||
- The name of each event in the first column.
|
||||
- The number of callbacks of this name, and the number of total lines of these callbacks in the
|
||||
second column.
|
||||
- A short help to tell you when the event is triggered in the third column.
|
||||
|
||||
If you execute `@call #1` for instance, you might see a table like this:
|
||||
|
||||
```
|
||||
+------------------+---------+-----------------------------------------------+
|
||||
| Event name | Number | Description |
|
||||
+~~~~~~~~~~~~~~~~~~+~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
||||
| can_delete | 0 (0) | Can the character be deleted? |
|
||||
| can_move | 0 (0) | Can the character move? |
|
||||
| can_part | 0 (0) | Can the departing character leave this room? |
|
||||
| delete | 0 (0) | Before deleting the character. |
|
||||
| greet | 0 (0) | A new character arrives in the location of |
|
||||
| | | this character. |
|
||||
| move | 0 (0) | After the character has moved into its new |
|
||||
| | | room. |
|
||||
| puppeted | 0 (0) | When the character has been puppeted by a |
|
||||
| | | player. |
|
||||
| time | 0 (0) | A repeated event to be called regularly. |
|
||||
| unpuppeted | 0 (0) | When the character is about to be un- |
|
||||
| | | puppeted. |
|
||||
+------------------+---------+-----------------------------------------------+
|
||||
```
|
||||
|
||||
### Creating a new callback
|
||||
|
||||
The `/add` switch should be used to add a callback. It takes two arguments beyond the object's
|
||||
name/DBREF:
|
||||
|
||||
1. After an = sign, the name of the event to be edited (if not supplied, will display the list of
|
||||
possible events, like above).
|
||||
2. The parameters (optional).
|
||||
|
||||
We'll see callbacks with parameters later. For the time being, let's try to prevent a character
|
||||
from going through the "north" exit of this room:
|
||||
|
||||
```
|
||||
@call north
|
||||
+------------------+---------+-----------------------------------------------+
|
||||
| Event name | Number | Description |
|
||||
+~~~~~~~~~~~~~~~~~~+~~~~~~~~~+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~+
|
||||
| can_traverse | 0 (0) | Can the character traverse through this exit? |
|
||||
| msg_arrive | 0 (0) | Customize the message when a character |
|
||||
| | | arrives through this exit. |
|
||||
| msg_leave | 0 (0) | Customize the message when a character leaves |
|
||||
| | | through this exit. |
|
||||
| time | 0 (0) | A repeated event to be called regularly. |
|
||||
| traverse | 0 (0) | After the character has traversed through |
|
||||
| | | this exit. |
|
||||
+------------------+---------+-----------------------------------------------+
|
||||
```
|
||||
|
||||
If we want to prevent a character from traversing through this exit, the best event for us would be
|
||||
"can_traverse".
|
||||
|
||||
> Why not "traverse"? If you read the description of both events, you will see "traverse" is called
|
||||
**after** the character has traversed through this exit. It would be too late to prevent it. On
|
||||
> the other hand, "can_traverse" is obviously checked before the character traverses.
|
||||
|
||||
When we edit the event, we have some more information:
|
||||
|
||||
@call/add north = can_traverse
|
||||
|
||||
Can the character traverse through this exit?
|
||||
This event is called when a character is about to traverse this
|
||||
exit. You can use the deny() eventfunc to deny the character from
|
||||
exiting for this time.
|
||||
|
||||
Variables you can use in this event:
|
||||
|
||||
- character: the character that wants to traverse this exit.
|
||||
- exit: the exit to be traversed.
|
||||
- room: the room in which stands the character before moving.
|
||||
|
||||
The section dedicated to [eventfuncs](#the-eventfuncs) will elaborate on the `deny()` function and
|
||||
other eventfuncs. Let us say, for the time being, that it can prevent an action (in this case, it
|
||||
can prevent the character from traversing through this exit). In the editor that opened when you
|
||||
used `@call/add`, you can type something like:
|
||||
|
||||
```python
|
||||
if character.id == 1:
|
||||
character.msg("You're the superuser, 'course I'll let you pass.")
|
||||
else:
|
||||
character.msg("Hold on, what do you think you're doing?")
|
||||
deny()
|
||||
```
|
||||
|
||||
You can now enter `:wq` to leave the editor by saving the callback.
|
||||
|
||||
If you enter `@call north`, you should see that "can_traverse" now has an active callback. You can
|
||||
use `@call north = can_traverse` to see more details on the connected callbacks:
|
||||
|
||||
```
|
||||
@call north = can_traverse
|
||||
+--------------+--------------+----------------+--------------+--------------+
|
||||
| Number | Author | Updated | Param | Valid |
|
||||
+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+~~~~~~~~~~~~~~+
|
||||
| 1 | XXXXX | 5 seconds ago | | Yes |
|
||||
+--------------+--------------+----------------+--------------+--------------+
|
||||
```
|
||||
|
||||
The left column contains callback numbers. You can use them to have even more information on a
|
||||
specific event. Here, for instance:
|
||||
|
||||
```
|
||||
@call north = can_traverse 1
|
||||
Callback can_traverse 1 of north:
|
||||
Created by XXXXX on 2017-04-02 17:58:05.
|
||||
Updated by XXXXX on 2017-04-02 18:02:50
|
||||
This callback is connected and active.
|
||||
Callback code:
|
||||
if character.id == 1:
|
||||
character.msg("You're the superuser, 'course I'll let you pass.")
|
||||
else:
|
||||
character.msg("Hold on, what do you think you're doing?")
|
||||
deny()
|
||||
```
|
||||
|
||||
Then try to walk through this exit. Do it with another character if possible, too, to see the
|
||||
difference.
|
||||
|
||||
### Editing and removing a callback
|
||||
|
||||
You can use the `/edit` switch to the `@call` command to edit a callback. You should provide, after
|
||||
the name of the object to edit and the equal sign:
|
||||
|
||||
1. The name of the event (as seen above).
|
||||
2. A number, if several callbacks are connected at this location.
|
||||
|
||||
You can type `@call/edit <object> = <event name>` to see the callbacks that are linked at this
|
||||
location. If there is only one callback, it will be opened in the editor; if more are defined, you
|
||||
will be asked for a number to provide (for instance, `@call/edit north = can_traverse 2`).
|
||||
|
||||
The command `@call` also provides a `/del` switch to remove a callback. It takes the same arguments
|
||||
as the `/edit` switch.
|
||||
|
||||
When removed, callbacks are logged, so an administrator can retrieve its content, assuming the
|
||||
`/del` was an error.
|
||||
|
||||
### The code editor
|
||||
|
||||
When adding or editing a callback, the event editor should open in code mode. The additional
|
||||
options supported by the editor in this mode are describe in [a dedicated section of the EvEditor's
|
||||
documentation](https://github.com/evennia/evennia/wiki/EvEditor#the-eveditor-to-edit-code).
|
||||
|
||||
## Using events
|
||||
|
||||
The following sections describe how to use events for various tasks, from the most simple to the
|
||||
most complex.
|
||||
|
||||
### The eventfuncs
|
||||
|
||||
In order to make development a little easier, the event system provides eventfuncs to be used in
|
||||
callbacks themselves. You don't have to use them, they are just shortcuts. An eventfunc is just a
|
||||
simple function that can be used inside of your callback code.
|
||||
|
||||
Function | Argument | Description | Example
|
||||
-----------|--------------------------|-----------------------------------|--------
|
||||
deny | `()` | Prevent an action from happening. | `deny()`
|
||||
get | `(**kwargs)` | Get a single object. | `char = get(id=1)`
|
||||
call_event | `(obj, name, seconds=0)` | Call another event. | `call_event(char, "chain_1", 20)`
|
||||
|
||||
#### deny
|
||||
|
||||
The `deny()` function allows to interrupt the callback and the action that called it. In the
|
||||
`can_*` events, it can be used to prevent the action from happening. For instance, in `can_say` on
|
||||
rooms, it can prevent the character from saying something in the room. One could have a `can_eat`
|
||||
event set on food that would prevent this character from eating this food.
|
||||
|
||||
Behind the scenes, the `deny()` function raises an exception that is being intercepted by the
|
||||
handler of events. The handler will then report that the action was cancelled.
|
||||
|
||||
#### get
|
||||
|
||||
The `get` eventfunc is a shortcut to get a single object with a specific identity. It's often used
|
||||
to retrieve an object with a given ID. In the section dedicated to [chained
|
||||
events](#chained-events), you will see a concrete example of this function in action.
|
||||
|
||||
#### call_event
|
||||
|
||||
Some callbacks will call other events. It is particularly useful for [chained
|
||||
events](#chained-events) that are described in a dedicated section. This eventfunc is used to call
|
||||
another event, immediately or in a defined time.
|
||||
|
||||
You need to specify as first parameter the object containing the event. The second parameter is the
|
||||
name of the event to call. The third parameter is the number of seconds before calling this event.
|
||||
By default, this parameter is set to 0 (the event is called immediately).
|
||||
|
||||
### Variables in callbacks
|
||||
|
||||
In the Python code you will enter in individual callbacks, you will have access to variables in your
|
||||
locals. These variables will depend on the event, and will be clearly listed when you add or edit a
|
||||
callback. As you've seen in the previous example, when we manipulate characters or character
|
||||
actions, we often have a `character` variable that holds the character doing the action.
|
||||
|
||||
In most cases, when an event is fired, all callbacks from this event are called. Variables are
|
||||
created for each event. Sometimes, however, the callback will execute and then ask for a variable
|
||||
in your locals: in other words, some callbacks can alter the actions being performed by changing
|
||||
values of variables. This is always clearly specified in the help of the event.
|
||||
|
||||
One example that will illustrate this system is the "msg_leave" event that can be set on exits.
|
||||
This event can alter the message that will be sent to other characters when someone leaves through
|
||||
this exit.
|
||||
|
||||
@call/add down = msg_leave
|
||||
|
||||
Which should display:
|
||||
|
||||
```
|
||||
Customize the message when a character leaves through this exit.
|
||||
This event is called when a character leaves through this exit.
|
||||
To customize the message that will be sent to the room where the
|
||||
character came from, change the value of the variable "message"
|
||||
to give it your custom message. The character itself will not be
|
||||
notified. You can use mapping between braces, like this:
|
||||
message = "{character} falls into a hole!"
|
||||
In your mapping, you can use {character} (the character who is
|
||||
about to leave), {exit} (the exit), {origin} (the room in which
|
||||
the character is), and {destination} (the room in which the character
|
||||
is heading for). If you need to customize the message with other
|
||||
information, you can also set "message" to None and send something
|
||||
else instead.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who is leaving through this exit.
|
||||
exit: the exit being traversed.
|
||||
origin: the location of the character.
|
||||
destination: the destination of the character.
|
||||
message: the message to be displayed in the location.
|
||||
mapping: a dictionary containing additional mapping.
|
||||
```
|
||||
|
||||
If you write something like this in your event:
|
||||
|
||||
```python
|
||||
message = "{character} falls into a hole in the ground!"
|
||||
```
|
||||
|
||||
And if the character Wilfred takes this exit, others in the room will see:
|
||||
|
||||
Wildred falls into a hole in the ground!
|
||||
|
||||
In this case, the event system placed the variable "message" in the callback locals, but will read
|
||||
from it when the event has been executed.
|
||||
|
||||
### Callbacks with parameters
|
||||
|
||||
Some callbacks are called without parameter. It has been the case for all examples we have seen
|
||||
before. In some cases, you can create callbacks that are triggered under only some conditions. A
|
||||
typical example is the room's "say" event. This event is triggered when somebody says something in
|
||||
the room. Individual callbacks set on this event can be configured to fire only when some words are
|
||||
used in the sentence.
|
||||
|
||||
For instance, let's say we want to create a cool voice-operated elevator. You enter into the
|
||||
elevator and say the floor number... and the elevator moves in the right direction. In this case,
|
||||
we could create an callback with the parameter "one":
|
||||
|
||||
@call/add here = say one
|
||||
|
||||
This callback will only fire when the user says a sentence that contains "one".
|
||||
|
||||
But what if we want to have a callback that would fire if the user says 1 or one? We can provide
|
||||
several parameters, separated by a comma.
|
||||
|
||||
@call/add here = say 1, one
|
||||
|
||||
Or, still more keywords:
|
||||
|
||||
@call/add here = say 1, one, ground
|
||||
|
||||
This time, the user could say something like "take me to the ground floor" ("ground" is one of our
|
||||
keywords defined in the above callback).
|
||||
|
||||
Not all events can take parameters, and these who do have different ways of handling them. There
|
||||
isn't a single meaning to parameters that could apply to all events. Refer to the event
|
||||
documentation for details.
|
||||
|
||||
> If you get confused between callback variables and parameters, think of parameters as checks
|
||||
> performed before the callback is run. Event with parameters will only fire some specific
|
||||
> callbacks, not all of them.
|
||||
|
||||
### Time-related events
|
||||
|
||||
Events are usually linked to commands, as we saw before. However, this is not always the case.
|
||||
Events can be triggered by other actions and, as we'll see later, could even be called from inside
|
||||
other events!
|
||||
|
||||
There is a specific event, on all objects, that can trigger at a specific time. It's an event with
|
||||
a mandatory parameter, which is the time you expect this event to fire.
|
||||
|
||||
For instance, let's add an event on this room that should trigger every day, at precisely 12:00 PM
|
||||
(the time is given as game time, not real time):
|
||||
|
||||
@call here = time 12:00
|
||||
|
||||
```python
|
||||
# This will be called every MUD day at 12:00 PM
|
||||
room.msg_contents("It's noon, time to have lunch!")
|
||||
```
|
||||
|
||||
Now, at noon every MUD day, this event will fire and this callback will be executed. You can use
|
||||
this event on every kind of typeclassed object, to have a specific action done every MUD day at the
|
||||
same time.
|
||||
|
||||
Time-related events can be much more complex than this. They can trigger every in-game hour or more
|
||||
often (it might not be a good idea to have events trigger that often on a lot of objects). You can
|
||||
have events that run every in-game week or month or year. It will greatly vary depending on the
|
||||
type of calendar used in your game. The number of time units is described in the game
|
||||
configuration.
|
||||
|
||||
With a standard calendar, for instance, you have the following units: minutes, hours, days, months
|
||||
and years. You will specify them as numbers separated by either a colon (:), a space ( ), or a dash
|
||||
(-). Pick whatever feels more appropriate (usually, we separate hours and minutes with a colon, the
|
||||
other units with a dash).
|
||||
|
||||
Some examples of syntax:
|
||||
|
||||
- `18:30`: every day at 6:30 PM.
|
||||
- `01 12:00`: every month, the first day, at 12 PM.
|
||||
- `06-15 09:58`: every year, on the 15th of June (month comes before day), at 9:58 AM.
|
||||
- `2025-01-01 00:00`: January 1st, 2025 at midnight (obviously, this will trigger only once).
|
||||
|
||||
Notice that we specify units in the reverse order (year, month, day, hour and minute) and separate
|
||||
them with logical separators. The smallest unit that is not defined is going to set how often the
|
||||
event should fire. That's why, if you use `12:00`, the smallest unit that is not defined is "day":
|
||||
the event will fire every day at the specified time.
|
||||
|
||||
> You can use chained events (see below) in conjunction with time-related events to create more
|
||||
random or frequent actions in events.
|
||||
|
||||
### Chained events
|
||||
|
||||
Callbacks can call other events, either now or a bit later. It is potentially very powerful.
|
||||
|
||||
To use chained events, just use the `call_event` eventfunc. It takes 2-3 arguments:
|
||||
|
||||
- The object containing the event.
|
||||
- The name of the event to call.
|
||||
- Optionally, the number of seconds to wait before calling this event.
|
||||
|
||||
All objects have events that are not triggered by commands or game-related operations. They are
|
||||
called "chain_X", like "chain_1", "chain_2", "chain_3" and so on. You can give them more specific
|
||||
names, as long as it begins by "chain_", like "chain_flood_room".
|
||||
|
||||
Rather than a long explanation, let's look at an example: a subway that will go from one place to
|
||||
the next at regular times. Connecting exits (opening its doors), waiting a bit, closing them,
|
||||
rolling around and stopping at a different station. That's quite a complex set of callbacks, as it
|
||||
is, but let's only look at the part that opens and closes the doors:
|
||||
|
||||
@call/add here = time 10:00
|
||||
|
||||
```python
|
||||
# At 10:00 AM, the subway arrives in the room of ID 22.
|
||||
# Notice that exit #23 and #24 are respectively the exit leading
|
||||
# on the platform and back in the subway.
|
||||
station = get(id=22)
|
||||
to_exit = get(id=23)
|
||||
back_exit = get(id=24)
|
||||
|
||||
# Open the door
|
||||
to_exit.name = "platform"
|
||||
to_exit.aliases = ["p"]
|
||||
to_exit.location = room
|
||||
to_exit.destination = station
|
||||
back_exit.name = "subway"
|
||||
back_exit.location = station
|
||||
back_exit.destination = room
|
||||
|
||||
# Display some messages
|
||||
room.msg_contents("The doors open and wind gushes in the subway")
|
||||
station.msg_contents("The doors of the subway open with a dull clank.")
|
||||
|
||||
# Set the doors to close in 20 seconds
|
||||
call_event(room, "chain_1", 20)
|
||||
```
|
||||
|
||||
This callback will:
|
||||
|
||||
1. Be called at 10:00 AM (specify 22:00 to set it to 10:00 PM).
|
||||
2. Set an exit between the subway and the station. Notice that the exits already exist (you will
|
||||
not have to create them), but they don't need to have specific location and destination.
|
||||
3. Display a message both in the subway and on the platform.
|
||||
4. Call the event "chain_1" to execute in 20 seconds.
|
||||
|
||||
And now, what should we have in "chain_1"?
|
||||
|
||||
@call/add here = chain_1
|
||||
|
||||
```python
|
||||
# Close the doors
|
||||
to_exit.location = None
|
||||
to_exit.destination = None
|
||||
back_exit.location = None
|
||||
back_exit.destination = None
|
||||
room.msg_content("After a short warning signal, the doors close and the subway begins moving.")
|
||||
station.msg_content("After a short warning signal, the doors close and the subway begins moving.")
|
||||
```
|
||||
|
||||
Behind the scenes, the `call_event` function freezes all variables ("room", "station", "to_exit",
|
||||
"back_exit" in our example), so you don't need to define them again.
|
||||
|
||||
A word of caution on callbacks that call chained events: it isn't impossible for a callback to call
|
||||
itself at some recursion level. If `chain_1` calls `chain_2` that calls `chain_3` that calls
|
||||
`chain_`, particularly if there's no pause between them, you might run into an infinite loop.
|
||||
|
||||
Be also careful when it comes to handling characters or objects that may very well move during your
|
||||
pause between event calls. When you use `call_event()`, the MUD doesn't pause and commands can be
|
||||
entered by players, fortunately. It also means that, a character could start an event that pauses
|
||||
for awhile, but be gone when the chained event is called. You need to check that, even lock the
|
||||
character into place while you are pausing (some actions should require locking) or at least,
|
||||
checking that the character is still in the room, for it might create illogical situations if you
|
||||
don't.
|
||||
|
||||
> Chained events are a special case: contrary to standard events, they are created in-game, not
|
||||
through code. They usually contain only one callback, although nothing prevents you from creating
|
||||
several chained events in the same object.
|
||||
|
||||
## Using events in code
|
||||
|
||||
This section describes callbacks and events from code, how to create new events, how to call them in
|
||||
a command, and how to handle specific cases like parameters.
|
||||
|
||||
Along this section, we will see how to implement the following example: we would like to create a
|
||||
"push" command that could be used to push objects. Objects could react to this command and have
|
||||
specific events fired.
|
||||
|
||||
### Adding new events
|
||||
|
||||
Adding new events should be done in your typeclasses. Events are contained in the `_events` class
|
||||
variable, a dictionary of event names as keys, and tuples to describe these events as values. You
|
||||
also need to register this class, to tell the event system that it contains events to be added to
|
||||
this typeclass.
|
||||
|
||||
Here, we want to add a "push" event on objects. In your `typeclasses/objects.py` file, you should
|
||||
write something like:
|
||||
|
||||
```python
|
||||
from evennia.contrib.events.utils import register_events
|
||||
from evennia.contrib.events.typeclasses import EventObject
|
||||
|
||||
EVENT_PUSH = """
|
||||
A character push the object.
|
||||
This event is called when a character uses the "push" command on
|
||||
an object in the same room.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character that pushes this object.
|
||||
obj: the object connected to this event.
|
||||
"""
|
||||
|
||||
@register_events
|
||||
class Object(EventObject):
|
||||
"""
|
||||
Class representing objects.
|
||||
"""
|
||||
|
||||
_events = {
|
||||
"push": (["character", "obj"], EVENT_PUSH),
|
||||
}
|
||||
```
|
||||
|
||||
- Line 1-2: we import several things we will need from the event system. Note that we use
|
||||
`EventObject` as a parent instead of `DefaultObject`, as explained in the installation.
|
||||
- Line 4-12: we usually define the help of the event in a separate variable, this is more readable,
|
||||
though there's no rule against doing it another way. Usually, the help should contain a short
|
||||
explanation on a single line, a longer explanation on several lines, and then the list of variables
|
||||
with explanations.
|
||||
- Line 14: we call a decorator on the class to indicate it contains events. If you're not familiar
|
||||
with decorators, you don't really have to worry about it, just remember to put this line just
|
||||
above the class definition if your class contains events.
|
||||
- Line 15: we create the class inheriting from `EventObject`.
|
||||
- Line 20-22: we define the events of our objects in an `_events` class variable. It is a
|
||||
dictionary. Keys are event names. Values are a tuple containing:
|
||||
- The list of variable names (list of str). This will determine what variables are needed when
|
||||
the event triggers. These variables will be used in callbacks (as we'll see below).
|
||||
- The event help (a str, the one we have defined above).
|
||||
|
||||
If you add this code and reload your game, create an object and examine its events with `@call`, you
|
||||
should see the "push" event with its help. Of course, right now, the event exists, but it's not
|
||||
fired.
|
||||
|
||||
### Calling an event in code
|
||||
|
||||
The event system is accessible through a handler on all objects. This handler is named `callbacks`
|
||||
and can be accessed from any typeclassed object (your character, a room, an exit...). This handler
|
||||
offers several methods to examine and call an event or callback on this object.
|
||||
|
||||
To call an event, use the `callbacks.call` method in an object. It takes as argument:
|
||||
|
||||
- The name of the event to call.
|
||||
- All variables that will be accessible in the event as positional arguments. They should be
|
||||
specified in the order chosen when [creating new events](#adding-new-events).
|
||||
|
||||
Following the same example, so far, we have created an event on all objects, called "push". This
|
||||
event is never fired for the time being. We could add a "push" command, taking as argument the name
|
||||
of an object. If this object is valid, it will call its "push" event.
|
||||
|
||||
```python
|
||||
from commands.command import Command
|
||||
|
||||
class CmdPush(Command):
|
||||
|
||||
"""
|
||||
Push something.
|
||||
|
||||
Usage:
|
||||
push <something>
|
||||
|
||||
Push something where you are, like an elevator button.
|
||||
|
||||
"""
|
||||
|
||||
key = "push"
|
||||
|
||||
def func(self):
|
||||
"""Called when pushing something."""
|
||||
if not self.args.strip():
|
||||
self.msg("Usage: push <something>")
|
||||
return
|
||||
|
||||
# Search for this object
|
||||
obj = self.caller.search(self.args)
|
||||
if not obj:
|
||||
return
|
||||
|
||||
self.msg("You push {}.".format(obj.get_display_name(self.caller)))
|
||||
|
||||
# Call the "push" event of this object
|
||||
obj.callbacks.call("push", self.caller, obj)
|
||||
```
|
||||
|
||||
Here we use `callbacks.call` with the following arguments:
|
||||
|
||||
- `"push"`: the name of the event to be called.
|
||||
- `self.caller`: the one who pushed the button (this is our first variable, `character`).
|
||||
- `obj`: the object being pushed (our second variable, `obj`).
|
||||
|
||||
In the "push" callbacks of our objects, we then can use the "character" variable (containing the one
|
||||
who pushed the object), and the "obj" variable (containing the object that was pushed).
|
||||
|
||||
### See it all work
|
||||
|
||||
To see the effect of the two modifications above (the added event and the "push" command), let us
|
||||
create a simple object:
|
||||
|
||||
@create/drop rock
|
||||
@desc rock = It's a single rock, apparently pretty heavy. Perhaps you can try to push it though.
|
||||
@call/add rock = push
|
||||
|
||||
In the callback you could write:
|
||||
|
||||
```python
|
||||
from random import randint
|
||||
number = randint(1, 6)
|
||||
character.msg("You push a rock... is... it... going... to... move?")
|
||||
if number == 6:
|
||||
character.msg("The rock topples over to reveal a beautiful ant-hill!")
|
||||
```
|
||||
|
||||
You can now try to "push rock". You'll try to push the rock, and once out of six times, you will
|
||||
see a message about a "beautiful ant-hill".
|
||||
|
||||
### Adding new eventfuncs
|
||||
|
||||
Eventfuncs, like `deny(), are defined in `contrib/events/eventfuncs.py`. You can add your own
|
||||
eventfuncs by creating a file named `eventfuncs.py` in your `world` directory. The functions
|
||||
defined in this file will be added as helpers.
|
||||
|
||||
You can also decide to create your eventfuncs in another location, or even in several locations. To
|
||||
do so, edit the `EVENTFUNCS_LOCATION` setting in your `server/conf/settings.py` file, specifying
|
||||
either a python path or a list of Python paths in which your helper functions are defined. For
|
||||
instance:
|
||||
|
||||
```python
|
||||
EVENTFUNCS_LOCATIONS = [
|
||||
"world.events.functions",
|
||||
]
|
||||
```
|
||||
|
||||
### Creating events with parameters
|
||||
|
||||
If you want to create events with parameters (if you create a "whisper" or "ask" command, for
|
||||
instance, and need to have some characters automatically react to words), you can set an additional
|
||||
argument in the tuple of events in your typeclass' ```_events``` class variable. This third argument
|
||||
must contain a callback that will be called to filter through the list of callbacks when the event
|
||||
fires. Two types of parameters are commonly used (but you can define more parameter types, although
|
||||
this is out of the scope of this documentation).
|
||||
|
||||
- Keyword parameters: callbacks of this event will be filtered based on specific keywords. This is
|
||||
useful if you want the user to specify a word and compare this word to a list.
|
||||
- Phrase parameters: callbacks will be filtered using an entire phrase and checking all its words.
|
||||
The "say" command uses phrase parameters (you can set a "say" callback to fires if a phrase
|
||||
contains one specific word).
|
||||
|
||||
In both cases, you need to import a function from `evennia.contrib.events.utils` and use it as third
|
||||
parameter in your event definition.
|
||||
|
||||
- `keyword_event` should be used for keyword parameters.
|
||||
- `phrase_event` should be used for phrase parameters.
|
||||
|
||||
For example, here is the definition of the "say" event:
|
||||
|
||||
```python
|
||||
from evennia.contrib.events.utils import register_events, phrase_event
|
||||
# ...
|
||||
@register_events
|
||||
class SomeTypeclass:
|
||||
_events = {
|
||||
"say": (["speaker", "character", "message"], CHARACTER_SAY, phrase_event),
|
||||
}
|
||||
```
|
||||
|
||||
When you call an event using the `obj.callbacks.call` method, you should also provide the parameter,
|
||||
using the `parameters` keyword:
|
||||
|
||||
```python
|
||||
obj.callbacks.call(..., parameters="<put parameters here>")
|
||||
```
|
||||
|
||||
It is necessary to specifically call the event with parameters, otherwise the system will not be
|
||||
able to know how to filter down the list of callbacks.
|
||||
|
||||
## Disabling all events at once
|
||||
|
||||
When callbacks are running in an infinite loop, for instance, or sending unwanted information to
|
||||
players or other sources, you, as the game administrator, have the power to restart without events.
|
||||
The best way to do this is to use a custom setting, in your setting file
|
||||
(`server/conf/settings.py`):
|
||||
|
||||
```python
|
||||
# Disable all events
|
||||
EVENTS_DISABLED = True
|
||||
```
|
||||
|
||||
The event system will still be accessible (you will have access to the `@call` command, to debug),
|
||||
but no event will be called automatically.
|
||||
0
evennia/contrib/events/__init__.py
Normal file
0
evennia/contrib/events/__init__.py
Normal file
204
evennia/contrib/events/callbackhandler.py
Normal file
204
evennia/contrib/events/callbackhandler.py
Normal file
|
|
@ -0,0 +1,204 @@
|
|||
"""
|
||||
Module containing the CallbackHandler for individual objects.
|
||||
"""
|
||||
|
||||
from collections import namedtuple
|
||||
|
||||
class CallbackHandler(object):
|
||||
|
||||
"""
|
||||
The event handler for a specific object.
|
||||
|
||||
The script that contains all events will be reached through this
|
||||
handler. This handler is therefore a shortcut to be used by
|
||||
developers. This handler (accessible through `obj.callbacks`) is a
|
||||
shortcut to manipulating callbacks within this object, getting,
|
||||
adding, editing, deleting and calling them.
|
||||
|
||||
"""
|
||||
|
||||
script = None
|
||||
|
||||
def __init__(self, obj):
|
||||
self.obj = obj
|
||||
|
||||
def all(self):
|
||||
"""
|
||||
Return all callbacks linked to this object.
|
||||
|
||||
Returns:
|
||||
All callbacks in a dictionary callback_name: callback}. The callback
|
||||
is returned as a namedtuple to simplify manipulation.
|
||||
|
||||
"""
|
||||
callbacks = {}
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
dicts = handler.get_callbacks(self.obj)
|
||||
for callback_name, in_list in dicts.items():
|
||||
new_list = []
|
||||
for callback in in_list:
|
||||
callback = self.format_callback(callback)
|
||||
new_list.append(callback)
|
||||
|
||||
if new_list:
|
||||
callbacks[callback_name] = new_list
|
||||
|
||||
return callbacks
|
||||
|
||||
def get(self, callback_name):
|
||||
"""
|
||||
Return the callbacks associated with this name.
|
||||
|
||||
Args:
|
||||
callback_name (str): the name of the callback.
|
||||
|
||||
Returns:
|
||||
A list of callbacks associated with this object and of this name.
|
||||
|
||||
Note:
|
||||
This method returns a list of callback objects (namedtuple
|
||||
representations). If the callback name cannot be found in the
|
||||
object's callbacks, return an empty list.
|
||||
|
||||
"""
|
||||
return self.all().get(callback_name, [])
|
||||
|
||||
def get_variable(self, variable_name):
|
||||
"""
|
||||
Return the variable value or None.
|
||||
|
||||
Args:
|
||||
variable_name (str): the name of the variable.
|
||||
|
||||
Returns:
|
||||
Either the variable's value or None.
|
||||
|
||||
"""
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
return handler.get_variable(variable_name)
|
||||
|
||||
return None
|
||||
|
||||
def add(self, callback_name, code, author=None, valid=False, parameters=""):
|
||||
"""
|
||||
Add a new callback for this object.
|
||||
|
||||
Args:
|
||||
callback_name (str): the name of the callback to add.
|
||||
code (str): the Python code associated with this callback.
|
||||
author (Character or Player, optional): the author of the callback.
|
||||
valid (bool, optional): should the callback be connected?
|
||||
parameters (str, optional): optional parameters.
|
||||
|
||||
Returns:
|
||||
The callback definition that was added or None.
|
||||
|
||||
"""
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
return self.format_callback(handler.add_callback(self.obj, callback_name, code,
|
||||
author=author, valid=valid, parameters=parameters))
|
||||
|
||||
def edit(self, callback_name, number, code, author=None, valid=False):
|
||||
"""
|
||||
Edit an existing callback bound to this object.
|
||||
|
||||
Args:
|
||||
callback_name (str): the name of the callback to edit.
|
||||
number (int): the callback number to be changed.
|
||||
code (str): the Python code associated with this callback.
|
||||
author (Character or Player, optional): the author of the callback.
|
||||
valid (bool, optional): should the callback be connected?
|
||||
|
||||
Returns:
|
||||
The callback definition that was edited or None.
|
||||
|
||||
Raises:
|
||||
RuntimeError if the callback is locked.
|
||||
|
||||
"""
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
return self.format_callback(handler.edit_callback(self.obj, callback_name,
|
||||
number, code, author=author, valid=valid))
|
||||
|
||||
def remove(self, callback_name, number):
|
||||
"""
|
||||
Delete the specified callback bound to this object.
|
||||
|
||||
Args:
|
||||
callback_name (str): the name of the callback to delete.
|
||||
number (int): the number of the callback to delete.
|
||||
|
||||
Raises:
|
||||
RuntimeError if the callback is locked.
|
||||
|
||||
"""
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
handler.del_callback(self.obj, callback_name, number)
|
||||
|
||||
def call(self, callback_name, *args, **kwargs):
|
||||
"""
|
||||
Call the specified callback(s) bound to this object.
|
||||
|
||||
Args:
|
||||
callback_name (str): the callback name to call.
|
||||
*args: additional variables for this callback.
|
||||
|
||||
Kwargs:
|
||||
number (int, optional): call just a specific callback.
|
||||
parameters (str, optional): call a callback with parameters.
|
||||
locals (dict, optional): a locals replacement.
|
||||
|
||||
Returns:
|
||||
True to report the callback was called without interruption,
|
||||
False otherwise. If the callbackHandler isn't found, return
|
||||
None.
|
||||
|
||||
"""
|
||||
handler = type(self).script
|
||||
if handler:
|
||||
return handler.call(self.obj, callback_name, *args, **kwargs)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def format_callback(callback):
|
||||
"""
|
||||
Return the callback namedtuple to represent the specified callback.
|
||||
|
||||
Args:
|
||||
callback (dict): the callback definition.
|
||||
|
||||
The callback given in argument should be a dictionary containing
|
||||
the expected fields for a callback (code, author, valid...).
|
||||
|
||||
"""
|
||||
if "obj" not in callback:
|
||||
callback["obj"] = None
|
||||
if "name" not in callback:
|
||||
callback["name"] = "unknown"
|
||||
if "number" not in callback:
|
||||
callback["number"] = -1
|
||||
if "code" not in callback:
|
||||
callback["code"] = ""
|
||||
if "author" not in callback:
|
||||
callback["author"] = None
|
||||
if "valid" not in callback:
|
||||
callback["valid"] = False
|
||||
if "parameters" not in callback:
|
||||
callback["parameters"] = ""
|
||||
if "created_on" not in callback:
|
||||
callback["created_on"] = None
|
||||
if "updated_by" not in callback:
|
||||
callback["updated_by"] = None
|
||||
if "updated_on" not in callback:
|
||||
callback["updated_on"] = None
|
||||
|
||||
return Callback(**callback)
|
||||
|
||||
Callback = namedtuple("Callback", ("obj", "name", "number", "code", "author",
|
||||
"valid", "parameters", "created_on", "updated_by", "updated_on"))
|
||||
562
evennia/contrib/events/commands.py
Normal file
562
evennia/contrib/events/commands.py
Normal file
|
|
@ -0,0 +1,562 @@
|
|||
"""
|
||||
Module containing the commands of the callback system.
|
||||
"""
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import Command
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.utils.eveditor import EvEditor
|
||||
from evennia.utils.evtable import EvTable
|
||||
from evennia.utils.utils import class_from_module, time_format
|
||||
from evennia.contrib.events.utils import get_event_handler
|
||||
|
||||
COMMAND_DEFAULT_CLASS = class_from_module(settings.COMMAND_DEFAULT_CLASS)
|
||||
|
||||
# Permissions
|
||||
WITH_VALIDATION = getattr(settings, "callbackS_WITH_VALIDATION", None)
|
||||
WITHOUT_VALIDATION = getattr(settings, "callbackS_WITHOUT_VALIDATION",
|
||||
"immortals")
|
||||
VALIDATING = getattr(settings, "callbackS_VALIDATING", "immortals")
|
||||
|
||||
# Split help text
|
||||
BASIC_HELP = "Add, edit or delete callbacks."
|
||||
|
||||
BASIC_USAGES = [
|
||||
"@call <object name> [= <callback name>]",
|
||||
"@call/add <object name> = <callback name> [parameters]",
|
||||
"@call/edit <object name> = <callback name> [callback number]",
|
||||
"@call/del <object name> = <callback name> [callback number]",
|
||||
"@call/tasks [object name [= <callback name>]]",
|
||||
]
|
||||
|
||||
BASIC_SWITCHES = [
|
||||
"add - add and edit a new callback",
|
||||
"edit - edit an existing callback",
|
||||
"del - delete an existing callback",
|
||||
"tasks - show the list of differed tasks",
|
||||
]
|
||||
|
||||
VALIDATOR_USAGES = [
|
||||
"@call/accept [object name = <callback name> [callback number]]",
|
||||
]
|
||||
|
||||
VALIDATOR_SWITCHES = [
|
||||
"accept - show callbacks to be validated or accept one",
|
||||
]
|
||||
|
||||
BASIC_TEXT = """
|
||||
This command is used to manipulate callbacks. A callback can be linked to
|
||||
an object, to fire at a specific moment. You can use the command without
|
||||
switches to see what callbacks are active on an object:
|
||||
@call self
|
||||
You can also specify a callback name if you want the list of callbacks
|
||||
associated with this object of this name:
|
||||
@call north = can_traverse
|
||||
You can also add a number after the callback name to see details on one callback:
|
||||
@call here = say 2
|
||||
You can also add, edit or remove callbacks using the add, edit or del switches.
|
||||
Additionally, you can see the list of differed tasks created by callbacks
|
||||
(chained events to be called) using the /tasks switch.
|
||||
"""
|
||||
|
||||
VALIDATOR_TEXT = """
|
||||
You can also use this command to validate callbacks. Depending on your game
|
||||
setting, some users might be allowed to add new callbacks, but these callbacks
|
||||
will not be fired until you accept them. To see the callbacks needing
|
||||
validation, enter the /accept switch without argument:
|
||||
@call/accept
|
||||
A table will show you the callbacks that are not validated yet, who created
|
||||
them and when. You can then accept a specific callback:
|
||||
@call here = enter 1
|
||||
Use the /del switch to remove callbacks that should not be connected.
|
||||
"""
|
||||
|
||||
class CmdCallback(COMMAND_DEFAULT_CLASS):
|
||||
|
||||
"""
|
||||
Command to edit callbacks.
|
||||
"""
|
||||
|
||||
key = "@call"
|
||||
aliases = ["@callback", "@callbacks", "@calls"]
|
||||
locks = "cmd:perm({})".format(VALIDATING)
|
||||
if WITH_VALIDATION:
|
||||
locks += " or perm({})".format(WITH_VALIDATION)
|
||||
help_category = "Building"
|
||||
|
||||
def get_help(self, caller, cmdset):
|
||||
"""
|
||||
Return the help message for this command and this caller.
|
||||
|
||||
The help text of this specific command will vary depending
|
||||
on user permission.
|
||||
|
||||
Args:
|
||||
caller (Object or Player): the caller asking for help on the command.
|
||||
cmdset (CmdSet): the command set (if you need additional commands).
|
||||
|
||||
Returns:
|
||||
docstring (str): the help text to provide the caller for this command.
|
||||
|
||||
"""
|
||||
lock = "perm({}) or perm(callbacks_validating)".format(VALIDATING)
|
||||
validator = caller.locks.check_lockstring(caller, lock)
|
||||
text = "\n" + BASIC_HELP + "\n\nUsages:\n "
|
||||
|
||||
# Usages
|
||||
text += "\n ".join(BASIC_USAGES)
|
||||
if validator:
|
||||
text += "\n " + "\n ".join(VALIDATOR_USAGES)
|
||||
|
||||
# Switches
|
||||
text += "\n\nSwitches:\n "
|
||||
text += "\n ".join(BASIC_SWITCHES)
|
||||
if validator:
|
||||
text += "\n " + "\n ".join(VALIDATOR_SWITCHES)
|
||||
|
||||
# Text
|
||||
text += "\n" + BASIC_TEXT
|
||||
if validator:
|
||||
text += "\n" + VALIDATOR_TEXT
|
||||
|
||||
return text
|
||||
|
||||
def func(self):
|
||||
"""Command body."""
|
||||
caller = self.caller
|
||||
lock = "perm({}) or perm(events_validating)".format(VALIDATING)
|
||||
validator = caller.locks.check_lockstring(caller, lock)
|
||||
lock = "perm({}) or perm(events_without_validation)".format(
|
||||
WITHOUT_VALIDATION)
|
||||
autovalid = caller.locks.check_lockstring(caller, lock)
|
||||
|
||||
# First and foremost, get the callback handler and set other variables
|
||||
self.handler = get_event_handler()
|
||||
self.obj = None
|
||||
rhs = self.rhs or ""
|
||||
self.callback_name, sep, self.parameters = rhs.partition(" ")
|
||||
self.callback_name = self.callback_name.lower()
|
||||
self.is_validator = validator
|
||||
self.autovalid = autovalid
|
||||
if self.handler is None:
|
||||
caller.msg("The event handler is not running, can't " \
|
||||
"access the event system.")
|
||||
return
|
||||
|
||||
# Before the equal sign, there is an object name or nothing
|
||||
if self.lhs:
|
||||
self.obj = caller.search(self.lhs)
|
||||
if not self.obj:
|
||||
return
|
||||
|
||||
# Switches are mutually exclusive
|
||||
switch = self.switches and self.switches[0] or ""
|
||||
if switch in ("", "add", "edit", "del") and self.obj is None:
|
||||
caller.msg("Specify an object's name or #ID.")
|
||||
return
|
||||
|
||||
if switch == "":
|
||||
self.list_callbacks()
|
||||
elif switch == "add":
|
||||
self.add_callback()
|
||||
elif switch == "edit":
|
||||
self.edit_callback()
|
||||
elif switch == "del":
|
||||
self.del_callback()
|
||||
elif switch == "accept" and validator:
|
||||
self.accept_callback()
|
||||
elif switch in ["tasks", "task"]:
|
||||
self.list_tasks()
|
||||
else:
|
||||
caller.msg("Mutually exclusive or invalid switches were " \
|
||||
"used, cannot proceed.")
|
||||
|
||||
def list_callbacks(self):
|
||||
"""Display the list of callbacks connected to the object."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
parameters = self.parameters
|
||||
callbacks = self.handler.get_callbacks(obj)
|
||||
types = self.handler.get_events(obj)
|
||||
|
||||
if callback_name:
|
||||
# Check that the callback name can be found in this object
|
||||
created = callbacks.get(callback_name)
|
||||
if created is None:
|
||||
self.msg("No callback {} has been set on {}.".format(callback_name,
|
||||
obj))
|
||||
return
|
||||
|
||||
if parameters:
|
||||
# Check that the parameter points to an existing callback
|
||||
try:
|
||||
number = int(parameters) - 1
|
||||
assert number >= 0
|
||||
callback = callbacks[callback_name][number]
|
||||
except (ValueError, AssertionError, IndexError):
|
||||
self.msg("The callback {} {} cannot be found in {}.".format(
|
||||
callback_name, parameters, obj))
|
||||
return
|
||||
|
||||
# Display the callback's details
|
||||
author = callback.get("author")
|
||||
author = author.key if author else "|gUnknown|n"
|
||||
updated_by = callback.get("updated_by")
|
||||
updated_by = updated_by.key if updated_by else "|gUnknown|n"
|
||||
created_on = callback.get("created_on")
|
||||
created_on = created_on.strftime("%Y-%m-%d %H:%M:%S") if created_on else "|gUnknown|n"
|
||||
updated_on = callback.get("updated_on")
|
||||
updated_on = updated_on.strftime("%Y-%m-%d %H:%M:%S") if updated_on else "|gUnknown|n"
|
||||
msg = "Callback {} {} of {}:".format(callback_name, parameters, obj)
|
||||
msg += "\nCreated by {} on {}.".format(author, created_on)
|
||||
msg += "\nUpdated by {} on {}".format(updated_by, updated_on)
|
||||
|
||||
if self.is_validator:
|
||||
if callback.get("valid"):
|
||||
msg += "\nThis callback is |rconnected|n and active."
|
||||
else:
|
||||
msg += "\nThis callback |rhasn't been|n accepted yet."
|
||||
|
||||
msg += "\nCallback code:\n"
|
||||
msg += raw(callback["code"])
|
||||
self.msg(msg)
|
||||
return
|
||||
|
||||
# No parameter has been specified, display the table of callbacks
|
||||
cols = ["Number", "Author", "Updated", "Param"]
|
||||
if self.is_validator:
|
||||
cols.append("Valid")
|
||||
|
||||
table = EvTable(*cols, width=78)
|
||||
table.reformat_column(0, align="r")
|
||||
now = datetime.now()
|
||||
for i, callback in enumerate(created):
|
||||
author = callback.get("author")
|
||||
author = author.key if author else "|gUnknown|n"
|
||||
updated_on = callback.get("updated_on")
|
||||
if updated_on is None:
|
||||
updated_on = callback.get("created_on")
|
||||
|
||||
if updated_on:
|
||||
updated_on = "{} ago".format(time_format(
|
||||
(now - updated_on).total_seconds(),
|
||||
4).capitalize())
|
||||
else:
|
||||
updated_on = "|gUnknown|n"
|
||||
parameters = callback.get("parameters", "")
|
||||
|
||||
row = [str(i + 1), author, updated_on, parameters]
|
||||
if self.is_validator:
|
||||
row.append("Yes" if callback.get("valid") else "No")
|
||||
table.add_row(*row)
|
||||
|
||||
self.msg(unicode(table))
|
||||
else:
|
||||
names = list(set(list(types.keys()) + list(callbacks.keys())))
|
||||
table = EvTable("Callback name", "Number", "Description",
|
||||
valign="t", width=78)
|
||||
table.reformat_column(0, width=20)
|
||||
table.reformat_column(1, width=10, align="r")
|
||||
table.reformat_column(2, width=48)
|
||||
for name in sorted(names):
|
||||
number = len(callbacks.get(name, []))
|
||||
lines = sum(len(e["code"].splitlines()) for e in callbacks.get(name, []))
|
||||
no = "{} ({})".format(number, lines)
|
||||
description = types.get(name, (None, "Chained event."))[1]
|
||||
description = description.strip("\n").splitlines()[0]
|
||||
table.add_row(name, no, description)
|
||||
|
||||
self.msg(unicode(table))
|
||||
|
||||
def add_callback(self):
|
||||
"""Add a callback."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
types = self.handler.get_events(obj)
|
||||
|
||||
# Check that the callback exists
|
||||
if not callback_name.startswith("chain_") and not callback_name in types:
|
||||
self.msg("The callback name {} can't be found in {} of " \
|
||||
"typeclass {}.".format(callback_name, obj, type(obj)))
|
||||
return
|
||||
|
||||
definition = types.get(callback_name, (None, "Chained event."))
|
||||
description = definition[1]
|
||||
self.msg(raw(description.strip("\n")))
|
||||
|
||||
# Open the editor
|
||||
callback = self.handler.add_callback(obj, callback_name, "",
|
||||
self.caller, False, parameters=self.parameters)
|
||||
|
||||
# Lock this callback right away
|
||||
self.handler.db.locked.append((obj, callback_name, callback["number"]))
|
||||
|
||||
# Open the editor for this callback
|
||||
self.caller.db._callback = callback
|
||||
EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save,
|
||||
quitfunc=_ev_quit, key="Callback {} of {}".format(
|
||||
callback_name, obj), persistent=True, codefunc=_ev_save)
|
||||
|
||||
def edit_callback(self):
|
||||
"""Edit a callback."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
parameters = self.parameters
|
||||
callbacks = self.handler.get_callbacks(obj)
|
||||
types = self.handler.get_events(obj)
|
||||
|
||||
# If no callback name is specified, display the list of callbacks
|
||||
if not callback_name:
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the callback exists
|
||||
if not callback_name in callbacks:
|
||||
self.msg("The callback name {} can't be found in {}.".format(
|
||||
callback_name, obj))
|
||||
return
|
||||
|
||||
# If there's only one callback, just edit it
|
||||
if len(callbacks[callback_name]) == 1:
|
||||
number = 0
|
||||
callback = callbacks[callback_name][0]
|
||||
else:
|
||||
if not parameters:
|
||||
self.msg("Which callback do you wish to edit? Specify a number.")
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the parameter points to an existing callback
|
||||
try:
|
||||
number = int(parameters) - 1
|
||||
assert number >= 0
|
||||
callback = callbacks[callback_name][number]
|
||||
except (ValueError, AssertionError, IndexError):
|
||||
self.msg("The callback {} {} cannot be found in {}.".format(
|
||||
callback_name, parameters, obj))
|
||||
return
|
||||
|
||||
# If caller can't edit without validation, forbid editing
|
||||
# others' works
|
||||
if not self.autovalid and callback["author"] is not self.caller:
|
||||
self.msg("You cannot edit this callback created by someone else.")
|
||||
return
|
||||
|
||||
# If the callback is locked (edited by someone else)
|
||||
if (obj, callback_name, number) in self.handler.db.locked:
|
||||
self.msg("This callback is locked, you cannot edit it.")
|
||||
return
|
||||
|
||||
self.handler.db.locked.append((obj, callback_name, number))
|
||||
|
||||
# Check the definition of the callback
|
||||
definition = types.get(callback_name, (None, "Chained event."))
|
||||
description = definition[1]
|
||||
self.msg(raw(description.strip("\n")))
|
||||
|
||||
# Open the editor
|
||||
callback = dict(callback)
|
||||
callback["obj"] = obj
|
||||
callback["name"] = callback_name
|
||||
callback["number"] = number
|
||||
self.caller.db._callback = callback
|
||||
EvEditor(self.caller, loadfunc=_ev_load, savefunc=_ev_save,
|
||||
quitfunc=_ev_quit, key="Callback {} of {}".format(
|
||||
callback_name, obj), persistent=True, codefunc=_ev_save)
|
||||
|
||||
def del_callback(self):
|
||||
"""Delete a callback."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
parameters = self.parameters
|
||||
callbacks = self.handler.get_callbacks(obj)
|
||||
types = self.handler.get_events(obj)
|
||||
|
||||
# If no callback name is specified, display the list of callbacks
|
||||
if not callback_name:
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the callback exists
|
||||
if not callback_name in callbacks:
|
||||
self.msg("The callback name {} can't be found in {}.".format(
|
||||
callback_name, obj))
|
||||
return
|
||||
|
||||
# If there's only one callback, just delete it
|
||||
if len(callbacks[callback_name]) == 1:
|
||||
number = 0
|
||||
callback = callbacks[callback_name][0]
|
||||
else:
|
||||
if not parameters:
|
||||
self.msg("Which callback do you wish to delete? Specify " \
|
||||
"a number.")
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the parameter points to an existing callback
|
||||
try:
|
||||
number = int(parameters) - 1
|
||||
assert number >= 0
|
||||
callback = callbacks[callback_name][number]
|
||||
except (ValueError, AssertionError, IndexError):
|
||||
self.msg("The callback {} {} cannot be found in {}.".format(
|
||||
callback_name, parameters, obj))
|
||||
return
|
||||
|
||||
# If caller can't edit without validation, forbid deleting
|
||||
# others' works
|
||||
if not self.autovalid and callback["author"] is not self.caller:
|
||||
self.msg("You cannot delete this callback created by someone else.")
|
||||
return
|
||||
|
||||
# If the callback is locked (edited by someone else)
|
||||
if (obj, callback_name, number) in self.handler.db.locked:
|
||||
self.msg("This callback is locked, you cannot delete it.")
|
||||
return
|
||||
|
||||
# Delete the callback
|
||||
self.handler.del_callback(obj, callback_name, number)
|
||||
self.msg("The callback {}[{}] of {} was deleted.".format(
|
||||
callback_name, number + 1, obj))
|
||||
|
||||
def accept_callback(self):
|
||||
"""Accept a callback."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
parameters = self.parameters
|
||||
|
||||
# If no object, display the list of callbacks to be checked
|
||||
if obj is None:
|
||||
table = EvTable("ID", "Type", "Object", "Name", "Updated by",
|
||||
"On", width=78)
|
||||
table.reformat_column(0, align="r")
|
||||
now = datetime.now()
|
||||
for obj, name, number in self.handler.db.to_valid:
|
||||
callbacks = self.handler.get_callbacks(obj).get(name)
|
||||
if callbacks is None:
|
||||
continue
|
||||
|
||||
try:
|
||||
callback = callbacks[number]
|
||||
except IndexError:
|
||||
continue
|
||||
|
||||
type_name = obj.typeclass_path.split(".")[-1]
|
||||
by = callback.get("updated_by")
|
||||
by = by.key if by else "|gUnknown|n"
|
||||
updated_on = callback.get("updated_on")
|
||||
if updated_on is None:
|
||||
updated_on = callback.get("created_on")
|
||||
|
||||
if updated_on:
|
||||
updated_on = "{} ago".format(time_format(
|
||||
(now - updated_on).total_seconds(),
|
||||
4).capitalize())
|
||||
else:
|
||||
updated_on = "|gUnknown|n"
|
||||
|
||||
table.add_row(obj.id, type_name, obj, name, by, updated_on)
|
||||
self.msg(unicode(table))
|
||||
return
|
||||
|
||||
# An object was specified
|
||||
callbacks = self.handler.get_callbacks(obj)
|
||||
types = self.handler.get_events(obj)
|
||||
|
||||
# If no callback name is specified, display the list of callbacks
|
||||
if not callback_name:
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the callback exists
|
||||
if not callback_name in callbacks:
|
||||
self.msg("The callback name {} can't be found in {}.".format(
|
||||
callback_name, obj))
|
||||
return
|
||||
|
||||
if not parameters:
|
||||
self.msg("Which callback do you wish to accept? Specify a number.")
|
||||
self.list_callbacks()
|
||||
return
|
||||
|
||||
# Check that the parameter points to an existing callback
|
||||
try:
|
||||
number = int(parameters) - 1
|
||||
assert number >= 0
|
||||
callback = callbacks[callback_name][number]
|
||||
except (ValueError, AssertionError, IndexError):
|
||||
self.msg("The callback {} {} cannot be found in {}.".format(
|
||||
callback_name, parameters, obj))
|
||||
return
|
||||
|
||||
# Accept the callback
|
||||
if callback["valid"]:
|
||||
self.msg("This callback has already been accepted.")
|
||||
else:
|
||||
self.handler.accept_callback(obj, callback_name, number)
|
||||
self.msg("The callback {} {} of {} has been accepted.".format(
|
||||
callback_name, parameters, obj))
|
||||
|
||||
def list_tasks(self):
|
||||
"""List the active tasks."""
|
||||
obj = self.obj
|
||||
callback_name = self.callback_name
|
||||
handler = self.handler
|
||||
tasks = [(k, v[0], v[1], v[2]) for k, v in handler.db.tasks.items()]
|
||||
if obj:
|
||||
tasks = [task for task in tasks if task[2] is obj]
|
||||
if callback_name:
|
||||
tasks = [task for task in tasks if task[3] == callback_name]
|
||||
|
||||
tasks.sort()
|
||||
table = EvTable("ID", "Object", "Callback", "In", width=78)
|
||||
table.reformat_column(0, align="r")
|
||||
now = datetime.now()
|
||||
for task_id, future, obj, callback_name in tasks:
|
||||
key = obj.get_display_name(self.caller)
|
||||
delta = time_format((future - now).total_seconds(), 1)
|
||||
table.add_row(task_id, key, callback_name, delta)
|
||||
|
||||
self.msg(unicode(table))
|
||||
|
||||
# Private functions to handle editing
|
||||
def _ev_load(caller):
|
||||
return caller.db._callback and caller.db._callback.get("code", "") or ""
|
||||
|
||||
def _ev_save(caller, buf):
|
||||
"""Save and add the callback."""
|
||||
lock = "perm({}) or perm(events_without_validation)".format(
|
||||
WITHOUT_VALIDATION)
|
||||
autovalid = caller.locks.check_lockstring(caller, lock)
|
||||
callback = caller.db._callback
|
||||
handler = get_event_handler()
|
||||
if not handler or not callback or not all(key in callback for key in \
|
||||
("obj", "name", "number", "valid")):
|
||||
caller.msg("Couldn't save this callback.")
|
||||
return False
|
||||
|
||||
if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
|
||||
handler.db.locked.remove((callback["obj"], callback["name"],
|
||||
callback["number"]))
|
||||
|
||||
handler.edit_callback(callback["obj"], callback["name"], callback["number"], buf,
|
||||
caller, valid=autovalid)
|
||||
return True
|
||||
|
||||
def _ev_quit(caller):
|
||||
callback = caller.db._callback
|
||||
handler = get_event_handler()
|
||||
if not handler or not callback or not all(key in callback for key in \
|
||||
("obj", "name", "number", "valid")):
|
||||
caller.msg("Couldn't save this callback.")
|
||||
return False
|
||||
|
||||
if (callback["obj"], callback["name"], callback["number"]) in handler.db.locked:
|
||||
handler.db.locked.remove((callback["obj"], callback["name"],
|
||||
callback["number"]))
|
||||
|
||||
del caller.db._callback
|
||||
caller.msg("Exited the code editor.")
|
||||
88
evennia/contrib/events/eventfuncs.py
Normal file
88
evennia/contrib/events/eventfuncs.py
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
"""
|
||||
Module defining basic eventfuncs for the event system.
|
||||
|
||||
Eventfuncs are just Python functions that can be used inside of calllbacks.
|
||||
|
||||
"""
|
||||
|
||||
from evennia import ObjectDB, ScriptDB
|
||||
from evennia.contrib.events.utils import InterruptEvent
|
||||
|
||||
def deny():
|
||||
"""
|
||||
Deny, that is stop, the event here.
|
||||
|
||||
Notes:
|
||||
This function will raise an exception to terminate the event
|
||||
in a controlled way. If you use this function in an event called
|
||||
prior to a command, the command will be cancelled as well. Good
|
||||
situations to use the `deny()` function are in events that begins
|
||||
by `can_`, because they usually can be cancelled as easily as that.
|
||||
|
||||
"""
|
||||
raise InterruptEvent
|
||||
|
||||
def get(**kwargs):
|
||||
"""
|
||||
Return an object with the given search option or None if None is found.
|
||||
|
||||
Kwargs:
|
||||
Any searchable data or property (id, db_key, db_location...).
|
||||
|
||||
Returns:
|
||||
The object found that meet these criteria for research, or
|
||||
None if none is found.
|
||||
|
||||
Notes:
|
||||
This function is very useful to retrieve objects with a specific
|
||||
ID. You know that room #32 exists, but you don't have it in
|
||||
the callback variables. Quite simple:
|
||||
room = get(id=32)
|
||||
|
||||
This function doesn't perform a search on objects, but a direct
|
||||
search in the database. It's recommended to use it for objects
|
||||
you know exist, using their IDs or other unique attributes.
|
||||
Looking for objects by key is possible (use `db_key` as an
|
||||
argument) but remember several objects can share the same key.
|
||||
|
||||
"""
|
||||
try:
|
||||
object = ObjectDB.objects.get(**kwargs)
|
||||
except ObjectDB.DoesNotExist:
|
||||
object = None
|
||||
|
||||
return object
|
||||
|
||||
def call_event(obj, event_name, seconds=0):
|
||||
"""
|
||||
Call the specified event in X seconds.
|
||||
|
||||
Args:
|
||||
obj (Object): the typeclassed object containing the event.
|
||||
event_name (str): the event name to be called.
|
||||
seconds (int or float): the number of seconds to wait before calling
|
||||
the event.
|
||||
|
||||
Notes:
|
||||
This eventfunc can be used to call other events from inside of an
|
||||
event in a given time. This will create a pause between events. This
|
||||
will not freeze the game, and you can expect characters to move
|
||||
around (unless you prevent them from doing so).
|
||||
|
||||
Variables that are accessible in your event using 'call()' will be
|
||||
kept and passed on to the event to call.
|
||||
|
||||
Chained callbacks are designed for this very purpose: they
|
||||
are never called automatically by the game, rather, they need
|
||||
to be called from inside another event.
|
||||
|
||||
"""
|
||||
script = type(obj.callbacks).script
|
||||
if script:
|
||||
# If seconds is 0, call the event immediately
|
||||
if seconds == 0:
|
||||
locals = dict(script.ndb.current_locals)
|
||||
obj.callbacks.call(event_name, locals=locals)
|
||||
else:
|
||||
# Schedule the task
|
||||
script.set_task(seconds, obj, event_name)
|
||||
655
evennia/contrib/events/scripts.py
Normal file
655
evennia/contrib/events/scripts.py
Normal file
|
|
@ -0,0 +1,655 @@
|
|||
"""
|
||||
Scripts for the event system.
|
||||
"""
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from Queue import Queue
|
||||
import re
|
||||
import sys
|
||||
import traceback
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import DefaultObject, DefaultScript, ChannelDB, ScriptDB
|
||||
from evennia import logger
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.utils.create import create_channel
|
||||
from evennia.utils.dbserialize import dbserialize
|
||||
from evennia.utils.utils import all_from_module, delay, pypath_to_realpath
|
||||
from evennia.contrib.events.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.events.utils import get_next_wait, EVENTS, InterruptEvent
|
||||
|
||||
# Constants
|
||||
RE_LINE_ERROR = re.compile(r'^ File "\<string\>", line (\d+)')
|
||||
|
||||
class EventHandler(DefaultScript):
|
||||
|
||||
"""
|
||||
The event handler that contains all events in a global script.
|
||||
|
||||
This script shouldn't be created more than once. It contains
|
||||
event (in a non-persistent attribute) and callbacks (in a
|
||||
persistent attribute). The script method would help adding,
|
||||
editing and deleting these events.
|
||||
|
||||
"""
|
||||
|
||||
def at_script_creation(self):
|
||||
"""Hook called when the script is created."""
|
||||
self.key = "event_handler"
|
||||
self.desc = "Global event handler"
|
||||
self.persistent = True
|
||||
|
||||
# Permanent data to be stored
|
||||
self.db.callbacks = {}
|
||||
self.db.to_valid = []
|
||||
self.db.locked = []
|
||||
|
||||
# Tasks
|
||||
self.db.tasks = {}
|
||||
|
||||
def at_start(self):
|
||||
"""Set up the event system when starting.
|
||||
|
||||
Note that this hook is called every time the server restarts
|
||||
(including when it's reloaded). This hook performs the following
|
||||
tasks:
|
||||
|
||||
- Create temporarily stored events.
|
||||
- Generate locals (individual events' namespace).
|
||||
- Load eventfuncs, including user-defined ones.
|
||||
- Re-schedule tasks that aren't set to fire anymore.
|
||||
- Effectively connect the handler to the main script.
|
||||
|
||||
"""
|
||||
self.ndb.events = {}
|
||||
for typeclass, name, variables, help_text, custom_call, custom_add in EVENTS:
|
||||
self.add_event(typeclass, name, variables, help_text, custom_call, custom_add)
|
||||
|
||||
# Generate locals
|
||||
self.ndb.current_locals = {}
|
||||
self.ndb.fresh_locals = {}
|
||||
addresses = ["evennia.contrib.events.eventfuncs"]
|
||||
addresses.extend(getattr(settings, "EVENTFUNCS_LOCATIONS", ["world.eventfuncs"]))
|
||||
for address in addresses:
|
||||
if pypath_to_realpath(address):
|
||||
self.ndb.fresh_locals.update(all_from_module(address))
|
||||
|
||||
# Restart the delayed tasks
|
||||
now = datetime.now()
|
||||
for task_id, definition in tuple(self.db.tasks.items()):
|
||||
future, obj, event_name, locals = definition
|
||||
seconds = (future - now).total_seconds()
|
||||
if seconds < 0:
|
||||
seconds = 0
|
||||
|
||||
delay(seconds, complete_task, task_id)
|
||||
|
||||
# Place the script in the CallbackHandler
|
||||
from evennia.contrib.events import typeclasses
|
||||
CallbackHandler.script = self
|
||||
DefaultObject.callbacks = typeclasses.EventObject.callbacks
|
||||
|
||||
# Create the channel if non-existent
|
||||
try:
|
||||
self.ndb.channel = ChannelDB.objects.get(db_key="everror")
|
||||
except ChannelDB.DoesNotExist:
|
||||
self.ndb.channel = create_channel("everror", desc="Event errors",
|
||||
locks="control:false();listen:perm(Builders);send:false()")
|
||||
|
||||
def get_events(self, obj):
|
||||
"""
|
||||
Return a dictionary of events on this object.
|
||||
|
||||
Args:
|
||||
obj (Object): the connected object.
|
||||
|
||||
Returns:
|
||||
A dictionary of the object's events.
|
||||
|
||||
Note:
|
||||
Events would define what the object can have as
|
||||
callbacks. Note, however, that chained callbacks will not
|
||||
appear in events and are handled separately.
|
||||
|
||||
"""
|
||||
events = {}
|
||||
all_events = self.ndb.events
|
||||
classes = Queue()
|
||||
classes.put(type(obj))
|
||||
invalid = []
|
||||
while not classes.empty():
|
||||
typeclass = classes.get()
|
||||
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
||||
for key, etype in all_events.get(typeclass_name, {}).items():
|
||||
if key in invalid:
|
||||
continue
|
||||
if etype[0] is None: # Invalidate
|
||||
invalid.append(key)
|
||||
continue
|
||||
if key not in events:
|
||||
events[key] = etype
|
||||
|
||||
# Look for the parent classes
|
||||
for parent in typeclass.__bases__:
|
||||
classes.put(parent)
|
||||
|
||||
return events
|
||||
|
||||
def get_variable(self, variable_name):
|
||||
"""
|
||||
Return the variable defined in the locals.
|
||||
|
||||
This can be very useful to check the value of a variable that can be modified in an event, and whose value will be used in code. This system allows additional customization.
|
||||
|
||||
Args:
|
||||
variable_name (str): the name of the variable to return.
|
||||
|
||||
Returns:
|
||||
The variable if found in the locals.
|
||||
None if not found in the locals.
|
||||
|
||||
Note:
|
||||
This will return the variable from the current locals.
|
||||
Keep in mind that locals are shared between events. As
|
||||
every event is called one by one, this doesn't pose
|
||||
additional problems if you get the variable right after
|
||||
an event has been executed. If, however, you differ,
|
||||
there's no guarantee the variable will be here or will
|
||||
mean the same thing.
|
||||
|
||||
"""
|
||||
return self.ndb.current_locals.get(variable_name)
|
||||
|
||||
def get_callbacks(self, obj):
|
||||
"""
|
||||
Return a dictionary of the object's callbacks.
|
||||
|
||||
Args:
|
||||
obj (Object): the connected objects.
|
||||
|
||||
Returns:
|
||||
A dictionary of the object's callbacks.
|
||||
|
||||
Note:
|
||||
This method can be useful to override in some contexts,
|
||||
when several objects would share callbacks.
|
||||
|
||||
"""
|
||||
obj_callbacks = self.db.callbacks.get(obj, {})
|
||||
callbacks = {}
|
||||
for callback_name, callback_list in obj_callbacks.items():
|
||||
new_list = []
|
||||
for i, callback in enumerate(callback_list):
|
||||
callback = dict(callback)
|
||||
callback["obj"] = obj
|
||||
callback["name"] = callback_name
|
||||
callback["number"] = i
|
||||
new_list.append(callback)
|
||||
|
||||
if new_list:
|
||||
callbacks[callback_name] = new_list
|
||||
|
||||
return callbacks
|
||||
|
||||
def add_callback(self, obj, callback_name, code, author=None, valid=False,
|
||||
parameters=""):
|
||||
"""
|
||||
Add the specified callback.
|
||||
|
||||
Args:
|
||||
obj (Object): the Evennia typeclassed object to be extended.
|
||||
callback_name (str): the name of the callback to add.
|
||||
code (str): the Python code associated with this callback.
|
||||
author (Character or Player, optional): the author of the callback.
|
||||
valid (bool, optional): should the callback be connected?
|
||||
parameters (str, optional): optional parameters.
|
||||
|
||||
Note:
|
||||
This method doesn't check that the callback type exists.
|
||||
|
||||
"""
|
||||
obj_callbacks = self.db.callbacks.get(obj, {})
|
||||
if not obj_callbacks:
|
||||
self.db.callbacks[obj] = {}
|
||||
obj_callbacks = self.db.callbacks[obj]
|
||||
|
||||
callbacks = obj_callbacks.get(callback_name, [])
|
||||
if not callbacks:
|
||||
obj_callbacks[callback_name] = []
|
||||
callbacks = obj_callbacks[callback_name]
|
||||
|
||||
# Add the callback in the list
|
||||
callbacks.append({
|
||||
"created_on": datetime.now(),
|
||||
"author": author,
|
||||
"valid": valid,
|
||||
"code": code,
|
||||
"parameters": parameters,
|
||||
})
|
||||
|
||||
# If not valid, set it in 'to_valid'
|
||||
if not valid:
|
||||
self.db.to_valid.append((obj, callback_name, len(callbacks) - 1))
|
||||
|
||||
# Call the custom_add if needed
|
||||
custom_add = self.get_events(obj).get(
|
||||
callback_name, [None, None, None, None])[3]
|
||||
if custom_add:
|
||||
custom_add(obj, callback_name, len(callbacks) - 1, parameters)
|
||||
|
||||
# Build the definition to return (a dictionary)
|
||||
definition = dict(callbacks[-1])
|
||||
definition["obj"] = obj
|
||||
definition["name"] = callback_name
|
||||
definition["number"] = len(callbacks) - 1
|
||||
return definition
|
||||
|
||||
def edit_callback(self, obj, callback_name, number, code, author=None,
|
||||
valid=False):
|
||||
"""
|
||||
Edit the specified callback.
|
||||
|
||||
Args:
|
||||
obj (Object): the Evennia typeclassed object to be edited.
|
||||
callback_name (str): the name of the callback to edit.
|
||||
number (int): the callback number to be changed.
|
||||
code (str): the Python code associated with this callback.
|
||||
author (Character or Player, optional): the author of the callback.
|
||||
valid (bool, optional): should the callback be connected?
|
||||
|
||||
Raises:
|
||||
RuntimeError if the callback is locked.
|
||||
|
||||
Note:
|
||||
This method doesn't check that the callback type exists.
|
||||
|
||||
"""
|
||||
obj_callbacks = self.db.callbacks.get(obj, {})
|
||||
if not obj_callbacks:
|
||||
self.db.callbacks[obj] = {}
|
||||
obj_callbacks = self.db.callbacks[obj]
|
||||
|
||||
callbacks = obj_callbacks.get(callback_name, [])
|
||||
if not callbacks:
|
||||
obj_callbacks[callback_name] = []
|
||||
callbacks = obj_callbacks[callback_name]
|
||||
|
||||
# If locked, don't edit it
|
||||
if (obj, callback_name, number) in self.db.locked:
|
||||
raise RuntimeError("this callback is locked.")
|
||||
|
||||
# Edit the callback
|
||||
callbacks[number].update({
|
||||
"updated_on": datetime.now(),
|
||||
"updated_by": author,
|
||||
"valid": valid,
|
||||
"code": code,
|
||||
})
|
||||
|
||||
# If not valid, set it in 'to_valid'
|
||||
if not valid and (obj, callback_name, number) not in self.db.to_valid:
|
||||
self.db.to_valid.append((obj, callback_name, number))
|
||||
elif valid and (obj, callback_name, number) in self.db.to_valid:
|
||||
self.db.to_valid.remove((obj, callback_name, number))
|
||||
|
||||
# Build the definition to return (a dictionary)
|
||||
definition = dict(callbacks[number])
|
||||
definition["obj"] = obj
|
||||
definition["name"] = callback_name
|
||||
definition["number"] = number
|
||||
return definition
|
||||
|
||||
def del_callback(self, obj, callback_name, number):
|
||||
"""
|
||||
Delete the specified callback.
|
||||
|
||||
Args:
|
||||
obj (Object): the typeclassed object containing the callback.
|
||||
callback_name (str): the name of the callback to delete.
|
||||
number (int): the number of the callback to delete.
|
||||
|
||||
Raises:
|
||||
RuntimeError if the callback is locked.
|
||||
|
||||
"""
|
||||
obj_callbacks = self.db.callbacks.get(obj, {})
|
||||
callbacks = obj_callbacks.get(callback_name, [])
|
||||
|
||||
# If locked, don't edit it
|
||||
if (obj, callback_name, number) in self.db.locked:
|
||||
raise RuntimeError("this callback is locked.")
|
||||
|
||||
# Delete the callback itself
|
||||
try:
|
||||
code = callbacks[number]["code"]
|
||||
except IndexError:
|
||||
return
|
||||
else:
|
||||
logger.log_info("Deleting callback {} {} of {}:\n{}".format(
|
||||
callback_name, number, obj, code))
|
||||
del callbacks[number]
|
||||
|
||||
# Change IDs of callbacks to be validated
|
||||
i = 0
|
||||
while i < len(self.db.to_valid):
|
||||
t_obj, t_callback_name, t_number = self.db.to_valid[i]
|
||||
if obj is t_obj and callback_name == t_callback_name:
|
||||
if t_number == number:
|
||||
# Strictly equal, delete the callback
|
||||
del self.db.to_valid[i]
|
||||
i -= 1
|
||||
elif t_number > number:
|
||||
# Change the ID for this callback
|
||||
self.db.to_valid.insert(i, (t_obj, t_callback_name,
|
||||
t_number - 1))
|
||||
del self.db.to_valid[i + 1]
|
||||
i += 1
|
||||
|
||||
# Update locked callback
|
||||
for i, line in enumerate(self.db.locked):
|
||||
t_obj, t_callback_name, t_number = line
|
||||
if obj is t_obj and callback_name == t_callback_name:
|
||||
if number < t_number:
|
||||
self.db.locked[i] = (t_obj, t_callback_name, t_number - 1)
|
||||
|
||||
# Delete time-related callbacks associated with this object
|
||||
for script in list(obj.scripts.all()):
|
||||
if isinstance(script, TimecallbackScript):
|
||||
if script.obj is obj and script.db.callback_name == callback_name:
|
||||
if script.db.number == number:
|
||||
script.stop()
|
||||
elif script.db.number > number:
|
||||
script.db.number -= 1
|
||||
|
||||
def accept_callback(self, obj, callback_name, number):
|
||||
"""
|
||||
Valid a callback.
|
||||
|
||||
Args:
|
||||
obj (Object): the object containing the callback.
|
||||
callback_name (str): the name of the callback.
|
||||
number (int): the number of the callback.
|
||||
|
||||
"""
|
||||
obj_callbacks = self.db.callbacks.get(obj, {})
|
||||
callbacks = obj_callbacks.get(callback_name, [])
|
||||
|
||||
# Accept and connect the callback
|
||||
callbacks[number].update({"valid": True})
|
||||
if (obj, callback_name, number) in self.db.to_valid:
|
||||
self.db.to_valid.remove((obj, callback_name, number))
|
||||
|
||||
def call(self, obj, callback_name, *args, **kwargs):
|
||||
"""
|
||||
Call the connected callbacks.
|
||||
|
||||
Args:
|
||||
obj (Object): the Evennia typeclassed object.
|
||||
callback_name (str): the callback name to call.
|
||||
*args: additional variables for this callback.
|
||||
|
||||
Kwargs:
|
||||
number (int, optional): call just a specific callback.
|
||||
parameters (str, optional): call a callback with parameters.
|
||||
locals (dict, optional): a locals replacement.
|
||||
|
||||
Returns:
|
||||
True to report the callback was called without interruption,
|
||||
False otherwise.
|
||||
|
||||
"""
|
||||
# First, look for the callback type corresponding to this name
|
||||
number = kwargs.get("number")
|
||||
parameters = kwargs.get("parameters")
|
||||
locals = kwargs.get("locals")
|
||||
|
||||
# Errors should not pass silently
|
||||
allowed = ("number", "parameters", "locals")
|
||||
if any(k for k in kwargs if k not in allowed):
|
||||
raise TypeError("Unknown keyword arguments were specified " \
|
||||
"to call callbacks: {}".format(kwargs))
|
||||
|
||||
event = self.get_events(obj).get(callback_name)
|
||||
if locals is None and not event:
|
||||
logger.log_err("The callback {} for the object {} (typeclass " \
|
||||
"{}) can't be found".format(callback_name, obj, type(obj)))
|
||||
return False
|
||||
|
||||
# Prepare the locals if necessary
|
||||
if locals is None:
|
||||
locals = self.ndb.fresh_locals.copy()
|
||||
for i, variable in enumerate(event[0]):
|
||||
try:
|
||||
locals[variable] = args[i]
|
||||
except IndexError:
|
||||
logger.log_trace("callback {} of {} ({}): need variable " \
|
||||
"{} in position {}".format(callback_name, obj,
|
||||
type(obj), variable, i))
|
||||
return False
|
||||
else:
|
||||
locals = {key: value for key, value in locals.items()}
|
||||
|
||||
callbacks = self.get_callbacks(obj).get(callback_name, [])
|
||||
if event:
|
||||
custom_call = event[2]
|
||||
if custom_call:
|
||||
callbacks = custom_call(callbacks, parameters)
|
||||
|
||||
# Now execute all the valid callbacks linked at this address
|
||||
self.ndb.current_locals = locals
|
||||
for i, callback in enumerate(callbacks):
|
||||
if not callback["valid"]:
|
||||
continue
|
||||
|
||||
if number is not None and callback["number"] != number:
|
||||
continue
|
||||
|
||||
try:
|
||||
exec(callback["code"], locals, locals)
|
||||
except InterruptEvent:
|
||||
return False
|
||||
except Exception:
|
||||
etype, evalue, tb = sys.exc_info()
|
||||
trace = traceback.format_exception(etype, evalue, tb)
|
||||
self.handle_error(callback, trace)
|
||||
|
||||
return True
|
||||
|
||||
def handle_error(self, callback, trace):
|
||||
"""
|
||||
Handle an error in a callback.
|
||||
|
||||
Args:
|
||||
callback (dict): the callback representation.
|
||||
trace (list): the traceback containing the exception.
|
||||
|
||||
Notes:
|
||||
This method can be useful to override to change the default
|
||||
handling of errors. By default, the error message is sent to
|
||||
the character who last updated the callback, if connected.
|
||||
If not, display to the everror channel.
|
||||
|
||||
"""
|
||||
callback_name = callback["name"]
|
||||
number = callback["number"]
|
||||
obj = callback["obj"]
|
||||
oid = obj.id
|
||||
logger.log_err("An error occurred during the callback {} of " \
|
||||
"{} (#{}), number {}\n{}".format(callback_name, obj,
|
||||
oid, number + 1, "\n".join(trace)))
|
||||
|
||||
# Create the error message
|
||||
line = "|runknown|n"
|
||||
lineno = "|runknown|n"
|
||||
for error in trace:
|
||||
if error.startswith(' File "<string>", line '):
|
||||
res = RE_LINE_ERROR.search(error)
|
||||
if res:
|
||||
lineno = int(res.group(1))
|
||||
|
||||
# Try to extract the line
|
||||
try:
|
||||
line = raw(callback["code"].splitlines()[lineno - 1])
|
||||
except IndexError:
|
||||
continue
|
||||
else:
|
||||
break
|
||||
|
||||
exc = raw(trace[-1].strip("\n").splitlines()[-1])
|
||||
err_msg = "Error in {} of {} (#{})[{}], line {}:" \
|
||||
" {}\n{}".format(callback_name, obj,
|
||||
oid, number + 1, lineno, line, exc)
|
||||
|
||||
# Inform the last updater if connected
|
||||
updater = callback.get("updated_by")
|
||||
if updater is None:
|
||||
updater = callback["created_by"]
|
||||
|
||||
if updater and updater.sessions.all():
|
||||
updater.msg(err_msg)
|
||||
else:
|
||||
err_msg = "Error in {} of {} (#{})[{}], line {}:" \
|
||||
" {}\n {}".format(callback_name, obj,
|
||||
oid, number + 1, lineno, line, exc)
|
||||
self.ndb.channel.msg(err_msg)
|
||||
|
||||
def add_event(self, typeclass, name, variables, help_text, custom_call, custom_add):
|
||||
"""
|
||||
Add a new event for a defined typeclass.
|
||||
|
||||
Args:
|
||||
typeclass (str): the path leading to the typeclass.
|
||||
name (str): the name of the event to add.
|
||||
variables (list of str): list of variable names for this event.
|
||||
help_text (str): the long help text of the event.
|
||||
custom_call (callable or None): the function to be called
|
||||
when the event fires.
|
||||
custom_add (callable or None): the function to be called when
|
||||
a callback is added.
|
||||
|
||||
"""
|
||||
if typeclass not in self.ndb.events:
|
||||
self.ndb.events[typeclass] = {}
|
||||
|
||||
events = self.ndb.events[typeclass]
|
||||
if name not in events:
|
||||
events[name] = (variables, help_text, custom_call, custom_add)
|
||||
|
||||
def set_task(self, seconds, obj, callback_name):
|
||||
"""
|
||||
Set and schedule a task to run.
|
||||
|
||||
Args:
|
||||
seconds (int, float): the delay in seconds from now.
|
||||
obj (Object): the typecalssed object connected to the event.
|
||||
callback_name (str): the callback's name.
|
||||
|
||||
Notes:
|
||||
This method allows to schedule a "persistent" task.
|
||||
'utils.delay' is called, but a copy of the task is kept in
|
||||
the event handler, and when the script restarts (after reload),
|
||||
the differed delay is called again.
|
||||
The dictionary of locals is frozen and will be available
|
||||
again when the task runs. This feature, however, is limited
|
||||
by the database: all data cannot be saved. Lambda functions,
|
||||
class methods, objects inside an instance and so on will
|
||||
not be kept in the locals dictionary.
|
||||
|
||||
"""
|
||||
now = datetime.now()
|
||||
delta = timedelta(seconds=seconds)
|
||||
|
||||
# Choose a free task_id
|
||||
used_ids = list(self.db.tasks.keys())
|
||||
task_id = 1
|
||||
while task_id in used_ids:
|
||||
task_id += 1
|
||||
|
||||
# Collect and freeze current locals
|
||||
locals = {}
|
||||
for key, value in self.ndb.current_locals.items():
|
||||
try:
|
||||
dbserialize(value)
|
||||
except TypeError:
|
||||
continue
|
||||
else:
|
||||
locals[key] = value
|
||||
|
||||
self.db.tasks[task_id] = (now + delta, obj, callback_name, locals)
|
||||
delay(seconds, complete_task, task_id)
|
||||
|
||||
|
||||
# Script to call time-related events
|
||||
class TimeEventScript(DefaultScript):
|
||||
|
||||
"""Gametime-sensitive script."""
|
||||
|
||||
def at_script_creation(self):
|
||||
"""The script is created."""
|
||||
self.start_delay = True
|
||||
self.persistent = True
|
||||
|
||||
# Script attributes
|
||||
self.db.time_format = None
|
||||
self.db.event_name = "time"
|
||||
self.db.number = None
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Call the event and reset interval.
|
||||
|
||||
It is necessary to restart the script to reset its interval
|
||||
only twice after a reload. When the script has undergone
|
||||
down time, there's usually a slight shift in game time. Once
|
||||
the script restarts once, it will set the average time it
|
||||
needs for all its future intervals and should not need to be
|
||||
restarted. In short, a script that is created shouldn't need
|
||||
to restart more than once, and a script that is reloaded should
|
||||
restart only twice.
|
||||
|
||||
"""
|
||||
if self.db.time_format:
|
||||
# If the 'usual' time is set, use it
|
||||
seconds = self.ndb.usual
|
||||
if seconds is None:
|
||||
seconds, usual, details = get_next_wait(self.db.time_format)
|
||||
self.ndb.usual = usual
|
||||
|
||||
if self.interval != seconds:
|
||||
self.restart(interval=seconds)
|
||||
|
||||
if self.db.event_name and self.db.number is not None:
|
||||
obj = self.obj
|
||||
if not obj.callbacks:
|
||||
return
|
||||
|
||||
event_name = self.db.event_name
|
||||
number = self.db.number
|
||||
obj.callbacks.call(event_name, obj, number=number)
|
||||
|
||||
|
||||
# Functions to manipulate tasks
|
||||
def complete_task(task_id):
|
||||
"""
|
||||
Mark the task in the event handler as complete.
|
||||
|
||||
Args:
|
||||
task_id (int): the task ID.
|
||||
|
||||
Note:
|
||||
This function should be called automatically for individual tasks.
|
||||
|
||||
"""
|
||||
try:
|
||||
script = ScriptDB.objects.get(db_key="event_handler")
|
||||
except ScriptDB.DoesNotExist:
|
||||
logger.log_trace("Can't get the event handler.")
|
||||
return
|
||||
|
||||
if task_id not in script.db.tasks:
|
||||
logger.log_err("The task #{} was scheduled, but it cannot be " \
|
||||
"found".format(task_id))
|
||||
return
|
||||
|
||||
delta, obj, callback_name, locals = script.db.tasks.pop(task_id)
|
||||
script.call(obj, callback_name, locals=locals)
|
||||
516
evennia/contrib/events/tests.py
Normal file
516
evennia/contrib/events/tests.py
Normal file
|
|
@ -0,0 +1,516 @@
|
|||
"""
|
||||
Module containing the test cases for the event system.
|
||||
"""
|
||||
|
||||
from mock import Mock
|
||||
from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import ScriptDB
|
||||
from evennia.commands.default.tests import CommandTest
|
||||
from evennia.objects.objects import ExitCommand
|
||||
from evennia.utils import ansi, utils
|
||||
from evennia.utils.create import create_object, create_script
|
||||
from evennia.utils.test_resources import EvenniaTest
|
||||
from evennia.contrib.events.commands import CmdCallback
|
||||
from evennia.contrib.events.callbackhandler import CallbackHandler
|
||||
|
||||
# Force settings
|
||||
settings.EVENTS_CALENDAR = "standard"
|
||||
|
||||
# Constants
|
||||
OLD_EVENTS = {}
|
||||
|
||||
class TestEventHandler(EvenniaTest):
|
||||
|
||||
"""
|
||||
Test cases of the event handler to add, edit or delete events.
|
||||
"""
|
||||
|
||||
def setUp(self):
|
||||
"""Create the event handler."""
|
||||
super(TestEventHandler, self).setUp()
|
||||
self.handler = create_script(
|
||||
"evennia.contrib.events.scripts.EventHandler")
|
||||
|
||||
# Copy old events if necessary
|
||||
if OLD_EVENTS:
|
||||
self.handler.ndb.events = dict(OLD_EVENTS)
|
||||
|
||||
# Alter typeclasses
|
||||
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop the event handler."""
|
||||
OLD_EVENTS.clear()
|
||||
OLD_EVENTS.update(self.handler.ndb.events)
|
||||
self.handler.stop()
|
||||
CallbackHandler.script = None
|
||||
super(TestEventHandler, self).tearDown()
|
||||
|
||||
def test_start(self):
|
||||
"""Simply make sure the handler runs with proper initial values."""
|
||||
self.assertEqual(self.handler.db.callbacks, {})
|
||||
self.assertEqual(self.handler.db.to_valid, [])
|
||||
self.assertEqual(self.handler.db.locked, [])
|
||||
self.assertEqual(self.handler.db.tasks, {})
|
||||
self.assertIsNotNone(self.handler.ndb.events)
|
||||
|
||||
def test_add_validation(self):
|
||||
"""Add a callback while needing validation."""
|
||||
author = self.char1
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 40", author=author, valid=False)
|
||||
callback = self.handler.get_callbacks(self.room1).get("dummy")
|
||||
callback = callback[0]
|
||||
self.assertIsNotNone(callback)
|
||||
self.assertEqual(callback["author"], author)
|
||||
self.assertEqual(callback["valid"], False)
|
||||
|
||||
# Since this callback is not valid, it should appear in 'to_valid'
|
||||
self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)
|
||||
|
||||
# Run this dummy callback (shouldn't do anything)
|
||||
self.char1.db.strength = 10
|
||||
locals = {"character": self.char1}
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals=locals))
|
||||
self.assertEqual(self.char1.db.strength, 10)
|
||||
|
||||
def test_edit(self):
|
||||
"""Test editing a callback."""
|
||||
author = self.char1
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 60", author=author, valid=True)
|
||||
|
||||
# Edit it right away
|
||||
self.handler.edit_callback(self.room1, "dummy", 0,
|
||||
"character.db.strength = 65", author=self.char2, valid=True)
|
||||
|
||||
# Check that the callback was written
|
||||
callback = self.handler.get_callbacks(self.room1).get("dummy")
|
||||
callback = callback[0]
|
||||
self.assertIsNotNone(callback)
|
||||
self.assertEqual(callback["author"], author)
|
||||
self.assertEqual(callback["valid"], True)
|
||||
self.assertEqual(callback["updated_by"], self.char2)
|
||||
|
||||
# Run this dummy callback
|
||||
self.char1.db.strength = 10
|
||||
locals = {"character": self.char1}
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals=locals))
|
||||
self.assertEqual(self.char1.db.strength, 65)
|
||||
|
||||
def test_edit_validation(self):
|
||||
"""Edit a callback when validation isn't automatic."""
|
||||
author = self.char1
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 70", author=author, valid=True)
|
||||
|
||||
# Edit it right away
|
||||
self.handler.edit_callback(self.room1, "dummy", 0,
|
||||
"character.db.strength = 80", author=self.char2, valid=False)
|
||||
|
||||
# Run this dummy callback (shouldn't do anything)
|
||||
self.char1.db.strength = 10
|
||||
locals = {"character": self.char1}
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals=locals))
|
||||
self.assertEqual(self.char1.db.strength, 10)
|
||||
|
||||
def test_del(self):
|
||||
"""Try to delete a callback."""
|
||||
# Add 3 callbacks
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 5", author=self.char1, valid=True)
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 8", author=self.char2, valid=False)
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 9", author=self.char1, valid=True)
|
||||
|
||||
# Note that the second callback isn't valid
|
||||
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
|
||||
|
||||
# Lock the third callback
|
||||
self.handler.db.locked.append((self.room1, "dummy", 2))
|
||||
|
||||
# Delete the first callback
|
||||
self.handler.del_callback(self.room1, "dummy", 0)
|
||||
|
||||
# The callback #1 that was to valid should be #0 now
|
||||
self.assertIn((self.room1, "dummy", 0), self.handler.db.to_valid)
|
||||
self.assertNotIn((self.room1, "dummy", 1), self.handler.db.to_valid)
|
||||
|
||||
# The lock has been updated too
|
||||
self.assertIn((self.room1, "dummy", 1), self.handler.db.locked)
|
||||
self.assertNotIn((self.room1, "dummy", 2), self.handler.db.locked)
|
||||
|
||||
# Now delete the first (not valid) callback
|
||||
self.handler.del_callback(self.room1, "dummy", 0)
|
||||
self.assertEqual(self.handler.db.to_valid, [])
|
||||
self.assertIn((self.room1, "dummy", 0), self.handler.db.locked)
|
||||
self.assertNotIn((self.room1, "dummy", 1), self.handler.db.locked)
|
||||
|
||||
# Call the remaining callback
|
||||
self.char1.db.strength = 10
|
||||
locals = {"character": self.char1}
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals=locals))
|
||||
self.assertEqual(self.char1.db.strength, 9)
|
||||
|
||||
def test_accept(self):
|
||||
"""Accept an callback."""
|
||||
# Add 2 callbacks
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 5", author=self.char1, valid=True)
|
||||
self.handler.add_callback(self.room1, "dummy",
|
||||
"character.db.strength = 8", author=self.char2, valid=False)
|
||||
|
||||
# Note that the second callback isn't valid
|
||||
self.assertIn((self.room1, "dummy", 1), self.handler.db.to_valid)
|
||||
|
||||
# Accept the second callback
|
||||
self.handler.accept_callback(self.room1, "dummy", 1)
|
||||
callback = self.handler.get_callbacks(self.room1).get("dummy")
|
||||
callback = callback[1]
|
||||
self.assertIsNotNone(callback)
|
||||
self.assertEqual(callback["valid"], True)
|
||||
|
||||
# Call the dummy callback
|
||||
self.char1.db.strength = 10
|
||||
locals = {"character": self.char1}
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals=locals))
|
||||
self.assertEqual(self.char1.db.strength, 8)
|
||||
|
||||
def test_call(self):
|
||||
"""Test to call amore complex callback."""
|
||||
self.char1.key = "one"
|
||||
self.char2.key = "two"
|
||||
|
||||
# Add an callback
|
||||
code = dedent("""
|
||||
if character.key == "one":
|
||||
character.db.health = 50
|
||||
else:
|
||||
character.db.health = 0
|
||||
""".strip("\n"))
|
||||
self.handler.add_callback(self.room1, "dummy", code,
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Call the dummy callback
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals={"character": self.char1}))
|
||||
self.assertEqual(self.char1.db.health, 50)
|
||||
self.assertTrue(self.handler.call(
|
||||
self.room1, "dummy", locals={"character": self.char2}))
|
||||
self.assertEqual(self.char2.db.health, 0)
|
||||
|
||||
def test_handler(self):
|
||||
"""Test the object handler."""
|
||||
self.assertIsNotNone(self.char1.callbacks)
|
||||
|
||||
# Add an callback
|
||||
callback = self.room1.callbacks.add("dummy", "pass", author=self.char1,
|
||||
valid=True)
|
||||
self.assertEqual(callback.obj, self.room1)
|
||||
self.assertEqual(callback.name, "dummy")
|
||||
self.assertEqual(callback.code, "pass")
|
||||
self.assertEqual(callback.author, self.char1)
|
||||
self.assertEqual(callback.valid, True)
|
||||
self.assertIn([callback], self.room1.callbacks.all().values())
|
||||
|
||||
# Edit this very callback
|
||||
new = self.room1.callbacks.edit("dummy", 0, "character.db.say = True",
|
||||
author=self.char1, valid=True)
|
||||
self.assertIn([new], self.room1.callbacks.all().values())
|
||||
self.assertNotIn([callback], self.room1.callbacks.all().values())
|
||||
|
||||
# Try to call this callback
|
||||
self.assertTrue(self.room1.callbacks.call("dummy",
|
||||
locals={"character": self.char2}))
|
||||
self.assertTrue(self.char2.db.say)
|
||||
|
||||
# Delete the callback
|
||||
self.room1.callbacks.remove("dummy", 0)
|
||||
self.assertEqual(self.room1.callbacks.all(), {})
|
||||
|
||||
|
||||
class TestCmdCallback(CommandTest):
|
||||
|
||||
"""Test the @callback command."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create the callback handler."""
|
||||
super(TestCmdCallback, self).setUp()
|
||||
self.handler = create_script(
|
||||
"evennia.contrib.events.scripts.EventHandler")
|
||||
|
||||
# Copy old events if necessary
|
||||
if OLD_EVENTS:
|
||||
self.handler.ndb.events = dict(OLD_EVENTS)
|
||||
|
||||
# Alter typeclasses
|
||||
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop the callback handler."""
|
||||
OLD_EVENTS.clear()
|
||||
OLD_EVENTS.update(self.handler.ndb.events)
|
||||
self.handler.stop()
|
||||
for script in ScriptDB.objects.filter(
|
||||
db_typeclass_path="evennia.contrib.events.scripts.TimeEventScript"):
|
||||
script.stop()
|
||||
|
||||
CallbackHandler.script = None
|
||||
super(TestCmdCallback, self).tearDown()
|
||||
|
||||
def test_list(self):
|
||||
"""Test listing callbacks with different rights."""
|
||||
table = self.call(CmdCallback(), "out")
|
||||
lines = table.splitlines()[3:-1]
|
||||
self.assertNotEqual(lines, [])
|
||||
|
||||
# Check that the second column only contains 0 (0) (no callback yet)
|
||||
for line in lines:
|
||||
cols = line.split("|")
|
||||
self.assertIn(cols[2].strip(), ("0 (0)", ""))
|
||||
|
||||
# Add some callback
|
||||
self.handler.add_callback(self.exit, "traverse", "pass",
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Try to obtain more details on a specific callback on exit
|
||||
table = self.call(CmdCallback(), "out = traverse")
|
||||
lines = table.splitlines()[3:-1]
|
||||
self.assertEqual(len(lines), 1)
|
||||
line = lines[0]
|
||||
cols = line.split("|")
|
||||
self.assertIn(cols[1].strip(), ("1", ""))
|
||||
self.assertIn(cols[2].strip(), (str(self.char1), ""))
|
||||
self.assertIn(cols[-1].strip(), ("Yes", "No", ""))
|
||||
|
||||
# Run the same command with char2
|
||||
# char2 shouldn't see the last column (Valid)
|
||||
table = self.call(CmdCallback(), "out = traverse", caller=self.char2)
|
||||
lines = table.splitlines()[3:-1]
|
||||
self.assertEqual(len(lines), 1)
|
||||
line = lines[0]
|
||||
cols = line.split("|")
|
||||
self.assertEqual(cols[1].strip(), "1")
|
||||
self.assertNotIn(cols[-1].strip(), ("Yes", "No"))
|
||||
|
||||
# In any case, display the callback
|
||||
# The last line should be "pass" (the callback code)
|
||||
details = self.call(CmdCallback(), "out = traverse 1")
|
||||
self.assertEqual(details.splitlines()[-1], "pass")
|
||||
|
||||
def test_add(self):
|
||||
"""Test to add an callback."""
|
||||
self.call(CmdCallback(), "/add out = traverse")
|
||||
editor = self.char1.ndb._eveditor
|
||||
self.assertIsNotNone(editor)
|
||||
|
||||
# Edit the callback
|
||||
editor.update_buffer(dedent("""
|
||||
if character.key == "one":
|
||||
character.msg("You can pass.")
|
||||
else:
|
||||
character.msg("You can't pass.")
|
||||
deny()
|
||||
""".strip("\n")))
|
||||
editor.save_buffer()
|
||||
editor.quit()
|
||||
callback = self.exit.callbacks.get("traverse")[0]
|
||||
self.assertEqual(callback.author, self.char1)
|
||||
self.assertEqual(callback.valid, True)
|
||||
self.assertTrue(len(callback.code) > 0)
|
||||
|
||||
# We're going to try the same thing but with char2
|
||||
# char2 being a player for our test, the callback won't be validated.
|
||||
self.call(CmdCallback(), "/add out = traverse", caller=self.char2)
|
||||
editor = self.char2.ndb._eveditor
|
||||
self.assertIsNotNone(editor)
|
||||
|
||||
# Edit the callback
|
||||
editor.update_buffer(dedent("""
|
||||
character.msg("No way.")
|
||||
""".strip("\n")))
|
||||
editor.save_buffer()
|
||||
editor.quit()
|
||||
callback = self.exit.callbacks.get("traverse")[1]
|
||||
self.assertEqual(callback.author, self.char2)
|
||||
self.assertEqual(callback.valid, False)
|
||||
self.assertTrue(len(callback.code) > 0)
|
||||
|
||||
def test_del(self):
|
||||
"""Add and remove an callback."""
|
||||
self.handler.add_callback(self.exit, "traverse", "pass",
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Try to delete the callback
|
||||
# char2 shouldn't be allowed to do so (that's not HIS callback)
|
||||
self.call(CmdCallback(), "/del out = traverse 1", caller=self.char2)
|
||||
self.assertTrue(len(self.handler.get_callbacks(self.exit).get(
|
||||
"traverse", [])) == 1)
|
||||
|
||||
# Now, char1 should be allowed to delete it
|
||||
self.call(CmdCallback(), "/del out = traverse 1")
|
||||
self.assertTrue(len(self.handler.get_callbacks(self.exit).get(
|
||||
"traverse", [])) == 0)
|
||||
|
||||
def test_lock(self):
|
||||
"""Test the lock of multiple editing."""
|
||||
self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
|
||||
self.assertIsNotNone(self.char2.ndb._eveditor)
|
||||
|
||||
# Now ask char1 to edit
|
||||
line = self.call(CmdCallback(), "/edit here = time 1")
|
||||
self.assertIsNone(self.char1.ndb._eveditor)
|
||||
|
||||
# Try to delete this callback while char2 is editing it
|
||||
line = self.call(CmdCallback(), "/del here = time 1")
|
||||
|
||||
def test_accept(self):
|
||||
"""Accept an callback."""
|
||||
self.call(CmdCallback(), "/add here = time 8:00", caller=self.char2)
|
||||
editor = self.char2.ndb._eveditor
|
||||
self.assertIsNotNone(editor)
|
||||
|
||||
# Edit the callback
|
||||
editor.update_buffer(dedent("""
|
||||
room.msg_contents("It's 8 PM, everybody up!")
|
||||
""".strip("\n")))
|
||||
editor.save_buffer()
|
||||
editor.quit()
|
||||
callback = self.room1.callbacks.get("time")[0]
|
||||
self.assertEqual(callback.valid, False)
|
||||
|
||||
# chars shouldn't be allowed to the callback
|
||||
self.call(CmdCallback(), "/accept here = time 1", caller=self.char2)
|
||||
callback = self.room1.callbacks.get("time")[0]
|
||||
self.assertEqual(callback.valid, False)
|
||||
|
||||
# char1 will accept the callback
|
||||
self.call(CmdCallback(), "/accept here = time 1")
|
||||
callback = self.room1.callbacks.get("time")[0]
|
||||
self.assertEqual(callback.valid, True)
|
||||
|
||||
|
||||
class TestDefaultCallbacks(CommandTest):
|
||||
|
||||
"""Test the default callbacks."""
|
||||
|
||||
def setUp(self):
|
||||
"""Create the callback handler."""
|
||||
super(TestDefaultCallbacks, self).setUp()
|
||||
self.handler = create_script(
|
||||
"evennia.contrib.events.scripts.EventHandler")
|
||||
|
||||
# Copy old events if necessary
|
||||
if OLD_EVENTS:
|
||||
self.handler.ndb.events = dict(OLD_EVENTS)
|
||||
|
||||
# Alter typeclasses
|
||||
self.char1.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.char2.swap_typeclass("evennia.contrib.events.typeclasses.EventCharacter")
|
||||
self.room1.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.room2.swap_typeclass("evennia.contrib.events.typeclasses.EventRoom")
|
||||
self.exit.swap_typeclass("evennia.contrib.events.typeclasses.EventExit")
|
||||
|
||||
def tearDown(self):
|
||||
"""Stop the callback handler."""
|
||||
OLD_EVENTS.clear()
|
||||
OLD_EVENTS.update(self.handler.ndb.events)
|
||||
self.handler.stop()
|
||||
CallbackHandler.script = None
|
||||
super(TestDefaultCallbacks, self).tearDown()
|
||||
|
||||
def test_exit(self):
|
||||
"""Test the callbacks of an exit."""
|
||||
self.char1.key = "char1"
|
||||
code = dedent("""
|
||||
if character.key == "char1":
|
||||
character.msg("You can leave.")
|
||||
else:
|
||||
character.msg("You cannot leave.")
|
||||
deny()
|
||||
""".strip("\n"))
|
||||
# Enforce self.exit.destination since swapping typeclass lose it
|
||||
self.exit.destination = self.room2
|
||||
|
||||
# Try the can_traverse callback
|
||||
self.handler.add_callback(self.exit, "can_traverse", code,
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Have char1 move through the exit
|
||||
self.call(ExitCommand(), "", "You can leave.", obj=self.exit)
|
||||
self.assertIs(self.char1.location, self.room2)
|
||||
|
||||
# Have char2 move through this exit
|
||||
self.call(ExitCommand(), "", "You cannot leave.", obj=self.exit,
|
||||
caller=self.char2)
|
||||
self.assertIs(self.char2.location, self.room1)
|
||||
|
||||
# Try the traverse callback
|
||||
self.handler.del_callback(self.exit, "can_traverse", 0)
|
||||
self.handler.add_callback(self.exit, "traverse", "character.msg('Fine!')",
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Have char2 move through the exit
|
||||
self.call(ExitCommand(), "", obj=self.exit, caller=self.char2)
|
||||
self.assertIs(self.char2.location, self.room2)
|
||||
self.handler.del_callback(self.exit, "traverse", 0)
|
||||
|
||||
# Move char1 and char2 back
|
||||
self.char1.location = self.room1
|
||||
self.char2.location = self.room1
|
||||
|
||||
# Test msg_arrive and msg_leave
|
||||
code = 'message = "{character} goes out."'
|
||||
self.handler.add_callback(self.exit, "msg_leave", code,
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Have char1 move through the exit
|
||||
old_msg = self.char2.msg
|
||||
try:
|
||||
self.char2.msg = Mock()
|
||||
self.call(ExitCommand(), "", obj=self.exit)
|
||||
stored_msg = [args[0] if args and args[0] else kwargs.get("text",utils.to_str(kwargs, force_string=True))
|
||||
for name, args, kwargs in self.char2.msg.mock_calls]
|
||||
# Get the first element of a tuple if msg received a tuple instead of a string
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
|
||||
self.assertEqual(returned_msg, "char1 goes out.")
|
||||
finally:
|
||||
self.char2.msg = old_msg
|
||||
|
||||
# Create a return exit
|
||||
back = create_object("evennia.objects.objects.DefaultExit",
|
||||
key="in", location=self.room2, destination=self.room1)
|
||||
code = 'message = "{character} goes in."'
|
||||
self.handler.add_callback(self.exit, "msg_arrive", code,
|
||||
author=self.char1, valid=True)
|
||||
|
||||
# Have char1 move through the exit
|
||||
old_msg = self.char2.msg
|
||||
try:
|
||||
self.char2.msg = Mock()
|
||||
self.call(ExitCommand(), "", obj=back)
|
||||
stored_msg = [args[0] if args and args[0] else kwargs.get("text",utils.to_str(kwargs, force_string=True))
|
||||
for name, args, kwargs in self.char2.msg.mock_calls]
|
||||
# Get the first element of a tuple if msg received a tuple instead of a string
|
||||
stored_msg = [smsg[0] if isinstance(smsg, tuple) else smsg for smsg in stored_msg]
|
||||
returned_msg = ansi.parse_ansi("\n".join(stored_msg), strip_ansi=True)
|
||||
self.assertEqual(returned_msg, "char1 goes in.")
|
||||
finally:
|
||||
self.char2.msg = old_msg
|
||||
820
evennia/contrib/events/typeclasses.py
Normal file
820
evennia/contrib/events/typeclasses.py
Normal file
|
|
@ -0,0 +1,820 @@
|
|||
"""
|
||||
Typeclasses for the event system.
|
||||
|
||||
To use thm, one should inherit from these classes (EventObject,
|
||||
EventRoom, EventCharacter and EventExit).
|
||||
|
||||
"""
|
||||
|
||||
from evennia import DefaultCharacter, DefaultExit, DefaultObject, DefaultRoom
|
||||
from evennia import ScriptDB
|
||||
from evennia.utils.utils import delay, inherits_from, lazy_property
|
||||
from evennia.contrib.events.callbackhandler import CallbackHandler
|
||||
from evennia.contrib.events.utils import register_events, time_event, phrase_event
|
||||
|
||||
# Character help
|
||||
CHARACTER_CAN_DELETE = """
|
||||
Can the character be deleted?
|
||||
This event is called before the character is deleted. You can use
|
||||
'deny()' in this event to prevent this character from being deleted.
|
||||
If this event doesn't prevent the character from being deleted, its
|
||||
'delete' event is called right away.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
CHARACTER_CAN_MOVE = """
|
||||
Can the character move?
|
||||
This event is called before the character moves into another
|
||||
location. You can prevent the character from moving
|
||||
using the 'deny()' eventfunc.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
origin: the current location of the character.
|
||||
destination: the future location of the character.
|
||||
"""
|
||||
|
||||
CHARACTER_CAN_PART = """
|
||||
Can the departing charaacter leave this room?
|
||||
This event is called before another character can move from the
|
||||
location where the current character also is. This event can be
|
||||
used to prevent someone to leave this room if, for instance, he/she
|
||||
hasn't paid, or he/she is going to a protected area, past a guard,
|
||||
and so on. Use 'deny()' to prevent the departing character from
|
||||
moving.
|
||||
|
||||
Variables you can use in this event:
|
||||
departing: the character who wants to leave this room.
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
CHARACTER_CAN_SAY = """
|
||||
Before another character can say something in the same location.
|
||||
This event is called before another character says something in the
|
||||
character's location. The "something" in question can be modified,
|
||||
or the action can be prevented by using 'deny()'. To change the
|
||||
content of what the character says, simply change the variable
|
||||
'message' to another string of characters.
|
||||
|
||||
Variables you can use in this event:
|
||||
speaker: the character who is using the say command.
|
||||
character: the character connected to this event.
|
||||
message: the text spoken by the character.
|
||||
"""
|
||||
|
||||
CHARACTER_DELETE = """
|
||||
Before deleting the character.
|
||||
This event is called just before deleting this character. It shouldn't
|
||||
be prevented (using the `deny()` function at this stage doesn't
|
||||
have any effect). If you want to prevent deletion of this character,
|
||||
use the event `can_delete` instead.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
CHARACTER_GREET = """
|
||||
A new character arrives in the location of this character.
|
||||
This event is called when another character arrives in the location
|
||||
where the current character is. For instance, a puppeted character
|
||||
arrives in the shop of a shopkeeper (assuming the shopkeeper is
|
||||
a character). As its name suggests, this event can be very useful
|
||||
to have NPC greeting one another, or players, who come to visit.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
newcomer: the character arriving in the same location.
|
||||
"""
|
||||
|
||||
CHARACTER_MOVE = """
|
||||
After the character has moved into its new room.
|
||||
This event is called when the character has moved into a new
|
||||
room. It is too late to prevent the move at this point.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
origin: the old location of the character.
|
||||
destination: the new location of the character.
|
||||
"""
|
||||
|
||||
CHARACTER_PUPPETED = """
|
||||
When the character has been puppeted by a player.
|
||||
This event is called when a player has just puppeted this character.
|
||||
This can commonly happen when a player connects onto this character,
|
||||
or when puppeting to a NPC or free character.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
CHARACTER_SAY = """
|
||||
After another character has said something in the character's room.
|
||||
This event is called right after another character has said
|
||||
something in the same location.. The action cannot be prevented
|
||||
at this moment. Instead, this event is ideal to create keywords
|
||||
that would trigger a character (like a NPC) in doing something
|
||||
if a specific phrase is spoken in the same location.
|
||||
To use this event, you have to specify a list of keywords as
|
||||
parameters that should be present, as separate words, in the
|
||||
spoken phrase. For instance, you can set an event tthat would
|
||||
fire if the phrase spoken by the character contains "menu" or
|
||||
"dinner" or "lunch":
|
||||
@event/add ... = say menu, dinner, lunch
|
||||
Then if one of the words is present in what the character says,
|
||||
this event will fire.
|
||||
|
||||
Variables you can use in this event:
|
||||
speaker: the character speaking in this room.
|
||||
character: the character connected to this event.
|
||||
message: the text having been spoken by the character.
|
||||
"""
|
||||
|
||||
CHARACTER_TIME = """
|
||||
A repeated event to be called regularly.
|
||||
This event is scheduled to repeat at different times, specified
|
||||
as parameters. You can set it to run every day at 8:00 AM (game
|
||||
time). You have to specify the time as an argument to @event/add, like:
|
||||
@event/add here = time 8:00
|
||||
The parameter (8:00 here) must be a suite of digits separated by
|
||||
spaces, colons or dashes. Keep it as close from a recognizable
|
||||
date format, like this:
|
||||
@event/add here = time 06-15 12:20
|
||||
This event will fire every year on June the 15th at 12 PM (still
|
||||
game time). Units have to be specified depending on your set calendar
|
||||
(ask a developer for more details).
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
CHARACTER_UNPUPPETED = """
|
||||
When the character is about to be un-puppeted.
|
||||
This event is called when a player is about to un-puppet the
|
||||
character, which can happen if the player is disconnecting or
|
||||
changing puppets.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
"""
|
||||
|
||||
@register_events
|
||||
class EventCharacter(DefaultCharacter):
|
||||
|
||||
"""Typeclass to represent a character and call event types."""
|
||||
|
||||
_events = {
|
||||
"can_delete": (["character"], CHARACTER_CAN_DELETE),
|
||||
"can_move": (["character", "origin", "destination"], CHARACTER_CAN_MOVE),
|
||||
"can_part": (["character", "departing"], CHARACTER_CAN_PART),
|
||||
"can_say": (["speaker", "character", "message"], CHARACTER_CAN_SAY, phrase_event),
|
||||
"delete": (["character"], CHARACTER_DELETE),
|
||||
"greet": (["character", "newcomer"], CHARACTER_GREET),
|
||||
"move": (["character", "origin", "destination"], CHARACTER_MOVE),
|
||||
"puppeted": (["character"], CHARACTER_PUPPETED),
|
||||
"say": (["speaker", "character", "message"], CHARACTER_SAY, phrase_event),
|
||||
"time": (["character"], CHARACTER_TIME, None, time_event),
|
||||
"unpuppeted": (["character"], CHARACTER_UNPUPPETED),
|
||||
}
|
||||
|
||||
def announce_move_from(self, destination, msg=None, mapping=None):
|
||||
"""
|
||||
Called if the move is to be announced. This is
|
||||
called while we are still standing in the old
|
||||
location.
|
||||
|
||||
Args:
|
||||
destination (Object): The place we are going to.
|
||||
msg (str, optional): a replacement message.
|
||||
mapping (dict, optional): additional mapping objects.
|
||||
|
||||
You can override this method and call its parent with a
|
||||
message to simply change the default message. In the string,
|
||||
you can use the following as mappings (between braces):
|
||||
object: the object which is moving.
|
||||
exit: the exit from which the object is moving (if found).
|
||||
origin: the location of the object before the move.
|
||||
destination: the location of the object after moving.
|
||||
|
||||
"""
|
||||
if not self.location:
|
||||
return
|
||||
|
||||
string = msg or "{object} is leaving {origin}, heading for {destination}."
|
||||
|
||||
# Get the exit from location to destination
|
||||
location = self.location
|
||||
exits = [o for o in location.contents if o.location is location and o.destination is destination]
|
||||
mapping = mapping or {}
|
||||
mapping.update({
|
||||
"character": self,
|
||||
})
|
||||
|
||||
if exits:
|
||||
exits[0].callbacks.call("msg_leave", self, exits[0],
|
||||
location, destination, string, mapping)
|
||||
string = exits[0].callbacks.get_variable("message")
|
||||
mapping = exits[0].callbacks.get_variable("mapping")
|
||||
|
||||
# If there's no string, don't display anything
|
||||
# It can happen if the "message" variable in events is set to None
|
||||
if not string:
|
||||
return
|
||||
|
||||
super(EventCharacter, self).announce_move_from(destination, msg=string, mapping=mapping)
|
||||
|
||||
def announce_move_to(self, source_location, msg=None, mapping=None):
|
||||
"""
|
||||
Called after the move if the move was not quiet. At this point
|
||||
we are standing in the new location.
|
||||
|
||||
Args:
|
||||
source_location (Object): The place we came from
|
||||
msg (str, optional): the replacement message if location.
|
||||
mapping (dict, optional): additional mapping objects.
|
||||
|
||||
You can override this method and call its parent with a
|
||||
message to simply change the default message. In the string,
|
||||
you can use the following as mappings (between braces):
|
||||
object: the object which is moving.
|
||||
exit: the exit from which the object is moving (if found).
|
||||
origin: the location of the object before the move.
|
||||
destination: the location of the object after moving.
|
||||
|
||||
"""
|
||||
|
||||
if not source_location and self.location.has_player:
|
||||
# This was created from nowhere and added to a player's
|
||||
# inventory; it's probably the result of a create command.
|
||||
string = "You now have %s in your possession." % self.get_display_name(self.location)
|
||||
self.location.msg(string)
|
||||
return
|
||||
|
||||
if source_location:
|
||||
string = msg or "{character} arrives to {destination} from {origin}."
|
||||
else:
|
||||
string = "{character} arrives to {destination}."
|
||||
|
||||
origin = source_location
|
||||
destination = self.location
|
||||
exits = []
|
||||
mapping = mapping or {}
|
||||
mapping.update({
|
||||
"character": self,
|
||||
})
|
||||
|
||||
if origin:
|
||||
exits = [o for o in destination.contents if o.location is destination and o.destination is origin]
|
||||
if exits:
|
||||
exits[0].callbacks.call("msg_arrive", self, exits[0],
|
||||
origin, destination, string, mapping)
|
||||
string = exits[0].callbacks.get_variable("message")
|
||||
mapping = exits[0].callbacks.get_variable("mapping")
|
||||
|
||||
# If there's no string, don't display anything
|
||||
# It can happen if the "message" variable in events is set to None
|
||||
if not string:
|
||||
return
|
||||
|
||||
super(EventCharacter, self).announce_move_to(source_location, msg=string, mapping=mapping)
|
||||
|
||||
def at_before_move(self, destination):
|
||||
"""
|
||||
Called just before starting to move this object to
|
||||
destination.
|
||||
|
||||
Args:
|
||||
destination (Object): The object we are moving to
|
||||
|
||||
Returns:
|
||||
shouldmove (bool): If we should move or not.
|
||||
|
||||
Notes:
|
||||
If this method returns False/None, the move is cancelled
|
||||
before it is even started.
|
||||
|
||||
"""
|
||||
origin = self.location
|
||||
Room = DefaultRoom
|
||||
if isinstance(origin, Room) and isinstance(destination, Room):
|
||||
can = self.callbacks.call("can_move", self,
|
||||
origin, destination)
|
||||
if can:
|
||||
can = origin.callbacks.call("can_move", self, origin)
|
||||
if can:
|
||||
# Call other character's 'can_part' event
|
||||
for present in [o for o in origin.contents if isinstance(
|
||||
o, DefaultCharacter) and o is not self]:
|
||||
can = present.callbacks.call("can_part", present, self)
|
||||
if not can:
|
||||
break
|
||||
|
||||
if can is None:
|
||||
return True
|
||||
|
||||
return can
|
||||
|
||||
return True
|
||||
|
||||
def at_after_move(self, source_location):
|
||||
"""
|
||||
Called after move has completed, regardless of quiet mode or
|
||||
not. Allows changes to the object due to the location it is
|
||||
now in.
|
||||
|
||||
Args:
|
||||
source_location (Object): Wwhere we came from. This may be `None`.
|
||||
|
||||
"""
|
||||
super(EventCharacter, self).at_after_move(source_location)
|
||||
|
||||
origin = source_location
|
||||
destination = self.location
|
||||
Room = DefaultRoom
|
||||
if isinstance(origin, Room) and isinstance(destination, Room):
|
||||
self.callbacks.call("move", self, origin, destination)
|
||||
destination.callbacks.call("move", self, origin, destination)
|
||||
|
||||
# Call the 'greet' event of characters in the location
|
||||
for present in [o for o in destination.contents if isinstance(
|
||||
o, DefaultCharacter) and o is not self]:
|
||||
present.callbacks.call("greet", present, self)
|
||||
|
||||
def at_object_delete(self):
|
||||
"""
|
||||
Called just before the database object is permanently
|
||||
delete()d from the database. If this method returns False,
|
||||
deletion is aborted.
|
||||
|
||||
"""
|
||||
if not self.callbacks.call("can_delete", self):
|
||||
return False
|
||||
|
||||
self.callbacks.call("delete", self)
|
||||
return True
|
||||
|
||||
def at_post_puppet(self):
|
||||
"""
|
||||
Called just after puppeting has been completed and all
|
||||
Player<->Object links have been established.
|
||||
|
||||
Note:
|
||||
You can use `self.player` and `self.sessions.get()` to get
|
||||
player and sessions at this point; the last entry in the
|
||||
list from `self.sessions.get()` is the latest Session
|
||||
puppeting this Object.
|
||||
|
||||
"""
|
||||
super(EventCharacter, self).at_post_puppet()
|
||||
|
||||
self.callbacks.call("puppeted", self)
|
||||
|
||||
# Call the room's puppeted_in event
|
||||
location = self.location
|
||||
if location and isinstance(location, DefaultRoom):
|
||||
location.callbacks.call("puppeted_in", self, location)
|
||||
|
||||
def at_pre_unpuppet(self):
|
||||
"""
|
||||
Called just before beginning to un-connect a puppeting from
|
||||
this Player.
|
||||
|
||||
Note:
|
||||
You can use `self.player` and `self.sessions.get()` to get
|
||||
player and sessions at this point; the last entry in the
|
||||
list from `self.sessions.get()` is the latest Session
|
||||
puppeting this Object.
|
||||
|
||||
"""
|
||||
self.callbacks.call("unpuppeted", self)
|
||||
|
||||
# Call the room's unpuppeted_in event
|
||||
location = self.location
|
||||
if location and isinstance(location, DefaultRoom):
|
||||
location.callbacks.call("unpuppeted_in", self, location)
|
||||
|
||||
super(EventCharacter, self).at_pre_unpuppet()
|
||||
|
||||
|
||||
# Exit help
|
||||
EXIT_CAN_TRAVERSE = """
|
||||
Can the character traverse through this exit?
|
||||
This event is called when a character is about to traverse this
|
||||
exit. You can use the deny() function to deny the character from
|
||||
exitting for this time.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character that wants to traverse this exit.
|
||||
exit: the exit to be traversed.
|
||||
room: the room in which stands the character before moving.
|
||||
"""
|
||||
|
||||
EXIT_MSG_ARRIVE = """
|
||||
Customize the message when a character arrives through this exit.
|
||||
This event is called when a character arrives through this exit.
|
||||
To customize the message that will be sent to the room where the
|
||||
character arrives, change the value of the variable "message"
|
||||
to give it your custom message. The character itself will not be
|
||||
notified. You can use mapping between braces, like this:
|
||||
message = "{character} climbs out of a hole."
|
||||
In your mapping, you can use {character} (the character who has
|
||||
arrived), {exit} (the exit), {origin} (the room in which
|
||||
the character was), and {destination} (the room in which the character
|
||||
now is). If you need to customize the message with other information,
|
||||
you can also set "message" to None and send something else instead.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who is arriving through this exit.
|
||||
exit: the exit having been traversed.
|
||||
origin: the past location of the character.
|
||||
destination: the current location of the character.
|
||||
message: the message to be displayed in the destination.
|
||||
mapping: a dictionary containing the mapping of the message.
|
||||
"""
|
||||
|
||||
EXIT_MSG_LEAVE = """
|
||||
Customize the message when a character leaves through this exit.
|
||||
This event is called when a character leaves through this exit.
|
||||
To customize the message that will be sent to the room where the
|
||||
character came from, change the value of the variable "message"
|
||||
to give it your custom message. The character itself will not be
|
||||
notified. You can use mapping between braces, like this:
|
||||
message = "{character} falls into a hole!"
|
||||
In your mapping, you can use {character} (the character who is
|
||||
about to leave), {exit} (the exit), {origin} (the room in which
|
||||
the character is), and {destination} (the room in which the character
|
||||
is heading for). If you need to customize the message with other
|
||||
information, you can also set "message" to None and send something
|
||||
else instead.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who is leaving through this exit.
|
||||
exit: the exit being traversed.
|
||||
origin: the location of the character.
|
||||
destination: the destination of the character.
|
||||
message: the message to be displayed in the location.
|
||||
mapping: a dictionary containing additional mapping.
|
||||
"""
|
||||
|
||||
EXIT_TIME = """
|
||||
A repeated event to be called regularly.
|
||||
This event is scheduled to repeat at different times, specified
|
||||
as parameters. You can set it to run every day at 8:00 AM (game
|
||||
time). You have to specify the time as an argument to @event/add, like:
|
||||
@event/add north = time 8:00
|
||||
The parameter (8:00 here) must be a suite of digits separated by
|
||||
spaces, colons or dashes. Keep it as close from a recognizable
|
||||
date format, like this:
|
||||
@event/add south = time 06-15 12:20
|
||||
This event will fire every year on June the 15th at 12 PM (still
|
||||
game time). Units have to be specified depending on your set calendar
|
||||
(ask a developer for more details).
|
||||
|
||||
Variables you can use in this event:
|
||||
exit: the exit connected to this event.
|
||||
"""
|
||||
|
||||
EXIT_TRAVERSE = """
|
||||
After the characer has traversed through this exit.
|
||||
This event is called after a character has traversed through this
|
||||
exit. Traversing cannot be prevented using 'deny()' at this
|
||||
point. The character will be in a different room and she will
|
||||
have received the room's description when this event is called.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who has traversed through this exit.
|
||||
exit: the exit that was just traversed through.
|
||||
origin: the exit's location (where the character was before moving).
|
||||
destination: the character's location after moving.
|
||||
"""
|
||||
|
||||
@register_events
|
||||
class EventExit(DefaultExit):
|
||||
|
||||
"""Modified exit including management of events."""
|
||||
|
||||
_events = {
|
||||
"can_traverse": (["character", "exit", "room"], EXIT_CAN_TRAVERSE),
|
||||
"msg_arrive": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_ARRIVE),
|
||||
"msg_leave": (["character", "exit", "origin", "destination", "message", "mapping"], EXIT_MSG_LEAVE),
|
||||
"time": (["exit"], EXIT_TIME, None, time_event),
|
||||
"traverse": (["character", "exit", "origin", "destination"], EXIT_TRAVERSE),
|
||||
}
|
||||
|
||||
def at_traverse(self, traversing_object, target_location):
|
||||
"""
|
||||
This hook is responsible for handling the actual traversal,
|
||||
normally by calling
|
||||
`traversing_object.move_to(target_location)`. It is normally
|
||||
only implemented by Exit objects. If it returns False (usually
|
||||
because `move_to` returned False), `at_after_traverse` below
|
||||
should not be called and instead `at_failed_traverse` should be
|
||||
called.
|
||||
|
||||
Args:
|
||||
traversing_object (Object): Object traversing us.
|
||||
target_location (Object): Where target is going.
|
||||
|
||||
"""
|
||||
is_character = inherits_from(traversing_object, DefaultCharacter)
|
||||
if is_character:
|
||||
allow = self.callbacks.call("can_traverse", traversing_object,
|
||||
self, self.location)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
super(EventExit, self).at_traverse(traversing_object, target_location)
|
||||
|
||||
# After traversing
|
||||
if is_character:
|
||||
self.callbacks.call("traverse", traversing_object,
|
||||
self, self.location, self.destination)
|
||||
|
||||
|
||||
# Object help
|
||||
OBJECT_DROP = """
|
||||
When a character drops this object.
|
||||
This event is called when a character drops this object. It is
|
||||
called after the command has ended and displayed its message, and
|
||||
the action cannot be prevented at this time.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character having dropped the object.
|
||||
obj: the object connected to this event.
|
||||
"""
|
||||
|
||||
OBJECT_GET = """
|
||||
When a character gets this object.
|
||||
This event is called when a character gets this object. It is
|
||||
called after the command has ended and displayed its message, and
|
||||
the action cannot be prevented at this time.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character having picked up the object.
|
||||
obj: the object connected to this event.
|
||||
"""
|
||||
|
||||
OBJECT_TIME = """
|
||||
A repeated event to be called regularly.
|
||||
This event is scheduled to repeat at different times, specified
|
||||
as parameters. You can set it to run every day at 8:00 AM (game
|
||||
time). You have to specify the time as an argument to @event/add, like:
|
||||
@event/add here = time 8:00
|
||||
The parameter (8:00 here) must be a suite of digits separated by
|
||||
spaces, colons or dashes. Keep it as close from a recognizable
|
||||
date format, like this:
|
||||
@event/add here = time 06-15 12:20
|
||||
This event will fire every year on June the 15th at 12 PM (still
|
||||
game time). Units have to be specified depending on your set calendar
|
||||
(ask a developer for more details).
|
||||
|
||||
Variables you can use in this event:
|
||||
object: the object connected to this event.
|
||||
"""
|
||||
|
||||
@register_events
|
||||
class EventObject(DefaultObject):
|
||||
|
||||
"""Default object with management of events."""
|
||||
|
||||
_events = {
|
||||
"drop": (["character", "obj"], OBJECT_DROP),
|
||||
"get": (["character", "obj"], OBJECT_GET),
|
||||
"time": (["object"], OBJECT_TIME, None, time_event),
|
||||
}
|
||||
|
||||
@lazy_property
|
||||
def callbacks(self):
|
||||
"""Return the CallbackHandler."""
|
||||
return CallbackHandler(self)
|
||||
|
||||
def at_get(self, getter):
|
||||
"""
|
||||
Called by the default `get` command when this object has been
|
||||
picked up.
|
||||
|
||||
Args:
|
||||
getter (Object): The object getting this object.
|
||||
|
||||
Notes:
|
||||
This hook cannot stop the pickup from happening. Use
|
||||
permissions for that.
|
||||
|
||||
"""
|
||||
super(EventObject, self).at_get(getter)
|
||||
self.callbacks.call("get", getter, self)
|
||||
|
||||
def at_drop(self, dropper):
|
||||
"""
|
||||
Called by the default `drop` command when this object has been
|
||||
dropped.
|
||||
|
||||
Args:
|
||||
dropper (Object): The object which just dropped this object.
|
||||
|
||||
Notes:
|
||||
This hook cannot stop the drop from happening. Use
|
||||
permissions from that.
|
||||
|
||||
"""
|
||||
super(EventObject, self).at_drop(dropper)
|
||||
self.callbacks.call("drop", dropper, self)
|
||||
|
||||
# Room help
|
||||
ROOM_CAN_DELETE = """
|
||||
Can the room be deleted?
|
||||
This event is called before the room is deleted. You can use
|
||||
'deny()' in this event to prevent this room from being deleted.
|
||||
If this event doesn't prevent the room from being deleted, its
|
||||
'delete' event is called right away.
|
||||
|
||||
Variables you can use in this event:
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
ROOM_CAN_MOVE = """
|
||||
Can the character move into this room?
|
||||
This event is called before the character can move into this
|
||||
specific room. You can prevent the move by using the 'deny()'
|
||||
function.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who wants to move in this room.
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
ROOM_CAN_SAY = """
|
||||
Before a character can say something in this room.
|
||||
This event is called before a character says something in this
|
||||
room. The "something" in question can be modified, or the action
|
||||
can be prevented by using 'deny()'. To change the content of what
|
||||
the character says, simply change the variable 'message' to another
|
||||
string of characters.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who is using the say command.
|
||||
room: the room connected to this event.
|
||||
message: the text spoken by the character.
|
||||
"""
|
||||
|
||||
ROOM_DELETE = """
|
||||
Before deleting the room.
|
||||
This event is called just before deleting this room. It shouldn't
|
||||
be prevented (using the `deny()` function at this stage doesn't
|
||||
have any effect). If you want to prevent deletion of this room,
|
||||
use the event `can_delete` instead.
|
||||
|
||||
Variables you can use in this event:
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
ROOM_MOVE = """
|
||||
After the character has moved into this room.
|
||||
This event is called when the character has moved into this
|
||||
room. It is too late to prevent the move at this point.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character connected to this event.
|
||||
origin: the old location of the character.
|
||||
destination: the new location of the character.
|
||||
"""
|
||||
|
||||
ROOM_PUPPETED_IN = """
|
||||
After the character has been puppeted in this room.
|
||||
This event is called after a character has been puppeted in this
|
||||
room. This can happen when a player, having connected, begins
|
||||
to puppet a character. The character's location at this point,
|
||||
if it's a room, will see this event fire.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who have just been puppeted in this room.
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
ROOM_SAY = """
|
||||
After the character has said something in the room.
|
||||
This event is called right after a character has said something
|
||||
in this room. The action cannot be prevented at this moment.
|
||||
Instead, this event is ideal to create actions that will respond
|
||||
to something being said aloud. To use this event, you have to
|
||||
specify a list of keywords as parameters that should be present,
|
||||
as separate words, in the spoken phrase. For instance, you can
|
||||
set an event tthat would fire if the phrase spoken by the character
|
||||
contains "menu" or "dinner" or "lunch":
|
||||
@event/add ... = say menu, dinner, lunch
|
||||
Then if one of the words is present in what the character says,
|
||||
this event will fire.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character having spoken in this room.
|
||||
room: the room connected to this event.
|
||||
message: the text having been spoken by the character.
|
||||
"""
|
||||
|
||||
ROOM_TIME = """
|
||||
A repeated event to be called regularly.
|
||||
This event is scheduled to repeat at different times, specified
|
||||
as parameters. You can set it to run every day at 8:00 AM (game
|
||||
time). You have to specify the time as an argument to @event/add, like:
|
||||
@event/add here = time 8:00
|
||||
The parameter (8:00 here) must be a suite of digits separated by
|
||||
spaces, colons or dashes. Keep it as close from a recognizable
|
||||
date format, like this:
|
||||
@event/add here = time 06-15 12:20
|
||||
This event will fire every year on June the 15th at 12 PM (still
|
||||
game time). Units have to be specified depending on your set calendar
|
||||
(ask a developer for more details).
|
||||
|
||||
Variables you can use in this event:
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
ROOM_UNPUPPETED_IN = """
|
||||
Before the character is un-puppeted in this room.
|
||||
This event is called before a character is un-puppeted in this
|
||||
room. This can happen when a player, puppeting a character, is
|
||||
disconnecting. The character's location at this point, if it's a
|
||||
room, will see this event fire.
|
||||
|
||||
Variables you can use in this event:
|
||||
character: the character who is about to be un-puppeted in this room.
|
||||
room: the room connected to this event.
|
||||
"""
|
||||
|
||||
@register_events
|
||||
class EventRoom(DefaultRoom):
|
||||
|
||||
"""Default room with management of events."""
|
||||
|
||||
_events = {
|
||||
"can_delete": (["room"], ROOM_CAN_DELETE),
|
||||
"can_move": (["character", "room"], ROOM_CAN_MOVE),
|
||||
"can_say": (["character", "room", "message"], ROOM_CAN_SAY, phrase_event),
|
||||
"delete": (["room"], ROOM_DELETE),
|
||||
"move": (["character", "origin", "destination"], ROOM_MOVE),
|
||||
"puppeted_in": (["character", "room"], ROOM_PUPPETED_IN),
|
||||
"say": (["character", "room", "message"], ROOM_SAY, phrase_event),
|
||||
"time": (["room"], ROOM_TIME, None, time_event),
|
||||
"unpuppeted_in": (["character", "room"], ROOM_UNPUPPETED_IN),
|
||||
}
|
||||
|
||||
def at_object_delete(self):
|
||||
"""
|
||||
Called just before the database object is permanently
|
||||
delete()d from the database. If this method returns False,
|
||||
deletion is aborted.
|
||||
|
||||
"""
|
||||
if not self.callbacks.call("can_delete", self):
|
||||
return False
|
||||
|
||||
self.callbacks.call("delete", self)
|
||||
return True
|
||||
|
||||
def at_say(self, speaker, message):
|
||||
"""
|
||||
Called on this object if an object inside this object speaks.
|
||||
The string returned from this method is the final form of the
|
||||
speech.
|
||||
|
||||
Args:
|
||||
speaker (Object): The object speaking.
|
||||
message (str): The words spoken.
|
||||
|
||||
Returns:
|
||||
The message to be said (str) or None.
|
||||
|
||||
Notes:
|
||||
You should not need to add things like 'you say: ' or
|
||||
similar here, that should be handled by the say command before
|
||||
this.
|
||||
|
||||
"""
|
||||
allow = self.callbacks.call("can_say", speaker, self, message,
|
||||
parameters=message)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
message = self.callbacks.get_variable("message")
|
||||
|
||||
# Call the event "can_say" of other characters in the location
|
||||
for present in [o for o in self.contents if isinstance(
|
||||
o, DefaultCharacter) and o is not speaker]:
|
||||
allow = present.callbacks.call("can_say", speaker, present,
|
||||
message, parameters=message)
|
||||
if not allow:
|
||||
return
|
||||
|
||||
message = present.callbacks.get_variable("message")
|
||||
|
||||
# We force the next event to be called after the message
|
||||
# This will have to change when the Evennia API adds new hooks
|
||||
delay(0, self.callbacks.call, "say", speaker, self, message,
|
||||
parameters=message)
|
||||
for present in [o for o in self.contents if isinstance(
|
||||
o, DefaultCharacter) and o is not speaker]:
|
||||
delay(0, present.callbacks.call, "say", speaker, present, message,
|
||||
parameters=message)
|
||||
|
||||
return message
|
||||
249
evennia/contrib/events/utils.py
Normal file
249
evennia/contrib/events/utils.py
Normal file
|
|
@ -0,0 +1,249 @@
|
|||
"""
|
||||
Functions to extend the event system.
|
||||
|
||||
These functions are to be used by developers to customize events and callbacks.
|
||||
|
||||
"""
|
||||
|
||||
from textwrap import dedent
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import logger
|
||||
from evennia import ScriptDB
|
||||
from evennia.utils.create import create_script
|
||||
from evennia.utils.gametime import real_seconds_until as standard_rsu
|
||||
from evennia.utils.utils import class_from_module
|
||||
from evennia.contrib.custom_gametime import UNITS
|
||||
from evennia.contrib.custom_gametime import gametime_to_realtime
|
||||
from evennia.contrib.custom_gametime import real_seconds_until as custom_rsu
|
||||
|
||||
# Temporary storage for events waiting for the script to be started
|
||||
EVENTS = []
|
||||
|
||||
def get_event_handler():
|
||||
"""Return the event handler or None."""
|
||||
try:
|
||||
script = ScriptDB.objects.get(db_key="event_handler")
|
||||
except ScriptDB.DoesNotExist:
|
||||
logger.log_trace("Can't get the event handler.")
|
||||
script = None
|
||||
|
||||
return script
|
||||
|
||||
def register_events(path_or_typeclass):
|
||||
"""
|
||||
Register the events in this typeclass.
|
||||
|
||||
Args:
|
||||
path_or_typeclass (str or type): the Python path leading to the
|
||||
class containing events, or the class itself.
|
||||
|
||||
Returns:
|
||||
The typeclass itself.
|
||||
|
||||
Notes:
|
||||
This function will read events from the `_events` class variable
|
||||
defined in the typeclass given in parameters. It will add
|
||||
the events, either to the script if it exists, or to some
|
||||
temporary storage, waiting for the script to be initialized.
|
||||
|
||||
"""
|
||||
if isinstance(path_or_typeclass, basestring):
|
||||
typeclass = class_from_module(path_or_typeclass)
|
||||
else:
|
||||
typeclass = path_or_typeclass
|
||||
|
||||
typeclass_name = typeclass.__module__ + "." + typeclass.__name__
|
||||
try:
|
||||
storage = ScriptDB.objects.get(db_key="event_handler")
|
||||
assert storage.is_active
|
||||
assert storage.ndb.events is not None
|
||||
except (ScriptDB.DoesNotExist, AssertionError):
|
||||
storage = EVENTS
|
||||
|
||||
# If the script is started, add the event directly.
|
||||
# Otherwise, add it to the temporary storage.
|
||||
for name, tup in getattr(typeclass, "_events", {}).items():
|
||||
if len(tup) == 4:
|
||||
variables, help_text, custom_call, custom_add = tup
|
||||
elif len(tup) == 3:
|
||||
variables, help_text, custom_call = tup
|
||||
custom_add = None
|
||||
elif len(tup) == 2:
|
||||
variables, help_text = tup
|
||||
custom_call = None
|
||||
custom_add = None
|
||||
else:
|
||||
variables = help_text = custom_call = custom_add = None
|
||||
|
||||
if isinstance(storage, list):
|
||||
storage.append((typeclass_name, name, variables, help_text, custom_call, custom_add))
|
||||
else:
|
||||
storage.add_event(typeclass_name, name, variables, help_text, custom_call, custom_add)
|
||||
|
||||
return typeclass
|
||||
|
||||
# Custom callbacks for specific event types
|
||||
def get_next_wait(format):
|
||||
"""
|
||||
Get the length of time in seconds before format.
|
||||
|
||||
Args:
|
||||
format (str): a time format matching the set calendar.
|
||||
|
||||
Returns:
|
||||
until (int or float): the number of seconds until the event.
|
||||
usual (int or float): the usual number of seconds between events.
|
||||
format (str): a string format representing the time.
|
||||
|
||||
Notes:
|
||||
The time format could be something like "2018-01-08 12:00". The
|
||||
number of units set in the calendar affects the way seconds are
|
||||
calculated.
|
||||
|
||||
"""
|
||||
calendar = getattr(settings, "EVENTS_CALENDAR", None)
|
||||
if calendar is None:
|
||||
logger.log_err("A time-related event has been set whereas " \
|
||||
"the gametime calendar has not been set in the settings.")
|
||||
return
|
||||
elif calendar == "standard":
|
||||
rsu = standard_rsu
|
||||
units = ["min", "hour", "day", "month", "year"]
|
||||
elif calendar == "custom":
|
||||
rsu = custom_rsu
|
||||
back = dict([(value, name) for name, value in UNITS.items()])
|
||||
sorted_units = sorted(back.items())
|
||||
del sorted_units[0]
|
||||
units = [n for v, n in sorted_units]
|
||||
|
||||
params = {}
|
||||
for delimiter in ("-", ":"):
|
||||
format = format.replace(delimiter, " ")
|
||||
|
||||
pieces = list(reversed(format.split()))
|
||||
details = []
|
||||
i = 0
|
||||
for uname in units:
|
||||
try:
|
||||
piece = pieces[i]
|
||||
except IndexError:
|
||||
break
|
||||
|
||||
if not piece.isdigit():
|
||||
logger.log_trace("The time specified '{}' in {} isn't " \
|
||||
"a valid number".format(piece, format))
|
||||
return
|
||||
|
||||
# Convert the piece to int
|
||||
piece = int(piece)
|
||||
params[uname] = piece
|
||||
details.append("{}={}".format(uname, piece))
|
||||
if i < len(units):
|
||||
next_unit = units[i + 1]
|
||||
else:
|
||||
next_unit = None
|
||||
i += 1
|
||||
|
||||
params["sec"] = 0
|
||||
details = " ".join(details)
|
||||
until = rsu(**params)
|
||||
usual = -1
|
||||
if next_unit:
|
||||
kwargs = {next_unit: 1}
|
||||
usual = gametime_to_realtime(**kwargs)
|
||||
return until, usual, details
|
||||
|
||||
def time_event(obj, event_name, number, parameters):
|
||||
"""
|
||||
Create a time-related event.
|
||||
|
||||
Args:
|
||||
obj (Object): the object on which sits the event.
|
||||
event_name (str): the event's name.
|
||||
number (int): the number of the event.
|
||||
parameters (str): the parameter of the event.
|
||||
|
||||
"""
|
||||
seconds, usual, key = get_next_wait(parameters)
|
||||
script = create_script("evennia.contrib.events.scripts.TimeEventScript", interval=seconds, obj=obj)
|
||||
script.key = key
|
||||
script.desc = "event on {}".format(key)
|
||||
script.db.time_format = parameters
|
||||
script.db.number = number
|
||||
script.ndb.usual = usual
|
||||
|
||||
def keyword_event(callbacks, parameters):
|
||||
"""
|
||||
Custom call for events with keywords (like push, or pull, or turn...).
|
||||
|
||||
Args:
|
||||
callbacks (list of dict): the list of callbacks to be called.
|
||||
parameters (str): the actual parameters entered to trigger the callback.
|
||||
|
||||
Returns:
|
||||
A list containing the callback dictionaries to be called.
|
||||
|
||||
Notes:
|
||||
This function should be imported and added as a custom_call
|
||||
parameter to add the event when the event supports keywords
|
||||
as parameters. Keywords in parameters are one or more words
|
||||
separated by a comma. For instance, a 'push 1, one' callback can
|
||||
be set to trigger when the player 'push 1' or 'push one'.
|
||||
|
||||
"""
|
||||
key = parameters.strip().lower()
|
||||
to_call = []
|
||||
for callback in callbacks:
|
||||
keys = callback["parameters"]
|
||||
if not keys or key in [p.strip().lower() for p in keys.split(",")]:
|
||||
to_call.append(callback)
|
||||
|
||||
return to_call
|
||||
|
||||
def phrase_event(callbacks, parameters):
|
||||
"""
|
||||
Custom call for events with keywords in sentences (like say or whisper).
|
||||
|
||||
Args:
|
||||
callbacks (list of dict): the list of callbacks to be called.
|
||||
parameters (str): the actual parameters entered to trigger the callback.
|
||||
|
||||
Returns:
|
||||
A list containing the callback dictionaries to be called.
|
||||
|
||||
Notes:
|
||||
This function should be imported and added as a custom_call
|
||||
parameter to add the event when the event supports keywords
|
||||
in phrases as parameters. Keywords in parameters are one or more
|
||||
words separated by a comma. For instance, a 'say yes, okay' callback
|
||||
can be set to trigger when the player says something containing
|
||||
either "yes" or "okay" (maybe 'say I don't like it, but okay').
|
||||
|
||||
"""
|
||||
phrase = parameters.strip().lower()
|
||||
# Remove punctuation marks
|
||||
punctuations = ',.";?!'
|
||||
for p in punctuations:
|
||||
phrase = phrase.replace(p, " ")
|
||||
words = phrase.split()
|
||||
words = [w.strip("' ") for w in words if w.strip("' ")]
|
||||
to_call = []
|
||||
for callback in callbacks:
|
||||
keys = callback["parameters"]
|
||||
if not keys or any(key.strip().lower() in words for key in keys.split(",")):
|
||||
to_call.append(callback)
|
||||
|
||||
return to_call
|
||||
|
||||
class InterruptEvent(RuntimeError):
|
||||
|
||||
"""
|
||||
Interrupt the current event.
|
||||
|
||||
You shouldn't have to use this exception directly, probably use the
|
||||
`deny()` function that handles it instead.
|
||||
|
||||
"""
|
||||
|
||||
pass
|
||||
|
|
@ -46,12 +46,13 @@ from evennia import syscmdkeys
|
|||
from evennia.utils.evmenu import EvMenu
|
||||
from evennia.utils.utils import random_string_from_module
|
||||
|
||||
## Constants
|
||||
# Constants
|
||||
RE_VALID_USERNAME = re.compile(r"^[a-z]{3,}$", re.I)
|
||||
LEN_PASSWD = 6
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
|
||||
## Menu notes (top-level functions)
|
||||
# Menu notes (top-level functions)
|
||||
|
||||
|
||||
def start(caller):
|
||||
"""The user should enter his/her username or NEW to create one.
|
||||
|
|
@ -66,23 +67,17 @@ def start(caller):
|
|||
text = random_string_from_module(CONNECTION_SCREEN_MODULE)
|
||||
text += "\n\nEnter your username or |yNEW|n to create a new account."
|
||||
options = (
|
||||
{ "key": "",
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "new",
|
||||
"goto": "create_account",
|
||||
},
|
||||
{ "key": "quit",
|
||||
"goto": "quit"
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "username",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"goto": "start"},
|
||||
{"key": "new",
|
||||
"goto": "create_account"},
|
||||
{"key": "quit",
|
||||
"goto": "quit"},
|
||||
{"key": "_default",
|
||||
"goto": "username"})
|
||||
return text, options
|
||||
|
||||
|
||||
def username(caller, string_input):
|
||||
"""Check that the username leads to an existing player.
|
||||
|
||||
|
|
@ -100,34 +95,25 @@ def username(caller, string_input):
|
|||
Try another name or leave empty to go back.
|
||||
""".strip("\n")).format(string_input)
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "username",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "username"})
|
||||
else:
|
||||
caller.ndb._menutree.player = player
|
||||
text = "Enter the password for the {} account.".format(player.name)
|
||||
# Disables echo for the password
|
||||
caller.msg("", options={"echo": False})
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "ask_password",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "ask_password"})
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def ask_password(caller, string_input):
|
||||
"""Ask the user to enter the password to this player.
|
||||
|
||||
|
|
@ -145,10 +131,10 @@ def ask_password(caller, string_input):
|
|||
|
||||
player = menutree.player
|
||||
password_attempts = menutree.password_attempts \
|
||||
if hasattr(menutree, "password_attempts") else 0
|
||||
if hasattr(menutree, "password_attempts") else 0
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
banned = bans and (any(tup[0] == player.name.lower() for tup in bans) \
|
||||
or any(tup[2].match(caller.address) for tup in bans if tup[2]))
|
||||
banned = bans and (any(tup[0] == player.name.lower() for tup in bans) or
|
||||
any(tup[2].match(caller.address) for tup in bans if tup[2]))
|
||||
|
||||
if not player.check_password(string_input):
|
||||
# Didn't enter a correct password
|
||||
|
|
@ -156,7 +142,7 @@ def ask_password(caller, string_input):
|
|||
if password_attempts > 2:
|
||||
# Too many tries
|
||||
caller.sessionhandler.disconnect(
|
||||
caller, "|rToo many failed attempts. Disconnecting.|n")
|
||||
caller, "|rToo many failed attempts. Disconnecting.|n")
|
||||
text = ""
|
||||
options = {}
|
||||
else:
|
||||
|
|
@ -167,16 +153,11 @@ def ask_password(caller, string_input):
|
|||
""".strip("\n"))
|
||||
# Loops on the same node
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "ask_password",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "ask_password"})
|
||||
elif banned:
|
||||
# This is a banned IP or name!
|
||||
string = dedent("""
|
||||
|
|
@ -196,6 +177,7 @@ def ask_password(caller, string_input):
|
|||
|
||||
return text, options
|
||||
|
||||
|
||||
def create_account(caller):
|
||||
"""Create a new account.
|
||||
|
||||
|
|
@ -205,13 +187,11 @@ def create_account(caller):
|
|||
"""
|
||||
text = "Enter your new account name."
|
||||
options = (
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "create_username",
|
||||
},
|
||||
)
|
||||
{"key": "_default",
|
||||
"goto": "create_username"},)
|
||||
return text, options
|
||||
|
||||
|
||||
def create_username(caller, string_input):
|
||||
"""Prompt to enter a valid username (one that doesnt exist).
|
||||
|
||||
|
|
@ -231,15 +211,10 @@ def create_username(caller, string_input):
|
|||
""".strip("\n")).format(string_input)
|
||||
# Loops on the same node
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "create_username",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "create_username"})
|
||||
elif not RE_VALID_USERNAME.search(string_input):
|
||||
text = dedent("""
|
||||
|rThis username isn't valid.|n
|
||||
|
|
@ -248,15 +223,10 @@ def create_username(caller, string_input):
|
|||
Enter another username or leave blank to go back.
|
||||
""".strip("\n"))
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "create_username",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "create_username"})
|
||||
else:
|
||||
# a valid username - continue getting the password
|
||||
menutree.playername = string_input
|
||||
|
|
@ -265,14 +235,12 @@ def create_username(caller, string_input):
|
|||
# Redirects to the creation of a password
|
||||
text = "Enter this account's new password."
|
||||
options = (
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "create_password",
|
||||
},
|
||||
)
|
||||
{"key": "_default",
|
||||
"goto": "create_password"},)
|
||||
|
||||
return text, options
|
||||
|
||||
|
||||
def create_password(caller, string_input):
|
||||
"""Ask the user to create a password.
|
||||
|
||||
|
|
@ -284,16 +252,11 @@ def create_password(caller, string_input):
|
|||
menutree = caller.ndb._menutree
|
||||
text = ""
|
||||
options = (
|
||||
{
|
||||
"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start",
|
||||
},
|
||||
{
|
||||
"key": "_default",
|
||||
"goto": "create_password",
|
||||
},
|
||||
)
|
||||
{"key": "",
|
||||
"exec": lambda caller: caller.msg("", options={"echo": True}),
|
||||
"goto": "start"},
|
||||
{"key": "_default",
|
||||
"goto": "create_password"})
|
||||
|
||||
password = string_input.strip()
|
||||
playername = menutree.playername
|
||||
|
|
@ -313,13 +276,13 @@ def create_password(caller, string_input):
|
|||
permissions = settings.PERMISSION_PLAYER_DEFAULT
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
new_player = unloggedin._create_player(caller, playername,
|
||||
password, permissions)
|
||||
password, permissions)
|
||||
if new_player:
|
||||
if settings.MULTISESSION_MODE < 2:
|
||||
default_home = ObjectDB.objects.get_id(
|
||||
settings.DEFAULT_HOME)
|
||||
settings.DEFAULT_HOME)
|
||||
unloggedin._create_character(caller, new_player,
|
||||
typeclass, default_home, permissions)
|
||||
typeclass, default_home, permissions)
|
||||
except Exception:
|
||||
# We are in the middle between logged in and -not, so we have
|
||||
# to handle tracebacks ourselves at this point. If we don't, we
|
||||
|
|
@ -338,11 +301,13 @@ def create_password(caller, string_input):
|
|||
|
||||
return text, options
|
||||
|
||||
|
||||
def quit(caller):
|
||||
caller.sessionhandler.disconnect(caller, "Goodbye! Logging off.")
|
||||
return "", {}
|
||||
|
||||
## Other functions
|
||||
# Other functions
|
||||
|
||||
|
||||
def _formatter(nodetext, optionstext, caller=None):
|
||||
"""Do not display the options, only the text.
|
||||
|
|
@ -354,7 +319,8 @@ def _formatter(nodetext, optionstext, caller=None):
|
|||
"""
|
||||
return nodetext
|
||||
|
||||
## Commands and CmdSets
|
||||
|
||||
# Commands and CmdSets
|
||||
|
||||
class UnloggedinCmdSet(CmdSet):
|
||||
"Cmdset for the unloggedin state"
|
||||
|
|
|
|||
|
|
@ -101,20 +101,20 @@ class CmdOpen(default_cmds.CmdOpen):
|
|||
"""
|
||||
Simple wrapper for the default CmdOpen.create_exit
|
||||
"""
|
||||
# create a new exit as normal
|
||||
new_exit = super(CmdOpen, self).create_exit(exit_name, location, destination,
|
||||
exit_aliases=exit_aliases, typeclass=typeclass)
|
||||
if hasattr(self, "return_exit_already_created"):
|
||||
# we don't create a return exit if it was already created (because
|
||||
# we created a door)
|
||||
del self.return_exit_already_created
|
||||
return None
|
||||
# create a new exit as normal
|
||||
new_exit = super(CmdOpen, self).create_exit(exit_name, location, destination,
|
||||
exit_aliases=exit_aliases, typeclass=typeclass)
|
||||
return new_exit
|
||||
if inherits_from(new_exit, SimpleDoor):
|
||||
# a door - create its counterpart and make sure to turn off the default
|
||||
# return-exit creation of CmdOpen
|
||||
self.caller.msg("Note: A door-type exit was created - ignored eventual custom return-exit type.")
|
||||
self.return_exit_already_created = True
|
||||
back_exit = super(CmdOpen, self).create_exit(exit_name, destination, location,
|
||||
back_exit = self.create_exit(exit_name, destination, location,
|
||||
exit_aliases=exit_aliases, typeclass=typeclass)
|
||||
new_exit.db.return_exit = back_exit
|
||||
back_exit.db.return_exit = new_exit
|
||||
|
|
|
|||
|
|
@ -451,7 +451,97 @@ class TestChargen(CommandTest):
|
|||
self.assertTrue(self.player.db._character_dbrefs)
|
||||
self.call(chargen.CmdOOCLook(), "", "You, TestPlayer, are an OOC ghost without form.",caller=self.player)
|
||||
self.call(chargen.CmdOOCLook(), "testchar", "testchar(", caller=self.player)
|
||||
|
||||
# Testing clothing contrib
|
||||
from evennia.contrib import clothing
|
||||
from evennia.objects.objects import DefaultRoom
|
||||
|
||||
class TestClothingCmd(CommandTest):
|
||||
|
||||
def test_clothingcommands(self):
|
||||
wearer = create_object(clothing.ClothedCharacter, key="Wearer")
|
||||
friend = create_object(clothing.ClothedCharacter, key="Friend")
|
||||
room = create_object(DefaultRoom, key="room")
|
||||
wearer.location = room
|
||||
friend.location = room
|
||||
# Make a test hat
|
||||
test_hat = create_object(clothing.Clothing, key="test hat")
|
||||
test_hat.db.clothing_type = 'hat'
|
||||
test_hat.location = wearer
|
||||
# Make a test scarf
|
||||
test_scarf = create_object(clothing.Clothing, key="test scarf")
|
||||
test_scarf.db.clothing_type = 'accessory'
|
||||
test_scarf.location = wearer
|
||||
# Test wear command
|
||||
self.call(clothing.CmdWear(), "", "Usage: wear <obj> [wear style]", caller=wearer)
|
||||
self.call(clothing.CmdWear(), "hat", "Wearer puts on test hat.", caller=wearer)
|
||||
self.call(clothing.CmdWear(), "scarf stylishly", "Wearer wears test scarf stylishly.", caller=wearer)
|
||||
# Test cover command.
|
||||
self.call(clothing.CmdCover(), "", "Usage: cover <worn clothing> [with] <clothing object>", caller=wearer)
|
||||
self.call(clothing.CmdCover(), "hat with scarf", "Wearer covers test hat with test scarf.", caller=wearer)
|
||||
# Test remove command.
|
||||
self.call(clothing.CmdRemove(), "", "Could not find ''.", caller=wearer)
|
||||
self.call(clothing.CmdRemove(), "hat", "You have to take off test scarf first.", caller=wearer)
|
||||
self.call(clothing.CmdRemove(), "scarf", "Wearer removes test scarf, revealing test hat.", caller=wearer)
|
||||
# Test uncover command.
|
||||
test_scarf.wear(wearer, True)
|
||||
test_hat.db.covered_by = test_scarf
|
||||
self.call(clothing.CmdUncover(), "", "Usage: uncover <worn clothing object>", caller=wearer)
|
||||
self.call(clothing.CmdUncover(), "hat", "Wearer uncovers test hat.", caller=wearer)
|
||||
# Test drop command.
|
||||
test_hat.db.covered_by = test_scarf
|
||||
self.call(clothing.CmdDrop(), "", "Drop what?", caller=wearer)
|
||||
self.call(clothing.CmdDrop(), "hat", "You can't drop that because it's covered by test scarf.", caller=wearer)
|
||||
self.call(clothing.CmdDrop(), "scarf", "You drop test scarf.", caller=wearer)
|
||||
# Test give command.
|
||||
self.call(clothing.CmdGive(), "", "Usage: give <inventory object> = <target>", caller=wearer)
|
||||
self.call(clothing.CmdGive(), "hat = Friend", "Wearer removes test hat.|You give test hat to Friend.", caller=wearer)
|
||||
# Test inventory command.
|
||||
self.call(clothing.CmdInventory(), "", "You are not carrying or wearing anything.", caller=wearer)
|
||||
|
||||
class TestClothingFunc(EvenniaTest):
|
||||
|
||||
def test_clothingfunctions(self):
|
||||
wearer = create_object(clothing.ClothedCharacter, key="Wearer")
|
||||
room = create_object(DefaultRoom, key="room")
|
||||
wearer.location = room
|
||||
# Make a test hat
|
||||
test_hat = create_object(clothing.Clothing, key="test hat")
|
||||
test_hat.db.clothing_type = 'hat'
|
||||
test_hat.location = wearer
|
||||
# Make a test shirt
|
||||
test_shirt = create_object(clothing.Clothing, key="test shirt")
|
||||
test_shirt.db.clothing_type = 'top'
|
||||
test_shirt.location = wearer
|
||||
# Make a test pants
|
||||
test_pants = create_object(clothing.Clothing, key="test pants")
|
||||
test_pants.db.clothing_type = 'bottom'
|
||||
test_pants.location = wearer
|
||||
|
||||
test_hat.wear(wearer, 'on the head')
|
||||
self.assertEqual(test_hat.db.worn, 'on the head')
|
||||
|
||||
test_hat.remove(wearer)
|
||||
self.assertEqual(test_hat.db.worn, False)
|
||||
|
||||
test_hat.worn = True
|
||||
test_hat.at_get(wearer)
|
||||
self.assertEqual(test_hat.db.worn, False)
|
||||
|
||||
clothes_list = [test_shirt, test_hat, test_pants]
|
||||
self.assertEqual(clothing.order_clothes_list(clothes_list), [test_hat, test_shirt, test_pants])
|
||||
|
||||
test_hat.wear(wearer, True)
|
||||
test_pants.wear(wearer, True)
|
||||
self.assertEqual(clothing.get_worn_clothes(wearer), [test_hat, test_pants])
|
||||
|
||||
self.assertEqual(clothing.clothing_type_count(clothes_list), {'hat':1, 'top':1, 'bottom':1})
|
||||
|
||||
self.assertEqual(clothing.single_type_count(clothes_list, 'hat'), 1)
|
||||
|
||||
|
||||
|
||||
|
||||
# Testing custom_gametime
|
||||
from evennia.contrib import custom_gametime
|
||||
|
||||
|
|
@ -754,4 +844,95 @@ class TestTutorialWorldRooms(CommandTest):
|
|||
def test_outroroom(self):
|
||||
create_object(tutrooms.OutroRoom, key="outroroom")
|
||||
|
||||
# test turnbattle
|
||||
from evennia.contrib import turnbattle
|
||||
from evennia.objects.objects import DefaultRoom
|
||||
|
||||
class TestTurnBattleCmd(CommandTest):
|
||||
|
||||
# Test combat commands
|
||||
def test_turnbattlecmd(self):
|
||||
self.call(turnbattle.CmdFight(), "", "You can't start a fight if you've been defeated!")
|
||||
self.call(turnbattle.CmdAttack(), "", "You can only do that in combat. (see: help fight)")
|
||||
self.call(turnbattle.CmdPass(), "", "You can only do that in combat. (see: help fight)")
|
||||
self.call(turnbattle.CmdDisengage(), "", "You can only do that in combat. (see: help fight)")
|
||||
self.call(turnbattle.CmdRest(), "", "Char rests to recover HP.")
|
||||
|
||||
class TestTurnBattleFunc(EvenniaTest):
|
||||
|
||||
# Test combat functions
|
||||
def test_turnbattlefunc(self):
|
||||
attacker = create_object(turnbattle.BattleCharacter, key="Attacker")
|
||||
defender = create_object(turnbattle.BattleCharacter, key="Defender")
|
||||
testroom = create_object(DefaultRoom, key="Test Room")
|
||||
attacker.location = testroom
|
||||
defender.loaction = testroom
|
||||
# Initiative roll
|
||||
initiative = turnbattle.roll_init(attacker)
|
||||
self.assertTrue(initiative >= 0 and initiative <= 1000)
|
||||
# Attack roll
|
||||
attack_roll = turnbattle.get_attack(attacker, defender)
|
||||
self.assertTrue(attack_roll >= 0 and attack_roll <= 100)
|
||||
# Defense roll
|
||||
defense_roll = turnbattle.get_defense(attacker, defender)
|
||||
self.assertTrue(defense_roll == 50)
|
||||
# Damage roll
|
||||
damage_roll = turnbattle.get_damage(attacker, defender)
|
||||
self.assertTrue(damage_roll >= 15 and damage_roll <= 25)
|
||||
# Apply damage
|
||||
defender.db.hp = 10
|
||||
turnbattle.apply_damage(defender, 3)
|
||||
self.assertTrue(defender.db.hp == 7)
|
||||
# Resolve attack
|
||||
defender.db.hp = 40
|
||||
turnbattle.resolve_attack(attacker, defender, attack_value=20, defense_value=10)
|
||||
self.assertTrue(defender.db.hp < 40)
|
||||
# Combat cleanup
|
||||
attacker.db.Combat_attribute = True
|
||||
turnbattle.combat_cleanup(attacker)
|
||||
self.assertFalse(attacker.db.combat_attribute)
|
||||
# Is in combat
|
||||
self.assertFalse(turnbattle.is_in_combat(attacker))
|
||||
# Set up turn handler script for further tests
|
||||
attacker.location.scripts.add(turnbattle.TurnHandler)
|
||||
turnhandler = attacker.db.combat_TurnHandler
|
||||
self.assertTrue(attacker.db.combat_TurnHandler)
|
||||
# Force turn order
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
# Test is turn
|
||||
self.assertTrue(turnbattle.is_turn(attacker))
|
||||
# Spend actions
|
||||
attacker.db.Combat_ActionsLeft = 1
|
||||
turnbattle.spend_action(attacker, 1, action_name="Test")
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "Test")
|
||||
# Initialize for combat
|
||||
attacker.db.Combat_ActionsLeft = 983
|
||||
turnhandler.initialize_for_combat(attacker)
|
||||
self.assertTrue(attacker.db.Combat_ActionsLeft == 0)
|
||||
self.assertTrue(attacker.db.Combat_LastAction == "null")
|
||||
# Start turn
|
||||
defender.db.Combat_ActionsLeft = 0
|
||||
turnhandler.start_turn(defender)
|
||||
self.assertTrue(defender.db.Combat_ActionsLeft == 1)
|
||||
# Next turn
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.next_turn()
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
# Turn end check
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
attacker.db.Combat_ActionsLeft = 0
|
||||
turnhandler.turn_end_check(attacker)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
# Join fight
|
||||
joiner = create_object(turnbattle.BattleCharacter, key="Joiner")
|
||||
turnhandler.db.fighters = [attacker, defender]
|
||||
turnhandler.db.turn = 0
|
||||
turnhandler.join_fight(joiner)
|
||||
self.assertTrue(turnhandler.db.turn == 1)
|
||||
self.assertTrue(turnhandler.db.fighters == [joiner, attacker, defender])
|
||||
# Remove the script at the end
|
||||
turnhandler.stop()
|
||||
|
|
|
|||
735
evennia/contrib/turnbattle.py
Normal file
735
evennia/contrib/turnbattle.py
Normal file
|
|
@ -0,0 +1,735 @@
|
|||
"""
|
||||
Simple turn-based combat system
|
||||
|
||||
Contrib - Tim Ashley Jenkins 2017
|
||||
|
||||
This is a framework for a simple turn-based combat system, similar
|
||||
to those used in D&D-style tabletop role playing games. It allows
|
||||
any character to start a fight in a room, at which point initiative
|
||||
is rolled and a turn order is established. Each participant in combat
|
||||
has a limited time to decide their action for that turn (30 seconds by
|
||||
default), and combat progresses through the turn order, looping through
|
||||
the participants until the fight ends.
|
||||
|
||||
Only simple rolls for attacking are implemented here, but this system
|
||||
is easily extensible and can be used as the foundation for implementing
|
||||
the rules from your turn-based tabletop game of choice or making your
|
||||
own battle system.
|
||||
|
||||
To install and test, import this module's BattleCharacter object into
|
||||
your game's character.py module:
|
||||
|
||||
from evennia.contrib.turnbattle import BattleCharacter
|
||||
|
||||
And change your game's character typeclass to inherit from BattleCharacter
|
||||
instead of the default:
|
||||
|
||||
class Character(BattleCharacter):
|
||||
|
||||
Next, import this module into your default_cmdsets.py module:
|
||||
|
||||
from evennia.contrib import turnbattle
|
||||
|
||||
And add the battle command set to your default command set:
|
||||
|
||||
#
|
||||
# any commands you add below will overload the default ones.
|
||||
#
|
||||
self.add(turnbattle.BattleCmdSet())
|
||||
|
||||
This module is meant to be heavily expanded on, so you may want to copy it
|
||||
to your game's 'world' folder and modify it there rather than importing it
|
||||
in your game and using it as-is.
|
||||
"""
|
||||
|
||||
from random import randint
|
||||
from evennia import DefaultCharacter, Command, default_cmds, DefaultScript
|
||||
from evennia.commands.default.help import CmdHelp
|
||||
|
||||
"""
|
||||
----------------------------------------------------------------------------
|
||||
COMBAT FUNCTIONS START HERE
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
def roll_init(character):
|
||||
"""
|
||||
Rolls a number between 1-1000 to determine initiative.
|
||||
|
||||
Args:
|
||||
character (obj): The character to determine initiative for
|
||||
|
||||
Returns:
|
||||
initiative (int): The character's place in initiative - higher
|
||||
numbers go first.
|
||||
|
||||
Notes:
|
||||
By default, does not reference the character and simply returns
|
||||
a random integer from 1 to 1000.
|
||||
|
||||
Since the character is passed to this function, you can easily reference
|
||||
a character's stats to determine an initiative roll - for example, if your
|
||||
character has a 'dexterity' attribute, you can use it to give that character
|
||||
an advantage in turn order, like so:
|
||||
|
||||
return (randint(1,20)) + character.db.dexterity
|
||||
|
||||
This way, characters with a higher dexterity will go first more often.
|
||||
"""
|
||||
return randint(1, 1000)
|
||||
|
||||
|
||||
def get_attack(attacker, defender):
|
||||
"""
|
||||
Returns a value for an attack roll.
|
||||
|
||||
Args:
|
||||
attacker (obj): Character doing the attacking
|
||||
defender (obj): Character being attacked
|
||||
|
||||
Returns:
|
||||
attack_value (int): Attack roll value, compared against a defense value
|
||||
to determine whether an attack hits or misses.
|
||||
|
||||
Notes:
|
||||
By default, returns a random integer from 1 to 100 without using any
|
||||
properties from either the attacker or defender.
|
||||
|
||||
This can easily be expanded to return a value based on characters stats,
|
||||
equipment, and abilities. This is why the attacker and defender are passed
|
||||
to this function, even though nothing from either one are used in this example.
|
||||
"""
|
||||
# For this example, just return a random integer up to 100.
|
||||
attack_value = randint(1, 100)
|
||||
return attack_value
|
||||
|
||||
|
||||
def get_defense(attacker, defender):
|
||||
"""
|
||||
Returns a value for defense, which an attack roll must equal or exceed in order
|
||||
for an attack to hit.
|
||||
|
||||
Args:
|
||||
attacker (obj): Character doing the attacking
|
||||
defender (obj): Character being attacked
|
||||
|
||||
Returns:
|
||||
defense_value (int): Defense value, compared against an attack roll
|
||||
to determine whether an attack hits or misses.
|
||||
|
||||
Notes:
|
||||
By default, returns 50, not taking any properties of the defender or
|
||||
attacker into account.
|
||||
|
||||
As above, this can be expanded upon based on character stats and equipment.
|
||||
"""
|
||||
# For this example, just return 50, for about a 50/50 chance of hit.
|
||||
defense_value = 50
|
||||
return defense_value
|
||||
|
||||
|
||||
def get_damage(attacker, defender):
|
||||
"""
|
||||
Returns a value for damage to be deducted from the defender's HP after abilities
|
||||
successful hit.
|
||||
|
||||
Args:
|
||||
attacker (obj): Character doing the attacking
|
||||
defender (obj): Character being damaged
|
||||
|
||||
Returns:
|
||||
damage_value (int): Damage value, which is to be deducted from the defending
|
||||
character's HP.
|
||||
|
||||
Notes:
|
||||
By default, returns a random integer from 15 to 25 without using any
|
||||
properties from either the attacker or defender.
|
||||
|
||||
Again, this can be expanded upon.
|
||||
"""
|
||||
# For this example, just generate a number between 15 and 25.
|
||||
damage_value = randint(15, 25)
|
||||
return damage_value
|
||||
|
||||
|
||||
def apply_damage(defender, damage):
|
||||
"""
|
||||
Applies damage to a target, reducing their HP by the damage amount to a
|
||||
minimum of 0.
|
||||
|
||||
Args:
|
||||
defender (obj): Character taking damage
|
||||
damage (int): Amount of damage being taken
|
||||
"""
|
||||
defender.db.hp -= damage # Reduce defender's HP by the damage dealt.
|
||||
# If this reduces it to 0 or less, set HP to 0.
|
||||
if defender.db.hp <= 0:
|
||||
defender.db.hp = 0
|
||||
|
||||
|
||||
def resolve_attack(attacker, defender, attack_value=None, defense_value=None):
|
||||
"""
|
||||
Resolves an attack and outputs the result.
|
||||
|
||||
Args:
|
||||
attacker (obj): Character doing the attacking
|
||||
defender (obj): Character being attacked
|
||||
|
||||
Notes:
|
||||
Even though the attack and defense values are calculated
|
||||
extremely simply, they are separated out into their own functions
|
||||
so that they are easier to expand upon.
|
||||
"""
|
||||
# Get an attack roll from the attacker.
|
||||
if not attack_value:
|
||||
attack_value = get_attack(attacker, defender)
|
||||
# Get a defense value from the defender.
|
||||
if not defense_value:
|
||||
defense_value = get_defense(attacker, defender)
|
||||
# If the attack value is lower than the defense value, miss. Otherwise, hit.
|
||||
if attack_value < defense_value:
|
||||
attacker.location.msg_contents("%s's attack misses %s!" % (attacker, defender))
|
||||
else:
|
||||
damage_value = get_damage(attacker, defender) # Calculate damage value.
|
||||
# Announce damage dealt and apply damage.
|
||||
attacker.location.msg_contents("%s hits %s for %i damage!" % (attacker, defender, damage_value))
|
||||
apply_damage(defender, damage_value)
|
||||
# If defender HP is reduced to 0 or less, announce defeat.
|
||||
if defender.db.hp <= 0:
|
||||
attacker.location.msg_contents("%s has been defeated!" % defender)
|
||||
|
||||
|
||||
def combat_cleanup(character):
|
||||
"""
|
||||
Cleans up all the temporary combat-related attributes on a character.
|
||||
|
||||
Args:
|
||||
character (obj): Character to have their combat attributes removed
|
||||
|
||||
Notes:
|
||||
Any attribute whose key begins with 'combat_' is temporary and no
|
||||
longer needed once a fight ends.
|
||||
"""
|
||||
for attr in character.attributes.all():
|
||||
if attr.key[:7] == "combat_": # If the attribute name starts with 'combat_'...
|
||||
character.attributes.remove(key=attr.key) # ...then delete it!
|
||||
|
||||
|
||||
def is_in_combat(character):
|
||||
"""
|
||||
Returns true if the given character is in combat.
|
||||
|
||||
Args:
|
||||
character (obj): Character to determine if is in combat or not
|
||||
|
||||
Returns:
|
||||
(bool): True if in combat or False if not in combat
|
||||
"""
|
||||
if character.db.Combat_TurnHandler:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def is_turn(character):
|
||||
"""
|
||||
Returns true if it's currently the given character's turn in combat.
|
||||
|
||||
Args:
|
||||
character (obj): Character to determine if it is their turn or not
|
||||
|
||||
Returns:
|
||||
(bool): True if it is their turn or False otherwise
|
||||
"""
|
||||
turnhandler = character.db.Combat_TurnHandler
|
||||
currentchar = turnhandler.db.fighters[turnhandler.db.turn]
|
||||
if character == currentchar:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def spend_action(character, actions, action_name=None):
|
||||
"""
|
||||
Spends a character's available combat actions and checks for end of turn.
|
||||
|
||||
Args:
|
||||
character (obj): Character spending the action
|
||||
actions (int) or 'all': Number of actions to spend, or 'all' to spend all actions
|
||||
|
||||
Kwargs:
|
||||
action_name (str or None): If a string is given, sets character's last action in
|
||||
combat to provided string
|
||||
"""
|
||||
if action_name:
|
||||
character.db.Combat_LastAction = action_name
|
||||
if actions == 'all': # If spending all actions
|
||||
character.db.Combat_ActionsLeft = 0 # Set actions to 0
|
||||
else:
|
||||
character.db.Combat_ActionsLeft -= actions # Use up actions.
|
||||
if character.db.Combat_ActionsLeft < 0:
|
||||
character.db.Combat_ActionsLeft = 0 # Can't have fewer than 0 actions
|
||||
character.db.Combat_TurnHandler.turn_end_check(character) # Signal potential end of turn.
|
||||
|
||||
|
||||
"""
|
||||
----------------------------------------------------------------------------
|
||||
CHARACTER TYPECLASS
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
class BattleCharacter(DefaultCharacter):
|
||||
"""
|
||||
A character able to participate in turn-based combat. Has attributes for current
|
||||
and maximum HP, and access to combat commands.
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
Called once, when this object is first created. This is the
|
||||
normal hook to overload for most object types.
|
||||
"""
|
||||
self.db.max_hp = 100 # Set maximum HP to 100
|
||||
self.db.hp = self.db.max_hp # Set current HP to maximum
|
||||
"""
|
||||
Adds attributes for a character's current and maximum HP.
|
||||
We're just going to set this value at '100' by default.
|
||||
|
||||
You may want to expand this to include various 'stats' that
|
||||
can be changed at creation and factor into combat calculations.
|
||||
"""
|
||||
|
||||
def at_before_move(self, destination):
|
||||
"""
|
||||
Called just before starting to move this object to
|
||||
destination.
|
||||
|
||||
Args:
|
||||
destination (Object): The object we are moving to
|
||||
|
||||
Returns:
|
||||
shouldmove (bool): If we should move or not.
|
||||
|
||||
Notes:
|
||||
If this method returns False/None, the move is cancelled
|
||||
before it is even started.
|
||||
|
||||
"""
|
||||
# Keep the character from moving if at 0 HP or in combat.
|
||||
if is_in_combat(self):
|
||||
self.msg("You can't exit a room while in combat!")
|
||||
return False # Returning false keeps the character from moving.
|
||||
if self.db.HP <= 0:
|
||||
self.msg("You can't move, you've been defeated!")
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
"""
|
||||
----------------------------------------------------------------------------
|
||||
COMMANDS START HERE
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
class CmdFight(Command):
|
||||
"""
|
||||
Starts a fight with everyone in the same room as you.
|
||||
|
||||
Usage:
|
||||
fight
|
||||
|
||||
When you start a fight, everyone in the room who is able to
|
||||
fight is added to combat, and a turn order is randomly rolled.
|
||||
When it's your turn, you can attack other characters.
|
||||
"""
|
||||
key = "fight"
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
here = self.caller.location
|
||||
fighters = []
|
||||
|
||||
if not self.caller.db.hp: # If you don't have any hp
|
||||
self.caller.msg("You can't start a fight if you've been defeated!")
|
||||
return
|
||||
if is_in_combat(self.caller): # Already in a fight
|
||||
self.caller.msg("You're already in a fight!")
|
||||
return
|
||||
for thing in here.contents: # Test everything in the room to add it to the fight.
|
||||
if thing.db.HP: # If the object has HP...
|
||||
fighters.append(thing) # ...then add it to the fight.
|
||||
if len(fighters) <= 1: # If you're the only able fighter in the room
|
||||
self.caller.msg("There's nobody here to fight!")
|
||||
return
|
||||
if here.db.Combat_TurnHandler: # If there's already a fight going on...
|
||||
here.msg_contents("%s joins the fight!" % self.caller)
|
||||
here.db.Combat_TurnHandler.join_fight(self.caller) # Join the fight!
|
||||
return
|
||||
here.msg_contents("%s starts a fight!" % self.caller)
|
||||
# Add a turn handler script to the room, which starts combat.
|
||||
here.scripts.add("contrib.turnbattle.TurnHandler")
|
||||
# Remember you'll have to change the path to the script if you copy this code to your own modules!
|
||||
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
Attacks another character.
|
||||
|
||||
Usage:
|
||||
attack <target>
|
||||
|
||||
When in a fight, you may attack another character. The attack has
|
||||
a chance to hit, and if successful, will deal damage.
|
||||
"""
|
||||
|
||||
key = "attack"
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"This performs the actual command."
|
||||
"Set the attacker to the caller and the defender to the target."
|
||||
|
||||
if not is_in_combat(self.caller): # If not in combat, can't attack.
|
||||
self.caller.msg("You can only do that in combat. (see: help fight)")
|
||||
return
|
||||
|
||||
if not is_turn(self.caller): # If it's not your turn, can't attack.
|
||||
self.caller.msg("You can only do that on your turn.")
|
||||
return
|
||||
|
||||
if not self.caller.db.hp: # Can't attack if you have no HP.
|
||||
self.caller.msg("You can't attack, you've been defeated.")
|
||||
return
|
||||
|
||||
attacker = self.caller
|
||||
defender = self.caller.search(self.args)
|
||||
|
||||
if not defender: # No valid target given.
|
||||
return
|
||||
|
||||
if not defender.db.hp: # Target object has no HP left or to begin with
|
||||
self.caller.msg("You can't fight that!")
|
||||
return
|
||||
|
||||
if attacker == defender: # Target and attacker are the same
|
||||
self.caller.msg("You can't attack yourself!")
|
||||
return
|
||||
|
||||
"If everything checks out, call the attack resolving function."
|
||||
resolve_attack(attacker, defender)
|
||||
spend_action(self.caller, 1, action_name="attack") # Use up one action.
|
||||
|
||||
|
||||
class CmdPass(Command):
|
||||
"""
|
||||
Passes on your turn.
|
||||
|
||||
Usage:
|
||||
pass
|
||||
|
||||
When in a fight, you can use this command to end your turn early, even
|
||||
if there are still any actions you can take.
|
||||
"""
|
||||
|
||||
key = "pass"
|
||||
aliases = ["wait", "hold"]
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
if not is_in_combat(self.caller): # Can only pass a turn in combat.
|
||||
self.caller.msg("You can only do that in combat. (see: help fight)")
|
||||
return
|
||||
|
||||
if not is_turn(self.caller): # Can only pass if it's your turn.
|
||||
self.caller.msg("You can only do that on your turn.")
|
||||
return
|
||||
|
||||
self.caller.location.msg_contents("%s takes no further action, passing the turn." % self.caller)
|
||||
spend_action(self.caller, 'all', action_name="pass") # Spend all remaining actions.
|
||||
|
||||
|
||||
class CmdDisengage(Command):
|
||||
"""
|
||||
Passes your turn and attempts to end combat.
|
||||
|
||||
Usage:
|
||||
disengage
|
||||
|
||||
Ends your turn early and signals that you're trying to end
|
||||
the fight. If all participants in a fight disengage, the
|
||||
fight ends.
|
||||
"""
|
||||
|
||||
key = "disengage"
|
||||
aliases = ["spare"]
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This performs the actual command.
|
||||
"""
|
||||
if not is_in_combat(self.caller): # If you're not in combat
|
||||
self.caller.msg("You can only do that in combat. (see: help fight)")
|
||||
return
|
||||
|
||||
if not is_turn(self.caller): # If it's not your turn
|
||||
self.caller.msg("You can only do that on your turn.")
|
||||
return
|
||||
|
||||
self.caller.location.msg_contents("%s disengages, ready to stop fighting." % self.caller)
|
||||
spend_action(self.caller, 'all', action_name="disengage") # Spend all remaining actions.
|
||||
"""
|
||||
The action_name kwarg sets the character's last action to "disengage", which is checked by
|
||||
the turn handler script to see if all fighters have disengaged.
|
||||
"""
|
||||
|
||||
|
||||
class CmdRest(Command):
|
||||
"""
|
||||
Recovers damage.
|
||||
|
||||
Usage:
|
||||
rest
|
||||
|
||||
Resting recovers your HP to its maximum, but you can only
|
||||
rest if you're not in a fight.
|
||||
"""
|
||||
|
||||
key = "rest"
|
||||
help_category = "combat"
|
||||
|
||||
def func(self):
|
||||
"This performs the actual command."
|
||||
|
||||
if is_in_combat(self.caller): # If you're in combat
|
||||
self.caller.msg("You can't rest while you're in combat.")
|
||||
return
|
||||
|
||||
self.caller.db.hp = self.caller.db.max_hp # Set current HP to maximum
|
||||
self.caller.location.msg_contents("%s rests to recover HP." % self.caller)
|
||||
"""
|
||||
You'll probably want to replace this with your own system for recovering HP.
|
||||
"""
|
||||
|
||||
|
||||
class CmdCombatHelp(CmdHelp):
|
||||
"""
|
||||
View help or a list of topics
|
||||
|
||||
Usage:
|
||||
help <topic or command>
|
||||
help list
|
||||
help all
|
||||
|
||||
This will search for help on commands and other
|
||||
topics related to the game.
|
||||
"""
|
||||
# Just like the default help command, but will give quick
|
||||
# tips on combat when used in a fight with no arguments.
|
||||
|
||||
def func(self):
|
||||
if is_in_combat(self.caller) and not self.args: # In combat and entered 'help' alone
|
||||
self.caller.msg("Available combat commands:|/" +
|
||||
"|wAttack:|n Attack a target, attempting to deal damage.|/" +
|
||||
"|wPass:|n Pass your turn without further action.|/" +
|
||||
"|wDisengage:|n End your turn and attempt to end combat.|/")
|
||||
else:
|
||||
super(CmdCombatHelp, self).func() # Call the default help command
|
||||
|
||||
|
||||
class BattleCmdSet(default_cmds.CharacterCmdSet):
|
||||
"""
|
||||
This command set includes all the commmands used in the battle system.
|
||||
"""
|
||||
key = "DefaultCharacter"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"""
|
||||
Populates the cmdset
|
||||
"""
|
||||
self.add(CmdFight())
|
||||
self.add(CmdAttack())
|
||||
self.add(CmdRest())
|
||||
self.add(CmdPass())
|
||||
self.add(CmdDisengage())
|
||||
self.add(CmdCombatHelp())
|
||||
|
||||
|
||||
"""
|
||||
----------------------------------------------------------------------------
|
||||
SCRIPTS START HERE
|
||||
----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
class TurnHandler(DefaultScript):
|
||||
"""
|
||||
This is the script that handles the progression of combat through turns.
|
||||
On creation (when a fight is started) it adds all combat-ready characters
|
||||
to its roster and then sorts them into a turn order. There can only be one
|
||||
fight going on in a single room at a time, so the script is assigned to a
|
||||
room as its object.
|
||||
|
||||
Fights persist until only one participant is left with any HP or all
|
||||
remaining participants choose to end the combat with the 'disengage' command.
|
||||
"""
|
||||
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Called once, when the script is created.
|
||||
"""
|
||||
self.key = "Combat Turn Handler"
|
||||
self.interval = 5 # Once every 5 seconds
|
||||
self.persistent = True
|
||||
self.db.fighters = []
|
||||
|
||||
# Add all fighters in the room with at least 1 HP to the combat."
|
||||
for object in self.obj.contents:
|
||||
if object.db.hp:
|
||||
self.db.fighters.append(object)
|
||||
|
||||
# Initialize each fighter for combat
|
||||
for fighter in self.db.fighters:
|
||||
self.initialize_for_combat(fighter)
|
||||
|
||||
# Add a reference to this script to the room
|
||||
self.obj.db.Combat_TurnHandler = self
|
||||
|
||||
# Roll initiative and sort the list of fighters depending on who rolls highest to determine turn order.
|
||||
# The initiative roll is determined by the roll_init function and can be customized easily.
|
||||
ordered_by_roll = sorted(self.db.fighters, key=roll_init, reverse=True)
|
||||
self.db.fighters = ordered_by_roll
|
||||
|
||||
# Announce the turn order.
|
||||
self.obj.msg_contents("Turn order is: %s " % ", ".join(obj.key for obj in self.db.fighters))
|
||||
|
||||
# Set up the current turn and turn timeout delay.
|
||||
self.db.turn = 0
|
||||
self.db.timer = 30 # 30 seconds
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
Called at script termination.
|
||||
"""
|
||||
for fighter in self.db.fighters:
|
||||
combat_cleanup(fighter) # Clean up the combat attributes for every fighter.
|
||||
self.obj.db.Combat_TurnHandler = None # Remove reference to turn handler in location
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Called once every self.interval seconds.
|
||||
"""
|
||||
currentchar = self.db.fighters[self.db.turn] # Note the current character in the turn order.
|
||||
self.db.timer -= self.interval # Count down the timer.
|
||||
|
||||
if self.db.timer <= 0:
|
||||
# Force current character to disengage if timer runs out.
|
||||
self.obj.msg_contents("%s's turn timed out!" % currentchar)
|
||||
spend_action(currentchar, 'all', action_name="disengage") # Spend all remaining actions.
|
||||
return
|
||||
elif self.db.timer <= 10 and not self.db.timeout_warning_given: # 10 seconds left
|
||||
# Warn the current character if they're about to time out.
|
||||
currentchar.msg("WARNING: About to time out!")
|
||||
self.db.timeout_warning_given = True
|
||||
|
||||
def initialize_for_combat(self, character):
|
||||
"""
|
||||
Prepares a character for combat when starting or entering a fight.
|
||||
|
||||
Args:
|
||||
character (obj): Character to initialize for combat.
|
||||
"""
|
||||
combat_cleanup(character) # Clean up leftover combat attributes beforehand, just in case.
|
||||
character.db.Combat_ActionsLeft = 0 # Actions remaining - start of turn adds to this, turn ends when it reaches 0
|
||||
character.db.Combat_TurnHandler = self # Add a reference to this turn handler script to the character
|
||||
character.db.Combat_LastAction = "null" # Track last action taken in combat
|
||||
|
||||
def start_turn(self, character):
|
||||
"""
|
||||
Readies a character for the start of their turn by replenishing their
|
||||
available actions and notifying them that their turn has come up.
|
||||
|
||||
Args:
|
||||
character (obj): Character to be readied.
|
||||
|
||||
Notes:
|
||||
Here, you only get one action per turn, but you might want to allow more than
|
||||
one per turn, or even grant a number of actions based on a character's
|
||||
attributes. You can even add multiple different kinds of actions, I.E. actions
|
||||
separated for movement, by adding "character.db.Combat_MovesLeft = 3" or
|
||||
something similar.
|
||||
"""
|
||||
character.db.Combat_ActionsLeft = 1 # 1 action per turn.
|
||||
# Prompt the character for their turn and give some information.
|
||||
character.msg("|wIt's your turn! You have %i HP remaining.|n" % character.db.hp)
|
||||
|
||||
def next_turn(self):
|
||||
"""
|
||||
Advances to the next character in the turn order.
|
||||
"""
|
||||
|
||||
# Check to see if every character disengaged as their last action. If so, end combat.
|
||||
disengage_check = True
|
||||
for fighter in self.db.fighters:
|
||||
if fighter.db.Combat_LastAction != "disengage": # If a character has done anything but disengage
|
||||
disengage_check = False
|
||||
if disengage_check: # All characters have disengaged
|
||||
self.obj.msg_contents("All fighters have disengaged! Combat is over!")
|
||||
self.stop() # Stop this script and end combat.
|
||||
return
|
||||
|
||||
# Check to see if only one character is left standing. If so, end combat.
|
||||
defeated_characters = 0
|
||||
for fighter in self.db.fighters:
|
||||
if fighter.db.HP == 0:
|
||||
defeated_characters += 1 # Add 1 for every fighter with 0 HP left (defeated)
|
||||
if defeated_characters == (len(self.db.fighters) - 1): # If only one character isn't defeated
|
||||
for fighter in self.db.fighters:
|
||||
if fighter.db.HP != 0:
|
||||
LastStanding = fighter # Pick the one fighter left with HP remaining
|
||||
self.obj.msg_contents("Only %s remains! Combat is over!" % LastStanding)
|
||||
self.stop() # Stop this script and end combat.
|
||||
return
|
||||
|
||||
# Cycle to the next turn.
|
||||
currentchar = self.db.fighters[self.db.turn]
|
||||
self.db.turn += 1 # Go to the next in the turn order.
|
||||
if self.db.turn > len(self.db.fighters) - 1:
|
||||
self.db.turn = 0 # Go back to the first in the turn order once you reach the end.
|
||||
newchar = self.db.fighters[self.db.turn] # Note the new character
|
||||
self.db.timer = 30 + self.time_until_next_repeat() # Reset the timer.
|
||||
self.db.timeout_warning_given = False # Reset the timeout warning.
|
||||
self.obj.msg_contents("%s's turn ends - %s's turn begins!" % (currentchar, newchar))
|
||||
self.start_turn(newchar) # Start the new character's turn.
|
||||
|
||||
def turn_end_check(self, character):
|
||||
"""
|
||||
Tests to see if a character's turn is over, and cycles to the next turn if it is.
|
||||
|
||||
Args:
|
||||
character (obj): Character to test for end of turn
|
||||
"""
|
||||
if not character.db.Combat_ActionsLeft: # Character has no actions remaining
|
||||
self.next_turn()
|
||||
return
|
||||
|
||||
def join_fight(self, character):
|
||||
"""
|
||||
Adds a new character to a fight already in progress.
|
||||
|
||||
Args:
|
||||
character (obj): Character to be added to the fight.
|
||||
"""
|
||||
# Inserts the fighter to the turn order, right behind whoever's turn it currently is.
|
||||
self.db.fighters.insert(self.db.turn, character)
|
||||
# Tick the turn counter forward one to compensate.
|
||||
self.db.turn += 1
|
||||
# Initialize the character like you do at the start.
|
||||
self.initialize_for_combat(character)
|
||||
|
|
@ -21,17 +21,17 @@ class Command(BaseCommand):
|
|||
|
||||
Each Command implements the following methods, called
|
||||
in this order (only func() is actually required):
|
||||
- at_pre_command(): If this returns True, execution is aborted.
|
||||
- at_pre_cmd(): If this returns True, execution is aborted.
|
||||
- parse(): Should perform any extra parsing needed on self.args
|
||||
and store the result on self.
|
||||
- func(): Performs the actual work.
|
||||
- at_post_command(): Extra actions, often things done after
|
||||
- at_post_cmd(): Extra actions, often things done after
|
||||
every command, like prompts.
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
#------------------------------------------------------------
|
||||
# -------------------------------------------------------------
|
||||
#
|
||||
# The default commands inherit from
|
||||
#
|
||||
|
|
@ -46,139 +46,140 @@ class Command(BaseCommand):
|
|||
# the functionality implemented in the parse() method, so be
|
||||
# careful with what you change.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
# -------------------------------------------------------------
|
||||
|
||||
#from evennia.utils import utils
|
||||
#class MuxCommand(Command):
|
||||
# """
|
||||
# This sets up the basis for a MUX command. The idea
|
||||
# is that most other Mux-related commands should just
|
||||
# inherit from this and don't have to implement much
|
||||
# parsing of their own unless they do something particularly
|
||||
# advanced.
|
||||
# from evennia.utils import utils
|
||||
#
|
||||
# Note that the class's __doc__ string (this text) is
|
||||
# used by Evennia to create the automatic help entry for
|
||||
# the command, so make sure to document consistently here.
|
||||
# """
|
||||
# def has_perm(self, srcobj):
|
||||
# """
|
||||
# This is called by the cmdhandler to determine
|
||||
# if srcobj is allowed to execute this command.
|
||||
# We just show it here for completeness - we
|
||||
# are satisfied using the default check in Command.
|
||||
# """
|
||||
# return super(MuxCommand, self).has_perm(srcobj)
|
||||
#
|
||||
# def at_pre_cmd(self):
|
||||
# """
|
||||
# This hook is called before self.parse() on all commands
|
||||
# """
|
||||
# pass
|
||||
# class MuxCommand(Command):
|
||||
# """
|
||||
# This sets up the basis for a MUX command. The idea
|
||||
# is that most other Mux-related commands should just
|
||||
# inherit from this and don't have to implement much
|
||||
# parsing of their own unless they do something particularly
|
||||
# advanced.
|
||||
#
|
||||
# def at_post_cmd(self):
|
||||
# """
|
||||
# This hook is called after the command has finished executing
|
||||
# (after self.func()).
|
||||
# """
|
||||
# pass
|
||||
# Note that the class's __doc__ string (this text) is
|
||||
# used by Evennia to create the automatic help entry for
|
||||
# the command, so make sure to document consistently here.
|
||||
# """
|
||||
# def has_perm(self, srcobj):
|
||||
# """
|
||||
# This is called by the cmdhandler to determine
|
||||
# if srcobj is allowed to execute this command.
|
||||
# We just show it here for completeness - we
|
||||
# are satisfied using the default check in Command.
|
||||
# """
|
||||
# return super(MuxCommand, self).has_perm(srcobj)
|
||||
#
|
||||
# def parse(self):
|
||||
# """
|
||||
# This method is called by the cmdhandler once the command name
|
||||
# has been identified. It creates a new set of member variables
|
||||
# that can be later accessed from self.func() (see below)
|
||||
# def at_pre_cmd(self):
|
||||
# """
|
||||
# This hook is called before self.parse() on all commands
|
||||
# """
|
||||
# pass
|
||||
#
|
||||
# The following variables are available for our use when entering this
|
||||
# method (from the command definition, and assigned on the fly by the
|
||||
# cmdhandler):
|
||||
# self.key - the name of this command ('look')
|
||||
# self.aliases - the aliases of this cmd ('l')
|
||||
# self.permissions - permission string for this command
|
||||
# self.help_category - overall category of command
|
||||
# def at_post_cmd(self):
|
||||
# """
|
||||
# This hook is called after the command has finished executing
|
||||
# (after self.func()).
|
||||
# """
|
||||
# pass
|
||||
#
|
||||
# self.caller - the object calling this command
|
||||
# self.cmdstring - the actual command name used to call this
|
||||
# (this allows you to know which alias was used,
|
||||
# for example)
|
||||
# self.args - the raw input; everything following self.cmdstring.
|
||||
# self.cmdset - the cmdset from which this command was picked. Not
|
||||
# often used (useful for commands like 'help' or to
|
||||
# list all available commands etc)
|
||||
# self.obj - the object on which this command was defined. It is often
|
||||
# the same as self.caller.
|
||||
# def parse(self):
|
||||
# """
|
||||
# This method is called by the cmdhandler once the command name
|
||||
# has been identified. It creates a new set of member variables
|
||||
# that can be later accessed from self.func() (see below)
|
||||
#
|
||||
# A MUX command has the following possible syntax:
|
||||
# The following variables are available for our use when entering this
|
||||
# method (from the command definition, and assigned on the fly by the
|
||||
# cmdhandler):
|
||||
# self.key - the name of this command ('look')
|
||||
# self.aliases - the aliases of this cmd ('l')
|
||||
# self.permissions - permission string for this command
|
||||
# self.help_category - overall category of command
|
||||
#
|
||||
# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
|
||||
# self.caller - the object calling this command
|
||||
# self.cmdstring - the actual command name used to call this
|
||||
# (this allows you to know which alias was used,
|
||||
# for example)
|
||||
# self.args - the raw input; everything following self.cmdstring.
|
||||
# self.cmdset - the cmdset from which this command was picked. Not
|
||||
# often used (useful for commands like 'help' or to
|
||||
# list all available commands etc)
|
||||
# self.obj - the object on which this command was defined. It is often
|
||||
# the same as self.caller.
|
||||
#
|
||||
# The 'name[ with several words]' part is already dealt with by the
|
||||
# cmdhandler at this point, and stored in self.cmdname (we don't use
|
||||
# it here). The rest of the command is stored in self.args, which can
|
||||
# start with the switch indicator /.
|
||||
# A MUX command has the following possible syntax:
|
||||
#
|
||||
# This parser breaks self.args into its constituents and stores them in the
|
||||
# following variables:
|
||||
# self.switches = [list of /switches (without the /)]
|
||||
# self.raw = This is the raw argument input, including switches
|
||||
# self.args = This is re-defined to be everything *except* the switches
|
||||
# self.lhs = Everything to the left of = (lhs:'left-hand side'). If
|
||||
# no = is found, this is identical to self.args.
|
||||
# self.rhs: Everything to the right of = (rhs:'right-hand side').
|
||||
# If no '=' is found, this is None.
|
||||
# self.lhslist - [self.lhs split into a list by comma]
|
||||
# self.rhslist - [list of self.rhs split into a list by comma]
|
||||
# self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
|
||||
# name[ with several words][/switch[/switch..]] arg1[,arg2,...] [[=|,] arg[,..]]
|
||||
#
|
||||
# All args and list members are stripped of excess whitespace around the
|
||||
# strings, but case is preserved.
|
||||
# """
|
||||
# raw = self.args
|
||||
# args = raw.strip()
|
||||
# The 'name[ with several words]' part is already dealt with by the
|
||||
# cmdhandler at this point, and stored in self.cmdname (we don't use
|
||||
# it here). The rest of the command is stored in self.args, which can
|
||||
# start with the switch indicator /.
|
||||
#
|
||||
# # split out switches
|
||||
# switches = []
|
||||
# if args and len(args) > 1 and args[0] == "/":
|
||||
# # we have a switch, or a set of switches. These end with a space.
|
||||
# switches = args[1:].split(None, 1)
|
||||
# if len(switches) > 1:
|
||||
# switches, args = switches
|
||||
# switches = switches.split('/')
|
||||
# else:
|
||||
# args = ""
|
||||
# switches = switches[0].split('/')
|
||||
# arglist = [arg.strip() for arg in args.split()]
|
||||
# This parser breaks self.args into its constituents and stores them in the
|
||||
# following variables:
|
||||
# self.switches = [list of /switches (without the /)]
|
||||
# self.raw = This is the raw argument input, including switches
|
||||
# self.args = This is re-defined to be everything *except* the switches
|
||||
# self.lhs = Everything to the left of = (lhs:'left-hand side'). If
|
||||
# no = is found, this is identical to self.args.
|
||||
# self.rhs: Everything to the right of = (rhs:'right-hand side').
|
||||
# If no '=' is found, this is None.
|
||||
# self.lhslist - [self.lhs split into a list by comma]
|
||||
# self.rhslist - [list of self.rhs split into a list by comma]
|
||||
# self.arglist = [list of space-separated args (stripped, including '=' if it exists)]
|
||||
#
|
||||
# # check for arg1, arg2, ... = argA, argB, ... constructs
|
||||
# lhs, rhs = args, None
|
||||
# lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
||||
# if args and '=' in args:
|
||||
# lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
||||
# lhslist = [arg.strip() for arg in lhs.split(',')]
|
||||
# rhslist = [arg.strip() for arg in rhs.split(',')]
|
||||
# All args and list members are stripped of excess whitespace around the
|
||||
# strings, but case is preserved.
|
||||
# """
|
||||
# raw = self.args
|
||||
# args = raw.strip()
|
||||
#
|
||||
# # save to object properties:
|
||||
# self.raw = raw
|
||||
# self.switches = switches
|
||||
# self.args = args.strip()
|
||||
# self.arglist = arglist
|
||||
# self.lhs = lhs
|
||||
# self.lhslist = lhslist
|
||||
# self.rhs = rhs
|
||||
# self.rhslist = rhslist
|
||||
# # split out switches
|
||||
# switches = []
|
||||
# if args and len(args) > 1 and args[0] == "/":
|
||||
# # we have a switch, or a set of switches. These end with a space.
|
||||
# switches = args[1:].split(None, 1)
|
||||
# if len(switches) > 1:
|
||||
# switches, args = switches
|
||||
# switches = switches.split('/')
|
||||
# else:
|
||||
# args = ""
|
||||
# switches = switches[0].split('/')
|
||||
# arglist = [arg.strip() for arg in args.split()]
|
||||
#
|
||||
# # if the class has the player_caller property set on itself, we make
|
||||
# # sure that self.caller is always the player if possible. We also create
|
||||
# # a special property "character" for the puppeted object, if any. This
|
||||
# # is convenient for commands defined on the Player only.
|
||||
# if hasattr(self, "player_caller") and self.player_caller:
|
||||
# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# # caller is an Object/Character
|
||||
# self.character = self.caller
|
||||
# self.caller = self.caller.player
|
||||
# elif utils.inherits_from(self.caller, "evennia.players.players.DefaultPlayer"):
|
||||
# # caller was already a Player
|
||||
# self.character = self.caller.get_puppet(self.session)
|
||||
# else:
|
||||
# self.character = None
|
||||
# # check for arg1, arg2, ... = argA, argB, ... constructs
|
||||
# lhs, rhs = args, None
|
||||
# lhslist, rhslist = [arg.strip() for arg in args.split(',')], []
|
||||
# if args and '=' in args:
|
||||
# lhs, rhs = [arg.strip() for arg in args.split('=', 1)]
|
||||
# lhslist = [arg.strip() for arg in lhs.split(',')]
|
||||
# rhslist = [arg.strip() for arg in rhs.split(',')]
|
||||
#
|
||||
# # save to object properties:
|
||||
# self.raw = raw
|
||||
# self.switches = switches
|
||||
# self.args = args.strip()
|
||||
# self.arglist = arglist
|
||||
# self.lhs = lhs
|
||||
# self.lhslist = lhslist
|
||||
# self.rhs = rhs
|
||||
# self.rhslist = rhslist
|
||||
#
|
||||
# # if the class has the player_caller property set on itself, we make
|
||||
# # sure that self.caller is always the player if possible. We also create
|
||||
# # a special property "character" for the puppeted object, if any. This
|
||||
# # is convenient for commands defined on the Player only.
|
||||
# if hasattr(self, "player_caller") and self.player_caller:
|
||||
# if utils.inherits_from(self.caller, "evennia.objects.objects.DefaultObject"):
|
||||
# # caller is an Object/Character
|
||||
# self.character = self.caller
|
||||
# self.caller = self.caller.player
|
||||
# elif utils.inherits_from(self.caller, "evennia.players.players.DefaultPlayer"):
|
||||
# # caller was already a Player
|
||||
# self.character = self.caller.get_puppet(self.session)
|
||||
# else:
|
||||
# self.character = None
|
||||
|
|
|
|||
|
|
@ -25,9 +25,9 @@ __pycache__
|
|||
*.restart
|
||||
*.db3
|
||||
|
||||
# Installation-specific.
|
||||
# Installation-specific.
|
||||
# For group efforts, comment out some or all of these.
|
||||
server/conf/settings.py
|
||||
server/conf/secret_settings.py
|
||||
server/logs/*.log.*
|
||||
web/static/*
|
||||
web/media/*
|
||||
17
evennia/game_template/server/conf/secret_settings.py
Normal file
17
evennia/game_template/server/conf/secret_settings.py
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
"""
|
||||
This file is meant for when you want to share your game dir with
|
||||
others but don't want to share all details of your specific game
|
||||
or local server setup. The settings in this file will override those
|
||||
in settings.py and is in .gitignore by default.
|
||||
|
||||
A good guideline when sharing your game dir is that you want your
|
||||
game to run correctly also without this file and only use this
|
||||
to override your public, shared settings.
|
||||
|
||||
"""
|
||||
|
||||
# The secret key is randomly seeded upon creation. It is used to sign
|
||||
# Django's cookies and should not be publicly known. It should also
|
||||
# generally not be changed once people have registered with the game
|
||||
# since it will invalidate their existing sessions.
|
||||
SECRET_KEY = {secret_key}
|
||||
|
|
@ -19,6 +19,9 @@ paths (path.to.module) should be given relative to the game's root
|
|||
folder (typeclasses.foo) whereas paths within the Evennia library
|
||||
needs to be given explicitly (evennia.foo).
|
||||
|
||||
If you want to share your game dir, including its settings, you can
|
||||
put secret game- or server-specific settings in secret_settings.py.
|
||||
|
||||
"""
|
||||
|
||||
# Use the defaults from Evennia unless explicitly overridden
|
||||
|
|
@ -31,13 +34,11 @@ from evennia.settings_default import *
|
|||
# This is the name of your game. Make it catchy!
|
||||
SERVERNAME = {servername}
|
||||
|
||||
######################################################################
|
||||
# Django web features
|
||||
######################################################################
|
||||
|
||||
|
||||
# The secret key is randomly seeded upon creation. It is used to sign
|
||||
# Django's cookies. Do not share this with anyone. Changing it will
|
||||
# log out all active web browsing sessions. Game web client sessions
|
||||
# may survive.
|
||||
SECRET_KEY = {secret_key}
|
||||
######################################################################
|
||||
# Settings given in secret_settings.py override those in this file.
|
||||
######################################################################
|
||||
try:
|
||||
from server.conf.secret_settings import *
|
||||
except ImportError:
|
||||
print "secret_settings.py file not found or failed to import."
|
||||
|
|
|
|||
20
evennia/help/migrations/0002_auto_20170606_1731.py
Normal file
20
evennia/help/migrations/0002_auto_20170606_1731.py
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-06-06 17:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('help', '0001_initial'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='helpentry',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(blank=True, help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
|
||||
),
|
||||
]
|
||||
|
|
@ -58,7 +58,7 @@ class HelpEntry(SharedMemoryModel):
|
|||
# lock string storage
|
||||
db_lock_storage = models.TextField('locks', blank=True, help_text='normally view:all().')
|
||||
# tags are primarily used for permissions
|
||||
db_tags = models.ManyToManyField(Tag, null=True,
|
||||
db_tags = models.ManyToManyField(Tag, blank=True,
|
||||
help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.')
|
||||
# (deprecated, only here to allow MUX helpfile load (don't use otherwise)).
|
||||
# TODO: remove this when not needed anymore.
|
||||
|
|
|
|||
32
evennia/objects/migrations/0006_auto_20170606_1731.py
Normal file
32
evennia/objects/migrations/0006_auto_20170606_1731.py
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-06-06 17:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.core.validators
|
||||
from django.db import migrations, models
|
||||
import re
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('objects', '0005_auto_20150403_2339'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='objectdb',
|
||||
name='db_attributes',
|
||||
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objectdb',
|
||||
name='db_sessid',
|
||||
field=models.CharField(help_text=b'csv list of session ids of connected Player, if any.', max_length=32, null=True, validators=[django.core.validators.RegexValidator(re.compile('^\\d+(?:\\,\\d+)*\\Z'), code='invalid', message='Enter only digits separated by commas.')], verbose_name=b'session id'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='objectdb',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
|
||||
),
|
||||
]
|
||||
|
|
@ -18,6 +18,7 @@ from builtins import object
|
|||
from django.conf import settings
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.validators import validate_comma_separated_integer_list
|
||||
|
||||
from evennia.typeclasses.models import TypedObject
|
||||
from evennia.objects.manager import ObjectDBManager
|
||||
|
|
@ -172,7 +173,8 @@ class ObjectDB(TypedObject):
|
|||
db_player = models.ForeignKey("players.PlayerDB", null=True, verbose_name='player', on_delete=models.SET_NULL,
|
||||
help_text='a Player connected to this object, if any.')
|
||||
# the session id associated with this player, if any
|
||||
db_sessid = models.CommaSeparatedIntegerField(null=True, max_length=32, verbose_name="session id",
|
||||
db_sessid = models.CharField(null=True, max_length=32, validators=[validate_comma_separated_integer_list],
|
||||
verbose_name="session id",
|
||||
help_text="csv list of session ids of connected Player, if any.")
|
||||
# The location in the game world. Since this one is likely
|
||||
# to change often, we set this with the 'location' property
|
||||
|
|
|
|||
|
|
@ -58,7 +58,7 @@ class ObjectSessionHandler(object):
|
|||
self._sessid_cache = list(set(int(val) for val in (self.obj.db_sessid or "").split(",") if val))
|
||||
if any(sessid for sessid in self._sessid_cache if sessid not in _SESSIONS):
|
||||
# cache is out of sync with sessionhandler! Only retain the ones in the handler.
|
||||
self.sessid_cache = [sessid for sessid in self._sessid_cache if sessid in _SESSIONS]
|
||||
self._sessid_cache = [sessid for sessid in self._sessid_cache if sessid in _SESSIONS]
|
||||
self.obj.db_sessid = ",".join(str(val) for val in self._sessid_cache)
|
||||
self.obj.save(update_fields=["db_sessid"])
|
||||
|
||||
|
|
|
|||
31
evennia/players/migrations/0006_auto_20170606_1731.py
Normal file
31
evennia/players/migrations/0006_auto_20170606_1731.py
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-06-06 17:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import django.contrib.auth.validators
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('players', '0005_auto_20160905_0902'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='playerdb',
|
||||
name='db_attributes',
|
||||
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='playerdb',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='playerdb',
|
||||
name='username',
|
||||
field=models.CharField(error_messages={'unique': 'A user with that username already exists.'}, help_text='Required. 150 characters or fewer. Letters, digits and @/./+/-/_ only.', max_length=150, unique=True, validators=[django.contrib.auth.validators.ASCIIUsernameValidator()], verbose_name='username'),
|
||||
),
|
||||
]
|
||||
25
evennia/scripts/migrations/0008_auto_20170606_1731.py
Normal file
25
evennia/scripts/migrations/0008_auto_20170606_1731.py
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
# Generated by Django 1.11.2 on 2017-06-06 17:31
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('scripts', '0007_auto_20150403_2339'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='scriptdb',
|
||||
name='db_attributes',
|
||||
field=models.ManyToManyField(help_text=b'attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).', to='typeclasses.Attribute'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='scriptdb',
|
||||
name='db_tags',
|
||||
field=models.ManyToManyField(help_text=b'tags on this object. Tags are simple string markers to identify, group and alias objects.', to='typeclasses.Tag'),
|
||||
),
|
||||
]
|
||||
|
|
@ -64,8 +64,8 @@ ENFORCED_SETTING = False
|
|||
# requirements
|
||||
PYTHON_MIN = '2.7'
|
||||
TWISTED_MIN = '16.0.0'
|
||||
DJANGO_MIN = '1.8'
|
||||
DJANGO_REC = '1.9'
|
||||
DJANGO_MIN = '1.11'
|
||||
DJANGO_REC = '1.11'
|
||||
|
||||
sys.path[1] = EVENNIA_ROOT
|
||||
|
||||
|
|
@ -344,10 +344,13 @@ ERROR_DJANGO_MIN = \
|
|||
ERROR: Django {dversion} found. Evennia requires version {django_min}
|
||||
or higher.
|
||||
|
||||
Install it with for example `pip install --upgrade django`
|
||||
If you are using a virtualenv, use the command `pip install --upgrade -e evennia` where
|
||||
`evennia` is the folder to where you cloned the Evennia library. If not
|
||||
in a virtualenv you can install django with for example `pip install --upgrade django`
|
||||
or with `pip install django=={django_min}` to get a specific version.
|
||||
|
||||
It's also a good idea to run `evennia migrate` after this upgrade.
|
||||
It's also a good idea to run `evennia migrate` after this upgrade. Ignore
|
||||
any warnings and don't run `makemigrate` even if told to.
|
||||
"""
|
||||
|
||||
NOTE_DJANGO_MIN = \
|
||||
|
|
@ -506,7 +509,7 @@ def create_secret_key():
|
|||
return secret_key
|
||||
|
||||
|
||||
def create_settings_file(init=True):
|
||||
def create_settings_file(init=True, secret_settings=False):
|
||||
"""
|
||||
Uses the template settings file to build a working settings file.
|
||||
|
||||
|
|
@ -514,18 +517,27 @@ def create_settings_file(init=True):
|
|||
init (bool): This is part of the normal evennia --init
|
||||
operation. If false, this function will copy a fresh
|
||||
template file in (asking if it already exists).
|
||||
secret_settings (bool, optional): If False, create settings.py, otherwise
|
||||
create the secret_settings.py file.
|
||||
|
||||
"""
|
||||
settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py")
|
||||
if secret_settings:
|
||||
settings_path = os.path.join(GAMEDIR, "server", "conf", "secret_settings.py")
|
||||
setting_dict = {"secret_key": "\'%s\'" % create_secret_key()}
|
||||
else:
|
||||
settings_path = os.path.join(GAMEDIR, "server", "conf", "settings.py")
|
||||
setting_dict = {
|
||||
"settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"),
|
||||
"servername": "\"%s\"" % GAMEDIR.rsplit(os.path.sep, 1)[1].capitalize(),
|
||||
"secret_key": "\'%s\'" % create_secret_key()}
|
||||
|
||||
if not init:
|
||||
# if not --init mode, settings file may already exist from before
|
||||
if os.path.exists(settings_path):
|
||||
inp = input("server/conf/settings.py already exists. "
|
||||
"Do you want to reset it? y/[N]> ")
|
||||
inp = input("%s already exists. Do you want to reset it? y/[N]> " % settings_path)
|
||||
if not inp.lower() == 'y':
|
||||
print ("Aborted.")
|
||||
sys.exit()
|
||||
return
|
||||
else:
|
||||
print ("Reset the settings file.")
|
||||
|
||||
|
|
@ -535,12 +547,6 @@ def create_settings_file(init=True):
|
|||
with open(settings_path, 'r') as f:
|
||||
settings_string = f.read()
|
||||
|
||||
# tweak the settings
|
||||
setting_dict = {
|
||||
"settings_default": os.path.join(EVENNIA_LIB, "settings_default.py"),
|
||||
"servername": "\"%s\"" % GAMEDIR.rsplit(os.path.sep, 1)[1].capitalize(),
|
||||
"secret_key": "\'%s\'" % create_secret_key()}
|
||||
|
||||
settings_string = settings_string.format(**setting_dict)
|
||||
|
||||
with open(settings_path, 'w') as f:
|
||||
|
|
@ -564,8 +570,13 @@ def create_game_directory(dirname):
|
|||
sys.exit()
|
||||
# copy template directory
|
||||
shutil.copytree(EVENNIA_TEMPLATE, GAMEDIR)
|
||||
# rename gitignore to .gitignore
|
||||
os.rename(os.path.join(GAMEDIR, 'gitignore'),
|
||||
os.path.join(GAMEDIR, '.gitignore'))
|
||||
|
||||
# pre-build settings file in the new GAMEDIR
|
||||
create_settings_file()
|
||||
create_settings_file(secret_settings=True)
|
||||
|
||||
|
||||
def create_superuser():
|
||||
|
|
|
|||
|
|
@ -193,7 +193,8 @@ def client_options(session, *args, **kwargs):
|
|||
"UTF-8", "SCREENREADER", "ENCODING",
|
||||
"MCCP", "SCREENHEIGHT",
|
||||
"SCREENWIDTH", "INPUTDEBUG",
|
||||
"RAW", "NOCOLOR"))
|
||||
"RAW", "NOCOLOR",
|
||||
"NOGOAHEAD"))
|
||||
session.msg(client_options=options)
|
||||
return
|
||||
|
||||
|
|
@ -244,6 +245,8 @@ def client_options(session, *args, **kwargs):
|
|||
flags["NOCOLOR"] = validate_bool(value)
|
||||
elif key == "raw":
|
||||
flags["RAW"] = validate_bool(value)
|
||||
elif key == "nogoahead":
|
||||
flags["NOGOAHEAD"] = validate_bool(value)
|
||||
elif key in ('Char 1', 'Char.Skills 1', 'Char.Items 1',
|
||||
'Room 1', 'IRE.Rift 1', 'IRE.Composer 1'):
|
||||
# ignore mudlet's default send (aimed at IRE games)
|
||||
|
|
|
|||
|
|
@ -206,7 +206,7 @@ class IRCBot(irc.IRCClient, Session):
|
|||
reason (str): Motivation for the disconnect.
|
||||
|
||||
"""
|
||||
self.sessionhandler.disconnect(self, reason=reason)
|
||||
self.sessionhandler.disconnect(self)
|
||||
self.stopping = True
|
||||
self.transport.loseConnection()
|
||||
|
||||
|
|
|
|||
|
|
@ -294,6 +294,7 @@ if SSH_ENABLED:
|
|||
|
||||
|
||||
if WEBSERVER_ENABLED:
|
||||
from evennia.server.webserver import Website
|
||||
|
||||
# Start a reverse proxy to relay data to the Server-side webserver
|
||||
|
||||
|
|
@ -337,7 +338,7 @@ if WEBSERVER_ENABLED:
|
|||
websocket_started = True
|
||||
webclientstr = "\n + webclient%s" % pstring
|
||||
|
||||
web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
web_root = Website(web_root, logPath=settings.HTTP_LOG_FILE)
|
||||
proxy_service = internet.TCPServer(proxyport,
|
||||
web_root,
|
||||
interface=interface)
|
||||
|
|
|
|||
|
|
@ -283,7 +283,7 @@ class SshProtocol(Manhole, session.Session):
|
|||
else:
|
||||
# we need to make sure to kill the color at the end in order
|
||||
# to match the webclient output.
|
||||
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("|n" if text[-1] != "|" else "||n"),
|
||||
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
|
||||
strip_ansi=nocolor, xterm256=xterm256, mxp=False)
|
||||
self.sendLine(linetosend)
|
||||
|
||||
|
|
|
|||
65
evennia/server/portal/suppress_ga.py
Normal file
65
evennia/server/portal/suppress_ga.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
|
||||
SUPPRESS-GO-AHEAD
|
||||
|
||||
This supports suppressing or activating Evennia
|
||||
the GO-AHEAD telnet operation after every server reply.
|
||||
If the client sends no explicit DONT SUPRESS GO-AHEAD,
|
||||
Evennia will default to supressing it since many clients
|
||||
will fail to use it and has no knowledge of this standard.
|
||||
|
||||
It is set as the NOGOAHEAD protocol_flag option.
|
||||
|
||||
http://www.faqs.org/rfcs/rfc858.html
|
||||
|
||||
"""
|
||||
from builtins import object
|
||||
SUPPRESS_GA = chr(3)
|
||||
|
||||
# default taken from telnet specification
|
||||
|
||||
# try to get the customized mssp info, if it exists.
|
||||
|
||||
class SuppressGA(object):
|
||||
"""
|
||||
Implements the SUPRESS-GO-AHEAD protocol. Add this to a variable on the telnet
|
||||
protocol to set it up.
|
||||
|
||||
"""
|
||||
def __init__(self, protocol):
|
||||
"""
|
||||
Initialize suppression of GO-AHEADs.
|
||||
|
||||
Args:
|
||||
protocol (Protocol): The active protocol instance.
|
||||
|
||||
"""
|
||||
self.protocol = protocol
|
||||
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||
# tell the client that we prefer to suppress GA ...
|
||||
self.protocol.will(SUPPRESS_GA).addCallbacks(self.do_suppress_ga, self.dont_suppress_ga)
|
||||
# ... but also accept if the client really wants not to.
|
||||
self.protocol.do(SUPPRESS_GA).addCallbacks(self.do_suppress_ga, self.dont_suppress_ga)
|
||||
|
||||
def dont_suppress_ga(self, option):
|
||||
"""
|
||||
Called when client requests to not suppress GA.
|
||||
|
||||
Args:
|
||||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = False
|
||||
self.protocol.handshake_done()
|
||||
|
||||
def do_suppress_ga(self, option):
|
||||
"""
|
||||
Client wants to suppress GA
|
||||
|
||||
Args:
|
||||
option (Option): Not used.
|
||||
|
||||
"""
|
||||
self.protocol.protocol_flags["NOGOAHEAD"] = True
|
||||
self.protocol.handshake_done()
|
||||
|
|
@ -13,7 +13,7 @@ from twisted.conch.telnet import Telnet, StatefulTelnetProtocol
|
|||
from twisted.conch.telnet import IAC, NOP, LINEMODE, GA, WILL, WONT, ECHO, NULL
|
||||
from django.conf import settings
|
||||
from evennia.server.session import Session
|
||||
from evennia.server.portal import ttype, mssp, telnet_oob, naws
|
||||
from evennia.server.portal import ttype, mssp, telnet_oob, naws, suppress_ga
|
||||
from evennia.server.portal.mccp import Mccp, mccp_compress, MCCP
|
||||
from evennia.server.portal.mxp import Mxp, mxp_parse
|
||||
from evennia.utils import ansi
|
||||
|
|
@ -47,9 +47,11 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
client_address = client_address[0] if client_address else None
|
||||
# this number is counted down for every handshake that completes.
|
||||
# when it reaches 0 the portal/server syncs their data
|
||||
self.handshakes = 7 # naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
||||
self.handshakes = 8 # suppress-go-ahead, naws, ttype, mccp, mssp, msdp, gmcp, mxp
|
||||
self.init_session(self.protocol_name, client_address, self.factory.sessionhandler)
|
||||
|
||||
# suppress go-ahead
|
||||
self.sga = suppress_ga.SuppressGA(self)
|
||||
# negotiate client size
|
||||
self.naws = naws.Naws(self)
|
||||
# negotiate ttype (client info)
|
||||
|
|
@ -128,7 +130,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
option == ttype.TTYPE or
|
||||
option == naws.NAWS or
|
||||
option == MCCP or
|
||||
option == mssp.MSSP)
|
||||
option == mssp.MSSP or
|
||||
option == suppress_ga.SUPPRESS_GA)
|
||||
|
||||
def enableLocal(self, option):
|
||||
"""
|
||||
|
|
@ -141,7 +144,9 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
enable (bool): If this option should be enabled.
|
||||
|
||||
"""
|
||||
return option == MCCP or option == ECHO
|
||||
return (option == MCCP or
|
||||
option == ECHO or
|
||||
option == suppress_ga.SUPPRESS_GA)
|
||||
|
||||
def disableLocal(self, option):
|
||||
"""
|
||||
|
|
@ -221,6 +226,8 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
# escape IAC in line mode, and correctly add \r\n
|
||||
line += self.delimiter
|
||||
line = line.replace(IAC, IAC + IAC).replace('\n', '\r\n')
|
||||
if not self.protocol_flags.get("NOGOAHEAD", True):
|
||||
line += IAC + GA
|
||||
return self.transport.write(mccp_compress(self, line))
|
||||
|
||||
# Session hooks
|
||||
|
|
@ -309,7 +316,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
prompt = text
|
||||
if not raw:
|
||||
# processing
|
||||
prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + ("|n" if prompt[-1] != "|" else "||n"),
|
||||
prompt = ansi.parse_ansi(_RE_N.sub("", prompt) + ("||n" if prompt.endswith("|") else "|n"),
|
||||
strip_ansi=nocolor, xterm256=xterm256)
|
||||
if mxp:
|
||||
prompt = mxp_parse(prompt)
|
||||
|
|
@ -337,7 +344,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session):
|
|||
else:
|
||||
# we need to make sure to kill the color at the end in order
|
||||
# to match the webclient output.
|
||||
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("|n" if text[-1] != "|" else "||n"),
|
||||
linetosend = ansi.parse_ansi(_RE_N.sub("", text) + ("||n" if text.endswith("|") else "|n"),
|
||||
strip_ansi=nocolor, xterm256=xterm256, mxp=mxp)
|
||||
if mxp:
|
||||
linetosend = mxp_parse(linetosend)
|
||||
|
|
|
|||
|
|
@ -47,7 +47,11 @@ IAC = chr(255)
|
|||
SB = chr(250)
|
||||
SE = chr(240)
|
||||
|
||||
force_str = lambda inp: to_str(inp, force_string=True)
|
||||
|
||||
def force_str(inp):
|
||||
"""Helper to shorten code"""
|
||||
return to_str(inp, force_string=True)
|
||||
|
||||
|
||||
# pre-compiled regexes
|
||||
# returns 2-tuple
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import os
|
|||
from twisted.web import static
|
||||
from twisted.application import internet, service
|
||||
from twisted.internet import reactor, defer
|
||||
from twisted.internet.task import LoopingCall
|
||||
from twisted.internet.task import LoopingCall, deferLater
|
||||
|
||||
import django
|
||||
django.setup()
|
||||
|
|
@ -151,7 +151,8 @@ class Evennia(object):
|
|||
sys.path.insert(1, '.')
|
||||
|
||||
# create a store of services
|
||||
self.services = service.IServiceCollection(application)
|
||||
self.services = service.MultiService()
|
||||
self.services.setServiceParent(application)
|
||||
self.amp_protocol = None # set by amp factory
|
||||
self.sessions = SESSIONS
|
||||
self.sessions.server = self
|
||||
|
|
@ -167,10 +168,21 @@ class Evennia(object):
|
|||
# initialize channelhandler
|
||||
channelhandler.CHANNELHANDLER.update()
|
||||
|
||||
# set a callback if the server is killed abruptly,
|
||||
# by Ctrl-C, reboot etc.
|
||||
reactor.addSystemEventTrigger('before', 'shutdown',
|
||||
self.shutdown, _reactor_stopping=True)
|
||||
# wrap the SIGINT handler to make sure we empty the threadpool
|
||||
# even when we reload and we have long-running requests in queue.
|
||||
# this is necessary over using Twisted's signal handler.
|
||||
# (see https://github.com/evennia/evennia/issues/1128)
|
||||
def _wrap_sigint_handler(*args):
|
||||
from twisted.internet.defer import Deferred
|
||||
if hasattr(self, "web_root"):
|
||||
d = self.web_root.empty_threadpool()
|
||||
d.addCallback(lambda _: self.shutdown(_reactor_stopping=True))
|
||||
else:
|
||||
d = Deferred(lambda _: self.shutdown(_reactor_stopping=True))
|
||||
d.addCallback(lambda _: reactor.stop())
|
||||
reactor.callLater(1, d.callback, None)
|
||||
reactor.sigInt = _wrap_sigint_handler
|
||||
|
||||
self.game_running = True
|
||||
|
||||
# track the server time
|
||||
|
|
@ -183,7 +195,7 @@ class Evennia(object):
|
|||
Optimize some SQLite stuff at startup since we
|
||||
can't save it to the database.
|
||||
"""
|
||||
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASE_ENGINE == "sqlite3")
|
||||
if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASES.get('default', {}).get('ENGINE') == "sqlite3")
|
||||
or (hasattr(settings, 'DATABASES')
|
||||
and settings.DATABASES.get("default", {}).get('ENGINE', None)
|
||||
== 'django.db.backends.sqlite3')):
|
||||
|
|
@ -383,16 +395,16 @@ class Evennia(object):
|
|||
# always called, also for a reload
|
||||
self.at_server_stop()
|
||||
|
||||
# if _reactor_stopping is true, reactor does not need to
|
||||
# be stopped again.
|
||||
if os.name == 'nt' and os.path.exists(SERVER_PIDFILE):
|
||||
# for Windows we need to remove pid files manually
|
||||
os.remove(SERVER_PIDFILE)
|
||||
|
||||
if hasattr(self, "web_root"): # not set very first start
|
||||
yield self.web_root.empty_threadpool()
|
||||
|
||||
if not _reactor_stopping:
|
||||
# this will also send a reactor.stop signal, so we set a
|
||||
# flag to avoid loops.
|
||||
self.shutdown_complete = True
|
||||
# kill the server
|
||||
self.shutdown_complete = True
|
||||
reactor.callLater(1, reactor.stop)
|
||||
|
||||
# we make sure the proper gametime is saved as late as possible
|
||||
|
|
@ -526,18 +538,20 @@ if WEBSERVER_ENABLED:
|
|||
|
||||
# Start a django-compatible webserver.
|
||||
|
||||
from twisted.python import threadpool
|
||||
from evennia.server.webserver import DjangoWebRoot, WSGIWebServer, Website
|
||||
#from twisted.python import threadpool
|
||||
from evennia.server.webserver import DjangoWebRoot, WSGIWebServer, Website, LockableThreadPool
|
||||
|
||||
# start a thread pool and define the root url (/) as a wsgi resource
|
||||
# recognized by Django
|
||||
threads = threadpool.ThreadPool(minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
|
||||
threads = LockableThreadPool(minthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[0]),
|
||||
maxthreads=max(1, settings.WEBSERVER_THREADPOOL_LIMITS[1]))
|
||||
|
||||
web_root = DjangoWebRoot(threads)
|
||||
# point our media resources to url /media
|
||||
web_root.putChild("media", static.File(settings.MEDIA_ROOT))
|
||||
# point our static resources to url /static
|
||||
web_root.putChild("static", static.File(settings.STATIC_ROOT))
|
||||
EVENNIA.web_root = web_root
|
||||
|
||||
if WEB_PLUGINS_MODULE:
|
||||
# custom overloads
|
||||
|
|
|
|||
|
|
@ -18,6 +18,8 @@ from twisted.internet import reactor
|
|||
from twisted.application import internet
|
||||
from twisted.web.proxy import ReverseProxyResource
|
||||
from twisted.web.server import NOT_DONE_YET
|
||||
from twisted.python import threadpool
|
||||
from twisted.internet import defer
|
||||
|
||||
from twisted.web.wsgi import WSGIResource
|
||||
from django.conf import settings
|
||||
|
|
@ -28,6 +30,27 @@ from evennia.utils import logger
|
|||
_UPSTREAM_IPS = settings.UPSTREAM_IPS
|
||||
_DEBUG = settings.DEBUG
|
||||
|
||||
|
||||
class LockableThreadPool(threadpool.ThreadPool):
|
||||
"""
|
||||
Threadpool that can be locked from accepting new requests.
|
||||
"""
|
||||
def __init__(self, *args, **kwargs):
|
||||
self._accept_new = True
|
||||
threadpool.ThreadPool.__init__(self, *args, **kwargs)
|
||||
|
||||
def lock(self):
|
||||
self._accept_new = False
|
||||
|
||||
def callInThread(self, func, *args, **kwargs):
|
||||
"""
|
||||
called in the main reactor thread. Makes sure the pool
|
||||
is not locked before continuing.
|
||||
"""
|
||||
if self._accept_new:
|
||||
threadpool.ThreadPool.callInThread(self, func, *args, **kwargs)
|
||||
|
||||
|
||||
#
|
||||
# X-Forwarded-For Handler
|
||||
#
|
||||
|
|
@ -102,7 +125,7 @@ class EvenniaReverseProxyResource(ReverseProxyResource):
|
|||
clientFactory.noisy = False
|
||||
self.reactor.connectTCP(self.host, self.port, clientFactory)
|
||||
# don't trigger traceback if connection is lost before request finish.
|
||||
request.notifyFinish().addErrback(lambda f: f.cancel())
|
||||
request.notifyFinish().addErrback(lambda f: logger.log_trace("%s\nCaught errback in webserver.py:75." % f))
|
||||
return NOT_DONE_YET
|
||||
|
||||
|
||||
|
|
@ -115,8 +138,9 @@ class DjangoWebRoot(resource.Resource):
|
|||
"""
|
||||
This creates a web root (/) that Django
|
||||
understands by tweaking the way
|
||||
child instancee ars recognized.
|
||||
child instances are recognized.
|
||||
"""
|
||||
|
||||
def __init__(self, pool):
|
||||
"""
|
||||
Setup the django+twisted resource.
|
||||
|
|
@ -125,9 +149,30 @@ class DjangoWebRoot(resource.Resource):
|
|||
pool (ThreadPool): The twisted threadpool.
|
||||
|
||||
"""
|
||||
self.pool = pool
|
||||
self._echo_log = True
|
||||
self._pending_requests = {}
|
||||
resource.Resource.__init__(self)
|
||||
self.wsgi_resource = WSGIResource(reactor, pool, WSGIHandler())
|
||||
|
||||
def empty_threadpool(self):
|
||||
"""
|
||||
Converts our _pending_requests list of deferreds into a DeferredList
|
||||
|
||||
Returns:
|
||||
deflist (DeferredList): Contains all deferreds of pending requests.
|
||||
|
||||
"""
|
||||
self.pool.lock()
|
||||
if self._pending_requests and self._echo_log:
|
||||
self._echo_log = False # just to avoid multiple echoes
|
||||
msg = "Webserver waiting for %i requests ... "
|
||||
logger.log_info(msg % len(self._pending_requests))
|
||||
return defer.DeferredList(self._pending_requests, consumeErrors=True)
|
||||
|
||||
def _decrement_requests(self, *args, **kwargs):
|
||||
self._pending_requests.pop(kwargs.get('deferred', None), None)
|
||||
|
||||
def getChild(self, path, request):
|
||||
"""
|
||||
To make things work we nudge the url tree to make this the
|
||||
|
|
@ -137,11 +182,22 @@ class DjangoWebRoot(resource.Resource):
|
|||
path (str): Url path.
|
||||
request (Request object): Incoming request.
|
||||
|
||||
Notes:
|
||||
We make sure to save the request queue so
|
||||
that we can safely kill the threadpool
|
||||
on a server reload.
|
||||
|
||||
"""
|
||||
path0 = request.prepath.pop(0)
|
||||
request.postpath.insert(0, path0)
|
||||
|
||||
deferred = request.notifyFinish()
|
||||
self._pending_requests[deferred] = deferred
|
||||
deferred.addBoth(self._decrement_requests, deferred=deferred)
|
||||
|
||||
return self.wsgi_resource
|
||||
|
||||
|
||||
#
|
||||
# Site with deactivateable logging
|
||||
#
|
||||
|
|
@ -151,11 +207,13 @@ class Website(server.Site):
|
|||
This class will only log http requests if settings.DEBUG is True.
|
||||
"""
|
||||
noisy = False
|
||||
|
||||
def log(self, request):
|
||||
"Conditional logging"
|
||||
"""Conditional logging"""
|
||||
if _DEBUG:
|
||||
server.Site.log(self, request)
|
||||
|
||||
|
||||
#
|
||||
# Threaded Webserver
|
||||
#
|
||||
|
|
|
|||
|
|
@ -125,6 +125,10 @@ LOCKWARNING_LOG_FILE = os.path.join(LOG_DIR, 'lockwarnings.log')
|
|||
# file sizes down. Turn off to get ever growing log files and never
|
||||
# loose log info.
|
||||
CYCLE_LOGFILES = True
|
||||
# Number of lines to append to rotating channel logs when they rotate
|
||||
CHANNEL_LOG_NUM_TAIL_LINES = 20
|
||||
# Max size of channel log files before they rotate
|
||||
CHANNEL_LOG_ROTATE_SIZE = 1000000
|
||||
# Local time zone for this installation. All choices can be found here:
|
||||
# http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE
|
||||
TIME_ZONE = 'UTC'
|
||||
|
|
@ -224,7 +228,7 @@ IN_GAME_ERRORS = True
|
|||
# ENGINE - path to the the database backend. Possible choices are:
|
||||
# 'django.db.backends.sqlite3', (default)
|
||||
# 'django.db.backends.mysql',
|
||||
# 'django.db.backends.'postgresql_psycopg2',
|
||||
# 'django.db.backends.postgresql_psycopg2',
|
||||
# 'django.db.backends.oracle' (untested).
|
||||
# NAME - database name, or path to the db file for sqlite3
|
||||
# USER - db admin (unused in sqlite3)
|
||||
|
|
|
|||
|
|
@ -1,240 +0,0 @@
|
|||
"""
|
||||
This is a patch of django.db.models.base.py:__new__, to allow for the
|
||||
proxy system to allow multiple inheritance when both parents are of
|
||||
the same base model.
|
||||
|
||||
This patch is implemented as per
|
||||
https://code.djangoproject.com/ticket/11560 and will hopefully be
|
||||
possible to remove as it gets added to django's main branch.
|
||||
"""
|
||||
|
||||
# django patch imports
|
||||
import sys
|
||||
import copy
|
||||
import warnings
|
||||
from django.apps import apps
|
||||
from django.db.models.base import ModelBase, subclass_exception
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.options import Options
|
||||
from django.core.exceptions import MultipleObjectsReturned, FieldError
|
||||
from django.apps.config import MODELS_MODULE_NAME
|
||||
from django.db.models.fields.related import OneToOneField
|
||||
#/ django patch imports
|
||||
|
||||
def patched_new(cls, name, bases, attrs):
|
||||
"Patched version of __new__"
|
||||
|
||||
super_new = super(ModelBase, cls).__new__
|
||||
|
||||
# Also ensure initialization is only performed for subclasses of Model
|
||||
# (excluding Model class itself).
|
||||
parents = [b for b in bases if isinstance(b, ModelBase)]
|
||||
if not parents:
|
||||
return super_new(cls, name, bases, attrs)
|
||||
|
||||
# Create the class.
|
||||
module = attrs.pop('__module__')
|
||||
new_class = super_new(cls, name, bases, {'__module__': module})
|
||||
attr_meta = attrs.pop('Meta', None)
|
||||
abstract = getattr(attr_meta, 'abstract', False)
|
||||
if not attr_meta:
|
||||
meta = getattr(new_class, 'Meta', None)
|
||||
else:
|
||||
meta = attr_meta
|
||||
base_meta = getattr(new_class, '_meta', None)
|
||||
|
||||
# Look for an application configuration to attach the model to.
|
||||
app_config = apps.get_containing_app_config(module)
|
||||
|
||||
kwargs = {}
|
||||
if getattr(meta, 'app_label', None) is None:
|
||||
|
||||
if app_config is None:
|
||||
# If the model is imported before the configuration for its
|
||||
# application is created (#21719), or isn't in an installed
|
||||
# application (#21680), use the legacy logic to figure out the
|
||||
# app_label by looking one level up from the package or module
|
||||
# named 'models'. If no such package or module exists, fall
|
||||
# back to looking one level up from the module this model is
|
||||
# defined in.
|
||||
|
||||
# For 'django.contrib.sites.models', this would be 'sites'.
|
||||
# For 'geo.models.places' this would be 'geo'.
|
||||
|
||||
if abstract:
|
||||
kwargs = {"app_label": None}
|
||||
else:
|
||||
msg = (
|
||||
"Model class %s.%s doesn't declare an explicit app_label "
|
||||
"and either isn't in an application in INSTALLED_APPS or "
|
||||
"else was imported before its application was loaded. " %
|
||||
(module, name))
|
||||
raise RuntimeError(msg)
|
||||
|
||||
new_class.add_to_class('_meta', Options(meta, **kwargs))
|
||||
if not abstract:
|
||||
new_class.add_to_class(
|
||||
'DoesNotExist',
|
||||
subclass_exception(
|
||||
str('DoesNotExist'),
|
||||
tuple(x.DoesNotExist for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (ObjectDoesNotExist,),
|
||||
module,
|
||||
attached_to=new_class))
|
||||
new_class.add_to_class(
|
||||
'MultipleObjectsReturned',
|
||||
subclass_exception(
|
||||
str('MultipleObjectsReturned'),
|
||||
tuple(x.MultipleObjectsReturned for x in parents if hasattr(x, '_meta') and not x._meta.abstract) or (MultipleObjectsReturned,),
|
||||
module,
|
||||
attached_to=new_class))
|
||||
if base_meta and not base_meta.abstract:
|
||||
# Non-abstract child classes inherit some attributes from their
|
||||
# non-abstract parent (unless an ABC comes before it in the
|
||||
# method resolution order).
|
||||
if not hasattr(meta, 'ordering'):
|
||||
new_class._meta.ordering = base_meta.ordering
|
||||
if not hasattr(meta, 'get_latest_by'):
|
||||
new_class._meta.get_latest_by = base_meta.get_latest_by
|
||||
|
||||
is_proxy = new_class._meta.proxy
|
||||
|
||||
# If the model is a proxy, ensure that the base class
|
||||
# hasn't been swapped out.
|
||||
if is_proxy and base_meta and base_meta.swapped:
|
||||
raise TypeError("%s cannot proxy the swapped model '%s'." % (name, base_meta.swapped))
|
||||
|
||||
if getattr(new_class, '_default_manager', None):
|
||||
if not is_proxy:
|
||||
# Multi-table inheritance doesn't inherit default manager from
|
||||
# parents.
|
||||
new_class._default_manager = None
|
||||
new_class._base_manager = None
|
||||
else:
|
||||
# Proxy classes do inherit parent's default manager, if none is
|
||||
# set explicitly.
|
||||
new_class._default_manager = new_class._default_manager._copy_to_model(new_class)
|
||||
new_class._base_manager = new_class._base_manager._copy_to_model(new_class)
|
||||
|
||||
# Add all attributes to the class.
|
||||
for obj_name, obj in attrs.items():
|
||||
new_class.add_to_class(obj_name, obj)
|
||||
|
||||
# All the fields of any type declared on this model
|
||||
new_fields = (
|
||||
new_class._meta.local_fields +
|
||||
new_class._meta.local_many_to_many +
|
||||
new_class._meta.virtual_fields
|
||||
)
|
||||
field_names = set(f.name for f in new_fields)
|
||||
|
||||
# Basic setup for proxy models.
|
||||
if is_proxy:
|
||||
base = None
|
||||
for parent in [kls for kls in parents if hasattr(kls, '_meta')]:
|
||||
if parent._meta.abstract:
|
||||
if parent._meta.fields:
|
||||
raise TypeError("Abstract base class containing model fields not permitted for proxy model '%s'." % name)
|
||||
else:
|
||||
continue
|
||||
#if base is not None: # patch
|
||||
while parent._meta.proxy: # patch
|
||||
parent = parent._meta.proxy_for_model # patch
|
||||
if base is not None and base is not parent: # patch
|
||||
raise TypeError("Proxy model '%s' has more than one non-abstract model base class." % name)
|
||||
else:
|
||||
base = parent
|
||||
if base is None:
|
||||
raise TypeError("Proxy model '%s' has no non-abstract model base class." % name)
|
||||
new_class._meta.setup_proxy(base)
|
||||
new_class._meta.concrete_model = base._meta.concrete_model
|
||||
else:
|
||||
new_class._meta.concrete_model = new_class
|
||||
|
||||
# Collect the parent links for multi-table inheritance.
|
||||
parent_links = {}
|
||||
for base in reversed([new_class] + parents):
|
||||
# Conceptually equivalent to `if base is Model`.
|
||||
if not hasattr(base, '_meta'):
|
||||
continue
|
||||
# Skip concrete parent classes.
|
||||
if base != new_class and not base._meta.abstract:
|
||||
continue
|
||||
# Locate OneToOneField instances.
|
||||
for field in base._meta.local_fields:
|
||||
if isinstance(field, OneToOneField):
|
||||
parent_links[field.rel.to] = field
|
||||
|
||||
# Do the appropriate setup for any model parents.
|
||||
for base in parents:
|
||||
original_base = base
|
||||
if not hasattr(base, '_meta'):
|
||||
# Things without _meta aren't functional models, so they're
|
||||
# uninteresting parents.
|
||||
continue
|
||||
|
||||
parent_fields = base._meta.local_fields + base._meta.local_many_to_many
|
||||
# Check for clashes between locally declared fields and those
|
||||
# on the base classes (we cannot handle shadowed fields at the
|
||||
# moment).
|
||||
for field in parent_fields:
|
||||
if field.name in field_names:
|
||||
raise FieldError(
|
||||
'Local field %r in class %r clashes '
|
||||
'with field of similar name from '
|
||||
'base class %r' % (field.name, name, base.__name__)
|
||||
)
|
||||
if not base._meta.abstract:
|
||||
# Concrete classes...
|
||||
base = base._meta.concrete_model
|
||||
if base in parent_links:
|
||||
field = parent_links[base]
|
||||
elif not is_proxy:
|
||||
attr_name = '%s_ptr' % base._meta.model_name
|
||||
field = OneToOneField(base, name=attr_name,
|
||||
auto_created=True, parent_link=True)
|
||||
# Only add the ptr field if it's not already present;
|
||||
# e.g. migrations will already have it specified
|
||||
if not hasattr(new_class, attr_name):
|
||||
new_class.add_to_class(attr_name, field)
|
||||
else:
|
||||
field = None
|
||||
new_class._meta.parents[base] = field
|
||||
else:
|
||||
# .. and abstract ones.
|
||||
for field in parent_fields:
|
||||
new_class.add_to_class(field.name, copy.deepcopy(field))
|
||||
|
||||
# Pass any non-abstract parent classes onto child.
|
||||
new_class._meta.parents.update(base._meta.parents)
|
||||
|
||||
# Inherit managers from the abstract base classes.
|
||||
new_class.copy_managers(base._meta.abstract_managers)
|
||||
|
||||
# Proxy models inherit the non-abstract managers from their base,
|
||||
# unless they have redefined any of them.
|
||||
if is_proxy:
|
||||
new_class.copy_managers(original_base._meta.concrete_managers)
|
||||
|
||||
# Inherit virtual fields (like GenericForeignKey) from the parent
|
||||
# class
|
||||
for field in base._meta.virtual_fields:
|
||||
if base._meta.abstract and field.name in field_names:
|
||||
raise FieldError(
|
||||
'Local field %r in class %r clashes '
|
||||
'with field of similar name from '
|
||||
'abstract base class %r' % (field.name, name, base.__name__)
|
||||
)
|
||||
new_class.add_to_class(field.name, copy.deepcopy(field))
|
||||
|
||||
if abstract:
|
||||
# Abstract base models can't be instantiated and don't appear in
|
||||
# the list of models for an app. We do the final setup for them a
|
||||
# little differently from normal models.
|
||||
attr_meta.abstract = False
|
||||
new_class.Meta = attr_meta
|
||||
return new_class
|
||||
|
||||
new_class._prepare()
|
||||
new_class._meta.apps.register_model(new_class._meta.app_label, new_class)
|
||||
|
||||
return new_class
|
||||
|
|
@ -29,6 +29,7 @@ from builtins import object
|
|||
|
||||
from django.db.models import signals
|
||||
|
||||
from django.db.models.base import ModelBase
|
||||
from django.db import models
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.conf import settings
|
||||
|
|
@ -94,11 +95,7 @@ class TypeclassBase(SharedMemoryModelBase):
|
|||
attrs["Meta"] = Meta
|
||||
attrs["Meta"].proxy = True
|
||||
|
||||
# patch for django proxy multi-inheritance
|
||||
# this is a copy of django.db.models.base.__new__
|
||||
# with a few lines changed as per
|
||||
# https://code.djangoproject.com/ticket/11560
|
||||
new_class = patched_new(cls, name, bases, attrs)
|
||||
new_class = ModelBase.__new__(cls, name, bases, attrs)
|
||||
|
||||
# attach signals
|
||||
signals.post_save.connect(post_save, sender=new_class)
|
||||
|
|
@ -175,9 +172,9 @@ class TypedObject(SharedMemoryModel):
|
|||
db_lock_storage = models.TextField('locks', blank=True,
|
||||
help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.")
|
||||
# many2many relationships
|
||||
db_attributes = models.ManyToManyField(Attribute, null=True,
|
||||
db_attributes = models.ManyToManyField(Attribute,
|
||||
help_text='attributes on this object. An attribute can hold any pickle-able python object (see docs for special cases).')
|
||||
db_tags = models.ManyToManyField(Tag, null=True,
|
||||
db_tags = models.ManyToManyField(Tag,
|
||||
help_text='tags on this object. Tags are simple string markers to identify, group and alias objects.')
|
||||
|
||||
# Database manager
|
||||
|
|
@ -202,7 +199,7 @@ class TypedObject(SharedMemoryModel):
|
|||
self.__class__ = class_from_module(self.__defaultclasspath__)
|
||||
except Exception:
|
||||
log_trace()
|
||||
self.__class__ = self._meta.proxy_for_model or self.__class__
|
||||
self.__class__ = self._meta.concrete_model or self.__class__
|
||||
finally:
|
||||
self.db_typeclass_path = typeclass_path
|
||||
elif self.db_typeclass_path:
|
||||
|
|
@ -214,12 +211,12 @@ class TypedObject(SharedMemoryModel):
|
|||
self.__class__ = class_from_module(self.__defaultclasspath__)
|
||||
except Exception:
|
||||
log_trace()
|
||||
self.__dbclass__ = self._meta.proxy_for_model or self.__class__
|
||||
self.__dbclass__ = self._meta.concrete_model or self.__class__
|
||||
else:
|
||||
self.db_typeclass_path = "%s.%s" % (self.__module__, self.__class__.__name__)
|
||||
# important to put this at the end since _meta is based on the set __class__
|
||||
try:
|
||||
self.__dbclass__ = self._meta.proxy_for_model or self.__class__
|
||||
self.__dbclass__ = self._meta.concrete_model or self.__class__
|
||||
except AttributeError:
|
||||
err_class = repr(self.__class__)
|
||||
self.__class__ = class_from_module("evennia.objects.objects.DefaultObject")
|
||||
|
|
|
|||
|
|
@ -49,6 +49,7 @@ import re
|
|||
from django.conf import settings
|
||||
from evennia import Command, CmdSet
|
||||
from evennia.utils import is_iter, fill, dedent, logger, justify, to_str
|
||||
from evennia.utils.ansi import raw
|
||||
from evennia.commands import cmdhandler
|
||||
|
||||
# we use cmdhandler instead of evennia.syscmdkeys to
|
||||
|
|
@ -274,7 +275,7 @@ class CmdEditorBase(Command):
|
|||
|
||||
lstart, lend = cline, cline + 1
|
||||
linerange = False
|
||||
if arglist and ':' in arglist[0]:
|
||||
if arglist and arglist[0].count(':') == 1:
|
||||
part1, part2 = arglist[0].split(':')
|
||||
if part1 and part1.isdigit():
|
||||
lstart = min(max(0, int(part1)) - 1, nlines)
|
||||
|
|
@ -377,9 +378,9 @@ class CmdLineInput(CmdEditorBase):
|
|||
indent = "off"
|
||||
|
||||
self.caller.msg("|b%02i|||n (|g%s|n) %s" % (
|
||||
cline, indent, line))
|
||||
cline, indent, raw(line)))
|
||||
else:
|
||||
self.caller.msg("|b%02i|||n %s" % (cline, self.args))
|
||||
self.caller.msg("|b%02i|||n %s" % (cline, raw(self.args)))
|
||||
|
||||
|
||||
class CmdEditorGroup(CmdEditorBase):
|
||||
|
|
@ -425,7 +426,7 @@ class CmdEditorGroup(CmdEditorBase):
|
|||
editor.display_buffer(linenums=False, options={"raw": True})
|
||||
elif cmd == ":::":
|
||||
# Insert single colon alone on a line
|
||||
editor.update_buffer(editor.buffer + "\n:")
|
||||
editor.update_buffer([":"] if lstart == 0 else linebuffer + [":"])
|
||||
if echo_mode:
|
||||
caller.msg("Single ':' added to buffer.")
|
||||
elif cmd == ":h":
|
||||
|
|
@ -932,9 +933,9 @@ class EvEditor(object):
|
|||
footer = "|n" + sep * 10 +\
|
||||
"[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) + sep * 12 + "(:h for help)" + sep * 28
|
||||
if linenums:
|
||||
main = "\n".join("|b%02i|||n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))
|
||||
main = "\n".join("|b%02i|||n %s" % (iline + 1 + offset, raw(line)) for iline, line in enumerate(lines))
|
||||
else:
|
||||
main = "\n".join(lines)
|
||||
main = "\n".join([raw(line) for line in lines])
|
||||
string = "%s\n%s\n%s" % (header, main, footer)
|
||||
self._caller.msg(string, options=options)
|
||||
|
||||
|
|
|
|||
|
|
@ -881,9 +881,11 @@ class CmdGetInput(Command):
|
|||
|
||||
caller.ndb._getinput._session = self.session
|
||||
prompt = caller.ndb._getinput._prompt
|
||||
args = caller.ndb._getinput._args
|
||||
kwargs = caller.ndb._getinput._kwargs
|
||||
result = self.raw_string.strip() # we strip the ending line break caused by sending
|
||||
|
||||
ok = not callback(caller, prompt, result)
|
||||
ok = not callback(caller, prompt, result, *args, **kwargs)
|
||||
if ok:
|
||||
# only clear the state if the callback does not return
|
||||
# anything
|
||||
|
|
@ -917,7 +919,7 @@ class _Prompt(object):
|
|||
pass
|
||||
|
||||
|
||||
def get_input(caller, prompt, callback, session=None):
|
||||
def get_input(caller, prompt, callback, session=None, *args, **kwargs):
|
||||
"""
|
||||
This is a helper function for easily request input from
|
||||
the caller.
|
||||
|
|
@ -942,6 +944,13 @@ def get_input(caller, prompt, callback, session=None):
|
|||
greater than 2. The session is then updated by the
|
||||
command and is available (for example in callbacks)
|
||||
through `caller.ndb.getinput._session`.
|
||||
*args, **kwargs (optional): Extra arguments will be
|
||||
passed to the fall back function as a list 'args'
|
||||
and all keyword arguments as a dictionary 'kwargs'.
|
||||
To utilise *args and **kwargs, a value for the
|
||||
session argument must be provided (None by default)
|
||||
and the callback function must take *args and
|
||||
**kwargs as arguments.
|
||||
|
||||
Raises:
|
||||
RuntimeError: If the given callback is not callable.
|
||||
|
|
@ -961,6 +970,12 @@ def get_input(caller, prompt, callback, session=None):
|
|||
multisession modes), then it is available in the
|
||||
callback through `caller.ndb._getinput._session`.
|
||||
|
||||
Chaining get_input functions will result in the caller
|
||||
stacking ever more instances of InputCmdSets. Whilst
|
||||
they will all be cleared on concluding the get_input
|
||||
chain, EvMenu should be considered for anything beyond
|
||||
a single question.
|
||||
|
||||
"""
|
||||
if not callable(callback):
|
||||
raise RuntimeError("get_input: input callback is not callable.")
|
||||
|
|
@ -968,6 +983,8 @@ def get_input(caller, prompt, callback, session=None):
|
|||
caller.ndb._getinput._callback = callback
|
||||
caller.ndb._getinput._prompt = prompt
|
||||
caller.ndb._getinput._session = session
|
||||
caller.ndb._getinput._args = args
|
||||
caller.ndb._getinput._kwargs = kwargs
|
||||
caller.cmdset.add(InputCmdSet)
|
||||
caller.msg(prompt, session=session)
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,12 @@ class SharedMemoryManager(Manager):
|
|||
key = key[:-len('__exact')]
|
||||
if key in ('pk', self.model._meta.pk.attname):
|
||||
inst = self.model.get_cached_instance(kwargs[items[0]])
|
||||
try:
|
||||
# we got the item from cache, but if this is a fk, check it's ours
|
||||
if getattr(inst, str(self.field).split(".")[-1]) != self.instance:
|
||||
inst = None
|
||||
except Exception:
|
||||
pass
|
||||
if inst is None:
|
||||
inst = super(SharedMemoryManager, self).get(*args, **kwargs)
|
||||
return inst
|
||||
|
|
|
|||
|
|
@ -77,7 +77,7 @@ class SharedMemoryModelBase(ModelBase):
|
|||
|
||||
"""
|
||||
# the dbmodel is either the proxy base or ourselves
|
||||
dbmodel = cls._meta.proxy_for_model if cls._meta.proxy else cls
|
||||
dbmodel = cls._meta.concrete_model if cls._meta.proxy else cls
|
||||
cls.__dbclass__ = dbmodel
|
||||
if not hasattr(dbmodel, "__instance_cache__"):
|
||||
# we store __instance_cache__ only on the dbmodel base
|
||||
|
|
@ -572,7 +572,7 @@ def cache_size(mb=True):
|
|||
if not subclasses:
|
||||
num = len(submodel.get_all_cached_instances())
|
||||
numtotal[0] += num
|
||||
classdict[submodel.__name__] = num
|
||||
classdict[submodel.__dbclass__.__name__] = num
|
||||
else:
|
||||
get_recurse(subclasses)
|
||||
get_recurse(SharedMemoryModel.__subclasses__())
|
||||
|
|
|
|||
|
|
@ -19,12 +19,14 @@ import os
|
|||
import time
|
||||
from datetime import datetime
|
||||
from traceback import format_exc
|
||||
from twisted.python import log
|
||||
from twisted.python import log, logfile
|
||||
from twisted.internet.threads import deferToThread
|
||||
|
||||
|
||||
_LOGDIR = None
|
||||
_LOG_ROTATE_SIZE = None
|
||||
_TIMEZONE = None
|
||||
_CHANNEL_LOG_NUM_TAIL_LINES = None
|
||||
|
||||
|
||||
def timeformat(when=None):
|
||||
|
|
@ -153,6 +155,58 @@ log_depmsg = log_dep
|
|||
|
||||
# Arbitrary file logger
|
||||
|
||||
class EvenniaLogFile(logfile.LogFile):
|
||||
"""
|
||||
A rotating logfile based off Twisted's LogFile. It overrides
|
||||
the LogFile's rotate method in order to append some of the last
|
||||
lines of the previous log to the start of the new log, in order
|
||||
to preserve a continuous chat history for channel log files.
|
||||
"""
|
||||
# we delay import of settings to keep logger module as free
|
||||
# from django as possible.
|
||||
global _CHANNEL_LOG_NUM_TAIL_LINES
|
||||
if _CHANNEL_LOG_NUM_TAIL_LINES is None:
|
||||
from django.conf import settings
|
||||
_CHANNEL_LOG_NUM_TAIL_LINES = settings.CHANNEL_LOG_NUM_TAIL_LINES
|
||||
num_lines_to_append = _CHANNEL_LOG_NUM_TAIL_LINES
|
||||
|
||||
def rotate(self):
|
||||
"""
|
||||
Rotates our log file and appends some number of lines from
|
||||
the previous log to the start of the new one.
|
||||
"""
|
||||
append_tail = self.num_lines_to_append > 0
|
||||
if not append_tail:
|
||||
logfile.LogFile.rotate(self)
|
||||
return
|
||||
lines = tail_log_file(self.path, 0, self.num_lines_to_append)
|
||||
logfile.LogFile.rotate(self)
|
||||
for line in lines:
|
||||
self.write(line)
|
||||
|
||||
def seek(self, *args, **kwargs):
|
||||
"""
|
||||
Convenience method for accessing our _file attribute's seek method,
|
||||
which is used in tail_log_function.
|
||||
Args:
|
||||
*args: Same args as file.seek
|
||||
**kwargs: Same kwargs as file.seek
|
||||
"""
|
||||
return self._file.seek(*args, **kwargs)
|
||||
|
||||
def readlines(self, *args, **kwargs):
|
||||
"""
|
||||
Convenience method for accessing our _file attribute's readlines method,
|
||||
which is used in tail_log_function.
|
||||
Args:
|
||||
*args: same args as file.readlines
|
||||
**kwargs: same kwargs as file.readlines
|
||||
|
||||
Returns:
|
||||
lines (list): lines from our _file attribute.
|
||||
"""
|
||||
return self._file.readlines(*args, **kwargs)
|
||||
|
||||
_LOG_FILE_HANDLES = {} # holds open log handles
|
||||
|
||||
|
||||
|
|
@ -162,10 +216,13 @@ def _open_log_file(filename):
|
|||
handle. Will create a new file in the log dir if one didn't
|
||||
exist.
|
||||
"""
|
||||
global _LOG_FILE_HANDLES, _LOGDIR
|
||||
# we delay import of settings to keep logger module as free
|
||||
# from django as possible.
|
||||
global _LOG_FILE_HANDLES, _LOGDIR, _LOG_ROTATE_SIZE
|
||||
if not _LOGDIR:
|
||||
from django.conf import settings
|
||||
_LOGDIR = settings.LOG_DIR
|
||||
_LOG_ROTATE_SIZE = settings.CHANNEL_LOG_ROTATE_SIZE
|
||||
|
||||
filename = os.path.join(_LOGDIR, filename)
|
||||
if filename in _LOG_FILE_HANDLES:
|
||||
|
|
@ -173,7 +230,8 @@ def _open_log_file(filename):
|
|||
return _LOG_FILE_HANDLES[filename]
|
||||
else:
|
||||
try:
|
||||
filehandle = open(filename, "a+") # append mode + reading
|
||||
filehandle = EvenniaLogFile.fromFullPath(filename, rotateLength=_LOG_ROTATE_SIZE)
|
||||
# filehandle = open(filename, "a+") # append mode + reading
|
||||
_LOG_FILE_HANDLES[filename] = filehandle
|
||||
return filehandle
|
||||
except IOError:
|
||||
|
|
|
|||
|
|
@ -22,7 +22,7 @@ from os.path import join as osjoin
|
|||
from importlib import import_module
|
||||
from inspect import ismodule, trace, getmembers, getmodule
|
||||
from collections import defaultdict, OrderedDict
|
||||
from twisted.internet import threads, reactor
|
||||
from twisted.internet import threads, reactor, task
|
||||
from django.conf import settings
|
||||
from django.utils import timezone
|
||||
from django.utils.translation import ugettext as _
|
||||
|
|
@ -940,7 +940,7 @@ def delay(timedelay, callback, *args, **kwargs):
|
|||
specified here.
|
||||
|
||||
"""
|
||||
return reactor.callLater(timedelay, callback, *args, **kwargs)
|
||||
return task.deferLater(reactor, timedelay, callback, *args, **kwargs)
|
||||
|
||||
|
||||
_TYPECLASSMODELS = None
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
# Evennia dependencies, for Linux/Mac platforms
|
||||
|
||||
django >= 1.8, < 1.10
|
||||
django > 1.10, < 2.0
|
||||
twisted >= 16.0.0
|
||||
mock >= 1.0.1
|
||||
pillow == 2.9.0
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue