PIP packaging with setup.py, and fixes for bugs revealed by this.

This commit is contained in:
Jonathan Piacenti 2015-01-14 17:21:15 -06:00
parent 42e7d9164e
commit 265f8a4e30
52 changed files with 92 additions and 37 deletions

1
evennia/VERSION.txt Normal file
View file

@ -0,0 +1 @@
0.5.0

View file

@ -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():
"""

View file

@ -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
View 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.

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

761
evennia/contrib/barter.py Normal file
View 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

View 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
View 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
View 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())

View 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())

View 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))

View 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)

View 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()

View 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()

View 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.

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

View 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.

View 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.

View file

@ -0,0 +1,4 @@
from pool import deferToAMPProcess, pp
from commands import Shutdown, Ping, Echo
from child import AMPChild
__version__ = "0.2.1"

View 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)

View 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())]

View 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.
"""

View 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"})

View 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)

View 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)

View 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()

View 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())

View 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}
)

View 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

View 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)

View 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)

View 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.")

View 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)

View 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

View 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())

View 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)

View 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()

View 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!

View file

@ -0,0 +1 @@
# -*- coding: utf-8 -*-

File diff suppressed because it is too large Load diff

View 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)

View 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

View 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()

View 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())

View file

@ -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.
==========================================================================

View file

@ -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: