PIP packaging with setup.py, and fixes for bugs revealed by this.
This commit is contained in:
parent
42e7d9164e
commit
265f8a4e30
52 changed files with 92 additions and 37 deletions
1
evennia/VERSION.txt
Normal file
1
evennia/VERSION.txt
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.5.0
|
||||
|
|
@ -15,6 +15,7 @@ See www.evennia.com for full documentation.
|
|||
# Delayed loading of properties
|
||||
|
||||
# Typeclasses
|
||||
|
||||
DefaultPlayer = None
|
||||
DefaultGuest = None
|
||||
DefaultObject = None
|
||||
|
|
@ -61,13 +62,21 @@ spawn = None
|
|||
managers = None
|
||||
|
||||
import os
|
||||
from subprocess import check_output, CalledProcessError, STDOUT
|
||||
|
||||
__version__ = "Unknown"
|
||||
|
||||
root = os.path.dirname(os.path.abspath(__file__))
|
||||
try:
|
||||
__version__ = "Evennia"
|
||||
with os.path.join(open(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))), "VERSION.txt", 'r') as f:
|
||||
__version__ += " %s" % f.read().strip()
|
||||
except IOError:
|
||||
__version__ += " (unknown version)"
|
||||
del os
|
||||
with open(os.path.join(root, "VERSION.txt"), 'r') as f:
|
||||
__version__ = f.read().strip()
|
||||
except IOError as err:
|
||||
print err
|
||||
try:
|
||||
__version__ = "%s" % (check_output("git rev-parse --short HEAD", shell=True, cwd=root, stderr=STDOUT).strip())
|
||||
except (IOError, CalledProcessError):
|
||||
pass
|
||||
|
||||
|
||||
def init():
|
||||
"""
|
||||
|
|
|
|||
|
|
@ -242,7 +242,7 @@ class CmdBatchCommands(MuxCommand):
|
|||
except UnicodeDecodeError, err:
|
||||
caller.msg(_UTF8_ERROR % (python_path, err))
|
||||
return
|
||||
except IOError:
|
||||
except IOError as err:
|
||||
string = "'%s' not found.\nYou have to supply the python path "
|
||||
string += "of the file relative to \none of your batch-file directories (%s)."
|
||||
caller.msg(string % (python_path, ", ".join(settings.BASE_BATCHPROCESS_PATHS)))
|
||||
|
|
|
|||
49
evennia/contrib/README
Normal file
49
evennia/contrib/README
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
|
||||
'Contrib' folder
|
||||
----------------
|
||||
|
||||
This folder contains 'contributions': extra snippets of code that are
|
||||
potentially very useful for the game coder but which are considered
|
||||
too game-specific to be a part of the main Evennia game server. These
|
||||
modules are not used unless you explicitly import them. See each file
|
||||
for more detailed instructions on how to install.
|
||||
|
||||
Modules in this folder is distributed under the same licence as
|
||||
Evennia unless noted differently in the individual module.
|
||||
|
||||
If you want to edit, tweak or expand on this code you should copy the
|
||||
things you want from here into your game folder and change them there.
|
||||
|
||||
* Evennia MenuSystem (Griatch 2011) - A base set of classes and
|
||||
cmdsets for creating in-game multiple-choice menus in
|
||||
Evennia. The menu tree can be of any depth. Menu options can be
|
||||
numbered or given custom keys, and each option can execute
|
||||
code. Also contains a yes/no question generator function. This
|
||||
is intended to be used by commands and presents a y/n question
|
||||
to the user for accepting an action. Includes a simple new
|
||||
command 'menu' for testing and debugging.
|
||||
|
||||
* Evennia Line editor (Griatch 2011) - A powerful line-by-line editor
|
||||
for editing text in-game. Mimics the command names of the famous
|
||||
VI text editor. Supports undo/redo, search/replace,
|
||||
regex-searches, buffer formatting, indenting etc. It comes with
|
||||
its own help system. (Makes minute use of the MenuSystem module
|
||||
to show a y/n question if quitting without having
|
||||
saved). Includes a basic command '@edit' for activating the
|
||||
editor.
|
||||
|
||||
* Talking_NPC (Griatch 2011) - An example of a simple NPC object with
|
||||
which you can strike up a menu-driven converstaion. Uses the
|
||||
MenuSystem to allow conversation options. The npc object defines
|
||||
a command 'talk' for starting the (brief) conversation.
|
||||
|
||||
* Evennia Menu Login (Griatch 2011) - A menu-driven login screen that
|
||||
replaces the default command-based one. Uses the MenuSystem
|
||||
contrib. Does not require players to give their email and
|
||||
doesn't auto-create a Character object at first login like the
|
||||
default system does.
|
||||
|
||||
* CharGen (Griatch 2011) - A simple Character creator and selector for
|
||||
Evennia's ooc mode. Works well with the menu login contrib and
|
||||
is intended as a starting point for building a more full-featured
|
||||
character creation system.
|
||||
1
evennia/contrib/__init__.py
Normal file
1
evennia/contrib/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
761
evennia/contrib/barter.py
Normal file
761
evennia/contrib/barter.py
Normal file
|
|
@ -0,0 +1,761 @@
|
|||
"""
|
||||
Barter system
|
||||
|
||||
Evennia contribution - Griatch 2012
|
||||
|
||||
|
||||
This implements a full barter system - a way for players to safely
|
||||
trade items between each other using code rather than simple free-form
|
||||
talking. The advantage of this is increased buy/sell safety but it
|
||||
also streamlines the process and makes it faster when doing many
|
||||
transactions (since goods are automatically exchanged once both
|
||||
agree).
|
||||
|
||||
This system is primarily intended for a barter economy, but can easily
|
||||
be used in a monetary economy as well -- just let the "goods" on one
|
||||
side be coin objects (this is more flexible than a simple "buy"
|
||||
command since you can mix coins and goods in your trade).
|
||||
|
||||
In this module, a "barter" is generally referred to as a "trade".
|
||||
|
||||
|
||||
- Trade example
|
||||
|
||||
A trade (barter) action works like this: A and B are the parties.
|
||||
|
||||
1) opening a trade
|
||||
|
||||
A: trade B: Hi, I have a nice extra sword. You wanna trade?
|
||||
B sees: A says: "Hi, I have a nice extra sword. You wanna trade?"
|
||||
A wants to trade with you. Enter 'trade A <emote>' to accept.
|
||||
B: trade A: Hm, I could use a good sword ...
|
||||
A sees: B says: "Hm, I could use a good sword ...
|
||||
B accepts the trade. Use 'trade help' for aid.
|
||||
B sees: You are now trading with A. Use 'trade help' for aid.
|
||||
|
||||
2) negotiating
|
||||
|
||||
A: offer sword: This is a nice sword. I would need some rations in trade.
|
||||
B sees: A says: "This is a nice sword. I would need some rations in trade."
|
||||
[A offers Sword of might.]
|
||||
B evalute sword
|
||||
B sees: <Sword's description and possibly stats>
|
||||
B: offer ration: This is a prime ration.
|
||||
A sees: B says: "These is a prime ration."
|
||||
[B offers iron ration]
|
||||
A: say Hey, this is a nice sword, I need something more for it.
|
||||
B sees: A says: "Hey this is a nice sword, I need something more for it."
|
||||
B: offer sword,apple: Alright. I will also include a magic apple. That's my last offer.
|
||||
A sees: B says: "Alright, I will also include a magic apple. That's my last offer."
|
||||
[B offers iron ration and magic apple]
|
||||
A accept: You are killing me here, but alright.
|
||||
B sees: A says: "You are killing me here, but alright."
|
||||
[A accepts your offer. You must now also accept.]
|
||||
B accept: Good, nice making business with you.
|
||||
You accept the deal. Deal is made and goods changed hands.
|
||||
A sees: B says: "Good, nice making business with you."
|
||||
B accepts the deal. Deal is made and goods changed hands.
|
||||
|
||||
At this point the trading system is exited and the negotiated items
|
||||
are automatically exchanged between the parties. In this example B was
|
||||
the only one changing their offer, but also A could have changed their
|
||||
offer until the two parties found something they could agree on. The
|
||||
emotes are optional but useful for RP-heavy worlds.
|
||||
|
||||
- Technical info
|
||||
|
||||
The trade is implemented by use of a TradeHandler. This object is a
|
||||
common place for storing the current status of negotiations. It is
|
||||
created on the object initiating the trade, and also stored on the
|
||||
other party once that party agrees to trade. The trade request times
|
||||
out after a certain time - this is handled by a Script. Once trade
|
||||
starts, the CmdsetTrade cmdset is initiated on both parties along with
|
||||
the commands relevant for the trading.
|
||||
|
||||
- Ideas for NPC bartering:
|
||||
|
||||
This module is primarily intended for trade between two players. But
|
||||
it can also in principle be used for a player negotiating with an
|
||||
AI-controlled NPC. If the NPC uses normal commands they can use it
|
||||
directly -- but more efficient is to have the NPC object send its
|
||||
replies directly through the tradehandler to the player. One may want
|
||||
to add some functionality to the decline command, so players can
|
||||
decline specific objects in the NPC offer (decline <object>) and allow
|
||||
the AI to maybe offer something else and make it into a proper
|
||||
barter. Along with an AI that "needs" things or has some sort of
|
||||
personality in the trading, this can make bartering with NPCs at least
|
||||
moderately more interesting than just plain 'buy'.
|
||||
|
||||
- Installation:
|
||||
|
||||
Just import the CmdTrade command into (for example) the default
|
||||
cmdset. This will make the trade (or barter) command available
|
||||
in-game.
|
||||
|
||||
"""
|
||||
|
||||
from evennia import Command, Script, CmdSet
|
||||
|
||||
TRADE_TIMEOUT = 60 # timeout for B to accept trade
|
||||
|
||||
|
||||
class TradeTimeout(Script):
|
||||
"""
|
||||
This times out the trade request, in case player B did not reply in time.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"called when script is first created"
|
||||
self.key = "trade_request_timeout"
|
||||
self.desc = "times out trade requests"
|
||||
self.interval = TRADE_TIMEOUT
|
||||
self.start_delay = True
|
||||
self.repeats = 1
|
||||
self.persistent = False
|
||||
|
||||
def at_repeat(self):
|
||||
"called once"
|
||||
if self.ndb.tradeevent:
|
||||
self.obj.ndb.tradeevent.finish(force=True)
|
||||
self.obj.msg("Trade request timed out.")
|
||||
|
||||
def is_valid(self):
|
||||
"Only valid if the trade has not yet started"
|
||||
return self.obj.ndb.tradeevent and not self.obj.ndb.tradeevent.trade_started
|
||||
|
||||
|
||||
class TradeHandler(object):
|
||||
"""
|
||||
Objects of this class handles the ongoing trade, notably storing the current
|
||||
offers from each side and wether both have accepted or not.
|
||||
"""
|
||||
def __init__(self, partA, partB):
|
||||
"""
|
||||
Initializes the trade. This is called when part A tries to initiate
|
||||
a trade with part B. The trade will not start until part B repeats
|
||||
this command (B will then call the self.join() command)
|
||||
|
||||
We also store the back-reference from the respective party to
|
||||
this object.
|
||||
"""
|
||||
# parties
|
||||
self.partA = partA
|
||||
self.partB = partB
|
||||
|
||||
self.partA.cmdset.add(CmdsetTrade())
|
||||
self.trade_started = False
|
||||
self.partA.ndb.tradehandler = self
|
||||
# trade variables
|
||||
self.partA_offers = []
|
||||
self.partB_offers = []
|
||||
self.partA_accepted = False
|
||||
self.partB_accepted = False
|
||||
|
||||
def msg(self, party, string):
|
||||
"""
|
||||
Relay a message to the other party. This allows
|
||||
the calling command to not have to worry about
|
||||
which party they are in the handler.
|
||||
"""
|
||||
if self.partA == party:
|
||||
self.partB.msg(string)
|
||||
elif self.partB == party:
|
||||
self.partA.msg(string)
|
||||
else:
|
||||
# no match, relay to oneself
|
||||
self.party.msg(string)
|
||||
|
||||
def get_other(self, party):
|
||||
"Returns the other party of the trade"
|
||||
if self.partA == party:
|
||||
return self.partB
|
||||
if self.partB == party:
|
||||
return self.partA
|
||||
return None
|
||||
|
||||
def join(self, partB):
|
||||
"""
|
||||
This is used once B decides to join the trade
|
||||
"""
|
||||
print "join:", self.partB, partB, self.partB == partB, type(self.partB), type(partB)
|
||||
if self.partB == partB:
|
||||
self.partB.ndb.tradehandler = self
|
||||
self.partB.cmdset.add(CmdsetTrade())
|
||||
self.trade_started = True
|
||||
return True
|
||||
return False
|
||||
|
||||
def unjoin(self, partB):
|
||||
"""
|
||||
This is used if B decides not to join the trade
|
||||
"""
|
||||
if self.partB == partB:
|
||||
self.finish()
|
||||
return True
|
||||
return False
|
||||
|
||||
def offer(self, party, *args):
|
||||
"""
|
||||
Change the current standing offer. We leave it up to the
|
||||
command to do the actual checks that the offer consists
|
||||
of real, valid, objects.
|
||||
"""
|
||||
if self.trade_started:
|
||||
# reset accept statements whenever an offer changes
|
||||
self.partA_accepted = False
|
||||
self.partB_accepted = False
|
||||
if party == self.partA:
|
||||
self.partA_offers = list(args)
|
||||
elif party == self.partB:
|
||||
self.partB_offers = list(args)
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def list(self):
|
||||
"""
|
||||
Returns two lists of objects on offer, separated by partA/B.
|
||||
"""
|
||||
return self.partA_offers, self.partB_offers
|
||||
|
||||
def search(self, offername):
|
||||
"""
|
||||
Returns an object on offer, based on a search criterion.
|
||||
If the search criterion is an integer, treat it as an
|
||||
index to return in the list of offered items
|
||||
"""
|
||||
all_offers = self.partA_offers + self.partB_offers
|
||||
if isinstance(offername, int):
|
||||
# an index to return
|
||||
if 0 <= offername < len(all_offers):
|
||||
return all_offers[offername]
|
||||
|
||||
all_keys = [offer.key for offer in all_offers]
|
||||
try:
|
||||
imatch = all_keys.index(offername)
|
||||
return all_offers[imatch]
|
||||
except ValueError:
|
||||
for offer in all_offers:
|
||||
if offername in offer.aliases:
|
||||
return offer
|
||||
return None
|
||||
|
||||
def accept(self, party):
|
||||
"""
|
||||
Accept the current offer.
|
||||
|
||||
Returns True if this closes the deal, False otherwise
|
||||
"""
|
||||
if self.trade_started:
|
||||
if party == self.partA:
|
||||
self.partA_accepted = True
|
||||
elif party == self.partB:
|
||||
self.partB_accepted = True
|
||||
else:
|
||||
raise ValueError
|
||||
return self.finish() # try to close the deal
|
||||
|
||||
def decline(self, party):
|
||||
"""
|
||||
Remove an previously accepted status (changing ones mind)
|
||||
|
||||
returns True if there was really a status to change, False otherwise.
|
||||
"""
|
||||
if self.trade_started:
|
||||
if party == self.partA:
|
||||
if self.partA_accepted:
|
||||
self.partA_accepted = False
|
||||
return True
|
||||
return False
|
||||
elif party == self.partB:
|
||||
if self.partB_accepted:
|
||||
self.partB_accepted = False
|
||||
return True
|
||||
return False
|
||||
else:
|
||||
raise ValueError
|
||||
|
||||
def finish(self, force=False):
|
||||
"""
|
||||
Conclude trade - move all offers and clean up
|
||||
"""
|
||||
fin = False
|
||||
if self.trade_started and self.partA_accepted and self.partB_accepted:
|
||||
# both accepted - move objects before cleanup
|
||||
for obj in self.partA_offers:
|
||||
obj.location = self.partB
|
||||
for obj in self.partB_offers:
|
||||
obj.location = self.partA
|
||||
fin = True
|
||||
if fin or force:
|
||||
# cleanup
|
||||
self.partA.cmdset.delete("cmdset_trade")
|
||||
self.partB.cmdset.delete("cmdset_trade")
|
||||
self.partA_offers = None
|
||||
self.partB_offers = None
|
||||
# this will kill it also from partB
|
||||
del self.partA.ndb.tradehandler
|
||||
if self.partB.ndb.tradehandler:
|
||||
del self.partB.ndb.tradehandler
|
||||
return True
|
||||
|
||||
|
||||
# trading commands (will go into CmdsetTrade, initialized by the
|
||||
# CmdTrade command further down).
|
||||
|
||||
class CmdTradeBase(Command):
|
||||
"""
|
||||
Base command for Trade commands to inherit from. Implements
|
||||
the custom parsing.
|
||||
"""
|
||||
def parse(self):
|
||||
"""
|
||||
Parse the relevant parts and make it easily
|
||||
available to the command
|
||||
"""
|
||||
self.args = self.args.strip()
|
||||
self.tradehandler = self.caller.ndb.tradehandler
|
||||
self.partA = self.tradehandler.partA
|
||||
self.partB = self.tradehandler.partB
|
||||
|
||||
self.other = self.tradehandler.get_other(self.caller)
|
||||
self.msg_other = self.tradehandler.msg
|
||||
|
||||
self.trade_started = self.tradehandler.trade_started
|
||||
self.emote = ""
|
||||
self.str_caller = "Your trade action: %s"
|
||||
self.str_other = "%s:s trade action: " % self.caller.key + "%s"
|
||||
if ':' in self.args:
|
||||
self.args, self.emote = [part.strip() for part in self.args.rsplit(":", 1)]
|
||||
self.str_caller = 'You say, "' + self.emote + '"\n [%s]'
|
||||
if self.caller.has_player:
|
||||
self.str_other = '{c%s{n says, "' % self.caller.key + self.emote + '"\n [%s]'
|
||||
else:
|
||||
self.str_other = '%s says, "' % self.caller.key + self.emote + '"\n [%s]'
|
||||
|
||||
|
||||
# trade help
|
||||
|
||||
class CmdTradeHelp(CmdTradeBase):
|
||||
"""
|
||||
help command for the trade system.
|
||||
|
||||
Usage:
|
||||
trade help
|
||||
|
||||
Displays help for the trade commands.
|
||||
"""
|
||||
key = "trade help"
|
||||
#aliases = ["trade help"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trade"
|
||||
|
||||
def func(self):
|
||||
"Show the help"
|
||||
string = """
|
||||
Trading commands
|
||||
|
||||
{woffer <objects> [:emote]{n
|
||||
offer one or more objects for trade. The emote can be used for
|
||||
RP/arguments. A new offer will require both parties to re-accept
|
||||
it again.
|
||||
{waccept [:emote]{n
|
||||
accept the currently standing offer from both sides. Also 'agree'
|
||||
works. Once both have accepted, the deal is finished and goods
|
||||
will change hands.
|
||||
{wdecline [:emote]{n
|
||||
change your mind and remove a previous accept (until other
|
||||
has also accepted)
|
||||
{wstatus{n
|
||||
show the current offers on each side of the deal. Also 'offers'
|
||||
and 'deal' works.
|
||||
{wevaluate <nr> or <offer>{n
|
||||
examine any offer in the deal. List them with the 'status' command.
|
||||
{wend trade{n
|
||||
end the negotiations prematurely. No trade will take place.
|
||||
|
||||
You can also use {wemote{n, {wsay{n etc to discuss
|
||||
without making a decision or offer.
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
# offer
|
||||
|
||||
class CmdOffer(CmdTradeBase):
|
||||
"""
|
||||
offer one or more items in trade.
|
||||
|
||||
Usage:
|
||||
offer <object> [, object2, ...][:emote]
|
||||
|
||||
Offer objects in trade. This will replace the currently
|
||||
standing offer.
|
||||
"""
|
||||
key = "offer"
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"implement the offer"
|
||||
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: offer <object> [, object2, ...] [:emote]")
|
||||
return
|
||||
if not self.trade_started:
|
||||
caller.msg("Wait until the other party has accepted to trade with you.")
|
||||
return
|
||||
|
||||
# gather all offers
|
||||
offers = [part.strip() for part in self.args.split(',')]
|
||||
offerobjs = []
|
||||
for offername in offers:
|
||||
obj = caller.search(offername)
|
||||
if not obj:
|
||||
return
|
||||
offerobjs.append(obj)
|
||||
self.tradehandler.offer(self.caller, *offerobjs)
|
||||
|
||||
# output
|
||||
if len(offerobjs) > 1:
|
||||
objnames = ", ".join("{w%s{n" % obj.key for obj in offerobjs[:-1]) + " and {w%s{n" % offerobjs[-1].key
|
||||
else:
|
||||
objnames = "{w%s{n" % offerobjs[0].key
|
||||
|
||||
caller.msg(self.str_caller % ("You offer %s" % objnames))
|
||||
self.msg_other(caller, self.str_other % ("They offer %s" % objnames))
|
||||
|
||||
|
||||
# accept
|
||||
|
||||
class CmdAccept(CmdTradeBase):
|
||||
"""
|
||||
accept the standing offer
|
||||
|
||||
Usage:
|
||||
accept [:emote]
|
||||
agreee [:emote]
|
||||
|
||||
This will accept the current offer. The other party must also accept
|
||||
for the deal to go through. You can use the 'decline' command to change
|
||||
your mind as long as the other party has not yet accepted. You can inspect
|
||||
the current offer using the 'offers' command.
|
||||
"""
|
||||
key = "accept"
|
||||
aliases = ["agree"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"accept the offer"
|
||||
caller = self.caller
|
||||
if not self.trade_started:
|
||||
caller.msg("Wait until the other party has accepted to trade with you.")
|
||||
return
|
||||
if self.tradehandler.accept(self.caller):
|
||||
# deal finished. Trade ended and cleaned.
|
||||
caller.msg(self.str_caller % "You {gaccept{n the deal. {gDeal is made and goods changed hands.{n")
|
||||
self.msg_other(caller, self.str_other % "%s {gaccepts{n the deal. {gDeal is made and goods changed hands.{n" % caller.key)
|
||||
else:
|
||||
# a one-sided accept.
|
||||
caller.msg(self.str_caller % "You {Gaccept{n the offer. %s must now also accept." % self.other.key)
|
||||
self.msg_other(caller, self.str_other % "%s {Gaccepts{n the offer. You must now also accept." % caller.key)
|
||||
|
||||
|
||||
# decline
|
||||
|
||||
class CmdDecline(CmdTradeBase):
|
||||
"""
|
||||
decline the standing offer
|
||||
|
||||
Usage:
|
||||
decline [:emote]
|
||||
|
||||
This will decline a previously 'accept'ed offer (so this allows you to
|
||||
change your mind). You can only use this as long as the other party
|
||||
has not yet accepted the deal. Also, changing the offer will automatically
|
||||
decline the old offer.
|
||||
"""
|
||||
key = "decline"
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"decline the offer"
|
||||
caller = self.caller
|
||||
if not self.trade_started:
|
||||
caller.msg("Wait until the other party has accepted to trade with you.")
|
||||
return
|
||||
offerA, offerB = self.tradehandler.list()
|
||||
if not offerA or not offerB:
|
||||
caller.msg("Noone has offered anything (yet) so there is nothing to decline.")
|
||||
return
|
||||
if self.tradehandler.decline(self.caller):
|
||||
# changed a previous accept
|
||||
caller.msg(self.str_caller % "You change your mind, {Rdeclining{n the current offer.")
|
||||
self.msg_other(caller, self.str_other % "%s changes their mind, {Rdeclining{n the current offer." % caller.key)
|
||||
else:
|
||||
# no acceptance to change
|
||||
caller.msg(self.str_caller % "You {Rdecline{n the current offer.")
|
||||
self.msg_other(caller, self.str_other % "%s declines the current offer." % caller.key)
|
||||
|
||||
|
||||
# evaluate
|
||||
|
||||
# Note: This version only shows the description. If your particular game
|
||||
# lists other important properties of objects (such as weapon damage, weight,
|
||||
# magical properties, ammo requirements or whatnot), then you need to add this
|
||||
# here.
|
||||
|
||||
class CmdEvaluate(CmdTradeBase):
|
||||
"""
|
||||
evaluate objects on offer
|
||||
|
||||
Usage:
|
||||
evaluate <offered object>
|
||||
|
||||
This allows you to examine any object currently on offer, to
|
||||
determine if it's worth your while.
|
||||
"""
|
||||
key = "evaluate"
|
||||
aliases = ["eval"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"evaluate an object"
|
||||
caller = self.caller
|
||||
if not self.args:
|
||||
caller.msg("Usage: evaluate <offered object>")
|
||||
return
|
||||
# we also accept indices
|
||||
try:
|
||||
ind = int(self.args)
|
||||
self.args = ind - 1
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
offer = self.tradehandler.search(self.args)
|
||||
if not offer:
|
||||
caller.msg("No offer matching '%s' was found." % self.args)
|
||||
return
|
||||
# show the description
|
||||
caller.msg(offer.db.desc)
|
||||
|
||||
|
||||
# status
|
||||
|
||||
class CmdStatus(CmdTradeBase):
|
||||
"""
|
||||
show a list of the current deal
|
||||
|
||||
Usage:
|
||||
status
|
||||
deal
|
||||
offers
|
||||
|
||||
Shows the currently suggested offers on each sides of the deal. To
|
||||
accept the current deal, use the 'accept' command. Use 'offer' to
|
||||
change your deal. You might also want to use 'say', 'emote' etc to
|
||||
try to influence the other part in the deal.
|
||||
"""
|
||||
key = "status"
|
||||
aliases = ["offers", "deal"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"Show the current deal"
|
||||
caller = self.caller
|
||||
partA_offers, partB_offers = self.tradehandler.list()
|
||||
count = 1
|
||||
partA_offerlist = ""
|
||||
for offer in partA_offers:
|
||||
partA_offerlist += "\n {w%i{n %s" % (count, offer.key)
|
||||
count += 1
|
||||
if not partA_offerlist:
|
||||
partA_offerlist = "\n <nothing>"
|
||||
partB_offerlist = ""
|
||||
for offer in partB_offers:
|
||||
partB_offerlist += "\n {w%i{n %s" % (count, offer.key)
|
||||
count += 1
|
||||
if not partB_offerlist:
|
||||
partB_offerlist = "\n <nothing>"
|
||||
|
||||
string = "{gOffered by %s:{n%s\n{yOffered by %s:{n%s" % (self.partA.key,
|
||||
partA_offerlist,
|
||||
self.partB.key,
|
||||
partB_offerlist)
|
||||
acceptA = self.tradehandler.partA_accepted and "{gYes{n" or "{rNo{n"
|
||||
acceptB = self.tradehandler.partB_accepted and "{gYes{n" or "{rNo{n"
|
||||
string += "\n\n%s agreed: %s, %s agreed: %s" % \
|
||||
(self.partA.key, acceptA, self.partB.key, acceptB)
|
||||
string += "\n Use 'offer', 'eval' and 'accept'/'decline' to trade. See also 'trade help'."
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
# finish
|
||||
|
||||
class CmdFinish(CmdTradeBase):
|
||||
"""
|
||||
end the trade prematurely
|
||||
|
||||
Usage:
|
||||
end trade [:say]
|
||||
finish trade [:say]
|
||||
|
||||
This ends the trade prematurely. No trade will take place.
|
||||
|
||||
"""
|
||||
key = "end trade"
|
||||
aliases = "finish trade"
|
||||
locks = "cmd:all()"
|
||||
help_category = "Trading"
|
||||
|
||||
def func(self):
|
||||
"end trade"
|
||||
caller = self.caller
|
||||
self.tradehandler.finish(force=True)
|
||||
caller.msg(self.str_caller % "You {raborted{n trade. No deal was made.")
|
||||
self.msg_other(caller, self.str_other % "%s {raborted{n trade. No deal was made." % caller.key)
|
||||
|
||||
|
||||
# custom Trading cmdset
|
||||
|
||||
class CmdsetTrade(CmdSet):
|
||||
"""
|
||||
This cmdset is added when trade is initated. It is handled by the
|
||||
trade event handler.
|
||||
"""
|
||||
key = "cmdset_trade"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Called when cmdset is created"
|
||||
self.add(CmdTradeHelp())
|
||||
self.add(CmdOffer())
|
||||
self.add(CmdAccept())
|
||||
self.add(CmdDecline())
|
||||
self.add(CmdEvaluate())
|
||||
self.add(CmdStatus())
|
||||
self.add(CmdFinish())
|
||||
|
||||
|
||||
# access command - once both have given this, this will create the
|
||||
# trading cmdset to start trade.
|
||||
|
||||
class CmdTrade(Command):
|
||||
"""
|
||||
Initiate trade with another party
|
||||
|
||||
Usage:
|
||||
trade <other party> [:say]
|
||||
trade <other party> accept [:say]
|
||||
trade <other party> decline [:say]
|
||||
|
||||
Initiate trade with another party. The other party needs to repeat
|
||||
this command with trade accept/decline within a minute in order to
|
||||
properly initiate the trade action. You can use the decline option
|
||||
yourself if you want to retract an already suggested trade. The
|
||||
optional say part works like the say command and allows you to add
|
||||
info to your choice.
|
||||
"""
|
||||
key = "trade"
|
||||
aliases = ["barter"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Initiate trade"
|
||||
|
||||
if not self.args:
|
||||
if self.caller.ndb.tradehandler and self.caller.ndb.tradeevent.trade_started:
|
||||
self.caller.msg("You are already in a trade. Use 'end trade' to abort it.")
|
||||
else:
|
||||
self.caller.msg("Usage: trade <other party> [accept|decline] [:emote]")
|
||||
return
|
||||
self.args = self.args.strip()
|
||||
|
||||
# handle the emote manually here
|
||||
selfemote = ""
|
||||
theiremote = ""
|
||||
if ':' in self.args:
|
||||
self.args, emote = [part.strip() for part in self.args.rsplit(":", 1)]
|
||||
selfemote = 'You say, "%s"\n ' % emote
|
||||
if self.caller.has_player:
|
||||
theiremote = '{c%s{n says, "%s"\n ' % (self.caller.key, emote)
|
||||
else:
|
||||
theiremote = '%s says, "%s"\n ' % (self.caller.key, emote)
|
||||
|
||||
# for the sake of this command, the caller is always partA; this
|
||||
# might not match the actual name in tradehandler (in the case of
|
||||
# using this command to accept/decline a trade invitation).
|
||||
partA = self.caller
|
||||
accept = 'accept' in self.args
|
||||
decline = 'decline' in self.args
|
||||
if accept:
|
||||
partB = self.args.rstrip('accept').strip()
|
||||
elif decline:
|
||||
partB = self.args.rstrip('decline').strip()
|
||||
else:
|
||||
partB = self.args
|
||||
partB = self.caller.search(partB)
|
||||
if not partB:
|
||||
return
|
||||
if partA == partB:
|
||||
partA.msg("You play trader with yourself.")
|
||||
return
|
||||
|
||||
# messages
|
||||
str_initA = "You ask to trade with %s. They need to accept within %s secs."
|
||||
str_initB = "%s wants to trade with you. Use {wtrade %s accept/decline [:emote]{n to answer (within %s secs)."
|
||||
str_noinitA = "%s declines the trade"
|
||||
str_noinitB = "You decline trade with %s."
|
||||
str_startA = "%s starts to trade with you. See {wtrade help{n for aid."
|
||||
str_startB = "You start to trade with %s. See {wtrade help{n for aid."
|
||||
|
||||
if not (accept or decline):
|
||||
# initialization of trade
|
||||
if self.caller.ndb.tradehandler:
|
||||
# trying to start trade without stopping a previous one
|
||||
if self.caller.ndb.tradehandler.trade_started:
|
||||
string = "You are already in trade with %s. You need to end trade first."
|
||||
else:
|
||||
string = "You are already trying to initiate trade with %s. You need to decline that trade first."
|
||||
self.caller.msg(string % partB.key)
|
||||
elif partB.ndb.tradehandler and partB.ndb.tradehandler.partB == partA:
|
||||
# this is equivalent to partA accepting a trade from partB (so roles are reversed)
|
||||
partB.ndb.tradehandler.join(partA)
|
||||
partB.msg(theiremote + str_startA % partA.key)
|
||||
partA.msg(selfemote + str_startB % (partB.key))
|
||||
else:
|
||||
# initiate a new trade
|
||||
TradeHandler(partA, partB)
|
||||
partA.msg(selfemote + str_initA % (partB.key, TRADE_TIMEOUT))
|
||||
partB.msg(theiremote + str_initB % (partA.key, partA.key, TRADE_TIMEOUT))
|
||||
partA.scripts.add(TradeTimeout)
|
||||
return
|
||||
elif accept:
|
||||
# accept a trade proposal from partB (so roles are reversed)
|
||||
if partA.ndb.tradehandler:
|
||||
# already in a trade
|
||||
partA.msg("You are already in trade with %s. You need to end that first." % partB.key)
|
||||
return
|
||||
if partB.ndb.tradehandler.join(partA):
|
||||
partB.msg(theiremote + str_startA % partA.key)
|
||||
partA.msg(selfemote + str_startB % partB.key)
|
||||
else:
|
||||
partA.msg("No trade proposal to accept.")
|
||||
return
|
||||
else:
|
||||
# decline trade proposal from partB (so roles are reversed)
|
||||
if partA.ndb.tradehandler and partA.ndb.tradehandler.partB == partA:
|
||||
# stopping an invite
|
||||
partA.ndb.tradehandler.finish(force=True)
|
||||
partB.msg(theiremote + "%s aborted trade attempt with you." % partA)
|
||||
partA.msg(selfemote + "You aborted the trade attempt with %s." % partB)
|
||||
elif partB.ndb.tradehandler and partB.ndb.tradehandler.unjoin(partA):
|
||||
partB.msg(theiremote + str_noinitA % partA.key)
|
||||
partA.msg(selfemote + str_noinitB % partB.key)
|
||||
else:
|
||||
partA.msg("No trade proposal to decline.")
|
||||
return
|
||||
|
||||
171
evennia/contrib/battle_for_evennia/README
Normal file
171
evennia/contrib/battle_for_evennia/README
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
|
||||
Battle for Evennia
|
||||
------------------
|
||||
|
||||
Evennia contrib - Griatch 2012 (WORK IN PROGRESS)
|
||||
|
||||
This is the beginnings of what will be a tutorial for building a
|
||||
simple yet still reasonably playable and not-quite-bog-standard
|
||||
starting game in Evennia. The tutorial text itself will eventually be
|
||||
found from the Dev blog and from the wiki.
|
||||
|
||||
|
||||
Ideas & Initial Brainstorm
|
||||
---------------------------
|
||||
|
||||
This is to be a hack&slash game. Characters fight mobiles and each
|
||||
other for random loot and better weapons. The highscore is based on
|
||||
most accumulated gold. They can sell loot to NPC merchants for gold,
|
||||
and also buy stuff others sold there (spending gold). Characters get
|
||||
better in the skills they use (no levels). They automatically collect
|
||||
loot when they kill things, and they cannot drop it (but they can give
|
||||
it away and, most importantly sell it). Death sends the player back to
|
||||
a starting position, but gives all but their weakest gear to their
|
||||
nemesis (they keep all their gold though).
|
||||
|
||||
Inventory of code we need:
|
||||
- Loot/Equipment lists of Weapons, Armour, Potions and Spells - maybe partly randomly generated.
|
||||
- Way to spawn in-game objects based on the loot lists
|
||||
- Character creation module (choose skills, attributes, assigns starting gear)
|
||||
- 3 Attributes, about 10 skills (some magic?)
|
||||
- Experience -> skill increase code
|
||||
- Skill success code - same between PCs as between NPC and PC
|
||||
- Combat code (twitch-based? Turn-based? Turn based seems easier to balance. Same for NPC vs PC and PC vs PC)
|
||||
- Mobile code (same for NPCs and enemies)
|
||||
- 'Give' mechanism (should require consent by receiver)
|
||||
- No quests, for simplicity. Use gold as a highscore.
|
||||
- Death respawn mechanism
|
||||
|
||||
|
||||
Elaboration based on Brainstorm
|
||||
-------------------------------
|
||||
|
||||
* Loot/Equipment lists and spawn - These could be global-level
|
||||
dictionaries in a module. Each dictionary gives info such as name,
|
||||
description and typeclass. Attributes could be set or
|
||||
randomized. The loot-spawner (probably a handler tied to a dead
|
||||
mob, treasure chest etc) would use utils.variable_from_module to
|
||||
extract a random item-template.
|
||||
|
||||
* Characters have 3 attributes: Wile, Strength and Agility. At
|
||||
creation, they distribute points between them. Wile is used for
|
||||
bartering with merchants, and using Magic. Strength determines
|
||||
hand-weapon damage and how heavy armour can be worn. Agility
|
||||
determines ability to dodge, initiative and using lighter weapons.
|
||||
Health is based on an average of all three attributes (i.e. all
|
||||
chars start with the same health).
|
||||
|
||||
* Skills are as follows (may change):
|
||||
- Long blades (str) ability to hit with swords and also axes.
|
||||
- Blunt (str) usage of blunt weapons like clubs. Good on armoured foes.
|
||||
- Spears (agi) usage of spears and hillebards. Bonus on first attack, minus on initiative.
|
||||
- Daggers (agi) usage of daggers and short blades. Bonus on initiative, bad on armour
|
||||
- Unarmoured (agi) usage of your fists and feet. Very fast. Bad on armour.
|
||||
- Dodge (agi) avoiding blows by swift footwork
|
||||
- Feint (agi) faign attacks to keep the enemy guessing
|
||||
- Shield (str) absorbing hits with a shielf
|
||||
- Platemail (str) utilizing heavy armour
|
||||
- Chainmail (max(str, agi)) utilizing medium armour
|
||||
- Leather (agi) utilizing light armour
|
||||
- Barter (wil) barter with merchants for a good price
|
||||
- Magic (wil) use of single-use magical scrolls to achieve various effects
|
||||
- Potions (wil) making the best of potions with various effects
|
||||
- Heal (wil) fixing yourself (or a friend) up between combat. Also judge opponent's wounds.
|
||||
|
||||
* Experience simply rises upon kills and is distributed between the
|
||||
skills used in the battle (so we need to log this). After N amount of
|
||||
XP in a skill, that skill automatically goes up one
|
||||
point. Increasing skills at least N points in 3+ different skills
|
||||
of a certain type (str, agi, wil) will increase the most trained
|
||||
Attribute by one point.
|
||||
|
||||
* Skill success is a comparison between the value of a random.gauss
|
||||
centered around the attacker's skill value vs the result of a
|
||||
random.gauss centered on the defender's skill. Certain
|
||||
weapons/defense combinations might be especially effective against
|
||||
one another (or not). The difference is the base damage, then
|
||||
adjusted by weapon and armour. In the case of bartering, skill
|
||||
challenge is between barter skill of both sides; difference
|
||||
influences the discount/higher price offered for selling/buying.
|
||||
|
||||
* Attacking another player or NPC will start a combat queue.
|
||||
Combat happens in turns. Each turn each player may enter two actions,
|
||||
picking among the following:
|
||||
- attack
|
||||
- parry (with weapon)
|
||||
- shield
|
||||
- feint
|
||||
- dodge
|
||||
- flee <direction>
|
||||
- block (anti-flee)
|
||||
- switch <weapon>
|
||||
Emoting is free in each round, but movement is forbidden unless one
|
||||
tries to flee (agi challenge, or cancelled by block action). All
|
||||
combattants involved in a fight submits their actions, then combat
|
||||
is resolved simultaneously by the code. Order of the two actions
|
||||
matter, so for example if both attack, neither is trying to parry,
|
||||
but may hit each other simultaneously. If both parry, shield or
|
||||
dodge, it means both are dancing about each other. If one feints
|
||||
and the other parries or dodges, they will have a disadvantage on
|
||||
the next defensive movement. A successful parry will give the
|
||||
parried attacker a disadvantage on their next attack. And so on.
|
||||
Another player may "join the queue" at any time by attacking one of
|
||||
the combatting PCs. They get to insert their actions together with
|
||||
the rest on the next round. A round should probably have a timeout
|
||||
to avoid a Character clogging the queue.
|
||||
|
||||
* Mobiles will use "a global ticker system" where they
|
||||
subscribe. They act the same way as PCs in combat, except with a
|
||||
semi- random selection of actions they take (they will probably be
|
||||
more predictable than PCs). Adding aggressive and passive mobiles
|
||||
should be straightforward, as well as un-killable ones (merchants).
|
||||
|
||||
* The inventory of a defeated enemy is automatically transferred to
|
||||
the winner's inventory. If there are many alternative pieces of
|
||||
equipment, they get to keep the weakest one, otherwise it's all
|
||||
transferred. There are no limits to carrying except the fear of
|
||||
losing gear. This should hopefully prevent hoarding of good items.
|
||||
One can give item(s) to another player - that player must then
|
||||
conceed to receiving it (use Y/N module in contrib.menusystem).
|
||||
There is no way of dropping items on the ground; one must either
|
||||
give them away or sell them for gold to a merchant.
|
||||
|
||||
* Gold is used for buying items from merchants, but is also the
|
||||
highscore. Whereas sell prices are fixed, buy prices are not fixed
|
||||
but is based on a percentage of the gold carried, adjusted by the
|
||||
barter skill (this should defeat inflation quite effectively). Items
|
||||
sold to merchants are made available for other players to buy.
|
||||
|
||||
* Death means loosing inventory (except weakest item, as mentioned),
|
||||
but no loss of gold. Otherwise death is cheap - one respawns at a
|
||||
random starting position (probably needing special-aliased rooms to
|
||||
use for this - maybe with one-way exits).
|
||||
|
||||
|
||||
Rough plan for order of implementation
|
||||
--------------------------------------
|
||||
|
||||
1) Conflict resolution system - work out how basic challenges should work, what format ingoing Skills
|
||||
should have and how generic bonuses from attributes and equipment affect things. Make a generic
|
||||
API for it. Try to list all supported plus/minues equipment may offer.
|
||||
2) Using skills - create XP and automatic skill improvement code
|
||||
3) Define new Character typeclass that stores skills/attributes in a way that the conflict
|
||||
system understands. Chars should also have the ability to "wear" things, so some
|
||||
sort of slot system is needed. Gold needs to be stored in a separate variable.
|
||||
4) Create "sell/buy" command stump, for testing the Skill resolution code with fixed on-character values.
|
||||
5) Create Combat queue code for turn-taking combat. Reiterate so that it works with the generic form of
|
||||
Skills and conflict resolution.
|
||||
6) Create all included skills and their associated commands.
|
||||
7) Test commands manually with two PC characters in the Combat queue and in other challenge situations.
|
||||
8) Create loot-creation mechanism based on equipment lists, for spawning semi-random items and gear.
|
||||
9) Create Death-respawn mechanism, including loss of equipment and transfer of same to the winner.
|
||||
10) Create NPC/mobile object runner Script. Use a copy of Character typeclass for mobiles, except some
|
||||
automation hooks and AI states. Tie loot creation to the death of NPCs.
|
||||
11) Test PC vs NPC combat and other challenges.
|
||||
12) Create merchants as interactive, immortal NPCs with the "barter" skill.
|
||||
13) Create Character creation module, for assigning attributes/skills when first starting.
|
||||
Add appropriate commands to ooccmdset.
|
||||
14) Add "give" command to command set. Remove unused commands like "drop" (or make it admin-only). Possibly
|
||||
expand "look" command to allow to look across exits into the next room. Also add "highscore" command for
|
||||
viewing game statistics.
|
||||
15) <starting building of game world>
|
||||
195
evennia/contrib/chargen.py
Normal file
195
evennia/contrib/chargen.py
Normal file
|
|
@ -0,0 +1,195 @@
|
|||
"""
|
||||
|
||||
Contribution - Griatch 2011
|
||||
|
||||
[Note - with the advent of MULTISESSION_MODE=2, this is not really
|
||||
as necessary anymore - the ooclook and @charcreate commands in that
|
||||
mode replaces this module with better functionality.]
|
||||
|
||||
This is a simple character creation commandset. A suggestion is to
|
||||
test this together with menu_login, which doesn't create a Character
|
||||
on its own. This shows some more info and gives the Player the option
|
||||
to create a character without any more customizations than their name
|
||||
(further options are unique for each game anyway).
|
||||
|
||||
Since this extends the OOC cmdset, logging in from the menu will
|
||||
automatically drop the Player into this cmdset unless they logged off
|
||||
while puppeting a Character already before.
|
||||
|
||||
Installation:
|
||||
|
||||
Read the instructions in contrib/examples/cmdset.py in
|
||||
order to create a new default cmdset module for Evennia to use (copy
|
||||
the template up one level, and change the settings file's relevant
|
||||
variables to point to the cmdsets inside). If you already have such
|
||||
a module you should of course use that.
|
||||
|
||||
Next import this module in your custom cmdset module and add the
|
||||
following line to the end of OOCCmdSet's at_cmdset_creation():
|
||||
|
||||
self.add(chargen.OOCCmdSetCharGen)
|
||||
|
||||
"""
|
||||
|
||||
from django.conf import settings
|
||||
from evennia import Command, create_object, utils
|
||||
from evennia import default_cmds, managers
|
||||
|
||||
CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
||||
class CmdOOCLook(default_cmds.CmdLook):
|
||||
"""
|
||||
ooc look
|
||||
|
||||
Usage:
|
||||
look
|
||||
look <character>
|
||||
|
||||
This is an OOC version of the look command. Since a Player doesn't
|
||||
have an in-game existence, there is no concept of location or
|
||||
"self".
|
||||
|
||||
If any characters are available for you to control, you may look
|
||||
at them with this command.
|
||||
"""
|
||||
|
||||
key = "look"
|
||||
aliases = ["l", "ls"]
|
||||
locks = "cmd:all()"
|
||||
help_cateogory = "General"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Implements the ooc look command
|
||||
|
||||
We use an attribute _character_dbrefs on the player in order
|
||||
to figure out which characters are "theirs". A drawback of this
|
||||
is that only the CmdCharacterCreate command adds this attribute,
|
||||
and thus e.g. player #1 will not be listed (although it will work).
|
||||
Existence in this list does not depend on puppeting rights though,
|
||||
that is checked by the @ic command directly.
|
||||
"""
|
||||
|
||||
# making sure caller is really a player
|
||||
self.character = None
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.Object"):
|
||||
# An object of some type is calling. Convert to player.
|
||||
#print self.caller, self.caller.__class__
|
||||
self.character = self.caller
|
||||
if hasattr(self.caller, "player"):
|
||||
self.caller = self.caller.player
|
||||
|
||||
if not self.character:
|
||||
# ooc mode, we are players
|
||||
|
||||
avail_chars = self.caller.db._character_dbrefs
|
||||
if self.args:
|
||||
# Maybe the caller wants to look at a character
|
||||
if not avail_chars:
|
||||
self.caller.msg("You have no characters to look at. Why not create one?")
|
||||
return
|
||||
objs = managers.objects.get_objs_with_key_and_typeclass(self.args.strip(), CHARACTER_TYPECLASS)
|
||||
objs = [obj for obj in objs if obj.id in avail_chars]
|
||||
if not objs:
|
||||
self.caller.msg("You cannot see this Character.")
|
||||
return
|
||||
self.caller.msg(objs[0].return_appearance(self.caller))
|
||||
return
|
||||
|
||||
# not inspecting a character. Show the OOC info.
|
||||
charobjs = []
|
||||
charnames = []
|
||||
if self.caller.db._character_dbrefs:
|
||||
dbrefs = self.caller.db._character_dbrefs
|
||||
charobjs = [managers.objects.get_id(dbref) for dbref in dbrefs]
|
||||
charnames = [charobj.key for charobj in charobjs if charobj]
|
||||
if charnames:
|
||||
charlist = "The following Character(s) are available:\n\n"
|
||||
charlist += "\n\r".join(["{w %s{n" % charname for charname in charnames])
|
||||
charlist += "\n\n Use {w@ic <character name>{n to switch to that Character."
|
||||
else:
|
||||
charlist = "You have no Characters."
|
||||
string = \
|
||||
""" You, %s, are an {wOOC ghost{n without form. The world is hidden
|
||||
from you and besides chatting on channels your options are limited.
|
||||
You need to have a Character in order to interact with the world.
|
||||
|
||||
%s
|
||||
|
||||
Use {wcreate <name>{n to create a new character and {whelp{n for a
|
||||
list of available commands.""" % (self.caller.key, charlist)
|
||||
self.caller.msg(string)
|
||||
|
||||
else:
|
||||
# not ooc mode - leave back to normal look
|
||||
# we have to put this back for normal look to work.
|
||||
self.caller = self.character
|
||||
super(CmdOOCLook, self).func()
|
||||
|
||||
|
||||
class CmdOOCCharacterCreate(Command):
|
||||
"""
|
||||
creates a character
|
||||
|
||||
Usage:
|
||||
create <character name>
|
||||
|
||||
This will create a new character, assuming
|
||||
the given character name does not already exist.
|
||||
"""
|
||||
|
||||
key = "create"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Tries to create the Character object. We also put an
|
||||
attribute on ourselves to remember it.
|
||||
"""
|
||||
|
||||
# making sure caller is really a player
|
||||
self.character = None
|
||||
if utils.inherits_from(self.caller, "evennia.objects.objects.Object"):
|
||||
# An object of some type is calling. Convert to player.
|
||||
#print self.caller, self.caller.__class__
|
||||
self.character = self.caller
|
||||
if hasattr(self.caller, "player"):
|
||||
self.caller = self.caller.player
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: create <character name>")
|
||||
return
|
||||
charname = self.args.strip()
|
||||
old_char = managers.objects.get_objs_with_key_and_typeclass(charname, CHARACTER_TYPECLASS)
|
||||
if old_char:
|
||||
self.caller.msg("Character {c%s{n already exists." % charname)
|
||||
return
|
||||
# create the character
|
||||
|
||||
new_character = create_object(CHARACTER_TYPECLASS, key=charname)
|
||||
if not new_character:
|
||||
self.caller.msg("{rThe Character couldn't be created. This is a bug. Please contact an admin.")
|
||||
return
|
||||
# make sure to lock the character to only be puppeted by this player
|
||||
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
|
||||
(new_character.id, self.caller.id))
|
||||
|
||||
# save dbref
|
||||
avail_chars = self.caller.db._character_dbrefs
|
||||
if avail_chars:
|
||||
avail_chars.append(new_character.id)
|
||||
else:
|
||||
avail_chars = [new_character.id]
|
||||
self.caller.db._character_dbrefs = avail_chars
|
||||
self.caller.msg("{gThe Character {c%s{g was successfully created!" % charname)
|
||||
|
||||
|
||||
class OOCCmdSetCharGen(default_cmds.OOCCmdSet):
|
||||
"""
|
||||
Extends the default OOC cmdset.
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
"Install everything from the default set, then overload"
|
||||
#super(OOCCmdSetCharGen, self).at_cmdset_creation()
|
||||
self.add(CmdOOCLook())
|
||||
self.add(CmdOOCCharacterCreate())
|
||||
234
evennia/contrib/dice.py
Normal file
234
evennia/contrib/dice.py
Normal file
|
|
@ -0,0 +1,234 @@
|
|||
"""
|
||||
Dice - rolls dice for roleplaying, in-game gambling or GM:ing
|
||||
|
||||
Evennia contribution - Griatch 2012
|
||||
|
||||
|
||||
This module implements a full-fledged dice-roller and a 'dice' command to
|
||||
go with it. It uses standard RPG 'd'-syntax (e.g. 2d6 to roll two
|
||||
six-sided die) and also supports modifiers such as 3d6 + 5.
|
||||
|
||||
One can also specify a standard Python operator in order to specify
|
||||
eventual target numbers and get results in a fair and guaranteed
|
||||
unbiased way. For example a GM could (using the dice command) from
|
||||
the start define the roll as 2d6 < 8 to show that a roll below 8 is
|
||||
required to succeed. The command will normally echo this result to all
|
||||
parties (although it also has options for hidden and secret rolls).
|
||||
|
||||
|
||||
Installation:
|
||||
|
||||
To use in your code, just import the roll_dice function from this module.
|
||||
|
||||
To use the dice/roll command, just import this module in your custom
|
||||
cmdset module and add the following line to the end of DefaultCmdSet's
|
||||
at_cmdset_creation():
|
||||
|
||||
self.add(dice.CmdDice())
|
||||
|
||||
After a reload the dice (or roll) command will be available in-game.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from random import randint
|
||||
from evennia import default_cmds, CmdSet
|
||||
|
||||
|
||||
def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
|
||||
"""
|
||||
This is a standard dice roller.
|
||||
|
||||
Input:
|
||||
dicenum - number of dice to roll (the result to be added)
|
||||
dicetype - number of sides of the dice to be rolled
|
||||
modifier - tuple (operator, value), where operator is a character string
|
||||
with one of +,-,/ or *. The entire result of the dice rolls will
|
||||
be modified by this value.
|
||||
conditional - tuple (conditional, value), where conditional is a character
|
||||
string with one of ==,<,>,>=,<= or !=.
|
||||
return_tuple - return result as a tuple containing all relevant info
|
||||
return_tuple - (default False) - return a tuple with all individual roll
|
||||
results
|
||||
All input numbers are converted to integers.
|
||||
|
||||
Returns:
|
||||
normally returns the result
|
||||
if return_tuple=True, returns a tuple (result, outcome, diff, rolls)
|
||||
In this tuple, outcome and diff will be None if conditional is
|
||||
not set. rolls is itself a tuple holding all the individual
|
||||
rolls in the case of multiple die-rolls.
|
||||
|
||||
Raises:
|
||||
TypeError if non-supported modifiers or conditionals are given.
|
||||
|
||||
"""
|
||||
dicelimit = 0 # This is the maximum number of dice that can be used in a single roll.
|
||||
dicenum = int(dicenum)
|
||||
dicetype = int(dicetype)
|
||||
|
||||
# roll all dice, remembering each roll
|
||||
rolls = tuple([randint(1, dicetype) for roll in range(dicenum)])
|
||||
result = sum(rolls)
|
||||
|
||||
if modifier:
|
||||
# make sure to check types well before eval
|
||||
mod, modvalue = modifier
|
||||
if not mod in ('+', '-', '*', '/'):
|
||||
raise TypeError("Non-supported dice modifier: %s" % mod)
|
||||
modvalue = int(modvalue) # for safety
|
||||
result = eval("%s %s %s" % (result, mod, modvalue))
|
||||
outcome, diff = None, None
|
||||
if conditional:
|
||||
# make sure to check types well before eval
|
||||
cond, condvalue = conditional
|
||||
if not cond in ('>', '<', '>=', '<=', '!=', '=='):
|
||||
raise TypeError("Non-supported dice result conditional: %s" % conditional)
|
||||
condvalue = int(condvalue) # for safety
|
||||
outcome = eval("%s %s %s" % (result, cond, condvalue)) # True/False
|
||||
diff = abs(result - condvalue)
|
||||
if return_tuple:
|
||||
return (result, outcome, diff, rolls)
|
||||
else:
|
||||
return result
|
||||
|
||||
RE_PARTS = re.compile(r"(d|\+|-|/|\*|<|>|<=|>=|!=|==)")
|
||||
RE_MOD = re.compile(r"(\+|-|/|\*)")
|
||||
RE_COND = re.compile(r"(<|>|<=|>=|!=|==)")
|
||||
|
||||
|
||||
class CmdDice(default_cmds.MuxCommand):
|
||||
"""
|
||||
roll dice
|
||||
|
||||
Usage:
|
||||
dice[/switch] <nr>d<sides> [modifier] [success condition]
|
||||
|
||||
Switch:
|
||||
hidden - tell the room the roll is being done, but don't show the result
|
||||
secret - don't inform the room about neither roll nor result
|
||||
|
||||
Examples:
|
||||
dice 3d6 + 4
|
||||
dice 1d100 - 2 < 50
|
||||
|
||||
This will roll the given number of dice with given sides and modifiers.
|
||||
So e.g. 2d6 + 3 means to 'roll a 6-sided die 2 times and add the result,
|
||||
then add 3 to the total'.
|
||||
Accepted modifiers are +, -, * and /.
|
||||
A success condition is given as normal Python conditionals
|
||||
(<,>,<=,>=,==,!=). So e.g. 2d6 + 3 > 10 means that the roll will succeed
|
||||
only if the final result is above 8. If a success condition is given, the
|
||||
outcome (pass/fail) will be echoed along with how much it succeeded/failed
|
||||
with. The hidden/secret switches will hide all or parts of the roll from
|
||||
everyone but the person rolling.
|
||||
"""
|
||||
|
||||
key = "dice"
|
||||
aliases = ["roll", "@dice"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Mostly parsing for calling the dice roller function"
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Usage: @dice <nr>d<sides> [modifier] [conditional]")
|
||||
return
|
||||
argstring = "".join(str(arg) for arg in self.args)
|
||||
|
||||
parts = RE_PARTS.split(self.args)
|
||||
lparts = len(parts)
|
||||
|
||||
ndice = 0
|
||||
nsides = 0
|
||||
modifier = None
|
||||
conditional = None
|
||||
|
||||
if lparts < 3 or parts[1] != 'd':
|
||||
self.caller.msg("You must specify the die roll(s) as <nr>d<sides>. So 2d6 means rolling a 6-sided die 2 times.")
|
||||
return
|
||||
|
||||
# Limit the number of dice and sides a character can roll to prevent server slow down and crashes
|
||||
ndicelimit = 10000 # Maximum number of dice
|
||||
nsidelimit = 10000 # Maximum number of sides
|
||||
if int(parts[0]) > ndicelimit or int(parts[2]) > nsidelimit:
|
||||
self.caller.msg("The maximum roll allowed is %sd%s." % (ndicelimit, nsidelimit))
|
||||
return
|
||||
|
||||
ndice, nsides = parts[0], parts[2]
|
||||
if lparts == 3:
|
||||
# just something like 1d6
|
||||
pass
|
||||
elif lparts == 5:
|
||||
# either e.g. 1d6 + 3 or something like 1d6 > 3
|
||||
if parts[3] in ('+', '-', '*', '/'):
|
||||
modifier = (parts[3], parts[4])
|
||||
else: # assume it is a conditional
|
||||
conditional = (parts[3], parts[4])
|
||||
elif lparts == 7:
|
||||
# the whole sequence, e.g. 1d6 + 3 > 5
|
||||
modifier = (parts[3], parts[4])
|
||||
conditional = (parts[5], parts[6])
|
||||
else:
|
||||
# error
|
||||
self.caller.msg("You must specify a valid die roll")
|
||||
return
|
||||
# do the roll
|
||||
try:
|
||||
result, outcome, diff, rolls = roll_dice(ndice,
|
||||
nsides,
|
||||
modifier=modifier,
|
||||
conditional=conditional,
|
||||
return_tuple=True)
|
||||
except ValueError:
|
||||
self.caller.msg("You need to enter valid integer numbers, modifiers and operators. {w%s{n was not understood." % self.args)
|
||||
return
|
||||
# format output
|
||||
if len(rolls) > 1:
|
||||
rolls = ", ".join(str(roll) for roll in rolls[:-1]) + " and " + str(rolls[-1])
|
||||
else:
|
||||
rolls = rolls[0]
|
||||
if outcome is None:
|
||||
outcomestring = ""
|
||||
elif outcome:
|
||||
outcomestring = " This is a {gsuccess{n (by %s)." % diff
|
||||
else:
|
||||
outcomestring = " This is a {rfailure{n (by %s)." % diff
|
||||
yourollstring = "You roll %s%s."
|
||||
roomrollstring = "%s rolls %s%s."
|
||||
resultstring = " Roll(s): %s. Total result is {w%s{n."
|
||||
|
||||
if 'secret' in self.switches:
|
||||
# don't echo to the room at all
|
||||
string = yourollstring % (argstring, " (secret, not echoed)")
|
||||
string += "\n" + resultstring % (rolls, result)
|
||||
string += outcomestring + " (not echoed)"
|
||||
self.caller.msg(string)
|
||||
elif 'hidden' in self.switches:
|
||||
# announce the roll to the room, result only to caller
|
||||
string = yourollstring % (argstring, " (hidden)")
|
||||
self.caller.msg(string)
|
||||
string = roomrollstring % (self.caller.key, argstring, " (hidden)")
|
||||
self.caller.location.msg_contents(string, exclude=self.caller)
|
||||
# handle result
|
||||
string = resultstring % (rolls, result)
|
||||
string += outcomestring + " (not echoed)"
|
||||
self.caller.msg(string)
|
||||
else:
|
||||
# normal roll
|
||||
string = yourollstring % (argstring, "")
|
||||
self.caller.msg(string)
|
||||
string = roomrollstring % (self.caller.key, argstring, "")
|
||||
self.caller.location.msg_contents(string, exclude=self.caller)
|
||||
string = resultstring % (rolls, result)
|
||||
string += outcomestring
|
||||
self.caller.location.msg_contents(string)
|
||||
|
||||
class DiceCmdSet(CmdSet):
|
||||
"""
|
||||
a small cmdset for testing purposes.
|
||||
Add with @py self.cmdset.add("contrib.dice.DiceCmdSet")
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
"Called when set is created"
|
||||
self.add(CmdDice())
|
||||
352
evennia/contrib/email-login.py
Normal file
352
evennia/contrib/email-login.py
Normal file
|
|
@ -0,0 +1,352 @@
|
|||
"""
|
||||
Email-based login system
|
||||
|
||||
Evennia contrib - Griatch 2012
|
||||
|
||||
|
||||
This is a variant of the login system that requires a email-adress
|
||||
instead of a username to login.
|
||||
|
||||
This used to be the default Evennia login before replacing it with a
|
||||
more standard username + password system (having to supply an email
|
||||
for some reason caused a lot of confusion when people wanted to expand
|
||||
on it. The email is not strictly needed internally, nor is any
|
||||
confirmation email sent out anyway).
|
||||
|
||||
|
||||
Install is simple:
|
||||
|
||||
To your settings file, add/edit the line:
|
||||
|
||||
CMDSET_UNLOGGEDIN = "contrib.email-login.UnloggedinCmdSet"
|
||||
|
||||
That's it. Reload the server and try to log in to see it.
|
||||
|
||||
The initial login "graphic" will still not mention email addresses
|
||||
after this change. The login splash screen is taken from strings in
|
||||
the module given by settings.CONNECTION_SCREEN_MODULE.
|
||||
|
||||
"""
|
||||
import re
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from evennia.players.models import PlayerDB
|
||||
from evennia.objects.models import ObjectDB
|
||||
from evennia.server.models import ServerConfig
|
||||
from evennia.comms.models import ChannelDB
|
||||
|
||||
from evennia.commands.cmdset import CmdSet
|
||||
from evennia.utils import create, logger, utils, ansi
|
||||
from evennia.commands.default.muxcommand import MuxCommand
|
||||
from evennia.commands.cmdhandler import CMD_LOGINSTART
|
||||
|
||||
# limit symbol import for API
|
||||
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
|
||||
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
|
||||
|
||||
MULTISESSION_MODE = settings.MULTISESSION_MODE
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
CONNECTION_SCREEN = ""
|
||||
try:
|
||||
CONNECTION_SCREEN = ansi.parse_ansi(utils.random_string_from_module(CONNECTION_SCREEN_MODULE))
|
||||
except Exception:
|
||||
pass
|
||||
if not CONNECTION_SCREEN:
|
||||
CONNECTION_SCREEN = "\nEvennia: Error in CONNECTION_SCREEN MODULE (randomly picked connection screen variable is not a string). \nEnter 'help' for aid."
|
||||
|
||||
|
||||
class CmdUnconnectedConnect(MuxCommand):
|
||||
"""
|
||||
Connect to the game.
|
||||
|
||||
Usage (at login screen):
|
||||
connect <email> <password>
|
||||
|
||||
Use the create command to first create an account before logging in.
|
||||
"""
|
||||
key = "connect"
|
||||
aliases = ["conn", "con", "co"]
|
||||
locks = "cmd:all()" # not really needed
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Uses the Django admin api. Note that unlogged-in commands
|
||||
have a unique position in that their func() receives
|
||||
a session object instead of a source_object like all
|
||||
other types of logged-in commands (this is because
|
||||
there is no object yet before the player has logged in)
|
||||
"""
|
||||
|
||||
session = self.caller
|
||||
arglist = self.arglist
|
||||
|
||||
if not arglist or len(arglist) < 2:
|
||||
session.msg("\n\r Usage (without <>): connect <email> <password>")
|
||||
return
|
||||
email = arglist[0]
|
||||
password = arglist[1]
|
||||
|
||||
# Match an email address to an account.
|
||||
player = PlayerDB.objects.get_player_from_email(email)
|
||||
# No playername match
|
||||
if not player:
|
||||
string = "The email '%s' does not match any accounts." % email
|
||||
string += "\n\r\n\rIf you are new you should first create a new account "
|
||||
string += "using the 'create' command."
|
||||
session.msg(string)
|
||||
return
|
||||
# We have at least one result, so we can check the password.
|
||||
if not player.check_password(password):
|
||||
session.msg("Incorrect password.")
|
||||
return
|
||||
|
||||
# Check IP and/or name bans
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
if bans and (any(tup[0] == player.name for tup in bans)
|
||||
or
|
||||
any(tup[2].match(session.address[0]) for tup in bans if tup[2])):
|
||||
# this is a banned IP or name!
|
||||
string = "{rYou have been banned and cannot continue from here."
|
||||
string += "\nIf you feel this ban is in error, please email an admin.{x"
|
||||
session.msg(string)
|
||||
session.execute_cmd("quit")
|
||||
return
|
||||
|
||||
# actually do the login. This will call all hooks.
|
||||
session.sessionhandler.login(session, player)
|
||||
|
||||
class CmdUnconnectedCreate(MuxCommand):
|
||||
"""
|
||||
Create a new account.
|
||||
|
||||
Usage (at login screen):
|
||||
create \"playername\" <email> <password>
|
||||
|
||||
This creates a new player account.
|
||||
|
||||
"""
|
||||
key = "create"
|
||||
aliases = ["cre", "cr"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
The parser must handle the multiple-word player
|
||||
name enclosed in quotes:
|
||||
connect "Long name with many words" my@myserv.com mypassw
|
||||
"""
|
||||
super(CmdUnconnectedCreate, self).parse()
|
||||
|
||||
self.playerinfo = []
|
||||
if len(self.arglist) < 3:
|
||||
return
|
||||
if len(self.arglist) > 3:
|
||||
# this means we have a multi_word playername. pop from the back.
|
||||
password = self.arglist.pop()
|
||||
email = self.arglist.pop()
|
||||
# what remains is the playername.
|
||||
playername = " ".join(self.arglist)
|
||||
else:
|
||||
playername, email, password = self.arglist
|
||||
|
||||
playername = playername.replace('"', '') # remove "
|
||||
playername = playername.replace("'", "")
|
||||
self.playerinfo = (playername, email, password)
|
||||
|
||||
def func(self):
|
||||
"Do checks and create account"
|
||||
|
||||
session = self.caller
|
||||
|
||||
try:
|
||||
playername, email, password = self.playerinfo
|
||||
except ValueError:
|
||||
string = "\n\r Usage (without <>): create \"<playername>\" <email> <password>"
|
||||
session.msg(string)
|
||||
return
|
||||
if not re.findall('^[\w. @+-]+$', playername) or not (0 < len(playername) <= 30):
|
||||
session.msg("\n\r Playername can max be 30 characters or fewer. Letters, spaces, dig\
|
||||
its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth module.
|
||||
return
|
||||
if not email or not password:
|
||||
session.msg("\n\r You have to supply an e-mail address followed by a password." )
|
||||
return
|
||||
|
||||
if not utils.validate_email_address(email):
|
||||
# check so the email at least looks ok.
|
||||
session.msg("'%s' is not a valid e-mail address." % email)
|
||||
return
|
||||
|
||||
# Run sanity and security checks
|
||||
|
||||
if PlayerDB.objects.filter(username=playername):
|
||||
# player already exists
|
||||
session.msg("Sorry, there is already a player with the name '%s'." % playername)
|
||||
return
|
||||
if PlayerDB.objects.get_player_from_email(email):
|
||||
# email already set on a player
|
||||
session.msg("Sorry, there is already a player with that email address.")
|
||||
return
|
||||
if len(password) < 3:
|
||||
# too short password
|
||||
string = "Your password must be at least 3 characters or longer."
|
||||
string += "\n\rFor best security, make it at least 8 characters long, "
|
||||
string += "avoid making it a real word and mix numbers into it."
|
||||
session.msg(string)
|
||||
return
|
||||
|
||||
# everything's ok. Create the new player account.
|
||||
try:
|
||||
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
|
||||
|
||||
typeclass = settings.BASE_CHARACTER_TYPECLASS
|
||||
permissions = settings.PERMISSION_PLAYER_DEFAULT
|
||||
|
||||
try:
|
||||
new_player = create.create_player(playername, email, password,
|
||||
permissions=permissions)
|
||||
|
||||
except Exception, e:
|
||||
session.msg("There was an error creating the default Player/Character:\n%s\n If this problem persists, contact an admin." % e)
|
||||
logger.log_trace()
|
||||
return
|
||||
|
||||
# This needs to be called so the engine knows this player is
|
||||
# logging in for the first time. (so it knows to call the right
|
||||
# hooks during login later)
|
||||
utils.init_new_player(new_player)
|
||||
|
||||
# join the new player to the public channel
|
||||
pchanneldef = settings.CHANNEL_PUBLIC
|
||||
if pchanneldef:
|
||||
pchannel = ChannelDB.objects.get_channel(pchanneldef[0])
|
||||
if not pchannel.connect(new_player):
|
||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||
logger.log_errmsg(string)
|
||||
|
||||
if MULTISESSION_MODE < 2:
|
||||
# if we only allow one character, create one with the same name as Player
|
||||
# (in mode 2, the character must be created manually once logging in)
|
||||
new_character = create.create_object(typeclass, key=playername,
|
||||
location=default_home, home=default_home,
|
||||
permissions=permissions)
|
||||
# set playable character list
|
||||
new_player.db._playable_characters.append(new_character)
|
||||
|
||||
# allow only the character itself and the player to puppet this character (and Immortals).
|
||||
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
|
||||
(new_character.id, new_player.id))
|
||||
|
||||
# If no description is set, set a default description
|
||||
if not new_character.db.desc:
|
||||
new_character.db.desc = "This is a Player."
|
||||
# We need to set this to have @ic auto-connect to this character
|
||||
new_player.db._last_puppet = new_character
|
||||
|
||||
# tell the caller everything went well.
|
||||
string = "A new account '%s' was created. Welcome!"
|
||||
if " " in playername:
|
||||
string += "\n\nYou can now log in with the command 'connect %s <your password>'."
|
||||
else:
|
||||
string += "\n\nYou can now log with the command 'connect %s <your password>'."
|
||||
session.msg(string % (playername, email))
|
||||
|
||||
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 won't see any errors at all.
|
||||
string = "%s\nThis is a bug. Please e-mail an admin if the problem persists."
|
||||
session.msg(string % (traceback.format_exc()))
|
||||
logger.log_errmsg(traceback.format_exc())
|
||||
|
||||
class CmdUnconnectedQuit(MuxCommand):
|
||||
"""
|
||||
We maintain a different version of the quit command
|
||||
here for unconnected players for the sake of simplicity. The logged in
|
||||
version is a bit more complicated.
|
||||
"""
|
||||
key = "quit"
|
||||
aliases = ["q", "qu"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Simply close the connection."
|
||||
session = self.caller
|
||||
session.msg("Good bye! Disconnecting ...")
|
||||
session.session_disconnect()
|
||||
|
||||
|
||||
class CmdUnconnectedLook(MuxCommand):
|
||||
"""
|
||||
This is an unconnected version of the look command for simplicity.
|
||||
|
||||
This is called by the server and kicks everything in gear.
|
||||
All it does is display the connect screen.
|
||||
"""
|
||||
key = CMD_LOGINSTART
|
||||
aliases = ["look", "l"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Show the connect screen."
|
||||
self.caller.msg(CONNECTION_SCREEN)
|
||||
|
||||
|
||||
class CmdUnconnectedHelp(MuxCommand):
|
||||
"""
|
||||
This is an unconnected version of the help command,
|
||||
for simplicity. It shows a pane of info.
|
||||
"""
|
||||
key = "help"
|
||||
aliases = ["h", "?"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Shows help"
|
||||
|
||||
string = \
|
||||
"""
|
||||
You are not yet logged into the game. Commands available at this point:
|
||||
{wcreate, connect, look, help, quit{n
|
||||
|
||||
To login to the system, you need to do one of the following:
|
||||
|
||||
{w1){n If you have no previous account, you need to use the 'create'
|
||||
command like this:
|
||||
|
||||
{wcreate "Anna the Barbarian" anna@myemail.com c67jHL8p{n
|
||||
|
||||
It's always a good idea (not only here, but everywhere on the net)
|
||||
to not use a regular word for your password. Make it longer than
|
||||
3 characters (ideally 6 or more) and mix numbers and capitalization
|
||||
into it.
|
||||
|
||||
{w2){n If you have an account already, either because you just created
|
||||
one in {w1){n above or you are returning, use the 'connect' command:
|
||||
|
||||
{wconnect anna@myemail.com c67jHL8p{n
|
||||
|
||||
This should log you in. Run {whelp{n again once you're logged in
|
||||
to get more aid. Hope you enjoy your stay!
|
||||
|
||||
You can use the {wlook{n command if you want to see the connect screen again.
|
||||
"""
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
# command set for the mux-like login
|
||||
|
||||
class UnloggedinCmdSet(CmdSet):
|
||||
"""
|
||||
Sets up the unlogged cmdset.
|
||||
"""
|
||||
key = "Unloggedin"
|
||||
priority = 0
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populate the cmdset"
|
||||
self.add(CmdUnconnectedConnect())
|
||||
self.add(CmdUnconnectedCreate())
|
||||
self.add(CmdUnconnectedQuit())
|
||||
self.add(CmdUnconnectedLook())
|
||||
self.add(CmdUnconnectedHelp())
|
||||
455
evennia/contrib/extended_room.py
Normal file
455
evennia/contrib/extended_room.py
Normal file
|
|
@ -0,0 +1,455 @@
|
|||
"""
|
||||
Extended Room
|
||||
|
||||
Evennia Contribution - Griatch 2012
|
||||
|
||||
This is an extended Room typeclass for Evennia. It is supported
|
||||
by an extended Look command and an extended @desc command, also
|
||||
in this module.
|
||||
|
||||
|
||||
Features:
|
||||
|
||||
1) Time-changing description slots
|
||||
|
||||
This allows to change the full description text the room shows
|
||||
depending on larger time variations. Four seasons - spring, summer,
|
||||
autumn and winter are used by default). The season is calculated
|
||||
on-demand (no Script or timer needed) and updates the full text block.
|
||||
|
||||
There is also a general description which is used as fallback if
|
||||
one or more of the seasonal descriptions are not set when their
|
||||
time comes.
|
||||
|
||||
An updated @desc command allows for setting seasonal descriptions.
|
||||
|
||||
The room uses the evennia.utils.gametime.GameTime global script. This is
|
||||
started by default, but if you have deactivated it, you need to
|
||||
supply your own time keeping mechanism.
|
||||
|
||||
|
||||
2) In-description changing tags
|
||||
|
||||
Within each seasonal (or general) description text, you can also embed
|
||||
time-of-day dependent sections. Text inside such a tag will only show
|
||||
during that particular time of day. The tags looks like <timeslot> ...
|
||||
</timeslot>. By default there are four timeslots per day - morning,
|
||||
afternoon, evening and night.
|
||||
|
||||
|
||||
3) Details
|
||||
|
||||
The Extended Room can be "detailed" with special keywords. This makes
|
||||
use of a special Look command. Details are "virtual" targets to look
|
||||
at, without there having to be a database object created for it. The
|
||||
Details are simply stored in a dictionary on the room and if the look
|
||||
command cannot find an object match for a "look <target>" command it
|
||||
will also look through the available details at the current location
|
||||
if applicable. An extended @desc command is used to set details.
|
||||
|
||||
|
||||
4) Extra commands
|
||||
|
||||
CmdExtendedLook - look command supporting room details
|
||||
CmdExtendedDesc - @desc command allowing to add seasonal descs and details,
|
||||
as well as listing them
|
||||
CmdGameTime - A simple "time" command, displaying the current
|
||||
time and season.
|
||||
|
||||
|
||||
Installation/testing:
|
||||
|
||||
1) Add CmdExtendedLook, CmdExtendedDesc and CmdGameTime to the default cmdset
|
||||
(see wiki how to do this).
|
||||
2) @dig a room of type contrib.extended_room.ExtendedRoom (or make it the
|
||||
default room type)
|
||||
3) Use @desc and @detail to customize the room, then play around!
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from django.conf import settings
|
||||
from evennia import Room
|
||||
from evennia import gametime
|
||||
from evennia import default_cmds
|
||||
from evennia import utils
|
||||
|
||||
# error return function, needed by Extended Look command
|
||||
_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1))
|
||||
|
||||
# regexes for in-desc replacements
|
||||
RE_MORNING = re.compile(r"<morning>(.*?)</morning>", re.IGNORECASE)
|
||||
RE_AFTERNOON = re.compile(r"<afternoon>(.*?)</afternoon>", re.IGNORECASE)
|
||||
RE_EVENING = re.compile(r"<evening>(.*?)</evening>", re.IGNORECASE)
|
||||
RE_NIGHT = re.compile(r"<night>(.*?)</night>", re.IGNORECASE)
|
||||
# this map is just a faster way to select the right regexes (the first
|
||||
# regex in each tuple will be parsed, the following will always be weeded out)
|
||||
REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
|
||||
"afternoon": (RE_AFTERNOON, RE_MORNING, RE_EVENING, RE_NIGHT),
|
||||
"evening": (RE_EVENING, RE_MORNING, RE_AFTERNOON, RE_NIGHT),
|
||||
"night": (RE_NIGHT, RE_MORNING, RE_AFTERNOON, RE_EVENING)}
|
||||
|
||||
# set up the seasons and time slots. This assumes gametime started at the
|
||||
# beginning of the year (so month 1 is equivalent to January), and that
|
||||
# one CAN divive the game's year into four seasons in the first place ...
|
||||
MONTHS_PER_YEAR = settings.TIME_MONTH_PER_YEAR
|
||||
SEASONAL_BOUNDARIES = (3 / 12.0, 6 / 12.0, 9 / 12.0)
|
||||
HOURS_PER_DAY = settings.TIME_HOUR_PER_DAY
|
||||
DAY_BOUNDARIES = (0, 6 / 24.0, 12 / 24.0, 18 / 24.0)
|
||||
|
||||
|
||||
# implements the Extended Room
|
||||
|
||||
class ExtendedRoom(Room):
|
||||
"""
|
||||
This room implements a more advanced look functionality depending on
|
||||
time. It also allows for "details", together with a slightly modified
|
||||
look command.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when room is first created only."
|
||||
self.db.spring_desc = ""
|
||||
self.db.summer_desc = ""
|
||||
self.db.autumn_desc = ""
|
||||
self.db.winter_desc = ""
|
||||
# the general desc is used as a fallback if a seasonal one is not set
|
||||
self.db.general_desc = ""
|
||||
# will be set dynamically. Can contain raw timeslot codes
|
||||
self.db.raw_desc = ""
|
||||
# this will be set dynamically at first look. Parsed for timeslot codes
|
||||
self.db.desc = ""
|
||||
# these will be filled later
|
||||
self.ndb.last_season = None
|
||||
self.ndb.last_timeslot = None
|
||||
# detail storage
|
||||
self.db.details = {}
|
||||
|
||||
def get_time_and_season(self):
|
||||
"""
|
||||
Calculate the current time and season ids
|
||||
"""
|
||||
# get the current time as parts of year and parts of day
|
||||
# returns a tuple (years,months,weeks,days,hours,minutes,sec)
|
||||
time = gametime.gametime(format=True)
|
||||
month, hour = time[1], time[4]
|
||||
season = float(month) / MONTHS_PER_YEAR
|
||||
timeslot = float(hour) / HOURS_PER_DAY
|
||||
|
||||
# figure out which slots these represent
|
||||
if SEASONAL_BOUNDARIES[0] <= season < SEASONAL_BOUNDARIES[1]:
|
||||
curr_season = "spring"
|
||||
elif SEASONAL_BOUNDARIES[1] <= season < SEASONAL_BOUNDARIES[2]:
|
||||
curr_season = "summer"
|
||||
elif SEASONAL_BOUNDARIES[2] <= season < 1.0 + SEASONAL_BOUNDARIES[0]:
|
||||
curr_season = "autumn"
|
||||
else:
|
||||
curr_season = "winter"
|
||||
|
||||
if DAY_BOUNDARIES[0] <= timeslot < DAY_BOUNDARIES[1]:
|
||||
curr_timeslot = "night"
|
||||
elif DAY_BOUNDARIES[1] <= timeslot < DAY_BOUNDARIES[2]:
|
||||
curr_timeslot = "morning"
|
||||
elif DAY_BOUNDARIES[2] <= timeslot < DAY_BOUNDARIES[3]:
|
||||
curr_timeslot = "afternoon"
|
||||
else:
|
||||
curr_timeslot = "evening"
|
||||
|
||||
return curr_season, curr_timeslot
|
||||
|
||||
def replace_timeslots(self, raw_desc, curr_time):
|
||||
"""
|
||||
Filter so that only time markers <timeslot>...</timeslot> of the
|
||||
correct timeslot remains in the description.
|
||||
"""
|
||||
if raw_desc:
|
||||
regextuple = REGEXMAP[curr_time]
|
||||
raw_desc = regextuple[0].sub(r"\1", raw_desc)
|
||||
raw_desc = regextuple[1].sub("", raw_desc)
|
||||
raw_desc = regextuple[2].sub("", raw_desc)
|
||||
return regextuple[3].sub("", raw_desc)
|
||||
return raw_desc
|
||||
|
||||
def return_detail(self, key):
|
||||
"""
|
||||
This will attempt to match a "detail" to look for in the room. A detail
|
||||
is a way to offer more things to look at in a room without having to
|
||||
add new objects. For this to work, we require a custom look command that
|
||||
allows for "look <detail>" - the look command should defer to this
|
||||
method on the current location (if it exists) before giving up on
|
||||
finding the target.
|
||||
|
||||
Details are not season-sensitive, but are parsed for timeslot markers.
|
||||
"""
|
||||
try:
|
||||
detail = self.db.details.get(key.lower(), None)
|
||||
except AttributeError:
|
||||
# this happens if no attribute details is set at all
|
||||
return None
|
||||
if detail:
|
||||
season, timeslot = self.get_time_and_season()
|
||||
detail = self.replace_timeslots(detail, timeslot)
|
||||
return detail
|
||||
return None
|
||||
|
||||
def return_appearance(self, looker):
|
||||
"This is called when e.g. the look command wants to retrieve the description of this object."
|
||||
raw_desc = self.db.raw_desc or ""
|
||||
update = False
|
||||
|
||||
# get current time and season
|
||||
curr_season, curr_timeslot = self.get_time_and_season()
|
||||
|
||||
# compare with previously stored slots
|
||||
last_season = self.ndb.last_season
|
||||
last_timeslot = self.ndb.last_timeslot
|
||||
|
||||
if curr_season != last_season:
|
||||
# season changed. Load new desc, or a fallback.
|
||||
if curr_season == 'spring':
|
||||
new_raw_desc = self.db.spring_desc
|
||||
elif curr_season == 'summer':
|
||||
new_raw_desc = self.db.summer_desc
|
||||
elif curr_season == 'autumn':
|
||||
new_raw_desc = self.db.autumn_desc
|
||||
else:
|
||||
new_raw_desc = self.db.winter_desc
|
||||
if new_raw_desc:
|
||||
raw_desc = new_raw_desc
|
||||
else:
|
||||
# no seasonal desc set. Use fallback
|
||||
raw_desc = self.db.general_desc or self.db.desc
|
||||
self.db.raw_desc = raw_desc
|
||||
self.ndb.last_season = curr_season
|
||||
update = True
|
||||
|
||||
if curr_timeslot != last_timeslot:
|
||||
# timeslot changed. Set update flag.
|
||||
self.ndb.last_timeslot = curr_timeslot
|
||||
update = True
|
||||
|
||||
if update:
|
||||
# if anything changed we have to re-parse
|
||||
# the raw_desc for time markers
|
||||
# and re-save the description again.
|
||||
self.db.desc = self.replace_timeslots(self.db.raw_desc, curr_timeslot)
|
||||
# run the normal return_appearance method, now that desc is updated.
|
||||
return super(ExtendedRoom, self).return_appearance(looker)
|
||||
|
||||
|
||||
# Custom Look command supporting Room details. Add this to
|
||||
# the Default cmdset to use.
|
||||
|
||||
class CmdExtendedLook(default_cmds.CmdLook):
|
||||
"""
|
||||
look
|
||||
|
||||
Usage:
|
||||
look
|
||||
look <obj>
|
||||
look <room detail>
|
||||
look *<player>
|
||||
|
||||
Observes your location, details at your location or objects in your vicinity.
|
||||
"""
|
||||
def func(self):
|
||||
"""
|
||||
Handle the looking - add fallback to details.
|
||||
"""
|
||||
caller = self.caller
|
||||
args = self.args
|
||||
if args:
|
||||
looking_at_obj = caller.search(args, use_nicks=True, quiet=True)
|
||||
if not looking_at_obj:
|
||||
# no object found. Check if there is a matching
|
||||
# detail at location.
|
||||
location = caller.location
|
||||
if location and hasattr(location, "return_detail") and callable(location.return_detail):
|
||||
detail = location.return_detail(args)
|
||||
if detail:
|
||||
# we found a detail instead. Show that.
|
||||
caller.msg(detail)
|
||||
return
|
||||
# no detail found. Trigger delayed error messages
|
||||
_AT_SEARCH_RESULT(caller, args, looking_at_obj, False)
|
||||
return
|
||||
else:
|
||||
# we need to extract the match manually.
|
||||
looking_at_obj = utils.make_iter(looking_at_obj)[0]
|
||||
else:
|
||||
looking_at_obj = caller.location
|
||||
if not looking_at_obj:
|
||||
caller.msg("You have no location to look at!")
|
||||
return
|
||||
|
||||
if not hasattr(looking_at_obj, 'return_appearance'):
|
||||
# this is likely due to us having a player instead
|
||||
looking_at_obj = looking_at_obj.character
|
||||
if not looking_at_obj.access(caller, "view"):
|
||||
caller.msg("Could not find '%s'." % args)
|
||||
return
|
||||
# get object's appearance
|
||||
caller.msg(looking_at_obj.return_appearance(caller))
|
||||
# the object's at_desc() method.
|
||||
looking_at_obj.at_desc(looker=caller)
|
||||
|
||||
|
||||
# Custom build commands for setting seasonal descriptions
|
||||
# and detailing extended rooms.
|
||||
|
||||
class CmdExtendedDesc(default_cmds.CmdDesc):
|
||||
"""
|
||||
@desc - describe an object or room
|
||||
|
||||
Usage:
|
||||
@desc[/switch] [<obj> =] <description>
|
||||
@detail[/del] [<key> = <description>]
|
||||
|
||||
|
||||
Switches for @desc:
|
||||
spring - set description for <season> in current room
|
||||
summer
|
||||
autumn
|
||||
winter
|
||||
|
||||
Switch for @detail:
|
||||
del - delete a named detail
|
||||
|
||||
Sets the "desc" attribute on an object. If an object is not given,
|
||||
describe the current room.
|
||||
|
||||
The alias @detail allows to assign a "detail" (a non-object
|
||||
target for the look command) to the current room (only).
|
||||
|
||||
You can also embed special time markers in your room description, like this:
|
||||
<night>In the darkness, the forest looks foreboding.</night>. Text
|
||||
marked this way will only display when the server is truly at the given
|
||||
time slot. The available times
|
||||
are night, morning, afternoon and evening.
|
||||
|
||||
Note that @detail, seasons and time-of-day slots only works on rooms in this
|
||||
version of the @desc command.
|
||||
|
||||
"""
|
||||
aliases = ["@describe", "@detail"]
|
||||
|
||||
def reset_times(self, obj):
|
||||
"By deleteting the caches we force a re-load."
|
||||
obj.ndb.last_season = None
|
||||
obj.ndb.last_timeslot = None
|
||||
|
||||
def func(self):
|
||||
"Define extended command"
|
||||
caller = self.caller
|
||||
location = caller.location
|
||||
if self.cmdstring == '@detail':
|
||||
# switch to detailing mode. This operates only on current location
|
||||
if not location:
|
||||
caller.msg("No location to detail!")
|
||||
return
|
||||
if not self.rhs:
|
||||
# no '=' used - list content of given detail
|
||||
if self.args in location.db.details:
|
||||
string = "{wDetail '%s' on %s:\n{n" % (self.args, location)
|
||||
string += location.db.details[self.args]
|
||||
caller.msg(string)
|
||||
return
|
||||
if not self.args:
|
||||
# No args given. Return all details on location
|
||||
string = "{wDetails on %s{n:\n" % location
|
||||
string += "\n".join(" {w%s{n: %s" % (key, utils.crop(text)) for key, text in location.db.details.items())
|
||||
caller.msg(string)
|
||||
return
|
||||
if self.switches and self.switches[0] in 'del':
|
||||
# removing a detail.
|
||||
if self.lhs in location.db.details:
|
||||
del location.db.detail
|
||||
caller.msg("Detail %s deleted, if it existed." % self.lhs)
|
||||
self.reset_times(location)
|
||||
return
|
||||
# setting a detail
|
||||
location.db.details[self.lhs] = self.rhs
|
||||
caller.msg("Set Detail %s to '%s'." % (self.lhs, self.rhs))
|
||||
self.reset_times(location)
|
||||
return
|
||||
else:
|
||||
# we are doing a @desc call
|
||||
if not self.args:
|
||||
if location:
|
||||
string = "{wDescriptions on %s{n:\n" % location.key
|
||||
string += " {wspring:{n %s\n" % location.db.spring_desc
|
||||
string += " {wsummer:{n %s\n" % location.db.summer_desc
|
||||
string += " {wautumn:{n %s\n" % location.db.autumn_desc
|
||||
string += " {wwinter:{n %s\n" % location.db.winter_desc
|
||||
string += " {wgeneral:{n %s" % location.db.general_desc
|
||||
caller.msg(string)
|
||||
return
|
||||
if self.switches and self.switches[0] in ("spring",
|
||||
"summer",
|
||||
"autumn",
|
||||
"winter"):
|
||||
# a seasonal switch was given
|
||||
if self.rhs:
|
||||
caller.msg("Seasonal descs only works with rooms, not objects.")
|
||||
return
|
||||
switch = self.switches[0]
|
||||
if not location:
|
||||
caller.msg("No location was found!")
|
||||
return
|
||||
if switch == 'spring':
|
||||
location.db.spring_desc = self.args
|
||||
elif switch == 'summer':
|
||||
location.db.summer_desc = self.args
|
||||
elif switch == 'autumn':
|
||||
location.db.autumn_desc = self.args
|
||||
elif switch == 'winter':
|
||||
location.db.winter_desc = self.args
|
||||
# clear flag to force an update
|
||||
self.reset_times(location)
|
||||
caller.msg("Seasonal description was set on %s." % location.key)
|
||||
else:
|
||||
# Not seasonal desc set, maybe this is not an extended room
|
||||
if self.rhs:
|
||||
text = self.rhs
|
||||
obj = caller.search(self.lhs)
|
||||
if not obj:
|
||||
return
|
||||
else:
|
||||
text = self.args
|
||||
obj = location
|
||||
obj.db.desc = self.rhs # a compatability fallback
|
||||
if utils.inherits_from(obj, ExtendedRoom):
|
||||
# this is an extendedroom, we need to reset
|
||||
# times and set general_desc
|
||||
obj.db.general_desc = text
|
||||
self.reset_times(obj)
|
||||
caller.msg("General description was set on %s." % obj.key)
|
||||
else:
|
||||
caller.msg("The description was set on %s." % obj.key)
|
||||
|
||||
|
||||
# Simple command to view the current time and season
|
||||
|
||||
class CmdGameTime(default_cmds.MuxCommand):
|
||||
"""
|
||||
Check the game time
|
||||
|
||||
Usage:
|
||||
time
|
||||
|
||||
Shows the current in-game time and season.
|
||||
"""
|
||||
key = "time"
|
||||
locks = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Reads time info from current room"
|
||||
location = self.caller.location
|
||||
if not location or not hasattr(location, "get_time_and_season"):
|
||||
self.caller.msg("No location available - you are outside time.")
|
||||
else:
|
||||
season, timeslot = location.get_time_and_season()
|
||||
prep = "a"
|
||||
if season == "autumn":
|
||||
prep = "an"
|
||||
self.caller.msg("It's %s %s day, in the %s." % (prep, season, timeslot))
|
||||
685
evennia/contrib/lineeditor.py
Normal file
685
evennia/contrib/lineeditor.py
Normal file
|
|
@ -0,0 +1,685 @@
|
|||
"""
|
||||
|
||||
Evennia Line Editor
|
||||
|
||||
Contribution - Griatch 2011
|
||||
|
||||
This implements an advanced line editor for editing longer texts
|
||||
in-game. The editor mimics the command mechanisms of the VI editor as
|
||||
far as possible.
|
||||
|
||||
Features of the editor:
|
||||
undo/redo
|
||||
edit/replace on any line of the buffer
|
||||
search&replace text anywhere in buffer
|
||||
formatting of buffer, or selection, to certain width + indentations
|
||||
allow to echo the input or not depending on your client.
|
||||
|
||||
|
||||
Whereas the editor is intended to be called from other commands that
|
||||
requires more elaborate text editing of data, there is also a
|
||||
stand-alone editor command for editing Attributes at the end of this
|
||||
module. To use it just import and add it to your default cmdset.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
from evennia import Command, CmdSet, utils
|
||||
from evennia import syscmdkeys
|
||||
from contrib.menusystem import prompt_yesno
|
||||
|
||||
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
|
||||
RE_GROUP = re.compile(r"\".*?\"|\'.*?\'|\S*")
|
||||
|
||||
|
||||
class CmdEditorBase(Command):
|
||||
"""
|
||||
Base parent for editor commands
|
||||
"""
|
||||
locks = "cmd:all()"
|
||||
help_entry = "LineEditor"
|
||||
|
||||
code = None
|
||||
editor = None
|
||||
|
||||
def parse(self):
|
||||
"""
|
||||
Handles pre-parsing
|
||||
|
||||
Editor commands are on the form
|
||||
:cmd [li] [w] [txt]
|
||||
|
||||
Where all arguments are optional.
|
||||
li - line number (int), starting from 1. This could also
|
||||
be a range given as <l>:<l>
|
||||
w - word(s) (string), could be encased in quotes.
|
||||
txt - extra text (string), could be encased in quotes
|
||||
"""
|
||||
|
||||
linebuffer = []
|
||||
if self.editor:
|
||||
linebuffer = self.editor.buffer.split("\n")
|
||||
nlines = len(linebuffer)
|
||||
|
||||
# The regular expression will split the line by whitespaces,
|
||||
# stripping extra whitespaces, except if the text is
|
||||
# surrounded by single- or double quotes, in which case they
|
||||
# will be kept together and extra whitespace preserved. You
|
||||
# can input quotes on the line by alternating single and
|
||||
# double quotes.
|
||||
arglist = [part for part in RE_GROUP.findall(self.args) if part]
|
||||
temp = []
|
||||
for arg in arglist:
|
||||
# we want to clean the quotes, but only one type,
|
||||
# in case we are nesting.
|
||||
if arg.startswith('"'):
|
||||
arg.strip('"')
|
||||
elif arg.startswith("'"):
|
||||
arg.strip("'")
|
||||
temp.append(arg)
|
||||
arglist = temp
|
||||
|
||||
# A dumb split, without grouping quotes
|
||||
words = self.args.split()
|
||||
|
||||
# current line number
|
||||
cline = nlines - 1
|
||||
|
||||
# the first argument could also be a range of line numbers, on the
|
||||
# form <lstart>:<lend>. Either of the ends could be missing, to
|
||||
# mean start/end of buffer respectively.
|
||||
|
||||
lstart, lend = cline, cline + 1
|
||||
linerange = False
|
||||
if arglist and ':' in arglist[0]:
|
||||
part1, part2 = arglist[0].split(':')
|
||||
if part1 and part1.isdigit():
|
||||
lstart = min(max(0, int(part1)) - 1, nlines)
|
||||
linerange = True
|
||||
if part2 and part2.isdigit():
|
||||
lend = min(lstart + 1, int(part2)) + 1
|
||||
linerange = True
|
||||
elif arglist and arglist[0].isdigit():
|
||||
lstart = min(max(0, int(arglist[0]) - 1), nlines)
|
||||
lend = lstart + 1
|
||||
linerange = True
|
||||
if linerange:
|
||||
arglist = arglist[1:]
|
||||
|
||||
# nicer output formatting of the line range.
|
||||
lstr = ""
|
||||
if not linerange or lstart + 1 == lend:
|
||||
lstr = "line %i" % (lstart + 1)
|
||||
else:
|
||||
lstr = "lines %i-%i" % (lstart + 1, lend)
|
||||
|
||||
# arg1 and arg2 is whatever arguments. Line numbers or -ranges are
|
||||
# never included here.
|
||||
args = " ".join(arglist)
|
||||
arg1, arg2 = "", ""
|
||||
if len(arglist) > 1:
|
||||
arg1, arg2 = arglist[0], " ".join(arglist[1:])
|
||||
else:
|
||||
arg1 = " ".join(arglist)
|
||||
|
||||
# store for use in func()
|
||||
|
||||
self.linebuffer = linebuffer
|
||||
self.nlines = nlines
|
||||
self.arglist = arglist
|
||||
self.cline = cline
|
||||
self.lstart = lstart
|
||||
self.lend = lend
|
||||
self.linerange = linerange
|
||||
self.lstr = lstr
|
||||
self.words = words
|
||||
self.args = args
|
||||
self.arg1 = arg1
|
||||
self.arg2 = arg2
|
||||
|
||||
def func(self):
|
||||
"Implements the Editor commands"
|
||||
pass
|
||||
|
||||
|
||||
class CmdLineInput(CmdEditorBase):
|
||||
"""
|
||||
No command match - Inputs line of text into buffer.
|
||||
"""
|
||||
key = CMD_NOMATCH
|
||||
aliases = [CMD_NOINPUT]
|
||||
|
||||
def func(self):
|
||||
"Adds the line without any formatting changes."
|
||||
# add a line of text
|
||||
if not self.editor.buffer:
|
||||
buf = self.args
|
||||
else:
|
||||
buf = self.editor.buffer + "\n%s" % self.args
|
||||
self.editor.update_buffer(buf)
|
||||
if self.editor.echo_mode:
|
||||
# need to do it here or we will be off one line
|
||||
cline = len(self.editor.buffer.split('\n'))
|
||||
self.caller.msg("{b%02i|{n %s" % (cline, self.args))
|
||||
|
||||
|
||||
class CmdEditorGroup(CmdEditorBase):
|
||||
"""
|
||||
Commands for the editor
|
||||
"""
|
||||
key = ":editor_command_group"
|
||||
aliases = [":","::", ":::", ":h", ":w", ":wq", ":q", ":q!", ":u", ":uu", ":UU",
|
||||
":dd", ":dw", ":DD", ":y", ":x", ":p", ":i",
|
||||
":r", ":I", ":A", ":s", ":S", ":f", ":fi", ":fd", ":echo"]
|
||||
arg_regex = r"\s.*?|$"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This command handles all the in-editor :-style commands. Since
|
||||
each command is small and very limited, this makes for a more
|
||||
efficient presentation.
|
||||
"""
|
||||
caller = self.caller
|
||||
editor = self.editor
|
||||
linebuffer = self.linebuffer
|
||||
lstart, lend = self.lstart, self.lend
|
||||
cmd = self.cmdstring
|
||||
echo_mode = self.editor.echo_mode
|
||||
string = ""
|
||||
|
||||
if cmd == ":":
|
||||
# Echo buffer
|
||||
if self.linerange:
|
||||
buf = linebuffer[lstart:lend]
|
||||
string = editor.display_buffer(buf=buf, offset=lstart)
|
||||
else:
|
||||
string = editor.display_buffer()
|
||||
elif cmd == "::":
|
||||
# Echo buffer without the line numbers and syntax parsing
|
||||
if self.linerange:
|
||||
buf = linebuffer[lstart:lend]
|
||||
string = editor.display_buffer(buf=buf,
|
||||
offset=lstart,
|
||||
linenums=False)
|
||||
else:
|
||||
string = editor.display_buffer(linenums=False)
|
||||
self.caller.msg(string, raw=True)
|
||||
return
|
||||
elif cmd == ":::":
|
||||
# Insert single colon alone on a line
|
||||
editor.update_buffer(editor.buffer + "\n:")
|
||||
if echo_mode:
|
||||
string = "Single ':' added to buffer."
|
||||
elif cmd == ":h":
|
||||
# help entry
|
||||
string = editor.display_help()
|
||||
elif cmd == ":w":
|
||||
# save without quitting
|
||||
string = editor.save_buffer()
|
||||
elif cmd == ":wq":
|
||||
# save and quit
|
||||
string = editor.save_buffer()
|
||||
string += " " + editor.quit()
|
||||
elif cmd == ":q":
|
||||
# quit. If not saved, will ask
|
||||
if self.editor.unsaved:
|
||||
prompt_yesno(caller, "Save before quitting?",
|
||||
yescode = "self.caller.ndb._lineeditor.save_buffer()\nself.caller.ndb._lineeditor.quit()",
|
||||
nocode = "self.caller.msg(self.caller.ndb._lineeditor.quit())", default="Y")
|
||||
else:
|
||||
string = editor.quit()
|
||||
elif cmd == ":q!":
|
||||
# force quit, not checking saving
|
||||
string = editor.quit()
|
||||
elif cmd == ":u":
|
||||
# undo
|
||||
string = editor.update_undo(-1)
|
||||
elif cmd == ":uu":
|
||||
# redo
|
||||
string = editor.update_undo(1)
|
||||
elif cmd == ":UU":
|
||||
# reset buffer
|
||||
editor.update_buffer(editor.pristine_buffer)
|
||||
string = "Reverted all changes to the buffer back to original state."
|
||||
elif cmd == ":dd":
|
||||
# :dd <l> - delete line <l>
|
||||
buf = linebuffer[:lstart] + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Deleted %s." % (self.lstr)
|
||||
elif cmd == ":dw":
|
||||
# :dw <w> - delete word in entire buffer
|
||||
# :dw <l> <w> delete word only on line(s) <l>
|
||||
if not self.arg1:
|
||||
string = "You must give a search word to delete."
|
||||
else:
|
||||
if not self.linerange:
|
||||
lstart = 0
|
||||
lend = self.cline + 1
|
||||
string = "Removed %s for lines %i-%i." % (self.arg1, lstart + 1, lend + 1)
|
||||
else:
|
||||
string = "Removed %s for %s." % (self.arg1, self.lstr)
|
||||
sarea = "\n".join(linebuffer[lstart:lend])
|
||||
sarea = re.sub(r"%s" % self.arg1.strip("\'").strip('\"'), "", sarea, re.MULTILINE)
|
||||
buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
elif cmd == ":DD":
|
||||
# clear buffer
|
||||
editor.update_buffer("")
|
||||
string = "Cleared %i lines from buffer." % self.nlines
|
||||
elif cmd == ":y":
|
||||
# :y <l> - yank line(s) to copy buffer
|
||||
cbuf = linebuffer[lstart:lend]
|
||||
editor.copy_buffer = cbuf
|
||||
string = "%s, %s yanked." % (self.lstr.capitalize(), cbuf)
|
||||
elif cmd == ":x":
|
||||
# :x <l> - cut line to copy buffer
|
||||
cbuf = linebuffer[lstart:lend]
|
||||
editor.copy_buffer = cbuf
|
||||
buf = linebuffer[:lstart] + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
string = "%s, %s cut." % (self.lstr.capitalize(), cbuf)
|
||||
elif cmd == ":p":
|
||||
# :p <l> paste line(s) from copy buffer
|
||||
if not editor.copy_buffer:
|
||||
string = "Copy buffer is empty."
|
||||
else:
|
||||
buf = linebuffer[:lstart] + editor.copy_buffer + linebuffer[lstart:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Copied buffer %s to %s." % (editor.copy_buffer, self.lstr)
|
||||
elif cmd == ":i":
|
||||
# :i <l> <txt> - insert new line
|
||||
new_lines = self.args.split('\n')
|
||||
if not new_lines:
|
||||
string = "You need to enter a new line and where to insert it."
|
||||
else:
|
||||
buf = linebuffer[:lstart] + new_lines + linebuffer[lstart:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Inserted %i new line(s) at %s." % (len(new_lines), self.lstr)
|
||||
elif cmd == ":r":
|
||||
# :r <l> <txt> - replace lines
|
||||
new_lines = self.args.split('\n')
|
||||
if not new_lines:
|
||||
string = "You need to enter a replacement string."
|
||||
else:
|
||||
buf = linebuffer[:lstart] + new_lines + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Replaced %i line(s) at %s." % (len(new_lines), self.lstr)
|
||||
elif cmd == ":I":
|
||||
# :I <l> <txt> - insert text at beginning of line(s) <l>
|
||||
if not self.args:
|
||||
string = "You need to enter text to insert."
|
||||
else:
|
||||
buf = linebuffer[:lstart] + ["%s%s" % (self.args, line) for line in linebuffer[lstart:lend]] + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Inserted text at beginning of %s." % self.lstr
|
||||
elif cmd == ":A":
|
||||
# :A <l> <txt> - append text after end of line(s)
|
||||
if not self.args:
|
||||
string = "You need to enter text to append."
|
||||
else:
|
||||
buf = linebuffer[:lstart] + ["%s%s" % (line, self.args) for line in linebuffer[lstart:lend]] + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
string = "Appended text to end of %s." % self.lstr
|
||||
elif cmd == ":s":
|
||||
# :s <li> <w> <txt> - search and replace words
|
||||
# in entire buffer or on certain lines
|
||||
if not self.arg1 or not self.arg2:
|
||||
string = "You must give a search word and something to replace it with."
|
||||
else:
|
||||
if not self.linerange:
|
||||
lstart = 0
|
||||
lend = self.cline + 1
|
||||
string = "Search-replaced %s -> %s for lines %i-%i." % (self.arg1, self.arg2, lstart + 1 , lend)
|
||||
else:
|
||||
string = "Search-replaced %s -> %s for %s." % (self.arg1, self.arg2, self.lstr)
|
||||
sarea = "\n".join(linebuffer[lstart:lend])
|
||||
|
||||
regex = r"%s|^%s(?=\s)|(?<=\s)%s(?=\s)|^%s$|(?<=\s)%s$"
|
||||
regarg = self.arg1.strip("\'").strip('\"')
|
||||
if " " in regarg:
|
||||
regarg = regarg.replace(" ", " +")
|
||||
sarea = re.sub(regex % (regarg, regarg, regarg, regarg, regarg), self.arg2.strip("\'").strip('\"'), sarea, re.MULTILINE)
|
||||
buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
elif cmd == ":f":
|
||||
# :f <l> flood-fill buffer or <l> lines of buffer.
|
||||
width = 78
|
||||
if not self.linerange:
|
||||
lstart = 0
|
||||
lend = self.cline + 1
|
||||
string = "Flood filled lines %i-%i." % (lstart + 1 , lend)
|
||||
else:
|
||||
string = "Flood filled %s." % self.lstr
|
||||
fbuf = "\n".join(linebuffer[lstart:lend])
|
||||
fbuf = utils.fill(fbuf, width=width)
|
||||
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
elif cmd == ":fi":
|
||||
# :fi <l> indent buffer or lines <l> of buffer.
|
||||
indent = " " * 4
|
||||
if not self.linerange:
|
||||
lstart = 0
|
||||
lend = self.cline + 1
|
||||
string = "Indented lines %i-%i." % (lstart + 1 , lend)
|
||||
else:
|
||||
string = "Indented %s." % self.lstr
|
||||
fbuf = [indent + line for line in linebuffer[lstart:lend]]
|
||||
buf = linebuffer[:lstart] + fbuf + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
elif cmd == ":fd":
|
||||
# :fi <l> indent buffer or lines <l> of buffer.
|
||||
if not self.linerange:
|
||||
lstart = 0
|
||||
lend = self.cline + 1
|
||||
string = "Removed left margin (dedented) lines %i-%i." % (lstart + 1 , lend)
|
||||
else:
|
||||
string = "Removed left margin (dedented) %s." % self.lstr
|
||||
fbuf = "\n".join(linebuffer[lstart:lend])
|
||||
fbuf = utils.dedent(fbuf)
|
||||
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
|
||||
editor.update_buffer(buf)
|
||||
elif cmd == ":echo":
|
||||
# set echoing on/off
|
||||
editor.echo_mode = not editor.echo_mode
|
||||
string = "Echo mode set to %s" % editor.echo_mode
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class EditorCmdSet(CmdSet):
|
||||
"CmdSet for the editor commands"
|
||||
key = "editorcmdset"
|
||||
mergetype = "Replace"
|
||||
|
||||
|
||||
class LineEditor(object):
|
||||
"""
|
||||
This defines a line editor object. It creates all relevant commands
|
||||
and tracks the current state of the buffer. It also cleans up after
|
||||
itself.
|
||||
"""
|
||||
|
||||
def __init__(self, caller,
|
||||
loadfunc=None, loadfunc_args=None,
|
||||
savefunc=None, savefunc_args=None,
|
||||
quitfunc=None, quitfunc_args=None,
|
||||
key=""):
|
||||
"""
|
||||
caller - who is using the editor
|
||||
|
||||
loadfunc - this will be called as func(*loadfunc_args) when the
|
||||
editor is first started, e.g. for pre-loading text into it.
|
||||
loadfunc_args - optional tuple of arguments to supply to loadfunc.
|
||||
savefunc - this will be called as func(*savefunc_args) when the
|
||||
save-command is given and is used to actually determine
|
||||
where/how result is saved. It should return True if save
|
||||
was successful and also handle any feedback to the user.
|
||||
savefunc_args - optional tuple of arguments to supply to savefunc.
|
||||
quitfunc - this will optionally e called as func(*quitfunc_args) when
|
||||
the editor is exited. If defined, it should handle all
|
||||
wanted feedback to the user.
|
||||
quitfunc_args - optional tuple of arguments to supply to quitfunc.
|
||||
|
||||
key = an optional key for naming this session (such as which attribute
|
||||
is being edited)
|
||||
"""
|
||||
self.key = key
|
||||
self.caller = caller
|
||||
self.caller.ndb._lineeditor = self
|
||||
self.buffer = ""
|
||||
self.unsaved = False
|
||||
|
||||
if loadfunc:
|
||||
# execute command for loading initial data
|
||||
try:
|
||||
args = loadfunc_args or ()
|
||||
self.buffer = loadfunc(*args)
|
||||
except Exception, e:
|
||||
caller.msg("%s\n{rBuffer load function error. Could not load initial data.{n" % e)
|
||||
if not savefunc:
|
||||
# If no save function is defined, save an error-reporting function
|
||||
err = "{rNo save function defined. Buffer cannot be saved.{n"
|
||||
caller.msg(err)
|
||||
savefunc = lambda: self.caller.msg(err)
|
||||
self.savefunc = savefunc
|
||||
self.savefunc_args = savefunc_args or ()
|
||||
self.quitfunc = quitfunc
|
||||
self.quitfunc_args = quitfunc_args or ()
|
||||
|
||||
# Create the commands we need
|
||||
cmd1 = CmdLineInput()
|
||||
cmd1.editor = self
|
||||
cmd1.obj = self
|
||||
cmd2 = CmdEditorGroup()
|
||||
cmd2.obj = self
|
||||
cmd2.editor = self
|
||||
# Populate cmdset and add it to caller
|
||||
editor_cmdset = EditorCmdSet()
|
||||
editor_cmdset.add(cmd1)
|
||||
editor_cmdset.add(cmd2)
|
||||
self.caller.cmdset.add(editor_cmdset)
|
||||
|
||||
# store the original version
|
||||
self.pristine_buffer = self.buffer
|
||||
self.sep = "-"
|
||||
|
||||
# undo operation buffer
|
||||
self.undo_buffer = [self.buffer]
|
||||
self.undo_pos = 0
|
||||
self.undo_max = 20
|
||||
|
||||
# copy buffer
|
||||
self.copy_buffer = []
|
||||
|
||||
# echo inserted text back to caller
|
||||
self.echo_mode = False
|
||||
|
||||
# show the buffer ui
|
||||
self.caller.msg(self.display_buffer())
|
||||
|
||||
def update_buffer(self, buf):
|
||||
"""
|
||||
This should be called when the buffer has been changed somehow.
|
||||
It will handle unsaved flag and undo updating.
|
||||
"""
|
||||
if utils.is_iter(buf):
|
||||
buf = "\n".join(buf)
|
||||
|
||||
if buf != self.buffer:
|
||||
self.buffer = buf
|
||||
self.update_undo()
|
||||
self.unsaved = True
|
||||
|
||||
def quit(self):
|
||||
"Cleanly exit the editor."
|
||||
if self.quitfunc:
|
||||
# call quit function hook if available
|
||||
try:
|
||||
self.quitfunc(*self.quitfunc_args)
|
||||
except Exception, e:
|
||||
self.caller.msg("%s\n{Quit function gave an error. Skipping.{n" % e)
|
||||
del self.caller.ndb._lineeditor
|
||||
self.caller.cmdset.delete(EditorCmdSet)
|
||||
if self.quitfunc:
|
||||
# if quitfunc is defined, it should manage exit messages.
|
||||
return ""
|
||||
return "Exited editor."
|
||||
|
||||
def save_buffer(self):
|
||||
"""
|
||||
Saves the content of the buffer. The 'quitting' argument is a bool
|
||||
indicating whether or not the editor intends to exit after saving.
|
||||
"""
|
||||
if self.unsaved:
|
||||
try:
|
||||
if self.savefunc(*self.savefunc_args):
|
||||
# Save codes should return a true value to indicate
|
||||
# save worked. The saving function is responsible for
|
||||
# any status messages.
|
||||
self.unsaved = False
|
||||
return ""
|
||||
except Exception, e:
|
||||
return "%s\n{rSave function gave an error. Buffer not saved." % e
|
||||
else:
|
||||
return "No changes need saving."
|
||||
|
||||
def update_undo(self, step=None):
|
||||
"""
|
||||
This updates the undo position.
|
||||
|
||||
"""
|
||||
if step and step < 0:
|
||||
if self.undo_pos <= 0:
|
||||
return "Nothing to undo."
|
||||
self.undo_pos = max(0, self.undo_pos + step)
|
||||
self.buffer = self.undo_buffer[self.undo_pos]
|
||||
return "Undo."
|
||||
elif step and step > 0:
|
||||
if self.undo_pos >= len(self.undo_buffer) - 1 or self.undo_pos + 1 >= self.undo_max:
|
||||
return "Nothing to redo."
|
||||
self.undo_pos = min(self.undo_pos + step, min(len(self.undo_buffer), self.undo_max) - 1)
|
||||
self.buffer = self.undo_buffer[self.undo_pos]
|
||||
return "Redo."
|
||||
if not self.undo_buffer or (self.undo_buffer and self.buffer != self.undo_buffer[self.undo_pos]):
|
||||
self.undo_buffer = self.undo_buffer[:self.undo_pos + 1] + [self.buffer]
|
||||
self.undo_pos = len(self.undo_buffer) - 1
|
||||
|
||||
def display_buffer(self, buf=None, offset=0, linenums=True):
|
||||
"""
|
||||
This displays the line editor buffer, or selected parts of it.
|
||||
|
||||
If buf is set and is not the full buffer, offset should define
|
||||
the starting line number, to get the linenum display right.
|
||||
"""
|
||||
if buf == None:
|
||||
buf = self.buffer
|
||||
if utils.is_iter(buf):
|
||||
buf = "\n".join(buf)
|
||||
|
||||
lines = buf.split('\n')
|
||||
nlines = len(lines)
|
||||
nwords = len(buf.split())
|
||||
nchars = len(buf)
|
||||
|
||||
sep = self.sep
|
||||
header = "{n" + sep * 10 + "Line Editor [%s]" % self.key + sep * (78-25-len(self.key))
|
||||
footer = "{n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) + sep * 12 + "(:h for help)" + sep * 23
|
||||
if linenums:
|
||||
main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))
|
||||
else:
|
||||
main = "\n".join(lines)
|
||||
string = "%s\n%s\n%s" % (header, main, footer)
|
||||
return string
|
||||
|
||||
def display_help(self):
|
||||
"""
|
||||
Shows the help entry for the editor.
|
||||
"""
|
||||
string = self.sep * 78 + """
|
||||
<txt> - any non-command is appended to the end of the buffer.
|
||||
: <l> - view buffer or only line <l>
|
||||
:: <l> - view buffer without line numbers or other parsing
|
||||
::: - print a ':' as the only character on the line...
|
||||
:h - this help.
|
||||
|
||||
:w - saves the buffer (don't quit)
|
||||
:wq - save buffer and quit
|
||||
:q - quits (will be asked to save if buffer was changed)
|
||||
:q! - quit without saving, no questions asked
|
||||
|
||||
:u - (undo) step backwards in undo history
|
||||
:uu - (redo) step forward in undo history
|
||||
:UU - reset all changes back to initial
|
||||
|
||||
:dd <l> - delete line <n>
|
||||
:dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
|
||||
:DD - clear buffer
|
||||
|
||||
:y <l> - yank (copy) line <l> to the copy buffer
|
||||
:x <l> - cut line <l> and store it in the copy buffer
|
||||
:p <l> - put (paste) previously copied line directly after <l>
|
||||
:i <l> <txt> - insert new text <txt> at line <l>. Old line will move down
|
||||
:r <l> <txt> - replace line <l> with text <txt>
|
||||
:I <l> <txt> - insert text at the beginning of line <l>
|
||||
:A <l> <txt> - append text after the end of line <l>
|
||||
|
||||
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
|
||||
|
||||
:f <l> - flood-fill entire buffer or line <l>
|
||||
:fi <l> - indent entire buffer or line <l>
|
||||
:fd <l> - de-indent entire buffer or line <l>
|
||||
|
||||
:echo - turn echoing of the input on/off (helpful for some clients)
|
||||
|
||||
Legend:
|
||||
<l> - line numbers, or range lstart:lend, e.g. '3:7'.
|
||||
<w> - one word or several enclosed in quotes.
|
||||
<txt> - longer string, usually not needed to be enclosed in quotes.
|
||||
""" + self.sep * 78
|
||||
return string
|
||||
|
||||
|
||||
#
|
||||
# Editor access command for editing a given attribute on an object.
|
||||
#
|
||||
|
||||
class CmdEditor(Command):
|
||||
"""
|
||||
start editor
|
||||
|
||||
Usage:
|
||||
@editor <obj>/<attr>
|
||||
|
||||
This will start Evennia's powerful line editor to edit an
|
||||
Attribute. The editor has a host of commands on its own. Use :h
|
||||
for a list of commands.
|
||||
|
||||
"""
|
||||
|
||||
key = "@editor"
|
||||
aliases = ["@edit"]
|
||||
locks = "cmd:perm(editor) or perm(Builders)"
|
||||
help_category = "Building"
|
||||
|
||||
def func(self):
|
||||
"setup and start the editor"
|
||||
|
||||
if not self.args or not '/' in self.args:
|
||||
self.caller.msg("Usage: @editor <obj>/<attrname>")
|
||||
return
|
||||
self.objname, self.attrname = [part.strip()
|
||||
for part in self.args.split("/", 1)]
|
||||
self.obj = self.caller.search(self.objname)
|
||||
if not self.obj:
|
||||
return
|
||||
|
||||
# hook save/load functions
|
||||
def load_attr():
|
||||
"inital loading of buffer data from given attribute."
|
||||
target = self.obj.attributes.get(self.attrname)
|
||||
if target is not None and not isinstance(target, basestring):
|
||||
typ = type(target).__name__
|
||||
self.caller.msg("{RWARNING! Saving this buffer will overwrite the current attribute (of type %s) with a string!{n" % typ)
|
||||
return target and str(target) or ""
|
||||
|
||||
def save_attr():
|
||||
"""
|
||||
Save line buffer to given attribute name. This should
|
||||
return True if successful and also report its status.
|
||||
"""
|
||||
self.obj.attributes.add(self.attrname, self.editor.buffer)
|
||||
self.caller.msg("Saved.")
|
||||
return True
|
||||
|
||||
def quit_hook():
|
||||
"Example quit hook. Since it's given, it's responsible for giving feedback messages."
|
||||
self.caller.msg("Exited Editor.")
|
||||
|
||||
editor_key = "%s/%s" % (self.objname, self.attrname)
|
||||
# start editor, it will handle things from here.
|
||||
self.editor = LineEditor(self.caller,
|
||||
loadfunc=load_attr,
|
||||
savefunc=save_attr,
|
||||
quitfunc=quit_hook,
|
||||
key=editor_key)
|
||||
360
evennia/contrib/menu_login.py
Normal file
360
evennia/contrib/menu_login.py
Normal file
|
|
@ -0,0 +1,360 @@
|
|||
"""
|
||||
Menu-driven login system
|
||||
|
||||
Contribution - Griatch 2011
|
||||
|
||||
|
||||
This is an alternative login system for Evennia, using the
|
||||
contrib.menusystem module. As opposed to the default system it doesn't
|
||||
use emails for authentication and also don't auto-creates a Character
|
||||
with the same name as the Player (instead assuming some sort of
|
||||
character-creation to come next).
|
||||
|
||||
|
||||
Install is simple:
|
||||
|
||||
To your settings file, add/edit the line:
|
||||
|
||||
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet"
|
||||
|
||||
That's it. Reload the server and try to log in to see it.
|
||||
|
||||
The initial login "graphic" is taken from strings in the module given
|
||||
by settings.CONNECTION_SCREEN_MODULE.
|
||||
|
||||
"""
|
||||
|
||||
import re
|
||||
import traceback
|
||||
from django.conf import settings
|
||||
from evennia import managers
|
||||
from evennia import utils, logger, create_player
|
||||
from evennia import Command, CmdSet
|
||||
from evennia import syscmdkeys
|
||||
from evennia.server.models import ServerConfig
|
||||
|
||||
from contrib.menusystem import MenuNode, MenuTree
|
||||
|
||||
CMD_LOGINSTART = syscmdkeys.CMD_LOGINSTART
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
|
||||
|
||||
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
|
||||
|
||||
|
||||
# Commands run on the unloggedin screen. Note that this is not using
|
||||
# settings.UNLOGGEDIN_CMDSET but the menu system, which is why some are
|
||||
# named for the numbers in the menu.
|
||||
#
|
||||
# Also note that the menu system will automatically assign all
|
||||
# commands used in its structure a property "menutree" holding a reference
|
||||
# back to the menutree. This allows the commands to do direct manipulation
|
||||
# for example by triggering a conditional jump to another node.
|
||||
#
|
||||
|
||||
# Menu entry 1a - Entering a Username
|
||||
|
||||
class CmdBackToStart(Command):
|
||||
"""
|
||||
Step back to node0
|
||||
"""
|
||||
key = CMD_NOINPUT
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
self.menutree.goto("START")
|
||||
|
||||
|
||||
class CmdUsernameSelect(Command):
|
||||
"""
|
||||
Handles the entering of a username and
|
||||
checks if it exists.
|
||||
"""
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
player = managers.players.get_player_from_name(self.args)
|
||||
if not player:
|
||||
self.caller.msg("{rThis account name couldn't be found. Did you create it? If you did, make sure you spelled it right (case doesn't matter).{n")
|
||||
self.menutree.goto("node1a")
|
||||
else:
|
||||
# store the player so next step can find it
|
||||
self.menutree.player = player
|
||||
self.caller.msg(echo=False)
|
||||
self.menutree.goto("node1b")
|
||||
|
||||
|
||||
# Menu entry 1b - Entering a Password
|
||||
|
||||
class CmdPasswordSelectBack(Command):
|
||||
"""
|
||||
Steps back from the Password selection
|
||||
"""
|
||||
key = CMD_NOINPUT
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
self.menutree.goto("node1a")
|
||||
self.caller.msg(echo=True)
|
||||
|
||||
|
||||
class CmdPasswordSelect(Command):
|
||||
"""
|
||||
Handles the entering of a password and logs into the game.
|
||||
"""
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
self.caller.msg(echo=True)
|
||||
if not hasattr(self.menutree, "player"):
|
||||
self.caller.msg("{rSomething went wrong! The player was not remembered from last step!{n")
|
||||
self.menutree.goto("node1a")
|
||||
return
|
||||
player = self.menutree.player
|
||||
if not player.check_password(self.args):
|
||||
self.caller.msg("{rIncorrect password.{n")
|
||||
self.menutree.goto("node1b")
|
||||
return
|
||||
|
||||
# before going on, check eventual bans
|
||||
bans = ServerConfig.objects.conf("server_bans")
|
||||
if bans and (any(tup[0]==player.name.lower() for tup in bans)
|
||||
or
|
||||
any(tup[2].match(self.caller.address) for tup in bans if tup[2])):
|
||||
# this is a banned IP or name!
|
||||
string = "{rYou have been banned and cannot continue from here."
|
||||
string += "\nIf you feel this ban is in error, please email an admin.{x"
|
||||
self.caller.msg(string)
|
||||
self.caller.sessionhandler.disconnect(self.caller, "Good bye! Disconnecting...")
|
||||
return
|
||||
|
||||
# we are ok, log us in.
|
||||
self.caller.msg("{gWelcome %s! Logging in ...{n" % player.key)
|
||||
#self.caller.session_login(player)
|
||||
self.caller.sessionhandler.login(self.caller, player)
|
||||
|
||||
# abort menu, do cleanup.
|
||||
self.menutree.goto("END")
|
||||
|
||||
|
||||
# Menu entry 2a - Creating a Username
|
||||
|
||||
class CmdUsernameCreate(Command):
|
||||
"""
|
||||
Handle the creation of a valid username
|
||||
"""
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
playername = self.args
|
||||
|
||||
# sanity check on the name
|
||||
if not re.findall('^[\w. @+-]+$', playername) or not (3 <= len(playername) <= 30):
|
||||
self.caller.msg("\n\r {rAccount name should be between 3 and 30 characters. Letters, spaces, dig\
|
||||
its and @/./+/-/_ only.{n") # this echoes the restrictions made by django's auth module.
|
||||
self.menutree.goto("node2a")
|
||||
return
|
||||
if managers.players.get_player_from_name(playername):
|
||||
self.caller.msg("\n\r {rAccount name %s already exists.{n" % playername)
|
||||
self.menutree.goto("node2a")
|
||||
return
|
||||
# store the name for the next step
|
||||
self.menutree.playername = playername
|
||||
self.caller.msg(echo=False)
|
||||
self.menutree.goto("node2b")
|
||||
|
||||
|
||||
# Menu entry 2b - Creating a Password
|
||||
|
||||
class CmdPasswordCreateBack(Command):
|
||||
"Step back from the password creation"
|
||||
key = CMD_NOINPUT
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
self.caller.msg(echo=True)
|
||||
self.menutree.goto("node2a")
|
||||
|
||||
|
||||
class CmdPasswordCreate(Command):
|
||||
"Handle the creation of a password. This also creates the actual Player/User object."
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Execute the command"
|
||||
password = self.args
|
||||
self.caller.msg(echo=False)
|
||||
if not hasattr(self.menutree, 'playername'):
|
||||
self.caller.msg("{rSomething went wrong! Playername not remembered from previous step!{n")
|
||||
self.menutree.goto("node2a")
|
||||
return
|
||||
playername = self.menutree.playername
|
||||
if len(password) < 3:
|
||||
# too short password
|
||||
string = "{rYour password must be at least 3 characters or longer."
|
||||
string += "\n\rFor best security, make it at least 8 characters "
|
||||
string += "long, avoid making it a real word and mix numbers "
|
||||
string += "into it.{n"
|
||||
self.caller.msg(string)
|
||||
self.menutree.goto("node2b")
|
||||
return
|
||||
# everything's ok. Create the new player account. Don't create
|
||||
# a Character here.
|
||||
try:
|
||||
permissions = settings.PERMISSION_PLAYER_DEFAULT
|
||||
typeclass = settings.BASE_PLAYER_TYPECLASS
|
||||
new_player = create_player(playername, None, password,
|
||||
typeclass=typeclass,
|
||||
permissions=permissions)
|
||||
if not new_player:
|
||||
self.msg("There was an error creating the Player. This error was logged. Contact an admin.")
|
||||
self.menutree.goto("START")
|
||||
return
|
||||
utils.init_new_player(new_player)
|
||||
|
||||
# join the new player to the public channel
|
||||
pchanneldef = settings.CHANNEL_PUBLIC
|
||||
if pchanneldef:
|
||||
pchannel = managers.channels.get_channel(pchanneldef[0])
|
||||
if not pchannel.connect(new_player):
|
||||
string = "New player '%s' could not connect to public channel!" % new_player.key
|
||||
logger.log_errmsg(string)
|
||||
|
||||
# tell the caller everything went well.
|
||||
string = "{gA new account '%s' was created. Now go log in from the menu!{n"
|
||||
self.caller.msg(string % (playername))
|
||||
self.menutree.goto("START")
|
||||
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
|
||||
# won't see any errors at all.
|
||||
string = "%s\nThis is a bug. Please e-mail an admin if the problem persists."
|
||||
self.caller.msg(string % (traceback.format_exc()))
|
||||
logger.log_errmsg(traceback.format_exc())
|
||||
|
||||
|
||||
# Menu entry 3 - help screen
|
||||
|
||||
LOGIN_SCREEN_HELP = \
|
||||
"""
|
||||
Welcome to %s!
|
||||
|
||||
To login you need to first create an account. This is easy and
|
||||
free to do: Choose option {w(1){n in the menu and enter an account
|
||||
name and password when prompted. Obs- the account name is {wnot{n
|
||||
the name of the Character you will play in the game!
|
||||
|
||||
It's always a good idea (not only here, but everywhere on the net)
|
||||
to not use a regular word for your password. Make it longer than 3
|
||||
characters (ideally 6 or more) and mix numbers and capitalization
|
||||
into it. The password also handles whitespace, so why not make it
|
||||
a small sentence - easy to remember, hard for a computer to crack.
|
||||
|
||||
Once you have an account, use option {w(2){n to log in using the
|
||||
account name and password you specified.
|
||||
|
||||
Use the {whelp{n command once you're logged in to get more
|
||||
aid. Hope you enjoy your stay!
|
||||
|
||||
|
||||
(return to go back)""" % settings.SERVERNAME
|
||||
|
||||
|
||||
# Menu entry 4
|
||||
|
||||
class CmdUnloggedinQuit(Command):
|
||||
"""
|
||||
We maintain a different version of the quit command
|
||||
here for unconnected players for the sake of simplicity. The logged in
|
||||
version is a bit more complicated.
|
||||
"""
|
||||
key = "4"
|
||||
aliases = ["quit", "qu", "q"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Simply close the connection."
|
||||
self.menutree.goto("END")
|
||||
self.caller.sessionhandler.disconnect(self.caller, "Good bye! Disconnecting...")
|
||||
|
||||
|
||||
# The login menu tree, using the commands above
|
||||
|
||||
START = MenuNode("START", text=utils.random_string_from_module(CONNECTION_SCREEN_MODULE),
|
||||
links=["node1a", "node2a", "node3", "END"],
|
||||
linktexts=["Log in with an existing account",
|
||||
"Create a new account",
|
||||
"Help",
|
||||
"Quit"],
|
||||
selectcmds=[None, None, None, CmdUnloggedinQuit])
|
||||
|
||||
node1a = MenuNode("node1a", text="Please enter your account name (empty to abort).",
|
||||
links=["START", "node1b"],
|
||||
helptext=["Enter the account name you previously registered with."],
|
||||
keywords=[CMD_NOINPUT, CMD_NOMATCH],
|
||||
selectcmds=[CmdBackToStart, CmdUsernameSelect],
|
||||
nodefaultcmds=True) # if we don't, default help/look will be triggered by names starting with l/h ...
|
||||
node1b = MenuNode("node1b", text="Please enter your password (empty to go back).",
|
||||
links=["node1a", "END"],
|
||||
keywords=[CMD_NOINPUT, CMD_NOMATCH],
|
||||
selectcmds=[CmdPasswordSelectBack, CmdPasswordSelect],
|
||||
nodefaultcmds=True)
|
||||
|
||||
node2a = MenuNode("node2a", text="Please enter your desired account name (empty to abort).",
|
||||
links=["START", "node2b"],
|
||||
helptext="Account name can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.",
|
||||
keywords=[CMD_NOINPUT, CMD_NOMATCH],
|
||||
selectcmds=[CmdBackToStart, CmdUsernameCreate],
|
||||
nodefaultcmds=True)
|
||||
node2b = MenuNode("node2b", text="Please enter your password (empty to go back).",
|
||||
links=["node2a", "START"],
|
||||
helptext="Try to pick a long and hard-to-guess password.",
|
||||
keywords=[CMD_NOINPUT, CMD_NOMATCH],
|
||||
selectcmds=[CmdPasswordCreateBack, CmdPasswordCreate],
|
||||
nodefaultcmds=True)
|
||||
node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP,
|
||||
links=["START"],
|
||||
helptext="",
|
||||
keywords=[CMD_NOINPUT],
|
||||
selectcmds=[CmdBackToStart])
|
||||
|
||||
|
||||
# access commands
|
||||
|
||||
class UnloggedInCmdSet(CmdSet):
|
||||
"Cmdset for the unloggedin state"
|
||||
key = "DefaultUnloggedin"
|
||||
priority = 0
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Called when cmdset is first created"
|
||||
self.add(CmdUnloggedinLook())
|
||||
|
||||
|
||||
class CmdUnloggedinLook(Command):
|
||||
"""
|
||||
An unloggedin version of the look command. This is called by the server
|
||||
when the player first connects. It sets up the menu before handing off
|
||||
to the menu's own look command..
|
||||
"""
|
||||
key = CMD_LOGINSTART
|
||||
aliases = ["look", "l"]
|
||||
locks = "cmd:all()"
|
||||
arg_regex = r"^$"
|
||||
|
||||
def func(self):
|
||||
"Execute the menu"
|
||||
menu = MenuTree(self.caller, nodes=(START, node1a, node1b,
|
||||
node2a, node2b, node3),
|
||||
exec_end=None)
|
||||
menu.start()
|
||||
616
evennia/contrib/menusystem.py
Normal file
616
evennia/contrib/menusystem.py
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
"""
|
||||
Evennia menu system.
|
||||
|
||||
Contribution - Griatch 2011
|
||||
|
||||
This module offers the ability for admins to let their game be fully
|
||||
or partly menu-driven. Menu choices can be numbered or use arbitrary
|
||||
keys. There are also some formatting options, such a putting options
|
||||
in one or more collumns.
|
||||
|
||||
The menu system consists of a MenuTree object populated by MenuNode
|
||||
objects. Nodes are linked together with automatically created commands
|
||||
so the player may select and traverse the menu. Each node can display
|
||||
text and show options, but also execute arbitrary code to act on the
|
||||
system and the calling object when they are selected.
|
||||
|
||||
There is also a simple Yes/No function supplied. This will create a
|
||||
one-off Yes/No question and executes a given code depending on which
|
||||
choice was made.
|
||||
|
||||
To test, add this to the default cmdset
|
||||
|
||||
"""
|
||||
from types import MethodType
|
||||
from evennia import syscmdkeys
|
||||
|
||||
from evennia import Command, CmdSet, utils
|
||||
from evennia import default_cmds, logger
|
||||
|
||||
# imported only to make it available during execution of code blocks
|
||||
import ev
|
||||
|
||||
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
|
||||
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
|
||||
|
||||
|
||||
#
|
||||
# Commands used by the Menu system
|
||||
#
|
||||
|
||||
class CmdMenuNode(Command):
|
||||
"""
|
||||
Parent for menu selection commands.
|
||||
"""
|
||||
key = "selection"
|
||||
aliases = []
|
||||
locks = "cmd:all()"
|
||||
help_category = "Menu"
|
||||
|
||||
menutree = None
|
||||
callback = None
|
||||
# deprecated
|
||||
code = None
|
||||
|
||||
def func(self):
|
||||
"Execute a selection"
|
||||
|
||||
if self.callback:
|
||||
try:
|
||||
self.callback()
|
||||
except Exception, e:
|
||||
self.caller.msg("%s\n{rThere was an error with this selection.{n" % e)
|
||||
elif self.code:
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.func.")
|
||||
try:
|
||||
exec(self.code)
|
||||
except Exception, e:
|
||||
self.caller.msg("%s\n{rThere was an error with this selection.{n" % e)
|
||||
else:
|
||||
self.caller.msg("{rThis option is not available.{n")
|
||||
|
||||
|
||||
class CmdMenuLook(default_cmds.CmdLook):
|
||||
"""
|
||||
ooc look
|
||||
|
||||
Usage:
|
||||
look
|
||||
|
||||
This is a Menu version of the look command. It will normally show
|
||||
the options available, otherwise works like the normal look
|
||||
command..
|
||||
"""
|
||||
key = "look"
|
||||
aliases = ["l", "ls"]
|
||||
locks = "cmd:all()"
|
||||
help_cateogory = "General"
|
||||
|
||||
def func(self):
|
||||
"implement the menu look command"
|
||||
if self.caller.db._menu_data:
|
||||
# if we have menu data, try to use that.
|
||||
lookstring = self.caller.db._menu_data.get("look", None)
|
||||
if lookstring:
|
||||
self.caller.msg(lookstring)
|
||||
return
|
||||
# otherwise we use normal look
|
||||
super(CmdMenuLook, self).func()
|
||||
|
||||
|
||||
class CmdMenuHelp(default_cmds.CmdHelp):
|
||||
"""
|
||||
help
|
||||
|
||||
Usage:
|
||||
help
|
||||
|
||||
Get help specific to the menu, if available. If not,
|
||||
works like the normal help command.
|
||||
"""
|
||||
key = "help"
|
||||
aliases = "h"
|
||||
locks = "cmd:all()"
|
||||
help_category = "Menu"
|
||||
|
||||
def func(self):
|
||||
"implement the menu help command"
|
||||
if self.caller.db._menu_data:
|
||||
# if we have menu data, try to use that.
|
||||
lookstring = self.caller.db._menu_data.get("help", None)
|
||||
if lookstring:
|
||||
self.caller.msg(lookstring)
|
||||
return
|
||||
# otherwise we use normal help
|
||||
super(CmdMenuHelp, self).func()
|
||||
|
||||
|
||||
class MenuCmdSet(CmdSet):
|
||||
"""
|
||||
Cmdset for the menu. Will replace all other commands.
|
||||
This always has a few basic commands available.
|
||||
|
||||
Note that you must always supply a way to exit the
|
||||
cmdset manually!
|
||||
"""
|
||||
key = "menucmdset"
|
||||
priority = 1
|
||||
mergetype = "Replace"
|
||||
# secure the menu against local cmdsets (but leave channels)
|
||||
no_objs = True
|
||||
no_exits = True
|
||||
no_channels = False
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"populate cmdset"
|
||||
pass
|
||||
|
||||
|
||||
#
|
||||
# Menu Node system
|
||||
#
|
||||
|
||||
class MenuTree(object):
|
||||
"""
|
||||
The menu tree object holds the full menu structure consisting of
|
||||
MenuNodes. Each node is identified by a unique key. The tree
|
||||
allows for traversal of nodes as well as entering and exiting the
|
||||
tree as needed. For safety, being in a menu will not survive a
|
||||
server reboot.
|
||||
|
||||
A menutree has two special node keys given by 'startnode' and
|
||||
'endnode' arguments. The startnode is where the user will start
|
||||
upon first entering the menu. The endnode need not actually
|
||||
exist, the moment it is linked to and that link is used, the menu
|
||||
will be exited and cleanups run. The default keys for these are
|
||||
'START' and 'END' respectively.
|
||||
|
||||
"""
|
||||
def __init__(self, caller, nodes=None,
|
||||
startnode="START", endnode="END", exec_end="look"):
|
||||
"""
|
||||
We specify startnode/endnode so that the system knows where to
|
||||
enter and where to exit the menu tree. If nodes is given, it
|
||||
shuld be a list of valid node objects to add to the tree.
|
||||
|
||||
exec_end - if not None, will execute the given command string
|
||||
directly after the menu system has been exited.
|
||||
"""
|
||||
self.tree = {}
|
||||
self.startnode = startnode
|
||||
self.endnode = endnode
|
||||
self.exec_end = exec_end
|
||||
self.caller = caller
|
||||
if nodes and utils.is_iter(nodes):
|
||||
for node in nodes:
|
||||
self.add(node)
|
||||
|
||||
def start(self):
|
||||
"""
|
||||
Initialize the menu
|
||||
"""
|
||||
self.goto(self.startnode)
|
||||
|
||||
def add(self, menunode):
|
||||
"""
|
||||
Add a menu node object to the tree. Each node itself keeps
|
||||
track of which nodes it is connected to.
|
||||
"""
|
||||
self.tree[menunode.key] = menunode
|
||||
|
||||
def goto(self, key):
|
||||
"""
|
||||
Go to a key in the tree. This sets up the cmdsets on the
|
||||
caller so that they match the choices in that node.
|
||||
"""
|
||||
if key == self.endnode:
|
||||
# if we was given the END node key, we clean up immediately.
|
||||
self.caller.cmdset.delete("menucmdset")
|
||||
del self.caller.db._menu_data
|
||||
if self.exec_end is not None:
|
||||
self.caller.execute_cmd(self.exec_end)
|
||||
return
|
||||
# not exiting, look for a valid code.
|
||||
node = self.tree.get(key, None)
|
||||
# make caller available on node
|
||||
node.caller = self.caller
|
||||
if node:
|
||||
# call on-node callback
|
||||
if node.callback:
|
||||
try:
|
||||
node.callback()
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
self.caller.msg("{rNode callback could not be executed for node %s. Continuing anyway.{n" % key)
|
||||
if node.code:
|
||||
# Execute eventual code active on this node. self.caller is available at this point.
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
try:
|
||||
exec(node.code)
|
||||
except Exception:
|
||||
self.caller.msg("{rCode could not be executed for node %s. Continuing anyway.{n" % key)
|
||||
# initialize - this creates new cmdset
|
||||
node.init(self)
|
||||
# clean old menu cmdset and replace with the new one
|
||||
self.caller.cmdset.delete("menucmdset")
|
||||
self.caller.cmdset.add(node.cmdset)
|
||||
# set the menu flag data for the default commands
|
||||
self.caller.db._menu_data = {"help": node.helptext,
|
||||
"look": str(node.text)}
|
||||
# display the node
|
||||
self.caller.msg(node.text)
|
||||
else:
|
||||
self.caller.msg("{rMenu node '%s' does not exist - maybe it's not created yet..{n" % key)
|
||||
|
||||
|
||||
class MenuNode(object):
|
||||
"""
|
||||
This represents a node in a menu tree. The node will display its
|
||||
textual content and offer menu links to other nodes (the relevant
|
||||
commands are created automatically)
|
||||
|
||||
"""
|
||||
def __init__(self, key, text="", links=None, linktexts=None,
|
||||
keywords=None, cols=1, helptext=None,
|
||||
selectcmds=None, callback=None, code="", nodefaultcmds=False, separator=""):
|
||||
"""
|
||||
key - the unique identifier of this node.
|
||||
text - is the text that will be displayed at top when viewing this
|
||||
node.
|
||||
links - a list of keys for unique menunodes this is connected to.
|
||||
The actual keys will not printed - keywords will be used
|
||||
(or a number)
|
||||
linktexts - an optional list of texts to describe the links. Must
|
||||
match link list if defined. Entries can be None to not
|
||||
generate any extra text for a particular link.
|
||||
keywords - an optional list of unique keys for choosing links. Must
|
||||
match links list. If not given, index numbers will be used.
|
||||
Also individual list entries can be None and will be replaed
|
||||
by indices. If CMD_NOMATCH or CMD_NOENTRY, no text will be
|
||||
generated to indicate the option exists.
|
||||
cols - how many columns to use for displaying options.
|
||||
helptext - if defined, this is shown when using the help command
|
||||
instead of the normal help index.
|
||||
selectcmds- a list of custom cmdclasses for handling each option.
|
||||
Must match links list, but some entries may be set to None
|
||||
to use default menu cmds. The given command's key will be
|
||||
used for the menu list entry unless it's CMD_NOMATCH or
|
||||
CMD_NOENTRY, in which case no text will be generated. These
|
||||
commands have access to self.menutree and so can be used to
|
||||
select nodes.
|
||||
code - functional code. Deprecated. This will be executed just before this
|
||||
node is loaded (i.e. as soon after it's been selected from
|
||||
another node). self.caller is available to call from this
|
||||
code block, as well as ev.
|
||||
callback - function callback. This will be called as callback(currentnode) just
|
||||
before this node is loaded (i.e. as soon as possible as it's
|
||||
been selected from another node). currentnode.caller is available.
|
||||
nodefaultcmds - if true, don't offer the default help and look commands
|
||||
in the node
|
||||
separator - this string will be put on the line between menu nodes.
|
||||
"""
|
||||
self.key = key
|
||||
self.cmdset = None
|
||||
self.links = links
|
||||
self.linktexts = linktexts
|
||||
self.keywords = keywords
|
||||
self.cols = cols
|
||||
self.selectcmds = selectcmds
|
||||
self.code = code
|
||||
self.callback = MethodType(callback, self, MenuNode) if callback else None
|
||||
self.nodefaultcmds = nodefaultcmds
|
||||
self.separator = separator
|
||||
Nlinks = len(self.links)
|
||||
|
||||
if code:
|
||||
ev.logger.log_depmsg("menusystem.code is deprecated. Use menusystem.callback.")
|
||||
|
||||
# validate the input
|
||||
if not self.links:
|
||||
self.links = []
|
||||
if not self.linktexts or (len(self.linktexts) != Nlinks):
|
||||
self.linktexts = [None for i in range(Nlinks)]
|
||||
if not self.keywords or (len(self.keywords) != Nlinks):
|
||||
self.keywords = [None for i in range(Nlinks)]
|
||||
if not selectcmds or (len(self.selectcmds) != Nlinks):
|
||||
self.selectcmds = [None for i in range(Nlinks)]
|
||||
|
||||
# Format default text for the menu-help command
|
||||
if not helptext:
|
||||
helptext = "Select one of the valid options ("
|
||||
for i in range(Nlinks):
|
||||
if self.keywords[i]:
|
||||
if self.keywords[i] not in (CMD_NOMATCH, CMD_NOINPUT):
|
||||
helptext += "%s, " % self.keywords[i]
|
||||
else:
|
||||
helptext += "%s, " % (i + 1)
|
||||
helptext = helptext.rstrip(", ") + ")"
|
||||
self.helptext = helptext
|
||||
|
||||
# Format text display
|
||||
string = ""
|
||||
if text:
|
||||
string += "%s\n" % text
|
||||
|
||||
# format the choices into as many collumns as specified
|
||||
choices = []
|
||||
for ilink, link in enumerate(self.links):
|
||||
choice = ""
|
||||
if self.keywords[ilink]:
|
||||
if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT):
|
||||
choice += "{g{lc%s{lt%s{le{n" % (self.keywords[ilink], self.keywords[ilink])
|
||||
else:
|
||||
choice += "{g {lc%i{lt%i{le{n" % ((ilink + 1), (ilink + 1))
|
||||
if self.linktexts[ilink]:
|
||||
choice += " - %s" % self.linktexts[ilink]
|
||||
choices.append(choice)
|
||||
cols = [[] for i in range(min(len(choices), cols))]
|
||||
while True:
|
||||
for i in range(len(cols)):
|
||||
if not choices:
|
||||
cols[i].append("")
|
||||
else:
|
||||
cols[i].append(choices.pop(0))
|
||||
if not choices:
|
||||
break
|
||||
ftable = utils.format_table(cols)
|
||||
for row in ftable:
|
||||
string += "\n" + "".join(row)
|
||||
# store text
|
||||
self.text = self.separator + "\n" + string.rstrip()
|
||||
|
||||
def init(self, menutree):
|
||||
"""
|
||||
Called by menu tree. Initializes the commands needed by
|
||||
the menutree structure.
|
||||
"""
|
||||
# Create the relevant cmdset
|
||||
self.cmdset = MenuCmdSet()
|
||||
if not self.nodefaultcmds:
|
||||
# add default menu commands
|
||||
self.cmdset.add(CmdMenuLook())
|
||||
self.cmdset.add(CmdMenuHelp())
|
||||
|
||||
for i, link in enumerate(self.links):
|
||||
if self.selectcmds[i]:
|
||||
cmd = self.selectcmds[i]()
|
||||
else:
|
||||
# this is the operable command, it moves us to the next node.
|
||||
cmd = CmdMenuNode()
|
||||
cmd.key = str(i + 1)
|
||||
cmd.link = link
|
||||
def _callback(self):
|
||||
self.menutree.goto(self.link)
|
||||
cmd.callback = MethodType(_callback, cmd, CmdMenuNode)
|
||||
# also custom commands get access to the menutree.
|
||||
cmd.menutree = menutree
|
||||
if self.keywords[i] and cmd.key not in (CMD_NOMATCH, CMD_NOINPUT):
|
||||
cmd.aliases = [self.keywords[i]]
|
||||
self.cmdset.add(cmd)
|
||||
|
||||
def __str__(self):
|
||||
"Returns the string representation."
|
||||
return self.text
|
||||
|
||||
|
||||
#
|
||||
# A simple yes/no question. Call this from a command to give object
|
||||
# a cmdset where they may say yes or no to a question. Does not
|
||||
# make use the node system since there is only one level of choice.
|
||||
#
|
||||
|
||||
def prompt_yesno(caller, question="", yesfunc=None, nofunc=None, yescode="", nocode="", default="N"):
|
||||
"""
|
||||
This sets up a simple yes/no questionnaire. Question will be
|
||||
asked, followed by a Y/[N] prompt where the [x] signifies the
|
||||
default selection. Note that this isn't making use of the menu
|
||||
node system.
|
||||
|
||||
yesfunc - function callback to be called as yesfunc(self) when choosing yes (self.caller is available)
|
||||
nofunc - function callback to be called as yesfunc(self) when choosing no (self.caller is available)
|
||||
yescode - deprecated, executable code
|
||||
nocode - "
|
||||
"""
|
||||
|
||||
# creating and defining commands
|
||||
cmdyes = CmdMenuNode(key="yes", aliases=["y"])
|
||||
if yesfunc:
|
||||
cmdyes.yesfunc = yesfunc
|
||||
def _yesfunc(self):
|
||||
self.caller.cmdset.delete('menucmdset')
|
||||
del self.caller.db._menu_data
|
||||
self.yesfunc(self)
|
||||
cmdyes.callback = MethodType(_yesfunc, cmdyes, CmdMenuNode)
|
||||
|
||||
cmdno = CmdMenuNode(key="no", aliases=["n"])
|
||||
if nofunc:
|
||||
cmdno.nofunc = nofunc
|
||||
def _nofunc(self):
|
||||
self.caller.cmdset.delete('menucmdset')
|
||||
del self.caller.db._menu_data
|
||||
self.nofunc(self) if self.nofunc else None
|
||||
cmdno.callback = MethodType(_nofunc, cmdno, CmdMenuNode)
|
||||
|
||||
errorcmd = CmdMenuNode(key=CMD_NOMATCH)
|
||||
def _errorcmd(self):
|
||||
self.caller.msg("Please choose either Yes or No.")
|
||||
errorcmd.callback = MethodType(_errorcmd, errorcmd, CmdMenuNode)
|
||||
|
||||
defaultcmd = CmdMenuNode(key=CMD_NOINPUT)
|
||||
def _defaultcmd(self):
|
||||
self.caller.execute_cmd('%s' % default)
|
||||
defaultcmd.callback = MethodType(_defaultcmd, defaultcmd, CmdMenuNode)
|
||||
|
||||
# code exec is deprecated:
|
||||
if yescode:
|
||||
ev.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
cmdyes.code = yescode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
|
||||
if nocode:
|
||||
ev.logger.log_depmsg("yesnosystem.code is deprecated. Use yesnosystem.callback.")
|
||||
cmdno.code = nocode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
|
||||
|
||||
# creating cmdset (this will already have look/help commands)
|
||||
yesnocmdset = MenuCmdSet()
|
||||
yesnocmdset.add(cmdyes)
|
||||
yesnocmdset.add(cmdno)
|
||||
yesnocmdset.add(errorcmd)
|
||||
yesnocmdset.add(defaultcmd)
|
||||
yesnocmdset.add(CmdMenuLook())
|
||||
yesnocmdset.add(CmdMenuHelp())
|
||||
|
||||
# assinging menu data flags to caller.
|
||||
caller.db._menu_data = {"help": "Please select Yes or No.",
|
||||
"look": "Please select Yes or No."}
|
||||
# assign cmdset and ask question
|
||||
caller.cmdset.add(yesnocmdset)
|
||||
if default == "Y":
|
||||
prompt = "{lcY{lt[Y]{le/{lcN{ltN{le"
|
||||
else:
|
||||
prompt = "{lcY{ltY{le/{lcN{lt[N]{le"
|
||||
prompt = "%s %s: " % (question, prompt)
|
||||
caller.msg(prompt)
|
||||
|
||||
|
||||
#
|
||||
# A simple choice question. Call this from a command to give object
|
||||
# a cmdset where they need to make a choice. Does not
|
||||
# make use the node system since there is only one level of choice.
|
||||
#
|
||||
|
||||
def prompt_choice(caller, question="", prompts=None, choicefunc=None, force_choose=False):
|
||||
"""
|
||||
This sets up a simple choice questionnaire. Question will be
|
||||
asked, followed by a serie of prompts. Note that this isn't
|
||||
making use of the menu node system.
|
||||
|
||||
caller - the object calling and being offered the choice
|
||||
question - text describing the offered choice
|
||||
prompts - list of choices
|
||||
choicefunc - functions callback to be called as func(self) when
|
||||
make choice (self.caller is available) The function's definision
|
||||
should be like func(self, menu_node), and menu_node.key is user's
|
||||
choice.
|
||||
force_choose - force user to make a choice or not
|
||||
"""
|
||||
|
||||
# creating and defining commands
|
||||
count = 0
|
||||
choices = ""
|
||||
commands = []
|
||||
for choice in utils.makeiter(prompts):
|
||||
count += 1
|
||||
choices += "\n{lc%d{lt[%d]{le %s" % (count, count, choice)
|
||||
|
||||
cmdfunc = CmdMenuNode(key="%d" % count)
|
||||
if choicefunc:
|
||||
cmdfunc.choicefunc = choicefunc
|
||||
def _choicefunc(self):
|
||||
self.caller.cmdset.delete('menucmdset')
|
||||
del self.caller.db._menu_data
|
||||
self.choicefunc(self)
|
||||
cmdfunc.callback = MethodType(_choicefunc, cmdfunc, CmdMenuNode)
|
||||
|
||||
commands.append(cmdfunc)
|
||||
|
||||
if not force_choose:
|
||||
choices += "\n{lc{lt[No choice]{le"
|
||||
|
||||
prompt = question + choices + "\nPlease choose one."
|
||||
|
||||
errorcmd = CmdMenuNode(key=CMD_NOMATCH)
|
||||
if force_choose:
|
||||
def _errorcmd(self):
|
||||
self.caller.msg("You can only choose given choices.")
|
||||
else:
|
||||
if choicefunc:
|
||||
errorcmd.choicefunc = choicefunc
|
||||
def _errorcmd(self):
|
||||
self.caller.msg("No choice.")
|
||||
self.caller.cmdset.delete('menucmdset')
|
||||
del self.caller.db._menu_data
|
||||
self.choicefunc(self)
|
||||
errorcmd.callback = MethodType(_errorcmd, errorcmd, CmdMenuNode)
|
||||
|
||||
defaultcmd = CmdMenuNode(key=CMD_NOINPUT)
|
||||
if force_choose:
|
||||
def _defaultcmd(self):
|
||||
caller.msg(prompt)
|
||||
else:
|
||||
if choicefunc:
|
||||
defaultcmd.choicefunc = choicefunc
|
||||
def _defaultcmd(self):
|
||||
self.caller.msg("No choice.")
|
||||
self.caller.cmdset.delete('menucmdset')
|
||||
del self.caller.db._menu_data
|
||||
self.choicefunc(self)
|
||||
defaultcmd.callback = MethodType(_defaultcmd, defaultcmd, CmdMenuNode)
|
||||
|
||||
# creating cmdset (this will already have look/help commands)
|
||||
choicecmdset = MenuCmdSet()
|
||||
for cmdfunc in commands: choicecmdset.add(cmdfunc)
|
||||
choicecmdset.add(errorcmd)
|
||||
choicecmdset.add(defaultcmd)
|
||||
choicecmdset.add(CmdMenuLook())
|
||||
choicecmdset.add(CmdMenuHelp())
|
||||
|
||||
# assinging menu data flags to caller.
|
||||
caller.db._menu_data = {"help": "Please select.",
|
||||
"look": prompt}
|
||||
|
||||
# assign cmdset and ask question
|
||||
caller.cmdset.add(choicecmdset)
|
||||
caller.msg(prompt)
|
||||
|
||||
|
||||
#
|
||||
# Menu command test
|
||||
#
|
||||
|
||||
class CmdMenuTest(Command):
|
||||
"""
|
||||
testing menu module
|
||||
|
||||
Usage:
|
||||
menu
|
||||
menu yesno
|
||||
|
||||
This will test the menu system. The normal operation will produce
|
||||
a small menu tree you can move around in. The 'yesno' option will
|
||||
instead show a one-time yes/no question.
|
||||
|
||||
"""
|
||||
|
||||
key = "menu"
|
||||
locks = "cmd:all()"
|
||||
help_category = "Menu"
|
||||
|
||||
def func(self):
|
||||
"Testing the menu system"
|
||||
|
||||
if self.args.strip() == "yesno":
|
||||
"Testing the yesno question"
|
||||
prompt_yesno(self.caller, question="Please answer yes or no - Are you the master of this mud or not?",
|
||||
yesfunc=lambda self: self.caller.msg('{gGood for you!{n'),
|
||||
nofunc=lambda self: self.caller.msg('{GNow you are just being modest ...{n'),
|
||||
default="N")
|
||||
else:
|
||||
# testing the full menu-tree system
|
||||
|
||||
node0 = MenuNode("START", text="Start node. Select one of the links below. Here the links are ordered in one column.",
|
||||
links=["node1", "node2", "END"], linktexts=["Goto first node", "Goto second node", "Quit"])
|
||||
node1 = MenuNode("node1", text="First node. This node shows letters instead of numbers for the choices.",
|
||||
links=["END", "START"], linktexts=["Quit", "Back to start"], keywords=["q","b"])
|
||||
node2 = MenuNode("node2", text="Second node. This node lists choices in two columns.",
|
||||
links=["node3", "START"], linktexts=["Set an attribute", "Back to start"], cols=2)
|
||||
node3 = MenuNode("node3", text="Attribute 'menutest' set on you. You can examine it (only works if you are allowed to use the examine command) or remove it. You can also quit and examine it manually.",
|
||||
links=["node4", "node5", "node2", "END"], linktexts=["Remove attribute", "Examine attribute",
|
||||
"Back to second node", "Quit menu"], cols=2,
|
||||
callback=lambda self: self.caller.attributes.add("menutest",'Testing!'))
|
||||
node4 = MenuNode("node4", text="Attribute 'menutest' removed again.",
|
||||
links=["node2"], linktexts=["Back to second node."], cols=2,
|
||||
callback=lambda self: self.caller.attributes.remove("menutest"))
|
||||
node5 = MenuNode("node5", links=["node4", "node2"], linktexts=["Remove attribute", "Back to second node."], cols=2,
|
||||
callback=lambda self: self.caller.msg('%s/%s = %s' % (self.caller.key, 'menutest', self.caller.db.menutest)))
|
||||
|
||||
menu = MenuTree(self.caller, nodes=(node0, node1, node2, node3, node4, node5))
|
||||
menu.start()
|
||||
40
evennia/contrib/procpools/README.txt
Normal file
40
evennia/contrib/procpools/README.txt
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
|
||||
ProcPools
|
||||
---------
|
||||
|
||||
This contrib defines a process pool subsystem for Evennia.
|
||||
|
||||
A process pool handles a range of separately running processes that
|
||||
can accept information from the main Evennia process. The pool dynamically
|
||||
grows and shrinks depending on the need (and will queue requests if there
|
||||
are no free slots available).
|
||||
|
||||
The main use of this is to launch long-running, possibly blocking code
|
||||
in a way that will not freeze up the rest of the server. So you could
|
||||
execute time.sleep(10) on the process pool without anyone else on the
|
||||
server noticing anything.
|
||||
|
||||
This folder has the following contents:
|
||||
|
||||
ampoule/ - this is a separate library managing the process pool. You
|
||||
should not need to touch this.
|
||||
|
||||
Python Procpool
|
||||
---------------
|
||||
python_procpool.py - this implements a way to execute arbitrary python
|
||||
code on the procpool. Import run_async() from this
|
||||
module in order to use this functionality in-code
|
||||
(this is a replacement to the in-process run_async
|
||||
found in evennia.utils.utils).
|
||||
python_procpool_plugin.py - this is a plugin module for the python
|
||||
procpool, to start and add it to the server. Adding it
|
||||
is a single line in your settings file - see the header
|
||||
of the file for more info.
|
||||
|
||||
|
||||
|
||||
Adding other Procpools
|
||||
----------------------
|
||||
To add other types of procpools (such as for executing other remote languages
|
||||
than Python), you can pretty much mimic the layout of python_procpool.py
|
||||
and python_procpool_plugin.py.
|
||||
1
evennia/contrib/procpools/__init__.py
Normal file
1
evennia/contrib/procpools/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
23
evennia/contrib/procpools/ampoule/COPYING.txt
Normal file
23
evennia/contrib/procpools/ampoule/COPYING.txt
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
Copyright (c) 2008
|
||||
Valentino Volonghi
|
||||
Matthew Lefkowitz
|
||||
Copyright (c) 2009 Canonical Ltd.
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including
|
||||
without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to
|
||||
permit persons to whom the Software is furnished to do so, subject to
|
||||
the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be
|
||||
included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
20
evennia/contrib/procpools/ampoule/EVENNIA.txt
Normal file
20
evennia/contrib/procpools/ampoule/EVENNIA.txt
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
|
||||
AMPOULE
|
||||
-------
|
||||
|
||||
https://launchpad.net/ampoule
|
||||
|
||||
AMPoule is a process management system using Twisted spawnProcess
|
||||
functionality. It uses AMP to pipe messages to a process Pool that it
|
||||
manages. The service is called ProcPool in Evennia settings.
|
||||
|
||||
AMPoule's very good, but unfortunately the source is very poorly
|
||||
documented. Hence the source in this directory does not comform to
|
||||
Evennia's normally rigid standards - for now we try to edit it as
|
||||
little as possible so as to make it easy to apply upstream updates
|
||||
down the line.
|
||||
|
||||
Changes made by Evennia are minor - it's mainly limiting spam to the
|
||||
log and an added ability to turn this on/off through settings. Most
|
||||
Evennia related code are found in evennia/server/procpool.py and
|
||||
evennia/server/server.py.
|
||||
4
evennia/contrib/procpools/ampoule/__init__.py
Normal file
4
evennia/contrib/procpools/ampoule/__init__.py
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
from pool import deferToAMPProcess, pp
|
||||
from commands import Shutdown, Ping, Echo
|
||||
from child import AMPChild
|
||||
__version__ = "0.2.1"
|
||||
60
evennia/contrib/procpools/ampoule/child.py
Normal file
60
evennia/contrib/procpools/ampoule/child.py
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
"""
|
||||
This defines the the parent for all subprocess children.
|
||||
|
||||
Inherit from this to define a new type of subprocess.
|
||||
|
||||
"""
|
||||
|
||||
from twisted.python import log
|
||||
from twisted.internet import error
|
||||
from twisted.protocols import amp
|
||||
from contrib.procpools.ampoule.commands import Echo, Shutdown, Ping
|
||||
|
||||
class AMPChild(amp.AMP):
|
||||
def __init__(self):
|
||||
super(AMPChild, self).__init__(self)
|
||||
self.shutdown = False
|
||||
|
||||
def connectionLost(self, reason):
|
||||
amp.AMP.connectionLost(self, reason)
|
||||
from twisted.internet import reactor
|
||||
try:
|
||||
reactor.stop()
|
||||
except error.ReactorNotRunning:
|
||||
# woa, this means that something bad happened,
|
||||
# most probably we received a SIGINT. Now this is only
|
||||
# a problem when you use Ctrl+C to stop the main process
|
||||
# because it would send the SIGINT to child processes too.
|
||||
# In all other cases receiving a SIGINT here would be an
|
||||
# error condition and correctly restarted. maybe we should
|
||||
# use sigprocmask?
|
||||
pass
|
||||
if not self.shutdown:
|
||||
# if the shutdown wasn't explicit we presume that it's an
|
||||
# error condition and thus we return a -1 error returncode.
|
||||
import os
|
||||
os._exit(-1)
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
This method is needed to shutdown the child gently without
|
||||
generating an exception.
|
||||
"""
|
||||
#log.msg("Shutdown message received, goodbye.")
|
||||
self.shutdown = True
|
||||
return {}
|
||||
Shutdown.responder(shutdown)
|
||||
|
||||
def ping(self):
|
||||
"""
|
||||
Ping the child and return an answer
|
||||
"""
|
||||
return {'response': "pong"}
|
||||
Ping.responder(ping)
|
||||
|
||||
def echo(self, data):
|
||||
"""
|
||||
Echo some data through the child.
|
||||
"""
|
||||
return {'response': data}
|
||||
Echo.responder(echo)
|
||||
11
evennia/contrib/procpools/ampoule/commands.py
Normal file
11
evennia/contrib/procpools/ampoule/commands.py
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
from twisted.protocols import amp
|
||||
|
||||
class Shutdown(amp.Command):
|
||||
responseType = amp.QuitBox
|
||||
|
||||
class Ping(amp.Command):
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Echo(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
24
evennia/contrib/procpools/ampoule/iampoule.py
Normal file
24
evennia/contrib/procpools/ampoule/iampoule.py
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
from zope.interface import Interface
|
||||
|
||||
class IStarter(Interface):
|
||||
def startAMPProcess(ampChild, ampParent=None):
|
||||
"""
|
||||
@param ampChild: The AMP protocol spoken by the created child.
|
||||
@type ampChild: L{twisted.protocols.amp.AMP}
|
||||
|
||||
@param ampParent: The AMP protocol spoken by the parent.
|
||||
@type ampParent: L{twisted.protocols.amp.AMP}
|
||||
"""
|
||||
|
||||
def startPythonProcess(prot, *args):
|
||||
"""
|
||||
@param prot: a L{protocol.ProcessProtocol} subclass
|
||||
@type prot: L{protocol.ProcessProtocol}
|
||||
|
||||
@param args: a tuple of arguments that will be passed to the
|
||||
child process.
|
||||
|
||||
@return: a tuple of the child process and the deferred finished.
|
||||
finished triggers when the subprocess dies for any reason.
|
||||
"""
|
||||
|
||||
302
evennia/contrib/procpools/ampoule/main.py
Normal file
302
evennia/contrib/procpools/ampoule/main.py
Normal file
|
|
@ -0,0 +1,302 @@
|
|||
import os
|
||||
import sys
|
||||
import imp
|
||||
import itertools
|
||||
|
||||
from zope.interface import implements
|
||||
|
||||
from twisted.internet import reactor, protocol, defer, error
|
||||
from twisted.python import log, util, reflect
|
||||
from twisted.protocols import amp
|
||||
from twisted.python import runtime
|
||||
from twisted.python.compat import set
|
||||
|
||||
from contrib.procpools.ampoule import iampoule
|
||||
|
||||
gen = itertools.count()
|
||||
|
||||
if runtime.platform.isWindows():
|
||||
IS_WINDOWS = True
|
||||
TO_CHILD = 0
|
||||
FROM_CHILD = 1
|
||||
else:
|
||||
IS_WINDOWS = False
|
||||
TO_CHILD = 3
|
||||
FROM_CHILD = 4
|
||||
|
||||
class AMPConnector(protocol.ProcessProtocol):
|
||||
"""
|
||||
A L{ProcessProtocol} subclass that can understand and speak AMP.
|
||||
|
||||
@ivar amp: the children AMP process
|
||||
@type amp: L{amp.AMP}
|
||||
|
||||
@ivar finished: a deferred triggered when the process dies.
|
||||
@type finished: L{defer.Deferred}
|
||||
|
||||
@ivar name: Unique name for the connector, much like a pid.
|
||||
@type name: int
|
||||
"""
|
||||
|
||||
def __init__(self, proto, name=None):
|
||||
"""
|
||||
@param proto: An instance or subclass of L{amp.AMP}
|
||||
@type proto: L{amp.AMP}
|
||||
|
||||
@param name: optional name of the subprocess.
|
||||
@type name: int
|
||||
"""
|
||||
self.finished = defer.Deferred()
|
||||
self.amp = proto
|
||||
self.name = name
|
||||
if name is None:
|
||||
self.name = gen.next()
|
||||
|
||||
def signalProcess(self, signalID):
|
||||
"""
|
||||
Send the signal signalID to the child process
|
||||
|
||||
@param signalID: The signal ID that you want to send to the
|
||||
corresponding child
|
||||
@type signalID: C{str} or C{int}
|
||||
"""
|
||||
return self.transport.signalProcess(signalID)
|
||||
|
||||
def connectionMade(self):
|
||||
#log.msg("Subprocess %s started." % (self.name,))
|
||||
self.amp.makeConnection(self)
|
||||
|
||||
# Transport
|
||||
disconnecting = False
|
||||
|
||||
def write(self, data):
|
||||
if IS_WINDOWS:
|
||||
self.transport.write(data)
|
||||
else:
|
||||
self.transport.writeToChild(TO_CHILD, data)
|
||||
|
||||
def loseConnection(self):
|
||||
self.transport.closeChildFD(TO_CHILD)
|
||||
self.transport.closeChildFD(FROM_CHILD)
|
||||
self.transport.loseConnection()
|
||||
|
||||
def getPeer(self):
|
||||
return ('subprocess %i' % self.name,)
|
||||
|
||||
def getHost(self):
|
||||
return ('Evennia Server',)
|
||||
|
||||
def childDataReceived(self, childFD, data):
|
||||
if childFD == FROM_CHILD:
|
||||
self.amp.dataReceived(data)
|
||||
return
|
||||
self.errReceived(data)
|
||||
|
||||
def errReceived(self, data):
|
||||
for line in data.strip().splitlines():
|
||||
log.msg("FROM %s: %s" % (self.name, line))
|
||||
|
||||
def processEnded(self, status):
|
||||
#log.msg("Process: %s ended" % (self.name,))
|
||||
self.amp.connectionLost(status)
|
||||
if status.check(error.ProcessDone):
|
||||
self.finished.callback('')
|
||||
return
|
||||
self.finished.errback(status)
|
||||
|
||||
BOOTSTRAP = """\
|
||||
import sys
|
||||
|
||||
def main(reactor, ampChildPath):
|
||||
from twisted.application import reactors
|
||||
reactors.installReactor(reactor)
|
||||
|
||||
from twisted.python import log
|
||||
%s
|
||||
|
||||
from twisted.internet import reactor, stdio
|
||||
from twisted.python import reflect, runtime
|
||||
|
||||
ampChild = reflect.namedAny(ampChildPath)
|
||||
ampChildInstance = ampChild(*sys.argv[1:-2])
|
||||
if runtime.platform.isWindows():
|
||||
stdio.StandardIO(ampChildInstance)
|
||||
else:
|
||||
stdio.StandardIO(ampChildInstance, %s, %s)
|
||||
enter = getattr(ampChildInstance, '__enter__', None)
|
||||
if enter is not None:
|
||||
enter()
|
||||
try:
|
||||
reactor.run()
|
||||
except:
|
||||
if enter is not None:
|
||||
info = sys.exc_info()
|
||||
if not ampChildInstance.__exit__(*info):
|
||||
raise
|
||||
else:
|
||||
raise
|
||||
else:
|
||||
if enter is not None:
|
||||
ampChildInstance.__exit__(None, None, None)
|
||||
|
||||
main(sys.argv[-2], sys.argv[-1])
|
||||
""" % ('%s', TO_CHILD, FROM_CHILD)
|
||||
|
||||
# in the first spot above, either insert an empty string or
|
||||
# 'log.startLogging(sys.stderr)'
|
||||
# to start logging
|
||||
|
||||
class ProcessStarter(object):
|
||||
|
||||
implements(iampoule.IStarter)
|
||||
|
||||
connectorFactory = AMPConnector
|
||||
def __init__(self, bootstrap=BOOTSTRAP, args=(), env={},
|
||||
path=None, uid=None, gid=None, usePTY=0,
|
||||
packages=(), childReactor="select"):
|
||||
"""
|
||||
@param bootstrap: Startup code for the child process
|
||||
@type bootstrap: C{str}
|
||||
|
||||
@param args: Arguments that should be supplied to every child
|
||||
created.
|
||||
@type args: C{tuple} of C{str}
|
||||
|
||||
@param env: Environment variables that should be present in the
|
||||
child environment
|
||||
@type env: C{dict}
|
||||
|
||||
@param path: Path in which to run the child
|
||||
@type path: C{str}
|
||||
|
||||
@param uid: if defined, the uid used to run the new process.
|
||||
@type uid: C{int}
|
||||
|
||||
@param gid: if defined, the gid used to run the new process.
|
||||
@type gid: C{int}
|
||||
|
||||
@param usePTY: Should the child processes use PTY processes
|
||||
@type usePTY: 0 or 1
|
||||
|
||||
@param packages: A tuple of packages that should be guaranteed
|
||||
to be importable in the child processes
|
||||
@type packages: C{tuple} of C{str}
|
||||
|
||||
@param childReactor: a string that sets the reactor for child
|
||||
processes
|
||||
@type childReactor: C{str}
|
||||
"""
|
||||
self.bootstrap = bootstrap
|
||||
self.args = args
|
||||
self.env = env
|
||||
self.path = path
|
||||
self.uid = uid
|
||||
self.gid = gid
|
||||
self.usePTY = usePTY
|
||||
self.packages = ("ampoule",) + packages
|
||||
self.packages = packages
|
||||
self.childReactor = childReactor
|
||||
|
||||
def __repr__(self):
|
||||
"""
|
||||
Represent the ProcessStarter with a string.
|
||||
"""
|
||||
return """ProcessStarter(bootstrap=%r,
|
||||
args=%r,
|
||||
env=%r,
|
||||
path=%r,
|
||||
uid=%r,
|
||||
gid=%r,
|
||||
usePTY=%r,
|
||||
packages=%r,
|
||||
childReactor=%r)""" % (self.bootstrap,
|
||||
self.args,
|
||||
self.env,
|
||||
self.path,
|
||||
self.uid,
|
||||
self.gid,
|
||||
self.usePTY,
|
||||
self.packages,
|
||||
self.childReactor)
|
||||
|
||||
def _checkRoundTrip(self, obj):
|
||||
"""
|
||||
Make sure that an object will properly round-trip through 'qual' and
|
||||
'namedAny'.
|
||||
|
||||
Raise a L{RuntimeError} if they aren't.
|
||||
"""
|
||||
tripped = reflect.namedAny(reflect.qual(obj))
|
||||
if tripped is not obj:
|
||||
raise RuntimeError("importing %r is not the same as %r" %
|
||||
(reflect.qual(obj), obj))
|
||||
|
||||
def startAMPProcess(self, ampChild, ampParent=None, ampChildArgs=()):
|
||||
"""
|
||||
@param ampChild: a L{ampoule.child.AMPChild} subclass.
|
||||
@type ampChild: L{ampoule.child.AMPChild}
|
||||
|
||||
@param ampParent: an L{amp.AMP} subclass that implements the parent
|
||||
protocol for this process pool
|
||||
@type ampParent: L{amp.AMP}
|
||||
"""
|
||||
self._checkRoundTrip(ampChild)
|
||||
fullPath = reflect.qual(ampChild)
|
||||
if ampParent is None:
|
||||
ampParent = amp.AMP
|
||||
prot = self.connectorFactory(ampParent())
|
||||
args = ampChildArgs + (self.childReactor, fullPath)
|
||||
return self.startPythonProcess(prot, *args)
|
||||
|
||||
|
||||
def startPythonProcess(self, prot, *args):
|
||||
"""
|
||||
@param prot: a L{protocol.ProcessProtocol} subclass
|
||||
@type prot: L{protocol.ProcessProtocol}
|
||||
|
||||
@param args: a tuple of arguments that will be added after the
|
||||
ones in L{self.args} to start the child process.
|
||||
|
||||
@return: a tuple of the child process and the deferred finished.
|
||||
finished triggers when the subprocess dies for any reason.
|
||||
"""
|
||||
spawnProcess(prot, self.bootstrap, self.args+args, env=self.env,
|
||||
path=self.path, uid=self.uid, gid=self.gid,
|
||||
usePTY=self.usePTY, packages=self.packages)
|
||||
|
||||
# XXX: we could wait for startup here, but ... is there really any
|
||||
# reason to? the pipe should be ready for writing. The subprocess
|
||||
# might not start up properly, but then, a subprocess might shut down
|
||||
# at any point too. So we just return amp and have this piece to be
|
||||
# synchronous.
|
||||
return prot.amp, prot.finished
|
||||
|
||||
def spawnProcess(processProtocol, bootstrap, args=(), env={},
|
||||
path=None, uid=None, gid=None, usePTY=0,
|
||||
packages=()):
|
||||
env = env.copy()
|
||||
|
||||
pythonpath = []
|
||||
for pkg in packages:
|
||||
pkg_path, name = os.path.split(pkg)
|
||||
p = os.path.split(imp.find_module(name, [pkg_path] if pkg_path else None)[1])[0]
|
||||
if p.startswith(os.path.join(sys.prefix, 'lib')):
|
||||
continue
|
||||
pythonpath.append(p)
|
||||
pythonpath = list(set(pythonpath))
|
||||
pythonpath.extend(env.get('PYTHONPATH', '').split(os.pathsep))
|
||||
env['PYTHONPATH'] = os.pathsep.join(pythonpath)
|
||||
args = (sys.executable, '-c', bootstrap) + args
|
||||
# childFDs variable is needed because sometimes child processes
|
||||
# misbehave and use stdout to output stuff that should really go
|
||||
# to stderr. Of course child process might even use the wrong FDs
|
||||
# that I'm using here, 3 and 4, so we are going to fix all these
|
||||
# issues when I add support for the configuration object that can
|
||||
# fix this stuff in a more configurable way.
|
||||
if IS_WINDOWS:
|
||||
return reactor.spawnProcess(processProtocol, sys.executable, args,
|
||||
env, path, uid, gid, usePTY)
|
||||
else:
|
||||
return reactor.spawnProcess(processProtocol, sys.executable, args,
|
||||
env, path, uid, gid, usePTY,
|
||||
childFDs={0:"w", 1:"r", 2:"r", 3:"w", 4:"r"})
|
||||
414
evennia/contrib/procpools/ampoule/pool.py
Normal file
414
evennia/contrib/procpools/ampoule/pool.py
Normal file
|
|
@ -0,0 +1,414 @@
|
|||
import time
|
||||
import random
|
||||
import heapq
|
||||
import itertools
|
||||
import signal
|
||||
choice = random.choice
|
||||
now = time.time
|
||||
count = itertools.count().next
|
||||
pop = heapq.heappop
|
||||
|
||||
from twisted.internet import defer, task, error
|
||||
from twisted.python import log, failure
|
||||
|
||||
from contrib.procpools.ampoule import commands, main
|
||||
|
||||
try:
|
||||
DIE = signal.SIGKILL
|
||||
except AttributeError:
|
||||
# Windows doesn't have SIGKILL, let's just use SIGTERM then
|
||||
DIE = signal.SIGTERM
|
||||
|
||||
class ProcessPool(object):
|
||||
"""
|
||||
This class generalizes the functionality of a pool of
|
||||
processes to which work can be dispatched.
|
||||
|
||||
@ivar finished: Boolean flag, L{True} when the pool is finished.
|
||||
|
||||
@ivar started: Boolean flag, L{True} when the pool is started.
|
||||
|
||||
@ivar name: Optional name for the process pool
|
||||
|
||||
@ivar min: Minimum number of subprocesses to set up
|
||||
|
||||
@ivar max: Maximum number of subprocesses to set up
|
||||
|
||||
@ivar maxIdle: Maximum number of seconds of indleness in a child
|
||||
|
||||
@ivar starter: A process starter instance that provides
|
||||
L{iampoule.IStarter}.
|
||||
|
||||
@ivar recycleAfter: Maximum number of calls before restarting a
|
||||
subprocess, 0 to not recycle.
|
||||
|
||||
@ivar ampChild: The child AMP protocol subclass with the commands
|
||||
that the child should implement.
|
||||
|
||||
@ivar ampParent: The parent AMP protocol subclass with the commands
|
||||
that the parent should implement.
|
||||
|
||||
@ivar timeout: The general timeout (in seconds) for every child
|
||||
process call.
|
||||
"""
|
||||
|
||||
finished = False
|
||||
started = False
|
||||
name = None
|
||||
|
||||
def __init__(self, ampChild=None, ampParent=None, min=5, max=20,
|
||||
name=None, maxIdle=20, recycleAfter=500, starter=None,
|
||||
timeout=None, timeout_signal=DIE, ampChildArgs=()):
|
||||
self.starter = starter
|
||||
self.ampChildArgs = tuple(ampChildArgs)
|
||||
if starter is None:
|
||||
self.starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
self.ampParent = ampParent
|
||||
self.ampChild = ampChild
|
||||
if ampChild is None:
|
||||
from contrib.procpools.ampoule.child import AMPChild
|
||||
self.ampChild = AMPChild
|
||||
self.min = min
|
||||
self.max = max
|
||||
self.name = name
|
||||
self.maxIdle = maxIdle
|
||||
self.recycleAfter = recycleAfter
|
||||
self.timeout = timeout
|
||||
self.timeout_signal = timeout_signal
|
||||
self._queue = []
|
||||
|
||||
self.processes = set()
|
||||
self.ready = set()
|
||||
self.busy = set()
|
||||
self._finishCallbacks = {}
|
||||
self._lastUsage = {}
|
||||
self._calls = {}
|
||||
self.looping = task.LoopingCall(self._pruneProcesses)
|
||||
self.looping.start(maxIdle, now=False)
|
||||
|
||||
def start(self, ampChild=None):
|
||||
"""
|
||||
Starts the ProcessPool with a given child protocol.
|
||||
|
||||
@param ampChild: a L{ampoule.child.AMPChild} subclass.
|
||||
@type ampChild: L{ampoule.child.AMPChild} subclass
|
||||
"""
|
||||
if ampChild is not None and not self.started:
|
||||
self.ampChild = ampChild
|
||||
self.finished = False
|
||||
self.started = True
|
||||
return self.adjustPoolSize()
|
||||
|
||||
def _pruneProcesses(self):
|
||||
"""
|
||||
Remove idle processes from the pool.
|
||||
"""
|
||||
n = now()
|
||||
d = []
|
||||
for child, lastUse in self._lastUsage.iteritems():
|
||||
if len(self.processes) > self.min and (n - lastUse) > self.maxIdle:
|
||||
# we are setting lastUse when processing finishes, it
|
||||
# might be processing right now
|
||||
if child not in self.busy:
|
||||
# we need to remove this child from the ready set
|
||||
# and the processes set because otherwise it might
|
||||
# get calls from doWork
|
||||
self.ready.discard(child)
|
||||
self.processes.discard(child)
|
||||
d.append(self.stopAWorker(child))
|
||||
return defer.DeferredList(d)
|
||||
|
||||
def _pruneProcess(self, child):
|
||||
"""
|
||||
Remove every trace of the process from this instance.
|
||||
"""
|
||||
self.processes.discard(child)
|
||||
self.ready.discard(child)
|
||||
self.busy.discard(child)
|
||||
self._lastUsage.pop(child, None)
|
||||
self._calls.pop(child, None)
|
||||
self._finishCallbacks.pop(child, None)
|
||||
|
||||
def _addProcess(self, child, finished):
|
||||
"""
|
||||
Adds the newly created child process to the pool.
|
||||
"""
|
||||
def restart(child, reason):
|
||||
#log.msg("FATAL: Restarting after %s" % (reason,))
|
||||
self._pruneProcess(child)
|
||||
return self.startAWorker()
|
||||
|
||||
def dieGently(data, child):
|
||||
#log.msg("STOPPING: '%s'" % (data,))
|
||||
self._pruneProcess(child)
|
||||
|
||||
self.processes.add(child)
|
||||
self.ready.add(child)
|
||||
finished.addCallback(dieGently, child
|
||||
).addErrback(lambda reason: restart(child, reason))
|
||||
self._finishCallbacks[child] = finished
|
||||
self._lastUsage[child] = now()
|
||||
self._calls[child] = 0
|
||||
self._catchUp()
|
||||
|
||||
def _catchUp(self):
|
||||
"""
|
||||
If there are queued items in the list then run them.
|
||||
"""
|
||||
if self._queue:
|
||||
_, (d, command, kwargs) = pop(self._queue)
|
||||
self._cb_doWork(command, **kwargs).chainDeferred(d)
|
||||
|
||||
def _handleTimeout(self, child):
|
||||
"""
|
||||
One of the children went timeout, we need to deal with it
|
||||
|
||||
@param child: The child process
|
||||
@type child: L{child.AMPChild}
|
||||
"""
|
||||
try:
|
||||
child.transport.signalProcess(self.timeout_signal)
|
||||
except error.ProcessExitedAlready:
|
||||
# don't do anything then... we are too late
|
||||
# or we were too early to call
|
||||
pass
|
||||
|
||||
def startAWorker(self):
|
||||
"""
|
||||
Start a worker and set it up in the system.
|
||||
"""
|
||||
if self.finished:
|
||||
# this is a race condition: basically if we call self.stop()
|
||||
# while a process is being recycled what happens is that the
|
||||
# process will be created anyway. By putting a check for
|
||||
# self.finished here we make sure that in no way we are creating
|
||||
# processes when the pool is stopped.
|
||||
# The race condition comes from the fact that:
|
||||
# stopAWorker() is asynchronous while stop() is synchronous.
|
||||
# so if you call:
|
||||
# pp.stopAWorker(child).addCallback(lambda _: pp.startAWorker())
|
||||
# pp.stop()
|
||||
# You might end up with a dirty reactor due to the stop()
|
||||
# returning before the new process is created.
|
||||
return
|
||||
startAMPProcess = self.starter.startAMPProcess
|
||||
child, finished = startAMPProcess(self.ampChild,
|
||||
ampParent=self.ampParent,
|
||||
ampChildArgs=self.ampChildArgs)
|
||||
return self._addProcess(child, finished)
|
||||
|
||||
def _cb_doWork(self, command, _timeout=None, _deadline=None,
|
||||
**kwargs):
|
||||
"""
|
||||
Go and call the command.
|
||||
|
||||
@param command: The L{amp.Command} to be executed in the child
|
||||
@type command: L{amp.Command}
|
||||
|
||||
@param _d: The deferred for the calling code.
|
||||
@type _d: L{defer.Deferred}
|
||||
|
||||
@param _timeout: The timeout for this call only
|
||||
@type _timeout: C{int}
|
||||
@param _deadline: The deadline for this call only
|
||||
@type _deadline: C{int}
|
||||
"""
|
||||
timeoutCall = None
|
||||
deadlineCall = None
|
||||
|
||||
def _returned(result, child, is_error=False):
|
||||
def cancelCall(call):
|
||||
if call is not None and call.active():
|
||||
call.cancel()
|
||||
cancelCall(timeoutCall)
|
||||
cancelCall(deadlineCall)
|
||||
self.busy.discard(child)
|
||||
if not die:
|
||||
# we are not marked to be removed, so add us back to
|
||||
# the ready set and let's see if there's some catching
|
||||
# up to do
|
||||
self.ready.add(child)
|
||||
self._catchUp()
|
||||
else:
|
||||
# We should die and we do, then we start a new worker
|
||||
# to pick up stuff from the queue otherwise we end up
|
||||
# without workers and the queue will remain there.
|
||||
self.stopAWorker(child).addCallback(lambda _: self.startAWorker())
|
||||
self._lastUsage[child] = now()
|
||||
# we can't do recycling here because it's too late and
|
||||
# the process might have received tons of calls already
|
||||
# which would make it run more calls than what is
|
||||
# configured to do.
|
||||
return result
|
||||
|
||||
die = False
|
||||
child = self.ready.pop()
|
||||
self.busy.add(child)
|
||||
self._calls[child] += 1
|
||||
|
||||
# Let's see if this call goes over the recycling barrier
|
||||
if self.recycleAfter and self._calls[child] >= self.recycleAfter:
|
||||
# it does so mark this child, using a closure, to be
|
||||
# removed at the end of the call.
|
||||
die = True
|
||||
|
||||
# If the command doesn't require a response then callRemote
|
||||
# returns nothing, so we prepare for that too.
|
||||
# We also need to guard against timeout errors for child
|
||||
# and local timeout parameter overrides the global one
|
||||
if _timeout == 0:
|
||||
timeout = _timeout
|
||||
else:
|
||||
timeout = _timeout or self.timeout
|
||||
|
||||
if timeout is not None:
|
||||
from twisted.internet import reactor
|
||||
timeoutCall = reactor.callLater(timeout, self._handleTimeout, child)
|
||||
|
||||
if _deadline is not None:
|
||||
from twisted.internet import reactor
|
||||
delay = max(0, _deadline - reactor.seconds())
|
||||
deadlineCall = reactor.callLater(delay, self._handleTimeout,
|
||||
child)
|
||||
|
||||
return defer.maybeDeferred(child.callRemote, command, **kwargs
|
||||
).addCallback(_returned, child
|
||||
).addErrback(_returned, child, is_error=True)
|
||||
|
||||
def callRemote(self, *args, **kwargs):
|
||||
"""
|
||||
Proxy call to keep the API homogeneous across twisted's RPCs
|
||||
"""
|
||||
return self.doWork(*args, **kwargs)
|
||||
|
||||
def doWork(self, command, **kwargs):
|
||||
"""
|
||||
Sends the command to one child.
|
||||
|
||||
@param command: an L{amp.Command} type object.
|
||||
@type command: L{amp.Command}
|
||||
|
||||
@param kwargs: dictionary containing the arguments for the command.
|
||||
"""
|
||||
if self.ready: # there are unused processes, let's use them
|
||||
return self._cb_doWork(command, **kwargs)
|
||||
else:
|
||||
if len(self.processes) < self.max:
|
||||
# no unused but we can start some new ones
|
||||
# since startAWorker is synchronous we won't have a
|
||||
# race condition here in case of multiple calls to
|
||||
# doWork, so we will end up in the else clause in case
|
||||
# of such calls:
|
||||
# Process pool with min=1, max=1, recycle_after=1
|
||||
# [call(Command) for x in xrange(BIG_NUMBER)]
|
||||
self.startAWorker()
|
||||
return self._cb_doWork(command, **kwargs)
|
||||
else:
|
||||
# No one is free... just queue up and wait for a process
|
||||
# to start and pick up the first item in the queue.
|
||||
d = defer.Deferred()
|
||||
self._queue.append((count(), (d, command, kwargs)))
|
||||
return d
|
||||
|
||||
def stopAWorker(self, child=None):
|
||||
"""
|
||||
Gently stop a child so that it's not restarted anymore
|
||||
|
||||
@param command: an L{ampoule.child.AmpChild} type object.
|
||||
@type command: L{ampoule.child.AmpChild} or None
|
||||
|
||||
"""
|
||||
if child is None:
|
||||
if self.ready:
|
||||
child = self.ready.pop()
|
||||
else:
|
||||
child = choice(list(self.processes))
|
||||
child.callRemote(commands.Shutdown
|
||||
# This is needed for timeout handling, the reason is pretty hard
|
||||
# to explain but I'll try to:
|
||||
# There's another small race condition in the system. If the
|
||||
# child process is shut down by a signal and you try to stop
|
||||
# the process pool immediately afterwards, like tests would do,
|
||||
# the child AMP object would still be in the system and trying
|
||||
# to call the command Shutdown on it would result in the same
|
||||
# errback that we got originally, for this reason we need to
|
||||
# trap it now so that it doesn't raise by not being handled.
|
||||
# Does this even make sense to you?
|
||||
).addErrback(lambda reason: reason.trap(error.ProcessTerminated))
|
||||
return self._finishCallbacks[child]
|
||||
|
||||
def _startSomeWorkers(self):
|
||||
"""
|
||||
Start a bunch of workers until we reach the max number of them.
|
||||
"""
|
||||
if len(self.processes) < self.max:
|
||||
self.startAWorker()
|
||||
|
||||
def adjustPoolSize(self, min=None, max=None):
|
||||
"""
|
||||
Change the pool size to be at least min and less than max,
|
||||
useful when you change the values of max and min in the instance
|
||||
and you want the pool to adapt to them.
|
||||
"""
|
||||
if min is None:
|
||||
min = self.min
|
||||
if max is None:
|
||||
max = self.max
|
||||
|
||||
assert min >= 0, 'minimum is negative'
|
||||
assert min <= max, 'minimum is greater than maximum'
|
||||
|
||||
self.min = min
|
||||
self.max = max
|
||||
|
||||
l = []
|
||||
if self.started:
|
||||
|
||||
for i in xrange(len(self.processes)-self.max):
|
||||
l.append(self.stopAWorker())
|
||||
while len(self.processes) < self.min:
|
||||
self.startAWorker()
|
||||
|
||||
return defer.DeferredList(l)#.addCallback(lambda _: self.dumpStats())
|
||||
|
||||
def stop(self):
|
||||
"""
|
||||
Stops the process protocol.
|
||||
"""
|
||||
self.finished = True
|
||||
l = [self.stopAWorker(process) for process in self.processes]
|
||||
def _cb(_):
|
||||
if self.looping.running:
|
||||
self.looping.stop()
|
||||
|
||||
return defer.DeferredList(l).addCallback(_cb)
|
||||
|
||||
def dumpStats(self):
|
||||
log.msg("ProcessPool stats:")
|
||||
log.msg('\tworkers: %s' % len(self.processes))
|
||||
log.msg('\ttimeout: %s' % (self.timeout))
|
||||
log.msg('\tparent: %r' % (self.ampParent,))
|
||||
log.msg('\tchild: %r' % (self.ampChild,))
|
||||
log.msg('\tmax idle: %r' % (self.maxIdle,))
|
||||
log.msg('\trecycle after: %r' % (self.recycleAfter,))
|
||||
log.msg('\tProcessStarter:')
|
||||
log.msg('\t\t%r' % (self.starter,))
|
||||
|
||||
pp = None
|
||||
|
||||
def deferToAMPProcess(command, **kwargs):
|
||||
"""
|
||||
Helper function that sends a command to the default process pool
|
||||
and returns a deferred that fires when the result of the
|
||||
subprocess computation is ready.
|
||||
|
||||
@param command: an L{amp.Command} subclass
|
||||
@param kwargs: dictionary containing the arguments for the command.
|
||||
|
||||
@return: a L{defer.Deferred} with the data from the subprocess.
|
||||
"""
|
||||
global pp
|
||||
if pp is None:
|
||||
pp = ProcessPool()
|
||||
return pp.start().addCallback(lambda _: pp.doWork(command, **kwargs))
|
||||
return pp.doWork(command, **kwargs)
|
||||
65
evennia/contrib/procpools/ampoule/rpool.py
Normal file
65
evennia/contrib/procpools/ampoule/rpool.py
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
"""
|
||||
This module implements a remote pool to use with AMP.
|
||||
"""
|
||||
|
||||
from twisted.protocols import amp
|
||||
|
||||
class AMPProxy(amp.AMP):
|
||||
"""
|
||||
A Proxy AMP protocol that forwards calls to a wrapped
|
||||
callRemote-like callable.
|
||||
"""
|
||||
def __init__(self, wrapped, child):
|
||||
"""
|
||||
@param wrapped: A callRemote-like callable that takes an
|
||||
L{amp.Command} as first argument and other
|
||||
optional keyword arguments afterwards.
|
||||
@type wrapped: L{callable}.
|
||||
|
||||
@param child: The protocol class of the process pool children.
|
||||
Used to forward only the methods that are actually
|
||||
understood correctly by them.
|
||||
@type child: L{amp.AMP}
|
||||
"""
|
||||
amp.AMP.__init__(self)
|
||||
self.wrapped = wrapped
|
||||
self.child = child
|
||||
|
||||
localCd = set(self._commandDispatch.keys())
|
||||
childCd = set(self.child._commandDispatch.keys())
|
||||
assert localCd.intersection(childCd) == set(["StartTLS"]), \
|
||||
"Illegal method overriding in Proxy"
|
||||
|
||||
def locateResponder(self, name):
|
||||
"""
|
||||
This is a custom locator to forward calls to the children
|
||||
processes while keeping the ProcessPool a transparent MITM.
|
||||
|
||||
This way of working has a few limitations, the first of which
|
||||
is the fact that children won't be able to take advantage of
|
||||
any dynamic locator except for the default L{CommandLocator}
|
||||
that is based on the _commandDispatch attribute added by the
|
||||
metaclass. This limitation might be lifted in the future.
|
||||
"""
|
||||
if name == "StartTLS":
|
||||
# This is a special case where the proxy takes precedence
|
||||
return amp.AMP.locateResponder(self, "StartTLS")
|
||||
|
||||
# Get the dict of commands from the child AMP implementation.
|
||||
cd = self.child._commandDispatch
|
||||
if name in cd:
|
||||
# If the command is there, then we forward stuff to it.
|
||||
commandClass, _responderFunc = cd[name]
|
||||
# We need to wrap the doWork function because the wrapping
|
||||
# call doesn't pass the command as first argument since it
|
||||
# thinks that we are the actual receivers and callable is
|
||||
# already the responder while it isn't.
|
||||
doWork = lambda **kw: self.wrapped(commandClass, **kw)
|
||||
# Now let's call the right function and wrap the result
|
||||
# dictionary.
|
||||
return self._wrapWithSerialization(doWork, commandClass)
|
||||
# of course if the name of the command is not in the child it
|
||||
# means that it might be in this class, so fallback to the
|
||||
# default behavior of this module.
|
||||
return amp.AMP.locateResponder(self, name)
|
||||
|
||||
69
evennia/contrib/procpools/ampoule/service.py
Normal file
69
evennia/contrib/procpools/ampoule/service.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import os
|
||||
|
||||
from twisted.application import service
|
||||
from twisted.internet.protocol import ServerFactory
|
||||
|
||||
def makeService(options):
|
||||
"""
|
||||
Create the service for the application
|
||||
"""
|
||||
ms = service.MultiService()
|
||||
|
||||
from contrib.procpools.ampoule.pool import ProcessPool
|
||||
from contrib.procpools.ampoule.main import ProcessStarter
|
||||
name = options['name']
|
||||
ampport = options['ampport']
|
||||
ampinterface = options['ampinterface']
|
||||
child = options['child']
|
||||
parent = options['parent']
|
||||
min = options['min']
|
||||
max = options['max']
|
||||
maxIdle = options['max_idle']
|
||||
recycle = options['recycle']
|
||||
childReactor = options['reactor']
|
||||
timeout = options['timeout']
|
||||
|
||||
starter = ProcessStarter(packages=("twisted", "ampoule"), childReactor=childReactor)
|
||||
pp = ProcessPool(child, parent, min, max, name, maxIdle, recycle, starter, timeout)
|
||||
svc = AMPouleService(pp, child, ampport, ampinterface)
|
||||
svc.setServiceParent(ms)
|
||||
|
||||
return ms
|
||||
|
||||
class AMPouleService(service.Service):
|
||||
def __init__(self, pool, child, port, interface):
|
||||
self.pool = pool
|
||||
self.port = port
|
||||
self.child = child
|
||||
self.interface = interface
|
||||
self.server = None
|
||||
|
||||
def startService(self):
|
||||
"""
|
||||
Before reactor.run() is called we setup the system.
|
||||
"""
|
||||
service.Service.startService(self)
|
||||
from contrib.procpools.ampoule import rpool
|
||||
from twisted.internet import reactor
|
||||
|
||||
try:
|
||||
factory = ServerFactory()
|
||||
factory.protocol = lambda: rpool.AMPProxy(wrapped=self.pool.doWork,
|
||||
child=self.child)
|
||||
self.server = reactor.listenTCP(self.port,
|
||||
factory,
|
||||
interface=self.interface)
|
||||
# this is synchronous when it's the startup, even though
|
||||
# it returns a deferred. But we need to run it after the
|
||||
# first cycle in order to wait for signal handlers to be
|
||||
# installed.
|
||||
reactor.callLater(0, self.pool.start)
|
||||
except:
|
||||
import traceback
|
||||
print traceback.format_exc()
|
||||
|
||||
def stopService(self):
|
||||
service.Service.stopService(self)
|
||||
if self.server is not None:
|
||||
self.server.stopListening()
|
||||
return self.pool.stop()
|
||||
0
evennia/contrib/procpools/ampoule/test/__init__.py
Normal file
0
evennia/contrib/procpools/ampoule/test/__init__.py
Normal file
867
evennia/contrib/procpools/ampoule/test/test_process.py
Normal file
867
evennia/contrib/procpools/ampoule/test/test_process.py
Normal file
|
|
@ -0,0 +1,867 @@
|
|||
|
||||
from signal import SIGHUP
|
||||
import math
|
||||
import os
|
||||
import os.path
|
||||
from cStringIO import StringIO as sio
|
||||
import tempfile
|
||||
|
||||
from twisted.internet import error, defer, reactor
|
||||
from twisted.python import failure, reflect
|
||||
from twisted.trial import unittest
|
||||
from twisted.protocols import amp
|
||||
from contrib.procpools.ampoule import main, child, commands, pool
|
||||
|
||||
class ShouldntHaveBeenCalled(Exception):
|
||||
pass
|
||||
|
||||
def _raise(_):
|
||||
raise ShouldntHaveBeenCalled(_)
|
||||
|
||||
class _FakeT(object):
|
||||
closeStdinCalled = False
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
|
||||
def closeStdin(self):
|
||||
self.closeStdinCalled = True
|
||||
|
||||
def write(self, data):
|
||||
self.s.write(data)
|
||||
|
||||
class FakeAMP(object):
|
||||
connector = None
|
||||
reason = None
|
||||
def __init__(self, s):
|
||||
self.s = s
|
||||
|
||||
def makeConnection(self, connector):
|
||||
if self.connector is not None:
|
||||
raise Exception("makeConnection called twice")
|
||||
self.connector = connector
|
||||
|
||||
def connectionLost(self, reason):
|
||||
if self.reason is not None:
|
||||
raise Exception("connectionLost called twice")
|
||||
self.reason = reason
|
||||
|
||||
def dataReceived(self, data):
|
||||
self.s.write(data)
|
||||
|
||||
class Ping(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Pong(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Pid(amp.Command):
|
||||
response = [('pid', amp.Integer())]
|
||||
|
||||
class Reactor(amp.Command):
|
||||
response = [('classname', amp.String())]
|
||||
|
||||
class NoResponse(amp.Command):
|
||||
arguments = [('arg', amp.String())]
|
||||
requiresAnswer = False
|
||||
|
||||
class GetResponse(amp.Command):
|
||||
response = [("response", amp.String())]
|
||||
|
||||
class Child(child.AMPChild):
|
||||
def ping(self, data):
|
||||
return self.callRemote(Pong, data=data)
|
||||
Ping.responder(ping)
|
||||
|
||||
class PidChild(child.AMPChild):
|
||||
def pid(self):
|
||||
import os
|
||||
return {'pid': os.getpid()}
|
||||
Pid.responder(pid)
|
||||
|
||||
class NoResponseChild(child.AMPChild):
|
||||
_set = False
|
||||
def noresponse(self, arg):
|
||||
self._set = arg
|
||||
return {}
|
||||
NoResponse.responder(noresponse)
|
||||
|
||||
def getresponse(self):
|
||||
return {"response": self._set}
|
||||
GetResponse.responder(getresponse)
|
||||
|
||||
class ReactorChild(child.AMPChild):
|
||||
def reactor(self):
|
||||
from twisted.internet import reactor
|
||||
return {'classname': reactor.__class__.__name__}
|
||||
Reactor.responder(reactor)
|
||||
|
||||
class First(amp.Command):
|
||||
arguments = [('data', amp.String())]
|
||||
response = [('response', amp.String())]
|
||||
|
||||
class Second(amp.Command):
|
||||
pass
|
||||
|
||||
class WaitingChild(child.AMPChild):
|
||||
deferred = None
|
||||
def first(self, data):
|
||||
self.deferred = defer.Deferred()
|
||||
return self.deferred.addCallback(lambda _: {'response': data})
|
||||
First.responder(first)
|
||||
def second(self):
|
||||
self.deferred.callback('')
|
||||
return {}
|
||||
Second.responder(second)
|
||||
|
||||
class Die(amp.Command):
|
||||
pass
|
||||
|
||||
class BadChild(child.AMPChild):
|
||||
def die(self):
|
||||
self.shutdown = False
|
||||
self.transport.loseConnection()
|
||||
return {}
|
||||
Die.responder(die)
|
||||
|
||||
|
||||
class Write(amp.Command):
|
||||
response = [("response", amp.String())]
|
||||
pass
|
||||
|
||||
|
||||
class Writer(child.AMPChild):
|
||||
|
||||
def __init__(self, data='hello'):
|
||||
child.AMPChild.__init__(self)
|
||||
self.data = data
|
||||
|
||||
def write(self):
|
||||
return {'response': self.data}
|
||||
Write.responder(write)
|
||||
|
||||
|
||||
class GetCWD(amp.Command):
|
||||
|
||||
response = [("cwd", amp.String())]
|
||||
|
||||
|
||||
class TempDirChild(child.AMPChild):
|
||||
|
||||
def __init__(self, directory=None):
|
||||
child.AMPChild.__init__(self)
|
||||
self.directory = directory
|
||||
|
||||
def __enter__(self):
|
||||
directory = tempfile.mkdtemp()
|
||||
os.chdir(directory)
|
||||
if self.directory is not None:
|
||||
os.mkdir(self.directory)
|
||||
os.chdir(self.directory)
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
cwd = os.getcwd()
|
||||
os.chdir('..')
|
||||
os.rmdir(cwd)
|
||||
|
||||
def getcwd(self):
|
||||
return {'cwd': os.getcwd()}
|
||||
GetCWD.responder(getcwd)
|
||||
|
||||
|
||||
class TestAMPConnector(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
The only reason why this method exists is to let 'trial ampoule'
|
||||
to install the signal handlers (#3178 for reference).
|
||||
"""
|
||||
super(TestAMPConnector, self).setUp()
|
||||
d = defer.Deferred()
|
||||
reactor.callLater(0, d.callback, None)
|
||||
return d
|
||||
|
||||
def _makeConnector(self, s, sa):
|
||||
a = FakeAMP(sa)
|
||||
ac = main.AMPConnector(a)
|
||||
assert ac.name is not None
|
||||
ac.transport = _FakeT(s)
|
||||
return ac
|
||||
|
||||
def test_protocol(self):
|
||||
"""
|
||||
Test that outReceived writes to AMP and that it triggers the
|
||||
finished deferred once the process ended.
|
||||
"""
|
||||
s = sio()
|
||||
sa = sio()
|
||||
ac = self._makeConnector(s, sa)
|
||||
|
||||
for x in xrange(99):
|
||||
ac.childDataReceived(4, str(x))
|
||||
|
||||
ac.processEnded(failure.Failure(error.ProcessDone(0)))
|
||||
return ac.finished.addCallback(
|
||||
lambda _: self.assertEqual(sa.getvalue(), ''.join(str(x) for x in xrange(99)))
|
||||
)
|
||||
|
||||
def test_protocol_failing(self):
|
||||
"""
|
||||
Test that a failure in the process termination is correctly
|
||||
propagated to the finished deferred.
|
||||
"""
|
||||
s = sio()
|
||||
sa = sio()
|
||||
ac = self._makeConnector(s, sa)
|
||||
|
||||
ac.finished.addCallback(_raise)
|
||||
fail = failure.Failure(error.ProcessTerminated())
|
||||
self.assertFailure(ac.finished, error.ProcessTerminated)
|
||||
ac.processEnded(fail)
|
||||
|
||||
def test_startProcess(self):
|
||||
"""
|
||||
Test that startProcess actually starts a subprocess and that
|
||||
it receives data back from the process through AMP.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys, os
|
||||
def main(arg):
|
||||
os.write(4, arg)
|
||||
main(sys.argv[1])
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT,
|
||||
args=(STRING,),
|
||||
packages=("twisted", "ampoule"))
|
||||
|
||||
amp, finished = starter.startPythonProcess(main.AMPConnector(a))
|
||||
def _eb(reason):
|
||||
print reason
|
||||
finished.addErrback(_eb)
|
||||
return finished.addCallback(lambda _: self.assertEquals(s.getvalue(), STRING))
|
||||
|
||||
def test_failing_deferToProcess(self):
|
||||
"""
|
||||
Test failing subprocesses and the way they terminate and preserve
|
||||
failing information.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys
|
||||
def main(arg):
|
||||
raise Exception(arg)
|
||||
main(sys.argv[1])
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT, args=(STRING,), packages=("twisted", "ampoule"))
|
||||
ready, finished = starter.startPythonProcess(main.AMPConnector(a), "I'll be ignored")
|
||||
|
||||
self.assertFailure(finished, error.ProcessTerminated)
|
||||
finished.addErrback(lambda reason: self.assertEquals(reason.getMessage(), STRING))
|
||||
return finished
|
||||
|
||||
def test_env_setting(self):
|
||||
"""
|
||||
Test that and environment variable passed to the process starter
|
||||
is correctly passed to the child process.
|
||||
"""
|
||||
s = sio()
|
||||
a = FakeAMP(s)
|
||||
STRING = "ciao"
|
||||
BOOT = """\
|
||||
import sys, os
|
||||
def main():
|
||||
os.write(4, os.getenv("FOOBAR"))
|
||||
main()
|
||||
"""
|
||||
starter = main.ProcessStarter(bootstrap=BOOT,
|
||||
packages=("twisted", "ampoule"),
|
||||
env={"FOOBAR": STRING})
|
||||
amp, finished = starter.startPythonProcess(main.AMPConnector(a), "I'll be ignored")
|
||||
def _eb(reason):
|
||||
print reason
|
||||
finished.addErrback(_eb)
|
||||
return finished.addCallback(lambda _: self.assertEquals(s.getvalue(), STRING))
|
||||
|
||||
def test_startAMPProcess(self):
|
||||
"""
|
||||
Test that you can start an AMP subprocess and that it correctly
|
||||
accepts commands and correctly answers them.
|
||||
"""
|
||||
STRING = "ciao"
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
c, finished = starter.startAMPProcess(child.AMPChild)
|
||||
c.callRemote(commands.Echo, data=STRING
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], STRING)
|
||||
).addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
return finished
|
||||
|
||||
def test_BootstrapContext(self):
|
||||
starter = main.ProcessStarter(packages=('twisted', 'ampoule'))
|
||||
c, finished = starter.startAMPProcess(TempDirChild)
|
||||
cwd = []
|
||||
def checkBootstrap(response):
|
||||
cwd.append(response['cwd'])
|
||||
self.assertNotEquals(cwd, os.getcwd())
|
||||
d = c.callRemote(GetCWD)
|
||||
d.addCallback(checkBootstrap)
|
||||
d.addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
finished.addCallback(lambda _: self.assertFalse(os.path.exists(cwd[0])))
|
||||
return finished
|
||||
|
||||
def test_BootstrapContextInstance(self):
|
||||
starter = main.ProcessStarter(packages=('twisted', 'ampoule'))
|
||||
c, finished = starter.startAMPProcess(TempDirChild,
|
||||
ampChildArgs=('foo',))
|
||||
cwd = []
|
||||
def checkBootstrap(response):
|
||||
cwd.append(response['cwd'])
|
||||
self.assertTrue(cwd[0].endswith('/foo'))
|
||||
d = c.callRemote(GetCWD)
|
||||
d.addCallback(checkBootstrap)
|
||||
d.addCallback(lambda _: c.callRemote(commands.Shutdown))
|
||||
finished.addCallback(lambda _: self.assertFalse(os.path.exists(cwd[0])))
|
||||
return finished
|
||||
|
||||
def test_startAMPAndParentProtocol(self):
|
||||
"""
|
||||
Test that you can start an AMP subprocess and the children can
|
||||
call methods on their parent.
|
||||
"""
|
||||
DATA = "CIAO"
|
||||
APPEND = "123"
|
||||
|
||||
class Parent(amp.AMP):
|
||||
def pong(self, data):
|
||||
return {'response': DATA+APPEND}
|
||||
Pong.responder(pong)
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
|
||||
subp, finished = starter.startAMPProcess(ampChild=Child, ampParent=Parent)
|
||||
subp.callRemote(Ping, data=DATA
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], DATA+APPEND)
|
||||
).addCallback(lambda _: subp.callRemote(commands.Shutdown))
|
||||
return finished
|
||||
|
||||
def test_roundtripError(self):
|
||||
"""
|
||||
Test that invoking a child using an unreachable class raises
|
||||
a L{RunTimeError} .
|
||||
"""
|
||||
class Child(child.AMPChild):
|
||||
pass
|
||||
|
||||
starter = main.ProcessStarter(packages=("twisted", "ampoule"))
|
||||
|
||||
self.assertRaises(RuntimeError, starter.startAMPProcess, ampChild=Child)
|
||||
|
||||
class TestProcessPool(unittest.TestCase):
|
||||
|
||||
def test_startStopWorker(self):
|
||||
"""
|
||||
Test that starting and stopping a worker keeps the state of
|
||||
the process pool consistent.
|
||||
"""
|
||||
pp = pool.ProcessPool()
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _checks():
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), 1)
|
||||
self.assertEquals(len(pp._finishCallbacks), 1)
|
||||
return pp.stopAWorker()
|
||||
|
||||
def _closingUp(_):
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), 0)
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
pp.startAWorker()
|
||||
return _checks().addCallback(_closingUp).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_startAndStop(self):
|
||||
"""
|
||||
Test that a process pool's start and stop method create the
|
||||
expected number of workers and keep state consistent in the
|
||||
process pool.
|
||||
"""
|
||||
pp = pool.ProcessPool()
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.stop()
|
||||
|
||||
def _closingUp(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, True)
|
||||
self.assertEquals(len(pp.processes), 0)
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
return pp.start().addCallback(_checks).addCallback(_closingUp)
|
||||
|
||||
def test_adjustPoolSize(self):
|
||||
"""
|
||||
Test that calls to pool.adjustPoolSize are correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(min=10)
|
||||
self.assertEquals(pp.started, False)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.processes, set())
|
||||
self.assertEquals(pp._finishCallbacks, {})
|
||||
|
||||
def _resize1(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.adjustPoolSize(min=2, max=3)
|
||||
|
||||
def _resize2(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(pp.max, 3)
|
||||
self.assertEquals(pp.min, 2)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
def _resize3(_):
|
||||
self.assertRaises(AssertionError, pp.adjustPoolSize, min=-1, max=5)
|
||||
self.assertRaises(AssertionError, pp.adjustPoolSize, min=5, max=1)
|
||||
return pp.stop()
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_resize1
|
||||
).addCallback(_resize2
|
||||
).addCallback(_resize3)
|
||||
|
||||
def test_childRestart(self):
|
||||
"""
|
||||
Test that a failing child process is immediately restarted.
|
||||
"""
|
||||
pp = pool.ProcessPool(ampChild=BadChild, min=1)
|
||||
STRING = "DATA"
|
||||
|
||||
def _checks(_):
|
||||
d = pp._finishCallbacks.values()[0]
|
||||
pp.doWork(Die).addErrback(lambda _: None)
|
||||
return d.addBoth(_checksAgain)
|
||||
|
||||
def _checksAgain(_):
|
||||
return pp.doWork(commands.Echo, data=STRING
|
||||
).addCallback(lambda result: self.assertEquals(result['response'], STRING))
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_parentProtocolChange(self):
|
||||
"""
|
||||
Test that the father can use an AMP protocol too.
|
||||
"""
|
||||
DATA = "CIAO"
|
||||
APPEND = "123"
|
||||
|
||||
class Parent(amp.AMP):
|
||||
def pong(self, data):
|
||||
return {'response': DATA+APPEND}
|
||||
Pong.responder(pong)
|
||||
|
||||
pp = pool.ProcessPool(ampChild=Child, ampParent=Parent)
|
||||
def _checks(_):
|
||||
return pp.doWork(Ping, data=DATA
|
||||
).addCallback(lambda response:
|
||||
self.assertEquals(response['response'], DATA+APPEND)
|
||||
)
|
||||
|
||||
return pp.start().addCallback(_checks).addCallback(lambda _: pp.stop())
|
||||
|
||||
|
||||
def test_deferToAMPProcess(self):
|
||||
"""
|
||||
Test that deferToAMPProcess works as expected.
|
||||
"""
|
||||
def cleanupGlobalPool():
|
||||
d = pool.pp.stop()
|
||||
pool.pp = None
|
||||
return d
|
||||
self.addCleanup(cleanupGlobalPool)
|
||||
|
||||
STRING = "CIAOOOO"
|
||||
d = pool.deferToAMPProcess(commands.Echo, data=STRING)
|
||||
d.addCallback(self.assertEquals, {"response": STRING})
|
||||
return d
|
||||
|
||||
def test_checkStateInPool(self):
|
||||
"""
|
||||
Test that busy and ready lists are correctly maintained.
|
||||
"""
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild)
|
||||
|
||||
DATA = "foobar"
|
||||
|
||||
def _checks(_):
|
||||
d = pp.callRemote(First, data=DATA)
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
self.assertEquals(len(pp.ready), pp.min-1)
|
||||
self.assertEquals(len(pp.busy), 1)
|
||||
child = pp.busy.pop()
|
||||
pp.busy.add(child)
|
||||
child.callRemote(Second)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_growingToMax(self):
|
||||
"""
|
||||
Test that the pool grows over time until it reaches max processes.
|
||||
"""
|
||||
MAX = 5
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild, min=1, max=MAX)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
|
||||
D = "DATA"
|
||||
d = [pp.doWork(First, data=D) for x in xrange(MAX)]
|
||||
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
[child.callRemote(Second) for child in pp.processes]
|
||||
return defer.DeferredList(d)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_growingToMaxAndShrinking(self):
|
||||
"""
|
||||
Test that the pool grows but after 'idle' time the number of
|
||||
processes goes back to the minimum.
|
||||
"""
|
||||
|
||||
MAX = 5
|
||||
MIN = 1
|
||||
IDLE = 1
|
||||
pp = pool.ProcessPool(ampChild=WaitingChild, min=MIN, max=MAX, maxIdle=IDLE)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
|
||||
D = "DATA"
|
||||
d = [pp.doWork(First, data=D) for x in xrange(MAX)]
|
||||
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.max)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.max)
|
||||
|
||||
[child.callRemote(Second) for child in pp.processes]
|
||||
return defer.DeferredList(d).addCallback(_realChecks)
|
||||
|
||||
def _realChecks(_):
|
||||
from twisted.internet import reactor
|
||||
d = defer.Deferred()
|
||||
def _cb():
|
||||
def __(_):
|
||||
try:
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
d.callback(None)
|
||||
except Exception, e:
|
||||
d.errback(e)
|
||||
return pp._pruneProcesses().addCallback(__)
|
||||
# just to be shure we are called after the pruner
|
||||
pp.looping.stop() # stop the looping, we don't want it to
|
||||
# this right here
|
||||
reactor.callLater(IDLE, _cb)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_recycling(self):
|
||||
"""
|
||||
Test that after a given number of calls subprocesses are
|
||||
recycled.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 1
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
self.addCleanup(pp.stop)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid'])
|
||||
|
||||
def _checks2(pid):
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid']
|
||||
).addCallback(self.assertNotEquals, pid)
|
||||
|
||||
|
||||
d = pp.start()
|
||||
d.addCallback(_checks)
|
||||
d.addCallback(_checks2)
|
||||
return d
|
||||
|
||||
def test_recyclingWithQueueOverload(self):
|
||||
"""
|
||||
Test that we get the correct number of different results when
|
||||
we overload the pool of calls.
|
||||
"""
|
||||
MAX = 5
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 10
|
||||
CALLS = 60
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
self.addCleanup(pp.stop)
|
||||
|
||||
def _check(results):
|
||||
s = set()
|
||||
for succeed, response in results:
|
||||
s.add(response['pid'])
|
||||
|
||||
# For the first C{MAX} calls, each is basically guaranteed to go to
|
||||
# a different child. After that, though, there are no guarantees.
|
||||
# All the rest might go to a single child, since the child to
|
||||
# perform a job is selected arbitrarily from the "ready" set. Fair
|
||||
# distribution of jobs needs to be implemented; right now it's "set
|
||||
# ordering" distribution of jobs.
|
||||
self.assertTrue(len(s) > MAX)
|
||||
|
||||
def _work(_):
|
||||
l = [pp.doWork(Pid) for x in xrange(CALLS)]
|
||||
d = defer.DeferredList(l)
|
||||
return d.addCallback(_check)
|
||||
d = pp.start()
|
||||
d.addCallback(_work)
|
||||
return d
|
||||
|
||||
|
||||
def test_disableProcessRecycling(self):
|
||||
"""
|
||||
Test that by setting 0 to recycleAfter we actually disable process recycling.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
RECYCLE_AFTER = 0
|
||||
pp = pool.ProcessPool(ampChild=PidChild, min=MIN, max=MAX, recycleAfter=RECYCLE_AFTER)
|
||||
|
||||
def _checks(_):
|
||||
self.assertEquals(pp.started, True)
|
||||
self.assertEquals(pp.finished, False)
|
||||
self.assertEquals(len(pp.processes), pp.min)
|
||||
self.assertEquals(len(pp._finishCallbacks), pp.min)
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid'])
|
||||
|
||||
def _checks2(pid):
|
||||
return pp.doWork(Pid
|
||||
).addCallback(lambda response: response['pid']
|
||||
).addCallback(self.assertEquals, pid
|
||||
).addCallback(lambda _: pid)
|
||||
|
||||
def finish(reason):
|
||||
return pp.stop().addCallback(lambda _: reason)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_checks
|
||||
).addCallback(_checks2
|
||||
).addCallback(_checks2
|
||||
).addCallback(finish)
|
||||
|
||||
def test_changeChildrenReactor(self):
|
||||
"""
|
||||
Test that by passing the correct argument children change their
|
||||
reactor type.
|
||||
"""
|
||||
MAX = 1
|
||||
MIN = 1
|
||||
FIRST = "select"
|
||||
SECOND = "poll"
|
||||
|
||||
def checkDefault():
|
||||
pp = pool.ProcessPool(
|
||||
starter=main.ProcessStarter(
|
||||
childReactor=FIRST,
|
||||
packages=("twisted", "ampoule")),
|
||||
ampChild=ReactorChild, min=MIN, max=MAX)
|
||||
pp.start()
|
||||
return pp.doWork(Reactor
|
||||
).addCallback(self.assertEquals, {'classname': "SelectReactor"}
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def checkPool(_):
|
||||
pp = pool.ProcessPool(
|
||||
starter=main.ProcessStarter(
|
||||
childReactor=SECOND,
|
||||
packages=("twisted", "ampoule")),
|
||||
ampChild=ReactorChild, min=MIN, max=MAX)
|
||||
pp.start()
|
||||
return pp.doWork(Reactor
|
||||
).addCallback(self.assertEquals, {'classname': "PollReactor"}
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
return checkDefault(
|
||||
).addCallback(checkPool)
|
||||
try:
|
||||
from select import poll
|
||||
except ImportError:
|
||||
test_changeChildrenReactor.skip = "This architecture doesn't support select.poll, I can't run this test"
|
||||
|
||||
def test_commandsWithoutResponse(self):
|
||||
"""
|
||||
Test that if we send a command without a required answer we
|
||||
actually don't have any problems.
|
||||
"""
|
||||
DATA = "hello"
|
||||
pp = pool.ProcessPool(ampChild=NoResponseChild, min=1, max=1)
|
||||
|
||||
def _check(_):
|
||||
return pp.doWork(GetResponse
|
||||
).addCallback(self.assertEquals, {"response": DATA})
|
||||
|
||||
def _work(_):
|
||||
return pp.doWork(NoResponse, arg=DATA)
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(_check
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_SupplyChildArgs(self):
|
||||
"""Ensure that arguments for the child constructor are passed in."""
|
||||
pp = pool.ProcessPool(Writer, ampChildArgs=['body'], min=0)
|
||||
def _check(result):
|
||||
return pp.doWork(Write).addCallback(
|
||||
self.assertEquals, {'response': 'body'})
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_check
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def processTimeoutTest(self, timeout):
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _timeout=timeout)
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processTimeout(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given timeout
|
||||
time is correctly handled.
|
||||
"""
|
||||
return self.processTimeoutTest(1)
|
||||
|
||||
def test_processTimeoutZero(self):
|
||||
"""
|
||||
Test that the process is correctly handled when the timeout is zero.
|
||||
"""
|
||||
return self.processTimeoutTest(0)
|
||||
|
||||
def test_processDeadline(self):
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _deadline=reactor.seconds())
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processBeforeDeadline(self):
|
||||
pp = pool.ProcessPool(PidChild, min=1, max=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(Pid, _deadline=reactor.seconds() + 10)
|
||||
d.addCallback(lambda result: self.assertNotEqual(result['pid'], 0))
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processTimeoutSignal(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given timeout
|
||||
time is correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1,
|
||||
timeout_signal=SIGHUP)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao", _timeout=1)
|
||||
d.addCallback(lambda d: self.fail())
|
||||
text = 'signal %d' % SIGHUP
|
||||
d.addErrback(
|
||||
lambda f: self.assertTrue(text in f.value[0],
|
||||
'"%s" not in "%s"' % (text, f.value[0])))
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
|
||||
def test_processGlobalTimeout(self):
|
||||
"""
|
||||
Test that a call that doesn't finish within the given global
|
||||
timeout time is correctly handled.
|
||||
"""
|
||||
pp = pool.ProcessPool(WaitingChild, min=1, max=1, timeout=1)
|
||||
|
||||
def _work(_):
|
||||
d = pp.callRemote(First, data="ciao")
|
||||
self.assertFailure(d, error.ProcessTerminated)
|
||||
return d
|
||||
|
||||
return pp.start(
|
||||
).addCallback(_work
|
||||
).addCallback(lambda _: pp.stop())
|
||||
49
evennia/contrib/procpools/ampoule/test/test_proxy.py
Normal file
49
evennia/contrib/procpools/ampoule/test/test_proxy.py
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
from twisted.internet import defer, reactor
|
||||
from twisted.internet.protocol import ClientFactory
|
||||
from twisted.trial import unittest
|
||||
from twisted.protocols import amp
|
||||
|
||||
from contrib.procpools.ampoule import service, child, pool, main
|
||||
from contrib.procpools.ampoule.commands import Echo
|
||||
|
||||
class ClientAMP(amp.AMP):
|
||||
factory = None
|
||||
def connectionMade(self):
|
||||
if self.factory is not None:
|
||||
self.factory.theProto = self
|
||||
if hasattr(self.factory, 'onMade'):
|
||||
self.factory.onMade.callback(None)
|
||||
|
||||
class TestAMPProxy(unittest.TestCase):
|
||||
def setUp(self):
|
||||
"""
|
||||
Setup the proxy service and the client connection to the proxy
|
||||
service in order to run call through them.
|
||||
|
||||
Inspiration comes from twisted.test.test_amp
|
||||
"""
|
||||
self.pp = pool.ProcessPool()
|
||||
self.svc = service.AMPouleService(self.pp, child.AMPChild, 0, "")
|
||||
self.svc.startService()
|
||||
self.proxy_port = self.svc.server.getHost().port
|
||||
self.clientFactory = ClientFactory()
|
||||
self.clientFactory.protocol = ClientAMP
|
||||
d = self.clientFactory.onMade = defer.Deferred()
|
||||
self.clientConn = reactor.connectTCP("127.0.0.1",
|
||||
self.proxy_port,
|
||||
self.clientFactory)
|
||||
self.addCleanup(self.clientConn.disconnect)
|
||||
self.addCleanup(self.svc.stopService)
|
||||
def setClient(_):
|
||||
self.client = self.clientFactory.theProto
|
||||
return d.addCallback(setClient)
|
||||
|
||||
def test_forwardCall(self):
|
||||
"""
|
||||
Test that a call made from a client is correctly forwarded to
|
||||
the process pool and the result is correctly reported.
|
||||
"""
|
||||
DATA = "hello"
|
||||
return self.client.callRemote(Echo, data=DATA).addCallback(
|
||||
self.assertEquals, {'response': DATA}
|
||||
)
|
||||
46
evennia/contrib/procpools/ampoule/util.py
Normal file
46
evennia/contrib/procpools/ampoule/util.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
"""
|
||||
some utilities
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
import __main__
|
||||
|
||||
from twisted.python.filepath import FilePath
|
||||
from twisted.python.reflect import namedAny
|
||||
# from twisted.python.modules import theSystemPath
|
||||
|
||||
def findPackagePath(modulePath):
|
||||
"""
|
||||
Try to find the sys.path entry from a modulePath object, simultaneously
|
||||
computing the module name of the targetted file.
|
||||
"""
|
||||
p = modulePath
|
||||
l = [p.basename().split(".")[0]]
|
||||
while p.parent() != p:
|
||||
for extension in ['py', 'pyc', 'pyo', 'pyd', 'dll']:
|
||||
sib = p.sibling("__init__."+extension)
|
||||
if sib.exists():
|
||||
p = p.parent()
|
||||
l.insert(0, p.basename())
|
||||
break
|
||||
else:
|
||||
return p.parent(), '.'.join(l)
|
||||
|
||||
|
||||
def mainpoint(function):
|
||||
"""
|
||||
Decorator which declares a function to be an object's mainpoint.
|
||||
"""
|
||||
if function.__module__ == '__main__':
|
||||
# OK time to run a function
|
||||
p = FilePath(__main__.__file__)
|
||||
p, mn = findPackagePath(p)
|
||||
pname = p.path
|
||||
if pname not in map(os.path.abspath, sys.path):
|
||||
sys.path.insert(0, pname)
|
||||
# Maybe remove the module's path?
|
||||
exitcode = namedAny(mn+'.'+function.__name__)(sys.argv)
|
||||
if exitcode is None:
|
||||
exitcode = 0
|
||||
sys.exit(exitcode)
|
||||
return function
|
||||
326
evennia/contrib/procpools/python_procpool.py
Normal file
326
evennia/contrib/procpools/python_procpool.py
Normal file
|
|
@ -0,0 +1,326 @@
|
|||
"""
|
||||
Python ProcPool
|
||||
|
||||
Evennia Contribution - Griatch 2012
|
||||
|
||||
The ProcPool is used to execute code on a separate process. This allows for
|
||||
true asynchronous operation. Process communication happens over AMP and is
|
||||
thus fully asynchronous as far as Evennia is concerned.
|
||||
|
||||
The process pool is implemented using a slightly modified version of
|
||||
the Ampoule package (included).
|
||||
|
||||
The python_process pool is a service activated with the instructions
|
||||
in python_procpool_plugin.py.
|
||||
|
||||
To use, import run_async from this module and use instead of the
|
||||
in-process version found in evennia.utils.utils. Note that this is a much
|
||||
more complex function than the default run_async, so make sure to read
|
||||
the header carefully.
|
||||
|
||||
To test it works, make sure to activate the process pool, then try the
|
||||
following as superuser:
|
||||
|
||||
@py from contrib.procpools.python_procpool import run_async;run_async("_return('Wohoo!')", at_return=self.msg, at_err=self.msg)
|
||||
|
||||
You can also try to import time and do time.sleep(5) before the
|
||||
_return statement, to test it really is asynchronous.
|
||||
|
||||
"""
|
||||
|
||||
from twisted.protocols import amp
|
||||
from twisted.internet import threads
|
||||
from contrib.procpools.ampoule.child import AMPChild
|
||||
from evennia.utils.dbserialize import to_pickle, from_pickle, do_pickle, do_unpickle
|
||||
from evennia.utils.idmapper.base import PROC_MODIFIED_OBJS
|
||||
from evennia.utils.utils import clean_object_caches, to_str
|
||||
from evennia.utils import logger
|
||||
|
||||
|
||||
#
|
||||
# Multiprocess command for communication Server<->Client, relaying
|
||||
# data for remote Python execution
|
||||
#
|
||||
|
||||
class ExecuteCode(amp.Command):
|
||||
"""
|
||||
Executes python code in the python process,
|
||||
returning result when ready.
|
||||
|
||||
source - a compileable Python source code string
|
||||
environment - a pickled dictionary of Python
|
||||
data. Each key will become the name
|
||||
of a variable available to the source
|
||||
code. Database objects are stored on
|
||||
the form ((app, modelname), id) allowing
|
||||
the receiver to easily rebuild them on
|
||||
this side.
|
||||
errors - an all-encompassing error handler
|
||||
response - a string or a pickled string
|
||||
|
||||
"""
|
||||
arguments = [('source', amp.String()),
|
||||
('environment', amp.String())]
|
||||
errors = [(Exception, 'EXCEPTION')]
|
||||
response = [('response', amp.String()),
|
||||
('recached', amp.String())]
|
||||
|
||||
|
||||
#
|
||||
# Multiprocess AMP client-side factory, for executing remote Python code
|
||||
#
|
||||
|
||||
class PythonProcPoolChild(AMPChild):
|
||||
"""
|
||||
This is describing what happens on the subprocess side.
|
||||
|
||||
This already supports Echo, Shutdown and Ping.
|
||||
|
||||
Methods:
|
||||
executecode - a remote code execution environment
|
||||
|
||||
"""
|
||||
def executecode(self, source, environment):
|
||||
"""
|
||||
Remote code execution
|
||||
|
||||
source - Python code snippet
|
||||
environment - pickled dictionary of environment
|
||||
variables. They are stored in
|
||||
two keys "normal" and "objs" where
|
||||
normal holds a dictionary of
|
||||
normally pickled python objects
|
||||
wheras objs points to a dictionary
|
||||
of database represenations ((app,key),id).
|
||||
|
||||
The environment's entries will be made available as
|
||||
local variables during the execution. Normal eval
|
||||
results will be returned as-is. For more complex
|
||||
code snippets (run by exec), the _return function
|
||||
is available: All data sent to _return(retval) will
|
||||
be returned from this system whenever the system
|
||||
finishes. Multiple calls to _return will result in
|
||||
a list being return. The return value is pickled
|
||||
and thus allows for returning any pickleable data.
|
||||
|
||||
"""
|
||||
|
||||
class Ret(object):
|
||||
"Helper class for holding returns from exec"
|
||||
def __init__(self):
|
||||
self.returns = []
|
||||
def __call__(self, *args, **kwargs):
|
||||
self.returns.extend(list(args))
|
||||
def get_returns(self):
|
||||
lr = len(self.returns)
|
||||
val = lr and (lr == 1 and self.returns[0] or self.returns) or None
|
||||
if val not in (None, [], ()):
|
||||
return do_pickle(to_pickle(val))
|
||||
else:
|
||||
return ""
|
||||
_return = Ret()
|
||||
|
||||
available_vars = {'_return': _return}
|
||||
if environment:
|
||||
# load environment
|
||||
try:
|
||||
environment = from_pickle(do_unpickle(environment))
|
||||
available_vars.update(environment)
|
||||
except Exception:
|
||||
logger.log_trace()
|
||||
# try to execute with eval first
|
||||
try:
|
||||
ret = eval(source, {}, available_vars)
|
||||
if ret not in (None, [], ()):
|
||||
ret = _return.get_returns() or do_pickle(to_pickle(ret))
|
||||
else:
|
||||
ret = ""
|
||||
except Exception:
|
||||
# use exec instead
|
||||
exec source in available_vars
|
||||
ret = _return.get_returns()
|
||||
# get the list of affected objects to recache
|
||||
objs = PROC_MODIFIED_OBJS.values()
|
||||
# we need to include the locations too, to update their content caches
|
||||
objs = objs + list(set([o.location for o in objs
|
||||
if hasattr(o, "location") and o.location]))
|
||||
#print "objs:", objs
|
||||
#print "to_pickle", to_pickle(objs, emptypickle=False, do_pickle=False)
|
||||
if objs not in (None, [], ()):
|
||||
to_recache = do_pickle(to_pickle(objs))
|
||||
else:
|
||||
to_recache = ""
|
||||
# empty the list without loosing memory reference
|
||||
#PROC_MODIFIED_OBJS[:] = []
|
||||
PROC_MODIFIED_OBJS.clear() #TODO - is this not messing anything up?
|
||||
return {'response': ret,
|
||||
'recached': to_recache}
|
||||
ExecuteCode.responder(executecode)
|
||||
|
||||
|
||||
#
|
||||
# Procpool run_async - Server-side access function for executing
|
||||
# code in another process
|
||||
#
|
||||
|
||||
_PPOOL = None
|
||||
_SESSIONS = None
|
||||
_PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9."
|
||||
|
||||
|
||||
def run_async(to_execute, *args, **kwargs):
|
||||
"""
|
||||
Runs a function or executes a code snippet asynchronously.
|
||||
|
||||
Inputs:
|
||||
to_execute (callable) - if this is a callable, it will
|
||||
be executed with *args and non-reserver *kwargs as
|
||||
arguments.
|
||||
The callable will be executed using ProcPool, or in
|
||||
a thread if ProcPool is not available.
|
||||
to_execute (string) - this is only available is ProcPool is
|
||||
running. If a string, to_execute this will be treated as a code
|
||||
snippet to execute asynchronously. *args are then not used
|
||||
and non-reserverd *kwargs are used to define the execution
|
||||
environment made available to the code.
|
||||
|
||||
reserved kwargs:
|
||||
'use_thread' (bool) - this only works with callables (not code).
|
||||
It forces the code to run in a thread instead
|
||||
of using the Process Pool, even if the latter
|
||||
is available. This could be useful if you want
|
||||
to make sure to not get out of sync with the
|
||||
main process (such as accessing in-memory global
|
||||
properties)
|
||||
'proc_timeout' (int) - only used if ProcPool is available. Sets a
|
||||
max time for execution. This alters the value set
|
||||
by settings.PROCPOOL_TIMEOUT
|
||||
'at_return' -should point to a callable with one argument.
|
||||
It will be called with the return value from
|
||||
to_execute.
|
||||
'at_return_kwargs' - this dictionary which be used as keyword
|
||||
arguments to the at_return callback.
|
||||
'at_err' - this will be called with a Failure instance if
|
||||
there is an error in to_execute.
|
||||
'at_err_kwargs' - this dictionary will be used as keyword
|
||||
arguments to the at_err errback.
|
||||
'procpool_name' - the Service name of the procpool to use.
|
||||
Default is PythonProcPool.
|
||||
|
||||
*args - if to_execute is a callable, these args will be used
|
||||
as arguments for that function. If to_execute is a string
|
||||
*args are not used.
|
||||
*kwargs - if to_execute is a callable, these kwargs will be used
|
||||
as keyword arguments in that function. If a string, they
|
||||
instead are used to define the executable environment
|
||||
that should be available to execute the code in to_execute.
|
||||
|
||||
run_async will either relay the code to a thread or to a processPool
|
||||
depending on input and what is available in the system. To activate
|
||||
Process pooling, settings.PROCPOOL_ENABLE must be set.
|
||||
|
||||
to_execute in string form should handle all imports needed. kwargs
|
||||
can be used to send objects and properties. Such properties will
|
||||
be pickled, except Database Objects which will be sent across
|
||||
on a special format and re-loaded on the other side.
|
||||
|
||||
To get a return value from your code snippet, Use the _return()
|
||||
function: Every call to this function from your snippet will
|
||||
append the argument to an internal list of returns. This return value
|
||||
(or a list) will be the first argument to the at_return callback.
|
||||
|
||||
Use this function with restrain and only for features/commands
|
||||
that you know has no influence on the cause-and-effect order of your
|
||||
game (commands given after the async function might be executed before
|
||||
it has finished). Accessing the same property from different
|
||||
threads/processes can lead to unpredicted behaviour if you are not
|
||||
careful (this is called a "race condition").
|
||||
|
||||
Also note that some databases, notably sqlite3, don't support access from
|
||||
multiple threads simultaneously, so if you do heavy database access from
|
||||
your to_execute under sqlite3 you will probably run very slow or even get
|
||||
tracebacks.
|
||||
|
||||
"""
|
||||
# handle all global imports.
|
||||
global _PPOOL, _SESSIONS
|
||||
|
||||
# get the procpool name, if set in kwargs
|
||||
procpool_name = kwargs.get("procpool_name", "PythonProcPool")
|
||||
|
||||
if _PPOOL is None:
|
||||
# Try to load process Pool
|
||||
from evennia.server.sessionhandler import SESSIONS as _SESSIONS
|
||||
try:
|
||||
_PPOOL = _SESSIONS.server.services.namedServices.get(procpool_name).pool
|
||||
except AttributeError:
|
||||
_PPOOL = False
|
||||
|
||||
use_timeout = kwargs.pop("proc_timeout", _PPOOL.timeout)
|
||||
|
||||
# helper converters for callbacks/errbacks
|
||||
def convert_return(f):
|
||||
def func(ret, *args, **kwargs):
|
||||
rval = ret["response"] and from_pickle(do_unpickle(ret["response"]))
|
||||
reca = ret["recached"] and from_pickle(do_unpickle(ret["recached"]))
|
||||
# recache all indicated objects
|
||||
[clean_object_caches(obj) for obj in reca]
|
||||
if f:
|
||||
return f(rval, *args, **kwargs)
|
||||
else:
|
||||
return rval
|
||||
return func
|
||||
def convert_err(f):
|
||||
def func(err, *args, **kwargs):
|
||||
err.trap(Exception)
|
||||
err = err.getErrorMessage()
|
||||
if use_timeout and err == _PROC_ERR:
|
||||
err = "Process took longer than %ss and timed out." % use_timeout
|
||||
if f:
|
||||
return f(err, *args, **kwargs)
|
||||
else:
|
||||
err = "Error reported from subprocess: '%s'" % err
|
||||
logger.log_errmsg(err)
|
||||
return func
|
||||
|
||||
# handle special reserved input kwargs
|
||||
use_thread = kwargs.pop("use_thread", False)
|
||||
callback = convert_return(kwargs.pop("at_return", None))
|
||||
errback = convert_err(kwargs.pop("at_err", None))
|
||||
callback_kwargs = kwargs.pop("at_return_kwargs", {})
|
||||
errback_kwargs = kwargs.pop("at_err_kwargs", {})
|
||||
|
||||
if _PPOOL and not use_thread:
|
||||
# process pool is running
|
||||
if isinstance(to_execute, basestring):
|
||||
# run source code in process pool
|
||||
cmdargs = {"_timeout": use_timeout}
|
||||
cmdargs["source"] = to_str(to_execute)
|
||||
if kwargs:
|
||||
cmdargs["environment"] = do_pickle(to_pickle(kwargs))
|
||||
else:
|
||||
cmdargs["environment"] = ""
|
||||
# defer to process pool
|
||||
deferred = _PPOOL.doWork(ExecuteCode, **cmdargs)
|
||||
elif callable(to_execute):
|
||||
# execute callable in process
|
||||
callname = to_execute.__name__
|
||||
cmdargs = {"_timeout": use_timeout}
|
||||
cmdargs["source"] = "_return(%s(*args,**kwargs))" % callname
|
||||
cmdargs["environment"] = do_pickle(to_pickle({callname: to_execute,
|
||||
"args": args,
|
||||
"kwargs": kwargs}))
|
||||
deferred = _PPOOL.doWork(ExecuteCode, **cmdargs)
|
||||
else:
|
||||
raise RuntimeError("'%s' could not be handled by the process pool" % to_execute)
|
||||
elif callable(to_execute):
|
||||
# no process pool available, fall back to old deferToThread mechanism.
|
||||
deferred = threads.deferToThread(to_execute, *args, **kwargs)
|
||||
else:
|
||||
# no appropriate input for this server setup
|
||||
raise RuntimeError("'%s' could not be handled by run_async - no valid input or no process pool." % to_execute)
|
||||
|
||||
# attach callbacks
|
||||
if callback:
|
||||
deferred.addCallback(callback, **callback_kwargs)
|
||||
deferred.addErrback(errback, **errback_kwargs)
|
||||
116
evennia/contrib/procpools/python_procpool_plugin.py
Normal file
116
evennia/contrib/procpools/python_procpool_plugin.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
"""
|
||||
Python ProcPool plugin
|
||||
|
||||
Evennia contribution - Griatch 2012
|
||||
|
||||
This is a plugin for the Evennia services. It will make the service
|
||||
and run_async in python_procpool.py available to the system.
|
||||
|
||||
To activate, add the following line to your settings file:
|
||||
|
||||
SERVER_SERVICES_PLUGIN_MODULES.append("contrib.procpools.python_procpool_plugin")
|
||||
|
||||
Next reboot the server and the new service will be available.
|
||||
|
||||
It is not recommended to use this with an SQLite3 database, at least
|
||||
if you plan to do many out-of-process database writes. SQLite3 does
|
||||
not work very well with a high frequency of off-process writes due to
|
||||
file locking clashes. Test what works with your mileage.
|
||||
|
||||
"""
|
||||
import os
|
||||
import sys
|
||||
from django.conf import settings
|
||||
|
||||
|
||||
# Process Pool setup
|
||||
|
||||
# convenient flag to turn off process pool without changing settings
|
||||
PROCPOOL_ENABLED = True
|
||||
# relay process stdout to log (debug mode, very spammy)
|
||||
PROCPOOL_DEBUG = False
|
||||
# max/min size of the process pool. Will expand up to max limit on demand.
|
||||
PROCPOOL_MIN_NPROC = 5
|
||||
PROCPOOL_MAX_NPROC = 20
|
||||
# maximum time (seconds) a process may idle before being pruned from
|
||||
# pool (if pool bigger than minsize)
|
||||
PROCPOOL_IDLETIME = 20
|
||||
# after sending a command, this is the maximum time in seconds the process
|
||||
# may run without returning. After this time the process will be killed. This
|
||||
# can be seen as a fallback; the run_async method takes a keyword proc_timeout
|
||||
# that will override this value on a per-case basis.
|
||||
PROCPOOL_TIMEOUT = 10
|
||||
# only change if the port clashes with something else on the system
|
||||
PROCPOOL_PORT = 5001
|
||||
# 0.0.0.0 means listening to all interfaces
|
||||
PROCPOOL_INTERFACE = '127.0.0.1'
|
||||
# user-id and group-id to run the processes as (for OS:es supporting this).
|
||||
# If you plan to run unsafe code one could experiment with setting this
|
||||
# to an unprivileged user.
|
||||
PROCPOOL_UID = None
|
||||
PROCPOOL_GID = None
|
||||
# real path to a directory where all processes will be run. If
|
||||
# not given, processes will be executed in game/.
|
||||
PROCPOOL_DIRECTORY = None
|
||||
|
||||
|
||||
# don't need to change normally
|
||||
SERVICE_NAME = "PythonProcPool"
|
||||
|
||||
|
||||
# plugin hook
|
||||
|
||||
def start_plugin_services(server):
|
||||
"""
|
||||
This will be called by the Evennia Server when starting up.
|
||||
|
||||
server - the main Evennia server application
|
||||
"""
|
||||
if not PROCPOOL_ENABLED:
|
||||
return
|
||||
|
||||
# terminal output
|
||||
print ' amp (Process Pool): %s' % PROCPOOL_PORT
|
||||
|
||||
from contrib.procpools.ampoule import main as ampoule_main
|
||||
from contrib.procpools.ampoule import service as ampoule_service
|
||||
from contrib.procpools.ampoule import pool as ampoule_pool
|
||||
from contrib.procpools.ampoule.main import BOOTSTRAP as _BOOTSTRAP
|
||||
from contrib.procpools.python_procpool import PythonProcPoolChild
|
||||
|
||||
# for some reason absolute paths don't work here, only relative ones.
|
||||
apackages = ("twisted",
|
||||
os.path.join(os.pardir, "contrib", "procpools", "ampoule"),
|
||||
os.path.join(os.pardir, "ev"),
|
||||
"settings")
|
||||
aenv = {"DJANGO_SETTINGS_MODULE": "settings",
|
||||
"DATABASE_NAME": settings.DATABASES.get("default", {}).get("NAME") or settings.DATABASE_NAME}
|
||||
if PROCPOOL_DEBUG:
|
||||
_BOOTSTRAP = _BOOTSTRAP % "log.startLogging(sys.stderr)"
|
||||
else:
|
||||
_BOOTSTRAP = _BOOTSTRAP % ""
|
||||
procpool_starter = ampoule_main.ProcessStarter(packages=apackages,
|
||||
env=aenv,
|
||||
path=PROCPOOL_DIRECTORY,
|
||||
uid=PROCPOOL_UID,
|
||||
gid=PROCPOOL_GID,
|
||||
bootstrap=_BOOTSTRAP,
|
||||
childReactor=sys.platform == 'linux2' and "epoll" or "default")
|
||||
procpool = ampoule_pool.ProcessPool(name=SERVICE_NAME,
|
||||
min=PROCPOOL_MIN_NPROC,
|
||||
max=PROCPOOL_MAX_NPROC,
|
||||
recycleAfter=500,
|
||||
timeout=PROCPOOL_TIMEOUT,
|
||||
maxIdle=PROCPOOL_IDLETIME,
|
||||
ampChild=PythonProcPoolChild,
|
||||
starter=procpool_starter)
|
||||
procpool_service = ampoule_service.AMPouleService(procpool,
|
||||
PythonProcPoolChild,
|
||||
PROCPOOL_PORT,
|
||||
PROCPOOL_INTERFACE)
|
||||
procpool_service.setName(SERVICE_NAME)
|
||||
# add the new services to the server
|
||||
server.services.addService(procpool_service)
|
||||
|
||||
|
||||
|
||||
143
evennia/contrib/slow_exit.py
Normal file
143
evennia/contrib/slow_exit.py
Normal file
|
|
@ -0,0 +1,143 @@
|
|||
"""
|
||||
Slow Exit typeclass
|
||||
|
||||
Contribution - Griatch 2014
|
||||
|
||||
|
||||
This is an example of an Exit-type that delays its traversal.This
|
||||
simulates slow movement, common in many different types of games. The
|
||||
contrib also contains two commands, CmdSetSpeed and CmdStop for changing
|
||||
the movement speed and abort an ongoing traversal, respectively.
|
||||
|
||||
To try out an exit of this type, you could connect two existing rooms
|
||||
using something like this:
|
||||
|
||||
@open north:contrib.slow_exit.SlowExit = <destination>
|
||||
|
||||
|
||||
Installation:
|
||||
|
||||
To make all new exits of this type, add the following line to your
|
||||
settings:
|
||||
|
||||
BASE_EXIT_TYPECLASS = "contrib.slow_exit.SlowExit"
|
||||
|
||||
To get the ability to change your speed and abort your movement,
|
||||
simply import and add CmdSetSpeed and CmdStop from this module to your
|
||||
default cmdset (see tutorials on how to do this if you are unsure).
|
||||
|
||||
Notes:
|
||||
|
||||
This implementation is efficient but not persistent; so incomplete
|
||||
movement will be lost in a server reload. This is acceptable for most
|
||||
game types - to simulate longer travel times (more than the couple of
|
||||
seconds assumed here), a more persistent variant using Scripts or the
|
||||
TickerHandler might be better.
|
||||
|
||||
"""
|
||||
|
||||
from evennia import Exit, utils, Command
|
||||
|
||||
MOVE_DELAY = {"stroll": 6,
|
||||
"walk": 4,
|
||||
"run": 2,
|
||||
"sprint": 1}
|
||||
|
||||
class SlowExit(Exit):
|
||||
"""
|
||||
This overloads the way moving happens.
|
||||
"""
|
||||
def at_traverse(self, traversing_object, target_location):
|
||||
"""
|
||||
Implements the actual traversal, using utils.delay to delay the move_to.
|
||||
"""
|
||||
|
||||
# if the traverser has an Attribute move_speed, use that,
|
||||
# otherwise default to "walk" speed
|
||||
move_speed = traversing_object.db.move_speed or "walk"
|
||||
move_delay = MOVE_DELAY.get(move_speed, 4)
|
||||
|
||||
def move_callback():
|
||||
"This callback will be called by utils.delay after move_delay seconds."
|
||||
source_location = traversing_object.location
|
||||
if traversing_object.move_to(target_location):
|
||||
self.at_after_traverse(traversing_object, source_location)
|
||||
else:
|
||||
if self.db.err_traverse:
|
||||
# if exit has a better error message, let's use it.
|
||||
self.caller.msg(self.db.err_traverse)
|
||||
else:
|
||||
# No shorthand error message. Call hook.
|
||||
self.at_failed_traverse(traversing_object)
|
||||
|
||||
traversing_object.msg("You start moving %s at a %s." % (self.key, move_speed))
|
||||
# create a delayed movement
|
||||
deferred = utils.delay(move_delay, callback=move_callback)
|
||||
# we store the deferred on the character, this will allow us
|
||||
# to abort the movement. We must use an ndb here since
|
||||
# deferreds cannot be pickled.
|
||||
traversing_object.ndb.currently_moving = deferred
|
||||
|
||||
|
||||
#
|
||||
# set speed - command
|
||||
#
|
||||
|
||||
SPEED_DESCS = {"stroll": "strolling",
|
||||
"walk": "walking",
|
||||
"run": "running",
|
||||
"sprint": "sprinting"}
|
||||
|
||||
class CmdSetSpeed(Command):
|
||||
"""
|
||||
set your movement speed
|
||||
|
||||
Usage:
|
||||
setspeed stroll|walk|run|sprint
|
||||
|
||||
This will set your movement speed, determining how long time
|
||||
it takes to traverse exits. If no speed is set, 'walk' speed
|
||||
is assumed.
|
||||
"""
|
||||
key = "setspeed"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Simply sets an Attribute used by the SlowExit above.
|
||||
"""
|
||||
speed = self.args.lower().strip()
|
||||
if speed not in SPEED_DESCS:
|
||||
self.caller.msg("Usage: setspeed stroll|walk|run|sprint")
|
||||
elif self.caller.db.move_speed == speed:
|
||||
self.caller.msg("You are already %s." % SPEED_DESCS[speed])
|
||||
else:
|
||||
self.caller.db.move_speed = speed
|
||||
self.caller.msg("You are now %s." % SPEED_DESCS[speed])
|
||||
|
||||
|
||||
#
|
||||
# stop moving - command
|
||||
#
|
||||
|
||||
class CmdStop(Command):
|
||||
"""
|
||||
stop moving
|
||||
|
||||
Usage:
|
||||
stop
|
||||
|
||||
Stops the current movement, if any.
|
||||
"""
|
||||
key = "stop"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
This is a very simple command, using the
|
||||
stored deferred from the exit traversal above.
|
||||
"""
|
||||
currently_moving = self.caller.ndb.currently_moving
|
||||
if currently_moving:
|
||||
currently_moving.cancel()
|
||||
self.caller.msg("You stop moving.")
|
||||
else:
|
||||
self.caller.msg("You are not moving.")
|
||||
125
evennia/contrib/talking_npc.py
Normal file
125
evennia/contrib/talking_npc.py
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
"""
|
||||
|
||||
Evennia Talkative NPC
|
||||
|
||||
Contribution - Griatch 2011
|
||||
|
||||
This is a simple NPC object capable of holding a
|
||||
simple menu-driven conversation. Create it by
|
||||
creating an object of typeclass contrib.talking_npc.TalkingNPC,
|
||||
For example using @create:
|
||||
|
||||
@create John : contrib.talking_npc.TalkingNPC
|
||||
|
||||
Walk up to it and give the talk command
|
||||
to strike up a conversation. If there are many
|
||||
talkative npcs in the same room you will get to
|
||||
choose which one's talk command to call (Evennia
|
||||
handles this automatically).
|
||||
|
||||
Note that this is only a prototype class, showcasing
|
||||
the uses of the menusystem module. It is NOT a full
|
||||
mob implementation.
|
||||
|
||||
"""
|
||||
|
||||
from evennia import Object, CmdSet, default_cmds
|
||||
from contrib import menusystem
|
||||
|
||||
|
||||
#
|
||||
# The talk command
|
||||
#
|
||||
|
||||
class CmdTalk(default_cmds.MuxCommand):
|
||||
"""
|
||||
talks to an npc
|
||||
|
||||
Usage:
|
||||
talk
|
||||
|
||||
This command is only available if a talkative non-player-character (NPC)
|
||||
is actually present. It will strike up a conversation with that NPC
|
||||
and give you options on what to talk about.
|
||||
"""
|
||||
key = "talk"
|
||||
locks = "cmd:all()"
|
||||
help_category = "General"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
|
||||
# self.obj is the NPC this is defined on
|
||||
obj = self.obj
|
||||
|
||||
self.caller.msg("(You walk up and talk to %s.)" % self.obj.key)
|
||||
|
||||
# conversation is a dictionary of keys, each pointing to
|
||||
# a dictionary defining the keyword arguments to the MenuNode
|
||||
# constructor.
|
||||
conversation = obj.db.conversation
|
||||
if not conversation:
|
||||
self.caller.msg("%s says: 'Sorry, I don't have time to talk right now.'" % (self.obj.key))
|
||||
return
|
||||
|
||||
# build all nodes by loading them from the conversation tree.
|
||||
menu = menusystem.MenuTree(self.caller)
|
||||
for key, kwargs in conversation.items():
|
||||
menu.add(menusystem.MenuNode(key, **kwargs))
|
||||
menu.start()
|
||||
|
||||
|
||||
class TalkingCmdSet(CmdSet):
|
||||
"Stores the talk command."
|
||||
key = "talkingcmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"populates the cmdset"
|
||||
self.add(CmdTalk())
|
||||
|
||||
#
|
||||
# Discussion tree. See contrib.menusystem.MenuNode for the keywords.
|
||||
# (This could be in a separate module too)
|
||||
#
|
||||
|
||||
CONV = {"START": {"text": "Hello there, how can I help you?",
|
||||
"links": ["info1", "info2"],
|
||||
"linktexts": ["Hey, do you know what this 'Evennia' thing is all about?",
|
||||
"What's your name, little NPC?"],
|
||||
"keywords": None,
|
||||
"code": None},
|
||||
"info1": {"text": "Oh, Evennia is where you are right now! Don't you feel the power?",
|
||||
"links": ["info3", "info2", "END"],
|
||||
"linktexts":["Sure, *I* do, not sure how you do though. You are just an NPC.",
|
||||
"Sure I do. What's yer name, NPC?",
|
||||
"Ok, bye for now then."],
|
||||
"keywords": None,
|
||||
"code": None},
|
||||
"info2": {"text": "My name is not really important ... I'm just an NPC after all.",
|
||||
"links": ["info3", "info1"],
|
||||
"linktexts": ["I didn't really want to know it anyhow.",
|
||||
"Okay then, so what's this 'Evennia' thing about?"],
|
||||
"keywords": None,
|
||||
"code": None},
|
||||
"info3": {"text": "Well ... I'm sort of busy so, have to go. NPC business. Important stuff. You wouldn't understand.",
|
||||
"links": ["END", "info2"],
|
||||
"linktexts": ["Oookay ... I won't keep you. Bye.",
|
||||
"Wait, why don't you tell me your name first?"],
|
||||
"keywords": None,
|
||||
"code": None},
|
||||
}
|
||||
|
||||
|
||||
class TalkingNPC(Object):
|
||||
"""
|
||||
This implements a simple Object using the talk command and using the
|
||||
conversation defined above. .
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"This is called when object is first created."
|
||||
# store the conversation.
|
||||
self.db.conversation = CONV
|
||||
self.db.desc = "This is a talkative NPC."
|
||||
# assign the talk command to npc
|
||||
self.cmdset.add_default(TalkingCmdSet, permanent=True)
|
||||
0
evennia/contrib/tutorial_examples/__init__.py
Normal file
0
evennia/contrib/tutorial_examples/__init__.py
Normal file
56
evennia/contrib/tutorial_examples/batch_cmds.ev
Normal file
56
evennia/contrib/tutorial_examples/batch_cmds.ev
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
#
|
||||
# This is an example batch build file for Evennia.
|
||||
#
|
||||
# It allows batch processing of normal Evennia commands.
|
||||
# Test it by loading it with the @batchprocess command
|
||||
#
|
||||
# @batchprocess[/interactive] examples.batch_example
|
||||
#
|
||||
# A # as the first symbol on a line begins a comment and
|
||||
# marks the end of a previous command definition (important!).
|
||||
#
|
||||
# All supplied commands are given as normal, on their own line
|
||||
# and accepts arguments in any format up until the first next
|
||||
# comment line begins. Extra whitespace is removed; an empty
|
||||
# line in a command definition translates into a newline.
|
||||
#
|
||||
|
||||
# This creates a red button
|
||||
|
||||
@create button:tutorial_examples.red_button.RedButton
|
||||
|
||||
# This comment ends input for @create
|
||||
# Next command:
|
||||
|
||||
@set button/desc =
|
||||
This is a large red button. Now and then
|
||||
it flashes in an evil, yet strangely tantalizing way.
|
||||
|
||||
A big sign sits next to it. It says:
|
||||
|
||||
|
||||
-----------
|
||||
|
||||
Press me!
|
||||
|
||||
-----------
|
||||
|
||||
|
||||
... It really begs to be pressed, doesn't it? You
|
||||
know you want to!
|
||||
|
||||
# This ends the @set command. Note that line breaks and extra spaces
|
||||
# in the argument are not considered. A completely empty line
|
||||
# translates to a \n newline in the command; two empty lines will thus
|
||||
# create a new paragraph. (note that few commands support it though, you
|
||||
# mainly want to use it for descriptions)
|
||||
|
||||
# Now let's place the button where it belongs (let's say limbo #2 is
|
||||
# the evil lair in our example)
|
||||
|
||||
@teleport #2
|
||||
|
||||
#... and drop it (remember, this comment ends input to @teleport, so don't
|
||||
#forget it!) The very last command in the file need not be ended with #.
|
||||
|
||||
drop button
|
||||
320
evennia/contrib/tutorial_examples/cmdset_red_button.py
Normal file
320
evennia/contrib/tutorial_examples/cmdset_red_button.py
Normal file
|
|
@ -0,0 +1,320 @@
|
|||
"""
|
||||
This defines the cmdset for the red_button. Here we have defined
|
||||
the commands and the cmdset in the same module, but if you
|
||||
have many different commands to merge it is often better
|
||||
to define the cmdset separately, picking and choosing from
|
||||
among the available commands as to what should be included in the
|
||||
cmdset - this way you can often re-use the commands too.
|
||||
"""
|
||||
|
||||
import random
|
||||
from evennia import Command, CmdSet
|
||||
|
||||
# Some simple commands for the red button
|
||||
|
||||
#------------------------------------------------------------
|
||||
# Commands defined on the red button
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdNudge(Command):
|
||||
"""
|
||||
Try to nudge the button's lid
|
||||
|
||||
Usage:
|
||||
nudge lid
|
||||
|
||||
This command will have you try to
|
||||
push the lid of the button away.
|
||||
"""
|
||||
|
||||
key = "nudge lid" # two-word command name!
|
||||
aliases = ["nudge"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
nudge the lid. Random chance of success to open it.
|
||||
"""
|
||||
rand = random.random()
|
||||
if rand < 0.5:
|
||||
self.caller.msg("You nudge at the lid. It seems stuck.")
|
||||
elif 0.5 <= rand < 0.7:
|
||||
self.caller.msg("You move the lid back and forth. It won't budge.")
|
||||
else:
|
||||
self.caller.msg("You manage to get a nail under the lid.")
|
||||
self.caller.execute_cmd("open lid")
|
||||
|
||||
|
||||
class CmdPush(Command):
|
||||
"""
|
||||
Push the red button
|
||||
|
||||
Usage:
|
||||
push button
|
||||
|
||||
"""
|
||||
key = "push button"
|
||||
aliases = ["push", "press button", "press"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Note that we choose to implement this with checking for
|
||||
if the lid is open/closed. This is because this command
|
||||
is likely to be tried regardless of the state of the lid.
|
||||
|
||||
An alternative would be to make two versions of this command
|
||||
and tuck them into the cmdset linked to the Open and Closed
|
||||
lid-state respectively.
|
||||
|
||||
"""
|
||||
|
||||
if self.obj.db.lid_open:
|
||||
string = "You reach out to press the big red button ..."
|
||||
string += "\n\nA BOOM! A bright light blinds you!"
|
||||
string += "\nThe world goes dark ..."
|
||||
self.caller.msg(string)
|
||||
self.caller.location.msg_contents("%s presses the button. BOOM! %s is blinded by a flash!" %
|
||||
(self.caller.name, self.caller.name), exclude=self.caller)
|
||||
# the button's method will handle all setup of scripts etc.
|
||||
self.obj.press_button(self.caller)
|
||||
else:
|
||||
string = "You cannot push the button - there is a glass lid covering it."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdSmashGlass(Command):
|
||||
"""
|
||||
smash glass
|
||||
|
||||
Usage:
|
||||
smash glass
|
||||
|
||||
Try to smash the glass of the button.
|
||||
"""
|
||||
|
||||
key = "smash glass"
|
||||
aliases = ["smash lid", "break lid", "smash"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
The lid won't open, but there is a small chance
|
||||
of causing the lamp to break.
|
||||
"""
|
||||
rand = random.random()
|
||||
|
||||
if rand < 0.2:
|
||||
string = "You smash your hand against the glass"
|
||||
string += " with all your might. The lid won't budge"
|
||||
string += " but you cause quite the tremor through the button's mount."
|
||||
string += "\nIt looks like the button's lamp stopped working for the time being."
|
||||
self.obj.lamp_works = False
|
||||
elif rand < 0.6:
|
||||
string = "You hit the lid hard. It doesn't move an inch."
|
||||
else:
|
||||
string = "You place a well-aimed fist against the glass of the lid."
|
||||
string += " Unfortunately all you get is a pain in your hand. Maybe"
|
||||
string += " you should just try to open the lid instead?"
|
||||
self.caller.msg(string)
|
||||
self.caller.location.msg_contents("%s tries to smash the glass of the button." %
|
||||
(self.caller.name), exclude=self.caller)
|
||||
|
||||
|
||||
class CmdOpenLid(Command):
|
||||
"""
|
||||
open lid
|
||||
|
||||
Usage:
|
||||
open lid
|
||||
|
||||
"""
|
||||
|
||||
key = "open lid"
|
||||
aliases = ["open button", 'open']
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"simply call the right function."
|
||||
|
||||
if self.obj.db.lid_locked:
|
||||
self.caller.msg("This lid seems locked in place for the moment.")
|
||||
return
|
||||
|
||||
string = "\nA ticking sound is heard, like a winding mechanism. Seems "
|
||||
string += "the lid will soon close again."
|
||||
self.caller.msg(string)
|
||||
self.caller.location.msg_contents("%s opens the lid of the button." %
|
||||
(self.caller.name), exclude=self.caller)
|
||||
# add the relevant cmdsets to button
|
||||
self.obj.cmdset.add(LidClosedCmdSet)
|
||||
# call object method
|
||||
self.obj.open_lid()
|
||||
|
||||
|
||||
class CmdCloseLid(Command):
|
||||
"""
|
||||
close the lid
|
||||
|
||||
Usage:
|
||||
close lid
|
||||
|
||||
Closes the lid of the red button.
|
||||
"""
|
||||
|
||||
key = "close lid"
|
||||
aliases = ["close"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Close the lid"
|
||||
|
||||
self.obj.close_lid()
|
||||
|
||||
# this will clean out scripts dependent on lid being open.
|
||||
self.caller.msg("You close the button's lid. It clicks back into place.")
|
||||
self.caller.location.msg_contents("%s closes the button's lid." %
|
||||
(self.caller.name), exclude=self.caller)
|
||||
|
||||
|
||||
class CmdBlindLook(Command):
|
||||
"""
|
||||
Looking around in darkness
|
||||
|
||||
Usage:
|
||||
look <obj>
|
||||
|
||||
... not that there's much to see in the dark.
|
||||
|
||||
"""
|
||||
|
||||
key = "look"
|
||||
aliases = ["l", "get", "examine", "ex", "feel", "listen"]
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"This replaces all the senses when blinded."
|
||||
|
||||
# we decide what to reply based on which command was
|
||||
# actually tried
|
||||
|
||||
if self.cmdstring == "get":
|
||||
string = "You fumble around blindly without finding anything."
|
||||
elif self.cmdstring == "examine":
|
||||
string = "You try to examine your surroundings, but can't see a thing."
|
||||
elif self.cmdstring == "listen":
|
||||
string = "You are deafened by the boom."
|
||||
elif self.cmdstring == "feel":
|
||||
string = "You fumble around, hands outstretched. You bump your knee."
|
||||
else:
|
||||
# trying to look
|
||||
string = "You are temporarily blinded by the flash. "
|
||||
string += "Until it wears off, all you can do is feel around blindly."
|
||||
self.caller.msg(string)
|
||||
self.caller.location.msg_contents("%s stumbles around, blinded." %
|
||||
(self.caller.name), exclude=self.caller)
|
||||
|
||||
|
||||
class CmdBlindHelp(Command):
|
||||
"""
|
||||
Help function while in the blinded state
|
||||
|
||||
Usage:
|
||||
help
|
||||
|
||||
"""
|
||||
key = "help"
|
||||
aliases = "h"
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Give a message."
|
||||
self.caller.msg("You are beyond help ... until you can see again.")
|
||||
|
||||
|
||||
#---------------------------------------------------------------
|
||||
# Command sets for the red button
|
||||
#---------------------------------------------------------------
|
||||
|
||||
# We next tuck these commands into their respective command sets.
|
||||
# (note that we are overdoing the cdmset separation a bit here
|
||||
# to show how it works).
|
||||
|
||||
class DefaultCmdSet(CmdSet):
|
||||
"""
|
||||
The default cmdset always sits
|
||||
on the button object and whereas other
|
||||
command sets may be added/merge onto it
|
||||
and hide it, removing them will always
|
||||
bring it back. It's added to the object
|
||||
using obj.cmdset.add_default().
|
||||
"""
|
||||
key = "RedButtonDefault"
|
||||
mergetype = "Union" # this is default, we don't really need to put it here.
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Init the cmdset"
|
||||
self.add(CmdPush())
|
||||
|
||||
|
||||
class LidClosedCmdSet(CmdSet):
|
||||
"""
|
||||
A simple cmdset tied to the redbutton object.
|
||||
|
||||
It contains the commands that launches the other
|
||||
command sets, making the red button a self-contained
|
||||
item (i.e. you don't have to manually add any
|
||||
scripts etc to it when creating it).
|
||||
"""
|
||||
key = "LidClosedCmdSet"
|
||||
# default Union is used *except* if we are adding to a
|
||||
# cmdset named LidOpenCmdSet - this one we replace
|
||||
# completely.
|
||||
key_mergetype = {"LidOpenCmdSet": "Replace"}
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Populates the cmdset when it is instantiated."
|
||||
self.add(CmdNudge())
|
||||
self.add(CmdSmashGlass())
|
||||
self.add(CmdOpenLid())
|
||||
|
||||
|
||||
class LidOpenCmdSet(CmdSet):
|
||||
"""
|
||||
This is the opposite of the Closed cmdset.
|
||||
"""
|
||||
key = "LidOpenCmdSet"
|
||||
# default Union is used *except* if we are adding to a
|
||||
# cmdset named LidClosedCmdSet - this one we replace
|
||||
# completely.
|
||||
key_mergetype = {"LidClosedCmdSet": "Replace"}
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"setup the cmdset (just one command)"
|
||||
self.add(CmdCloseLid())
|
||||
|
||||
|
||||
class BlindCmdSet(CmdSet):
|
||||
"""
|
||||
This is the cmdset added to the *player* when
|
||||
the button is pushed.
|
||||
"""
|
||||
key = "BlindCmdSet"
|
||||
# we want it to completely replace all normal commands
|
||||
# until the timed script removes it again.
|
||||
mergetype = "Replace"
|
||||
# we want to stop the player from walking around
|
||||
# in this blinded state, so we hide all exits too.
|
||||
# (channel commands will still work).
|
||||
no_exits = True # keep player in the same room
|
||||
no_objs = True # don't allow object commands
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Setup the blind cmdset"
|
||||
from evennia.commands.default.general import CmdSay
|
||||
from evennia.commands.default.general import CmdPose
|
||||
self.add(CmdSay())
|
||||
self.add(CmdPose())
|
||||
self.add(CmdBlindLook())
|
||||
self.add(CmdBlindHelp())
|
||||
158
evennia/contrib/tutorial_examples/red_button.py
Normal file
158
evennia/contrib/tutorial_examples/red_button.py
Normal file
|
|
@ -0,0 +1,158 @@
|
|||
"""
|
||||
|
||||
This is a more advanced example object. It combines functions from
|
||||
script.examples as well as commands.examples to make an interactive
|
||||
button typeclass.
|
||||
|
||||
Create this button with
|
||||
|
||||
@create/drop examples.red_button.RedButton
|
||||
|
||||
Note that you must drop the button before you can see its messages!
|
||||
"""
|
||||
import random
|
||||
from evennia import DefaultObject
|
||||
from contrib.tutorial_examples import red_button_scripts as scriptexamples
|
||||
from contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
||||
|
||||
#
|
||||
# Definition of the object itself
|
||||
#
|
||||
|
||||
|
||||
class RedButton(DefaultObject):
|
||||
"""
|
||||
This class describes an evil red button. It will use the script
|
||||
definition in contrib/examples/red_button_scripts to blink at regular
|
||||
intervals. It also uses a series of script and commands to handle
|
||||
pushing the button and causing effects when doing so.
|
||||
|
||||
The following attributes can be set on the button:
|
||||
desc_lid_open - description when lid is open
|
||||
desc_lid_closed - description when lid is closed
|
||||
desc_lamp_broken - description when lamp is broken
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"""
|
||||
This function is called when object is created. Use this
|
||||
instead of e.g. __init__.
|
||||
"""
|
||||
# store desc (default, you can change this at creation time)
|
||||
desc = "This is a large red button, inviting yet evil-looking. "
|
||||
desc += "A closed glass lid protects it."
|
||||
self.db.desc = desc
|
||||
|
||||
# We have to define all the variables the scripts
|
||||
# are checking/using *before* adding the scripts or
|
||||
# they might be deactivated before even starting!
|
||||
self.db.lid_open = False
|
||||
self.db.lamp_works = True
|
||||
self.db.lid_locked = False
|
||||
|
||||
self.cmdset.add_default(cmdsetexamples.DefaultCmdSet, permanent=True)
|
||||
|
||||
# since the cmdsets relevant to the button are added 'on the fly',
|
||||
# we need to setup custom scripts to do this for us (also, these scripts
|
||||
# check so they are valid (i.e. the lid is actually still closed)).
|
||||
# The AddClosedCmdSet script makes sure to add the Closed-cmdset.
|
||||
self.scripts.add(scriptexamples.ClosedLidState)
|
||||
# the script EventBlinkButton makes the button blink regularly.
|
||||
self.scripts.add(scriptexamples.BlinkButtonEvent)
|
||||
|
||||
# state-changing methods
|
||||
|
||||
def open_lid(self):
|
||||
"""
|
||||
Opens the glass lid and start the timer so it will soon close
|
||||
again.
|
||||
"""
|
||||
|
||||
if self.db.lid_open:
|
||||
return
|
||||
desc = self.db.desc_lid_open
|
||||
if not desc:
|
||||
desc = "This is a large red button, inviting yet evil-looking. "
|
||||
desc += "Its glass cover is open and the button exposed."
|
||||
self.db.desc = desc
|
||||
self.db.lid_open = True
|
||||
|
||||
# with the lid open, we validate scripts; this will clean out
|
||||
# scripts that depend on the lid to be closed.
|
||||
self.scripts.validate()
|
||||
# now add new scripts that define the open-lid state
|
||||
self.scripts.add(scriptexamples.OpenLidState)
|
||||
# we also add a scripted event that will close the lid after a while.
|
||||
# (this one cleans itself after being called once)
|
||||
self.scripts.add(scriptexamples.CloseLidEvent)
|
||||
|
||||
def close_lid(self):
|
||||
"""
|
||||
Close the glass lid. This validates all scripts on the button,
|
||||
which means that scripts only being valid when the lid is open
|
||||
will go away automatically.
|
||||
"""
|
||||
|
||||
if not self.db.lid_open:
|
||||
return
|
||||
desc = self.db.desc_lid_closed
|
||||
if not desc:
|
||||
desc = "This is a large red button, inviting yet evil-looking. "
|
||||
desc += "Its glass cover is closed, protecting it."
|
||||
self.db.desc = desc
|
||||
self.db.lid_open = False
|
||||
|
||||
# clean out scripts depending on lid to be open
|
||||
self.scripts.validate()
|
||||
# add scripts related to the closed state
|
||||
self.scripts.add(scriptexamples.ClosedLidState)
|
||||
|
||||
def break_lamp(self, feedback=True):
|
||||
"""
|
||||
Breaks the lamp in the button, stopping it from blinking.
|
||||
|
||||
"""
|
||||
self.db.lamp_works = False
|
||||
desc = self.db.desc_lamp_broken
|
||||
if not desc:
|
||||
self.db.desc += "\nThe big red button has stopped blinking for the time being."
|
||||
else:
|
||||
self.db.desc = desc
|
||||
|
||||
if feedback and self.location:
|
||||
self.location.msg_contents("The lamp flickers, the button going dark.")
|
||||
self.scripts.validate()
|
||||
|
||||
def press_button(self, pobject):
|
||||
"""
|
||||
Someone was foolish enough to press the button!
|
||||
pobject - the person pressing the button
|
||||
"""
|
||||
# deactivate the button so it won't flash/close lid etc.
|
||||
self.scripts.add(scriptexamples.DeactivateButtonEvent)
|
||||
# blind the person pressing the button. Note that this
|
||||
# script is set on the *character* pressing the button!
|
||||
pobject.scripts.add(scriptexamples.BlindedState)
|
||||
|
||||
# script-related methods
|
||||
|
||||
def blink(self):
|
||||
"""
|
||||
The script system will regularly call this
|
||||
function to make the button blink. Now and then
|
||||
it won't blink at all though, to add some randomness
|
||||
to how often the message is echoed.
|
||||
"""
|
||||
loc = self.location
|
||||
if loc:
|
||||
rand = random.random()
|
||||
if rand < 0.2:
|
||||
string = "The red button flashes briefly."
|
||||
elif rand < 0.4:
|
||||
string = "The red button blinks invitingly."
|
||||
elif rand < 0.6:
|
||||
string = "The red button flashes. You know you wanna push it!"
|
||||
else:
|
||||
# no blink
|
||||
return
|
||||
loc.msg_contents(string)
|
||||
275
evennia/contrib/tutorial_examples/red_button_scripts.py
Normal file
275
evennia/contrib/tutorial_examples/red_button_scripts.py
Normal file
|
|
@ -0,0 +1,275 @@
|
|||
"""
|
||||
Example of scripts.
|
||||
|
||||
These are scripts intended for a particular object - the
|
||||
red_button object type in contrib/examples. A few variations
|
||||
on uses of scripts are included.
|
||||
|
||||
"""
|
||||
from evennia import Script
|
||||
from contrib.tutorial_examples import cmdset_red_button as cmdsetexamples
|
||||
|
||||
#
|
||||
# Scripts as state-managers
|
||||
#
|
||||
# Scripts have many uses, one of which is to statically
|
||||
# make changes when a particular state of an object changes.
|
||||
# There is no "timer" involved in this case (although there could be),
|
||||
# whenever the script determines it is "invalid", it simply shuts down
|
||||
# along with all the things it controls.
|
||||
#
|
||||
# To show as many features as possible of the script and cmdset systems,
|
||||
# we will use three scripts controlling one state each of the red_button,
|
||||
# each with its own set of commands, handled by cmdsets - one for when
|
||||
# the button has its lid open, and one for when it is closed and a
|
||||
# last one for when the player pushed the button and gets blinded by
|
||||
# a bright light. The last one also has a timer component that allows it
|
||||
# to remove itself after a while (and the player recovers their eyesight).
|
||||
|
||||
class ClosedLidState(Script):
|
||||
"""
|
||||
This manages the cmdset for the "closed" button state. What this
|
||||
means is that while this script is valid, we add the RedButtonClosed
|
||||
cmdset to it (with commands like open, nudge lid etc)
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called when script first created."
|
||||
self.desc = "Script that manages the closed-state cmdsets for red button."
|
||||
self.persistent = True
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
This is called once every server restart, so we want to add the
|
||||
(memory-resident) cmdset to the object here. is_valid is automatically
|
||||
checked so we don't need to worry about adding the script to an
|
||||
open lid.
|
||||
"""
|
||||
#All we do is add the cmdset for the closed state.
|
||||
self.obj.cmdset.add(cmdsetexamples.LidClosedCmdSet)
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
The script is only valid while the lid is closed.
|
||||
self.obj is the red_button on which this script is defined.
|
||||
"""
|
||||
return not self.obj.db.lid_open
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
When the script stops we must make sure to clean up after us.
|
||||
|
||||
"""
|
||||
self.obj.cmdset.delete(cmdsetexamples.LidClosedCmdSet)
|
||||
|
||||
|
||||
class OpenLidState(Script):
|
||||
"""
|
||||
This manages the cmdset for the "open" button state. This will add
|
||||
the RedButtonOpen
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called when script first created."
|
||||
self.desc = "Script that manages the opened-state cmdsets for red button."
|
||||
self.persistent = True
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
This is called once every server restart, so we want to add the
|
||||
(memory-resident) cmdset to the object here. is_valid is
|
||||
automatically checked, so we don't need to worry about
|
||||
adding the cmdset to a closed lid-button.
|
||||
"""
|
||||
#print "In Open at_start (should add cmdset)"
|
||||
self.obj.cmdset.add(cmdsetexamples.LidOpenCmdSet)
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
The script is only valid while the lid is open.
|
||||
self.obj is the red_button on which this script is defined.
|
||||
"""
|
||||
return self.obj.db.lid_open
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
When the script stops (like if the lid is closed again)
|
||||
we must make sure to clean up after us.
|
||||
"""
|
||||
self.obj.cmdset.delete(cmdsetexamples.LidOpenCmdSet)
|
||||
|
||||
|
||||
class BlindedState(Script):
|
||||
"""
|
||||
This is a timed state.
|
||||
|
||||
This adds a (very limited) cmdset TO THE PLAYER, during a certain time,
|
||||
after which the script will close and all functions are
|
||||
restored. It's up to the function starting the script to actually
|
||||
set it on the right player object.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
We set up the script here.
|
||||
"""
|
||||
self.key = "temporary_blinder"
|
||||
self.desc = "Temporarily blinds the player for a little while."
|
||||
self.interval = 20 # seconds
|
||||
self.start_delay = True # we don't want it to stop until after 20s.
|
||||
self.repeats = 1 # this will go away after interval seconds.
|
||||
self.persistent = False # we will ditch this if server goes down
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
We want to add the cmdset to the linked object.
|
||||
|
||||
Note that the RedButtonBlind cmdset is defined to completly
|
||||
replace the other cmdsets on the stack while it is active
|
||||
(this means that while blinded, only operations in this cmdset
|
||||
will be possible for the player to perform). It is however
|
||||
not persistent, so should there be a bug in it, we just need
|
||||
to restart the server to clear out of it during development.
|
||||
"""
|
||||
self.obj.cmdset.add(cmdsetexamples.BlindCmdSet)
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
It's important that we clear out that blinded cmdset
|
||||
when we are done!
|
||||
"""
|
||||
self.obj.msg("You blink feverishly as your eyesight slowly returns.")
|
||||
self.obj.location.msg_contents("%s seems to be recovering their eyesight."
|
||||
% self.obj.name,
|
||||
exclude=self.obj)
|
||||
self.obj.cmdset.delete() # this will clear the latest added cmdset,
|
||||
# (which is the blinded one).
|
||||
|
||||
|
||||
#
|
||||
# Timer/Event-like Scripts
|
||||
#
|
||||
# Scripts can also work like timers, or "events". Below we
|
||||
# define three such timed events that makes the button a little
|
||||
# more "alive" - one that makes the button blink menacingly, another
|
||||
# that makes the lid covering the button slide back after a while.
|
||||
#
|
||||
|
||||
class CloseLidEvent(Script):
|
||||
"""
|
||||
This event closes the glass lid over the button
|
||||
some time after it was opened. It's a one-off
|
||||
script that should be started/created when the
|
||||
lid is opened.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Called when script object is first created. Sets things up.
|
||||
We want to have a lid on the button that the user can pull
|
||||
aside in order to make the button 'pressable'. But after a set
|
||||
time that lid should auto-close again, making the button safe
|
||||
from pressing (and deleting this command).
|
||||
"""
|
||||
self.key = "lid_closer"
|
||||
self.desc = "Closes lid on a red buttons"
|
||||
self.interval = 20 # seconds
|
||||
self.start_delay = True # we want to pospone the launch.
|
||||
self.repeats = 1 # we only close the lid once
|
||||
self.persistent = True # even if the server crashes in those 20 seconds,
|
||||
# the lid will still close once the game restarts.
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
This script can only operate if the lid is open; if it
|
||||
is already closed, the script is clearly invalid.
|
||||
|
||||
Note that we are here relying on an self.obj being
|
||||
defined (and being a RedButton object) - this we should be able to
|
||||
expect since this type of script is always tied to one individual
|
||||
red button object and not having it would be an error.
|
||||
"""
|
||||
return self.obj.db.lid_open
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Called after self.interval seconds. It closes the lid. Before this method is
|
||||
called, self.is_valid() is automatically checked, so there is no need to
|
||||
check this manually.
|
||||
"""
|
||||
self.obj.close_lid()
|
||||
|
||||
class BlinkButtonEvent(Script):
|
||||
"""
|
||||
This timed script lets the button flash at regular intervals.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Sets things up. We want the button's lamp to blink at
|
||||
regular intervals, unless it's broken (can happen
|
||||
if you try to smash the glass, say).
|
||||
"""
|
||||
self.key = "blink_button"
|
||||
self.desc = "Blinks red buttons"
|
||||
self.interval = 35 #seconds
|
||||
self.start_delay = False #blink right away
|
||||
self.persistent = True #keep blinking also after server reboot
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
Button will keep blinking unless it is broken.
|
||||
"""
|
||||
#print "self.obj.db.lamp_works:", self.obj.db.lamp_works
|
||||
return self.obj.db.lamp_works
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
Called every self.interval seconds. Makes the lamp in
|
||||
the button blink.
|
||||
"""
|
||||
self.obj.blink()
|
||||
|
||||
class DeactivateButtonEvent(Script):
|
||||
"""
|
||||
This deactivates the button for a short while (it won't blink, won't
|
||||
close its lid etc). It is meant to be called when the button is pushed
|
||||
and run as long as the blinded effect lasts. We cannot put these methods
|
||||
in the AddBlindedCmdSet script since that script is defined on the *player*
|
||||
whereas this one must be defined on the *button*.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"""
|
||||
Sets things up.
|
||||
"""
|
||||
self.key = "deactivate_button"
|
||||
self.desc = "Deactivate red button temporarily"
|
||||
self.interval = 21 #seconds
|
||||
self.start_delay = True # wait with the first repeat for self.interval seconds.
|
||||
self.persistent = True
|
||||
self.repeats = 1 # only do this once
|
||||
|
||||
def at_start(self):
|
||||
"""
|
||||
Deactivate the button. Observe that this method is always
|
||||
called directly, regardless of the value of self.start_delay
|
||||
(that just controls when at_repeat() is called)
|
||||
"""
|
||||
# closing the lid will also add the ClosedState script
|
||||
self.obj.close_lid()
|
||||
# lock the lid so other players can't access it until the
|
||||
# first one's effect has worn off.
|
||||
self.obj.db.lid_locked = True
|
||||
# breaking the lamp also sets a correct desc
|
||||
self.obj.break_lamp(feedback=False)
|
||||
|
||||
def at_repeat(self):
|
||||
"""
|
||||
When this is called, reset the functionality of the button.
|
||||
"""
|
||||
# restore button's desc.
|
||||
|
||||
self.obj.db.lamp_works = True
|
||||
desc = "This is a large red button, inviting yet evil-looking. "
|
||||
desc += "Its glass cover is closed, protecting it."
|
||||
self.db.desc = desc
|
||||
# re-activate the blink button event.
|
||||
self.obj.scripts.add(BlinkButtonEvent)
|
||||
# unlock the lid
|
||||
self.obj.db.lid_locked = False
|
||||
self.obj.scripts.validate()
|
||||
110
evennia/contrib/tutorial_world/README
Normal file
110
evennia/contrib/tutorial_world/README
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
|
||||
===============================================================
|
||||
Evennia Tutorial World
|
||||
|
||||
Griatch 2011
|
||||
===============================================================
|
||||
|
||||
This is a stand-alone tutorial area for an unmodified Evennia install.
|
||||
Think of it as a sort of single-player adventure rather than a
|
||||
full-fledged multi-player game world. The various rooms and objects
|
||||
herein are designed to show off features of the engine, not to be a
|
||||
very challenging (nor long) gaming experience. As such it's of course
|
||||
only skimming the surface of what is possible.
|
||||
|
||||
================================================================
|
||||
Install
|
||||
================================================================
|
||||
|
||||
Log in as superuser (#1), then run
|
||||
|
||||
@batchcommand contrib.tutorial_world.build
|
||||
|
||||
Wait a little while for building to complete. This should build the
|
||||
world and connect it to Limbo.
|
||||
|
||||
Log is as a non-superuser to play the game as intended. The
|
||||
tutorial area's systems mostly ignores the presence of a
|
||||
superuser (so use that to examine things "under the hood" later).
|
||||
|
||||
================================================================
|
||||
Comments
|
||||
================================================================
|
||||
|
||||
The tutorial world is intended for you playing around with the
|
||||
engine. It will help you learn how to accomplish some more advanced
|
||||
effects and might give some good ideas along the way.
|
||||
|
||||
It's suggested you play it through (as a normal user, NOT as
|
||||
Superuser!) and explore it a bit, then come back here and start
|
||||
looking into the (heavily documented) build/source code to find out
|
||||
how things tick - that's the "tutorial" in Tutorial world after all.
|
||||
|
||||
Please report bugs in the tutorial to the Evennia issue tracker.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
* Spoilers below - don't read on unless you already played the
|
||||
tutorial game. *
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
===============================================================
|
||||
Tutorial World Room map
|
||||
===============================================================
|
||||
|
||||
?
|
||||
|
|
||||
+---+----+ +-------------------+ +--------+ +--------+
|
||||
| | | | |gate | |corner |
|
||||
| cliff +----+ bridge +----+ +---+ |
|
||||
| | | | | | | |
|
||||
+---+---\+ +---------------+---+ +---+----+ +---+----+
|
||||
| \ | | castle |
|
||||
| \ +--------+ +----+---+ +---+----+ +---+----+
|
||||
| \ |under- | |ledge | |along | |court- |
|
||||
| \|ground +--+ | |wall +---+yard |
|
||||
| \ | | | | | | |
|
||||
| +------\-+ +--------+ +--------+ +---+----+
|
||||
| \ |
|
||||
++---------+ \ +--------+ +--------+ +---+----+
|
||||
|intro | \ |cell | |trap | |temple |
|
||||
o--+ | \| +----+ | | |
|
||||
L | | \ | /| | | |
|
||||
I +----+-----+ +--------+ / ---+-+-+-+ +---+----+
|
||||
M | / | | | |
|
||||
B +----+-----+ +--------+/ +--+-+-+---------+----+
|
||||
O |outro | |tomb | |antechamber |
|
||||
o--+ +----------+ | | |
|
||||
| | | | | |
|
||||
+----------+ +--------+ +---------------------+
|
||||
|
||||
Hints/Notes:
|
||||
|
||||
o-- connections to/from Limbo
|
||||
intro/outro areas are rooms that automatically sets/cleans the
|
||||
Character of any settings assigned to it during the
|
||||
tutorial game.
|
||||
The Cliff is a good place to get an overview of the surroundings.
|
||||
The Bridge may seem like a big room, but it is really only one
|
||||
room with custom move commands to make it take longer
|
||||
to cross. You can also fall off the bridge if you
|
||||
are unlucky or take your time to take in the view too
|
||||
long.
|
||||
In the Castle areas an aggressive mob is patrolling. It implements
|
||||
rudimentary AI but packs quite a punch unless you have
|
||||
found yourself a weapon that can harm it. Combat is only
|
||||
possible once you find a weapon.
|
||||
The Catacombs feature a puzzle for finding the correct Grave
|
||||
chamber.
|
||||
The Cell is your reward if you fail in various ways. Finding a
|
||||
way out of it is a small puzzle of its own.
|
||||
The Tomb is a nice place to find a weapon that can hurt the
|
||||
castle guardian. This is the goal of the tutorial.
|
||||
Explore on, or take the exit to finish the tutorial.
|
||||
? - look into the code if you cannot find this bonus area!
|
||||
1
evennia/contrib/tutorial_world/__init__.py
Normal file
1
evennia/contrib/tutorial_world/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
1229
evennia/contrib/tutorial_world/build.ev
Normal file
1229
evennia/contrib/tutorial_world/build.ev
Normal file
File diff suppressed because it is too large
Load diff
384
evennia/contrib/tutorial_world/mob.py
Normal file
384
evennia/contrib/tutorial_world/mob.py
Normal file
|
|
@ -0,0 +1,384 @@
|
|||
"""
|
||||
This module implements a simple mobile object with
|
||||
a very rudimentary AI as well as an aggressive enemy
|
||||
object based on that mobile class.
|
||||
|
||||
"""
|
||||
|
||||
import random, time
|
||||
from django.conf import settings
|
||||
|
||||
from evennia import search_object, utils, Script
|
||||
from contrib.tutorial_world import objects as tut_objects
|
||||
from contrib.tutorial_world import scripts as tut_scripts
|
||||
|
||||
BASE_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Mob - mobile object
|
||||
#
|
||||
# This object utilizes exits and moves about randomly from
|
||||
# room to room.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class Mob(tut_objects.TutorialObject):
|
||||
"""
|
||||
This type of mobile will roam from exit to exit at
|
||||
random intervals. Simply lock exits against the is_mob attribute
|
||||
to block them from the mob (lockstring = "traverse:not attr(is_mob)").
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"This is called when the object is first created."
|
||||
self.db.tutorial_info = "This is a moving object. It moves randomly from room to room."
|
||||
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
# this is a good attribute for exits to look for, to block
|
||||
# a mob from entering certain exits.
|
||||
self.db.is_mob = True
|
||||
self.db.last_location = None
|
||||
# only when True will the mob move.
|
||||
self.db.roam_mode = True
|
||||
|
||||
def announce_move_from(self, destination):
|
||||
"Called just before moving"
|
||||
self.location.msg_contents("With a cold breeze, %s drifts in the direction of %s." % (self.key, destination.key))
|
||||
|
||||
def announce_move_to(self, source_location):
|
||||
"Called just after arriving"
|
||||
self.location.msg_contents("With a wailing sound, %s appears from the %s." % (self.key, source_location.key))
|
||||
|
||||
def update_irregular(self):
|
||||
"Called at irregular intervals. Moves the mob."
|
||||
if self.roam_mode:
|
||||
exits = [ex for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
if exits:
|
||||
# Try to make it so the mob doesn't backtrack.
|
||||
new_exits = [ex for ex in exits
|
||||
if ex.destination != self.db.last_location]
|
||||
if new_exits:
|
||||
exits = new_exits
|
||||
self.db.last_location = self.location
|
||||
# execute_cmd() allows the mob to respect exit and
|
||||
# exit-command locks, but may pose a problem if there is more
|
||||
# than one exit with the same name.
|
||||
# - see Enemy example for another way to move
|
||||
self.execute_cmd("%s" % exits[random.randint(0, len(exits) - 1)].key)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Enemy - mobile attacking object
|
||||
#
|
||||
# An enemy is a mobile that is aggressive against players
|
||||
# in its vicinity. An enemy will try to attack characters
|
||||
# in the same location. It will also pursue enemies through
|
||||
# exits if possible.
|
||||
#
|
||||
# An enemy needs to have a Weapon object in order to
|
||||
# attack.
|
||||
#
|
||||
# This particular tutorial enemy is a ghostly apparition that can only
|
||||
# be hurt by magical weapons. It will also not truly "die", but only
|
||||
# teleport to another room. Players defeated by the apparition will
|
||||
# conversely just be teleported to a holding room.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class AttackTimer(Script):
|
||||
"""
|
||||
This script is what makes an eneny "tick".
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This sets up the script"
|
||||
self.key = "AttackTimer"
|
||||
self.desc = "Drives an Enemy's combat."
|
||||
self.interval = random.randint(2, 3) # how fast the Enemy acts
|
||||
self.start_delay = True # wait self.interval before first call
|
||||
self.persistent = True
|
||||
|
||||
def at_repeat(self):
|
||||
"Called every self.interval seconds."
|
||||
if self.obj.db.inactive:
|
||||
return
|
||||
# id(self.ndb.twisted_task)
|
||||
if self.obj.db.roam_mode:
|
||||
self.obj.roam()
|
||||
#return
|
||||
elif self.obj.db.battle_mode:
|
||||
#print "attack"
|
||||
self.obj.attack()
|
||||
return
|
||||
elif self.obj.db.pursue_mode:
|
||||
#print "pursue"
|
||||
self.obj.pursue()
|
||||
#return
|
||||
else:
|
||||
#dead mode. Wait for respawn.
|
||||
if not self.obj.db.dead_at:
|
||||
self.obj.db.dead_at = time.time()
|
||||
if (time.time() - self.obj.db.dead_at) > self.obj.db.dead_timer:
|
||||
self.obj.reset()
|
||||
|
||||
|
||||
class Enemy(Mob):
|
||||
"""
|
||||
This is a ghostly enemy with health (hit points). Their chance to hit,
|
||||
damage etc is determined by the weapon they are wielding, same as
|
||||
characters.
|
||||
|
||||
An enemy can be in four modes:
|
||||
roam (inherited from Mob) - where it just moves around randomly
|
||||
battle - where it stands in one place and attacks players
|
||||
pursue - where it follows a player, trying to enter combat again
|
||||
dead - passive and invisible until it is respawned
|
||||
|
||||
Upon creation, the following attributes describe the enemy's actions
|
||||
desc - description
|
||||
full_health - integer number > 0
|
||||
defeat_location - unique name or #dbref to the location the player is
|
||||
taken when defeated. If not given, will remain in room.
|
||||
defeat_text - text to show player when they are defeated (just before
|
||||
being whisped away to defeat_location)
|
||||
defeat_text_room - text to show other players in room when a player
|
||||
is defeated
|
||||
win_text - text to show player when defeating the enemy
|
||||
win_text_room - text to show room when a player defeates the enemy
|
||||
respawn_text - text to echo to room when the mob is reset/respawn in
|
||||
that room.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at object creation."
|
||||
super(Enemy, self).at_object_creation()
|
||||
|
||||
self.db.tutorial_info = "This moving object will attack players in the same room."
|
||||
|
||||
# state machine modes
|
||||
self.db.roam_mode = True
|
||||
self.db.battle_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.dead_mode = False
|
||||
# health (change this at creation time)
|
||||
self.db.full_health = 20
|
||||
self.db.health = 20
|
||||
self.db.dead_at = time.time()
|
||||
self.db.dead_timer = 100 # how long to stay dead
|
||||
# this is used during creation to make sure the mob doesn't move away
|
||||
self.db.inactive = True
|
||||
# store the last player to hit
|
||||
self.db.last_attacker = None
|
||||
# where to take defeated enemies
|
||||
self.db.defeat_location = "darkcell"
|
||||
self.scripts.add(AttackTimer)
|
||||
|
||||
def update_irregular(self):
|
||||
"the irregular event is inherited from Mob class"
|
||||
strings = self.db.irregular_echoes
|
||||
if strings:
|
||||
self.location.msg_contents(strings[random.randint(0, len(strings) - 1)])
|
||||
|
||||
def roam(self):
|
||||
"Called by Attack timer. Will move randomly as long as exits are open."
|
||||
|
||||
# in this mode, the mob is healed.
|
||||
self.db.health = self.db.full_health
|
||||
players = [obj for obj in self.location.contents
|
||||
if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
# we found players in the room. Attack.
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = True
|
||||
|
||||
elif random.random() < 0.2:
|
||||
# no players to attack, move about randomly.
|
||||
exits = [ex.destination for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
if exits:
|
||||
# Try to make it so the mob doesn't backtrack.
|
||||
new_exits = [ex for ex in exits
|
||||
if ex.destination != self.db.last_location]
|
||||
if new_exits:
|
||||
exits = new_exits
|
||||
self.db.last_location = self.location
|
||||
# locks should be checked here
|
||||
self.move_to(exits[random.randint(0, len(exits) - 1)])
|
||||
else:
|
||||
# no exits - a dead end room. Respawn back to start.
|
||||
self.move_to(self.home)
|
||||
|
||||
def attack(self):
|
||||
"""
|
||||
This is the main mode of combat. It will try to hit players in
|
||||
the location. If players are defeated, it will whisp them off
|
||||
to the defeat location.
|
||||
"""
|
||||
last_attacker = self.db.last_attacker
|
||||
players = [obj for obj in self.location.contents
|
||||
if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
|
||||
# find a target
|
||||
if last_attacker in players:
|
||||
# prefer to attack the player last attacking.
|
||||
target = last_attacker
|
||||
else:
|
||||
# otherwise attack a random player in location
|
||||
target = players[random.randint(0, len(players) - 1)]
|
||||
|
||||
# try to use the weapon in hand
|
||||
attack_cmds = ("thrust", "pierce", "stab", "slash", "chop")
|
||||
cmd = attack_cmds[random.randint(0, len(attack_cmds) - 1)]
|
||||
self.execute_cmd("%s %s" % (cmd, target))
|
||||
|
||||
# analyze result.
|
||||
if target.db.health <= 0:
|
||||
# we reduced enemy to 0 health. Whisp them off to
|
||||
# the prison room.
|
||||
tloc = search_object(self.db.defeat_location)
|
||||
tstring = self.db.defeat_text
|
||||
if not tstring:
|
||||
tstring = "You feel your conciousness slip away ... you fall to the ground as "
|
||||
tstring += "the misty apparition envelopes you ...\n The world goes black ...\n"
|
||||
target.msg(tstring)
|
||||
ostring = self.db.defeat_text_room
|
||||
if tloc:
|
||||
if not ostring:
|
||||
ostring = "\n%s envelops the fallen ... and then their body is suddenly gone!" % self.key
|
||||
# silently move the player to defeat location
|
||||
# (we need to call hook manually)
|
||||
target.location = tloc[0]
|
||||
tloc[0].at_object_receive(target, self.location)
|
||||
elif not ostring:
|
||||
ostring = "%s falls to the ground!" % target.key
|
||||
self.location.msg_contents(ostring, exclude=[target])
|
||||
# Pursue any stragglers after the battle
|
||||
self.battle_mode = False
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = True
|
||||
else:
|
||||
# no players found, this could mean they have fled.
|
||||
# Switch to pursue mode.
|
||||
self.battle_mode = False
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = True
|
||||
|
||||
def pursue(self):
|
||||
"""
|
||||
In pursue mode, the enemy tries to find players in adjoining rooms, preferably
|
||||
those that previously attacked it.
|
||||
"""
|
||||
last_attacker = self.db.last_attacker
|
||||
players = [obj for obj in self.location.contents if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser]
|
||||
if players:
|
||||
# we found players in the room. Maybe we caught up with some,
|
||||
# or some walked in on us before we had time to pursue them.
|
||||
# Switch to battle mode.
|
||||
self.battle_mode = True
|
||||
self.roam_mode = False
|
||||
self.pursue_mode = False
|
||||
else:
|
||||
# find all possible destinations.
|
||||
destinations = [ex.destination for ex in self.location.exits
|
||||
if ex.access(self, "traverse")]
|
||||
# find all players in the possible destinations. OBS-we cannot
|
||||
# just use the player's current position to move the Enemy; this
|
||||
# might have changed when the move is performed, causing the enemy
|
||||
# to teleport out of bounds.
|
||||
players = {}
|
||||
for dest in destinations:
|
||||
for obj in [o for o in dest.contents
|
||||
if utils.inherits_from(o, BASE_CHARACTER_TYPECLASS)]:
|
||||
players[obj] = dest
|
||||
if players:
|
||||
# we found targets. Move to intercept.
|
||||
if last_attacker in players:
|
||||
# preferably the one that last attacked us
|
||||
self.move_to(players[last_attacker])
|
||||
else:
|
||||
# otherwise randomly.
|
||||
key = players.keys()[random.randint(0, len(players) - 1)]
|
||||
self.move_to(players[key])
|
||||
else:
|
||||
# we found no players nearby. Return to roam mode.
|
||||
self.battle_mode = False
|
||||
self.roam_mode = True
|
||||
self.pursue_mode = False
|
||||
|
||||
def at_hit(self, weapon, attacker, damage):
|
||||
"""
|
||||
Called when this object is hit by an enemy's weapon
|
||||
Should return True if enemy is defeated, False otherwise.
|
||||
|
||||
In the case of players attacking, we handle all the events
|
||||
and information from here, so the return value is not used.
|
||||
"""
|
||||
|
||||
self.db.last_attacker = attacker
|
||||
if not self.db.battle_mode:
|
||||
# we were attacked, so switch to battle mode.
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = True
|
||||
#self.scripts.add(AttackTimer)
|
||||
|
||||
if not weapon.db.magic:
|
||||
# In the tutorial, the enemy is a ghostly apparition, so
|
||||
# only magical weapons can harm it.
|
||||
string = self.db.weapon_ineffective_text
|
||||
if not string:
|
||||
string = "Your weapon just passes through your enemy, causing no effect!"
|
||||
attacker.msg(string)
|
||||
return
|
||||
else:
|
||||
# an actual hit
|
||||
health = float(self.db.health)
|
||||
health -= damage
|
||||
self.db.health = health
|
||||
if health <= 0:
|
||||
string = self.db.win_text
|
||||
if not string:
|
||||
string = "After your last hit, %s folds in on itself, it seems to fade away into nothingness. " % self.key
|
||||
string += "In a moment there is nothing left but the echoes of its screams. But you have a "
|
||||
string += "feeling it is only temporarily weakened. "
|
||||
string += "You fear it's only a matter of time before it materializes somewhere again."
|
||||
attacker.msg(string)
|
||||
string = self.db.win_text_room
|
||||
if not string:
|
||||
string = "After %s's last hit, %s folds in on itself, it seems to fade away into nothingness. " % (attacker.name, self.key)
|
||||
string += "In a moment there is nothing left but the echoes of its screams. But you have a "
|
||||
string += "feeling it is only temporarily weakened. "
|
||||
string += "You fear it's only a matter of time before it materializes somewhere again."
|
||||
self.location.msg_contents(string, exclude=[attacker])
|
||||
|
||||
# put mob in dead mode and hide it from view.
|
||||
# AttackTimer will bring it back later.
|
||||
self.db.dead_at = time.time()
|
||||
self.db.roam_mode = False
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = False
|
||||
self.db.dead_mode = True
|
||||
self.location = None
|
||||
else:
|
||||
self.location.msg_contents("%s wails, shudders and writhes." % self.key)
|
||||
return False
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
If the mob was 'dead', respawn it to its home position and reset
|
||||
all modes and damage."""
|
||||
if self.db.dead_mode:
|
||||
self.db.health = self.db.full_health
|
||||
self.db.roam_mode = True
|
||||
self.db.pursue_mode = False
|
||||
self.db.battle_mode = False
|
||||
self.db.dead_mode = False
|
||||
self.location = self.home
|
||||
string = self.db.respawn_text
|
||||
if not string:
|
||||
string = "%s fades into existence from out of thin air. It's looking pissed." % self.key
|
||||
self.location.msg_contents(string)
|
||||
951
evennia/contrib/tutorial_world/objects.py
Normal file
951
evennia/contrib/tutorial_world/objects.py
Normal file
|
|
@ -0,0 +1,951 @@
|
|||
"""
|
||||
TutorialWorld - basic objects - Griatch 2011
|
||||
|
||||
This module holds all "dead" object definitions for
|
||||
the tutorial world. Object-commands and -cmdsets
|
||||
are also defined here, together with the object.
|
||||
|
||||
Objects:
|
||||
|
||||
TutorialObject
|
||||
|
||||
Readable
|
||||
Climbable
|
||||
Obelisk
|
||||
LightSource
|
||||
CrumblingWall
|
||||
Weapon
|
||||
WeaponRack
|
||||
|
||||
"""
|
||||
|
||||
import time
|
||||
import random
|
||||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet, Script
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# TutorialObject
|
||||
#
|
||||
# The TutorialObject is the base class for all items
|
||||
# in the tutorial. They have an attribute "tutorial_info"
|
||||
# on them that a global tutorial command can use to extract
|
||||
# interesting behind-the scenes information about the object.
|
||||
#
|
||||
# TutorialObjects may also be "reset". What the reset means
|
||||
# is up to the object. It can be the resetting of the world
|
||||
# itself, or the removal of an inventory item from a
|
||||
# character's inventory when leaving the tutorial, for example.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class TutorialObject(DefaultObject):
|
||||
"""
|
||||
This is the baseclass for all objects in the tutorial.
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called when the object is first created."
|
||||
super(TutorialObject, self).at_object_creation()
|
||||
self.db.tutorial_info = "No tutorial info is available for this object."
|
||||
#self.db.last_reset = time.time()
|
||||
|
||||
def reset(self):
|
||||
"Resets the object, whatever that may mean."
|
||||
self.location = self.home
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Readable - an object one can "read".
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdRead(Command):
|
||||
"""
|
||||
Usage:
|
||||
read [obj]
|
||||
|
||||
Read some text.
|
||||
"""
|
||||
|
||||
key = "read"
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the read command."
|
||||
if self.args:
|
||||
obj = self.caller.search(self.args.strip())
|
||||
else:
|
||||
obj = self.obj
|
||||
if not obj:
|
||||
return
|
||||
# we want an attribute read_text to be defined.
|
||||
readtext = obj.db.readable_text
|
||||
if readtext:
|
||||
string = "You read {C%s{n:\n %s" % (obj.key, readtext)
|
||||
else:
|
||||
string = "There is nothing to read on %s." % obj.key
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdSetReadable(CmdSet):
|
||||
"CmdSet for readables"
|
||||
def at_cmdset_creation(self):
|
||||
"called when object is created."
|
||||
self.add(CmdRead())
|
||||
|
||||
|
||||
class Readable(TutorialObject):
|
||||
"""
|
||||
This object defines some attributes and defines a read method on itself.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is created"
|
||||
super(Readable, self).at_object_creation()
|
||||
self.db.tutorial_info = "This is an object with a 'read' command defined in a command set on itself."
|
||||
self.db.readable_text = "There is no text written on %s." % self.key
|
||||
# define a command on the object.
|
||||
self.cmdset.add_default(CmdSetReadable, permanent=True)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Climbable object
|
||||
#
|
||||
# The climbable object works so that once climbed, it sets
|
||||
# a flag on the climber to show that it was climbed. A simple
|
||||
# command 'climb' handles the actual climbing.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdClimb(Command):
|
||||
"""
|
||||
Usage:
|
||||
climb <object>
|
||||
"""
|
||||
key = "climb"
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements function"
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("What do you want to climb?")
|
||||
return
|
||||
obj = self.caller.search(self.args.strip())
|
||||
if not obj:
|
||||
return
|
||||
if obj != self.obj:
|
||||
self.caller.msg("Try as you might, you cannot climb that.")
|
||||
return
|
||||
ostring = self.obj.db.climb_text
|
||||
if not ostring:
|
||||
ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name
|
||||
self.caller.msg(ostring)
|
||||
self.caller.db.last_climbed = self.obj
|
||||
|
||||
|
||||
class CmdSetClimbable(CmdSet):
|
||||
"Climbing cmdset"
|
||||
def at_cmdset_creation(self):
|
||||
"populate set"
|
||||
self.add(CmdClimb())
|
||||
|
||||
|
||||
class Climbable(TutorialObject):
|
||||
"A climbable object."
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called at initial creation only"
|
||||
self.cmdset.add_default(CmdSetClimbable, permanent=True)
|
||||
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Obelisk - a unique item
|
||||
#
|
||||
# The Obelisk is an object with a modified return_appearance
|
||||
# method that causes it to look slightly different every
|
||||
# time one looks at it. Since what you actually see
|
||||
# is a part of a game puzzle, the act of looking also
|
||||
# stores a key attribute on the looking object for later
|
||||
# reference.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
OBELISK_DESCS = ["You can briefly make out the image of {ba woman with a blue bird{n.",
|
||||
"You for a moment see the visage of {ba woman on a horse{n.",
|
||||
"For the briefest moment you make out an engraving of {ba regal woman wearing a crown{n.",
|
||||
"You think you can see the outline of {ba flaming shield{n in the stone.",
|
||||
"The surface for a moment seems to portray {ba woman fighting a beast{n."]
|
||||
|
||||
|
||||
class Obelisk(TutorialObject):
|
||||
"""
|
||||
This object changes its description randomly.
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called when object is created."
|
||||
super(Obelisk, self).at_object_creation()
|
||||
self.db.tutorial_info = "This object changes its desc randomly, and makes sure to remember which one you saw."
|
||||
# make sure this can never be picked up
|
||||
self.locks.add("get:false()")
|
||||
|
||||
def return_appearance(self, caller):
|
||||
"Overload the default version of this hook."
|
||||
clueindex = random.randint(0, len(OBELISK_DESCS) - 1)
|
||||
# set this description
|
||||
string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with "
|
||||
string += "different scenes and structures appearing whenever you look at it. "
|
||||
self.db.desc = string + OBELISK_DESCS[clueindex]
|
||||
# remember that this was the clue we got.
|
||||
caller.db.puzzle_clue = clueindex
|
||||
# call the parent function as normal (this will use db.desc we just set)
|
||||
return super(Obelisk, self).return_appearance(caller)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# LightSource
|
||||
#
|
||||
# This object that emits light and can be
|
||||
# turned on or off. It must be carried to use and has only
|
||||
# a limited burn-time.
|
||||
# When burned out, it will remove itself from the carrying
|
||||
# character's inventory.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class StateLightSourceOn(Script):
|
||||
"""
|
||||
This script controls how long the light source is burning. When
|
||||
it runs out of fuel, the lightsource goes out.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called at creation of script."
|
||||
self.key = "lightsourceBurn"
|
||||
self.desc = "Keeps lightsources burning."
|
||||
self.start_delay = True # only fire after self.interval s.
|
||||
self.repeats = 1 # only run once.
|
||||
self.persistent = True # survive a server reboot.
|
||||
|
||||
def at_start(self):
|
||||
"Called at script start - this can also happen if server is restarted."
|
||||
self.interval = self.obj.db.burntime
|
||||
self.db.script_started = time.time()
|
||||
|
||||
def at_repeat(self):
|
||||
"Called at self.interval seconds"
|
||||
# this is only called when torch has burnt out
|
||||
self.obj.db.burntime = -1
|
||||
self.obj.reset()
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
Since the user may also turn off the light
|
||||
prematurely, this hook will store the current
|
||||
burntime.
|
||||
"""
|
||||
# calculate remaining burntime, if object is not
|
||||
# already deleted (because it burned out)
|
||||
if self.obj:
|
||||
try:
|
||||
time_burnt = time.time() - self.db.script_started
|
||||
except TypeError:
|
||||
# can happen if script_started is not defined
|
||||
time_burnt = self.interval
|
||||
burntime = self.interval - time_burnt
|
||||
self.obj.db.burntime = burntime
|
||||
|
||||
def is_valid(self):
|
||||
"This script is only valid as long as the lightsource burns."
|
||||
return self.obj.db.is_active
|
||||
|
||||
|
||||
class CmdLightSourceOn(Command):
|
||||
"""
|
||||
Switches on the lightsource.
|
||||
"""
|
||||
key = "on"
|
||||
aliases = ["switch on", "turn on", "light"]
|
||||
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
|
||||
if self.obj.db.is_active:
|
||||
self.caller.msg("%s is already burning." % self.obj.key)
|
||||
else:
|
||||
# set lightsource to active
|
||||
self.obj.db.is_active = True
|
||||
# activate the script to track burn-time.
|
||||
self.obj.scripts.add(StateLightSourceOn)
|
||||
self.caller.msg("{gYou light {C%s.{n" % self.obj.key)
|
||||
self.caller.location.msg_contents("%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller])
|
||||
# run script validation on the room to make light/dark states tick.
|
||||
self.caller.location.scripts.validate()
|
||||
# look around
|
||||
self.caller.execute_cmd("look")
|
||||
|
||||
|
||||
class CmdLightSourceOff(Command):
|
||||
"""
|
||||
Switch off the lightsource.
|
||||
"""
|
||||
key = "off"
|
||||
aliases = ["switch off", "turn off", "dowse"]
|
||||
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the command "
|
||||
|
||||
if not self.obj.db.is_active:
|
||||
self.caller.msg("%s is not burning." % self.obj.key)
|
||||
else:
|
||||
# set lightsource to inactive
|
||||
self.obj.db.is_active = False
|
||||
# validating the scripts will kill it now that is_active=False.
|
||||
self.obj.scripts.validate()
|
||||
self.caller.msg("{GYou dowse {C%s.{n" % self.obj.key)
|
||||
self.caller.location.msg_contents("%s dowses %s." % (self.caller, self.obj.key), exclude=[self.caller])
|
||||
self.caller.location.scripts.validate()
|
||||
self.caller.execute_cmd("look")
|
||||
|
||||
|
||||
class CmdSetLightSource(CmdSet):
|
||||
"CmdSet for the lightsource commands"
|
||||
key = "lightsource_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"called at cmdset creation"
|
||||
self.add(CmdLightSourceOn())
|
||||
self.add(CmdLightSourceOff())
|
||||
|
||||
|
||||
class LightSource(TutorialObject):
|
||||
"""
|
||||
This implements a light source object.
|
||||
|
||||
When burned out, lightsource will be moved to its home - which by
|
||||
default is the location it was first created at.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created."
|
||||
super(LightSource, self).at_object_creation()
|
||||
self.db.tutorial_info = "This object can be turned on off and has a timed script controlling it."
|
||||
self.db.is_active = False
|
||||
self.db.burntime = 60 * 3 # 3 minutes
|
||||
self.db.desc = "A splinter of wood with remnants of resin on it, enough for burning."
|
||||
# add commands
|
||||
self.cmdset.add_default(CmdSetLightSource, permanent=True)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Can be called by tutorial world runner, or by the script when
|
||||
the lightsource has burned out.
|
||||
"""
|
||||
if self.db.burntime <= 0:
|
||||
# light burned out. Since the lightsources's "location" should be
|
||||
# a character, notify them this way.
|
||||
try:
|
||||
loc = self.location.location
|
||||
except AttributeError:
|
||||
loc = self.location
|
||||
loc.msg_contents("{c%s{n {Rburns out.{n" % self.key)
|
||||
self.db.is_active = False
|
||||
try:
|
||||
# validate in holders current room, if possible
|
||||
self.location.location.scripts.validate()
|
||||
except AttributeError:
|
||||
# maybe it was dropped, try validating at current location.
|
||||
try:
|
||||
self.location.scripts.validate()
|
||||
except AttributeError:
|
||||
pass
|
||||
self.delete()
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Crumbling wall - unique exit
|
||||
#
|
||||
# This implements a simple puzzle exit that needs to be
|
||||
# accessed with commands before one can get to traverse it.
|
||||
#
|
||||
# The puzzle is currently simply to move roots (that have
|
||||
# presumably covered the wall) aside until a button for a
|
||||
# secret door is revealed. The original position of the
|
||||
# roots blocks the button, so they have to be moved to a certain
|
||||
# position - when they have, the "press button" command
|
||||
# is made available and the Exit is made traversable.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# There are four roots - two horizontal and two vertically
|
||||
# running roots. Each can have three positions: top/middle/bottom
|
||||
# and left/middle/right respectively. There can be any number of
|
||||
# roots hanging through the middle position, but only one each
|
||||
# along the sides. The goal is to make the center position clear.
|
||||
# (yes, it's really as simple as it sounds, just move the roots
|
||||
# to each side to "win". This is just a tutorial, remember?)
|
||||
|
||||
class CmdShiftRoot(Command):
|
||||
"""
|
||||
Shifts roots around.
|
||||
|
||||
shift blue root left/right
|
||||
shift red root left/right
|
||||
shift yellow root up/down
|
||||
shift green root up/down
|
||||
|
||||
"""
|
||||
key = "shift"
|
||||
aliases = ["move"]
|
||||
# the locattr() lock looks for the attribute is_dark on the current room.
|
||||
locks = "cmd:not locattr(is_dark)"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def parse(self):
|
||||
"custom parser; split input by spaces"
|
||||
self.arglist = self.args.strip().split()
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
Implement the command.
|
||||
blue/red - vertical roots
|
||||
yellow/green - horizontal roots
|
||||
"""
|
||||
|
||||
if not self.arglist:
|
||||
self.caller.msg("What do you want to move, and in what direction?")
|
||||
return
|
||||
if "root" in self.arglist:
|
||||
self.arglist.remove("root")
|
||||
# we accept arguments on the form <color> <direction>
|
||||
if not len(self.arglist) > 1:
|
||||
self.caller.msg("You must define which colour of root you want to move, and in which direction.")
|
||||
return
|
||||
color = self.arglist[0].lower()
|
||||
direction = self.arglist[1].lower()
|
||||
# get current root positions dict
|
||||
root_pos = self.obj.db.root_pos
|
||||
|
||||
if not color in root_pos:
|
||||
self.caller.msg("No such root to move.")
|
||||
return
|
||||
|
||||
# first, vertical roots (red/blue) - can be moved left/right
|
||||
if color == "red":
|
||||
if direction == "left":
|
||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||
self.caller.msg("You shift the reddish root to the left.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]:
|
||||
root_pos["blue"] += 1
|
||||
self.caller.msg("The root with blue flowers gets in the way and is pushed to the right.")
|
||||
elif direction == "right":
|
||||
root_pos[color] = min(1, root_pos[color] + 1)
|
||||
self.caller.msg("You shove the reddish root to the right.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]:
|
||||
root_pos["blue"] -= 1
|
||||
self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
elif color == "blue":
|
||||
if direction == "left":
|
||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||
self.caller.msg("You shift the root with small blue flowers to the left.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
|
||||
root_pos["red"] += 1
|
||||
self.caller.msg("The reddish root is to big to fit as well, so that one falls away to the left.")
|
||||
elif direction == "right":
|
||||
root_pos[color] = min(1, root_pos[color] + 1)
|
||||
self.caller.msg("You shove the root adorned with small blue flowers to the right.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
|
||||
root_pos["red"] -= 1
|
||||
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
# now the horizontal roots (yellow/green). They can be moved up/down
|
||||
elif color == "yellow":
|
||||
if direction == "up":
|
||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||
self.caller.msg("You shift the root with small yellow flowers upwards.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["green"]:
|
||||
root_pos["green"] += 1
|
||||
self.caller.msg("The green weedy root falls down.")
|
||||
elif direction == "down":
|
||||
root_pos[color] = min(1, root_pos[color] + 1)
|
||||
self.caller.msg("You shove the root adorned with small yellow flowers downwards.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["green"]:
|
||||
root_pos["green"] -= 1
|
||||
self.caller.msg("The weedy green root is shifted upwards to make room.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
elif color == "green":
|
||||
if direction == "up":
|
||||
root_pos[color] = max(-1, root_pos[color] - 1)
|
||||
self.caller.msg("You shift the weedy green root upwards.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]:
|
||||
root_pos["yellow"] += 1
|
||||
self.caller.msg("The root with yellow flowers falls down.")
|
||||
elif direction == "down":
|
||||
root_pos[color] = min(1, root_pos[color] + 1)
|
||||
self.caller.msg("You shove the weedy green root downwards.")
|
||||
if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]:
|
||||
root_pos["yellow"] -= 1
|
||||
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
# store new position
|
||||
self.obj.db.root_pos = root_pos
|
||||
# check victory condition
|
||||
if root_pos.values().count(0) == 0: # no roots in middle position
|
||||
self.caller.db.crumbling_wall_found_button = True
|
||||
self.caller.msg("Holding aside the root you think you notice something behind it ...")
|
||||
|
||||
|
||||
class CmdPressButton(Command):
|
||||
"""
|
||||
Presses a button.
|
||||
"""
|
||||
key = "press"
|
||||
aliases = ["press button", "button", "push", "push button"]
|
||||
# only accessible if the button was found and there is light.
|
||||
locks = "cmd:attr(crumbling_wall_found_button) and not locattr(is_dark)"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
|
||||
if self.caller.db.crumbling_wall_found_exit:
|
||||
# we already pushed the button
|
||||
self.caller.msg("The button folded away when the secret passage opened. You cannot push it again.")
|
||||
return
|
||||
|
||||
# pushing the button
|
||||
string = "You move your fingers over the suspicious depression, then gives it a "
|
||||
string += "decisive push. First nothing happens, then there is a rumble and a hidden "
|
||||
string += "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside."
|
||||
|
||||
# we are done - this will make the exit traversable!
|
||||
self.caller.db.crumbling_wall_found_exit = True
|
||||
# this will make it into a proper exit
|
||||
eloc = self.caller.search(self.obj.db.destination, global_search=True)
|
||||
if not eloc:
|
||||
self.caller.msg("The exit leads nowhere, there's just more stone behind it ...")
|
||||
return
|
||||
self.obj.destination = eloc
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class CmdSetCrumblingWall(CmdSet):
|
||||
"Group the commands for crumblingWall"
|
||||
key = "crumblingwall_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"called when object is first created."
|
||||
self.add(CmdShiftRoot())
|
||||
self.add(CmdPressButton())
|
||||
|
||||
|
||||
class CrumblingWall(TutorialObject, DefaultExit):
|
||||
"""
|
||||
The CrumblingWall can be examined in various
|
||||
ways, but only if a lit light source is in the room. The traversal
|
||||
itself is blocked by a traverse: lock on the exit that only
|
||||
allows passage if a certain attribute is set on the trying
|
||||
player.
|
||||
|
||||
Important attribute
|
||||
destination - this property must be set to make this a valid exit
|
||||
whenever the button is pushed (this hides it as an exit
|
||||
until it actually is)
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"called when the object is first created."
|
||||
super(CrumblingWall, self).at_object_creation()
|
||||
|
||||
self.aliases.add(["secret passage", "passage",
|
||||
"crack", "opening", "secret door"])
|
||||
# this is assigned first when pushing button, so assign
|
||||
# this at creation time!
|
||||
|
||||
self.db.destination = 2
|
||||
# locks on the object directly transfer to the exit "command"
|
||||
self.locks.add("cmd:not locattr(is_dark)")
|
||||
|
||||
self.db.tutorial_info = "This is an Exit with a conditional traverse-lock. Try to shift the roots around."
|
||||
# the lock is important for this exit; we only allow passage
|
||||
# if we "found exit".
|
||||
self.locks.add("traverse:attr(crumbling_wall_found_exit)")
|
||||
# set cmdset
|
||||
self.cmdset.add(CmdSetCrumblingWall, permanent=True)
|
||||
|
||||
# starting root positions. H1/H2 are the horizontally hanging roots,
|
||||
# V1/V2 the vertically hanging ones. Each can have three positions:
|
||||
# (-1, 0, 1) where 0 means the middle position. yellow/green are
|
||||
# horizontal roots and red/blue vertical, all may have value 0, but n
|
||||
# ever any other identical value.
|
||||
self.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0}
|
||||
|
||||
def _translate_position(self, root, ipos):
|
||||
"Translates the position into words"
|
||||
rootnames = {"red": "The {rreddish{n vertical-hanging root ",
|
||||
"blue": "The thick vertical root with {bblue{n flowers ",
|
||||
"yellow": "The thin horizontal-hanging root with {yyellow{n flowers ",
|
||||
"green": "The weedy {ggreen{n horizontal root "}
|
||||
vpos = {-1: "hangs far to the {wleft{n on the wall.",
|
||||
0: "hangs straight down the {wmiddle{n of the wall.",
|
||||
1: "hangs far to the {wright{n of the wall."}
|
||||
hpos = {-1: "covers the {wupper{n part of the wall.",
|
||||
0: "passes right over the {wmiddle{n of the wall.",
|
||||
1: "nearly touches the floor, near the {wbottom{n of the wall."}
|
||||
|
||||
if root in ("yellow", "green"):
|
||||
string = rootnames[root] + hpos[ipos]
|
||||
else:
|
||||
string = rootnames[root] + vpos[ipos]
|
||||
return string
|
||||
|
||||
def return_appearance(self, caller):
|
||||
"""
|
||||
This is called when someone looks at the wall. We need to echo the
|
||||
current root positions.
|
||||
"""
|
||||
if caller.db.crumbling_wall_found_button:
|
||||
string = "Having moved all the roots aside, you find that the center of the wall, "
|
||||
string += "previously hidden by the vegetation, hid a curious square depression. It was maybe once "
|
||||
string += "concealed and made to look a part of the wall, but with the crumbling of stone around it,"
|
||||
string += "it's now easily identifiable as some sort of button."
|
||||
else:
|
||||
string = "The wall is old and covered with roots that here and there have permeated the stone. "
|
||||
string += "The roots (or whatever they are - some of them are covered in small non-descript flowers) "
|
||||
string += "crisscross the wall, making it hard to clearly see its stony surface.\n"
|
||||
for key, pos in self.db.root_pos.items():
|
||||
string += "\n" + self._translate_position(key, pos)
|
||||
self.db.desc = string
|
||||
# call the parent to continue execution (will use desc we just set)
|
||||
return super(CrumblingWall, self).return_appearance(caller)
|
||||
|
||||
def at_after_traverse(self, traverser, source_location):
|
||||
"""
|
||||
This is called after we traversed this exit. Cleans up and resets
|
||||
the puzzle.
|
||||
"""
|
||||
del traverser.db.crumbling_wall_found_button
|
||||
del traverser.db.crumbling_wall_found_exit
|
||||
self.reset()
|
||||
|
||||
def at_failed_traverse(self, traverser):
|
||||
"This is called if the player fails to pass the Exit."
|
||||
traverser.msg("No matter how you try, you cannot force yourself through %s." % self.key)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
Called by tutorial world runner, or whenever someone successfully
|
||||
traversed the Exit.
|
||||
"""
|
||||
self.location.msg_contents("The secret door closes abruptly, roots falling back into place.")
|
||||
for obj in self.location.contents:
|
||||
# clear eventual puzzle-solved attribues on everyone that didn't
|
||||
# get out in time. They have to try again.
|
||||
del obj.db.crumbling_wall_found_exit
|
||||
|
||||
# Reset the roots with some random starting positions for the roots:
|
||||
start_pos = [{"yellow":1, "green":0, "red":0, "blue":0},
|
||||
{"yellow":0, "green":0, "red":0, "blue":0},
|
||||
{"yellow":0, "green":1, "red":-1, "blue":0},
|
||||
{"yellow":1, "green":0, "red":0, "blue":0},
|
||||
{"yellow":0, "green":0, "red":0, "blue":1}]
|
||||
self.db.root_pos = start_pos[random.randint(0, 4)]
|
||||
self.destination = None
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Weapon - object type
|
||||
#
|
||||
# A weapon is necessary in order to fight in the tutorial
|
||||
# world. A weapon (which here is assumed to be a bladed
|
||||
# melee weapon for close combat) has three commands,
|
||||
# stab, slash and defend. Weapons also have a property "magic"
|
||||
# to determine if they are usable against certain enemies.
|
||||
#
|
||||
# Since Characters don't have special skills in the tutorial,
|
||||
# we let the weapon itself determine how easy/hard it is
|
||||
# to hit with it, and how much damage it can do.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdAttack(Command):
|
||||
"""
|
||||
Attack the enemy. Commands:
|
||||
|
||||
stab <enemy>
|
||||
slash <enemy>
|
||||
parry
|
||||
|
||||
stab - (thrust) makes a lot of damage but is harder to hit with.
|
||||
slash - is easier to land, but does not make as much damage.
|
||||
parry - forgoes your attack but will make you harder to hit on next
|
||||
enemy attack.
|
||||
|
||||
"""
|
||||
|
||||
# this is an example of implementing many commands as a single
|
||||
# command class, using the given command alias to separate between them.
|
||||
|
||||
key = "attack"
|
||||
aliases = ["hit","kill", "fight", "thrust", "pierce", "stab",
|
||||
"slash", "chop", "parry", "defend"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the stab"
|
||||
|
||||
cmdstring = self.cmdstring
|
||||
|
||||
if cmdstring in ("attack", "fight"):
|
||||
string = "How do you want to fight? Choose one of 'stab', 'slash' or 'defend'."
|
||||
self.caller.msg(string)
|
||||
return
|
||||
|
||||
# parry mode
|
||||
if cmdstring in ("parry", "defend"):
|
||||
string = "You raise your weapon in a defensive pose, ready to block the next enemy attack."
|
||||
self.caller.msg(string)
|
||||
self.caller.db.combat_parry_mode = True
|
||||
self.caller.location.msg_contents("%s takes a defensive stance" % self.caller, exclude=[self.caller])
|
||||
return
|
||||
|
||||
if not self.args:
|
||||
self.caller.msg("Who do you attack?")
|
||||
return
|
||||
target = self.caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
|
||||
string = ""
|
||||
tstring = ""
|
||||
ostring = ""
|
||||
if cmdstring in ("thrust", "pierce", "stab"):
|
||||
hit = float(self.obj.db.hit) * 0.7 # modified due to stab
|
||||
damage = self.obj.db.damage * 2 # modified due to stab
|
||||
string = "You stab with %s. " % self.obj.key
|
||||
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
|
||||
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
|
||||
self.caller.db.combat_parry_mode = False
|
||||
elif cmdstring in ("slash", "chop"):
|
||||
hit = float(self.obj.db.hit) # un modified due to slash
|
||||
damage = self.obj.db.damage # un modified due to slash
|
||||
string = "You slash with %s. " % self.obj.key
|
||||
tstring = "%s slash at you with %s. " % (self.caller.key, self.obj.key)
|
||||
ostring = "%s slash at %s with %s. " % (self.caller.key, target.key, self.obj.key)
|
||||
self.caller.db.combat_parry_mode = False
|
||||
else:
|
||||
self.caller.msg("You fumble with your weapon, unsure of whether to stab, slash or parry ...")
|
||||
self.caller.location.msg_contents("%s fumbles with their weapon." % self.obj.key)
|
||||
self.caller.db.combat_parry_mode = False
|
||||
return
|
||||
|
||||
if target.db.combat_parry_mode:
|
||||
# target is defensive; even harder to hit!
|
||||
target.msg("{GYou defend, trying to avoid the attack.{n")
|
||||
hit *= 0.5
|
||||
|
||||
if random.random() <= hit:
|
||||
self.caller.msg(string + "{gIt's a hit!{n")
|
||||
target.msg(tstring + "{rIt's a hit!{n")
|
||||
self.caller.location.msg_contents(ostring + "It's a hit!", exclude=[target,self.caller])
|
||||
|
||||
# call enemy hook
|
||||
if hasattr(target, "at_hit"):
|
||||
# should return True if target is defeated, False otherwise.
|
||||
return target.at_hit(self.obj, self.caller, damage)
|
||||
elif target.db.health:
|
||||
target.db.health -= damage
|
||||
else:
|
||||
# sorry, impossible to fight this enemy ...
|
||||
self.caller.msg("The enemy seems unaffacted.")
|
||||
return False
|
||||
else:
|
||||
self.caller.msg(string + "{rYou miss.{n")
|
||||
target.msg(tstring + "{gThey miss you.{n")
|
||||
self.caller.location.msg_contents(ostring + "They miss.", exclude=[target, self.caller])
|
||||
|
||||
|
||||
class CmdSetWeapon(CmdSet):
|
||||
"Holds the attack command."
|
||||
def at_cmdset_creation(self):
|
||||
"called at first object creation."
|
||||
self.add(CmdAttack())
|
||||
|
||||
|
||||
class Weapon(TutorialObject):
|
||||
"""
|
||||
This defines a bladed weapon.
|
||||
|
||||
Important attributes (set at creation):
|
||||
hit - chance to hit (0-1)
|
||||
parry - chance to parry (0-1)
|
||||
damage - base damage given (modified by hit success and
|
||||
type of attack) (0-10)
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at first creation of the object"
|
||||
super(Weapon, self).at_object_creation()
|
||||
self.db.hit = 0.4 # hit chance
|
||||
self.db.parry = 0.8 # parry chance
|
||||
self.db.damage = 8.0
|
||||
self.db.magic = False
|
||||
self.cmdset.add_default(CmdSetWeapon, permanent=True)
|
||||
|
||||
def reset(self):
|
||||
"""
|
||||
When reset, the weapon is simply deleted, unless it has a place
|
||||
to return to.
|
||||
"""
|
||||
if self.location.has_player and self.home == self.location:
|
||||
self.location.msg_contents("%s suddenly and magically fades into nothingness, as if it was never there ..." % self.key)
|
||||
self.delete()
|
||||
else:
|
||||
self.location = self.home
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Weapon rack - spawns weapons
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdGetWeapon(Command):
|
||||
"""
|
||||
Usage:
|
||||
get weapon
|
||||
|
||||
This will try to obtain a weapon from the container.
|
||||
"""
|
||||
key = "get"
|
||||
aliases = "get weapon"
|
||||
locks = "cmd:all()"
|
||||
help_cateogory = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
|
||||
rack_id = self.obj.db.rack_id
|
||||
if self.caller.attributes.get(rack_id):
|
||||
# we don't allow a player to take more than one weapon from rack.
|
||||
self.caller.msg("%s has no more to offer you." % self.obj.name)
|
||||
else:
|
||||
dmg, name, aliases, desc, magic = self.obj.randomize_type()
|
||||
new_weapon = create_object(Weapon, key=name, aliases=aliases,location=self.caller, home=self.caller)
|
||||
new_weapon.db.rack_id = rack_id
|
||||
new_weapon.db.damage = dmg
|
||||
new_weapon.db.desc = desc
|
||||
new_weapon.db.magic = magic
|
||||
ostring = self.obj.db.get_text
|
||||
if not ostring:
|
||||
ostring = "You pick up %s."
|
||||
if '%s' in ostring:
|
||||
self.caller.msg(ostring % name)
|
||||
else:
|
||||
self.caller.msg(ostring)
|
||||
# tag the caller so they cannot keep taking objects from the rack.
|
||||
self.caller.attributes.add(rack_id, True)
|
||||
|
||||
|
||||
class CmdSetWeaponRack(CmdSet):
|
||||
"group the rack cmd"
|
||||
key = "weaponrack_cmdset"
|
||||
mergemode = "Replace"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Called at first creation of cmdset"
|
||||
self.add(CmdGetWeapon())
|
||||
|
||||
|
||||
class WeaponRack(TutorialObject):
|
||||
"""
|
||||
This will spawn a new weapon for the player unless the player already has
|
||||
one from this rack.
|
||||
|
||||
attribute to set at creation:
|
||||
min_dmg - the minimum damage of objects from this rack
|
||||
max_dmg - the maximum damage of objects from this rack
|
||||
magic - if weapons should be magical (have the magic flag set)
|
||||
get_text - the echo text to return when getting the weapon. Give '%s'
|
||||
to include the name of the weapon.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"called at creation"
|
||||
self.cmdset.add_default(CmdSetWeaponRack, permanent=True)
|
||||
self.db.rack_id = "weaponrack_1"
|
||||
self.db.min_dmg = 1.0
|
||||
self.db.max_dmg = 4.0
|
||||
self.db.magic = False
|
||||
|
||||
def randomize_type(self):
|
||||
"""
|
||||
this returns a random weapon
|
||||
"""
|
||||
min_dmg = float(self.db.min_dmg)
|
||||
max_dmg = float(self.db.max_dmg)
|
||||
magic = bool(self.db.magic)
|
||||
dmg = min_dmg + random.random()*(max_dmg - min_dmg)
|
||||
aliases = [self.db.rack_id, "weapon"]
|
||||
if dmg < 1.5:
|
||||
name = "Knife"
|
||||
desc = "A rusty kitchen knife. Better than nothing."
|
||||
elif dmg < 2.0:
|
||||
name = "Rusty dagger"
|
||||
desc = "A double-edged dagger with nicked edge. It has a wooden handle."
|
||||
elif dmg < 3.0:
|
||||
name = "Sword"
|
||||
desc = "A rusty shortsword. It has leather wrapped around the handle."
|
||||
elif dmg < 4.0:
|
||||
name = "Club"
|
||||
desc = "A heavy wooden club with some rusty spikes in it."
|
||||
elif dmg < 5.0:
|
||||
name = "Ornate Longsword"
|
||||
aliases.extend(["longsword","ornate"])
|
||||
desc = "A fine longsword."
|
||||
elif dmg < 6.0:
|
||||
name = "Runeaxe"
|
||||
aliases.extend(["rune","axe"])
|
||||
desc = "A single-bladed axe, heavy but yet easy to use."
|
||||
elif dmg < 7.0:
|
||||
name = "Broadsword named Thruning"
|
||||
aliases.extend(["thruning","broadsword"])
|
||||
desc = "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands."
|
||||
elif dmg < 8.0:
|
||||
name = "Silver Warhammer"
|
||||
aliases.append("warhammer")
|
||||
desc = "A heavy war hammer with silver ornaments. This huge weapon causes massive damage."
|
||||
elif dmg < 9.0:
|
||||
name = "Slayer Waraxe"
|
||||
aliases.extend(["waraxe","slayer"])
|
||||
desc = "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher."
|
||||
elif dmg < 10.0:
|
||||
name = "The Ghostblade"
|
||||
aliases.append("ghostblade")
|
||||
desc = "This massive sword is large as you are tall. Its metal shine with a bluish glow."
|
||||
else:
|
||||
name = "The Hawkblade"
|
||||
aliases.append("hawkblade")
|
||||
desc = "White surges of magical power runs up and down this runic blade. The hawks depicted on its hilt almost seems to have a life of their own."
|
||||
if dmg < 9 and magic:
|
||||
desc += "\nThe metal seems to glow faintly, as if imbued with more power than what is immediately apparent."
|
||||
return dmg, name, aliases, desc, magic
|
||||
739
evennia/contrib/tutorial_world/rooms.py
Normal file
739
evennia/contrib/tutorial_world/rooms.py
Normal file
|
|
@ -0,0 +1,739 @@
|
|||
"""
|
||||
|
||||
Room Typeclasses for the TutorialWorld.
|
||||
|
||||
"""
|
||||
|
||||
import random
|
||||
from evennia import CmdSet, Script, Command, DefaultRoom
|
||||
from evennia import utils, create_object, search_object
|
||||
from contrib.tutorial_world import scripts as tut_scripts
|
||||
from contrib.tutorial_world.objects import LightSource, TutorialObject
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Tutorial room - parent room class
|
||||
#
|
||||
# This room is the parent of all rooms in the tutorial.
|
||||
# It defines a tutorial command on itself (available to
|
||||
# all who is in a tutorial room).
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdTutorial(Command):
|
||||
"""
|
||||
Get help during the tutorial
|
||||
|
||||
Usage:
|
||||
tutorial [obj]
|
||||
|
||||
This command allows you to get behind-the-scenes info
|
||||
about an object or the current location.
|
||||
|
||||
"""
|
||||
key = "tutorial"
|
||||
aliases = ["tut"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"""
|
||||
All we do is to scan the current location for an attribute
|
||||
called `tutorial_info` and display that.
|
||||
"""
|
||||
|
||||
caller = self.caller
|
||||
|
||||
if not self.args:
|
||||
target = self.obj # this is the room the command is defined on
|
||||
else:
|
||||
target = caller.search(self.args.strip())
|
||||
if not target:
|
||||
return
|
||||
helptext = target.db.tutorial_info
|
||||
if helptext:
|
||||
caller.msg("{G%s{n" % helptext)
|
||||
else:
|
||||
caller.msg("{RSorry, there is no tutorial help available here.{n")
|
||||
|
||||
|
||||
class TutorialRoomCmdSet(CmdSet):
|
||||
"Implements the simple tutorial cmdset"
|
||||
key = "tutorial_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"add the tutorial cmd"
|
||||
self.add(CmdTutorial())
|
||||
|
||||
|
||||
class TutorialRoom(DefaultRoom):
|
||||
"""
|
||||
This is the base room type for all rooms in the tutorial world.
|
||||
It defines a cmdset on itself for reading tutorial info about the location.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when room is first created"
|
||||
self.db.tutorial_info = "This is a tutorial room. It allows you to use the 'tutorial' command."
|
||||
self.cmdset.add_default(TutorialRoomCmdSet)
|
||||
|
||||
def reset(self):
|
||||
"Can be called by the tutorial runner."
|
||||
pass
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Weather room - scripted room
|
||||
#
|
||||
# The weather room is called by a script at
|
||||
# irregular intervals. The script is generally useful
|
||||
# and so is split out into tutorialworld.scripts.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class WeatherRoom(TutorialRoom):
|
||||
"""
|
||||
This should probably better be called a rainy room...
|
||||
|
||||
This sets up an outdoor room typeclass. At irregular intervals,
|
||||
the effects of weather will show in the room. Outdoor rooms should
|
||||
inherit from this.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created."
|
||||
super(WeatherRoom, self).at_object_creation()
|
||||
|
||||
# we use the imported IrregularEvent script
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
self.db.tutorial_info = \
|
||||
"This room has a Script running that has it echo a weather-related message at irregular intervals."
|
||||
|
||||
def update_irregular(self):
|
||||
"create a tuple of possible texts to return."
|
||||
strings = (
|
||||
"The rain coming down from the iron-grey sky intensifies.",
|
||||
"A gush of wind throws the rain right in your face. Despite your cloak you shiver.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"For a moment it looks like the rain is slowing, then it begins anew with renewed force.",
|
||||
"The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.",
|
||||
"The wind is picking up, howling around you, throwing water droplets in your face. It's cold.",
|
||||
"Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.",
|
||||
"It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.",
|
||||
"Lightning strikes in several thundering bolts, striking the trees in the forest to your west.",
|
||||
"You hear the distant howl of what sounds like some sort of dog or wolf.",
|
||||
"Large clouds rush across the sky, throwing their load of rain over the world.")
|
||||
|
||||
# get a random value so we can select one of the strings above.
|
||||
# Send this to the room.
|
||||
irand = random.randint(0, 15)
|
||||
if irand > 10:
|
||||
return # don't return anything, to add more randomness
|
||||
self.msg_contents("{w%s{n" % strings[irand])
|
||||
|
||||
|
||||
#------------------------------------------------------------------------------
|
||||
#
|
||||
# Dark Room - a scripted room
|
||||
#
|
||||
# This room limits the movemenets of its denizens unless they carry a and active
|
||||
# LightSource object (LightSource is defined in
|
||||
# tutorialworld.objects.LightSource)
|
||||
#
|
||||
#------------------------------------------------------------------------------
|
||||
|
||||
class CmdLookDark(Command):
|
||||
"""
|
||||
Look around in darkness
|
||||
|
||||
Usage:
|
||||
look
|
||||
|
||||
Looks in darkness
|
||||
"""
|
||||
key = "look"
|
||||
aliases = ["l", 'feel', 'feel around', 'fiddle']
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the command."
|
||||
caller = self.caller
|
||||
# we don't have light, grasp around blindly.
|
||||
messages = ("It's pitch black. You fumble around but cannot find anything.",
|
||||
"You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
|
||||
"You don't see a thing! Blindly grasping the air around you, you find nothing.",
|
||||
"It's totally dark here. You almost stumble over some un-evenness in the ground.",
|
||||
"You are completely blind. For a moment you think you hear someone breathing nearby ... \n ... surely you must be mistaken.",
|
||||
"Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.",
|
||||
"Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation, but its too damp to burn.",
|
||||
"You can't see anything, but the air is damp. It feels like you are far underground.")
|
||||
irand = random.randint(0, 10)
|
||||
if irand < len(messages):
|
||||
caller.msg(messages[irand])
|
||||
else:
|
||||
# check so we don't already carry a lightsource.
|
||||
carried_lights = [obj for obj in caller.contents
|
||||
if utils.inherits_from(obj, LightSource)]
|
||||
if carried_lights:
|
||||
string = "You don't want to stumble around in blindness anymore. You already found what you need. Let's get light already!"
|
||||
caller.msg(string)
|
||||
return
|
||||
#if we are lucky, we find the light source.
|
||||
lightsources = [obj for obj in self.obj.contents
|
||||
if utils.inherits_from(obj, LightSource)]
|
||||
if lightsources:
|
||||
lightsource = lightsources[0]
|
||||
else:
|
||||
# create the light source from scratch.
|
||||
lightsource = create_object(LightSource, key="splinter")
|
||||
lightsource.location = caller
|
||||
string = "Your fingers bump against a splinter of wood in a corner. It smells of resin and seems dry enough to burn!"
|
||||
string += "\nYou pick it up, holding it firmly. Now you just need to {wlight{n it using the flint and steel you carry with you."
|
||||
caller.msg(string)
|
||||
|
||||
|
||||
class CmdDarkHelp(Command):
|
||||
"""
|
||||
Help command for the dark state.
|
||||
"""
|
||||
key = "help"
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the help command."
|
||||
string = "Can't help you until you find some light! Try feeling around for something to burn."
|
||||
string += " You cannot give up even if you don't find anything right away."
|
||||
self.caller.msg(string)
|
||||
|
||||
# the nomatch system command will give a suitable error when we cannot find
|
||||
# the normal commands.
|
||||
from evennia.commands.default.syscommands import CMD_NOMATCH
|
||||
from evennia.commands.default.general import CmdSay
|
||||
|
||||
|
||||
class CmdDarkNoMatch(Command):
|
||||
"This is called when there is no match"
|
||||
key = CMD_NOMATCH
|
||||
locks = "cmd:all()"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.")
|
||||
|
||||
|
||||
class DarkCmdSet(CmdSet):
|
||||
"Groups the commands."
|
||||
key = "darkroom_cmdset"
|
||||
mergetype = "Replace" # completely remove all other commands
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"populates the cmdset."
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdLookDark())
|
||||
self.add(CmdDarkHelp())
|
||||
self.add(CmdDarkNoMatch())
|
||||
self.add(CmdSay)
|
||||
|
||||
#
|
||||
# Darkness room two-state system
|
||||
#
|
||||
|
||||
class DarkState(Script):
|
||||
"""
|
||||
The darkness state is a script that keeps tabs on when
|
||||
a player in the room carries an active light source. It places
|
||||
a new, very restrictive cmdset (DarkCmdSet) on all the players
|
||||
in the room whenever there is no light in it. Upon turning on
|
||||
a light, the state switches off and moves to LightState.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This setups the script"
|
||||
self.key = "tutorial_darkness_state"
|
||||
self.desc = "A dark room"
|
||||
self.persistent = True
|
||||
|
||||
def at_start(self):
|
||||
"called when the script is first starting up."
|
||||
for char in [char for char in self.obj.contents if char.has_player]:
|
||||
if char.is_superuser:
|
||||
char.msg("You are Superuser, so you are not affected by the dark state.")
|
||||
else:
|
||||
char.cmdset.add(DarkCmdSet)
|
||||
char.msg("The room is pitch dark! You are likely to be eaten by a Grue.")
|
||||
|
||||
def is_valid(self):
|
||||
"is valid only as long as noone in the room has lit the lantern."
|
||||
return not self.obj.is_lit()
|
||||
|
||||
def at_stop(self):
|
||||
"Someone turned on a light. This state dies. Switch to LightState."
|
||||
for char in [char for char in self.obj.contents if char.has_player]:
|
||||
char.cmdset.delete(DarkCmdSet)
|
||||
self.obj.db.is_dark = False
|
||||
self.obj.scripts.add(LightState)
|
||||
|
||||
|
||||
class LightState(Script):
|
||||
"""
|
||||
This is the counterpart to the Darkness state. It is active when the
|
||||
lantern is on.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called when script is first created."
|
||||
self.key = "tutorial_light_state"
|
||||
self.desc = "A room lit up"
|
||||
self.persistent = True
|
||||
|
||||
def is_valid(self):
|
||||
"""
|
||||
This state is only valid as long as there is an active light
|
||||
source in the room.
|
||||
"""
|
||||
return self.obj.is_lit()
|
||||
|
||||
def at_stop(self):
|
||||
"Light disappears. This state dies. Return to DarknessState."
|
||||
self.obj.db.is_dark = True
|
||||
self.obj.scripts.add(DarkState)
|
||||
|
||||
|
||||
class DarkRoom(TutorialRoom):
|
||||
"""
|
||||
A dark room. This tries to start the DarkState script on all
|
||||
objects entering. The script is responsible for making sure it is
|
||||
valid (that is, that there is no light source shining in the room).
|
||||
"""
|
||||
def is_lit(self):
|
||||
"""
|
||||
Helper method to check if the room is lit up. It checks all
|
||||
characters in room to see if they carry an active object of
|
||||
type LightSource.
|
||||
"""
|
||||
return any([any([True for obj in char.contents
|
||||
if utils.inherits_from(obj, LightSource) and obj.db.is_active])
|
||||
for char in self.contents if char.has_player])
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called when object is first created."
|
||||
super(DarkRoom, self).at_object_creation()
|
||||
self.db.tutorial_info = "This is a room with custom command sets on itself."
|
||||
# this variable is set by the scripts. It makes for an easy flag to
|
||||
# look for by other game elements (such as the crumbling wall in
|
||||
# the tutorial)
|
||||
self.db.is_dark = True
|
||||
# the room starts dark.
|
||||
self.scripts.add(DarkState)
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Called when an object enters the room. We crank the wheels to make
|
||||
sure scripts are synced.
|
||||
"""
|
||||
if character.has_player:
|
||||
if not self.is_lit() and not character.is_superuser:
|
||||
character.cmdset.add(DarkCmdSet)
|
||||
if character.db.health and character.db.health <= 0:
|
||||
# heal character coming here from being defeated by mob.
|
||||
health = character.db.health_max
|
||||
if not health:
|
||||
health = 20
|
||||
character.db.health = health
|
||||
self.scripts.validate()
|
||||
|
||||
def at_object_leave(self, character, target_location):
|
||||
"""
|
||||
In case people leave with the light, we make sure to update the
|
||||
states accordingly.
|
||||
"""
|
||||
character.cmdset.delete(DarkCmdSet) # in case we are teleported away
|
||||
self.scripts.validate()
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Teleport room - puzzle room
|
||||
#
|
||||
# This is a sort of puzzle room that requires a certain
|
||||
# attribute on the entering character to be the same as
|
||||
# an attribute of the room. If not, the character will
|
||||
# be teleported away to a target location. This is used
|
||||
# by the Obelisk - grave chamber puzzle, where one must
|
||||
# have looked at the obelisk to get an attribute set on
|
||||
# oneself, and then pick the grave chamber with the
|
||||
# matching imagery for this attribute.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class TeleportRoom(TutorialRoom):
|
||||
"""
|
||||
Teleporter - puzzle room.
|
||||
|
||||
Important attributes (set at creation):
|
||||
puzzle_key - which attr to look for on character
|
||||
puzzle_value - what char.db.puzzle_key must be set to
|
||||
teleport_to - where to teleport to in case of failure to match
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called at first creation"
|
||||
super(TeleportRoom, self).at_object_creation()
|
||||
# what character.db.puzzle_clue must be set to, to avoid teleportation.
|
||||
self.db.puzzle_value = 1
|
||||
# target of successful teleportation. Can be a dbref or a
|
||||
# unique room name.
|
||||
self.db.success_teleport_to = "treasure room"
|
||||
# the target of the failure teleportation.
|
||||
self.db.failure_teleport_to = "dark cell"
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved into
|
||||
this room.
|
||||
"""
|
||||
if not character.has_player:
|
||||
# only act on player characters.
|
||||
return
|
||||
#print character.db.puzzle_clue, self.db.puzzle_value
|
||||
if character.db.puzzle_clue != self.db.puzzle_value:
|
||||
# we didn't pass the puzzle. See if we can teleport.
|
||||
teleport_to = self.db.failure_teleport_to # this is a room name
|
||||
else:
|
||||
# passed the puzzle
|
||||
teleport_to = self.db.success_teleport_to # this is a room name
|
||||
|
||||
results = search_object(teleport_to)
|
||||
if not results or len(results) > 1:
|
||||
# we cannot move anywhere since no valid target was found.
|
||||
print "no valid teleport target for %s was found." % teleport_to
|
||||
return
|
||||
if character.player.is_superuser:
|
||||
# superusers don't get teleported
|
||||
character.msg("Superuser block: You would have been teleported to %s." % results[0])
|
||||
return
|
||||
# teleport
|
||||
character.execute_cmd("look")
|
||||
character.location = results[0] # stealth move
|
||||
character.location.at_object_receive(character, self)
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Bridge - unique room
|
||||
#
|
||||
# Defines a special west-eastward "bridge"-room, a large room it takes
|
||||
# several steps to cross. It is complete with custom commands and a
|
||||
# chance of falling off the bridge. This room has no regular exits,
|
||||
# instead the exiting are handled by custom commands set on the player
|
||||
# upon first entering the room.
|
||||
#
|
||||
# Since one can enter the bridge room from both ends, it is
|
||||
# divided into five steps:
|
||||
# westroom <- 0 1 2 3 4 -> eastroom
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
|
||||
class CmdEast(Command):
|
||||
"""
|
||||
Try to cross the bridge eastwards.
|
||||
"""
|
||||
key = "east"
|
||||
aliases = ["e"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move forward"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = min(5, caller.db.tutorial_bridge_position + 1)
|
||||
|
||||
if bridge_step > 4:
|
||||
# we have reached the far east end of the bridge.
|
||||
# Move to the east room.
|
||||
eexit = search_object(self.obj.db.east_exit)
|
||||
if eexit:
|
||||
caller.move_to(eexit[0])
|
||||
else:
|
||||
caller.msg("No east exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
caller.location.msg_contents("%s steps eastwards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
# go back across the bridge
|
||||
class CmdWest(Command):
|
||||
"""
|
||||
Go back across the bridge westwards.
|
||||
"""
|
||||
key = "west"
|
||||
aliases = ["w"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"move forward"
|
||||
caller = self.caller
|
||||
|
||||
bridge_step = max(-1, caller.db.tutorial_bridge_position - 1)
|
||||
|
||||
if bridge_step < 0:
|
||||
# we have reached the far west end of the bridge.#
|
||||
# Move to the west room.
|
||||
wexit = search_object(self.obj.db.west_exit)
|
||||
if wexit:
|
||||
caller.move_to(wexit[0])
|
||||
else:
|
||||
caller.msg("No west exit was found for this room. Contact an admin.")
|
||||
return
|
||||
caller.db.tutorial_bridge_position = bridge_step
|
||||
caller.location.msg_contents("%s steps westwartswards across the bridge." % caller.name, exclude=caller)
|
||||
caller.execute_cmd("look")
|
||||
|
||||
|
||||
class CmdLookBridge(Command):
|
||||
"""
|
||||
looks around at the bridge.
|
||||
"""
|
||||
key = 'look'
|
||||
aliases = ["l"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Looking around, including a chance to fall."
|
||||
bridge_position = self.caller.db.tutorial_bridge_position
|
||||
|
||||
|
||||
messages =("You are standing {wvery close to the the bridge's western foundation{n. If you go west you will be back on solid ground ...",
|
||||
"The bridge slopes precariously where it extends eastwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are {whalfways{n out on the unstable bridge.",
|
||||
"The bridge slopes precariously where it extends westwards towards the lowest point - the center point of the hang bridge.",
|
||||
"You are standing {wvery close to the bridge's eastern foundation{n. If you go east you will be back on solid ground ...")
|
||||
moods = ("The bridge sways in the wind.", "The hanging bridge creaks dangerously.",
|
||||
"You clasp the ropes firmly as the bridge sways and creaks under you.",
|
||||
"From the castle you hear a distant howling sound, like that of a large dog or other beast.",
|
||||
"The bridge creaks under your feet. Those planks does not seem very sturdy.",
|
||||
"Far below you the ocean roars and throws its waves against the cliff, as if trying its best to reach you.",
|
||||
"Parts of the bridge come loose behind you, falling into the chasm far below!",
|
||||
"A gust of wind causes the bridge to sway precariously.",
|
||||
"Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...",
|
||||
"The section of rope you hold onto crumble in your hands, parts of it breaking apart. You sway trying to regain balance.")
|
||||
message = "{c%s{n\n" % self.obj.key + messages[bridge_position] + "\n" + moods[random.randint(0, len(moods) - 1)]
|
||||
chars = [obj for obj in self.obj.contents if obj != self.caller and obj.has_player]
|
||||
if chars:
|
||||
message += "\n You see: %s" % ", ".join("{c%s{n" % char.key for char in chars)
|
||||
|
||||
self.caller.msg(message)
|
||||
|
||||
# there is a chance that we fall if we are on the western or central
|
||||
# part of the bridge.
|
||||
if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser:
|
||||
# we fall on 5% of the times.
|
||||
fexit = search_object(self.obj.db.fall_exit)
|
||||
if fexit:
|
||||
string = "\n Suddenly the plank you stand on gives way under your feet! You fall!"
|
||||
string += "\n You try to grab hold of an adjoining plank, but all you manage to do is to "
|
||||
string += "divert your fall westwards, towards the cliff face. This is going to hurt ... "
|
||||
string += "\n ... The world goes dark ...\n"
|
||||
# note that we move silently so as to not call look hooks (this is a little trick to leave
|
||||
# the player with the "world goes dark ..." message, giving them ample time to read it. They
|
||||
# have to manually call look to find out their new location). Thus we also call the
|
||||
# at_object_leave hook manually (otherwise this is done by move_to()).
|
||||
self.caller.msg("{r%s{n" % string)
|
||||
self.obj.at_object_leave(self.caller, fexit)
|
||||
self.caller.location = fexit[0] # stealth move, without any other hook calls.
|
||||
self.obj.msg_contents("A plank gives way under %s's feet and they fall from the bridge!" % self.caller.key)
|
||||
|
||||
|
||||
# custom help command
|
||||
class CmdBridgeHelp(Command):
|
||||
"""
|
||||
Overwritten help command
|
||||
"""
|
||||
key = "help"
|
||||
aliases = ["h"]
|
||||
locks = "cmd:all()"
|
||||
help_category = "Tutorial world"
|
||||
|
||||
def func(self):
|
||||
"Implements the command."
|
||||
string = "You are trying hard not to fall off the bridge ..."
|
||||
string += "\n\nWhat you can do is trying to cross the bridge {weast{n "
|
||||
string += "or try to get back to the mainland {wwest{n)."
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
class BridgeCmdSet(CmdSet):
|
||||
"This groups the bridge commands. We will store it on the room."
|
||||
key = "Bridge commands"
|
||||
priority = 1 # this gives it precedence over the normal look/help commands.
|
||||
def at_cmdset_creation(self):
|
||||
"Called at first cmdset creation"
|
||||
self.add(CmdTutorial())
|
||||
self.add(CmdEast())
|
||||
self.add(CmdWest())
|
||||
self.add(CmdLookBridge())
|
||||
self.add(CmdBridgeHelp())
|
||||
|
||||
|
||||
class BridgeRoom(TutorialRoom):
|
||||
"""
|
||||
The bridge room implements an unsafe bridge. It also enters the player into
|
||||
a state where they get new commands so as to try to cross the bridge.
|
||||
|
||||
We want this to result in the player getting a special set of
|
||||
commands related to crossing the bridge. The result is that it will
|
||||
take several steps to cross it, despite it being represented by only a
|
||||
single room.
|
||||
|
||||
We divide the bridge into steps:
|
||||
|
||||
self.db.west_exit - - | - - self.db.east_exit
|
||||
0 1 2 3 4
|
||||
|
||||
The position is handled by a variable stored on the player when entering
|
||||
and giving special move commands will increase/decrease the counter
|
||||
until the bridge is crossed.
|
||||
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Setups the room"
|
||||
super(BridgeRoom, self).at_object_creation()
|
||||
|
||||
# at irregular intervals, this will call self.update_irregular()
|
||||
self.scripts.add(tut_scripts.IrregularEvent)
|
||||
# this identifies the exits from the room (should be the command
|
||||
# needed to leave through that exit). These are defaults, but you
|
||||
# could of course also change them after the room has been created.
|
||||
self.db.west_exit = "cliff"
|
||||
self.db.east_exit = "gate"
|
||||
self.db.fall_exit = "cliffledge"
|
||||
# add the cmdset on the room.
|
||||
self.cmdset.add_default(BridgeCmdSet)
|
||||
|
||||
self.db.tutorial_info = \
|
||||
"""The bridge seem large but is actually only a single room that assigns custom west/east commands."""
|
||||
|
||||
def update_irregular(self):
|
||||
"""
|
||||
This is called at irregular intervals and makes the passage
|
||||
over the bridge a little more interesting.
|
||||
"""
|
||||
strings = (
|
||||
"The rain intensifies, making the planks of the bridge even more slippery.",
|
||||
"A gush of wind throws the rain right in your face.",
|
||||
"The rainfall eases a bit and the sky momentarily brightens.",
|
||||
"The bridge shakes under the thunder of a closeby thunder strike.",
|
||||
"The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.",
|
||||
"The wind is picking up, howling around you and causing the bridge to sway from side to side.",
|
||||
"Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.",
|
||||
"The bridge sways from side to side in the wind.")
|
||||
self.msg_contents("{w%s{n" % strings[random.randint(0, 7)])
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
This hook is called by the engine whenever the player is moved
|
||||
into this room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# we only run this if the entered object is indeed a player object.
|
||||
# check so our east/west exits are correctly defined.
|
||||
wexit = search_object(self.db.west_exit)
|
||||
eexit = search_object(self.db.east_exit)
|
||||
fexit = search_object(self.db.fall_exit)
|
||||
if not wexit or not eexit or not fexit:
|
||||
character.msg("The bridge's exits are not properly configured. Contact an admin. Forcing west-end placement.")
|
||||
character.db.tutorial_bridge_position = 0
|
||||
return
|
||||
if source_location == eexit[0]:
|
||||
character.db.tutorial_bridge_position = 4
|
||||
else:
|
||||
character.db.tutorial_bridge_position = 0
|
||||
|
||||
def at_object_leave(self, character, target_location):
|
||||
"""
|
||||
This is triggered when the player leaves the bridge room.
|
||||
"""
|
||||
if character.has_player:
|
||||
# clean up the position attribute
|
||||
del character.db.tutorial_bridge_position
|
||||
|
||||
|
||||
#-----------------------------------------------------------
|
||||
#
|
||||
# Intro Room - unique room
|
||||
#
|
||||
# This room marks the start of the tutorial. It sets up properties on
|
||||
# the player char that is needed for the tutorial.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class IntroRoom(TutorialRoom):
|
||||
"""
|
||||
Intro room
|
||||
|
||||
properties to customize:
|
||||
char_health - integer > 0 (default 20)
|
||||
"""
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Assign properties on characters
|
||||
"""
|
||||
|
||||
# setup
|
||||
health = self.db.char_health
|
||||
if not health:
|
||||
health = 20
|
||||
|
||||
if character.has_player:
|
||||
character.db.health = health
|
||||
character.db.health_max = health
|
||||
|
||||
if character.is_superuser:
|
||||
string = "-"*78
|
||||
string += "\nWARNING: YOU ARE PLAYING AS A SUPERUSER (%s). TO EXPLORE NORMALLY YOU NEED " % character.key
|
||||
string += "\nTO CREATE AND LOG IN AS A REGULAR USER INSTEAD. IF YOU CONTINUE, KNOW THAT "
|
||||
string += "\nMANY FUNCTIONS AND PUZZLES WILL IGNORE THE PRESENCE OF A SUPERUSER.\n"
|
||||
string += "-"*78
|
||||
character.msg("{r%s{n" % string)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Outro room - unique room
|
||||
#
|
||||
# Cleans up the character from all tutorial-related properties.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class OutroRoom(TutorialRoom):
|
||||
"""
|
||||
Outro room.
|
||||
|
||||
One can set an attribute list "wracklist" with weapon-rack ids
|
||||
in order to clear all weapon rack ids from the character.
|
||||
|
||||
"""
|
||||
|
||||
def at_object_receive(self, character, source_location):
|
||||
"""
|
||||
Do cleanup.
|
||||
"""
|
||||
if character.has_player:
|
||||
if self.db.wracklist:
|
||||
for wrackid in self.db.wracklist:
|
||||
character.del_attribute(wrackid)
|
||||
del character.db.health_max
|
||||
del character.db.health
|
||||
del character.db.last_climbed
|
||||
del character.db.puzzle_clue
|
||||
del character.db.combat_parry_mode
|
||||
del character.db.tutorial_bridge_position
|
||||
for tut_obj in [obj for obj in character.contents
|
||||
if utils.inherits_from(obj, TutorialObject)]:
|
||||
tut_obj.reset()
|
||||
114
evennia/contrib/tutorial_world/scripts.py
Normal file
114
evennia/contrib/tutorial_world/scripts.py
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
"""
|
||||
This defines some generally useful scripts for the tutorial world.
|
||||
"""
|
||||
|
||||
import random
|
||||
from evennia import Script
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# IrregularEvent - script firing at random intervals
|
||||
#
|
||||
# This is a generally useful script for updating
|
||||
# objects at irregular intervals. This is used by as diverse
|
||||
# entities as Weather rooms and mobs.
|
||||
#
|
||||
#
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class IrregularEvent(Script):
|
||||
"""
|
||||
This script, which should be tied to a particular object upon
|
||||
instantiation, calls update_irregular on the object at random
|
||||
intervals.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"This setups the script"
|
||||
|
||||
self.key = "update_irregular"
|
||||
self.desc = "Updates at irregular intervals"
|
||||
self.interval = random.randint(30, 70) # interval to call.
|
||||
self.start_delay = True # wait at least self.interval seconds before
|
||||
# calling at_repeat the first time
|
||||
self.persistent = True
|
||||
|
||||
# this attribute determines how likely it is the
|
||||
# 'update_irregular' method gets called on self.obj (value is
|
||||
# 0.0-1.0 with 1.0 meaning it being called every time.)
|
||||
self.db.random_chance = 0.2
|
||||
|
||||
def at_repeat(self):
|
||||
"This gets called every self.interval seconds."
|
||||
rand = random.random()
|
||||
if rand <= self.db.random_chance:
|
||||
try:
|
||||
#self.obj.msg_contents("irregular event for %s(#%i)" % (self.obj, self.obj.id))
|
||||
self.obj.update_irregular()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
|
||||
class FastIrregularEvent(IrregularEvent):
|
||||
"A faster updating irregular event"
|
||||
def at_script_creation(self):
|
||||
"Called at initial script creation"
|
||||
super(FastIrregularEvent, self).at_script_creation()
|
||||
self.interval = 5 # every 5 seconds, 1/5 chance of firing
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Tutorial world Runner - root reset timer for TutorialWorld
|
||||
#
|
||||
# This is a runner that resets the world
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
# #
|
||||
# # This sets up a reset system -- it resets the entire tutorial_world domain
|
||||
# # and all objects inheriting from it back to an initial state, MORPG style.
|
||||
# This is useful in order for different players to explore it without finding
|
||||
# # things missing.
|
||||
# #
|
||||
# # Note that this will of course allow a single player to end up with
|
||||
# # multiple versions of objects if they just wait around between resets;
|
||||
# # In a real game environment this would have to be resolved e.g.
|
||||
# # with custom versions of the 'get' command not accepting doublets.
|
||||
# #
|
||||
|
||||
# # setting up an event for reseting the world.
|
||||
|
||||
# UPDATE_INTERVAL = 60 * 10 # Measured in seconds
|
||||
|
||||
|
||||
# #This is a list of script parent objects that subscribe to the reset
|
||||
# functionality.
|
||||
# RESET_SUBSCRIBERS = ["examples.tutorial_world.p_weapon_rack",
|
||||
# "examples.tutorial_world.p_mob"]
|
||||
|
||||
# class EventResetTutorialWorld(Script):
|
||||
# """
|
||||
# This calls the reset function on all subscribed objects
|
||||
# """
|
||||
# def __init__(self):
|
||||
# super(EventResetTutorialWorld, self).__init__()
|
||||
# self.name = 'reset_tutorial_world'
|
||||
# #this you see when running @ps in game:
|
||||
# self.description = 'Reset the tutorial world .'
|
||||
# self.interval = UPDATE_INTERVAL
|
||||
# self.persistent = True
|
||||
|
||||
# def event_function(self):
|
||||
# """
|
||||
# This is called every self.interval seconds.
|
||||
# """
|
||||
# #find all objects inheriting the subscribing parents
|
||||
# for parent in RESET_SUBSCRIBERS:
|
||||
# objects = Object.objects.global_object_script_parent_search(parent)
|
||||
# for obj in objects:
|
||||
# try:
|
||||
# obj.scriptlink.reset()
|
||||
# except:
|
||||
# logger.log_errmsg(traceback.print_exc())
|
||||
|
|
@ -90,7 +90,7 @@ It seems the bottom of the box is a bit loose.
|
|||
# close the @drop command since it's the end of the file)
|
||||
-------------------------
|
||||
|
||||
An example batch file is contribs/examples/batch_example.ev.
|
||||
An example batch file is contrib/examples/batch_example.ev.
|
||||
|
||||
|
||||
==========================================================================
|
||||
|
|
|
|||
|
|
@ -306,25 +306,8 @@ def host_os_is(osname):
|
|||
|
||||
|
||||
def get_evennia_version():
|
||||
"""
|
||||
Get the Evennia version info from the main package.
|
||||
"""
|
||||
version = "Unknown"
|
||||
with open(os.path.join(settings.ROOT_DIR, "VERSION.txt"), 'r') as f:
|
||||
version = f.read().strip()
|
||||
try:
|
||||
version = "%s (rev %s)" % (version, check_output("git rev-parse --short HEAD", shell=True, cwd=settings.ROOT_DIR).strip())
|
||||
except IOError:
|
||||
pass
|
||||
return version
|
||||
"""
|
||||
Check for the evennia version info.
|
||||
"""
|
||||
try:
|
||||
f = open(settings.ROOT_DIR + os.sep + "VERSION.txt", 'r')
|
||||
return "%s-%s" % (f.read().strip(), os.popen("git rev-parse --short HEAD").read().strip())
|
||||
except IOError:
|
||||
return "Unknown version"
|
||||
import evennia
|
||||
return evennia.__version__
|
||||
|
||||
|
||||
def pypath_to_realpath(python_path, file_ending='.py'):
|
||||
|
|
@ -343,7 +326,7 @@ def pypath_to_realpath(python_path, file_ending='.py'):
|
|||
pathsplit = pathsplit[:-1]
|
||||
if not pathsplit:
|
||||
return python_path
|
||||
path = settings.ROOT_DIR
|
||||
path = settings.EVENNIA_DIR
|
||||
for directory in pathsplit:
|
||||
path = os.path.join(path, directory)
|
||||
if file_ending:
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue