PEP8 cleanup of the entire codebase. Unchanged are many cases of too-long lines, partly because of the rewrite they would require but also because splitting many lines up would make the code harder to read. Also the third-party libraries (idmapper, prettytable etc) were not cleaned.

This commit is contained in:
Griatch 2013-11-14 19:31:17 +01:00
parent 30b7d2a405
commit 1ae17bcbe4
154 changed files with 5613 additions and 4054 deletions

View file

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

View file

@ -96,7 +96,7 @@ in-game.
from ev import Command, Script, CmdSet
TRADE_TIMEOUT = 60 # timeout for B to accept trade
TRADE_TIMEOUT = 60 # timeout for B to accept trade
class TradeTimeout(Script):
@ -111,15 +111,18 @@ class TradeTimeout(Script):
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
@ -131,7 +134,8 @@ class TradeHandler(object):
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.
We also store the back-reference from the respective party to
this object.
"""
# parties
self.partA = partA
@ -145,7 +149,7 @@ class TradeHandler(object):
self.partB_offers = []
self.partA_accepted = False
self.partB_accepted = False
# start a timer
def msg(self, party, string):
"""
Relay a message to the other party. This allows
@ -159,6 +163,7 @@ class TradeHandler(object):
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:
@ -171,13 +176,14 @@ class TradeHandler(object):
"""
This is used once B decides to join the trade
"""
print "join:", self.partB, partB, self.partB == partB, type(self.partB),type(partB)
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
@ -203,6 +209,7 @@ class TradeHandler(object):
self.partB_offers = list(args)
else:
raise ValueError
def list(self):
"""
Returns two lists of objects on offer, separated by partA/B.
@ -244,7 +251,8 @@ class TradeHandler(object):
self.partB_accepted = True
else:
raise ValueError
return self.finish() # try to close the deal
return self.finish() # try to close the deal
def decline(self, party):
"""
Remove an previously accepted status (changing ones mind)
@ -264,6 +272,7 @@ class TradeHandler(object):
return False
else:
raise ValueError
def finish(self, force=False):
"""
Conclude trade - move all offers and clean up
@ -282,11 +291,13 @@ class TradeHandler(object):
self.partB.cmdset.delete("cmdset_trade")
self.partA_offers = None
self.partB_offers = None
del self.partA.ndb.tradehandler # this will kill it also from partB
# 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).
@ -320,6 +331,7 @@ class CmdTradeBase(Command):
else:
self.str_other = '%s says, "' % self.caller.key + self.emote + '"\n [%s]'
# trade help
class CmdTradeHelp(CmdTradeBase):
@ -342,24 +354,30 @@ class CmdTradeHelp(CmdTradeBase):
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.
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.
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)
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.
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.
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):
@ -406,6 +424,7 @@ class CmdOffer(CmdTradeBase):
caller.msg(self.str_caller % ("You offer %s" % objnames))
self.msg_other(caller, self.str_other % ("They offer %s" % objnames))
# accept
class CmdAccept(CmdTradeBase):
@ -441,6 +460,7 @@ class CmdAccept(CmdTradeBase):
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):
@ -521,6 +541,7 @@ class CmdEvaluate(CmdTradeBase):
# show the description
caller.msg(offer.db.desc)
# status
class CmdStatus(CmdTradeBase):
@ -571,6 +592,7 @@ class CmdStatus(CmdTradeBase):
string += "\n Use 'offer', 'eval' and 'accept'/'decline' to trade. See also 'trade help'."
caller.msg(string)
# finish
class CmdFinish(CmdTradeBase):
@ -596,6 +618,7 @@ class CmdFinish(CmdTradeBase):
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):
@ -616,8 +639,8 @@ class CmdsetTrade(CmdSet):
self.add(CmdFinish())
# access command - once both have given this, this will create the trading cmdset
# to start trade.
# access command - once both have given this, this will create the
# trading cmdset to start trade.
class CmdTrade(Command):
"""
@ -662,9 +685,9 @@ class CmdTrade(Command):
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).
# 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

View file

@ -122,9 +122,11 @@ class CmdOOCLook(default_cmds.CmdLook):
else:
# not ooc mode - leave back to normal look
self.caller = self.character # we have to put this back for normal look to work.
# 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
@ -179,9 +181,9 @@ class CmdOOCCharacterCreate(Command):
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.

View file

@ -34,6 +34,7 @@ import re
from random import randint
from ev import default_cmds
def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=False):
"""
This is a standard dice roller.
@ -41,18 +42,22 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
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 !=.
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
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.
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.
@ -62,7 +67,7 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
dicetype = int(dicetype)
# roll all dice, remembering each roll
rolls= tuple([randint(1, dicetype) for roll in range(dicenum)])
rolls = tuple([randint(1, dicetype) for roll in range(dicenum)])
result = sum(rolls)
if modifier:
@ -70,7 +75,7 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
mod, modvalue = modifier
if not mod in ('+', '-', '*', '/'):
raise TypeError("Non-supported dice modifier: %s" % mod)
modvalue = int(modvalue) # for safety
modvalue = int(modvalue) # for safety
result = eval("%s %s %s" % (result, mod, modvalue))
outcome, diff = None, None
if conditional:
@ -78,19 +83,19 @@ def roll_dice(dicenum, dicetype, modifier=None, conditional=None, return_tuple=F
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)) # gives True/False
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
@ -106,14 +111,16 @@ class CmdDice(default_cmds.MuxCommand):
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'.
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.
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"
@ -145,9 +152,9 @@ class CmdDice(default_cmds.MuxCommand):
pass
elif lparts == 5:
# either e.g. 1d6 + 3 or something like 1d6 > 3
if parts[3] in ('+','-','*','/'):
if parts[3] in ('+', '-', '*', '/'):
modifier = (parts[3], parts[4])
else: #assume it is a conditional
else: # assume it is a conditional
conditional = (parts[3], parts[4])
elif lparts == 7:
# the whole sequence, e.g. 1d6 + 3 > 5
@ -159,7 +166,11 @@ class CmdDice(default_cmds.MuxCommand):
return
# do the roll
try:
result, outcome, diff, rolls = roll_dice(ndice, nsides, modifier=modifier, conditional=conditional, return_tuple=True)
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
@ -168,7 +179,7 @@ class CmdDice(default_cmds.MuxCommand):
rolls = ", ".join(str(roll) for roll in rolls[:-1]) + " and " + str(rolls[-1])
else:
rolls = rolls[0]
if outcome == None:
if outcome is None:
outcomestring = ""
elif outcome:
outcomestring = " This is a {gsuccess{n (by %s)." % diff

View file

@ -46,7 +46,8 @@ from src.commands.default.muxcommand import MuxCommand
from src.commands.cmdhandler import CMD_LOGINSTART
# limit symbol import for API
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate", "CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
__all__ = ("CmdUnconnectedConnect", "CmdUnconnectedCreate",
"CmdUnconnectedQuit", "CmdUnconnectedLook", "CmdUnconnectedHelp")
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
CONNECTION_SCREEN = ""
@ -57,6 +58,7 @@ except Exception:
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.
@ -68,7 +70,7 @@ class CmdUnconnectedConnect(MuxCommand):
"""
key = "connect"
aliases = ["conn", "con", "co"]
locks = "cmd:all()" # not really needed
locks = "cmd:all()" # not really needed
def func(self):
"""
@ -104,7 +106,7 @@ class CmdUnconnectedConnect(MuxCommand):
# Check IP and/or name bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0]==player.name for tup in 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!
@ -160,7 +162,7 @@ class CmdUnconnectedCreate(MuxCommand):
else:
playername, email, password = self.arglist
playername = playername.replace('"', '') # remove "
playername = playername.replace('"', '') # remove "
playername = playername.replace("'", "")
self.playerinfo = (playername, email, password)
@ -214,17 +216,20 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
permissions = settings.PERMISSION_PLAYER_DEFAULT
try:
new_character = create.create_player(playername, email, password,
new_character = create.create_player(playername,
email,
password,
permissions=permissions,
character_typeclass=typeclass,
character_location=default_home,
character_home=default_home)
except Exception, e:
except Exception:
session.msg("There was an error creating the default Character/Player:\n%s\n If this problem persists, contact an admin.")
return
new_player = new_character.player
# This needs to be called so the engine knows this player is logging in for the first time.
# 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)
@ -236,11 +241,11 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
string = "New player '%s' could not connect to public channel!" % new_player.key
logger.log_errmsg(string)
# allow only the character itself and the player to puppet this character (and Immortals).
# 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))
# set a default description
new_character.db.desc = "This is a Player."
@ -250,12 +255,14 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
session.msg(string % (playername, email, 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.
# 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
@ -272,6 +279,7 @@ class CmdUnconnectedQuit(MuxCommand):
session.msg("Good bye! Disconnecting ...")
session.session_disconnect()
class CmdUnconnectedLook(MuxCommand):
"""
This is an unconnected version of the look command for simplicity.
@ -287,6 +295,7 @@ class CmdUnconnectedLook(MuxCommand):
"Show the connect screen."
self.caller.msg(CONNECTION_SCREEN)
class CmdUnconnectedHelp(MuxCommand):
"""
This is an unconnected version of the help command,
@ -328,6 +337,7 @@ 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):

View file

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

View file

@ -15,6 +15,7 @@ from ev import utils
from ev import default_cmds
from src.utils import prettytable
#------------------------------------------------------------
# Evlang-related commands
#
@ -86,13 +87,17 @@ class CmdCode(default_cmds.MuxCommand):
if not self.rhs:
if codetype:
scripts = [(name, tup[1], utils.crop(tup[0])) for name, tup in evlang_scripts.items() if name==codetype]
scripts.extend([(name, "--", "--") for name in evlang_locks if name not in evlang_scripts if name==codetype])
scripts = [(name, tup[1], utils.crop(tup[0]))
for name, tup in evlang_scripts.items() if name==codetype]
scripts.extend([(name, "--", "--") for name in evlang_locks
if name not in evlang_scripts if name==codetype])
else:
# no type specified. List all scripts/slots on object
print evlang_scripts
scripts = [(name, tup[1], utils.crop(tup[0])) for name, tup in evlang_scripts.items()]
scripts.extend([(name, "--", "--") for name in evlang_locks if name not in evlang_scripts])
scripts = [(name, tup[1], utils.crop(tup[0]))
for name, tup in evlang_scripts.items()]
scripts.extend([(name, "--", "--") for name in evlang_locks
if name not in evlang_scripts])
scripts = sorted(scripts, key=lambda p: p[0])
table = prettytable.PrettyTable(["{wtype", "{wcreator", "{wcode"])
@ -114,7 +119,7 @@ class CmdCode(default_cmds.MuxCommand):
# we have code access to this type.
oldcode = None
if codetype in evlang_scripts:
oldcode = str(evlang_scripts[codetype][0])
oldcode = str(evlang_scripts[codetype][0])
# this updates the database right away too
obj.ndb.evlang.add(codetype, codestring, scripter=caller)
if oldcode:

View file

@ -98,19 +98,25 @@ _LOGGER = None
# specifically forbidden symbols
_EV_UNALLOWED_SYMBOLS = ["attr", "attributes", "delete"]
try: _EV_UNALLOWED_SYMBOLS.expand(settings.EVLANG_UNALLOWED_SYMBOLS)
except AttributeError: pass
try:
_EV_UNALLOWED_SYMBOLS.expand(settings.EVLANG_UNALLOWED_SYMBOLS)
except AttributeError:
pass
# safe methods (including self in args) to make available on
# the evl object
_EV_SAFE_METHODS = {}
try: _EV_SAFE_METHODS.update(settings.EVLANG_SAFE_METHODS)
except AttributeError: pass
try:
_EV_SAFE_METHODS.update(settings.EVLANG_SAFE_METHODS)
except AttributeError:
pass
# symbols to make available directly in code
_EV_SAFE_CONTEXT = {"testvar": "This is a safe var!"}
try: _EV_SAFE_CONTEXT.update(settings.EVLANG_SAFE_CONTEXT)
except AttributeError: pass
try:
_EV_SAFE_CONTEXT.update(settings.EVLANG_SAFE_CONTEXT)
except AttributeError:
pass
#------------------------------------------------------------
@ -146,8 +152,9 @@ class Evl(object):
"""
# must do it this way since __dict__ is restricted
members = [mtup for mtup in inspect.getmembers(Evl, predicate=inspect.ismethod)
if not mtup[0].startswith("_")]
string = "\n".join(["{w%s{n\n %s" % (mtup[0], mtup[1].func_doc.strip()) for mtup in members])
if not mtup[0].startswith("_")]
string = "\n".join(["{w%s{n\n %s" % (mtup[0], mtup[1].func_doc.strip())
for mtup in members])
return string
def msg(self, string, obj=None):
@ -200,18 +207,24 @@ class Evl(object):
errobj = kwargs["errobj"]
del kwargs["errobj"]
# set up some callbacks for delayed execution
def errback(f, errobj):
"error callback"
if errobj:
try: f = f.getErrorMessage()
except: pass
try:
f = f.getErrorMessage()
except:
pass
errobj.msg("EVLANG delay error: " + str(f))
def runfunc(func, *args, **kwargs):
"threaded callback"
threads.deferToThread(func, *args, **kwargs).addErrback(errback, errobj)
# get things going
if seconds <= 120:
task.deferLater(reactor, seconds, runfunc, function, *args, **kwargs).addErrback(errback, errobj)
else:
raise EvlangError("delay() can only delay for a maximum of 120 seconds (got %ss)." % seconds )
raise EvlangError("delay() can only delay for a maximum of 120 seconds (got %ss)." % seconds)
return True
def attr(self, obj, attrname=None, value=None, delete=False):
@ -244,6 +257,7 @@ class EvlangError(Exception):
"Error for evlang handler"
pass
class Evlang(object):
"""
This is a handler for launching limited execution Python scripts.
@ -265,17 +279,23 @@ class Evlang(object):
assumed to be granted.
"""
def __init__(self, obj=None, scripts=None, storage_attr="evlang_scripts", safe_context=None, safe_timeout=2):
def __init__(self, obj=None, scripts=None, storage_attr="evlang_scripts",
safe_context=None, safe_timeout=2):
"""
Setup of the Evlang handler.
Input:
obj - a reference to the object this handler is defined on. If not set, handler will operate stand-alone.
scripts = dictionary {scriptname, (codestring, callerobj), ...} where callerobj can be None.
evlang_storage_attr - if obj is given, will look for a dictionary {scriptname, (codestring, callerobj)...}
stored in this given attribute name on that object.
safe_funcs - dictionary of {funcname:funcobj, ...} to make available for the execution environment
safe_timeout - the time we let a script run. If it exceeds this time, it will be blocked from running again.
obj - a reference to the object this handler is defined on. If not
set, handler will operate stand-alone.
scripts = dictionary {scriptname, (codestring, callerobj), ...}
where callerobj can be Noneevlang_storage_attr - if obj
is given, will look for a dictionary
{scriptname, (codestring, callerobj)...}
stored in this given attribute name on that object.
safe_funcs - dictionary of {funcname:funcobj, ...} to make available
for the execution environment
safe_timeout - the time we let a script run. If it exceeds this
time, it will be blocked from running again.
"""
self.obj = obj
@ -286,7 +306,7 @@ class Evlang(object):
self.evlang_scripts.update(scripts)
if self.obj:
self.evlang_scripts.update(obj.attributes.get(storage_attr))
self.safe_context = _EV_SAFE_CONTEXT # set by default + settings
self.safe_context = _EV_SAFE_CONTEXT # set by default + settings
if safe_context:
self.safe_context.update(safe_context)
self.timedout_codestrings = []
@ -322,12 +342,16 @@ class Evlang(object):
_LOGGER.log_errmsg("EVLANG time exceeded: caller: %s, scripter: %s, code: %s" % (caller, scripter, codestring))
if not self.msg(err, scripter, caller):
raise EvlangError(err)
def errback(f):
"We need an empty errback, to catch the traceback of defer.cancel()"
pass
return task.deferLater(reactor, timeout, alarm, codestring).addErrback(errback)
def stop_timer(self, _, deferred):
"Callback for stopping a previously started timer. Cancels the given deferred."
"""Callback for stopping a previously started timer.
Cancels the given deferred.
"""
deferred.cancel()
@inlineCallbacks
@ -337,7 +361,8 @@ class Evlang(object):
codestring - the actual code to execute.
scripter - the creator of the script. Preferentially sees error messages
caller - the object triggering the script - sees error messages if no scripter is given
caller - the object triggering the script - sees error messages if
no scripter is given
"""
# catching previously detected long-running code
@ -391,7 +416,6 @@ class Evlang(object):
# execute code
self.run(codestring, caller, scripter)
def add(self, scriptname, codestring, scripter=None):
"""
Add a new script to the handler. This will also save the
@ -401,7 +425,8 @@ class Evlang(object):
self.evlang_scripts[scriptname] = (codestring, scripter)
if self.obj:
# save to database
self.obj.attributes.add(self.evlang_storage_attr, self.evlang_scripts)
self.obj.attributes.add(self.evlang_storage_attr,
self.evlang_scripts)
def delete(self, scriptname):
"""
@ -411,7 +436,8 @@ class Evlang(object):
del self.evlang_scripts[scriptname]
if self.obj:
# update change to database
self.obj.attributes.add(self.evlang_storage_attr, self.evlang_scripts)
self.obj.attributes.add(self.evlang_storage_attr,
self.evlang_scripts)
#----------------------------------------------------------------------
@ -436,8 +462,6 @@ class Evlang(object):
# to create an infinite loop.
#----------------------------------------------------------------------
#----------------------------------------------------------------------
# Module globals.
#----------------------------------------------------------------------
@ -555,25 +579,31 @@ UNALLOWED_BUILTINS = set([
# in with new unsafe things
SAFE_BUILTINS = set([
'False', 'None', 'True', 'abs', 'all', 'any', 'apply', 'basestring',
'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr', 'classmethod',
'bin', 'bool', 'buffer', 'bytearray', 'bytes', 'callable', 'chr',
'classmethod',
'cmp', 'coerce', 'complex', 'dict', 'divmod', 'enumerate', 'filter',
'float', 'format', 'frozenset', 'hash', 'hex', 'id', 'int',
'isinstance', 'issubclass', 'iter', 'len', 'list', 'long', 'map', 'max', 'min',
'next', 'object', 'oct', 'ord', 'pow', 'print', 'property', 'range', 'reduce',
'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'staticmethod', 'str',
'sum', 'tuple', 'unichr', 'unicode', 'xrange', 'zip' ])
'isinstance', 'issubclass', 'iter', 'len', 'list', 'long', 'map',
'max', 'min',
'next', 'object', 'oct', 'ord', 'pow', 'print', 'property', 'range',
'reduce',
'repr', 'reversed', 'round', 'set', 'slice', 'sorted', 'staticmethod',
'str',
'sum', 'tuple', 'unichr', 'unicode', 'xrange', 'zip'])
for ast_name in UNALLOWED_AST_NODES:
assert(is_valid_ast_node(ast_name))
for name in UNALLOWED_BUILTINS:
assert(is_valid_builtin(name))
def _cross_match_whitelist():
"check the whitelist's completeness"
available = ALL_BUILTINS - UNALLOWED_BUILTINS
diff = available.difference(SAFE_BUILTINS)
assert not diff, diff # check so everything not disallowed is in safe
assert not diff, diff # check so everything not disallowed is in safe
diff = SAFE_BUILTINS.difference(available)
assert not diff, diff # check so everything everything in safe is in not-disallowed
assert not diff, diff # check so everything in safe is in not-disallowed
_cross_match_whitelist()
def is_unallowed_ast_node(kind):
@ -595,6 +625,7 @@ UNALLOWED_ATTR = [
'f_exc_type', 'f_exc_value', 'f_globals', 'f_locals']
UNALLOWED_ATTR.extend(_EV_UNALLOWED_SYMBOLS)
def is_unallowed_attr(name):
return (name[:2] == '__' and name[-2:] == '__') or \
(name in UNALLOWED_ATTR)
@ -614,19 +645,26 @@ class LimitedExecError(object):
"""
def __init__(self, errmsg, lineno):
self.errmsg, self.lineno = errmsg, lineno
def __str__(self):
return "line %d : %s" % (self.lineno, self.errmsg)
class LimitedExecASTNodeError(LimitedExecError):
"Expression/statement in AST evaluates to a restricted AST node type."
pass
class LimitedExecBuiltinError(LimitedExecError):
"Expression/statement in tried to access a restricted builtin."
pass
class LimitedExecAttrError(LimitedExecError):
"Expression/statement in tried to access a restricted attribute."
pass
class LimitedExecVisitor(object):
"""
Data-driven visitor which walks the AST for some code and makes
@ -672,7 +710,8 @@ class LimitedExecVisitor(object):
def visit(self, node, *args):
"Recursively validate node and all of its children."
fn = getattr(self, 'visit' + classname(node))
if DEBUG: self.trace(node)
if DEBUG:
self.trace(node)
fn(node, *args)
for child in node.getChildNodes():
self.visit(child, *args)
@ -682,10 +721,10 @@ class LimitedExecVisitor(object):
name = node.getChildren()[0]
lineno = get_node_lineno(node)
if is_unallowed_builtin(name):
self.errors.append(LimitedExecBuiltinError( \
self.errors.append(LimitedExecBuiltinError(
"access to builtin '%s' is denied" % name, lineno))
elif is_unallowed_attr(name):
self.errors.append(LimitedExecAttrError( \
self.errors.append(LimitedExecAttrError(
"access to attribute '%s' is denied" % name, lineno))
def visitGetattr(self, node, *args):
@ -696,10 +735,10 @@ class LimitedExecVisitor(object):
except Exception:
name = ""
lineno = get_node_lineno(node)
if attrname == 'attr' and name =='evl':
if attrname == 'attr' and name == 'evl':
pass
elif is_unallowed_attr(attrname):
self.errors.append(LimitedExecAttrError( \
self.errors.append(LimitedExecAttrError(
"access to attribute '%s' is denied" % attrname, lineno))
def visitAssName(self, node, *args):
@ -710,8 +749,8 @@ class LimitedExecVisitor(object):
def visitPower(self, node, *args):
"Make sure power-of operations don't get too big"
if node.left.value > 1000000 or node.right.value > 10:
lineno = get_node_lineno(node)
self.errors.append(LimitedExecAttrError( \
lineno = get_node_lineno(node)
self.errors.append(LimitedExecAttrError(
"power law solution too big - restricted", lineno))
def ok(self, node, *args):
@ -721,7 +760,7 @@ class LimitedExecVisitor(object):
def fail(self, node, *args):
"Default callback for unallowed AST nodes."
lineno = get_node_lineno(node)
self.errors.append(LimitedExecASTNodeError( \
self.errors.append(LimitedExecASTNodeError(
"execution of '%s' statements is denied" % classname(node),
lineno))
@ -732,6 +771,7 @@ class LimitedExecVisitor(object):
if attr[:2] != '__':
print ' ' * 4, "%-15.15s" % attr, getattr(node, attr)
#----------------------------------------------------------------------
# Safe 'eval' replacement.
#----------------------------------------------------------------------
@ -740,6 +780,7 @@ class LimitedExecException(Exception):
"Base class for all safe-eval related errors."
pass
class LimitedExecCodeException(LimitedExecException):
"""
Exception class for reporting all errors which occured while
@ -754,6 +795,7 @@ class LimitedExecCodeException(LimitedExecException):
def __str__(self):
return '\n'.join([str(err) for err in self.errors])
class LimitedExecContextException(LimitedExecException):
"""
Exception class for reporting unallowed objects found in the dict
@ -769,6 +811,7 @@ class LimitedExecContextException(LimitedExecException):
def __str__(self):
return '\n'.join([str(err) for err in self.errors])
class LimitedExecTimeoutException(LimitedExecException):
"""
Exception class for reporting that code evaluation execeeded
@ -798,6 +841,7 @@ def validate_context(context):
raise LimitedExecContextException(ctx_errkeys, ctx_errors)
return True
def validate_code(codestring):
"validate a code string"
# prepare the code tree for checking
@ -809,6 +853,7 @@ def validate_code(codestring):
raise LimitedExecCodeException(codestring, checker.errors)
return True
def limited_exec(code, context = {}, timeout_secs=2, retobj=None, procpool_async=None):
"""
Validate source code and make sure it contains no unauthorized
@ -824,8 +869,8 @@ def limited_exec(code, context = {}, timeout_secs=2, retobj=None, procpool_async
retobj - only used if procpool_async is also given. Defines an Object
(which must define a msg() method), for receiving returns from
the execution.
procpool_async - a run_async function alternative to the one in src.utils.utils.
this must accept the keywords
procpool_async - a run_async function alternative to the one in
src.utils.utils. This must accept the keywords
proc_timeout (will be set to timeout_secs
at_return - a callback
at_err - an errback
@ -842,7 +887,10 @@ def limited_exec(code, context = {}, timeout_secs=2, retobj=None, procpool_async
if retobj:
callback = lambda r: retobj.msg(r)
errback = lambda e: retobj.msg(e)
procpool_async(code, *context, proc_timeout=timeout_secs, at_return=callback, at_err=errback)
procpool_async(code, *context,
proc_timeout=timeout_secs,
at_return=callback,
at_err=errback)
else:
procpool_async(code, *context, proc_timeout=timeout_secs)
else:
@ -864,41 +912,41 @@ class TestLimitedExec(unittest.TestCase):
def test_getattr(self):
# attempt to get arround direct attr access
self.assertRaises(LimitedExecException, \
self.assertRaises(LimitedExecException,
limited_exec, "getattr(int, '__abs__')")
def test_func_globals(self):
# attempt to access global enviroment where fun was defined
self.assertRaises(LimitedExecException, \
self.assertRaises(LimitedExecException,
limited_exec, "def x(): pass; print x.func_globals")
def test_lowlevel(self):
# lowlevel tricks to access 'object'
self.assertRaises(LimitedExecException, \
self.assertRaises(LimitedExecException,
limited_exec, "().__class__.mro()[1].__subclasses__()")
def test_timeout_ok(self):
# attempt to exectute 'slow' code which finishes within timelimit
def test(): time.sleep(2)
env = {'test':test}
limited_exec("test()", env, timeout_secs = 5)
env = {'test': test}
limited_exec("test()", env, timeout_secs=5)
def test_timeout_exceed(self):
# attempt to exectute code which never teminates
self.assertRaises(LimitedExecException, \
self.assertRaises(LimitedExecException,
limited_exec, "while 1: pass")
def test_invalid_context(self):
# can't pass an enviroment with modules or builtins
env = {'f' : __builtins__.open, 'g' : time}
self.assertRaises(LimitedExecException, \
env = {'f': __builtins__.open, 'g': time}
self.assertRaises(LimitedExecException,
limited_exec, "print 1", env)
def test_callback(self):
# modify local variable via callback
self.value = 0
def test(): self.value = 1
env = {'test':test}
env = {'test': test}
limited_exec("test()", env)
self.assertEqual(self.value, 1)

View file

@ -53,13 +53,16 @@ if applicable. An extended @desc command is used to set details.
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.
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)
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!
"""
@ -90,16 +93,18 @@ REGEXMAP = {"morning": (RE_MORNING, RE_AFTERNOON, RE_EVENING, RE_NIGHT),
# 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)
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)
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.
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."
@ -107,10 +112,12 @@ class ExtendedRoom(Room):
self.db.summer_desc = ""
self.db.autumn_desc = ""
self.db.winter_desc = ""
# the general desc is used as a fallback if a given seasonal one is not set
# the general desc is used as a fallback if a seasonal one is not set
self.db.general_desc = ""
self.db.raw_desc = "" # will be set dynamically. Can contain raw timeslot codes
self.db.desc = "" # this will be set dynamically at first look. Parsed for timeslot codes
# 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
@ -122,28 +129,37 @@ class ExtendedRoom(Room):
Calculate the current time and season ids
"""
# get the current time as parts of year and parts of day
time = gametime.gametime(format=True) # returns a tuple (years,months,weeks,days,hours,minutes,sec)
# 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 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"
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.
Filter so that only time markers <timeslot>...</timeslot> of the
correct timeslot remains in the description.
"""
if raw_desc:
regextuple = REGEXMAP[curr_time]
@ -158,8 +174,9 @@ class ExtendedRoom(Room):
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.
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.
"""
@ -188,10 +205,14 @@ class ExtendedRoom(Room):
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 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:
@ -207,14 +228,16 @@ class ExtendedRoom(Room):
update = True
if update:
# if anything changed we have to re-parse the raw_desc for time markers
# 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.
# Custom Look command supporting Room details. Add this to
# the Default cmdset to use.
class CmdExtendedLook(default_cmds.CmdLook):
"""
@ -237,7 +260,8 @@ class CmdExtendedLook(default_cmds.CmdLook):
if args:
looking_at_obj = caller.search(args, use_nicks=True, ignore_errors=True)
if not looking_at_obj:
# no object found. Check if there is a matching detail at location.
# 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)
@ -269,7 +293,8 @@ class CmdExtendedLook(default_cmds.CmdLook):
looking_at_obj.at_desc(looker=caller)
# Custom build commands for setting seasonal descriptions and detailing extended rooms.
# Custom build commands for setting seasonal descriptions
# and detailing extended rooms.
class CmdExtendedDesc(default_cmds.CmdDesc):
"""
@ -358,7 +383,10 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
string += " {wgeneral:{n %s" % location.db.general_desc
caller.msg(string)
return
if self.switches and self.switches[0] in ("spring", "summer", "autumn", "winter"):
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.")
@ -367,10 +395,14 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
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
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)
@ -384,9 +416,10 @@ class CmdExtendedDesc(default_cmds.CmdDesc):
else:
text = self.args
obj = location
obj.db.desc = self.rhs # this is set as a compatability fallback
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
# 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)

View file

@ -26,6 +26,7 @@ CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
RE_GROUP = re.compile(r"\".*?\"|\'.*?\'|\S*")
class CmdEditorBase(Command):
"""
Base parent for editor commands
@ -44,7 +45,8 @@ class CmdEditorBase(Command):
: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>
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
"""
@ -63,7 +65,8 @@ class CmdEditorBase(Command):
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.
# we want to clean the quotes, but only one type,
# in case we are nesting.
if arg.startswith('"'):
arg.strip('"')
elif arg.startswith("'"):
@ -71,7 +74,6 @@ class CmdEditorBase(Command):
temp.append(arg)
arglist = temp
# A dumb split, without grouping quotes
words = self.args.split()
@ -106,8 +108,8 @@ class CmdEditorBase(Command):
else:
lstr = "lines %i-%i" % (lstart + 1, lend)
# arg1 and arg2 is whatever arguments. Line numbers or -ranges are never included here.
# arg1 and arg2 is whatever arguments. Line numbers or -ranges are
# never included here.
args = " ".join(arglist)
arg1, arg2 = "", ""
if len(arglist) > 1:
@ -141,6 +143,7 @@ class CmdLineInput(CmdEditorBase):
"""
key = CMD_NOMATCH
aliases = [CMD_NOINPUT]
def func(self):
"Adds the line without any formatting changes."
# add a line of text
@ -150,9 +153,11 @@ class CmdLineInput(CmdEditorBase):
buf = self.editor.buffer + "\n%s" % self.args
self.editor.update_buffer(buf)
if self.editor.echo_mode:
cline = len(self.editor.buffer.split('\n')) # need to do it here or we will be off one line
# 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
@ -165,8 +170,9 @@ class CmdEditorGroup(CmdEditorBase):
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.
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
@ -187,7 +193,9 @@ class CmdEditorGroup(CmdEditorBase):
# 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)
string = editor.display_buffer(buf=buf,
offset=lstart,
linenums=False)
else:
string = editor.display_buffer(linenums=False)
self.caller.msg(string, raw=True)
@ -242,7 +250,7 @@ class CmdEditorGroup(CmdEditorBase):
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Removed %s for lines %i-%i." % (self.arg1, lstart + 1 , lend + 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])
@ -279,7 +287,7 @@ class CmdEditorGroup(CmdEditorBase):
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:]
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":
@ -308,7 +316,8 @@ class CmdEditorGroup(CmdEditorBase):
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
# :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:
@ -376,6 +385,7 @@ class EditorCmdSet(CmdSet):
key = "editorcmdset"
mergetype = "Replace"
class LineEditor(object):
"""
This defines a line editor object. It creates all relevant commands
@ -391,17 +401,21 @@ class LineEditor(object):
"""
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 - 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 - 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 - 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)
key = an optional key for naming this session (such as which attribute
is being edited)
"""
self.key = key
self.caller = caller
@ -420,13 +434,12 @@ class LineEditor(object):
# 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)
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
@ -494,8 +507,9 @@ class LineEditor(object):
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.
# 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:
@ -555,7 +569,7 @@ class LineEditor(object):
"""
Shows the help entry for the editor.
"""
string = self.sep*78 + """
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
@ -578,7 +592,7 @@ class LineEditor(object):
: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 be shifted down
: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>
@ -627,7 +641,8 @@ class CmdEditor(Command):
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.objname, self.attrname = [part.strip()
for part in self.args.split("/", 1)]
self.obj = self.caller.search(self.objname)
if not self.obj:
return
@ -636,20 +651,28 @@ class CmdEditor(Command):
def load_attr():
"inital loading of buffer data from given attribute."
target = self.obj.attributes.get(self.attrname)
if target != None and not isinstance(target, basestring):
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."
"""
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)
self.editor = LineEditor(self.caller,
loadfunc=load_attr,
savefunc=save_attr,
quitfunc=quit_hook,
key=editor_key)

View file

@ -46,8 +46,9 @@ 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.
# 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
@ -63,10 +64,12 @@ class CmdBackToStart(Command):
"""
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
@ -74,6 +77,7 @@ class CmdUsernameSelect(Command):
"""
key = CMD_NOMATCH
locks = "cmd:all()"
def func(self):
"Execute the command"
player = managers.players.get_player_from_name(self.args)
@ -81,9 +85,11 @@ class CmdUsernameSelect(Command):
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:
self.menutree.player = player # store the player so next step can find it
# store the player so next step can find it
self.menutree.player = player
self.menutree.goto("node1b")
# Menu entry 1b - Entering a Password
class CmdPasswordSelectBack(Command):
@ -92,10 +98,12 @@ class CmdPasswordSelectBack(Command):
"""
key = CMD_NOINPUT
locks = "cmd:all()"
def func(self):
"Execute the command"
self.menutree.goto("node1a")
class CmdPasswordSelect(Command):
"""
Handles the entering of a password and logs into the game.
@ -117,9 +125,10 @@ class CmdPasswordSelect(Command):
# before going on, check eventual bans
bans = managers.serverconfigs.conf("server_bans")
if bans and (any(tup[0]==player.name for tup in bans)
if bans and (any(tup[0] == player.name for tup in bans)
or
any(tup[2].match(player.sessions[0].address[0]) for tup in bans if tup[2])):
any(tup[2].match(player.sessions[0].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"
@ -142,6 +151,7 @@ class CmdPasswordSelect(Command):
# we have no character yet; use player's look, if it exists
player.execute_cmd("look")
# Menu entry 2a - Creating a Username
class CmdUsernameCreate(Command):
@ -169,16 +179,19 @@ its and @/./+/-/_ only.{n") # this echoes the restrictions made by django's auth
self.menutree.playername = playername
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.menutree.goto("node2a")
class CmdPasswordCreate(Command):
"Handle the creation of a password. This also creates the actual Player/User object."
key = CMD_NOMATCH
@ -195,12 +208,14 @@ class CmdPasswordCreate(Command):
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 long, "
string += "avoid making it a real word and mix numbers into it.{n"
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.
# 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
@ -227,8 +242,9 @@ class CmdPasswordCreate(Command):
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.
# 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())
@ -259,6 +275,8 @@ LOGIN_SCREEN_HELP = \
(return to go back)""" % settings.SERVERNAME
# Menu entry 4
class CmdUnloggedinQuit(Command):
@ -285,7 +303,7 @@ START = MenuNode("START", text=utils.string_from_module(CONNECTION_SCREEN_MODULE
linktexts=["Log in with an existing account",
"Create a new account",
"Help",
"Quit",],
"Quit"],
selectcmds=[None, None, None, CmdUnloggedinQuit])
node1a = MenuNode("node1a", text="Please enter your account name (empty to abort).",
@ -325,13 +343,17 @@ class UnloggedInCmdSet(CmdSet):
"Cmdset for the unloggedin state"
key = "UnloggedinState"
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..
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"]
@ -339,5 +361,7 @@ class CmdUnloggedinLook(Command):
def func(self):
"Execute the menu"
menu = MenuTree(self.caller, nodes=(START, node1a, node1b, node2a, node2b, node3), exec_end=None)
menu = MenuTree(self.caller, nodes=(START, node1a, node1b,
node2a, node2b, node3),
exec_end=None)
menu.start()

View file

@ -65,6 +65,7 @@ class CmdMenuNode(Command):
else:
self.caller.msg("{rThis option is not available.{n")
class CmdMenuLook(default_cmds.CmdLook):
"""
ooc look
@ -92,6 +93,7 @@ class CmdMenuLook(default_cmds.CmdLook):
# otherwise we use normal look
super(CmdMenuLook, self).func()
class CmdMenuHelp(default_cmds.CmdHelp):
"""
help
@ -118,6 +120,7 @@ class CmdMenuHelp(default_cmds.CmdHelp):
# otherwise we use normal help
super(CmdMenuHelp, self).func()
class MenuCmdSet(CmdSet):
"""
Cmdset for the menu. Will replace all other commands.
@ -129,10 +132,12 @@ class MenuCmdSet(CmdSet):
key = "menucmdset"
priority = 1
mergetype = "Replace"
def at_cmdset_creation(self):
"populate cmdset"
pass
#
# Menu Node system
#
@ -153,7 +158,8 @@ class MenuTree(object):
'START' and 'END' respectively.
"""
def __init__(self, caller, nodes=None, startnode="START", endnode="END", exec_end="look"):
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
@ -194,7 +200,7 @@ class MenuTree(object):
# 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 != None:
if self.exec_end is not None:
self.caller.execute_cmd(self.exec_end)
return
# not exiting, look for a valid code.
@ -205,13 +211,14 @@ class MenuTree(object):
# node. self.caller is available at this point.
try:
exec(node.code)
except Exception, e:
except Exception:
self.caller.msg("{rCode could not be executed for node %s. Continuing anyway.{n" % key)
# 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)}
self.caller.db._menu_data = {"help": node.helptext,
"look": str(node.text)}
# display the node
self.caller.msg(node.text)
else:
@ -226,27 +233,39 @@ class MenuNode(object):
"""
def __init__(self, key, text="", links=None, linktexts=None,
keywords=None, cols=1, helptext=None, selectcmds=None, code="", nodefaultcmds=False, separator=""):
keywords=None, cols=1, helptext=None,
selectcmds=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 be
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.
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. 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.
nodefaultcmds - if true, don't offer the default help and look commands in the node
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. 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.
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 nodes5B.
"""
self.key = key
@ -311,13 +330,14 @@ class MenuNode(object):
break
ftable = utils.format_table(cols)
for row in ftable:
string +="\n" + "".join(row)
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.
Called by menu tree. Initializes the commands needed by
the menutree structure.
"""
# Create the relevant cmdset
self.cmdset = MenuCmdSet()
@ -362,7 +382,8 @@ def prompt_yesno(caller, question="", yescode="", nocode="", default="N"):
cmdyes = CmdMenuNode()
cmdyes.key = "yes"
cmdyes.aliases = ["y"]
# this will be executed in the context of the yes command (so self.caller will be available)
# this will be executed in the context of the yes command (so
# self.caller will be available)
cmdyes.code = yescode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
cmdno = CmdMenuNode()
@ -387,8 +408,8 @@ def prompt_yesno(caller, question="", yescode="", nocode="", default="N"):
yesnocmdset.add(defaultcmd)
# assinging menu data flags to caller.
caller.db._menu_data = {"help":"Please select Yes or No.",
"look":"Please select Yes or No."}
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":
@ -398,6 +419,7 @@ def prompt_yesno(caller, question="", yescode="", nocode="", default="N"):
prompt = "%s %s: " % (question, prompt)
caller.msg(prompt)
#
# Menu command test
#
@ -419,6 +441,7 @@ class CmdMenuTest(Command):
key = "menu"
locks = "cmd:all()"
help_category = "Menu"
def func(self):
"Testing the menu system"

View file

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

View file

@ -36,6 +36,7 @@ from src.utils.utils import clean_object_caches, to_str
from src.utils import logger
from src import PROC_MODIFIED_OBJS
#
# Multiprocess command for communication Server<->Client, relaying
# data for remote Python execution
@ -64,6 +65,7 @@ class ExecuteCode(amp.Command):
response = [('response', amp.String()),
('recached', amp.String())]
#
# Multiprocess AMP client-side factory, for executing remote Python code
#
@ -118,8 +120,7 @@ class PythonProcPoolChild(AMPChild):
return ""
_return = Ret()
available_vars = {'_return':_return}
available_vars = {'_return': _return}
if environment:
# load environment
try:
@ -141,7 +142,8 @@ class PythonProcPoolChild(AMPChild):
# get the list of affected objects to recache
objs = list(set(PROC_MODIFIED_OBJS))
# 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]))
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, [], ()):
@ -156,13 +158,15 @@ class PythonProcPoolChild(AMPChild):
#
# Procpool run_async - Server-side access function for executing code in another process
# 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.
@ -227,9 +231,9 @@ def run_async(to_execute, *args, **kwargs):
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").
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
@ -243,7 +247,7 @@ def run_async(to_execute, *args, **kwargs):
# get the procpool name, if set in kwargs
procpool_name = kwargs.get("procpool_name", "PythonProcPool")
if _PPOOL == None:
if _PPOOL is None:
# Try to load process Pool
from src.server.sessionhandler import SESSIONS as _SESSIONS
try:
@ -260,8 +264,10 @@ def run_async(to_execute, *args, **kwargs):
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
if f:
return f(rval, *args, **kwargs)
else:
return rval
return func
def convert_err(f):
def func(err, *args, **kwargs):
@ -287,18 +293,22 @@ def run_async(to_execute, *args, **kwargs):
# process pool is running
if isinstance(to_execute, basestring):
# run source code in process pool
cmdargs = {"_timeout":use_timeout}
cmdargs = {"_timeout": use_timeout}
cmdargs["source"] = to_str(to_execute)
if kwargs: cmdargs["environment"] = do_pickle(to_pickle(kwargs))
else: cmdargs["environment"] = ""
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 = {"_timeout": use_timeout}
cmdargs["source"] = "_return(%s(*args,**kwargs))" % callname
cmdargs["environment"] = do_pickle(to_pickle({callname:to_execute, "args":args, "kwargs":kwargs}))
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)

View file

@ -62,6 +62,7 @@ PROCPOOL_DIRECTORY = None
# don't need to change normally
SERVICE_NAME = "PythonProcPool"
# plugin hook
def start_plugin_services(server):
@ -87,8 +88,8 @@ def start_plugin_services(server):
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}
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:

View file

@ -54,8 +54,9 @@ class CmdTalk(default_cmds.MuxCommand):
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 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))
@ -67,9 +68,11 @@ class CmdTalk(default_cmds.MuxCommand):
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())
@ -79,33 +82,34 @@ class TalkingCmdSet(CmdSet):
# (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.",
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.",
"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},
"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
@ -118,4 +122,4 @@ class TalkingNPC(Object):
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)
self.cmdset.add_default(TalkingCmdSet, permanent=True)

View file

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

View file

@ -14,6 +14,7 @@ from contrib.tutorial_world import scripts as tut_scripts
BASE_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS
#------------------------------------------------------------
#
# Mob - mobile object
@ -52,15 +53,18 @@ class Mob(tut_objects.TutorialObject):
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")]
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]
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.
# 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)
@ -100,7 +104,8 @@ class AttackTimer(Script):
"Called every self.interval seconds."
if self.obj.db.inactive:
return
#print "attack timer: at_repeat", self.dbobj.id, self.ndb.twisted_task, id(self.ndb.twisted_task)
#print "attack timer: at_repeat", self.dbobj.id, self.ndb.twisted_task,
# id(self.ndb.twisted_task)
if self.obj.db.roam_mode:
self.obj.roam()
#return
@ -119,10 +124,12 @@ class AttackTimer(Script):
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.
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
@ -133,12 +140,16 @@ class Enemy(Mob):
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
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.
respawn_text - text to echo to room when the mob is reset/respawn in
that room.
"""
def at_object_creation(self):
@ -157,7 +168,8 @@ class Enemy(Mob):
self.db.health = 20
self.db.dead_at = time.time()
self.db.dead_timer = 100 # how long to stay dead
self.db.inactive = True # this is used during creation to make sure the mob doesn't move away
# 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
@ -185,10 +197,12 @@ class Enemy(Mob):
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")]
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]
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
@ -224,7 +238,8 @@ class Enemy(Mob):
# analyze result.
if target.db.health <= 0:
# we reduced enemy to 0 health. Whisp them off to the prison room.
# 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:
@ -235,7 +250,8 @@ class Enemy(Mob):
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)
# 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:
@ -246,7 +262,8 @@ class Enemy(Mob):
self.roam_mode = False
self.pursue_mode = True
else:
# no players found, this could mean they have fled. Switch to pursue mode.
# 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
@ -259,20 +276,24 @@ class Enemy(Mob):
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.
# 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.
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)]:
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.
@ -335,7 +356,8 @@ class Enemy(Mob):
string += "You fear it's only a matter of time before it materializes somewhere again."
self.location.msg_contents(string, exclude=[attacker])
# put enemy in dead mode and hide it from view. AttackTimer will bring it back later.
# 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
@ -347,7 +369,9 @@ class Enemy(Mob):
return False
def reset(self):
"If the mob was 'dead', respawn it to its home position and reset all modes and damage."
"""
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
@ -358,4 +382,4 @@ class Enemy(Mob):
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)
self.location.msg_contents(string)

View file

@ -19,9 +19,10 @@ WeaponRack
"""
import time, random
import time
import random
from ev import utils, create_object
from ev import create_object
from ev import Object, Exit, Command, CmdSet, Script
#------------------------------------------------------------
@ -91,12 +92,14 @@ class CmdRead(Command):
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.
@ -147,6 +150,7 @@ class CmdClimb(Command):
self.caller.msg(ostring)
self.caller.db.last_climbed = self.obj
class CmdSetClimbable(CmdSet):
"Climbing cmdset"
def at_cmdset_creation(self):
@ -182,6 +186,7 @@ OBELISK_DESCS = ["You can briefly make out the image of {ba woman with a blue bi
"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.
@ -196,7 +201,7 @@ class Obelisk(TutorialObject):
def return_appearance(self, caller):
"Overload the default version of this hook."
clueindex = random.randint(0, len(OBELISK_DESCS)-1)
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. "
@ -206,6 +211,7 @@ class Obelisk(TutorialObject):
# call the parent function as normal (this will use db.desc we just set)
return super(Obelisk, self).return_appearance(caller)
#------------------------------------------------------------
#
# LightSource
@ -237,6 +243,7 @@ class StateLightSourceOn(Script):
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()
@ -262,13 +269,14 @@ class StateLightSourceOn(Script):
"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.
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
help_category = "TutorialWorld"
def func(self):
@ -283,18 +291,19 @@ class CmdLightSourceOn(Command):
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])
# we run script validation on the room to make light/dark states tick.
# 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.
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
help_category = "TutorialWorld"
def func(self):
@ -311,38 +320,39 @@ class CmdLightSourceOff(Command):
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")
# we run script validation on the room to make light/dark states tick.
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.
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.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.
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
@ -360,10 +370,11 @@ class LightSource(TutorialObject):
# maybe it was dropped, try validating at current location.
try:
self.location.scripts.validate()
except AttributeError,e:
except AttributeError:
pass
self.delete()
#------------------------------------------------------------
#
# Crumbling wall - unique exit
@ -473,7 +484,7 @@ class CmdShiftRoot(Command):
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)
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
@ -502,13 +513,15 @@ class CmdShiftRoot(Command):
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"]
locks = "cmd:attr(crumbling_wall_found_button) and not locattr(is_dark)" # only accessible if the button was found and there is light.
# 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):
@ -534,14 +547,17 @@ class CmdPressButton(Command):
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, Exit):
"""
The CrumblingWall can be examined in various
@ -559,24 +575,28 @@ class CrumblingWall(TutorialObject, Exit):
"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.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".
# 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 never any other identical value.
self.db.root_pos = {"yellow":0, "green":0, "red":0, "blue":0}
# 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"
@ -598,7 +618,10 @@ class CrumblingWall(TutorialObject, Exit):
return string
def return_appearance(self, caller):
"This is called when someone looks at the wall. We need to echo the current root positions."
"""
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 "
@ -615,7 +638,10 @@ class CrumblingWall(TutorialObject, Exit):
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."
"""
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()
@ -625,11 +651,14 @@ class CrumblingWall(TutorialObject, 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."
"""
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.
# 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:
@ -641,6 +670,7 @@ class CrumblingWall(TutorialObject, Exit):
self.db.root_pos = start_pos[random.randint(0, 4)]
self.destination = None
#------------------------------------------------------------
#
# Weapon - object type
@ -667,15 +697,17 @@ class CmdAttack(Command):
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.
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.
# 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"]
aliases = ["hit","kill", "fight", "thrust", "pierce", "stab",
"slash", "chop", "parry", "defend"]
locks = "cmd:all()"
help_category = "TutorialWorld"
@ -684,7 +716,6 @@ class CmdAttack(Command):
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)
@ -709,15 +740,15 @@ class CmdAttack(Command):
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
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
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)
@ -753,12 +784,14 @@ class CmdAttack(Command):
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.
@ -766,7 +799,8 @@ class Weapon(TutorialObject):
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)
damage - base damage given (modified by hit success and
type of attack) (0-10)
"""
def at_object_creation(self):
@ -779,13 +813,17 @@ class Weapon(TutorialObject):
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."
"""
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
@ -833,18 +871,23 @@ 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.
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.
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"

View file

@ -10,6 +10,7 @@ from ev 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
@ -45,7 +46,7 @@ class CmdTutorial(Command):
caller = self.caller
if not self.args:
target = self.obj # this is the room object the command is defined on
target = self.obj # this is the room the command is defined on
else:
target = caller.search(self.args.strip())
if not target:
@ -56,13 +57,16 @@ class CmdTutorial(Command):
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(Room):
"""
This is the base room type for all rooms in the tutorial world.
@ -78,7 +82,6 @@ class TutorialRoom(Room):
pass
#------------------------------------------------------------
#
# Weather room - scripted room
@ -89,7 +92,6 @@ class TutorialRoom(Room):
#
#------------------------------------------------------------
class WeatherRoom(TutorialRoom):
"""
This should probably better be called a rainy room...
@ -107,6 +109,7 @@ class WeatherRoom(TutorialRoom):
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 = (
@ -122,21 +125,23 @@ class WeatherRoom(TutorialRoom):
"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.
# 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
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)
# LightSource object (LightSource is defined in
# tutorialworld.objects.LightSource)
#
#-----------------------------------------------------------------------------------
#------------------------------------------------------------------------------
class CmdLookDark(Command):
"""
@ -169,13 +174,15 @@ class CmdLookDark(Command):
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)]
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)]
lightsources = [obj for obj in self.obj.contents
if utils.inherits_from(obj, LightSource)]
if lightsources:
lightsource = lightsources[0]
else:
@ -186,6 +193,7 @@ class CmdLookDark(Command):
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.
@ -193,27 +201,34 @@ class CmdDarkHelp(Command):
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.
# the nomatch system command will give a suitable error when we cannot find
# the normal commands.
from src.commands.default.syscommands import CMD_NOMATCH
from src.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
mergetype = "Replace" # completely remove all other commands
def at_cmdset_creation(self):
"populates the cmdset."
self.add(CmdTutorial())
@ -221,6 +236,7 @@ class DarkCmdSet(CmdSet):
self.add(CmdDarkHelp())
self.add(CmdDarkNoMatch())
self.add(CmdSay)
#
# Darkness room two-state system
#
@ -238,6 +254,7 @@ class DarkState(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]:
@ -246,9 +263,11 @@ class DarkState(Script):
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]:
@ -256,23 +275,31 @@ class DarkState(Script):
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.
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."
"""
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
@ -287,20 +314,24 @@ class DarkRoom(TutorialRoom):
"""
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])
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)
# 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."
"""
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)
@ -313,10 +344,14 @@ class DarkRoom(TutorialRoom):
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
"""
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
@ -347,23 +382,27 @@ class TeleportRoom(TutorialRoom):
super(TeleportRoom, self).at_object_creation()
# what character.db.puzzle_clue must be set to, to avoid teleportation.
self.db.puzzle_value = 1
# the target of the success teleportation. Can be a dbref or a unique room name.
# 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."
"""
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
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
teleport_to = self.db.success_teleport_to # this is a room name
results = search_object(teleport_to)
if not results or len(results) > 1:
@ -376,7 +415,7 @@ class TeleportRoom(TutorialRoom):
return
# teleport
character.execute_cmd("look")
character.location = results[0] # stealth move
character.location = results[0] # stealth move
character.location.at_object_receive(character, self)
#------------------------------------------------------------
@ -412,7 +451,8 @@ class CmdEast(Command):
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.
# 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])
@ -423,6 +463,7 @@ class CmdEast(Command):
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):
"""
@ -440,7 +481,8 @@ class CmdWest(Command):
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.
# 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])
@ -451,6 +493,7 @@ class CmdWest(Command):
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.
@ -486,7 +529,8 @@ class CmdLookBridge(Command):
self.caller.msg(message)
# there is a chance that we fall if we are on the western or central part of the bridge.
# 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)
@ -504,6 +548,7 @@ class CmdLookBridge(Command):
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):
"""
@ -521,33 +566,38 @@ class CmdBridgeHelp(Command):
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.
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.
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.
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):
@ -617,8 +667,8 @@ class BridgeRoom(TutorialRoom):
#
# 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.
# This room marks the start of the tutorial. It sets up properties on
# the player char that is needed for the tutorial.
#
#------------------------------------------------------------
@ -652,6 +702,7 @@ class IntroRoom(TutorialRoom):
string += "-"*78
character.msg("{r%s{n" % string)
#------------------------------------------------------------
#
# Outro room - unique room
@ -683,5 +734,6 @@ class OutroRoom(TutorialRoom):
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)]:
for tut_obj in [obj for obj in character.contents
if utils.inherits_from(obj, TutorialObject)]:
tut_obj.reset()

View file

@ -5,6 +5,7 @@ This defines some generally useful scripts for the tutorial world.
import random
from ev import Script
#------------------------------------------------------------
#
# IrregularEvent - script firing at random intervals
@ -28,8 +29,9 @@ class IrregularEvent(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.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
@ -47,11 +49,13 @@ class IrregularEvent(Script):
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
self.interval = 5 # every 5 seconds, 1/5 chance of firing
#------------------------------------------------------------
@ -64,11 +68,13 @@ class FastIrregularEvent(IrregularEvent):
# #
# # 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.
# # 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.
# # 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.
# #
@ -77,7 +83,8 @@ class FastIrregularEvent(IrregularEvent):
# UPDATE_INTERVAL = 60 * 10 # Measured in seconds
# #This is a list of script parent objects that subscribe to the reset functionality.
# #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"]
@ -104,7 +111,4 @@ class FastIrregularEvent(IrregularEvent):
# try:
# obj.scriptlink.reset()
# except:
# logger.log_errmsg(traceback.print_exc())
# logger.log_errmsg(traceback.print_exc())