From 1ae17bcbe43ecd4e93e28cf3b5e8392a57b19620 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 14 Nov 2013 19:31:17 +0100 Subject: [PATCH] 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. --- __init__.py | 1 + contrib/__init__.py | 1 + contrib/barter.py | 59 +- contrib/chargen.py | 6 +- contrib/dice.py | 55 +- contrib/email-login.py | 32 +- contrib/evlang/__init__.py | 1 + contrib/evlang/command.py | 15 +- contrib/evlang/evlang.py | 154 +- contrib/extended_room.py | 109 +- contrib/lineeditor.py | 83 +- contrib/menu_login.py | 52 +- contrib/menusystem.py | 77 +- contrib/procpools/__init__.py | 1 + contrib/procpools/python_procpool.py | 40 +- contrib/procpools/python_procpool_plugin.py | 5 +- contrib/talking_npc.py | 54 +- contrib/tutorial_world/__init__.py | 1 + contrib/tutorial_world/mob.py | 78 +- contrib/tutorial_world/objects.py | 125 +- contrib/tutorial_world/rooms.py | 126 +- contrib/tutorial_world/scripts.py | 28 +- ev.py | 83 +- game/__init__.py | 1 + game/evennia.py | 21 +- game/gamesrc/__init__.py | 1 + game/gamesrc/commands/__init__.py | 1 + game/gamesrc/commands/examples/__init__.py | 1 + game/gamesrc/commands/examples/cmdset.py | 6 +- .../commands/examples/cmdset_red_button.py | 25 +- game/gamesrc/commands/examples/command.py | 6 +- game/gamesrc/conf/__init__.py | 1 + game/gamesrc/conf/examples/__init__.py | 1 + .../gamesrc/conf/examples/at_initial_setup.py | 1 + .../conf/examples/at_server_startstop.py | 6 + game/gamesrc/conf/examples/lockfuncs.py | 1 + game/gamesrc/conf/examples/oobfuncs.py | 7 +- .../conf/examples/portal_services_plugin.py | 1 + .../conf/examples/server_services_plugin.py | 1 + game/gamesrc/objects/__init__.py | 1 + game/gamesrc/objects/examples/__init__.py | 1 + game/gamesrc/objects/examples/character.py | 83 +- game/gamesrc/objects/examples/exit.py | 87 +- game/gamesrc/objects/examples/object.py | 294 ++-- game/gamesrc/objects/examples/player.py | 181 +- game/gamesrc/objects/examples/red_button.py | 1 + game/gamesrc/objects/examples/room.py | 65 +- game/gamesrc/scripts/__init__.py | 1 + game/gamesrc/scripts/examples/__init__.py | 1 + .../gamesrc/scripts/examples/bodyfunctions.py | 4 +- .../scripts/examples/red_button_scripts.py | 34 +- game/gamesrc/scripts/examples/script.py | 66 +- game/gamesrc/world/__init__.py | 1 + game/gamesrc/world/examples/batch_code.py | 30 +- game/manage.py | 6 +- game/runner.py | 28 +- src/__init__.py | 3 +- src/commands/__init__.py | 1 + src/commands/cmdhandler.py | 78 +- src/commands/cmdparser.py | 36 +- src/commands/cmdset.py | 60 +- src/commands/cmdsethandler.py | 65 +- src/commands/command.py | 54 +- src/commands/default/admin.py | 49 +- src/commands/default/batchprocess.py | 68 +- src/commands/default/building.py | 298 ++-- src/commands/default/cmdset_character.py | 7 +- src/commands/default/cmdset_player.py | 1 + src/commands/default/cmdset_unloggedin.py | 1 + src/commands/default/comms.py | 154 +- src/commands/default/general.py | 14 +- src/commands/default/help.py | 21 +- src/commands/default/muxcommand.py | 4 +- src/commands/default/player.py | 96 +- src/commands/default/syscommands.py | 9 +- src/commands/default/system.py | 95 +- src/commands/default/tests.py | 19 +- src/commands/default/unloggedin.py | 31 +- src/comms/__init__.py | 8 +- src/comms/admin.py | 106 +- src/comms/channelhandler.py | 9 +- src/comms/comms.py | 41 +- src/comms/imc2.py | 65 +- src/comms/imc2lib/__init__.py | 1 + src/comms/imc2lib/imc2_ansi.py | 119 +- src/comms/imc2lib/imc2_listeners.py | 48 +- src/comms/imc2lib/imc2_packets.py | 1561 +++++++++-------- src/comms/imc2lib/imc2_trackers.py | 212 +-- src/comms/irc.py | 423 ++--- src/comms/managers.py | 81 +- src/comms/models.py | 75 +- src/comms/rss.py | 19 +- src/help/__init__.py | 4 +- src/help/admin.py | 73 +- src/help/manager.py | 6 +- src/help/models.py | 3 +- src/locks/__init__.py | 1 + src/locks/lockfuncs.py | 70 +- src/locks/lockhandler.py | 46 +- src/locks/tests.py | 2 +- src/objects/admin.py | 278 ++- src/objects/manager.py | 103 +- src/objects/models.py | 168 +- src/objects/objects.py | 345 ++-- src/objects/tests.py | 15 +- src/players/__init__.py | 5 +- src/players/admin.py | 22 +- src/players/manager.py | 9 +- src/players/models.py | 131 +- src/players/player.py | 74 +- src/scripts/__init__.py | 5 +- src/scripts/admin.py | 9 +- src/scripts/manager.py | 14 +- src/scripts/models.py | 5 +- src/scripts/scripthandler.py | 15 +- src/scripts/scripts.py | 156 +- src/server/admin.py | 37 +- src/server/amp.py | 124 +- src/server/caches.py | 99 +- src/server/initial_setup.py | 69 +- src/server/manager.py | 10 +- src/server/models.py | 5 + src/server/oob_msdp.py | 93 +- src/server/oobhandler.py | 106 +- src/server/portal/__init__.py | 1 + src/server/portal/mccp.py | 2 + src/server/portal/msdp.py | 109 +- src/server/portal/portal.py | 27 +- src/server/portal/portalsessionhandler.py | 29 +- src/server/portal/ssh.py | 30 +- src/server/portal/ssl.py | 19 +- src/server/portal/telnet.py | 19 +- src/server/portal/ttype.py | 19 +- src/server/portal/webclient.py | 27 +- src/server/server.py | 109 +- src/server/serversession.py | 24 +- src/server/session.py | 32 +- src/server/sessionhandler.py | 59 +- src/server/webserver.py | 16 +- src/settings_default.py | 100 +- src/typeclasses/__init__.py | 1 + src/typeclasses/managers.py | 41 +- src/typeclasses/models.py | 240 ++- src/typeclasses/typeclass.py | 16 +- src/utils/ansi.py | 105 +- src/utils/batchprocessors.py | 38 +- src/utils/create.py | 34 +- src/utils/dbserialize.py | 87 +- src/utils/gametime.py | 68 +- src/utils/logger.py | 7 +- src/utils/search.py | 41 +- src/utils/test_utils.py | 7 +- src/utils/text2html.py | 20 +- src/utils/utils.py | 155 +- 154 files changed, 5613 insertions(+), 4054 deletions(-) diff --git a/__init__.py b/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/__init__.py +++ b/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/contrib/__init__.py b/contrib/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/contrib/__init__.py +++ b/contrib/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/contrib/barter.py b/contrib/barter.py index 27c6452e30..b593e7ee0a 100644 --- a/contrib/barter.py +++ b/contrib/barter.py @@ -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 [: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 or {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 diff --git a/contrib/chargen.py b/contrib/chargen.py index c11e00080d..519c610e3f 100644 --- a/contrib/chargen.py +++ b/contrib/chargen.py @@ -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. diff --git a/contrib/dice.py b/contrib/dice.py index fb728b871d..72a122ade0 100644 --- a/contrib/dice.py +++ b/contrib/dice.py @@ -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 diff --git a/contrib/email-login.py b/contrib/email-login.py index fd1020f4df..45a105604d 100644 --- a/contrib/email-login.py +++ b/contrib/email-login.py @@ -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): diff --git a/contrib/evlang/__init__.py b/contrib/evlang/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/contrib/evlang/__init__.py +++ b/contrib/evlang/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/contrib/evlang/command.py b/contrib/evlang/command.py index 97393dcc7f..75b6e00477 100644 --- a/contrib/evlang/command.py +++ b/contrib/evlang/command.py @@ -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: diff --git a/contrib/evlang/evlang.py b/contrib/evlang/evlang.py index 21c2997aba..658cd21bb8 100644 --- a/contrib/evlang/evlang.py +++ b/contrib/evlang/evlang.py @@ -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) diff --git a/contrib/extended_room.py b/contrib/extended_room.py index db576991c0..3b6bc8a4c7 100644 --- a/contrib/extended_room.py +++ b/contrib/extended_room.py @@ -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 ... of the correct timeslot - remains in the description. + Filter so that only time markers ... 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 " - 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 " - 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) diff --git a/contrib/lineeditor.py b/contrib/lineeditor.py index bd16679034..145fc55ff6 100644 --- a/contrib/lineeditor.py +++ b/contrib/lineeditor.py @@ -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 : + li - line number (int), starting from 1. This could also + be a range given as : 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
  • - search and replace words in entire buffer or on certain lines + # :s
  • - 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 + """ - any non-command is appended to the end of the buffer. : - view buffer or only line :: - view buffer without line numbers or other parsing @@ -578,7 +592,7 @@ class LineEditor(object): :y - yank (copy) line to the copy buffer :x - cut line and store it in the copy buffer :p - put (paste) previously copied line directly after -:i - insert new text at line . Old line will be shifted down +:i - insert new text at line . Old line will move down :r - replace line with text :I - insert text at the beginning of line :A - append text after the end of line @@ -627,7 +641,8 @@ class CmdEditor(Command): if not self.args or not '/' in self.args: self.caller.msg("Usage: @editor /") 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) \ No newline at end of file diff --git a/contrib/menu_login.py b/contrib/menu_login.py index a22bb05fdc..3bcd9a9242 100644 --- a/contrib/menu_login.py +++ b/contrib/menu_login.py @@ -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() diff --git a/contrib/menusystem.py b/contrib/menusystem.py index 66df672617..492c1def5f 100644 --- a/contrib/menusystem.py +++ b/contrib/menusystem.py @@ -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" diff --git a/contrib/procpools/__init__.py b/contrib/procpools/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/contrib/procpools/__init__.py +++ b/contrib/procpools/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/contrib/procpools/python_procpool.py b/contrib/procpools/python_procpool.py index 04ab0dac95..b77a1c7407 100644 --- a/contrib/procpools/python_procpool.py +++ b/contrib/procpools/python_procpool.py @@ -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) diff --git a/contrib/procpools/python_procpool_plugin.py b/contrib/procpools/python_procpool_plugin.py index 47cb8bc315..1ddc1f476d 100644 --- a/contrib/procpools/python_procpool_plugin.py +++ b/contrib/procpools/python_procpool_plugin.py @@ -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: diff --git a/contrib/talking_npc.py b/contrib/talking_npc.py index 61025407f5..58b7ed9852 100644 --- a/contrib/talking_npc.py +++ b/contrib/talking_npc.py @@ -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) \ No newline at end of file diff --git a/contrib/tutorial_world/__init__.py b/contrib/tutorial_world/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/contrib/tutorial_world/__init__.py +++ b/contrib/tutorial_world/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/contrib/tutorial_world/mob.py b/contrib/tutorial_world/mob.py index 87f6611b0c..bbdb4c561d 100644 --- a/contrib/tutorial_world/mob.py +++ b/contrib/tutorial_world/mob.py @@ -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) \ No newline at end of file diff --git a/contrib/tutorial_world/objects.py b/contrib/tutorial_world/objects.py index 9e293a254e..1e7561ff99 100644 --- a/contrib/tutorial_world/objects.py +++ b/contrib/tutorial_world/objects.py @@ -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" diff --git a/contrib/tutorial_world/rooms.py b/contrib/tutorial_world/rooms.py index 841996f7d5..514124a477 100644 --- a/contrib/tutorial_world/rooms.py +++ b/contrib/tutorial_world/rooms.py @@ -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() diff --git a/contrib/tutorial_world/scripts.py b/contrib/tutorial_world/scripts.py index cffb32efe7..42c28cf41c 100644 --- a/contrib/tutorial_world/scripts.py +++ b/contrib/tutorial_world/scripts.py @@ -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()) \ No newline at end of file diff --git a/ev.py b/ev.py index feb52d9280..0ec03eab52 100644 --- a/ev.py +++ b/ev.py @@ -2,53 +2,63 @@ Central API for the Evennia MUD/MUX/MU* creation system. -This is basically a set of shortcuts for accessing things in src/ with less boiler plate. -Import this from your code, use it with @py from in-game or explore it interactively from -a python shell. +This is basically a set of shortcuts for accessing things in src/ with less +boiler plate. Import this from your code, use it with @py from in-game or +explore it interactively from a python shell. Notes: - 0) Use ev.help(), ev.managers.help(), ev.default_cmds.help() and syscmdkeys.help() to - view the API structure and explore which variables/methods are available. + 0) Use ev.help(), ev.managers.help(), ev.default_cmds.help() and + syscmdkeys.help() to view the API structure and explore which + variables/methods are available. - 1) You should import things explicitly from the root of this module - you can not use - dot-notation to import deeper. Hence, to access a default command, you can do + 1) You should import things explicitly from the root of this module - you + can not use ot-notation to import deeper. Hence, to access a default c + ommand, you can do import ev ev.default_cmds.CmdLook or from ev import default_cmds default_cmds.CmdLook - But trying to import CmdLook directly with "from ev.default_cmds import CmdLook" will - not work since default_cmds is a property on the "ev" module, not a module of its own. + But trying to import CmdLook directly with + from ev.default_cmds import CmdLook + will not work since default_cmds is a property on the "ev" module, + not a module of its own. - 2) "managers" is a container object that contains shortcuts to initiated versions of Evennia's django - database managers (e.g. managers.objects is an alias for ObjectDB.objects). These allow - for exploring the database in various ways. To use in code, do 'from ev import managers', then - access the managers on the managers object. Please note that the evennia-specific methods in - managers return typeclasses (or lists of typeclasses), whereas the default django ones (filter etc) - return database objects. You can convert between the two easily via dbobj.typeclass and - typeclass.dbobj, but it's worth to remember this difference. + 2) "managers" is a container object that contains shortcuts to initiated + versions of Evennia's django database managers (e.g. managers.objects + is an alias for ObjectDB.objects). These allow for exploring the database + in various ways. To use in code, do 'from ev import managers', then access + the managers on the managers object. Please note that the evennia-specific + methods inmanagers return typeclasses (or lists of typeclasses), whereas + the default django ones (filter etc) return database objects. You can + convert between the two easily via dbobj.typeclass and typeclass.dbobj, + but it's worth to remember this difference. - 3) "syscmdkeys" is a container object holding the names of system commands. Import with - 'from ev import syscmdkeys', then access the variables on the syscmdkeys object. + 3) "syscmdkeys" is a container object holding the names of system commands. + Import with 'from ev import syscmdkeys', then access the variables on + the syscmdkeys object. - 4) You -have- to use the create_* functions (shortcuts to src.utils.create) to create new - Typeclassed game entities (Objects, Scripts or Players). Just initializing e.g. the Player class will - -not- set up Typeclasses correctly and will lead to errors. Other types of database objects - can be created normally, but there are conveniant create_* functions for those too, making - some more error checking. + 4) You -have- to use the create_* functions (shortcuts to src.utils.create) + to create new ypeclassed game entities (Objects, Scripts, Channels or + Players). Just initializing e.g. the Player class will -not- set up + Typeclasses correctly and will lead to errors. Other types of database + objects can be created normally, but there are conveniant create_* + functions for those too, making some more error checking. - 5) "settings" links to Evennia's game/settings file. "settings_full" shows all of django's available - settings. Note that this is for viewing only - you cannot *change* settings from here in a meaningful - way but have to update game/settings.py and restart the server. + 5) "settings" links to Evennia's game/settings file. "settings_full" shows + all of django's available settings. Note that this is for viewing only - + you cannot *change* settings from here in a meaningful way but have to + update game/settings.py and restart the server. - 6) The API accesses all relevant and most-neeeded functions/classes from src/ but might not - always include all helper-functions referenced from each such entity. To get to those, access - the modules in src/ directly. You can always do this anyway, if you do not want to go through - this API. + 6) The API accesses all relevant and most-neeeded functions/classes from + src/ but might not always include all helper-functions referenced from + each such entity. To get to those, access the modules in src/ directly. + You can always do this anyway, if you do not want to go through this API. """ -import sys, os +import sys +import os ###################################################################### # set Evennia version in __version__ property @@ -147,6 +157,7 @@ from src.utils import utils from src.utils import gametime from src.utils import ansi + ###################################################################### # API containers and helper functions ###################################################################### @@ -166,6 +177,7 @@ def help(header=False): names = [var for var in ev.__dict__ if not var.startswith('_')] return ", ".join(names) + class _EvContainer(object): """ Parent for other containers @@ -175,7 +187,8 @@ class _EvContainer(object): "Returns list of contents" names = [name for name in self.__class__.__dict__ if not name.startswith('_')] names += [name for name in self.__dict__ if not name.startswith('_')] - return self.__doc__ + "-"*60 + "\n" + ", ".join(names) + return self.__doc__ + "-" * 60 + "\n" + ", ".join(names) + class DBmanagers(_EvContainer): """ @@ -213,6 +226,7 @@ class DBmanagers(_EvContainer): managers = DBmanagers() del DBmanagers + class DefaultCmds(_EvContainer): """ This container holds direct shortcuts to all default commands in Evennia. @@ -235,7 +249,9 @@ class DefaultCmds(_EvContainer): cmdlist = utils.variable_from_module(module, module.__all__) self.__dict__.update(dict([(c.__name__, c) for c in cmdlist])) - from src.commands.default import admin, batchprocess, building, comms, general, player, help, system, unloggedin + from src.commands.default import (admin, batchprocess, + building, comms, general, + player, help, system, unloggedin) add_cmds(admin) add_cmds(building) add_cmds(batchprocess) @@ -249,6 +265,7 @@ class DefaultCmds(_EvContainer): default_cmds = DefaultCmds() del DefaultCmds + class SystemCmds(_EvContainer): """ Creating commands with keys set to these constants will make diff --git a/game/__init__.py b/game/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/__init__.py +++ b/game/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/evennia.py b/game/evennia.py index b17a97743c..dfc1a468a2 100755 --- a/game/evennia.py +++ b/game/evennia.py @@ -10,7 +10,8 @@ menu. Run the script with the -h flag to see usage information. """ import os -import sys, signal +import sys +import signal from optparse import OptionParser from subprocess import Popen @@ -132,7 +133,7 @@ from django.db import DatabaseError from src.players.models import PlayerDB try: superuser = PlayerDB.objects.get(id=1) -except DatabaseError,e: +except DatabaseError, e: print """ Your database does not seem to be set up correctly. (error was '%s') @@ -186,7 +187,7 @@ if os.name == 'nt': os.path.join(os.path.dirname(twistd.__file__), os.pardir, os.pardir, os.pardir, os.pardir, 'scripts', 'twistd.py')) - bat_file = open('twistd.bat','w') + bat_file = open('twistd.bat', 'w') bat_file.write("@\"%s\" \"%s\" %%*" % (sys.executable, twistd_path)) bat_file.close() print """ @@ -221,6 +222,7 @@ def get_pid(pidfile): pid = f.read() return pid + def del_pid(pidfile): """ The pidfile should normally be removed after a process has finished, but @@ -229,6 +231,7 @@ def del_pid(pidfile): if os.path.exists(pidfile): os.remove(pidfile) + def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART, restart="reload"): """ Send a kill signal to a process based on PID. A customized success/error @@ -255,6 +258,7 @@ def kill(pidfile, signal=SIG, succmsg="", errmsg="", restart_file=SERVER_RESTART return print "Evennia:", errmsg + def run_menu(): """ This launches an interactive menu. @@ -285,7 +289,7 @@ def run_menu(): errmsg = "The %s does not seem to be running." if inp < 5: if inp == 1: - pass # default operation + pass # default operation elif inp == 2: cmdstr.extend(['--iserver']) elif inp == 3: @@ -344,8 +348,9 @@ def handle_args(options, mode, service): if inter: cmdstr.append('--iportal') cmdstr.append('--noserver') - else: # all - # for convenience we don't start logging of portal, only of server with this command. + else: # all + # for convenience we don't start logging of + # portal, only of server with this command. if inter: cmdstr.extend(['--iserver']) return cmdstr @@ -429,7 +434,9 @@ def main(): parser = OptionParser(usage="%prog [-i] [menu|start|reload|stop [server|portal|all]]", description="""This is the main Evennia launcher. It handles the Portal and Server, the two services making up Evennia. Default is to operate on both services. Interactive mode sets the service to log to stdout, in the foreground. Note that when launching 'all' services with the \"--interactive\" flag, both services will be started, but only Server will actually be started in interactive mode, simply because this is the most commonly useful setup. To activate interactive mode also for Portal, use the menu or launch the two services explicitly as two separate calls to this program.""") - parser.add_option('-i', '--interactive', action='store_true', dest='interactive', default=False, help="Start given processes in interactive mode.") + parser.add_option('-i', '--interactive', action='store_true', + dest='interactive', default=False, + help="Start given processes in interactive mode.") options, args = parser.parse_args() diff --git a/game/gamesrc/__init__.py b/game/gamesrc/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/__init__.py +++ b/game/gamesrc/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/commands/__init__.py b/game/gamesrc/commands/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/commands/__init__.py +++ b/game/gamesrc/commands/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/commands/examples/__init__.py b/game/gamesrc/commands/examples/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/commands/examples/__init__.py +++ b/game/gamesrc/commands/examples/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/commands/examples/cmdset.py b/game/gamesrc/commands/examples/cmdset.py index fba9f44575..88e7c621b1 100644 --- a/game/gamesrc/commands/examples/cmdset.py +++ b/game/gamesrc/commands/examples/cmdset.py @@ -32,6 +32,7 @@ from ev import default_cmds #from contrib import misc_commands #from contrib import chargen + class ExampleCmdSet(CmdSet): """ Implements an empty, example cmdset. @@ -44,7 +45,8 @@ class ExampleCmdSet(CmdSet): This is the only method defined in a cmdset, called during its creation. It should populate the set with command instances. - As and example we just add the empty base Command object. It prints some info. + As and example we just add the empty base Command object. + It prints some info. """ self.add(Command()) @@ -75,6 +77,7 @@ class CharacterCmdSet(default_cmds.CharacterCmdSet): #self.add(lineeditor.CmdEditor()) #self.add(misc_commands.CmdQuell()) + class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): """ This is an example of how to overload the command set of the @@ -99,6 +102,7 @@ class UnloggedinCmdSet(default_cmds.UnloggedinCmdSet): # any commands you add below will overload the default ones. # + class PlayerCmdSet(default_cmds.PlayerCmdSet): """ This is set is available to the player when they have no diff --git a/game/gamesrc/commands/examples/cmdset_red_button.py b/game/gamesrc/commands/examples/cmdset_red_button.py index ba7c61b17c..583d15e252 100644 --- a/game/gamesrc/commands/examples/cmdset_red_button.py +++ b/game/gamesrc/commands/examples/cmdset_red_button.py @@ -16,6 +16,7 @@ from ev import Command, CmdSet # Commands defined on the red button #------------------------------------------------------------ + class CmdNudge(Command): """ Try to nudge the button's lid @@ -27,7 +28,7 @@ class CmdNudge(Command): push the lid of the button away. """ - key = "nudge lid" # two-word command name! + key = "nudge lid" # two-word command name! aliases = ["nudge"] locks = "cmd:all()" @@ -44,6 +45,7 @@ class CmdNudge(Command): self.caller.msg("You manage to get a nail under the lid.") self.caller.execute_cmd("open lid") + class CmdPush(Command): """ Push the red button @@ -82,7 +84,6 @@ class CmdPush(Command): self.caller.msg(string) - class CmdSmashGlass(Command): """ smash glass @@ -118,7 +119,8 @@ class CmdSmashGlass(Command): string += " you should just try to open the lid instead?" self.caller.msg(string) self.caller.location.msg_contents("%s tries to smash the glass of the button." % - (self.caller.name), exclude=self.caller) + (self.caller.name), exclude=self.caller) + class CmdOpenLid(Command): """ @@ -144,12 +146,13 @@ class CmdOpenLid(Command): string += "the lid will soon close again." self.caller.msg(string) self.caller.location.msg_contents("%s opens the lid of the button." % - (self.caller.name), exclude=self.caller) + (self.caller.name), exclude=self.caller) # add the relevant cmdsets to button self.obj.cmdset.add(LidClosedCmdSet) # call object method self.obj.open_lid() + class CmdCloseLid(Command): """ close the lid @@ -174,6 +177,7 @@ class CmdCloseLid(Command): self.caller.location.msg_contents("%s closes the button's lid." % (self.caller.name), exclude=self.caller) + class CmdBlindLook(Command): """ Looking around in darkness @@ -209,7 +213,8 @@ class CmdBlindLook(Command): string += "Until it wears off, all you can do is feel around blindly." self.caller.msg(string) self.caller.location.msg_contents("%s stumbles around, blinded." % - (self.caller.name), exclude=self.caller) + (self.caller.name), exclude=self.caller) + class CmdBlindHelp(Command): """ @@ -232,7 +237,6 @@ class CmdBlindHelp(Command): # Command sets for the red button #--------------------------------------------------------------- - # We next tuck these commands into their respective command sets. # (note that we are overdoing the cdmset separation a bit here # to show how it works). @@ -247,12 +251,13 @@ class DefaultCmdSet(CmdSet): using obj.cmdset.add_default(). """ key = "RedButtonDefault" - mergetype = "Union" # this is default, we don't really need to put it here. + mergetype = "Union" # this is default, we don't really need to put it here. def at_cmdset_creation(self): "Init the cmdset" self.add(CmdPush()) + class LidClosedCmdSet(CmdSet): """ A simple cmdset tied to the redbutton object. @@ -274,6 +279,7 @@ class LidClosedCmdSet(CmdSet): self.add(CmdSmashGlass()) self.add(CmdOpenLid()) + class LidOpenCmdSet(CmdSet): """ This is the opposite of the Closed cmdset. @@ -288,6 +294,7 @@ class LidOpenCmdSet(CmdSet): "setup the cmdset (just one command)" self.add(CmdCloseLid()) + class BlindCmdSet(CmdSet): """ This is the cmdset added to the *player* when @@ -300,8 +307,8 @@ class BlindCmdSet(CmdSet): # we want to stop the player from walking around # in this blinded state, so we hide all exits too. # (channel commands will still work). - no_exits = True # keep player in the same room - no_objs = True # don't allow object commands + no_exits = True # keep player in the same room + no_objs = True # don't allow object commands def at_cmdset_creation(self): "Setup the blind cmdset" diff --git a/game/gamesrc/commands/examples/command.py b/game/gamesrc/commands/examples/command.py index 1753a0430a..e0c945998b 100644 --- a/game/gamesrc/commands/examples/command.py +++ b/game/gamesrc/commands/examples/command.py @@ -12,6 +12,7 @@ from ev import Command as BaseCommand from ev import default_cmds from ev import utils + class Command(BaseCommand): """ Inherit from this if you want to create your own @@ -34,7 +35,6 @@ class Command(BaseCommand): # arg_regex = r"\s.*?|$" # optional regex detailing how the part after # the cmdname must look to match this command. - # (we don't implement hook method access() here, you don't need to # modify that unless you want to change how the lock system works # (in that case see src.commands.command.Command)) @@ -104,8 +104,8 @@ class MuxCommand(default_cmds.MuxCommand): cmdhandler at this point, and stored in self.cmdname. The rest is stored in self.args. - The MuxCommand parser breaks self.args into its constituents and stores them in the - following variables: + The MuxCommand parser breaks self.args into its constituents and stores them + in the following variables: self.switches = optional list of /switches (without the /) self.raw = This is the raw argument input, including switches self.args = This is re-defined to be everything *except* the switches diff --git a/game/gamesrc/conf/__init__.py b/game/gamesrc/conf/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/conf/__init__.py +++ b/game/gamesrc/conf/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/conf/examples/__init__.py b/game/gamesrc/conf/examples/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/conf/examples/__init__.py +++ b/game/gamesrc/conf/examples/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/conf/examples/at_initial_setup.py b/game/gamesrc/conf/examples/at_initial_setup.py index 0b0fb08b0d..e192331e12 100644 --- a/game/gamesrc/conf/examples/at_initial_setup.py +++ b/game/gamesrc/conf/examples/at_initial_setup.py @@ -20,5 +20,6 @@ does what you expect it to. """ + def at_initial_setup(): pass diff --git a/game/gamesrc/conf/examples/at_server_startstop.py b/game/gamesrc/conf/examples/at_server_startstop.py index 74f173c809..c3ad6384f3 100644 --- a/game/gamesrc/conf/examples/at_server_startstop.py +++ b/game/gamesrc/conf/examples/at_server_startstop.py @@ -26,6 +26,7 @@ at_server_cold_stop() """ + def at_server_start(): """ This is called every time the server starts up, regardless of @@ -33,6 +34,7 @@ def at_server_start(): """ pass + def at_server_stop(): """ This is called just before a server is shut down, regardless @@ -40,18 +42,21 @@ def at_server_stop(): """ pass + def at_server_reload_start(): """ This is called only when server starts back up after a reload. """ pass + def at_server_reload_stop(): """ This is called only time the server stops before a reload. """ pass + def at_server_cold_start(): """ This is called only when the server starts "cold", i.e. after a @@ -59,6 +64,7 @@ def at_server_cold_start(): """ pass + def at_server_cold_stop(): """ This is called only when the server goes down due to a shutdown or reset. diff --git a/game/gamesrc/conf/examples/lockfuncs.py b/game/gamesrc/conf/examples/lockfuncs.py index a3f3e931fc..5693f99886 100644 --- a/game/gamesrc/conf/examples/lockfuncs.py +++ b/game/gamesrc/conf/examples/lockfuncs.py @@ -22,6 +22,7 @@ See many more examples of lock functions in src.locks.lockfuncs. """ + def myfalse(accessing_obj, accessed_obj, *args, **kwargs): """ called in lockstring with myfalse(). diff --git a/game/gamesrc/conf/examples/oobfuncs.py b/game/gamesrc/conf/examples/oobfuncs.py index 24c7422cb9..4fdc328fe7 100644 --- a/game/gamesrc/conf/examples/oobfuncs.py +++ b/game/gamesrc/conf/examples/oobfuncs.py @@ -18,10 +18,11 @@ """ + def testoob(character, *args, **kwargs): "Simple test function" - print "Called testoob: %s" % val - return "testoob did stuff to the input string '%s'!" % val + print "Called testoob: %s" % args + return "testoob did stuff to the input string '%s'!" % args # MSDP_MAP is a standard suggestions for making it easy to create generic guis. @@ -62,7 +63,7 @@ MSDP_REPORTABLE = { # Combat "OPPONENT_HEALTH": "opponent_health", - "OPPONENT_HEALTH_MAX":"opponent_health_max", + "OPPONENT_HEALTH_MAX": "opponent_health_max", "OPPONENT_LEVEL": "opponent_level", "OPPONENT_NAME": "opponent_name", diff --git a/game/gamesrc/conf/examples/portal_services_plugin.py b/game/gamesrc/conf/examples/portal_services_plugin.py index 3ae90f9258..294c6d03d2 100644 --- a/game/gamesrc/conf/examples/portal_services_plugin.py +++ b/game/gamesrc/conf/examples/portal_services_plugin.py @@ -14,6 +14,7 @@ the Server startup process. """ + def start_plugin_services(server): """ This hook is called by Evennia, last in the Server startup process. diff --git a/game/gamesrc/conf/examples/server_services_plugin.py b/game/gamesrc/conf/examples/server_services_plugin.py index 886c845e20..a585c67705 100644 --- a/game/gamesrc/conf/examples/server_services_plugin.py +++ b/game/gamesrc/conf/examples/server_services_plugin.py @@ -14,6 +14,7 @@ the Portal startup process. """ + def start_plugin_services(portal): """ This hook is called by Evennia, last in the Portal startup process. diff --git a/game/gamesrc/objects/__init__.py b/game/gamesrc/objects/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/objects/__init__.py +++ b/game/gamesrc/objects/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/objects/examples/__init__.py b/game/gamesrc/objects/examples/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/objects/examples/__init__.py +++ b/game/gamesrc/objects/examples/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/objects/examples/character.py b/game/gamesrc/objects/examples/character.py index 64f75ce6cc..4d0d42140f 100644 --- a/game/gamesrc/objects/examples/character.py +++ b/game/gamesrc/objects/examples/character.py @@ -1,41 +1,42 @@ -""" - -Template for Characters - -Copy this module up one level and name it as you like, then -use it as a template to create your own Character class. - -To make new logins default to creating characters -of your new type, change settings.BASE_CHARACTER_TYPECLASS to point to -your new class, e.g. - -settings.BASE_CHARACTER_TYPECLASS = "game.gamesrc.objects.mychar.MyChar" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" -from ev import Character as DefaultCharacter - -class Character(DefaultCharacter): - """ - The Character is like any normal Object (see example/object.py for - a list of properties and methods), except it actually implements - some of its hook methods to do some work: - - at_basetype_setup - always assigns the default_cmdset to this object type - (important!)sets locks so character cannot be picked up - and its commands only be called by itself, not anyone else. - (to change things, use at_object_creation() instead) - at_after_move - launches the "look" command - at_post_puppet(player) - when Player disconnects from the Character, we - store the current location, so the "unconnected" character - object does not need to stay on grid but can be given a - None-location while offline. - at_pre_puppet - just before Player re-connects, retrieves the character's old - location and puts it back on the grid with a "charname has - connected" message echoed to the room - - """ - pass +""" + +Template for Characters + +Copy this module up one level and name it as you like, then +use it as a template to create your own Character class. + +To make new logins default to creating characters +of your new type, change settings.BASE_CHARACTER_TYPECLASS to point to +your new class, e.g. + +settings.BASE_CHARACTER_TYPECLASS = "game.gamesrc.objects.mychar.MyChar" + +Note that objects already created in the database will not notice +this change, you have to convert them manually e.g. with the +@typeclass command. + +""" +from ev import Character as DefaultCharacter + + +class Character(DefaultCharacter): + """ + The Character is like any normal Object (see example/object.py for + a list of properties and methods), except it actually implements + some of its hook methods to do some work: + + at_basetype_setup - always assigns the default_cmdset to this object type + (important!)sets locks so character cannot be picked up + and its commands only be called by itself, not anyone else. + (to change things, use at_object_creation() instead) + at_after_move - launches the "look" command + at_post_puppet(player) - when Player disconnects from the Character, we + store the current location, so the "unconnected" character + object does not need to stay on grid but can be given a + None-location while offline. + at_pre_puppet - just before Player re-connects, retrieves the character's + old location and puts it back on the grid with a "charname + has connected" message echoed to the room + + """ + pass diff --git a/game/gamesrc/objects/examples/exit.py b/game/gamesrc/objects/examples/exit.py index 2591367288..346441316c 100644 --- a/game/gamesrc/objects/examples/exit.py +++ b/game/gamesrc/objects/examples/exit.py @@ -1,43 +1,44 @@ -""" - -Template module for Exits - -Copy this module up one level and name it as you like, then -use it as a template to create your own Exits. - -To make the default commands (such as @dig/@open) default to creating exits -of your new type, change settings.BASE_EXIT_TYPECLASS to point to -your new class, e.g. - -settings.BASE_EXIT_TYPECLASS = "game.gamesrc.objects.myexit.MyExit" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" -from ev import Exit as DefaultExit - -class Exit(DefaultExit): - """ - Exits are connectors between rooms. Exits are normal Objects except - they defines the 'destination' property. It also does work in the - following methods: - - basetype_setup() - sets default exit locks (to change, use at_object_creation instead) - at_cmdset_get() - this auto-creates and caches a command and a command set on itself - with the same name as the Exit object. This - allows users to use the exit by only giving its - name alone on the command line. - at_failed_traverse() - gives a default error message ("You cannot - go there") if exit traversal fails and an - attribute err_traverse is not defined. - - Relevant hooks to overload (compared to other types of Objects): - at_before_traverse(traveller) - called just before traversing - at_after_traverse(traveller, source_loc) - called just after traversing - at_failed_traverse(traveller) - called if traversal failed for some reason. Will - not be called if the attribute 'err_traverse' is - defined, in which case that will simply be echoed. - """ - pass +""" + +Template module for Exits + +Copy this module up one level and name it as you like, then +use it as a template to create your own Exits. + +To make the default commands (such as @dig/@open) default to creating exits +of your new type, change settings.BASE_EXIT_TYPECLASS to point to +your new class, e.g. + +settings.BASE_EXIT_TYPECLASS = "game.gamesrc.objects.myexit.MyExit" + +Note that objects already created in the database will not notice +this change, you have to convert them manually e.g. with the +@typeclass command. + +""" +from ev import Exit as DefaultExit + + +class Exit(DefaultExit): + """ + Exits are connectors between rooms. Exits are normal Objects except + they defines the 'destination' property. It also does work in the + following methods: + + basetype_setup() - sets default exit locks (to change, use at_object_creation instead) + at_cmdset_get() - this auto-creates and caches a command and a command set on itself + with the same name as the Exit object. This + allows users to use the exit by only giving its + name alone on the command line. + at_failed_traverse() - gives a default error message ("You cannot + go there") if exit traversal fails and an + attribute err_traverse is not defined. + + Relevant hooks to overload (compared to other types of Objects): + at_before_traverse(traveller) - called just before traversing + at_after_traverse(traveller, source_loc) - called just after traversing + at_failed_traverse(traveller) - called if traversal failed for some reason. Will + not be called if the attribute 'err_traverse' is + defined, in which case that will simply be echoed. + """ + pass diff --git a/game/gamesrc/objects/examples/object.py b/game/gamesrc/objects/examples/object.py index bdd32e5287..dca6f88a64 100644 --- a/game/gamesrc/objects/examples/object.py +++ b/game/gamesrc/objects/examples/object.py @@ -1,127 +1,167 @@ -""" - -Template for Objects - -Copy this module up one level and name it as you like, then -use it as a template to create your own Objects. - -To make the default commands default to creating objects of your new -type (and also change the "fallback" object used when typeclass -creation fails), change settings.BASE_OBJECT_TYPECLASS to point to -your new class, e.g. - -settings.BASE_OBJECT_TYPECLASS = "game.gamesrc.objects.myobj.MyObj" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" -from ev import Object as DefaultObject - -class Object(DefaultObject): - """ - This is the root typeclass object, implementing an in-game Evennia - game object, such as having a location, being able to be - manipulated or looked at, etc. If you create a new typeclass, it - must always inherit from this object (or any of the other objects - in this file, since they all actually inherit from BaseObject, as - seen in src.object.objects). - - The BaseObject class implements several hooks tying into the game - engine. By re-implementing these hooks you can control the - system. You should never need to re-implement special Python - methods, such as __init__ and especially never __getattribute__ and - __setattr__ since these are used heavily by the typeclass system - of Evennia and messing with them might well break things for you. - - - * Base properties defined/available on all Objects - - key (string) - name of object - name (string)- same as key - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - player (Player) - controlling player (if any, only set together with sessid below) - sessid (int, read-only) - session id (if any, only set together with player above) - location (Object) - current location. Is None if this is a room - home (Object) - safety start-location - sessions (list of Sessions, read-only) - returns all sessions connected to this object - has_player (bool, read-only)- will only return *connected* players - contents (list of Objects, read-only) - returns all objects inside this object (including exits) - exits (list of Objects, read-only) - returns all exits from this object, if any - destination (Object) - only set if this object is an exit. - is_superuser (bool, read-only) - True/False if this user is a superuser - - * Handlers available - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). - - * Helper methods (see src.objects.objects.py for full headers) - - search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False) - execute_cmd(raw_string) - msg(text=None, **kwargs) - msg_contents(message, exclude=None, from_obj=None, **kwargs) - move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) - copy(new_key=None) - delete() - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) - - * Hooks (these are class methods, so their arguments should also start with self): - - basetype_setup() - only called once, used for behind-the-scenes setup. Normally not modified. - basetype_posthook_setup() - customization in basetype, after the object has been created; Normally not modified. - - at_object_creation() - only called once, when object is first created. Object customizations go here. - at_object_delete() - called just before deleting an object. If returning False, deletion is aborted. Note that all objects - inside a deleted object are automatically moved to their , they don't need to be removed here. - - at_init() - called whenever typeclass is cached from memory, at least once every server restart/reload - at_cmdset_get() - this is called just before the command handler requests a cmdset from this object - at_pre_puppet(player)- (player-controlled objects only) called just before puppeting - at_post_puppet() - (player-controlled objects only) called just after completing connection player<->object - at_pre_unpuppet() - (player-controlled objects only) called just before un-puppeting - at_post_unpuppet(player) - (player-controlled objects only) called just after disconnecting player<->object link - at_server_reload() - called before server is reloaded - at_server_shutdown() - called just before server is fully shut down - - at_access_success(accessing_obj, access_type) - called if an lock access check succeeded on this object - at_access_failure(accessing_obj, access_type) - called if an lock access check failed on this object - - at_before_move(destination) - called just before moving object to the destination. If returns False, move is cancelled. - announce_move_from(destination) - called in old location, just before move, if obj.move_to() has quiet=False - announce_move_to(source_location) - called in new location, just after move, if obj.move_to() has quiet=False - at_after_move(source_location) - always called after a move has been successfully performed. - at_object_leave(obj, target_location) - called when an object leaves this object in any fashion - at_object_receive(obj, source_location) - called when this object receives another object - - at_before_traverse(traversing_object) - (exit-objects only) called just before an object traverses this object - at_after_traverse(traversing_object, source_location) - (exit-objects only) called just after a traversal has happened. - at_failed_traverse(traversing_object) - (exit-objects only) called if traversal fails and property err_traverse is not defined. - - at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message (via self.msg()) is sent to this obj. - If returns false, aborts send. - at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects sends a message to someone via self.msg(). - - return_appearance(looker) - describes this object. Used by "look" command by default - at_desc(looker=None) - called by 'look' whenever the appearance is requested. - at_get(getter) - called after object has been picked up. Does not stop pickup. - at_drop(dropper) - called when this object has been dropped. - at_say(speaker, message) - by default, called if an object inside this object speaks - - """ - pass +""" + +Template for Objects + +Copy this module up one level and name it as you like, then +use it as a template to create your own Objects. + +To make the default commands default to creating objects of your new +type (and also change the "fallback" object used when typeclass +creation fails), change settings.BASE_OBJECT_TYPECLASS to point to +your new class, e.g. + +settings.BASE_OBJECT_TYPECLASS = "game.gamesrc.objects.myobj.MyObj" + +Note that objects already created in the database will not notice +this change, you have to convert them manually e.g. with the +@typeclass command. + +""" +from ev import Object as DefaultObject + + +class Object(DefaultObject): + """ + This is the root typeclass object, implementing an in-game Evennia + game object, such as having a location, being able to be + manipulated or looked at, etc. If you create a new typeclass, it + must always inherit from this object (or any of the other objects + in this file, since they all actually inherit from BaseObject, as + seen in src.object.objects). + + The BaseObject class implements several hooks tying into the game + engine. By re-implementing these hooks you can control the + system. You should never need to re-implement special Python + methods, such as __init__ and especially never __getattribute__ and + __setattr__ since these are used heavily by the typeclass system + of Evennia and messing with them might well break things for you. + + + * Base properties defined/available on all Objects + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + dbobj (Object, read-only) - link to database model. dbobj.typeclass points + back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + player (Player) - controlling player (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + player above) + location (Object) - current location. Is None if this is a room + home (Object) - safety start-location + sessions (list of Sessions, read-only) - returns all sessions connected + to this object + has_player (bool, read-only)- will only return *connected* players + contents (list of Objects, read-only) - returns all objects inside this + object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any + destination (Object) - only set if this object is an exit. + is_superuser (bool, read-only) - True/False if this user is a superuser + + * Handlers available + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create + a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods (see src.objects.objects.py for full headers) + + search(ostring, global_search=False, attribute_name=None, + use_nicks=False, location=None, ignore_errors=False, player=False) + execute_cmd(raw_string) + msg(text=None, **kwargs) + msg_contents(message, exclude=None, from_obj=None, **kwargs) + move_to(destination, quiet=False, emit_to_obj=None, use_destination=True) + copy(new_key=None) + delete() + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hooks (these are class methods, so args should start with self): + + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the object + has been created; Normally not modified. + + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If returning + False, deletion is aborted. Note that all objects + inside a deleted object are automatically moved + to their , they don't need to be removed here. + + at_init() - called whenever typeclass is cached from memory, + at least once every server restart/reload + at_cmdset_get() - this is called just before the command handler + requests a cmdset from this object + at_pre_puppet(player)- (player-controlled objects only) called just + before puppeting + at_post_puppet() - (player-controlled objects only) called just + after completing connection player<->object + at_pre_unpuppet() - (player-controlled objects only) called just + before un-puppeting + at_post_unpuppet(player) - (player-controlled objects only) called just + after disconnecting player<->object link + at_server_reload() - called before server is reloaded + at_server_shutdown() - called just before server is fully shut down + + at_access_success(accessing_obj, access_type) - called if an lock access + check succeeded on this object + at_access_failure(accessing_obj, access_type) - called if an lock access + check failed on this object + + at_before_move(destination) - called just before moving object + to the destination. If returns False, move is cancelled. + announce_move_from(destination) - called in old location, just + before move, if obj.move_to() has quiet=False + announce_move_to(source_location) - called in new location, just + after move, if obj.move_to() has quiet=False + at_after_move(source_location) - always called after a move has + been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object receives + another object + + at_before_traverse(traversing_object) - (exit-objects only) + called just before an object traverses this object + at_after_traverse(traversing_object, source_location) - (exit-objects only) + called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called if + traversal fails and property err_traverse is not defined. + + at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message + (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects + sends a message to someone via self.msg(). + + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the + appearance is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. + at_drop(dropper) - called when this object has been dropped. + at_say(speaker, message) - by default, called if an object inside this + object speaks + + """ + pass diff --git a/game/gamesrc/objects/examples/player.py b/game/gamesrc/objects/examples/player.py index 843e0cf2ce..5e587e035f 100644 --- a/game/gamesrc/objects/examples/player.py +++ b/game/gamesrc/objects/examples/player.py @@ -1,90 +1,91 @@ -""" - -Template module for Players - -Copy this module up one level and name it as you like, then -use it as a template to create your own Player class. - -To make the default account login default to using a Player -of your new type, change settings.BASE_PLAYER_TYPECLASS to point to -your new class, e.g. - -settings.BASE_PLAYER_TYPECLASS = "game.gamesrc.objects.myplayer.MyPlayer" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" -from ev import Player as DefaultPlayer - -class Player(DefaultPlayer): - """ - This class describes the actual OOC player (i.e. the user connecting - to the MUD). It does NOT have visual appearance in the game world (that - is handled by the character which is connected to this). Comm channels - are attended/joined using this object. - - It can be useful e.g. for storing configuration options for your game, but - should generally not hold any character-related info (that's best handled - on the character level). - - Can be set using BASE_PLAYER_TYPECLASS. - - - * available properties - - key (string) - name of player - name (string)- wrapper for user.username - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings - - user (User, read-only) - django User authorization object - obj (Object) - game object controlled by player. 'character' can also be used. - sessions (list of Sessions) - sessions connected to this player - is_superuser (bool, read-only) - if the connected user is a superuser - - * Handlers - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data - scripts - script-handler. Add new scripts to object with scripts.add() - cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object - nicks - nick-handler. New nicks with nicks.add(). - - * Helper methods - - msg(text=None, **kwargs) - swap_character(new_character, delete_old_character=False) - execute_cmd(raw_string, sessid=None) - search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False) - is_typeclass(typeclass, exact=False) - swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) - access(accessing_obj, access_type='read', default=False) - check_permstring(permstring) - - * Hook methods (when re-implementation, remember methods need to have self as first arg) - - basetype_setup() - at_player_creation() - - - note that the following hooks are also found on Objects and are - usually handled on the character level: - - at_init() - at_cmdset_get() - at_first_login() - at_post_login(sessid=None) - at_disconnect() - at_message_receive() - at_message_send() - at_server_reload() - at_server_shutdown() - - """ - pass +""" + +Template module for Players + +Copy this module up one level and name it as you like, then +use it as a template to create your own Player class. + +To make the default account login default to using a Player +of your new type, change settings.BASE_PLAYER_TYPECLASS to point to +your new class, e.g. + +settings.BASE_PLAYER_TYPECLASS = "game.gamesrc.objects.myplayer.MyPlayer" + +Note that objects already created in the database will not notice +this change, you have to convert them manually e.g. with the +@typeclass command. + +""" +from ev import Player as DefaultPlayer + + +class Player(DefaultPlayer): + """ + This class describes the actual OOC player (i.e. the user connecting + to the MUD). It does NOT have visual appearance in the game world (that + is handled by the character which is connected to this). Comm channels + are attended/joined using this object. + + It can be useful e.g. for storing configuration options for your game, but + should generally not hold any character-related info (that's best handled + on the character level). + + Can be set using BASE_PLAYER_TYPECLASS. + + + * available properties + + key (string) - name of player + name (string)- wrapper for user.username + aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class + typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + user (User, read-only) - django User authorization object + obj (Object) - game object controlled by player. 'character' can also be used. + sessions (list of Sessions) - sessions connected to this player + is_superuser (bool, read-only) - if the connected user is a superuser + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + scripts - script-handler. Add new scripts to object with scripts.add() + cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object + nicks - nick-handler. New nicks with nicks.add(). + + * Helper methods + + msg(text=None, **kwargs) + swap_character(new_character, delete_old_character=False) + execute_cmd(raw_string, sessid=None) + search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False) + is_typeclass(typeclass, exact=False) + swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) + access(accessing_obj, access_type='read', default=False) + check_permstring(permstring) + + * Hook methods (when re-implementation, remember methods need to have self as first arg) + + basetype_setup() + at_player_creation() + + - note that the following hooks are also found on Objects and are + usually handled on the character level: + + at_init() + at_cmdset_get() + at_first_login() + at_post_login(sessid=None) + at_disconnect() + at_message_receive() + at_message_send() + at_server_reload() + at_server_shutdown() + + """ + pass diff --git a/game/gamesrc/objects/examples/red_button.py b/game/gamesrc/objects/examples/red_button.py index a84ba7753e..7b827c4684 100644 --- a/game/gamesrc/objects/examples/red_button.py +++ b/game/gamesrc/objects/examples/red_button.py @@ -19,6 +19,7 @@ from game.gamesrc.commands.examples import cmdset_red_button as cmdsetexamples # Definition of the object itself # + class RedButton(Object): """ This class describes an evil red button. It will use the script diff --git a/game/gamesrc/objects/examples/room.py b/game/gamesrc/objects/examples/room.py index 1fb7e35173..01bb16d90a 100644 --- a/game/gamesrc/objects/examples/room.py +++ b/game/gamesrc/objects/examples/room.py @@ -1,32 +1,33 @@ -""" - -Template module for Rooms - -Copy this module up one level and name it as you like, then -use it as a template to create your own Objects. - -To make the default commands (such as @dig) default to creating rooms -of your new type, change settings.BASE_ROOM_TYPECLASS to point to -your new class, e.g. - -settings.BASE_ROOM_TYPECLASS = "game.gamesrc.objects.myroom.MyRoom" - -Note that objects already created in the database will not notice -this change, you have to convert them manually e.g. with the -@typeclass command. - -""" - -from ev import Room as DefaultRoom - -class Room(DefaultRoom): - """ - Rooms are like any Object, except their location is None - (which is default). They also use basetype_setup() to - add locks so they cannot be puppeted or picked up. - (to change that, use at_object_creation instead) - - See examples/object.py for a list of - properties and methods available on all Objects. - """ - pass +""" + +Template module for Rooms + +Copy this module up one level and name it as you like, then +use it as a template to create your own Objects. + +To make the default commands (such as @dig) default to creating rooms +of your new type, change settings.BASE_ROOM_TYPECLASS to point to +your new class, e.g. + +settings.BASE_ROOM_TYPECLASS = "game.gamesrc.objects.myroom.MyRoom" + +Note that objects already created in the database will not notice +this change, you have to convert them manually e.g. with the +@typeclass command. + +""" + +from ev import Room as DefaultRoom + + +class Room(DefaultRoom): + """ + Rooms are like any Object, except their location is None + (which is default). They also use basetype_setup() to + add locks so they cannot be puppeted or picked up. + (to change that, use at_object_creation instead) + + See examples/object.py for a list of + properties and methods available on all Objects. + """ + pass diff --git a/game/gamesrc/scripts/__init__.py b/game/gamesrc/scripts/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/scripts/__init__.py +++ b/game/gamesrc/scripts/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/scripts/examples/__init__.py b/game/gamesrc/scripts/examples/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/scripts/examples/__init__.py +++ b/game/gamesrc/scripts/examples/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/scripts/examples/bodyfunctions.py b/game/gamesrc/scripts/examples/bodyfunctions.py index 8dd5d805b2..56534d7beb 100644 --- a/game/gamesrc/scripts/examples/bodyfunctions.py +++ b/game/gamesrc/scripts/examples/bodyfunctions.py @@ -22,9 +22,9 @@ class BodyFunctions(Script): def at_script_creation(self): self.key = "bodyfunction" self.desc = "Adds various timed events to a character." - self.interval = 20 # seconds + self.interval = 20 # seconds #self.repeats = 5 # repeat only a certain number of times - self.start_delay = True # wait self.interval until first call + self.start_delay = True # wait self.interval until first call #self.persistent = True def at_repeat(self): diff --git a/game/gamesrc/scripts/examples/red_button_scripts.py b/game/gamesrc/scripts/examples/red_button_scripts.py index 9208a7e1c2..a3cf8da06f 100644 --- a/game/gamesrc/scripts/examples/red_button_scripts.py +++ b/game/gamesrc/scripts/examples/red_button_scripts.py @@ -112,10 +112,10 @@ class BlindedState(Script): """ self.key = "temporary_blinder" self.desc = "Temporarily blinds the player for a little while." - self.interval = 20 # seconds - self.start_delay = True # we don't want it to stop until after 20s. - self.repeats = 1 # this will go away after interval seconds. - self.persistent = False # we will ditch this if server goes down + self.interval = 20 # seconds + self.start_delay = True # we don't want it to stop until after 20s. + self.repeats = 1 # this will go away after interval seconds. + self.persistent = False # we will ditch this if server goes down def at_start(self): """ @@ -139,8 +139,8 @@ class BlindedState(Script): self.obj.location.msg_contents("%s seems to be recovering their eyesight." % self.obj.name, exclude=self.obj) - self.obj.cmdset.delete() # this will clear the latest added cmdset, - # (which is the blinded one). + self.obj.cmdset.delete() # this will clear the latest added cmdset, + # (which is the blinded one). # @@ -169,11 +169,11 @@ class CloseLidEvent(Script): """ self.key = "lid_closer" self.desc = "Closes lid on a red buttons" - self.interval = 20 # seconds - self.start_delay = True # we want to pospone the launch. - self.repeats = 1 # we only close the lid once - self.persistent = True # even if the server crashes in those 20 seconds, - # the lid will still close once the game restarts. + self.interval = 20 # seconds + self.start_delay = True # we want to pospone the launch. + self.repeats = 1 # we only close the lid once + self.persistent = True # even if the server crashes in those 20 seconds, + # the lid will still close once the game restarts. def is_valid(self): """ @@ -207,9 +207,9 @@ class BlinkButtonEvent(Script): """ self.key = "blink_button" self.desc = "Blinks red buttons" - self.interval = 35 #seconds - self.start_delay = False #blink right away - self.persistent = True #keep blinking also after server reboot + self.interval = 35 #seconds + self.start_delay = False #blink right away + self.persistent = True #keep blinking also after server reboot def is_valid(self): """ @@ -239,10 +239,10 @@ class DeactivateButtonEvent(Script): """ self.key = "deactivate_button" self.desc = "Deactivate red button temporarily" - self.interval = 21 #seconds - self.start_delay = True # wait with the first repeat for self.interval seconds. + self.interval = 21 #seconds + self.start_delay = True # wait with the first repeat for self.interval seconds. self.persistent = True - self.repeats = 1 # only do this once + self.repeats = 1 # only do this once def at_start(self): """ diff --git a/game/gamesrc/scripts/examples/script.py b/game/gamesrc/scripts/examples/script.py index 30e4b9db56..05b670fb95 100644 --- a/game/gamesrc/scripts/examples/script.py +++ b/game/gamesrc/scripts/examples/script.py @@ -20,44 +20,59 @@ dropped connections etc. """ -from ev import Script +from ev import Script as BaseScript + class ExampleScript(BaseScript): """ - A script type is customized by redefining some or all of its hook methods and variables. + A script type is customized by redefining some or all of its hook + methods and variables. * available properties key (string) - name of object name (string)- same as key - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + aliases (list of strings) - aliases to the object. Will be saved + to database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. + dbobj (Object, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings desc (string) - optional description of script, shown in listings - obj (Object) - optional object that this script is connected to and acts on (set automatically by obj.scripts.add()) - interval (int) - how often script should run, in seconds. <0 turns off ticker - start_delay (bool) - if the script should start repeating right away or wait self.interval seconds - repeats (int) - how many times the script should repeat before stopping. 0 means infinite repeats + obj (Object) - optional object that this script is connected to + and acts on (set automatically by obj.scripts.add()) + interval (int) - how often script should run, in seconds. <0 turns + off ticker + start_delay (bool) - if the script should start repeating right away or + wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. 0 means infinite repeats persistent (bool) - if script should survive a server shutdown or not is_active (bool) - if script is currently running * Handlers locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data * Helper methods - start() - start script (this usually happens automatically at creation and obj.script.add() etc) + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) stop() - stop script, and delete it - pause() - put the script on hold, until unpause() is called. If script is persistent, the pause state will survive a shutdown. - unpause() - restart a previously paused script. The script will continue as if it was never paused. - time_until_next_repeat() - if a timed script (interval>0), returns time until next tick + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will continue + from the paused timer (but at_start() will be called). + time_until_next_repeat() - if a timed script (interval>0), returns time + until next tick * Hook methods (should also include self as the first argument): @@ -71,16 +86,17 @@ class ExampleScript(BaseScript): actual combat going on). at_start() - Called every time the script is started, which for persistent scripts is at least once every server start. Note that this is - unaffected by self.delay_start, which only delays the first call - to at_repeat(). - at_repeat() - Called every self.interval seconds. It will be called immediately - upon launch unless self.delay_start is True, which will delay - the first call of this method by self.interval seconds. If - self.interval==0, this method will never be called. - at_stop() - Called as the script object is stopped and is about to be removed from - the game, e.g. because is_valid() returned False. - at_server_reload() - Called when server reloads. Can be used to save temporary - variables you want should survive a reload. + unaffected by self.delay_start, which only delays the first + call to at_repeat(). + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, which + will delay the first call of this method by self.interval + seconds. If self.interval==0, this method will never + be called. + at_stop() - Called as the script object is stopped and is about to be + removed from the game, e.g. because is_valid() returned False. + at_server_reload() - Called when server reloads. Can be used to + save temporary variables you want should survive a reload. at_server_shutdown() - called at a full server shutdown. """ diff --git a/game/gamesrc/world/__init__.py b/game/gamesrc/world/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/game/gamesrc/world/__init__.py +++ b/game/gamesrc/world/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/game/gamesrc/world/examples/batch_code.py b/game/gamesrc/world/examples/batch_code.py index 12f9fdb6d5..fe43209a40 100644 --- a/game/gamesrc/world/examples/batch_code.py +++ b/game/gamesrc/world/examples/batch_code.py @@ -17,20 +17,22 @@ # automatically be made available for each block. Observe # that changes to these variables made in one block is not # preserved between blocks!) -# #CODE (infotext) [objname, objname, ...] - This designates a code block that will be executed like a -# stand-alone piece of code together with any #HEADER -# defined. -# infotext is a describing text about what goes in in this block. It will be -# shown by the batchprocessing command. -# s mark the (variable-)names of objects created in the code, -# and which may be auto-deleted by the processor if desired (such as when -# debugging the script). E.g., if the code contains the command -# myobj = create.create_object(...), you could put 'myobj' in the #CODE header -# regardless of what the created object is actually called in-game. -# #INSERT filename - this includes another code batch file. The named file will be loaded and -# run at this point. Note that code from the inserted file will NOT share #HEADERs -# with the importing file, but will only use the headers in the importing file. -# make sure to not create a cyclic import here! +# #CODE (infotext) [objname, objname, ...] - This designates a code block that +# will be executed like a stand-alone piece of code together with +# any #HEADER defined. +# infotext is a describing text about what goes in in this block. +# It will be shown by the batchprocessing command. +# s mark the (variable-)names of objects created in +# the code, and which may be auto-deleted by the processor if +# desired (such as when debugging the script). E.g., if the code +# contains the command myobj = create.create_object(...), you could +# put 'myobj' in the #CODE header regardless of what the created +# object is actually called in-game. +# #INSERT filename - this includes another code batch file. The named file will +# be loaded and run at this point. Note that code from the inserted +# file will NOT share #HEADERs with the importing file, but will +# only use the headers in the importing file. Make sure to not +# create a cyclic import here! # The following variable is automatically made available for the script: diff --git a/game/manage.py b/game/manage.py index 3ef5384ec3..faf0cf9432 100755 --- a/game/manage.py +++ b/game/manage.py @@ -31,8 +31,10 @@ if not os.path.exists('settings.py'): # basic stuff. # make random secret_key. - import random, string - secret_key = list((string.letters + string.digits + string.punctuation).replace("\\","").replace("'",'"')) + import random + import string + secret_key = list((string.letters + + string.digits + string.punctuation).replace("\\", "").replace("'", '"')) random.shuffle(secret_key) secret_key = "".join(secret_key[:40]) diff --git a/game/runner.py b/game/runner.py index 584183ac8c..924fd9b955 100644 --- a/game/runner.py +++ b/game/runner.py @@ -82,6 +82,7 @@ if os.name == 'nt': # Functions + def set_restart_mode(restart_file, flag="reload"): """ This sets a flag file for the restart mode. @@ -89,6 +90,7 @@ def set_restart_mode(restart_file, flag="reload"): with open(restart_file, 'w') as f: f.write(str(flag)) + def get_restart_mode(restart_file): """ Parse the server/portal restart status @@ -98,6 +100,7 @@ def get_restart_mode(restart_file): return f.read() return "shutdown" + def get_pid(pidfile): """ Get the PID (Process ID) by trying to access @@ -109,6 +112,7 @@ def get_pid(pidfile): pid = f.read() return pid + def cycle_logfile(logfile): """ Rotate the old log files to .old @@ -126,13 +130,13 @@ def cycle_logfile(logfile): SERVER = None PORTAL = None + def start_services(server_argv, portal_argv): """ This calls a threaded loop that launces the Portal and Server and then restarts them when they finish. """ global SERVER, PORTAL - processes = Queue.Queue() def server_waiter(queue): @@ -141,7 +145,8 @@ def start_services(server_argv, portal_argv): except Exception, e: print "Server process error: %(e)s" % {'e': e} return - queue.put(("server_stopped", rc)) # this signals the controller that the program finished + # this signals the controller that the program finished + queue.put(("server_stopped", rc)) def portal_waiter(queue): try: @@ -149,7 +154,8 @@ def start_services(server_argv, portal_argv): except Exception, e: print "Portal process error: %(e)s" % {'e': e} return - queue.put(("portal_stopped", rc)) # this signals the controller that the program finished + # this signals the controller that the program finished + queue.put(("portal_stopped", rc)) if portal_argv: try: @@ -157,7 +163,8 @@ def start_services(server_argv, portal_argv): # start portal as interactive, reloadable thread PORTAL = thread.start_new_thread(portal_waiter, (processes, )) else: - # normal operation: start portal as a daemon; we don't care to monitor it for restart + # normal operation: start portal as a daemon; + # we don't care to monitor it for restart PORTAL = Popen(portal_argv) except IOError, e: print "Portal IOError: %s\nA possible explanation for this is that 'twistd' is not found." % e @@ -178,13 +185,15 @@ def start_services(server_argv, portal_argv): message, rc = processes.get() # restart only if process stopped cleanly - if message == "server_stopped" and int(rc) == 0 and get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset"): + if (message == "server_stopped" and int(rc) == 0 and + get_restart_mode(SERVER_RESTART) in ("True", "reload", "reset")): print "Evennia Server stopped. Restarting ..." SERVER = thread.start_new_thread(server_waiter, (processes, )) continue # normally the portal is not reloaded since it's run as a daemon. - if message == "portal_stopped" and int(rc) == 0 and get_restart_mode(PORTAL_RESTART) == "True": + if (message == "portal_stopped" and int(rc) == 0 and + get_restart_mode(PORTAL_RESTART) == "True"): print "Evennia Portal stopped in interactive mode. Restarting ..." PORTAL = thread.start_new_thread(portal_waiter, (processes, )) continue @@ -194,11 +203,12 @@ def start_services(server_argv, portal_argv): def main(): """ - This handles the command line input of the runner (it's most often called by evennia.py) + This handles the command line input of the runner + (it's most often called by evennia.py) """ parser = OptionParser(usage="%prog [options] start", - description="This runner should normally *not* be called directly - it is called automatically from the evennia.py main program. It manages the Evennia game server and portal processes an hosts a threaded loop to restart the Server whenever it is stopped (this constitues Evennia's reload mechanism).") + description="This runner should normally *not* be called directly - it is called automatically from the evennia.py main program. It manages the Evennia game server and portal processes an hosts a threaded loop to restart the Server whenever it is stopped (this constitues Evennia's reload mechanism).") parser.add_option('-s', '--noserver', action='store_true', dest='noserver', default=False, help='Do not start Server process') @@ -267,7 +277,6 @@ def main(): server_argv.extend(sprof_argv) print "\nRunning Evennia Server under cProfile." - # Portal pid = get_pid(PORTAL_PIDFILE) @@ -292,7 +301,6 @@ def main(): portal_argv.extend(pprof_argv) print "\nRunning Evennia Portal under cProfile." - # Windows fixes (Windows don't support pidfiles natively) if os.name == 'nt': if server_argv: diff --git a/src/__init__.py b/src/__init__.py index 2b758d664e..54e1114f71 100644 --- a/src/__init__.py +++ b/src/__init__.py @@ -1,2 +1,3 @@ -# experimental central dictionary for models in subprocesses to report they have been changed. +# experimental central dictionary for models in +# subprocesses to report they have been changed. PROC_MODIFIED_OBJS = [] diff --git a/src/commands/__init__.py b/src/commands/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/src/commands/__init__.py +++ b/src/commands/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index 005603178c..bf4650f94d 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -72,20 +72,25 @@ CMD_LOGINSTART = "__unloggedin_look_command" # custom Exceptions + class NoCmdSets(Exception): "No cmdsets found. Critical error." pass + + class ExecSystemCommand(Exception): "Run a system command" def __init__(self, syscmd, sysarg): - self.args = (syscmd, sysarg) # needed by exception error handling + self.args = (syscmd, sysarg) # needed by exception error handling self.syscmd = syscmd self.sysarg = sysarg # Helper function + @inlineCallbacks -def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None): +def get_and_merge_cmdsets(caller, session, player, obj, + callertype, sessid=None): """ Gather all relevant cmdsets and merge them. @@ -124,20 +129,25 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None) if location and not obj_cmdset.no_objs: # Gather all cmdsets stored on objects in the room and # also in the caller's inventory and the location itself - local_objlist = yield location.contents_get(exclude=obj.dbobj) + obj.contents + [location] + local_objlist = yield (location.contents_get(exclude=obj.dbobj) + + obj.contents + + [location]) for lobj in local_objlist: try: # call hook in case we need to do dynamic changing to cmdset _GA(lobj, "at_cmdset_get")() except Exception: logger.log_trace() - # the call-type lock is checked here, it makes sure a player is not seeing e.g. the commands - # on a fellow player (which is why the no_superuser_bypass must be True) - local_obj_cmdsets = yield [lobj.cmdset.current for lobj in local_objlist - if (lobj.cmdset.current and lobj.locks.check(caller, 'call', no_superuser_bypass=True))] + # the call-type lock is checked here, it makes sure a player + # is not seeing e.g. the commands on a fellow player (which is why + # the no_superuser_bypass must be True) + local_obj_cmdsets = \ + yield [lobj.cmdset.current for lobj in local_objlist + if (lobj.cmdset.current and + lobj.locks.check(caller, 'call', no_superuser_bypass=True))] for cset in local_obj_cmdsets: - #This is necessary for object sets, or we won't be able to separate - #the command sets from each other in a busy room. + #This is necessary for object sets, or we won't be able to + # separate the command sets from each other in a busy room. cset.old_duplicates = cset.duplicates cset.duplicates = True returnValue(local_obj_cmdsets) @@ -159,8 +169,8 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None) report_to = session session_cmdset = yield _get_cmdset(session) cmdsets = [session_cmdset] - if player: # this automatically implies logged-in - player_cmdset = yield _get_cmdset(player) + if player: # this automatically implies logged-in + player_cmdset = yield _get_cmdset(player) channel_cmdset = yield _get_channel_cmdsets(player, player_cmdset) cmdsets.extend([player_cmdset, channel_cmdset]) if obj: @@ -185,21 +195,26 @@ def get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid=None) cmdsets = [obj_cmdset] + local_obj_cmdsets else: raise Exception("get_and_merge_cmdsets: callertype %s is not valid." % callertype) - #cmdsets = yield [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_obj_cmdsets + #cmdsets = yield [caller_cmdset] + [player_cmdset] + + # [channel_cmdset] + local_obj_cmdsets # weed out all non-found sets - cmdsets = yield [cmdset for cmdset in cmdsets if cmdset and cmdset.key!="Empty"] + cmdsets = yield [cmdset for cmdset in cmdsets + if cmdset and cmdset.key != "Empty"] # report cmdset errors to user (these should already have been logged) - yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets if cmdset.key == "_CMDSET_ERROR"] + yield [report_to.msg(cmdset.errmessage) for cmdset in cmdsets + if cmdset.key == "_CMDSET_ERROR"] if cmdsets: - mergehash = tuple([id(cmdset) for cmdset in cmdsets]) # faster to do tuple on list than to build tuple directly + # faster to do tuple on list than to build tuple directly + mergehash = tuple([id(cmdset) for cmdset in cmdsets]) if mergehash in _CMDSET_MERGE_CACHE: # cached merge exist; use that cmdset = _CMDSET_MERGE_CACHE[mergehash] else: - # we group and merge all same-prio cmdsets separately (this avoids order-dependent - # clashes in certain cases, such as when duplicates=True) + # we group and merge all same-prio cmdsets separately (this avoids + # order-dependent clashes in certain cases, such as + # when duplicates=True) tempmergers = {} for cmdset in cmdsets: prio = cmdset.priority @@ -241,13 +256,13 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi if True, the command instance will be returned instead. callertype - this is one of "session", "player" or "object", in decending order. So when the Session is the caller, it will merge its - own cmdset into cmdsets from both Player and eventual puppeted Object (and - cmdsets in its room etc). A Player will only include its - own cmdset and the Objects and so on. Merge order is the - same order, so that Object cmdsets are merged in last, giving - them precendence for same-name and same-prio commands. - sessid - Relevant if callertype is "player" - the session id will help retrieve the - correct cmdsets from puppeted objects. + own cmdset into cmdsets from both Player and eventual puppeted + Object (and cmdsets in its room etc). A Player will only + include its own cmdset and the Objects and so on. Merge order + is the same order, so that Object cmdsets are merged in last, + giving them precendence for same-name and same-prio commands. + sessid - Relevant if callertype is "player" - the session id will help + retrieve the correct cmdsets from puppeted objects. Note that this function returns a deferred! """ @@ -270,10 +285,11 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi # we assign the caller with preference 'bottom up' caller = obj or player or session - try: # catch bugs in cmdhandler itself - try: # catch special-type commands + try: # catch bugs in cmdhandler itself + try: # catch special-type commands - cmdset = yield get_and_merge_cmdsets(caller, session, player, obj, callertype, sessid) + cmdset = yield get_and_merge_cmdsets(caller, session, player, obj, + callertype, sessid) if not cmdset: # this is bad and shouldn't happen. raise NoCmdSets @@ -323,14 +339,15 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi else: # fallback to default error text sysarg = _("Command '%s' is not available.") % raw_string - suggestions = string_suggestions(raw_string, cmdset.get_all_cmd_keys_and_aliases(caller), cutoff=0.7, maxnum=3) + suggestions = string_suggestions(raw_string, + cmdset.get_all_cmd_keys_and_aliases(caller), + cutoff=0.7, maxnum=3) if suggestions: sysarg += _(" Maybe you meant %s?") % utils.list_to_string(suggestions, _('or'), addquote=True) else: sysarg += _(" Type \"help\" for help.") raise ExecSystemCommand(syscmd, sysarg) - # Check if this is a Channel-cmd match. if hasattr(cmd, 'is_channel') and cmd.is_channel: # even if a user-defined syscmd is not defined, the @@ -380,7 +397,8 @@ def cmdhandler(called_by, raw_string, testing=False, callertype="session", sessi for func_part in make_iter(cmd.func_parts): err = yield func_part() # returning anything but a deferred/None will kill the chain - if err: break + if err: + break # post-command hook yield cmd.at_post_cmd() diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index e281c04aae..8b48bb84eb 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -51,10 +51,10 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): for cmd in cmdset: try: matches.extend([create_match(cmdname, raw_string, cmd) - for cmdname in [cmd.key] + cmd.aliases - if cmdname and l_raw_string.startswith(cmdname.lower()) - and (not cmd.arg_regex or - cmd.arg_regex.match(l_raw_string[len(cmdname):]))]) + for cmdname in [cmd.key] + cmd.aliases + if cmdname and l_raw_string.startswith(cmdname.lower()) + and (not cmd.arg_regex or + cmd.arg_regex.match(l_raw_string[len(cmdname):]))]) except Exception: log_trace("cmdhandler error. raw_input:%s" % raw_string) @@ -67,7 +67,8 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): if mindex.isdigit(): mindex = int(mindex) - 1 # feed result back to parser iteratively - return cmdparser(new_raw_string, cmdset, caller, match_index=mindex) + return cmdparser(new_raw_string, cmdset, + caller, match_index=mindex) # only select command matches we are actually allowed to call. matches = [match for match in matches if match[2].access(caller, 'cmd')] @@ -75,7 +76,8 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): if len(matches) > 1: # See if it helps to analyze the match with preserved case but only if # it leaves at least one match. - trimmed = [match for match in matches if raw_string.startswith(match[0])] + trimmed = [match for match in matches + if raw_string.startswith(match[0])] if trimmed: matches = trimmed @@ -94,15 +96,13 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): matches = matches[-quality.count(quality[-1]):] if len(matches) > 1 and match_index != None and 0 <= match_index < len(matches): - # We couldn't separate match by quality, but we have an index argument to - # tell us which match to use. + # We couldn't separate match by quality, but we have an + # index argument to tell us which match to use. matches = [matches[match_index]] # no matter what we have at this point, we have to return it. return matches - - #------------------------------------------------------------ # Search parsers and support methods #------------------------------------------------------------ @@ -118,7 +118,6 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): # The the replacing modules must have the same inputs and outputs as # those in this module. # - def at_search_result(msg_obj, ostring, results, global_search=False, nofound_string=None, multimatch_string=None): """ @@ -176,7 +175,7 @@ def at_search_result(msg_obj, ostring, results, global_search=False, invtext = _(" (carried)") if show_dbref: dbreftext = "(#%i)" % result.dbid - string += "\n %i-%s%s%s" % (num+1, result.name, + string += "\n %i-%s%s%s" % (num + 1, result.name, dbreftext, invtext) results = None else: @@ -187,6 +186,7 @@ def at_search_result(msg_obj, ostring, results, global_search=False, msg_obj.msg(string.strip()) return results + def at_multimatch_input(ostring): """ Parse number-identifiers. @@ -231,9 +231,9 @@ def at_multimatch_input(ostring): if not '-' in ostring: return (None, ostring) try: - index = ostring.find('-') - number = int(ostring[:index])-1 - return (number, ostring[index+1:]) + index = ostring.find('-') + number = int(ostring[:index]) - 1 + return (number, ostring[index + 1:]) except ValueError: #not a number; this is not an identifier. return (None, ostring) @@ -256,13 +256,15 @@ def at_multimatch_cmd(caller, matches): else: is_channel = "" if cmd.is_exit and cmd.destination: - is_exit = _(" (exit to %s)") % cmd.destination + is_exit = (" (exit to %s)") % cmd.destination else: is_exit = "" id1 = "" id2 = "" - if not (is_channel or is_exit) and (hasattr(cmd, 'obj') and cmd.obj != caller) and hasattr(cmd.obj, "key"): + if (not (is_channel or is_exit) and + (hasattr(cmd, 'obj') and cmd.obj != caller) and + hasattr(cmd.obj, "key")): # the command is defined on some other object id1 = "%s-%s" % (num + 1, cmdname) id2 = " (%s)" % (cmd.obj.key) diff --git a/src/commands/cmdset.py b/src/commands/cmdset.py index 4d2b3bda76..8b964bf0e4 100644 --- a/src/commands/cmdset.py +++ b/src/commands/cmdset.py @@ -18,6 +18,7 @@ from django.utils.translation import ugettext as _ from src.utils.utils import inherits_from, is_iter __all__ = ("CmdSet",) + class _CmdSetMeta(type): """ This metaclass makes some minor on-the-fly convenience fixes to @@ -38,15 +39,18 @@ class _CmdSetMeta(type): super(_CmdSetMeta, mcs).__init__(*args, **kwargs) + class CmdSet(object): """ This class describes a unique cmdset that understands priorities. CmdSets can be merged and made to perform various set operations on each other. - CmdSets have priorities that affect which of their ingoing commands gets used. + CmdSets have priorities that affect which of their ingoing commands + gets used. In the examples, cmdset A always have higher priority than cmdset B. - key - the name of the cmdset. This can be used on its own for game operations + key - the name of the cmdset. This can be used on its own for game + operations mergetype (partly from Set theory): @@ -80,8 +84,9 @@ class CmdSet(object): priority- All cmdsets are always merged in pairs of two so that the higher set's mergetype is applied to the lower-priority cmdset. Default commands have priority 0, - high-priority ones like Exits and Channels have 10 and 9. Priorities - can be negative as well to give default commands preference. + high-priority ones like Exits and Channels have 10 and 9. + Priorities can be negative as well to give default + commands preference. duplicates - determines what happens when two sets of equal priority merge. Default has the first of them in the @@ -130,16 +135,17 @@ class CmdSet(object): permanent = False errmessage = "" # pre-store properties to duplicate straight off - to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs", "no_channels", "permanent", - "mergetype", "priority", "duplicates", "errmessage") + to_duplicate = ("key", "cmdsetobj", "no_exits", "no_objs", + "no_channels", "permanent", "mergetype", + "priority", "duplicates", "errmessage") def __init__(self, cmdsetobj=None, key=None): """ Creates a new CmdSet instance. cmdsetobj - this is the database object to which this particular - instance of cmdset is related. It is often a player but may also be a - regular object. + instance of cmdset is related. It is often a character but + may also be a regular object. """ if key: self.key = key @@ -161,7 +167,8 @@ class CmdSet(object): if cmdset_a.duplicates and cmdset_a.priority == cmdset_b.priority: cmdset_c.commands.extend(cmdset_b.commands) else: - cmdset_c.commands.extend([cmd for cmd in cmdset_b if not cmd in cmdset_a]) + cmdset_c.commands.extend([cmd for cmd in cmdset_b + if not cmd in cmdset_a]) return cmdset_c def _intersect(self, cmdset_a, cmdset_b): @@ -206,7 +213,8 @@ class CmdSet(object): cmdset = CmdSet() for key, val in ((key, getattr(self, key)) for key in self.to_duplicate): if val != getattr(cmdset, key): - # only copy if different from default; avoid turning class-vars into instance vars + # only copy if different from default; avoid turning + # class-vars into instance vars setattr(cmdset, key, val) cmdset.key_mergetypes = self.key_mergetypes.copy() return cmdset @@ -230,10 +238,11 @@ class CmdSet(object): def __contains__(self, othercmd): """ Returns True if this cmdset contains the given command (as defined - by command name and aliases). This allows for things like 'if cmd in cmdset' + by command name and aliases). This allows for things + like 'if cmd in cmdset' """ ret = self._contains_cache.get(othercmd) - if ret == None: + if ret is None: ret = othercmd in self.commands self._contains_cache[othercmd] = ret return ret @@ -264,7 +273,8 @@ class CmdSet(object): # A higher or equal priority than B # preserve system __commands - sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b if cmd not in sys_commands_a] + sys_commands = sys_commands_a + [cmd for cmd in sys_commands_b + if cmd not in sys_commands_a] mergetype = self.key_mergetypes.get(cmdset_b.key, self.mergetype) if mergetype == "Intersect": @@ -286,7 +296,8 @@ class CmdSet(object): # B higher priority than A # preserver system __commands - sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a if cmd not in sys_commands_b] + sys_commands = sys_commands_b + [cmd for cmd in sys_commands_a + if cmd not in sys_commands_b] mergetype = cmdset_b.key_mergetypes.get(self.key, cmdset_b.mergetype) if mergetype == "Intersect": @@ -295,7 +306,7 @@ class CmdSet(object): cmdset_c = self._replace(cmdset_b, self) elif mergetype == "Remove": cmdset_c = self._remove(self, cmdset_b) - else: # Union + else: # Union cmdset_c = self._union(cmdset_b, self) cmdset_c.no_channels = cmdset_b.no_channels cmdset_c.no_exits = cmdset_b.no_exits @@ -311,10 +322,8 @@ class CmdSet(object): # return the system commands to the cmdset cmdset_c.add(sys_commands) - return cmdset_c - def add(self, cmd): """ Add a command, a list of commands or a cmdset to this cmdset. @@ -338,9 +347,12 @@ class CmdSet(object): try: cmd = self._instantiate(cmd) except RuntimeError: - string = "Adding cmdset %(cmd)s to %(class)s lead to an infinite loop. When adding a cmdset to another, " - string += "make sure they are not themself cyclically added to the new cmdset somewhere in the chain." - raise RuntimeError(_(string) % {"cmd":cmd, "class":self.__class__}) + string = "Adding cmdset %(cmd)s to %(class)s lead to an " + string += "infinite loop. When adding a cmdset to another, " + string += "make sure they are not themself cyclically added to " + string += "the new cmdset somewhere in the chain." + raise RuntimeError(_(string) % {"cmd": cmd, + "class": self.__class__}) cmds = cmd.commands elif is_iter(cmd): cmds = [self._instantiate(c) for c in cmd] @@ -354,7 +366,7 @@ class CmdSet(object): cmd.obj = self.cmdsetobj try: ic = commands.index(cmd) - commands[ic] = cmd # replace + commands[ic] = cmd # replace except ValueError: commands.append(cmd) # extra run to make sure to avoid doublets @@ -365,11 +377,10 @@ class CmdSet(object): if cmd.key.startswith("__"): try: ic = system_commands.index(cmd) - system_commands[ic] = cmd # replace + system_commands[ic] = cmd # replace except ValueError: system_commands.append(cmd) - def remove(self, cmd): """ Remove a command instance from the cmdset. @@ -431,7 +442,8 @@ class CmdSet(object): """ names = [] if caller: - [names.extend(cmd._keyaliases) for cmd in self.commands if cmd.access(caller)] + [names.extend(cmd._keyaliases) for cmd in self.commands + if cmd.access(caller)] else: [names.extend(cmd._keyaliases) for cmd in self.commands] return names diff --git a/src/commands/cmdsethandler.py b/src/commands/cmdsethandler.py index 59a3d658b1..573c19de13 100644 --- a/src/commands/cmdsethandler.py +++ b/src/commands/cmdsethandler.py @@ -63,7 +63,6 @@ can then implement separate sets for different situations. For example, you can have a 'On a boat' set, onto which you then tack on the 'Fishing' set. Fishing from a boat? No problem! """ -import traceback from src.utils import logger, utils from src.commands.cmdset import CmdSet from src.server.models import ServerConfig @@ -73,23 +72,27 @@ __all__ = ("import_cmdset", "CmdSetHandler") _CACHED_CMDSETS = {} + class _ErrorCmdSet(CmdSet): "This is a special cmdset used to report errors" key = "_CMDSET_ERROR" errmessage = "Error when loading cmdset." + def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False): """ This helper function is used by the cmdsethandler to load a cmdset instance from a python module, given a python_path. It's usually accessed through the cmdsethandler's add() and add_default() methods. python_path - This is the full path to the cmdset object. - cmdsetobj - the database object/typeclass on which this cmdset is to be assigned - (this can be also channels and exits, as well as players but there will - always be such an object) - emit_to_obj - if given, error is emitted to this object (in addition to logging) - no_logging - don't log/send error messages. This can be useful if import_cmdset is just - used to check if this is a valid python path or not. + cmdsetobj - the database object/typeclass on which this cmdset is to be + assigned (this can be also channels and exits, as well as players + but there will always be such an object) + emit_to_obj - if given, error is emitted to this object (in addition + to logging) + no_logging - don't log/send error messages. This can be useful + if import_cmdset is just used to check if this is a + valid python path or not. function returns None if an error was encountered or path not found. """ @@ -117,7 +120,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False): raise except KeyError: errstring = _("Error in loading cmdset: No cmdset class '%(classname)s' in %(modulepath)s.") - errstring = errstring % {"classname":classname, "modulepath":modulepath} + errstring = errstring % {"classname": classname, + "modulepath": modulepath} raise except Exception: errstring = _("Compile/Run error when loading cmdset '%s'. Error was logged.") @@ -135,15 +139,17 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False): # classes + class CmdSetHandler(object): """ - The CmdSetHandler is always stored on an object, this object is supplied as an argument. + The CmdSetHandler is always stored on an object, this object is supplied + as an argument. The 'current' cmdset is the merged set currently active for this object. This is the set the game engine will retrieve when determining which - commands are available to the object. The cmdset_stack holds a history of all CmdSets - to allow the handler to remove/add cmdsets at will. Doing so will re-calculate - the 'current' cmdset. + commands are available to the object. The cmdset_stack holds a history of + all CmdSets to allow the handler to remove/add cmdsets at will. Doing so + will re-calculate the 'current' cmdset. """ def __init__(self, obj): @@ -176,10 +182,8 @@ class CmdSetHandler(object): mergelist = [] if len(self.cmdset_stack) > 1: # We have more than one cmdset in stack; list them all - num = 0 #print self.cmdset_stack, self.mergetype_stack for snum, cmdset in enumerate(self.cmdset_stack): - num = snum mergetype = self.mergetype_stack[snum] permstring = "non-perm" if cmdset.permanent: @@ -196,17 +200,21 @@ class CmdSetHandler(object): mergetype = self.mergetype_stack[-1] if mergetype != self.current.mergetype: merged_on = self.cmdset_stack[-2].key - mergetype = _("custom %(mergetype)s on cmdset '%(merged_on)s'") % {"mergetype":mergetype, "merged_on":merged_on} + mergetype = _("custom %(mergetype)s on cmdset '%(merged_on)s'") % \ + {"mergetype": mergetype, "merged_on":merged_on} if mergelist: string += _(" : %(current)s") % \ - {"mergelist": "+".join(mergelist), "mergetype":mergetype, "prio":self.current.priority, "current":self.current} + {"mergelist": "+".join(mergelist), + "mergetype": mergetype, "prio": self.current.priority, + "current":self.current} else: permstring = "non-perm" if self.current.permanent: permstring = "perm" string += _(" <%(key)s (%(mergetype)s, prio %(prio)i, %(permstring)s)>: %(keylist)s") % \ - {"key":self.current.key, "mergetype":mergetype, "prio":self.current.priority, "permstring":permstring, - "keylist":", ".join(cmd.key for cmd in sorted(self.current, key=lambda o:o.key))} + {"key": self.current.key, "mergetype": mergetype, + "prio": self.current.priority, "permstring": permstring, + "keylist": ", ".join(cmd.key for cmd in sorted(self.current, key=lambda o: o.key))} return string.strip() def _import_cmdset(self, cmdset_path, emit_to_obj=None): @@ -362,10 +370,12 @@ class CmdSetHandler(object): else: # try it as a callable if callable(cmdset) and hasattr(cmdset, 'path'): - delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset.path] + delcmdsets = [cset for cset in self.cmdset_stack[1:] + if cset.path == cmdset.path] else: # try it as a path or key - delcmdsets = [cset for cset in self.cmdset_stack[1:] if cset.path == cmdset or cset.key == cmdset] + delcmdsets = [cset for cset in self.cmdset_stack[1:] + if cset.path == cmdset or cset.key == cmdset] storage = [] if any(cset.permanent for cset in delcmdsets): @@ -387,7 +397,10 @@ class CmdSetHandler(object): self.update() def delete_default(self): - "This explicitly deletes the default cmdset. It's the only command that can." + """ + This explicitly deletes the default cmdset. It's the + only command that can. + """ if self.cmdset_stack: cmdset = self.cmdset_stack[0] if cmdset.permanent: @@ -404,7 +417,8 @@ class CmdSetHandler(object): def all(self): """ - Returns the list of cmdsets. Mostly useful to check if stack if empty or not. + Returns the list of cmdsets. Mostly useful to check + if stack if empty or not. """ return self.cmdset_stack @@ -431,13 +445,6 @@ class CmdSetHandler(object): else: return any([cmdset.key == cmdset_key for cmdset in self.cmdset_stack]) - - def all(self): - """ - Returns all cmdsets. - """ - return self.cmdset_stack - def reset(self): """ Force reload of all cmdsets in handler. This should be called diff --git a/src/commands/command.py b/src/commands/command.py index 3e2fd19a14..3366fbe1a9 100644 --- a/src/commands/command.py +++ b/src/commands/command.py @@ -9,6 +9,7 @@ import re from src.locks.lockhandler import LockHandler from src.utils.utils import is_iter, fill + def _init_command(mcs, **kwargs): """ Helper command. @@ -17,20 +18,23 @@ def _init_command(mcs, **kwargs): Sets up locks to be more forgiving. This is used both by the metaclass and (optionally) at instantiation time. - If kwargs are given, these are set as instance-specific properties on the command. + If kwargs are given, these are set as instance-specific properties + on the command. """ for i in range(len(kwargs)): - # used for dynamic creation of commands - key, value = kwargs.popitem() - setattr(mcs, key, value) + # used for dynamic creation of commands + key, value = kwargs.popitem() + setattr(mcs, key, value) mcs.key = mcs.key.lower() if mcs.aliases and not is_iter(mcs.aliases): try: - mcs.aliases = [str(alias).strip().lower() for alias in mcs.aliases.split(',')] + mcs.aliases = [str(alias).strip().lower() + for alias in mcs.aliases.split(',')] except Exception: mcs.aliases = [] - mcs.aliases = list(set(alias for alias in mcs.aliases if alias and alias != mcs.key)) + mcs.aliases = list(set(alias for alias in mcs.aliases + if alias and alias != mcs.key)) # optimization - a set is much faster to match against than a list mcs._matchset = set([mcs.key] + mcs.aliases) @@ -84,6 +88,7 @@ class CommandMeta(type): # structure can parse the input string the same way, minimizing # parsing errors. + class Command(object): """ Base command @@ -112,13 +117,16 @@ class Command(object): key - identifier for command (e.g. "look") aliases - (optional) list of aliases (e.g. ["l", "loo"]) locks - lock string (default is "cmd:all()") - help_category - how to organize this help entry in help system (default is "General") + help_category - how to organize this help entry in help system + (default is "General") auto_help - defaults to True. Allows for turning off auto-help generation - arg_regex - (optional) raw string regex defining how the argument part of the command should look - in order to match for this command (e.g. must it be a space between cmdname and arg?) + arg_regex - (optional) raw string regex defining how the argument part of + the command should look in order to match for this command + (e.g. must it be a space between cmdname and arg?) - (Note that if auto_help is on, this initial string is also used by the system - to create the help entry for the command, so it's a good idea to format it similar to this one) + (Note that if auto_help is on, this initial string is also used by the + system to create the help entry for the command, so it's a good idea to + format it similar to this one) """ # Tie our metaclass, for some convenience cleanup __metaclass__ = CommandMeta @@ -127,7 +135,8 @@ class Command(object): key = "command" # alternative ways to call the command (e.g. 'l', 'glance', 'examine') aliases = [] - # a list of lock definitions on the form cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args) + # a list of lock definitions on the form + # cmd:[NOT] func(args) [ AND|OR][ NOT] func2(args) locks = "" # used by the help system to group commands in lists. help_category = "general" @@ -136,7 +145,8 @@ class Command(object): auto_help = True # auto-set (by Evennia on command instantiation) are: # obj - which object this command is defined on - # sessid - which session-id (if any) is responsible for triggering this command + # sessid - which session-id (if any) is responsible for + # triggering this command # def __init__(self, **kwargs): @@ -206,20 +216,22 @@ class Command(object): """ return self.lockhandler.check(srcobj, access_type, default=default) - def msg(self, msg="", to_obj=None, from_obj=None, sessid=None, all_sessions=False, **kwargs): + def msg(self, msg="", to_obj=None, from_obj=None, + sessid=None, all_sessions=False, **kwargs): """ - This is a shortcut instad of calling msg() directly on an object - it will - detect if caller is an Object or a Player and also appends self.sessid - automatically. + This is a shortcut instad of calling msg() directly on an object - it + will detect if caller is an Object or a Player and also appends + self.sessid automatically. msg - text string of message to send to_obj - target object of message. Defaults to self.caller from_obj - source of message. Defaults to to_obj data - optional dictionary of data - sessid - supply data only to a unique sessid (normally not used - this is only potentially useful if - to_obj is a Player object different from self.caller or self.caller.player) - all_sessions (bool) - default is to send only to the session connected to - the target object + sessid - supply data only to a unique sessid (normally not used - + this is only potentially useful if to_obj is a Player object + different from self.caller or self.caller.player) + all_sessions (bool) - default is to send only to the session + connected to the target object """ from_obj = from_obj or self.caller to_obj = to_obj or from_obj diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index f527b64ca2..1b0b975cb9 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -4,7 +4,8 @@ Admin commands """ -import time, re +import time +import re from django.conf import settings from django.contrib.auth.models import User from src.server.sessionhandler import SESSIONS @@ -15,8 +16,8 @@ from src.commands.default.muxcommand import MuxCommand PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] # limit members for API inclusion -__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer", "CmdEmit", "CmdNewPassword", - "CmdPerm", "CmdWall") +__all__ = ("CmdBoot", "CmdBan", "CmdUnban", "CmdDelPlayer", + "CmdEmit", "CmdNewPassword", "CmdPerm", "CmdWall") class CmdBoot(MuxCommand): @@ -98,6 +99,7 @@ class CmdBoot(MuxCommand): # regex matching IP addresses with wildcards, eg. 233.122.4.* IPREGEX = re.compile(r"[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}\.[0-9*]{1,3}") + def list_bans(banlist): """ Helper function to display a list of active bans. Input argument @@ -108,12 +110,13 @@ def list_bans(banlist): table = prettytable.PrettyTable(["{wid", "{wname/ip", "{wdate", "{wreason"]) for inum, ban in enumerate(banlist): - table.add_row([str(inum+1), + table.add_row([str(inum + 1), ban[0] and ban[0] or ban[1], ban[3], ban[4]]) string = "{wActive bans:{n\n%s" % table return string + class CmdBan(MuxCommand): """ ban a player from the server @@ -152,7 +155,7 @@ class CmdBan(MuxCommand): key = "@ban" aliases = ["@bans"] locks = "cmd:perm(ban) or perm(Immortals)" - help_category="Admin" + help_category = "Admin" def func(self): """ @@ -172,14 +175,15 @@ class CmdBan(MuxCommand): banlist = [] if not self.args or (self.switches - and not any(switch in ('ip', 'name') for switch in self.switches)): + and not any(switch in ('ip', 'name') + for switch in self.switches)): self.caller.msg(list_bans(banlist)) return now = time.ctime() reason = "" if ':' in self.args: - ban, reason = self.args.rsplit(':',1) + ban, reason = self.args.rsplit(':', 1) else: ban = self.args ban = ban.lower() @@ -193,7 +197,7 @@ class CmdBan(MuxCommand): typ = "IP" ban = ipban[0] # replace * with regex form and compile it - ipregex = ban.replace('.','\.') + ipregex = ban.replace('.', '\.') ipregex = ipregex.replace('*', '[0-9]{1,3}') #print "regex:",ipregex ipregex = re.compile(r"%s" % ipregex) @@ -203,6 +207,7 @@ class CmdBan(MuxCommand): ServerConfig.objects.conf('server_bans', banlist) self.caller.msg("%s-Ban {w%s{x was added." % (typ, ban)) + class CmdUnban(MuxCommand): """ remove a ban @@ -218,7 +223,7 @@ class CmdUnban(MuxCommand): """ key = "@unban" locks = "cmd:perm(unban) or perm(Immortals)" - help_category="Admin" + help_category = "Admin" def func(self): "Implement unbanning" @@ -241,10 +246,11 @@ class CmdUnban(MuxCommand): self.caller.msg("Ban id {w%s{x was not found." % self.args) else: # all is ok, clear ban - ban = banlist[num-1] - del banlist[num-1] + ban = banlist[num - 1] + del banlist[num - 1] ServerConfig.objects.conf('server_bans', banlist) - self.caller.msg("Cleared ban %s: %s" % (num, " ".join([s for s in ban[:2]]))) + self.caller.msg("Cleared ban %s: %s" % + (num, " ".join([s for s in ban[:2]]))) class CmdDelPlayer(MuxCommand): @@ -408,7 +414,7 @@ class CmdEmit(MuxCommand): obj = caller.search(objname, global_search=True) if not obj: return - if rooms_only and not obj.location == None: + if rooms_only and not obj.location is None: caller.msg("%s is not a room. Ignored." % objname) continue if players_only and not obj.has_player: @@ -425,7 +431,6 @@ class CmdEmit(MuxCommand): caller.msg("You are not allowed to emit to %s." % objname) - class CmdNewPassword(MuxCommand): """ @userpassword @@ -457,7 +462,8 @@ class CmdNewPassword(MuxCommand): player.user.save() self.msg("%s - new password set to '%s'." % (player.name, self.rhs)) if player.character != caller: - player.msg("%s has changed your password to '%s'." % (caller.name, self.rhs)) + player.msg("%s has changed your password to '%s'." % (caller.name, + self.rhs)) class CmdPerm(MuxCommand): @@ -497,7 +503,7 @@ class CmdPerm(MuxCommand): if playermode: obj = caller.search_player(lhs) else: - obj = caller.search(lhs, global_search=True) + obj = caller.search(lhs, global_search=True) if not obj: return @@ -511,7 +517,9 @@ class CmdPerm(MuxCommand): string += "" else: string += ", ".join(obj.permissions.all()) - if hasattr(obj, 'player') and hasattr(obj.player, 'is_superuser') and obj.player.is_superuser: + if (hasattr(obj, 'player') and + hasattr(obj.player, 'is_superuser') and + obj.player.is_superuser): string += "\n(... but this object is currently controlled by a SUPERUSER! " string += "All access checks are passed automatically.)" caller.msg(string) @@ -539,9 +547,10 @@ class CmdPerm(MuxCommand): for perm in self.rhslist: - # don't allow to set a permission higher in the hierarchy than the one the - # caller has (to prevent self-escalation) - if perm.lower() in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm): + # don't allow to set a permission higher in the hierarchy than + # the one the caller has (to prevent self-escalation) + if (perm.lower() in PERMISSION_HIERARCHY and not + obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm)): caller.msg("You cannot assign a permission higher than the one you have yourself.") return diff --git a/src/commands/default/batchprocess.py b/src/commands/default/batchprocess.py index 9830b6de31..fa203630d1 100644 --- a/src/commands/default/batchprocess.py +++ b/src/commands/default/batchprocess.py @@ -95,11 +95,12 @@ def format_header(caller, entry): stacklen = len(caller.ndb.batch_stack) header = "{w%02i/%02i{G: %s{n" % (ptr, stacklen, header) # add extra space to the side for padding. - header = "%s%s" % (header, " "*(width - len(header))) + header = "%s%s" % (header, " " * (width - len(header))) header = header.replace('\n', '\\n') return header + def format_code(entry): """ Formats the viewing of code and errors @@ -109,6 +110,7 @@ def format_code(entry): code += "\n{G>>>{n %s" % line return code.strip() + def batch_cmd_exec(caller): """ Helper function for executing a single batch-command entry @@ -124,6 +126,7 @@ def batch_cmd_exec(caller): return False return True + def batch_code_exec(caller): """ Helper function for executing a single batch-code entry @@ -135,12 +138,13 @@ def batch_code_exec(caller): caller.msg(format_header(caller, codedict['code'])) err = BATCHCODE.code_exec(codedict, - extra_environ={"caller":caller}, debug=debug) + extra_environ={"caller": caller}, debug=debug) if err: caller.msg(format_code(err)) return False return True + def step_pointer(caller, step=1): """ Step in stack, returning the item located. @@ -156,7 +160,8 @@ def step_pointer(caller, step=1): caller.msg("{RBeginning of batch file.") if ptr + step >= nstack: caller.msg("{REnd of batch file.") - caller.ndb.batch_stackptr = max(0, min(nstack-1, ptr + step)) + caller.ndb.batch_stackptr = max(0, min(nstack - 1, ptr + step)) + def show_curr(caller, showall=False): """ @@ -186,6 +191,7 @@ def show_curr(caller, showall=False): string += "\n{G|{n %s" % line caller.msg(string) + def purge_processor(caller): """ This purges all effects running @@ -201,12 +207,13 @@ def purge_processor(caller): # clear everything but the default cmdset. caller.cmdset.delete(BatchSafeCmdSet) caller.cmdset.clear() - caller.scripts.validate() # this will purge interactive mode + caller.scripts.validate() # this will purge interactive mode #------------------------------------------------------------ # main access commands #------------------------------------------------------------ + class CmdBatchCommands(MuxCommand): """ Build from batch-command file @@ -275,19 +282,25 @@ class CmdBatchCommands(MuxCommand): procpool = False if "PythonProcPool" in utils.server_services(): if utils.uses_database("sqlite3"): - caller.msg("Batchprocessor disabled ProcPool under SQLite3.") + caller.msg("Batchprocessor disabled ProcPool under SQLite3.") else: - procpool=True + procpool = True if procpool: # run in parallel process def callback(r): caller.msg(" {GBatchfile '%s' applied." % python_path) purge_processor(caller) + def errback(e): caller.msg(" {RError from processor: '%s'" % e) purge_processor(caller) - utils.run_async(_PROCPOOL_BATCHCMD_SOURCE, commands=commands, caller=caller, at_return=callback, at_err=errback) + + utils.run_async(_PROCPOOL_BATCHCMD_SOURCE, + commands=commands, + caller=caller, + at_return=callback, + at_err=errback) else: # run in-process (might block) for inum in range(len(commands)): @@ -295,11 +308,13 @@ class CmdBatchCommands(MuxCommand): if not batch_cmd_exec(caller): return step_pointer(caller, 1) - # clean out the safety cmdset and clean out all other temporary attrs. + # clean out the safety cmdset and clean out all other + # temporary attrs. string = " Batchfile '%s' applied." % python_path caller.msg("{G%s" % string) purge_processor(caller) + class CmdBatchCode(MuxCommand): """ Build from batch-code file @@ -352,7 +367,7 @@ class CmdBatchCode(MuxCommand): debug = False if 'debug' in switches: - debug = True + debug = True # Store work data in cache caller.ndb.batch_stack = codes @@ -376,18 +391,23 @@ class CmdBatchCode(MuxCommand): procpool = False if "PythonProcPool" in utils.server_services(): if utils.uses_database("sqlite3"): - caller.msg("Batchprocessor disabled ProcPool under SQLite3.") + caller.msg("Batchprocessor disabled ProcPool under SQLite3.") else: - procpool=True + procpool = True if procpool: # run in parallel process def callback(r): caller.msg(" {GBatchfile '%s' applied." % python_path) purge_processor(caller) + def errback(e): caller.msg(" {RError from processor: '%s'" % e) purge_processor(caller) - utils.run_async(_PROCPOOL_BATCHCODE_SOURCE, codes=codes, caller=caller, at_return=callback, at_err=errback) + utils.run_async(_PROCPOOL_BATCHCODE_SOURCE, + codes=codes, + caller=caller, + at_return=callback, + at_err=errback) else: # un in-process (will block) for inum in range(len(codes)): @@ -395,7 +415,8 @@ class CmdBatchCode(MuxCommand): if not batch_code_exec(caller): return step_pointer(caller, 1) - # clean out the safety cmdset and clean out all other temporary attrs. + # clean out the safety cmdset and clean out all other + # temporary attrs. string = " Batchfile '%s' applied." % python_path caller.msg("{G%s" % string) purge_processor(caller) @@ -423,6 +444,7 @@ class CmdStateAbort(MuxCommand): purge_processor(self.caller) self.caller.msg("Exited processor and reset out active cmdset back to the default one.") + class CmdStateLL(MuxCommand): """ ll @@ -479,6 +501,7 @@ class CmdStateRR(MuxCommand): caller.msg(format_code("File reloaded. Staying on same command.")) show_curr(caller) + class CmdStateRRR(MuxCommand): """ rrr @@ -500,6 +523,7 @@ class CmdStateRRR(MuxCommand): caller.msg(format_code("File reloaded. Restarting from top.")) show_curr(caller) + class CmdStateNN(MuxCommand): """ nn @@ -520,6 +544,7 @@ class CmdStateNN(MuxCommand): step_pointer(caller, step) show_curr(caller) + class CmdStateNL(MuxCommand): """ nl @@ -541,6 +566,7 @@ class CmdStateNL(MuxCommand): step_pointer(caller, step) show_curr(caller, showall=True) + class CmdStateBB(MuxCommand): """ bb @@ -562,6 +588,7 @@ class CmdStateBB(MuxCommand): step_pointer(caller, step) show_curr(caller) + class CmdStateBL(MuxCommand): """ bl @@ -583,6 +610,7 @@ class CmdStateBL(MuxCommand): step_pointer(caller, step) show_curr(caller, showall=True) + class CmdStateSS(MuxCommand): """ ss [steps] @@ -611,6 +639,7 @@ class CmdStateSS(MuxCommand): step_pointer(caller, 1) show_curr(caller) + class CmdStateSL(MuxCommand): """ sl [steps] @@ -639,6 +668,7 @@ class CmdStateSL(MuxCommand): step_pointer(caller, 1) show_curr(caller) + class CmdStateCC(MuxCommand): """ cc @@ -670,6 +700,7 @@ class CmdStateCC(MuxCommand): del caller.ndb.batch_batchmode caller.msg(format_code("Finished processing batch file.")) + class CmdStateJJ(MuxCommand): """ j @@ -684,7 +715,7 @@ class CmdStateJJ(MuxCommand): caller = self.caller arg = self.args if arg and arg.isdigit(): - number = int(self.args)-1 + number = int(self.args) - 1 else: caller.msg(format_code("You must give a number index.")) return @@ -693,6 +724,7 @@ class CmdStateJJ(MuxCommand): step_pointer(caller, step) show_curr(caller) + class CmdStateJL(MuxCommand): """ jl @@ -707,7 +739,7 @@ class CmdStateJL(MuxCommand): caller = self.caller arg = self.args if arg and arg.isdigit(): - number = int(self.args)-1 + number = int(self.args) - 1 else: caller.msg(format_code("You must give a number index.")) return @@ -716,6 +748,7 @@ class CmdStateJL(MuxCommand): step_pointer(caller, step) show_curr(caller, showall=True) + class CmdStateQQ(MuxCommand): """ qq @@ -730,6 +763,7 @@ class CmdStateQQ(MuxCommand): purge_processor(self.caller) self.caller.msg("Aborted interactive batch mode.") + class CmdStateHH(MuxCommand): "Help command" @@ -766,7 +800,6 @@ class CmdStateHH(MuxCommand): self.caller.msg(string) - #------------------------------------------------------------ # # Defining the cmdsets for the interactive batchprocessor @@ -781,12 +814,13 @@ class BatchSafeCmdSet(CmdSet): always be available to get out of everything. """ key = "Batch_default" - priority = 104 # override other cmdsets. + priority = 104 # override other cmdsets. def at_cmdset_creation(self): "Init the cmdset" self.add(CmdStateAbort()) + class BatchInteractiveCmdSet(CmdSet): """ The cmdset for the interactive batch processor mode. diff --git a/src/commands/default/building.py b/src/commands/default/building.py index 4e37874054..867a253a74 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -29,6 +29,7 @@ except ImportError: # used by @find CHAR_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS + class ObjManipCommand(MuxCommand): """ This is a parent class for some of the defining objmanip commands @@ -60,7 +61,7 @@ class ObjManipCommand(MuxCommand): # get all the normal parsing done (switches etc) super(ObjManipCommand, self).parse() - obj_defs = ([],[]) # stores left- and right-hand side of '=' + obj_defs = ([], []) # stores left- and right-hand side of '=' obj_attrs = ([], []) # " for iside, arglist in enumerate((self.lhslist, self.rhslist)): @@ -101,7 +102,7 @@ class CmdSetObjAlias(MuxCommand): by everyone. """ - key = "@alias" + key = "@alias" aliases = "@setobjalias" locks = "cmd:perm(setobjalias) or perm(Builders)" help_category = "Building" @@ -121,7 +122,7 @@ class CmdSetObjAlias(MuxCommand): obj = caller.search(objname) if not obj: return - if self.rhs == None: + if self.rhs is None: # no =, so we just list aliases on object. aliases = obj.aliases.all() if aliases: @@ -146,15 +147,18 @@ class CmdSetObjAlias(MuxCommand): # merge the old and new aliases (if any) old_aliases = obj.aliases.all() - new_aliases = [alias.strip().lower() for alias in self.rhs.split(',') if alias.strip()] + new_aliases = [alias.strip().lower() for alias in self.rhs.split(',') + if alias.strip()] # make the aliases only appear once old_aliases.extend(new_aliases) aliases = list(set(old_aliases)) # save back to object. obj.aliases.add(aliases) - # we treat this as a re-caching (relevant for exits to re-build their exit commands with the correct aliases) + # we treat this as a re-caching (relevant for exits to re-build their + # exit commands with the correct aliases) caller.msg("Alias(es) for '%s' set to %s." % (obj.key, str(obj.aliases))) + class CmdCopy(ObjManipCommand): """ @copy - copy objects @@ -167,8 +171,8 @@ class CmdCopy(ObjManipCommand): removing any changes that might have been made to the original since it was first created. - Create one or more copies of an object. If you don't supply any targets, one exact copy - of the original object will be created with the name *_copy. + Create one or more copies of an object. If you don't supply any targets, + one exact copt of the original object will be created with the name *_copy. """ key = "@copy" @@ -210,12 +214,15 @@ class CmdCopy(ObjManipCommand): to_obj_aliases = objdef['aliases'] to_obj_location = objdef['option'] if to_obj_location: - to_obj_location = caller.search(to_obj_location, global_search=True) + to_obj_location = caller.search(to_obj_location, + global_search=True) if not to_obj_location: return - copiedobj = ObjectDB.objects.copy_object(from_obj, new_key=to_obj_name, - new_location=to_obj_location, new_aliases=to_obj_aliases) + copiedobj = ObjectDB.objects.copy_object(from_obj, + new_key=to_obj_name, + new_location=to_obj_location, + new_aliases=to_obj_aliases) if copiedobj: string = "Copied %s to '%s' (aliases: %s)." % (from_obj_name, to_obj_name, to_obj_aliases) @@ -225,6 +232,7 @@ class CmdCopy(ObjManipCommand): # we are done, echo to user caller.msg(string) + class CmdCpAttr(ObjManipCommand): """ @cpattr - copy attributes @@ -244,8 +252,8 @@ class CmdCpAttr(ObjManipCommand): copies the coolness attribute (defined on yourself), to attributes on Anna and Tom. - Copy the attribute one object to one or more attributes on another object. If - you don't supply a source object, yourself is used. + Copy the attribute one object to one or more attributes on another object. + If you don't supply a source object, yourself is used. """ key = "@cpattr" locks = "cmd:perm(cpattr) or perm(Builders)" @@ -272,7 +280,8 @@ class CmdCpAttr(ObjManipCommand): from_obj_attrs = lhs_objattr[0]['attrs'] if not from_obj_attrs: - # this means the from_obj_name is actually an attribute name on self. + # this means the from_obj_name is actually an attribute + # name on self. from_obj_attrs = [from_obj_name] from_obj = self.caller from_obj_name = self.caller.name @@ -282,7 +291,8 @@ class CmdCpAttr(ObjManipCommand): caller.msg("You have to supply both source object and target(s).") return if not from_obj.attributes.has(from_obj_attrs[0]): - caller.msg("%s doesn't have an attribute %s." % (from_obj_name, from_obj_attrs[0])) + caller.msg("%s doesn't have an attribute %s." % (from_obj_name, + from_obj_attrs[0])) return srcvalue = from_obj.attributes.get(from_obj_attrs[0]) @@ -291,7 +301,8 @@ class CmdCpAttr(ObjManipCommand): string = "Moving " else: string = "Copying " - string += "%s/%s (with value %s) ..." % (from_obj_name, from_obj_attrs[0], srcvalue) + string += "%s/%s (with value %s) ..." % (from_obj_name, + from_obj_attrs[0], srcvalue) for to_obj in to_objs: to_obj_name = to_obj['name'] @@ -308,15 +319,20 @@ class CmdCpAttr(ObjManipCommand): # on the to_obj, we copy the original name instead. to_attr = from_attr to_obj.attributes.add(to_attr, srcvalue) - if "move" in self.switches and not (from_obj == to_obj and from_attr == to_attr): + if ("move" in self.switches and not (from_obj == to_obj and + from_attr == to_attr)): from_obj.del_attribute(from_attr) - string += "\nMoved %s.%s -> %s.%s." % (from_obj.name, from_attr, + string += "\nMoved %s.%s -> %s.%s." % (from_obj.name, + from_attr, to_obj_name, to_attr) else: - string += "\nCopied %s.%s -> %s.%s." % (from_obj.name, from_attr, - to_obj_name, to_attr) + string += "\nCopied %s.%s -> %s.%s." % (from_obj.name, + from_attr, + to_obj_name, + to_attr) caller.msg(string) + class CmdMvAttr(ObjManipCommand): """ @mvattr - move attributes @@ -330,8 +346,8 @@ class CmdMvAttr(ObjManipCommand): Switches: copy - Don't delete the original after moving. - Move an attribute from one object to one or more attributes on another object. If - you don't supply a source object, yourself is used. + Move an attribute from one object to one or more attributes on another + object. If you don't supply a source object, yourself is used. """ key = "@mvattr" locks = "cmd:perm(mvattr) or perm(Builders)" @@ -356,6 +372,7 @@ class CmdMvAttr(ObjManipCommand): else: self.caller.execute_cmd("@cpattr/move %s" % self.args) + class CmdCreate(ObjManipCommand): """ @create - create new objects @@ -364,8 +381,9 @@ class CmdCreate(ObjManipCommand): @create[/drop] objname[;alias;alias...][:typeclass], objname... switch: - drop - automatically drop the new object into your current location (this is not echoed) - this also sets the new object's home to the current location rather than to you. + drop - automatically drop the new object into your current + location (this is not echoed). This also sets the new + object's home to the current location rather than to you. Creates one or more new objects. If typeclass is given, the object is created as a child of this typeclass. The typeclass script is @@ -406,7 +424,8 @@ class CmdCreate(ObjManipCommand): # object typeclass will automatically be used) lockstring = "control:id(%s);examine:perm(Builders);delete:id(%s) or perm(Wizards);get:all()" % (caller.id, caller.id) obj = create.create_object(typeclass, name, caller, - home=caller, aliases=aliases, locks=lockstring, report_to=caller) + home=caller, aliases=aliases, + locks=lockstring, report_to=caller) if not obj: continue if aliases: @@ -423,7 +442,8 @@ class CmdCreate(ObjManipCommand): obj.home = caller.location obj.move_to(caller.location, quiet=True) if string: - caller.msg(string) + caller.msg(string) + class CmdDesc(MuxCommand): """ @@ -471,8 +491,8 @@ class CmdDestroy(MuxCommand): @destroy[/switches] [obj, obj2, obj3, [dbref-dbref], ...] switches: - override - The @destroy command will usually avoid accidentally destroying - player objects. This switch overrides this safety. + override - The @destroy command will usually avoid accidentally + destroying player objects. This switch overrides this safety. examples: @destroy house, roof, door, 44-78 @destroy 5-10, flower, 45 @@ -502,7 +522,8 @@ class CmdDestroy(MuxCommand): if not obj: self.caller.msg(" (Objects to destroy must either be local or specified with a unique #dbref.)") return "" - if not "override" in self.switches and obj.dbid == int(settings.CHARACTER_DEFAULT_HOME.lstrip("#")): + if (not "override" in self.switches and + obj.dbid == int(settings.CHARACTER_DEFAULT_HOME.lstrip("#"))): return "\nYou are trying to delete CHARACTER_DEFAULT_HOME. If you want to do this, use the /override switch." objname = obj.name if not obj.access(caller, 'delete'): @@ -529,9 +550,10 @@ class CmdDestroy(MuxCommand): for objname in self.lhslist: if '-' in objname: # might be a range of dbrefs - dmin, dmax = [utils.dbref(part, reqhash=False) for part in objname.split('-', 1)] + dmin, dmax = [utils.dbref(part, reqhash=False) + for part in objname.split('-', 1)] if dmin and dmax: - for dbref in range(int(dmin),int(dmax+1)): + for dbref in range(int(dmin), int(dmax + 1)): string += delobj("#" + str(dbref), True) else: string += delobj(objname) @@ -558,9 +580,11 @@ class CmdDig(ObjManipCommand): @dig house:myrooms.MyHouseTypeclass @dig sheer cliff;cliff;sheer = climb up, climb down - This command is a convenient way to build rooms quickly; it creates the new room and you can optionally - set up exits back and forth between your current room and the new one. You can add as many aliases as you - like to the name of the room and the exits in question; an example would be 'north;no;n'. + This command is a convenient way to build rooms quickly; it creates the + new room and you can optionally set up exits back and forth between your + current room and the new one. You can add as many aliases as you + like to the name of the room and the exits in question; an example + would be 'north;no;n'. """ key = "@dig" locks = "cmd:perm(dig) or perm(Builders)" @@ -595,13 +619,14 @@ class CmdDig(ObjManipCommand): lockstring = lockstring % (caller.dbref, caller.dbref, caller.dbref) new_room = create.create_object(typeclass, room["name"], - aliases=room["aliases"], report_to=caller) + aliases=room["aliases"], + report_to=caller) new_room.locks.add(lockstring) alias_string = "" if new_room.aliases.all(): alias_string = " (%s)" % ", ".join(new_room.aliases.all()) - room_string = "Created room %s(%s)%s of type %s." % (new_room, new_room.dbref, alias_string, typeclass) - + room_string = "Created room %s(%s)%s of type %s." % (new_room, + new_room.dbref, alias_string, typeclass) # create exit to room @@ -622,15 +647,21 @@ class CmdDig(ObjManipCommand): if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS - new_to_exit = create.create_object(typeclass, to_exit["name"], location, + new_to_exit = create.create_object(typeclass, to_exit["name"], + location, aliases=to_exit["aliases"], - locks=lockstring, destination=new_room, report_to=caller) + locks=lockstring, + destination=new_room, + report_to=caller) alias_string = "" if new_to_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_to_exit.aliases.all()) exit_to_string = "\nCreated Exit from %s to %s: %s(%s)%s." - exit_to_string = exit_to_string % (location.name, new_room.name, new_to_exit, - new_to_exit.dbref, alias_string) + exit_to_string = exit_to_string % (location.name, + new_room.name, + new_to_exit, + new_to_exit.dbref, + alias_string) # Create exit back from new room @@ -647,15 +678,22 @@ class CmdDig(ObjManipCommand): typeclass = back_exit["option"] if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS - new_back_exit = create.create_object(typeclass, back_exit["name"], - new_room, aliases=back_exit["aliases"], - locks=lockstring, destination=location, report_to=caller) + new_back_exit = create.create_object(typeclass, + back_exit["name"], + new_room, + aliases=back_exit["aliases"], + locks=lockstring, + destination=location, + report_to=caller) alias_string = "" if new_back_exit.aliases.all(): alias_string = " (%s)" % ", ".join(new_back_exit.aliases.all()) exit_back_string = "\nCreated Exit back from %s to %s: %s(%s)%s." - exit_back_string = exit_back_string % (new_room.name, location.name, - new_back_exit, new_back_exit.dbref, alias_string) + exit_back_string = exit_back_string % (new_room.name, + location.name, + new_back_exit, + new_back_exit.dbref, + alias_string) caller.msg("%s%s%s" % (room_string, exit_to_string, exit_back_string)) if new_room and ('teleport' in self.switches or "tel" in self.switches): caller.move_to(new_room) @@ -693,18 +731,18 @@ class CmdTunnel(MuxCommand): help_category = "Building" # store the direction, full name and its opposite - directions = {"n" : ("north", "s"), + directions = {"n": ("north", "s"), "ne": ("northeast", "sw"), - "e" : ("east", "w"), + "e": ("east", "w"), "se": ("southeast", "nw"), - "s" : ("south", "n"), + "s": ("south", "n"), "sw": ("southwest", "ne"), - "w" : ("west", "e"), + "w": ("west", "e"), "nw": ("northwest", "se"), - "u" : ("up", "d"), - "d" : ("down", "u"), - "i" : ("in", "o"), - "o" : ("out", "i")} + "u": ("up", "d"), + "d": ("down", "u"), + "i": ("in", "o"), + "o": ("out", "i")} def func(self): "Implements the tunnel command" @@ -725,7 +763,7 @@ class CmdTunnel(MuxCommand): roomname = "Some place" if self.rhs: - roomname = self.rhs # this may include aliases; that's fine. + roomname = self.rhs # this may include aliases; that's fine. telswitch = "" if "tel" in self.switches: @@ -735,9 +773,11 @@ class CmdTunnel(MuxCommand): backstring = ", %s;%s" % (backname, backshort) # build the string we will use to call @dig - digstring = "@dig%s %s = %s;%s%s" % (telswitch, roomname, exitname, exitshort, backstring) + digstring = "@dig%s %s = %s;%s%s" % (telswitch, roomname, + exitname, exitshort, backstring) self.caller.execute_cmd(digstring) + class CmdLink(MuxCommand): """ @link - connect objects @@ -754,8 +794,9 @@ class CmdLink(MuxCommand): If is an exit, set its destination to . Two-way operation instead sets the destination to the *locations* of the respective given arguments. - The second form (a lone =) sets the destination to None (same as the @unlink command) - and the third form (without =) just shows the currently set destination. + The second form (a lone =) sets the destination to None (same as + the @unlink command) and the third form (without =) just shows the + currently set destination. """ key = "@link" @@ -802,7 +843,7 @@ class CmdLink(MuxCommand): obj.destination = target string += "\nLink created %s -> %s (one way)." % (obj.name, target) - elif self.rhs == None: + elif self.rhs is None: # this means that no = was given (otherwise rhs # would have been an empty string). So we inspect # the home/destination on object @@ -823,6 +864,7 @@ class CmdLink(MuxCommand): # give feedback caller.msg(string.strip()) + class CmdUnLink(CmdLink): """ @unlink - unconnect objects @@ -857,6 +899,7 @@ class CmdUnLink(CmdLink): # call the @link functionality super(CmdUnLink, self).func() + class CmdSetHome(CmdLink): """ @home - control an object's home location @@ -893,7 +936,8 @@ class CmdSetHome(CmdLink): if not home: string = "This object has no home location set!" else: - string = "%s's current home is %s(%s)." % (obj, home, home.dbref) + string = "%s's current home is %s(%s)." % (obj, home, + home.dbref) else: # set a home location new_home = self.caller.search(self.rhs, global_search=True) @@ -907,6 +951,7 @@ class CmdSetHome(CmdLink): string = "%s' home location was set to %s(%s)." % (obj, new_home, new_home.dbref) self.caller.msg(string) + class CmdListCmdSets(MuxCommand): """ list command sets on an object @@ -935,6 +980,7 @@ class CmdListCmdSets(MuxCommand): string = "%s" % obj.cmdset caller.msg(string) + class CmdName(ObjManipCommand): """ cname - change the name and/or aliases of an object @@ -1006,7 +1052,8 @@ class CmdOpen(ObjManipCommand): help_category = "Building" # a custom member method to chug out exits and do checks - def create_exit(self, exit_name, location, destination, exit_aliases=None, typeclass=None): + def create_exit(self, exit_name, location, destination, + exit_aliases=None, typeclass=None): """ Helper function to avoid code duplication. At this point we know destination is a valid location @@ -1047,9 +1094,11 @@ class CmdOpen(ObjManipCommand): # exit does not exist before. Create a new one. if not typeclass: typeclass = settings.BASE_EXIT_TYPECLASS - exit_obj = create.create_object(typeclass, key=exit_name, + exit_obj = create.create_object(typeclass, + key=exit_name, location=location, - aliases=exit_aliases, report_to=caller) + aliases=exit_aliases, + report_to=caller) if exit_obj: # storing a destination is what makes it an exit! exit_obj.destination = destination @@ -1095,7 +1144,11 @@ class CmdOpen(ObjManipCommand): return # Create exit - ok = self.create_exit(exit_name, location, destination, exit_aliases, exit_typeclass) + ok = self.create_exit(exit_name, + location, + destination, + exit_aliases, + exit_typeclass) if not ok: # an error; the exit was not created, so we quit. return @@ -1104,7 +1157,11 @@ class CmdOpen(ObjManipCommand): back_exit_name = self.lhs_objs[1]['name'] back_exit_aliases = self.lhs_objs[1]['aliases'] back_exit_typeclass = self.lhs_objs[1]['option'] - ok = self.create_exit(back_exit_name, destination, location, back_exit_aliases, back_exit_typeclass) + ok = self.create_exit(back_exit_name, + destination, + location, + back_exit_aliases, + back_exit_typeclass) class CmdSetAttribute(ObjManipCommand): @@ -1126,7 +1183,8 @@ class CmdSetAttribute(ObjManipCommand): numbers. You can however also set Python primities such as lists, dictionaries and tuples on objects (this might be important for the functionality of certain custom objects). This is indicated - by you starting your value with one of {c'{n, {c"{n, {c({n, {c[{n or {c{ {n. + by you starting your value with one of {c'{n, {c"{n, {c({n, {c[{n + or {c{ {n. Note that you should leave a space after starting a dictionary ('{ ') so as to not confuse the dictionary start with a colour code like \{g. Remember that if you use Python primitives like this, you must @@ -1169,10 +1227,14 @@ class CmdSetAttribute(ObjManipCommand): used for Python <=2.5. After that literal_eval is available. """ # simple types - try: return int(obj) - except ValueError: pass - try: return float(obj) - except ValueError: pass + try: + return int(obj) + except ValueError: + pass + try: + return float(obj) + except ValueError: + pass # iterables if obj.startswith('[') and obj.endswith(']'): "A list. Traverse recursively." @@ -1182,7 +1244,8 @@ class CmdSetAttribute(ObjManipCommand): return tuple([rec_convert(val) for val in obj[1:-1].split(',')]) if obj.startswith('{') and obj.endswith('}') and ':' in obj: "A dict. Traverse recursively." - return dict([(rec_convert(pair.split(":",1)[0]), rec_convert(pair.split(":",1)[1])) + return dict([(rec_convert(pair.split(":", 1)[0]), + rec_convert(pair.split(":", 1)[1])) for pair in obj[1:-1].split(',') if ":" in pair]) # if nothing matches, return as-is return obj @@ -1198,7 +1261,8 @@ class CmdSetAttribute(ObjManipCommand): self.caller.msg(string) return utils.to_str(strobj) else: - # fall back to old recursive solution (does not support nested lists/dicts) + # fall back to old recursive solution (does not support + # nested lists/dicts) return rec_convert(strobj.strip()) def func(self): @@ -1223,17 +1287,18 @@ class CmdSetAttribute(ObjManipCommand): string = "" if not value: - if self.rhs == None: + if self.rhs is None: # no = means we inspect the attribute(s) if not attrs: attrs = [attr.key for attr in obj.get_all_attributes()] for attr in attrs: if obj.attributes.has(attr): - string += "\nAttribute %s/%s = %s" % (obj.name, attr, obj.attributes.get(attr)) + string += "\nAttribute %s/%s = %s" % (obj.name, attr, + obj.attributes.get(attr)) else: string += "\n%s has no attribute '%s'." % (obj.name, attr) # we view it without parsing markup. - self.caller.msg(string.strip(), data={"raw":True}) + self.caller.msg(string.strip(), data={"raw": True}) return else: # deleting the attribute(s) @@ -1252,9 +1317,11 @@ class CmdSetAttribute(ObjManipCommand): string += "\nCreated attribute %s/%s = %s" % (obj.name, attr, value) except SyntaxError: # this means literal_eval tried to parse a faulty string - string = "{RCritical Python syntax error in your value. Only primitive Python structures" - string += "\nare allowed. You also need to use correct Python syntax. Remember especially" - string += "\nto put quotes around all strings inside lists and dicts.{n" + string = "{RCritical Python syntax error in your value. " + string += "Only primitive Python structures are allowed. " + string += "\nYou also need to use correct Python syntax. " + string += "Remember especially to put quotes around all " + string += "strings inside lists and dicts.{n" # send feedback caller.msg(string.strip('\n')) @@ -1314,7 +1381,8 @@ class CmdTypeclass(MuxCommand): # we did not supply a new typeclass, view the # current one instead. if hasattr(obj, "typeclass"): - string = "%s's current typeclass is '%s' (%s)." % (obj.name, obj.typeclass.typename, obj.typeclass.path) + string = "%s's current typeclass is '%s' (%s)." % (obj.name, + obj.typeclass.typename, obj.typeclass.path) else: string = "%s is not a typed object." % obj.name caller.msg(string) @@ -1343,8 +1411,8 @@ class CmdTypeclass(MuxCommand): string = "%s updated its existing typeclass (%s).\n" % (obj.name, obj.typeclass.path) else: string = "%s changed typeclass from %s to %s.\n" % (obj.name, - old_typeclass_path, - obj.typeclass_path) + old_typeclass_path, + obj.typeclass_path) string += "Creation hooks were run." if reset: string += " All old attributes where deleted before the swap." @@ -1354,8 +1422,8 @@ class CmdTypeclass(MuxCommand): else: string = obj.typeclass_last_errmsg string += "\nCould not swap '%s' (%s) to typeclass '%s'." % (obj.name, - old_typeclass_path, - typeclass) + old_typeclass_path, + typeclass) caller.msg(string) @@ -1410,6 +1478,7 @@ class CmdWipe(ObjManipCommand): string = string % (",".join(attrs), obj.name) caller.msg(string) + class CmdLock(ObjManipCommand): """ lock - assign a lock definition to an object @@ -1493,6 +1562,7 @@ class CmdLock(ObjManipCommand): return caller.msg(obj.locks) + class CmdExamine(ObjManipCommand): """ examine - detailed info on objects @@ -1545,7 +1615,9 @@ class CmdExamine(ObjManipCommand): else: db_attr = [(attr.key, attr.value) for attr in obj.db_attributes.all()] try: - ndb_attr = [(aname, avalue) for aname, avalue in obj.ndb.__dict__.items() if not aname.startswith("_")] + ndb_attr = [(aname, avalue) + for aname, avalue in obj.ndb.__dict__.items() + if not aname.startswith("_")] except Exception: ndb_attr = None string = "" @@ -1572,7 +1644,8 @@ class CmdExamine(ObjManipCommand): if hasattr(obj, "sessid") and obj.sessid: string += "\n{wsession{n: %s" % obj.sessid elif hasattr(obj, "sessions") and obj.sessions: - string += "\n{wsession(s){n: %s" % (", ".join(str(sess.sessid) for sess in obj.sessions)) + string += "\n{wsession(s){n: %s" % (", ".join(str(sess.sessid) + for sess in obj.sessions)) if hasattr(obj, "has_player") and obj.has_player: string += "\n{wPlayer{n: {c%s{n" % obj.player.name perms = obj.player.permissions.all() @@ -1581,7 +1654,8 @@ class CmdExamine(ObjManipCommand): elif not perms: perms = [""] string += "\n{wPlayer Perms{n: %s" % (", ".join(perms)) - string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass.typename, obj.typeclass_path) + string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass.typename, + obj.typeclass_path) if hasattr(obj, "location"): string += "\n{wLocation{n: %s" % obj.location if obj.location: @@ -1610,14 +1684,20 @@ class CmdExamine(ObjManipCommand): if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "Empty"): # list the current cmdsets - all_cmdsets = obj.cmdset.all() + (hasattr(obj, "player") and obj.player and obj.player.cmdset.all() or []) - all_cmdsets += hasattr(obj, "sessid") and hasattr(obj, "player") and obj.player.get_session(obj.sessid).cmdset.all() - all_cmdsets.sort(key=lambda x:x.priority, reverse=True) - string += "\n{wStored Cmdset(s){n:\n %s" % ("\n ".join("%s [%s] (prio %s)" % - (cmdset.path, cmdset.key, cmdset.priority) for cmdset in all_cmdsets)) + all_cmdsets = (obj.cmdset.all() + + (hasattr(obj, "player") and + obj.player and obj.player.cmdset.all() or [])) + all_cmdsets += (hasattr(obj, "sessid") and + hasattr(obj, "player") and + obj.player.get_session(obj.sessid).cmdset.all()) + all_cmdsets.sort(key=lambda x: x.priority, reverse=True) + string += "\n{wStored Cmdset(s){n:\n %s" % ("\n ".join("%s [%s] (prio %s)" % \ + (cmdset.path, cmdset.key, cmdset.priority) + for cmdset in all_cmdsets)) # list the commands available to this object - avail_cmdset = sorted([cmd.key for cmd in avail_cmdset if cmd.access(obj, "cmd")]) + avail_cmdset = sorted([cmd.key for cmd in avail_cmdset + if cmd.access(obj, "cmd")]) cmdsetstr = utils.fill(", ".join(avail_cmdset), indent=2) string += "\n{wCommands available to %s (all cmdsets + exits and external cmds){n:\n %s" % (obj.key, cmdsetstr) @@ -1644,10 +1724,10 @@ class CmdExamine(ObjManipCommand): string += "\n{wCharacters{n: %s" % ", ".join(["{c%s{n" % pobj.name for pobj in pobjs]) if things: string += "\n{wContents{n: %s" % ", ".join([cont.name for cont in obj.contents - if cont not in exits and cont not in pobjs]) - separator = "-"*78 + if cont not in exits and cont not in pobjs]) + separator = "-" * 78 #output info - return '%s\n%s\n%s' % ( separator, string.strip(), separator ) + return '%s\n%s\n%s' % (separator, string.strip(), separator) def func(self): "Process command" @@ -1686,7 +1766,7 @@ class CmdExamine(ObjManipCommand): obj_attrs = objdef['attrs'] self.player_mode = utils.inherits_from(caller, "src.players.player.Player") or \ - "player" in self.switches or obj_name.startswith('*') + "player" in self.switches or obj_name.startswith('*') if self.player_mode: try: obj = caller.search_player(obj_name.lstrip('*')) @@ -1699,7 +1779,8 @@ class CmdExamine(ObjManipCommand): continue if not obj.access(caller, 'examine'): - #If we don't have special info access, just look at the object instead. + #If we don't have special info access, just look + # at the object instead. caller.execute_cmd('look %s' % obj_name) continue @@ -1727,7 +1808,8 @@ class CmdFind(MuxCommand): Searches the database for an object of a particular name or dbref. Use *playername to search for a player. The switches allows for limiting object matches to certain game entities. Dbrefmin and dbrefmax - limits matches to within the given dbrefs, or above/below if only one is given. + limits matches to within the given dbrefs, or above/below if only + one is given. """ key = "@find" @@ -1772,11 +1854,13 @@ class CmdFind(MuxCommand): if not low <= int(result.id) <= high: string += "\n {RNo match found for '%s' within the given dbref limits.{n" % searchstring else: - string += "\n{g %s(%s) - %s{n" % (result.key, result.dbref, result.typeclass.path) + string += "\n{g %s(%s) - %s{n" % (result.key, result.dbref, + result.typeclass.path) else: # Not a player/dbref search but a wider search; build a queryset. - results = ObjectDB.objects.filter(db_key__istartswith=searchstring, id__gte=low, id__lte=high) + results = ObjectDB.objects.filter(db_key__istartswith=searchstring, + id__gte=low, id__lte=high) if "room" in switches: results = results.filter(db_location__isnull=True) if "exit" in switches: @@ -1909,12 +1993,14 @@ class CmdTeleport(MuxCommand): use_destination = False # try the teleport - if obj_to_teleport.move_to(destination, quiet=tel_quietly, emit_to_obj=caller, + if obj_to_teleport.move_to(destination, quiet=tel_quietly, + emit_to_obj=caller, use_destination=use_destination): if obj_to_teleport == caller: caller.msg("Teleported to %s." % destination) else: - caller.msg("Teleported %s -> %s." % (obj_to_teleport, destination)) + caller.msg("Teleported %s -> %s." % (obj_to_teleport, + destination)) class CmdScript(MuxCommand): @@ -1931,9 +2017,10 @@ class CmdScript(MuxCommand): If no script path/key is given, lists all scripts active on the given object. Script path can be given from the base location for scripts as given in - settings. If adding a new script, it will be started automatically (no /start - switch is needed). Using the /start or /stop switches on an object without - specifying a script key/path will start/stop ALL scripts on the object. + settings. If adding a new script, it will be started automatically + (no /start switch is needed). Using the /start or /stop switches on an + object without specifying a script key/path will start/stop ALL scripts on + the object. """ key = "@script" @@ -1970,7 +2057,8 @@ class CmdScript(MuxCommand): string += "%s scripts started on %s." % (num, obj.key) elif "stop" in self.switches: for script in scripts: - string += "Stopping script %s on %s." % (script.key, obj.key) + string += "Stopping script %s on %s." % (script.key, + obj.key) script.stop() string = string.strip() obj.scripts.validate() diff --git a/src/commands/default/cmdset_character.py b/src/commands/default/cmdset_character.py index ef282a8b36..0a4723f349 100644 --- a/src/commands/default/cmdset_character.py +++ b/src/commands/default/cmdset_character.py @@ -1,14 +1,15 @@ """ This module ties together all the commands default Character objects have -available (i.e. IC commands). Note that some commands, such as communication-commands are -instead put on the player level, in the Player cmdset. Player commands remain -available also to Characters. +available (i.e. IC commands). Note that some commands, such as +communication-commands are instead put on the player level, in the +Player cmdset. Player commands remain available also to Characters. """ from src.commands.cmdset import CmdSet from src.commands.default import general, help, admin, system from src.commands.default import building from src.commands.default import batchprocess + class CharacterCmdSet(CmdSet): """ Implements the default command set. diff --git a/src/commands/default/cmdset_player.py b/src/commands/default/cmdset_player.py index 2a63e3ee34..52009e4fe8 100644 --- a/src/commands/default/cmdset_player.py +++ b/src/commands/default/cmdset_player.py @@ -13,6 +13,7 @@ from src.commands.cmdset import CmdSet from src.commands.default import help, comms, admin, system from src.commands.default import building, player + class PlayerCmdSet(CmdSet): """ Implements the player command set. diff --git a/src/commands/default/cmdset_unloggedin.py b/src/commands/default/cmdset_unloggedin.py index d65da40866..21d1dc3bd3 100644 --- a/src/commands/default/cmdset_unloggedin.py +++ b/src/commands/default/cmdset_unloggedin.py @@ -6,6 +6,7 @@ of the state instance in this module. from src.commands.cmdset import CmdSet from src.commands.default import unloggedin + class UnloggedinCmdSet(CmdSet): """ Sets up the unlogged cmdset. diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index 1b1025adf7..6cc3a4fd90 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -22,6 +22,7 @@ __all__ = ("CmdAddCom", "CmdDelCom", "CmdAllCom", "CmdPage", "CmdIRC2Chan", "CmdIMC2Chan", "CmdIMCInfo", "CmdIMCTell", "CmdRSS2Chan") + def find_channel(caller, channelname, silent=False, noaliases=False): """ Helper function for searching for a single channel with @@ -30,7 +31,8 @@ def find_channel(caller, channelname, silent=False, noaliases=False): channels = ChannelDB.objects.channel_search(channelname) if not channels: if not noaliases: - channels = [chan for chan in ChannelDB.objects.get_all_channels() if channelname in chan.aliases.all()] + channels = [chan for chan in ChannelDB.objects.get_all_channels() + if channelname in chan.aliases.all()] if channels: return channels[0] if not silent: @@ -43,6 +45,7 @@ def find_channel(caller, channelname, silent=False, noaliases=False): return None return channels[0] + class CmdAddCom(MuxPlayerCommand): """ addcom - subscribe to a channel with optional alias @@ -57,7 +60,7 @@ class CmdAddCom(MuxPlayerCommand): """ key = "addcom" - aliases = ["aliaschan","chanalias"] + aliases = ["aliaschan", "chanalias"] help_category = "Comms" locks = "cmd:not pperm(channel_banned)" @@ -168,6 +171,7 @@ class CmdDelCom(MuxPlayerCommand): else: self.msg("You had no such alias defined for this channel.") + class CmdAllCom(MuxPlayerCommand): """ allcom - operate on all channels @@ -197,8 +201,10 @@ class CmdAllCom(MuxPlayerCommand): return if args == "on": - # get names of all channels available to listen to and activate them all - channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')] + # get names of all channels available to listen to + # and activate them all + channels = [chan for chan in ChannelDB.objects.get_all_channels() + if chan.access(caller, 'listen')] for channel in channels: caller.execute_cmd("addcom %s" % channel.key) elif args == "off": @@ -208,13 +214,15 @@ class CmdAllCom(MuxPlayerCommand): caller.execute_cmd("delcom %s" % channel.key) elif args == "destroy": # destroy all channels you control - channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'control')] + channels = [chan for chan in ChannelDB.objects.get_all_channels() + if chan.access(caller, 'control')] for channel in channels: caller.execute_cmd("@cdestroy %s" % channel.key) elif args == "who": # run a who, listing the subscribers on visible channels. string = "\n{CChannel subscriptions{n" - channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')] + channels = [chan for chan in ChannelDB.objects.get_all_channels() + if chan.access(caller, 'listen')] if not channels: string += "No channels." for channel in channels: @@ -229,6 +237,7 @@ class CmdAllCom(MuxPlayerCommand): # wrong input self.msg("Usage: allcom on | off | who | clear") + class CmdChannels(MuxPlayerCommand): """ @clist @@ -253,7 +262,8 @@ class CmdChannels(MuxPlayerCommand): caller = self.caller # all channels we have available to listen to - channels = [chan for chan in ChannelDB.objects.get_all_channels() if chan.access(caller, 'listen')] + channels = [chan for chan in ChannelDB.objects.get_all_channels() + if chan.access(caller, 'listen')] #print channels if not channels: self.msg("No channels available.") @@ -264,28 +274,39 @@ class CmdChannels(MuxPlayerCommand): if self.cmdstring == "comlist": # just display the subscribed channels with no extra info - comtable = prettytable.PrettyTable(["{wchannel","{wmy aliases", "{wdescription"]) + comtable = prettytable.PrettyTable(["{wchannel", + "{wmy aliases", + "{wdescription"]) for chan in subs: clower = chan.key.lower() nicks = caller.nicks.get(category="channel") - comtable.add_row(["%s%s" % (chan.key, chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or ""), - "%s".join(nick for nick in make_iter(nicks) if nick and nick.lower()==clower), + comtable.add_row(["%s%s" % (chan.key, chan.aliases.all() and + "(%s)" % ",".join(chan.aliases.all()) or ""), + "%s".join(nick for nick in make_iter(nicks) + if nick and nick.lower() == clower), chan.db.desc]) caller.msg("\n{wChannel subscriptions{n (use {w@channels{n to list all, {waddcom{n/{wdelcom{n to sub/unsub):{n\n%s" % comtable) else: # full listing (of channels caller is able to listen to) - comtable = prettytable.PrettyTable(["{wsub","{wchannel","{wmy aliases","{wlocks","{wdescription"]) + comtable = prettytable.PrettyTable(["{wsub", + "{wchannel", + "{wmy aliases", + "{wlocks", + "{wdescription"]) for chan in channels: clower = chan.key.lower() nicks = caller.nicks.get(category="channel") nicks = nicks or [] comtable.add_row([chan in subs and "{gYes{n" or "{rNo{n", - "%s%s" % (chan.key, chan.aliases.all() and "(%s)" % ",".join(chan.aliases.all()) or ""), - "%s".join(nick for nick in make_iter(nicks) if nick.lower()==clower), + "%s%s" % (chan.key, chan.aliases.all() and + "(%s)" % ",".join(chan.aliases.all()) or ""), + "%s".join(nick for nick in make_iter(nicks) + if nick.lower() == clower), str(chan.locks), chan.db.desc]) caller.msg("\n{wAvailable channels{n (use {wcomlist{n,{waddcom{n and {wdelcom{n to manage subscriptions):\n%s" % comtable) + class CmdCdestroy(MuxPlayerCommand): """ @cdestroy @@ -322,6 +343,7 @@ class CmdCdestroy(MuxPlayerCommand): CHANNELHANDLER.update() self.msg("Channel '%s' was destroyed." % channel) + class CmdCBoot(MuxPlayerCommand): """ @cboot @@ -382,6 +404,7 @@ class CmdCBoot(MuxPlayerCommand): channel.disconnect_from(player) CHANNELHANDLER.update() + class CmdCemit(MuxPlayerCommand): """ @cemit - send a message to channel @@ -429,6 +452,7 @@ class CmdCemit(MuxPlayerCommand): string = "Sent to channel %s: %s" % (channel.key, message) self.msg(string) + class CmdCWho(MuxPlayerCommand): """ @cwho @@ -466,6 +490,7 @@ class CmdCWho(MuxPlayerCommand): string += " " self.msg(string.strip()) + class CmdChannelCreate(MuxPlayerCommand): """ @ccreate @@ -508,7 +533,10 @@ class CmdChannelCreate(MuxPlayerCommand): return # Create and set the channel up lockstring = "send:all();listen:all();control:id(%s)" % caller.id - new_chan = create.create_channel(channame, aliases, description, locks=lockstring) + new_chan = create.create_channel(channame, + aliases, + description, + locks=lockstring) new_chan.connect_to(caller) self.msg("Created channel %s and connected to it." % new_chan.key) @@ -593,7 +621,9 @@ class CmdCdesc(MuxPlayerCommand): # set the description channel.db.desc = self.rhs channel.save() - self.msg("Description of channel '%s' set to '%s'." % (channel.key, self.rhs)) + self.msg("Description of channel '%s' set to '%s'." % (channel.key, + self.rhs)) + class CmdPage(MuxPlayerCommand): """ @@ -624,15 +654,16 @@ class CmdPage(MuxPlayerCommand): caller = self.caller # get the messages we've sent (not to channels) - pages_we_sent = Msg.objects.get_messages_by_sender(caller, exclude_channel_messages=True) + pages_we_sent = Msg.objects.get_messages_by_sender(caller, + exclude_channel_messages=True) # get last messages we've got pages_we_got = Msg.objects.get_messages_by_receiver(caller) - if 'last' in self.switches: if pages_we_sent: recv = ",".join(obj.key for obj in pages_we_sent[-1].receivers) - self.msg("You last paged {c%s{n:%s" % (recv, pages_we_sent[-1].message)) + self.msg("You last paged {c%s{n:%s" % (recv, + pages_we_sent[-1].message)) return else: self.msg("You haven't paged anyone yet.") @@ -654,12 +685,12 @@ class CmdPage(MuxPlayerCommand): lastpages = pages[-number:] else: lastpages = pages - - lastpages = "\n ".join("{w%s{n {c%s{n to {c%s{n: %s" % (utils.datetime_format(page.date_sent), - ",".join(obj.key for obj in page.senders), - "{n,{c ".join([obj.name for obj in page.receivers]), - page.message) - for page in lastpages) + template = "{w%s{n {c%s{n to {c%s{n: %s" + lastpages = "\n ".join(template % + (utils.datetime_format(page.date_sent), + ",".join(obj.key for obj in page.senders), + "{n,{c ".join([obj.name for obj in page.receivers]), + page.message) for page in lastpages) if lastpages: string = "Your latest pages:\n %s" % lastpages @@ -668,7 +699,6 @@ class CmdPage(MuxPlayerCommand): self.msg(string) return - # We are sending. Build a list of targets if not self.lhs: @@ -722,7 +752,7 @@ class CmdPage(MuxPlayerCommand): else: received.append("{c%s{n" % pobj.name) if rstrings: - self.msg(rstrings = "\n".join(rstrings)) + self.msg(rstrings="\n".join(rstrings)) self.msg("You paged %s with: '%s'." % (", ".join(received), message)) @@ -734,18 +764,20 @@ class CmdIRC2Chan(MuxCommand): @irc2chan[/switches] = <#irchannel> Switches: - /disconnect - this will delete the bot and remove the irc connection to the channel. + /disconnect - this will delete the bot and remove the irc connection + to the channel. /remove - " /list - show all irc<->evennia mappings Example: @irc2chan myircchan = irc.dalnet.net 6667 myevennia-channel evennia-bot - This creates an IRC bot that connects to a given IRC network and channel. It will - relay everything said in the evennia channel to the IRC channel and vice versa. The - bot will automatically connect at server start, so this comman need only be given once. - The /disconnect switch will permanently delete the bot. To only temporarily deactivate it, - use the @services command instead. + This creates an IRC bot that connects to a given IRC network and channel. + It will relay everything said in the evennia channel to the IRC channel and + vice versa. The bot will automatically connect at server start, so this + comman need only be given once. The /disconnect switch will permanently + delete the bot. To only temporarily deactivate it, use the {w@services{n + command instead. """ key = "@irc2chan" @@ -780,19 +812,25 @@ class CmdIRC2Chan(MuxCommand): channel = self.lhs self.rhs = self.rhs.replace('#', ' ') # to avoid Python comment issues try: - irc_network, irc_port, irc_channel, irc_botname = [part.strip() for part in self.rhs.split(None, 3)] + irc_network, irc_port, irc_channel, irc_botname = \ + [part.strip() for part in self.rhs.split(None, 3)] irc_channel = "#%s" % irc_channel except Exception: string = "IRC bot definition '%s' is not valid." % self.rhs self.msg(string) return - if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: + if('disconnect' in self.switches or 'remove' in self.switches or + 'delete' in self.switches): chanmatch = find_channel(self.caller, channel, silent=True) if chanmatch: channel = chanmatch.key - ok = irc.delete_connection(channel, irc_network, irc_port, irc_channel, irc_botname) + ok = irc.delete_connection(channel, + irc_network, + irc_port, + irc_channel, + irc_botname) if not ok: self.msg("IRC connection/bot could not be removed, does it exist?") else: @@ -802,12 +840,17 @@ class CmdIRC2Chan(MuxCommand): channel = find_channel(self.caller, channel) if not channel: return - ok = irc.create_connection(channel, irc_network, irc_port, irc_channel, irc_botname) + ok = irc.create_connection(channel, + irc_network, + irc_port, + irc_channel, + irc_botname) if not ok: self.msg("This IRC connection already exists.") return self.msg("Connection created. Starting IRC bot.") + class CmdIMC2Chan(MuxCommand): """ imc2chan - link an evennia channel to imc2 @@ -863,9 +906,10 @@ class CmdIMC2Chan(MuxCommand): channel = self.lhs imc2_channel = self.rhs - if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: - # we don't search for channels before this since we want to clear the link - # also if the channel no longer exists. + if('disconnect' in self.switches or 'remove' in self.switches or + 'delete' in self.switches): + # we don't search for channels before this since we want + # to clear the link also if the channel no longer exists. ok = imc2.delete_connection(channel, imc2_channel) if not ok: self.msg("IMC2 connection could not be removed, does it exist?") @@ -932,7 +976,8 @@ class CmdIMCInfo(MuxCommand): IMC2_CLIENT.send_packet(pck.IMC2PacketIceRefresh()) self.msg("IMC2 lists were re-synced.") - elif "games" in self.switches or "muds" in self.switches or self.cmdstring == "@imclist": + elif("games" in self.switches or "muds" in self.switches + or self.cmdstring == "@imclist"): # list muds from src.comms.imc2 import IMC2_MUDLIST @@ -956,9 +1001,13 @@ class CmdIMCInfo(MuxCommand): return from src.comms.imc2 import IMC2_CLIENT self.msg("Sending IMC whois request. If you receive no response, no matches were found.") - IMC2_CLIENT.msg_imc2(None, from_obj=self.caller, packet_type="imcwhois", target=self.args) + IMC2_CLIENT.msg_imc2(None, + from_obj=self.caller, + packet_type="imcwhois", + target=self.args) - elif not self.switches or "channels" in self.switches or self.cmdstring == "@imcchanlist": + elif(not self.switches or "channels" in self.switches or + self.cmdstring == "@imcchanlist"): # show channels from src.comms.imc2 import IMC2_CHANLIST, IMC2_CLIENT @@ -968,7 +1017,8 @@ class CmdIMCInfo(MuxCommand): table = prettytable.PrettyTable(["Full name", "Name", "Owner", "Perm", "Policy"]) for chan in channels: nchans += 1 - table.add_row([chan.name, chan.localname, chan.owner, chan.level, chan.policy]) + table.add_row([chan.name, chan.localname, chan.owner, + chan.level, chan.policy]) string += "\n{wChannels on %s:{n\n%s" % (IMC2_CLIENT.factory.network, table) string += "\n%i Channels found." % nchans self.msg(string) @@ -977,6 +1027,7 @@ class CmdIMCInfo(MuxCommand): string = "Usage: imcinfo|imcchanlist|imclist" self.msg(string) + # unclear if this is working ... class CmdIMCTell(MuxCommand): """ @@ -1028,19 +1079,21 @@ class CmdRSS2Chan(MuxCommand): @rss2chan[/switches] = Switches: - /disconnect - this will stop the feed and remove the connection to the channel. + /disconnect - this will stop the feed and remove the connection to the + channel. /remove - " /list - show all rss->evennia mappings Example: @rss2chan rsschan = http://code.google.com/feeds/p/evennia/updates/basic - This creates an RSS reader that connects to a given RSS feed url. Updates will be - echoed as a title and news link to the given channel. The rate of updating is set - with the RSS_UPDATE_INTERVAL variable in settings (default is every 10 minutes). + This creates an RSS reader that connects to a given RSS feed url. Updates + will be echoed as a title and news link to the given channel. The rate of + updating is set with the RSS_UPDATE_INTERVAL variable in settings (default + is every 10 minutes). - When disconnecting you need to supply both the channel and url again so as to identify - the connection uniquely. + When disconnecting you need to supply both the channel and url again so as + to identify the connection uniquely. """ key = "@rss2chan" @@ -1075,7 +1128,8 @@ class CmdRSS2Chan(MuxCommand): channel = self.lhs url = self.rhs - if 'disconnect' in self.switches or 'remove' in self.switches or 'delete' in self.switches: + if('disconnect' in self.switches or 'remove' in self.switches or + 'delete' in self.switches): chanmatch = find_channel(self.caller, channel, silent=True) if chanmatch: channel = chanmatch.key diff --git a/src/commands/default/general.py b/src/commands/default/general.py index c6d4106305..ce8e3e2203 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -13,6 +13,7 @@ __all__ = ("CmdHome", "CmdLook", "CmdNick", AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) + class CmdHome(MuxCommand): """ home @@ -38,6 +39,7 @@ class CmdHome(MuxCommand): caller.move_to(home) caller.msg("There's no place like home ...") + class CmdLook(MuxCommand): """ look @@ -126,7 +128,9 @@ class CmdNick(MuxCommand): nicks = caller.nicks.get(category="channel") if 'list' in switches: - table = prettytable.PrettyTable(["{wNickType", "{wNickname", "{wTranslates-to"]) + table = prettytable.PrettyTable(["{wNickType", + "{wNickname", + "{wTranslates-to"]) for nick in nicks: table.add_row([nick.db_category, nick.db_key, nick.db_data]) string = "{wDefined Nicks:{n\n%s" % table @@ -170,6 +174,7 @@ class CmdNick(MuxCommand): caller.nicks.add(nick, real, category=switch) caller.msg(string) + class CmdInventory(MuxCommand): """ inventory @@ -198,6 +203,7 @@ class CmdInventory(MuxCommand): string = "{wYou are carrying:\n%s" % table self.caller.msg(string) + class CmdGet(MuxCommand): """ get @@ -327,7 +333,6 @@ class CmdGive(MuxCommand): target.msg("%s gives you %s." % (caller.key, to_give.key)) - class CmdSay(MuxCommand): """ say @@ -365,6 +370,7 @@ class CmdSay(MuxCommand): caller.location.msg_contents(emit_string, exclude=caller) + class CmdPose(MuxCommand): """ pose - strike a pose @@ -407,6 +413,7 @@ class CmdPose(MuxCommand): msg = "%s%s" % (self.caller.name, self.args) self.caller.location.msg_contents(msg) + class CmdAccess(MuxCommand): """ access - show access groups @@ -440,5 +447,4 @@ class CmdAccess(MuxCommand): string += "\nCharacter {c%s{n: %s" % (caller.key, cperms) if hasattr(caller, 'player'): string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms) - caller.msg(string) - + caller.msg(string) \ No newline at end of file diff --git a/src/commands/default/help.py b/src/commands/default/help.py index 05b98b20f4..314a953f80 100644 --- a/src/commands/default/help.py +++ b/src/commands/default/help.py @@ -18,7 +18,8 @@ from src.commands.default.muxcommand import MuxCommand __all__ = ("CmdHelp", "CmdSetHelp") -SEP = "{C" + "-"*78 + "{n" +SEP = "{C" + "-" * 78 + "{n" + def format_help_entry(title, help_text, aliases=None, suggested=None): """ @@ -38,6 +39,7 @@ def format_help_entry(title, help_text, aliases=None, suggested=None): string += "\n" + SEP return string + def format_help_list(hdict_cmds, hdict_db): """ Output a category-ordered list. The input are the @@ -57,6 +59,7 @@ def format_help_list(hdict_cmds, hdict_db): string += "{G" + fill(", ".join(sorted([str(topic) for topic in hdict_db[category]]))) + "{n" return string + class CmdHelp(Command): """ The main help command @@ -129,13 +132,18 @@ class CmdHelp(Command): # try an exact command auto-help match match = [cmd for cmd in all_cmds if cmd == query] if len(match) == 1: - self.msg(format_help_entry(match[0].key, match[0].__doc__, aliases=match[0].aliases, suggested=suggestions)) + self.msg(format_help_entry(match[0].key, + match[0].__doc__, + aliases=match[0].aliases, + suggested=suggestions)) return # try an exact database help entry match match = list(HelpEntry.objects.find_topicmatch(query, exact=True)) if len(match) == 1: - self.msg(format_help_entry(match[0].key, match[0].entrytext, suggested=suggestions)) + self.msg(format_help_entry(match[0].key, + match[0].entrytext, + suggested=suggestions)) return # try to see if a category name was entered @@ -147,6 +155,7 @@ class CmdHelp(Command): # no exact matches found. Just give suggestions. self.msg(format_help_entry("", "No help entry found for '%s'" % query, None, suggested=suggestions)) + class CmdSetHelp(MuxCommand): """ @help - edit the help database @@ -169,9 +178,9 @@ class CmdSetHelp(MuxCommand): @sethelp/append pickpocketing, ,attr(is_thief) = This steals ... This command manipulates the help database. A help entry can be created, - appended/merged to and deleted. If you don't assign a category, the "General" - category will be used. If no lockstring is specified, default is to let everyone read - the help file. + appended/merged to and deleted. If you don't assign a category, the + "General" category will be used. If no lockstring is specified, default + is to let everyone read the help file. """ key = "@help" diff --git a/src/commands/default/muxcommand.py b/src/commands/default/muxcommand.py index d284e32147..965189d2a9 100644 --- a/src/commands/default/muxcommand.py +++ b/src/commands/default/muxcommand.py @@ -74,8 +74,8 @@ class MuxCommand(Command): The 'name[ with several words]' part is already dealt with by the cmdhandler at this point, and stored in self.cmdname (we don't use - it here). The rest of the command is stored in self.args, which can start - with the switch indicator /. + it here). The rest of the command is stored in self.args, which can + start with the switch indicator /. This parser breaks self.args into its constituents and stores them in the following variables: diff --git a/src/commands/default/player.py b/src/commands/default/player.py index d971bef2ba..8b2db730d2 100644 --- a/src/commands/default/player.py +++ b/src/commands/default/player.py @@ -24,8 +24,9 @@ from src.utils import utils, create, search, prettytable from settings import MAX_NR_CHARACTERS, MULTISESSION_MODE # limit symbol import for API -__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit", "CmdCharCreate", - "CmdEncoding", "CmdSessions", "CmdWho", "CmdColorTest", "CmdQuell") +__all__ = ("CmdOOCLook", "CmdIC", "CmdOOC", "CmdPassword", "CmdQuit", + "CmdCharCreate", "CmdEncoding", "CmdSessions", "CmdWho", + "CmdColorTest", "CmdQuell") # force max nr chars to 1 if mode is 0 or 1 MAX_NR_CHARACTERS = MULTISESSION_MODE < 2 and 1 or MAX_NR_CHARACTERS @@ -58,7 +59,8 @@ class CmdOOCLook(MuxPlayerCommand): "Hook method for when an argument is given." player = self.caller key = self.args.lower() - chars = dict((utils.to_str(char.key.lower()), char) for char in player.db._playable_characters) + chars = dict((utils.to_str(char.key.lower()), char) + for char in player.db._playable_characters) looktarget = chars.get(key) if looktarget: self.msg(looktarget.return_appearance(player)) @@ -101,7 +103,7 @@ class CmdOOCLook(MuxPlayerCommand): string += "\n\nAvailable character%s (%i/unlimited):" % (string_s_ending, len(characters)) else: string += "\n\nAvailable character%s%s:" % (string_s_ending, - MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "") + MAX_NR_CHARACTERS > 1 and " (%i/%i)" % (len(characters), MAX_NR_CHARACTERS) or "") for char in characters: csessid = char.sessid @@ -171,8 +173,10 @@ class CmdCharCreate(MuxPlayerCommand): typeclass = settings.BASE_CHARACTER_TYPECLASS permissions = settings.PERMISSION_PLAYER_DEFAULT - new_character = create.create_object(typeclass, key=key, location=default_home, - home=default_home, permissions=permissions) + new_character = create.create_object(typeclass, key=key, + location=default_home, + home=default_home, + permissions=permissions) # only allow creator (and immortals) to puppet this char new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % (new_character.id, player.id)) @@ -203,7 +207,8 @@ class CmdIC(MuxPlayerCommand): """ key = "@ic" - locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it. + # lockmust be all() for different puppeted objects to access it. + locks = "cmd:all()" aliases = "@puppet" help_category = "General" @@ -235,7 +240,8 @@ class CmdIC(MuxPlayerCommand): if new_character.player: # may not puppet an already puppeted character if new_character.sessid and new_character.player == player: - # as a safeguard we allow "taking over chars from your own sessions. + # as a safeguard we allow "taking over chars from + # your own sessions. player.msg("{c%s{n{R is now acted from another of your sessions.{n" % (new_character.name), sessid=new_character.sessid) player.unpuppet_object(new_character.sessid) self.msg("Taking over {c%s{n from another of your sessions." % new_character.name) @@ -251,6 +257,7 @@ class CmdIC(MuxPlayerCommand): else: self.msg("{rYou cannot become {C%s{n." % new_character.name) + class CmdOOC(MuxPlayerCommand): """ go ooc @@ -264,7 +271,8 @@ class CmdOOC(MuxPlayerCommand): """ key = "@ooc" - locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it. + # lock must be all(), for different puppeted objects to access it. + locks = "cmd:all()" aliases = "@unpuppet" help_category = "General" @@ -311,17 +319,22 @@ class CmdSessions(MuxPlayerCommand): player = self.caller sessions = player.get_all_sessions() - table = prettytable.PrettyTable(["{wsessid", "{wprotocol", "{whost", "{wpuppet/character", "{wlocation"]) - for sess in sorted(sessions, key=lambda x:x.sessid): + table = prettytable.PrettyTable(["{wsessid", + "{wprotocol", + "{whost", + "{wpuppet/character", + "{wlocation"]) + for sess in sorted(sessions, key=lambda x: x.sessid): sessid = sess.sessid char = player.get_puppet(sessid) table.add_row([str(sessid), str(sess.protocol_key), - type(sess.address)==tuple and sess.address[0] or sess.address, + type(sess.address) == tuple and sess.address[0] or sess.address, char and str(char) or "None", char and str(char.location) or "N/A"]) string = "{wYour current session(s):{n\n%s" % table self.msg(string) + class CmdWho(MuxPlayerCommand): """ who @@ -355,7 +368,13 @@ class CmdWho(MuxPlayerCommand): nplayers = (SESSIONS.player_count()) if show_session_data: - table = prettytable.PrettyTable(["{wPlayer Name","{wOn for", "{wIdle", "{wRoom", "{wCmds", "{wProtocol", "{wHost"]) + table = prettytable.PrettyTable(["{wPlayer Name", + "{wOn for", + "{wIdle", + "{wRoom", + "{wCmds", + "{wProtocol", + "{wHost"]) for session in session_list: if not session.logged_in: continue delta_cmd = time.time() - session.cmd_last_visible @@ -372,7 +391,8 @@ class CmdWho(MuxPlayerCommand): else: table = prettytable.PrettyTable(["{wPlayer name", "{wOn for", "{wIdle"]) for session in session_list: - if not session.logged_in: continue + if not session.logged_in: + continue delta_cmd = time.time() - session.cmd_last_visible delta_conn = time.time() - session.conn_time plr_pobject = session.get_puppet() @@ -397,14 +417,16 @@ class CmdEncoding(MuxPlayerCommand): clear - clear your custom encoding - This sets the text encoding for communicating with Evennia. This is mostly an issue only if - you want to use non-ASCII characters (i.e. letters/symbols not found in English). If you see - that your characters look strange (or you get encoding errors), you should use this command - to set the server encoding to be the same used in your client program. + This sets the text encoding for communicating with Evennia. This is mostly + an issue only if you want to use non-ASCII characters (i.e. letters/symbols + not found in English). If you see that your characters look strange (or you + get encoding errors), you should use this command to set the server + encoding to be the same used in your client program. Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc. - If you don't submit an encoding, the current encoding will be displayed instead. + If you don't submit an encoding, the current encoding will be displayed + instead. """ key = "@encoding" @@ -444,6 +466,7 @@ class CmdEncoding(MuxPlayerCommand): string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding) self.msg(string.strip()) + class CmdPassword(MuxPlayerCommand): """ @password - set your password @@ -463,8 +486,8 @@ class CmdPassword(MuxPlayerCommand): if not self.rhs: self.msg("Usage: @password = ") return - oldpass = self.lhslist[0] # this is already stripped by parse() - newpass = self.rhslist[0] # '' + oldpass = self.lhslist[0] # this is already stripped by parse() + newpass = self.rhslist[0] # '' if not player.check_password(oldpass): self.msg("The specified old password isn't correct.") elif len(newpass) < 3: @@ -474,6 +497,7 @@ class CmdPassword(MuxPlayerCommand): player.save() self.msg("Password changed.") + class CmdQuit(MuxPlayerCommand): """ quit @@ -518,10 +542,10 @@ class CmdColorTest(MuxPlayerCommand): Usage: @color ansi|xterm256 - Print a color map along with in-mud color codes, while testing what is supported in your client. - Choices are 16-color ansi (supported in most muds) or the 256-color xterm256 standard. - No checking is done to determine your client supports color - if not you will - see rubbish appear. + Print a color map along with in-mud color codes, while testing what is + supported in your client. Choices are 16-color ansi (supported in most + muds) or the 256-color xterm256 standard. No checking is done to determine + your client supports color - if not you will see rubbish appear. """ key = "@color" locks = "cmd:all()" @@ -552,10 +576,10 @@ class CmdColorTest(MuxPlayerCommand): ap = ansi.ANSI_PARSER # ansi colors # show all ansi color-related codes - col1 = ["%s%s{n" % (code, code.replace("{","{{")) for code, _ in ap.ext_ansi_map[:-1]] + col1 = ["%s%s{n" % (code, code.replace("{", "{{")) for code, _ in ap.ext_ansi_map[:-1]] hi = "%ch" col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]] - col3 = ["%s%s{n" % (hi+code, (hi+code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]] + col3 = ["%s%s{n" % (hi + code, (hi + code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[3:-2]] table = utils.format_table([col1, col2, col3], extra_space=1) string = "ANSI colors:" for row in table: @@ -566,16 +590,16 @@ class CmdColorTest(MuxPlayerCommand): elif self.args.startswith("x"): # show xterm256 table - table = [[],[],[],[],[],[],[],[],[],[],[],[]] + table = [[], [], [], [], [], [], [], [], [], [], [], []] for ir in range(6): for ig in range(6): for ib in range(6): # foreground table - table[ir].append("%%c%i%i%i%s{n" % (ir,ig,ib, "{{%i%i%i" % (ir,ig,ib))) + table[ir].append("%%c%i%i%i%s{n" % (ir, ig, ib, "{{%i%i%i" % (ir, ig, ib))) # background table - table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib, - 5-ir,5-ig,5-ib, - "{{b%i%i%i" % (ir,ig,ib))) + table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir, ig, ib, + 5 - ir, 5 - ig, 5 - ib, + "{{b%i%i%i" % (ir, ig, ib))) table = self.table_format(table) string = "Xterm256 colors (if not all hues show, your client might not report that it can handle xterm256):" for row in table: @@ -586,6 +610,7 @@ class CmdColorTest(MuxPlayerCommand): # malformed input self.msg("Usage: @color ansi|xterm256") + class CmdQuell(MuxPlayerCommand): """ Quelling permissions @@ -604,7 +629,7 @@ class CmdQuell(MuxPlayerCommand): """ key = "@quell" - aliases =["@unquell"] + aliases = ["@unquell"] locks = "cmd:all()" help_category = "General" @@ -613,8 +638,9 @@ class CmdQuell(MuxPlayerCommand): if self.sessid: char = player.get_puppet(self.sessid) if char: - # we are already puppeting an object. We need to reset the lock caches - # (otherwise the superuser status change won't be visible until repuppet) + # we are already puppeting an object. We need to reset + # the lock caches (otherwise the superuser status change + # won't be visible until repuppet) char.locks.reset() player.locks.reset() diff --git a/src/commands/default/syscommands.py b/src/commands/default/syscommands.py index bc264d98d6..639d821423 100644 --- a/src/commands/default/syscommands.py +++ b/src/commands/default/syscommands.py @@ -33,6 +33,7 @@ from src.commands.default.muxcommand import MuxCommand # Command called when there is no input at line # (i.e. an lone return key) + class SystemNoInput(MuxCommand): """ This is called when there is no input given @@ -44,11 +45,11 @@ class SystemNoInput(MuxCommand): "Do nothing." pass + # # Command called when there was no match to the # command name # - class SystemNoMatch(MuxCommand): """ No command was found matching the given input. @@ -62,6 +63,7 @@ class SystemNoMatch(MuxCommand): """ self.caller.msg("Huh?") + # # Command called when there were mulitple matches to the command. # @@ -100,7 +102,7 @@ class SystemMultimatch(MuxCommand): is_channel = "" is_exit = hasattr(cmd, "is_exit") and cmd.is_exit if is_exit and cmd.destination: - is_exit = " (exit to %s)" % cmd.destination + is_exit = " (exit to %s)" % cmd.destination else: is_exit = "" @@ -124,6 +126,7 @@ class SystemMultimatch(MuxCommand): string = self.format_multimatches(self.caller, self.matches) self.caller.msg(string) + # Command called when the command given at the command line # was identified as a channel name, like there existing a # channel named 'ooc' and the user wrote @@ -167,4 +170,4 @@ class SystemSendToChannel(MuxCommand): return msg = "[%s] %s: %s" % (channel.key, caller.name, msg) msgobj = create.create_message(caller, msg, channels=[channel]) - channel.msg(msgobj) + channel.msg(msgobj) \ No newline at end of file diff --git a/src/commands/default/system.py b/src/commands/default/system.py index b563d03ddf..05ea6eeb94 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -5,10 +5,13 @@ System commands """ import traceback -import os, datetime, time -from time import time as timemeasure +import os +import datetime +import time import sys -import django, twisted +import django +import twisted +from time import time as timemeasure from django.conf import settings from src.server.caches import get_cache_sizes @@ -30,6 +33,7 @@ __all__ = ("CmdReload", "CmdReset", "CmdShutdown", "CmdPy", "CmdScripts", "CmdObjects", "CmdService", "CmdAbout", "CmdTime", "CmdServerLoad") + class CmdReload(MuxCommand): """ Reload the system @@ -55,6 +59,7 @@ class CmdReload(MuxCommand): SESSIONS.announce_all(" Server restarting %s..." % reason) SESSIONS.server.shutdown(mode='reload') + class CmdReset(MuxCommand): """ Reset and reboot the system @@ -110,6 +115,7 @@ class CmdShutdown(MuxCommand): SESSIONS.portal_shutdown() SESSIONS.server.shutdown(mode='shutdown') + class CmdPy(MuxCommand): """ Execute a snippet of python code @@ -157,18 +163,17 @@ class CmdPy(MuxCommand): # import useful variables import ev - available_vars = {'self':caller, - 'me':caller, - 'here':hasattr(caller, "location") and caller.location or None, - 'ev':ev, - 'inherits_from':utils.inherits_from} + available_vars = {'self': caller, + 'me': caller, + 'here': hasattr(caller, "location") and caller.location or None, + 'ev': ev, + 'inherits_from': utils.inherits_from} try: self.msg(">>> %s" % pycode, raw=True, sessid=self.sessid) except TypeError: self.msg(">>> %s" % pycode, raw=True) - mode = "eval" try: try: @@ -195,7 +200,7 @@ class CmdPy(MuxCommand): errlist = errlist[4:] ret = "\n".join("{n<<< %s" % line for line in errlist if line) - if ret != None: + if ret is not None: try: self.msg(ret, sessid=self.sessid) except TypeError: @@ -210,7 +215,16 @@ def format_script_list(scripts): if not scripts: return "" - table = prettytable.PrettyTable(["{wid","{wobj","{wkey","{wintval","{wnext","{wrept","{wdb"," {wtypeclass","{wdesc"],align='r') + table = prettytable.PrettyTable(["{wid", + "{wobj", + "{wkey", + "{wintval", + "{wnext", + "{wrept", + "{wdb", + "{wtypeclass", + "{wdesc"], + align='r') table.align = 'r' for script in scripts: nextrep = script.time_until_next_repeat() @@ -322,7 +336,6 @@ class CmdScripts(MuxCommand): caller.msg(string) - class CmdObjects(MuxCommand): """ @objects - Give a summary of object types in database @@ -357,32 +370,37 @@ class CmdObjects(MuxCommand): nother = nobjs - nchars - nrooms - nexits # total object sum table - totaltable = prettytable.PrettyTable(["{wtype","{wcomment","{wcount", "{w%%"]) + totaltable = prettytable.PrettyTable(["{wtype", "{wcomment", "{wcount", "{w%%"]) totaltable.align = 'l' - totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars)/nobjs)*100)]) - totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms)/nobjs)*100)]) - totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits)/nobjs)*100)]) - totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother)/nobjs)*100)]) + totaltable.add_row(["Characters", "(BASE_CHARACTER_TYPECLASS)", nchars, "%.2f" % ((float(nchars) / nobjs) * 100)]) + totaltable.add_row(["Rooms", "(location=None)", nrooms, "%.2f" % ((float(nrooms) / nobjs) * 100)]) + totaltable.add_row(["Exits", "(destination!=None)", nexits, "%.2f" % ((float(nexits) / nobjs) * 100)]) + totaltable.add_row(["Other", "", nother, "%.2f" % ((float(nother) / nobjs) * 100)]) # typeclass table - typetable = prettytable.PrettyTable(["{wtypeclass","{wcount", "{w%%"]) + typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"]) typetable.align = 'l' dbtotals = ObjectDB.objects.object_totals() for path, count in dbtotals.items(): - typetable.add_row([path, count, "%.2f" % ((float(count)/nobjs)*100)]) + typetable.add_row([path, count, "%.2f" % ((float(count) / nobjs) * 100)]) # last N table objs = ObjectDB.objects.all().order_by("db_date_created")[max(0, nobjs - nlim):] - latesttable = prettytable.PrettyTable(["{wcreated","{wdbref","{wname","{wtypeclass"]) + latesttable = prettytable.PrettyTable(["{wcreated", + "{wdbref", + "{wname", + "{wtypeclass"]) latesttable.align = 'l' for obj in objs: - latesttable.add_row([utils.datetime_format(obj.date_created), obj.dbref, obj.key, obj.typeclass.path]) + latesttable.add_row([utils.datetime_format(obj.date_created), + obj.dbref, obj.key, obj.typeclass.path]) string = "\n{wObject subtype totals (out of %i Objects):{n\n%s" % (nobjs, totaltable) string += "\n{wObject typeclass distribution:{n\n%s" % typetable string += "\n{wLast %s Objects created:{n\n%s" % (min(nobjs, nlim), latesttable) caller.msg(string) + class CmdPlayers(MuxCommand): """ @players - give a summary of all registed Players @@ -397,6 +415,7 @@ class CmdPlayers(MuxCommand): key = "@players" aliases = ["@listplayers"] locks = "cmd:perm(listplayers) or perm(Wizards)" + def func(self): "List the players" @@ -413,10 +432,10 @@ class CmdPlayers(MuxCommand): typetable = prettytable.PrettyTable(["{wtypeclass", "{wcount", "{w%%"]) typetable.align = 'l' for path, count in dbtotals.items(): - typetable.add_row([path, count, "%.2f" % ((float(count)/nplayers)*100)]) + typetable.add_row([path, count, "%.2f" % ((float(count) / nplayers) * 100)]) # last N table plyrs = PlayerDB.objects.all().order_by("db_date_created")[max(0, nplayers - nlim):] - latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref","{wname","{wtypeclass"]) + latesttable = prettytable.PrettyTable(["{wcreated", "{wdbref", "{wname", "{wtypeclass"]) latesttable.align = 'l' for ply in plyrs: latesttable.add_row([utils.datetime_format(ply.date_created), ply.dbref, ply.key, ply.typeclass.path]) @@ -425,6 +444,7 @@ class CmdPlayers(MuxCommand): string += "\n{wLast %s Players created:{n\n%s" % (min(nplayers, nlim), latesttable) caller.msg(string) + class CmdService(MuxCommand): """ @service - manage services @@ -441,7 +461,8 @@ class CmdService(MuxCommand): Service management system. Allows for the listing, starting, and stopping of services. If no switches are given, services will be listed. Note that to operate on the - service you have to supply the full (green or red) name as given in the list. + service you have to supply the full (green or red) name as given + in the list. """ key = "@service" @@ -520,6 +541,7 @@ class CmdService(MuxCommand): caller.msg("Starting service '%s'." % self.args) service.startService() + class CmdAbout(MuxCommand): """ @about - game engine info @@ -567,6 +589,7 @@ class CmdAbout(MuxCommand): sversion) self.caller.msg(string) + class CmdTime(MuxCommand): """ @time @@ -591,6 +614,7 @@ class CmdTime(MuxCommand): table.add_row(["Server time stamp", datetime.datetime.now()]) self.caller.msg(str(table)) + class CmdServerLoad(MuxCommand): """ server load and memory statistics @@ -648,20 +672,20 @@ class CmdServerLoad(MuxCommand): pid = os.getpid() rmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "rss")).read()) / 1024.0 # resident memory vmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "vsz")).read()) / 1024.0 # virtual memory - pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total + pmem = float(os.popen('ps -p %d -o %s | tail -1' % (pid, "%mem")).read()) # percent of resident memory to total rusage = resource.getrusage(resource.RUSAGE_SELF) # load table loadtable = prettytable.PrettyTable(["property", "statistic"]) loadtable.align = 'l' - loadtable.add_row(["Server load (1 min)","%g" % loadavg[0]]) - loadtable.add_row(["Process ID","%g" % pid]), - loadtable.add_row(["Bytes per page","%g " % psize]) + loadtable.add_row(["Server load (1 min)", "%g" % loadavg[0]]) + loadtable.add_row(["Process ID", "%g" % pid]), + loadtable.add_row(["Bytes per page", "%g " % psize]) loadtable.add_row(["CPU time used (total)", "%s (%gs)" % (utils.time_format(rusage.ru_utime), rusage.ru_utime)]) loadtable.add_row(["CPU time used (user)", "%s (%gs)" % (utils.time_format(rusage.ru_stime), rusage.ru_stime)]) loadtable.add_row(["Memory usage","%g MB (%g%%)" % (rmem, pmem)]) loadtable.add_row(["Virtual address space\n {x(resident+swap+caching){n", "%g MB" % vmem]) - loadtable.add_row(["Page faults","%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)]) + loadtable.add_row(["Page faults", "%g hard, %g soft, %g swapouts" % (rusage.ru_majflt, rusage.ru_minflt, rusage.ru_nswap)]) loadtable.add_row(["Disk I/O", "%g reads, %g writes" % (rusage.ru_inblock, rusage.ru_oublock)]) loadtable.add_row(["Network I/O", "%g in, %g out" % (rusage.ru_msgrcv, rusage.ru_msgsnd)]) loadtable.add_row(["Context switching", "%g vol, %g forced, %g signals" % (rusage.ru_nvcsw, rusage.ru_nivcsw, rusage.ru_nsignals)]) @@ -669,17 +693,24 @@ class CmdServerLoad(MuxCommand): string = "{wServer CPU and Memory load:{n\n%s" % loadtable if not is_pypy: - # Cache size measurements are not available on PyPy because it lacks sys.getsizeof + # Cache size measurements are not available on PyPy + # because it lacks sys.getsizeof # object cache size cachedict = _idmapper.cache_size() totcache = cachedict["_total"] sorted_cache = sorted([(key, tup[0], tup[1]) for key, tup in cachedict.items() if key !="_total" and tup[0] > 0], key=lambda tup: tup[2], reverse=True) - memtable = prettytable.PrettyTable(["entity name", "number", "cache (MB)", "idmapper %%"]) + memtable = prettytable.PrettyTable(["entity name", + "number", + "cache (MB)", + "idmapper %%"]) memtable.align = 'l' for tup in sorted_cache: - memtable.add_row([tup[0], "%i" % tup [1], "%5.2f" % tup[2], "%.2f" % (float(tup[2]/totcache[1])*100)]) + memtable.add_row([tup[0], + "%i" % tup[1], + "%5.2f" % tup[2], + "%.2f" % (float(tup[2] / totcache[1]) * 100)]) # get sizes of other caches attr_cache_info, field_cache_info, prop_cache_info = get_cache_sizes() diff --git a/src/commands/default/tests.py b/src/commands/default/tests.py index d00c84bf14..77c03be14b 100644 --- a/src/commands/default/tests.py +++ b/src/commands/default/tests.py @@ -18,7 +18,7 @@ from django.utils.unittest import TestCase from src.server.serversession import ServerSession from src.objects.objects import Object, Character from src.players.player import Player -from src.utils import create, utils, ansi +from src.utils import create, ansi from src.server.sessionhandler import SESSIONS from django.db.models.signals import pre_save @@ -33,16 +33,20 @@ _RE = re.compile(r"^\+|-+\+|\+-+|--*|\|", re.MULTILINE) # Command testing # ------------------------------------------------------------ + def dummy(self, *args, **kwargs): pass SESSIONS.data_out = dummy SESSIONS.disconnect = dummy + class TestObjectClass(Object): def msg(self, text="", **kwargs): "test message" pass + + class TestCharacterClass(Character): def msg(self, text="", **kwargs): "test message" @@ -52,17 +56,21 @@ class TestCharacterClass(Character): if not self.ndb.stored_msg: self.ndb.stored_msg = [] self.ndb.stored_msg.append(text) + + class TestPlayerClass(Player): def msg(self, text="", **kwargs): "test message" if not self.ndb.stored_msg: self.ndb.stored_msg = [] self.ndb.stored_msg.append(text) + def _get_superuser(self): "test with superuser flag" return self.ndb.is_superuser is_superuser = property(_get_superuser) + class CommandTest(TestCase): """ Tests a command @@ -93,6 +101,7 @@ class CommandTest(TestCase): SESSIONS.portal_connect(session.get_sync_data()) SESSIONS.login(SESSIONS.session_from_sessid(self.CID), self.player, testmode=True) + def call(self, cmdobj, args, msg=None, cmdset=None, noansi=True, caller=None): """ Test a command by assigning all the needed @@ -141,6 +150,7 @@ class CommandTest(TestCase): from src.commands.default import general class TestGeneral(CommandTest): CID = 1 + def test_cmds(self): self.call(general.CmdLook(), "here", "Room1\n room_desc") self.call(general.CmdHome(), "", "You are already home") @@ -158,6 +168,7 @@ class TestGeneral(CommandTest): self.call(general.CmdSay(), "Testing", "You say, \"Testing\"") self.call(general.CmdAccess(), "", "Permission Hierarchy (climbing):") + from src.commands.default import help from src.commands.default.cmdset_character import CharacterCmdSet class TestHelp(CommandTest): @@ -167,6 +178,7 @@ class TestHelp(CommandTest): self.call(help.CmdSetHelp(), "testhelp, General = This is a test", "Topic 'testhelp' was successfully created.") self.call(help.CmdHelp(), "testhelp", "Help topic for testhelp", cmdset=CharacterCmdSet()) + from src.commands.default import system class TestSystem(CommandTest): CID = 3 @@ -179,6 +191,7 @@ class TestSystem(CommandTest): self.call(system.CmdAbout(), "", None) self.call(system.CmdServerLoad(), "", "Server CPU and Memory load:") + from src.commands.default import admin class TestAdmin(CommandTest): CID = 4 @@ -190,6 +203,7 @@ class TestAdmin(CommandTest): self.call(admin.CmdPerm(), "Char4b = Builders","Permission 'Builders' given to Char4b.") self.call(admin.CmdBan(), "Char4", "NameBan char4 was added.") + from src.commands.default import player class TestPlayer(CommandTest): CID = 5 @@ -209,6 +223,7 @@ class TestPlayer(CommandTest): self.call(player.CmdCharCreate(), "Test1=Test char","Created new character Test1. Use @ic Test1 to enter the game", caller=self.player) self.call(player.CmdQuell(), "", "Quelling Player permissions (immortals). Use @unquell to get them back.", caller=self.player) + from src.commands.default import building class TestBuilding(CommandTest): CID = 6 @@ -239,6 +254,7 @@ class TestBuilding(CommandTest): self.call(building.CmdScript(), "Obj6 = src.scripts.scripts.Script", "Script src.scripts.scripts.Script successfully added") self.call(building.CmdTeleport(), "TestRoom1", "TestRoom1\nExits: back|Teleported to TestRoom1.") + from src.commands.default import comms class TestComms(CommandTest): CID = 7 @@ -257,6 +273,7 @@ class TestComms(CommandTest): self.call(comms.CmdCBoot(), "", "Usage: @cboot[/quiet] = [:reason]") # noone else connected to boot self.call(comms.CmdCdestroy(), "testchan" ,"Channel 'testchan' was destroyed.") + from src.commands.default import batchprocess class TestBatchProcess(CommandTest): CID = 8 diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index 1c04aef848..aef0eea7be 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -14,7 +14,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") MULTISESSION_MODE = settings.MULTISESSION_MODE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE @@ -26,6 +27,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. @@ -40,7 +42,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): """ @@ -92,12 +94,13 @@ class CmdUnconnectedConnect(MuxCommand): # actually do the login. This will call all other hooks: # session.at_login() - # player.at_init() # always called when object is loaded from disk + # player.at_init() # always called when object is loaded from disk # player.at_pre_login() # player.at_first_login() # only once # player.at_post_login(sessid=sessid) session.sessionhandler.login(session, player) + class CmdUnconnectedCreate(MuxCommand): """ Create a new account. @@ -134,8 +137,9 @@ class CmdUnconnectedCreate(MuxCommand): # sanity checks if not re.findall('^[\w. @+-]+$', playername) or not (0 < len(playername) <= 30): - # this echoes the restrictions made by django's auth module (except not - # allowing spaces, for convenience of logging in). + # this echoes the restrictions made by django's auth + # module (except not allowing spaces, for convenience of + # logging in). string = "\n\r Playername can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only." session.msg(string) return @@ -163,14 +167,14 @@ class CmdUnconnectedCreate(MuxCommand): new_player = create.create_player(playername, None, password, permissions=permissions) - except Exception, e: session.msg("There was an error creating the default Player/Character:\n%s\n If this problem persists, contact an admin." % e) logger.log_trace() return - # This needs to be called so the engine knows this player is logging in for the first time. - # (so it knows to call the right hooks during login later) + # This needs to be called so the engine knows this player is + # logging in for the first time. (so it knows to call the right + # hooks during login later) utils.init_new_player(new_player) # join the new player to the public channel @@ -181,7 +185,6 @@ class CmdUnconnectedCreate(MuxCommand): string = "New player '%s' could not connect to public channel!" % new_player.key logger.log_errmsg(string) - if MULTISESSION_MODE < 2: # if we only allow one character, create one with the same name as Player # (in mode 2, the character must be created manually once logging in) @@ -210,12 +213,14 @@ class CmdUnconnectedCreate(MuxCommand): session.msg(string % (playername, playername)) 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 @@ -230,7 +235,8 @@ class CmdUnconnectedQuit(MuxCommand): "Simply close the connection." session = self.caller #session.msg("Good bye! Disconnecting ...") - session.sessionhandler.disconnect(session, "Good bye! Disconnecting ...") + session.sessionhandler.disconnect(session, "Good bye! Disconnecting.") + class CmdUnconnectedLook(MuxCommand): """ @@ -247,6 +253,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, diff --git a/src/comms/__init__.py b/src/comms/__init__.py index 0fd02fb3a9..721f85bbbb 100644 --- a/src/comms/__init__.py +++ b/src/comms/__init__.py @@ -1,12 +1,14 @@ """ -Makes it easier to import by grouping all relevant things already at this level. +Makes it easier to import by grouping all relevant things already at this +level. You can henceforth import most things directly from src.comms -Also, the initiated object manager is available as src.comms.msgmanager and src.comms.channelmanager. +Also, the initiated object manager is available as src.comms.msgmanager and +src.comms.channelmanager. """ -from src.comms.models import * +from src.comms.models import * msgmanager = Msg.objects channelmanager = ChannelDB.objects diff --git a/src/comms/admin.py b/src/comms/admin.py index 6de3c14710..0d0a200f1c 100644 --- a/src/comms/admin.py +++ b/src/comms/admin.py @@ -1,51 +1,57 @@ -# -# This sets up how models are displayed -# in the web admin interface. -# - -from django.contrib import admin -from src.comms.models import ChannelDB, Msg, PlayerChannelConnection, ExternalChannelConnection - -class MsgAdmin(admin.ModelAdmin): - list_display = ('id', 'db_date_sent', 'db_sender', 'db_receivers', 'db_channels', 'db_message', 'db_lock_storage') - list_display_links = ("id",) - ordering = ["db_date_sent", 'db_sender', 'db_receivers', 'db_channels'] - #readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels'] - search_fields = ['id', '^db_date_sent', '^db_message'] - save_as = True - save_on_top = True - list_select_related = True -#admin.site.register(Msg, MsgAdmin) - -class PlayerChannelConnectionInline(admin.TabularInline): - model = PlayerChannelConnection - fieldsets = ( - (None, { - 'fields':(('db_player', 'db_channel')), - 'classes':('collapse',)}),) - extra = 1 - -class ExternalChannelConnectionInline(admin.StackedInline): - model = ExternalChannelConnection - fieldsets = ( - (None, { - 'fields':(('db_is_enabled','db_external_key', 'db_channel'), 'db_external_send_code', 'db_external_config'), - 'classes':('collapse',) - }),) - extra = 1 - -class ChannelAdmin(admin.ModelAdmin): - inlines = (PlayerChannelConnectionInline, ExternalChannelConnectionInline) - - list_display = ('id', 'db_key', 'db_lock_storage') - list_display_links = ("id", 'db_key') - ordering = ["db_key"] - search_fields = ['id', 'db_key', 'db_aliases'] - save_as = True - save_on_top = True - list_select_related = True - fieldsets = ( - (None, {'fields':(('db_key',),'db_lock_storage',)}), - ) - +# +# This sets up how models are displayed +# in the web admin interface. +# + +from django.contrib import admin +from src.comms.models import ChannelDB, Msg, PlayerChannelConnection, ExternalChannelConnection + + +class MsgAdmin(admin.ModelAdmin): + list_display = ('id', 'db_date_sent', 'db_sender', 'db_receivers', + 'db_channels', 'db_message', 'db_lock_storage') + list_display_links = ("id",) + ordering = ["db_date_sent", 'db_sender', 'db_receivers', 'db_channels'] + #readonly_fields = ['db_message', 'db_sender', 'db_receivers', 'db_channels'] + search_fields = ['id', '^db_date_sent', '^db_message'] + save_as = True + save_on_top = True + list_select_related = True +#admin.site.register(Msg, MsgAdmin) + + +class PlayerChannelConnectionInline(admin.TabularInline): + model = PlayerChannelConnection + fieldsets = ( + (None, { + 'fields':(('db_player', 'db_channel')), + 'classes':('collapse',)}),) + extra = 1 + + +class ExternalChannelConnectionInline(admin.StackedInline): + model = ExternalChannelConnection + fieldsets = ( + (None, { + 'fields': (('db_is_enabled','db_external_key', 'db_channel'), + 'db_external_send_code', 'db_external_config'), + 'classes': ('collapse',) + }),) + extra = 1 + + +class ChannelAdmin(admin.ModelAdmin): + inlines = (PlayerChannelConnectionInline, ExternalChannelConnectionInline) + + list_display = ('id', 'db_key', 'db_lock_storage') + list_display_links = ("id", 'db_key') + ordering = ["db_key"] + search_fields = ['id', 'db_key', 'db_aliases'] + save_as = True + save_on_top = True + list_select_related = True + fieldsets = ( + (None, {'fields': (('db_key',), 'db_lock_storage',)}), + ) + admin.site.register(ChannelDB, ChannelAdmin) \ No newline at end of file diff --git a/src/comms/channelhandler.py b/src/comms/channelhandler.py index 4616e43732..fb65344c72 100644 --- a/src/comms/channelhandler.py +++ b/src/comms/channelhandler.py @@ -23,9 +23,9 @@ update() on the channelhandler. Or use Channel.objects.delete() which does this for you. """ -from src.comms.models import ChannelDB, Msg +from src.comms.models import ChannelDB from src.commands import cmdset, command -from src.utils import utils + class ChannelCommand(command.Command): """ @@ -51,7 +51,8 @@ class ChannelCommand(command.Command): """ Simple parser """ - channelname, msg = self.args.split(":", 1) # cmdhandler sends channame:msg here. + # cmdhandler sends channame:msg here. + channelname, msg = self.args.split(":", 1) self.args = (channelname.strip(), msg.strip()) def func(self): @@ -128,7 +129,7 @@ class ChannelHandler(object): help_category="Channel names", obj=channel, is_channel=True) - cmd.__doc__= self._format_help(channel) + cmd.__doc__ = self._format_help(channel) self.cached_channel_cmds.append(cmd) self.cached_cmdsets = {} diff --git a/src/comms/comms.py b/src/comms/comms.py index 4ac1ad6ffc..867d0b5bb1 100644 --- a/src/comms/comms.py +++ b/src/comms/comms.py @@ -53,7 +53,6 @@ class Comm(TypeClass): else: return '%s: %s' % (sender_string, message) - def format_external(self, msg, senders, emit=False): """ Used for formatting external messages. This is needed as a separate @@ -71,7 +70,6 @@ class Comm(TypeClass): senders = ', '.join(senders) return self.pose_transform(msg, senders) - def format_message(self, msg, emit=False): """ Formats a message body for display. @@ -169,30 +167,39 @@ class Comm(TypeClass): conn.player.msg(msg.message, from_obj=msg.senders) except AttributeError: try: - conn.to_external(msg.message, senders=msg.senders, from_channel=self) + conn.to_external(msg.message, + senders=msg.senders, from_channel=self) except Exception: logger.log_trace("Cannot send msg to connection '%s'" % conn) - def msg(self, msgobj, header=None, senders=None, sender_strings=None, persistent=True, online=False, emit=False, external=False): """ Send the given message to all players connected to channel. Note that no permission-checking is done here; it is assumed to have been - done before calling this method. The optional keywords are not used if persistent is False. + done before calling this method. The optional keywords are not used if + persistent is False. - msgobj - a Msg/TempMsg instance or a message string. If one of the former, the remaining - keywords will be ignored. If a string, this will either be sent as-is (if persistent=False) or - it will be used together with header and senders keywords to create a Msg instance on the fly. - senders - an object, player or a list of objects or players. Optional if persistent=False. - sender_strings - Name strings of senders. Used for external connections where the sender - is not a player or object. When this is defined, external will be assumed. + msgobj - a Msg/TempMsg instance or a message string. If one of the + former, the remaining keywords will be ignored. If a string, + this will either be sent as-is (if persistent=False) or it + will be used together with header and senders keywords to + create a Msg instance on the fly. + senders - an object, player or a list of objects or players. + Optional if persistent=False. + sender_strings - Name strings of senders. Used for external + connections where the sender is not a player or object. When + this is defined, external will be assumed. external - Treat this message agnostic of its sender. - persistent (bool) - ignored if msgobj is a Msg or TempMsg. If True, a Msg will be created, using - header and senders keywords. If False, other keywords will be ignored. - online (bool) - If this is set true, only messages people who are online. Otherwise, messages all players - connected. This can make things faster, but may not trigger listeners on players that are offline. - emit (bool) - Signals to the message formatter that this message is not to be directly associated with a name. + persistent (bool) - ignored if msgobj is a Msg or TempMsg. If True, + a Msg will be created, using header and senders keywords. If + False, other keywords will be ignored. + online (bool) - If this is set true, only messages people who are + online. Otherwise, messages all players connected. This can + make things faster, but may not trigger listeners on players + that are offline. + emit (bool) - Signals to the message formatter that this message is + not to be directly associated with a name. """ if senders: senders = make_iter(senders) @@ -209,7 +216,7 @@ class Comm(TypeClass): msgobj = TempMsg() msgobj.header = header msgobj.message = msg - msgobj.channels = [self.dbobj] # add this channel + msgobj.channels = [self.dbobj] # add this channel if not msgobj.senders: msgobj.senders = senders diff --git a/src/comms/imc2.py b/src/comms/imc2.py index a64056f30f..082ab526e7 100644 --- a/src/comms/imc2.py +++ b/src/comms/imc2.py @@ -65,11 +65,14 @@ class Send_IsAlive(Script): self.interval = 900 self.desc = _("Send an IMC2 is-alive packet") self.persistent = True + def at_repeat(self): IMC2_CLIENT.send_packet(pck.IMC2PacketIsAlive()) + def is_valid(self): "Is only valid as long as there are channels to update" - return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_")) + return any(service for service in SESSIONS.server.services + if service.name.startswith("imc2_")) class Send_Keepalive_Request(Script): """ @@ -81,11 +84,14 @@ class Send_Keepalive_Request(Script): self.interval = 3500 self.desc = _("Send an IMC2 keepalive-request packet") self.persistent = True + def at_repeat(self): IMC2_CLIENT.channel.send_packet(pck.IMC2PacketKeepAliveRequest()) + def is_valid(self): "Is only valid as long as there are channels to update" - return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_")) + return any(service for service in SESSIONS.server.services + if service.name.startswith("imc2_")) class Prune_Inactive_Muds(Script): """ @@ -99,13 +105,17 @@ class Prune_Inactive_Muds(Script): self.desc = _("Check IMC2 list for inactive games") self.persistent = True self.inactive_threshold = 3599 + def at_repeat(self): for name, mudinfo in IMC2_MUDLIST.mud_list.items(): if time() - mudinfo.last_updated > self.inactive_threshold: del IMC2_MUDLIST.mud_list[name] + def is_valid(self): "Is only valid as long as there are channels to update" - return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_")) + return any(service for service in SESSIONS.server.services + if service.name.startswith("imc2_")) + class Sync_Server_Channel_List(Script): """ @@ -116,21 +126,25 @@ class Sync_Server_Channel_List(Script): """ def at_script_creation(self): self.key = "IMC2_Sync_Server_Channel_List" - self.interval = 24 * 3600 # once every day + self.interval = 24 * 3600 # once every day self.desc = _("Re-sync IMC2 network channel list") self.persistent = True + def at_repeat(self): checked_networks = [] network = IMC2_CLIENT.factory.network if not network in checked_networks: channel.send_packet(pkg.IMC2PacketIceRefresh()) checked_networks.append(network) + def is_valid(self): - return any(service for service in SESSIONS.server.services if service.name.startswith("imc2_")) + return any(service for service in SESSIONS.server.services + if service.name.startswith("imc2_")) + + # # IMC2 protocol # - class IMC2Protocol(telnet.StatefulTelnetProtocol): """ Provides the abstraction for the IMC2 protocol. Handles connection, @@ -145,7 +159,6 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol): self.network_name = None self.sequence = None - def connectionMade(self): """ Triggered after connecting to the IMC2 network. @@ -179,8 +192,10 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol): # This gets incremented with every command. self.sequence += 1 packet.imc2_protocol = self - packet_str = utils.to_str(packet.assemble(self.factory.mudname, self.factory.client_pwd, self.factory.server_pwd)) - if IMC2_DEBUG and not (hasattr(packet, 'packet_type') and packet.packet_type == "is-alive"): + packet_str = utils.to_str(packet.assemble(self.factory.mudname, + self.factory.client_pwd, self.factory.server_pwd)) + if IMC2_DEBUG and not (hasattr(packet, 'packet_type') and + packet.packet_type == "is-alive"): logger.log_infomsg("IMC2: SENT> %s" % packet_str) logger.log_infomsg(str(packet)) self.sendLine(packet_str) @@ -257,9 +272,9 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol): """ Handle tells over IMC2 by formatting the text properly """ - return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender":packet.sender, - "origin":packet.origin, - "msg":packet.optional_data.get('text', 'ERROR: No text provided.')} + return _("{c%(sender)s@%(origin)s{n {wpages (over IMC):{n %(msg)s") % {"sender": packet.sender, + "origin": packet.origin, + "msg": packet.optional_data.get('text', 'ERROR: No text provided.')} def lineReceived(self, line): """ @@ -349,6 +364,7 @@ class IMC2Protocol(telnet.StatefulTelnetProtocol): target = data.get("target", "Unknown") self.send_packet(pck.IMC2PacketWhois(from_obj.id, target)) + class IMC2Factory(protocol.ClientFactory): """ Creates instances of the IMC2Protocol. Should really only ever @@ -382,7 +398,9 @@ def build_connection_key(channel, imc2_channel): "Build an id hash for the connection" if hasattr(channel, "key"): channel = channel.key - return "imc2_%s:%s(%s)%s<>%s" % (IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME, imc2_channel, channel) + return "imc2_%s:%s(%s)%s<>%s" % (IMC2_NETWORK, IMC2_PORT, + IMC2_MUDNAME, imc2_channel, channel) + def start_scripts(validate=False): """ @@ -402,6 +420,7 @@ def start_scripts(validate=False): if not search.scripts("IMC2_Sync_Server_Channel_List"): create.create_script(Sync_Server_Channel_List) + def create_connection(channel, imc2_channel): """ This will create a new IMC2<->channel connection. @@ -417,7 +436,8 @@ def create_connection(channel, imc2_channel): old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) if old_conns: - # this evennia channel is already connected to imc. Check if imc2_channel is different. + # this evennia channel is already connected to imc. Check + # if imc2_channel is different. # connection already exists. We try to only connect a new channel old_config = old_conns[0].db_external_config.split(",") if imc2_channel in old_config: @@ -432,9 +452,9 @@ def create_connection(channel, imc2_channel): # no old connection found; create a new one. config = imc2_channel # how the evennia channel will be able to contact this protocol in reverse - send_code = "from src.comms.imc2 import IMC2_CLIENT\n" - send_code += "data={'channel':from_channel}\n" - send_code += "IMC2_CLIENT.msg_imc2(message, senders=[self])\n" + send_code = "from src.comms.imc2 import IMC2_CLIENT\n" + send_code += "data={'channel':from_channel}\n" + send_code += "IMC2_CLIENT.msg_imc2(message, senders=[self])\n" conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_send_code=send_code, db_external_config=config) conn.save() @@ -453,15 +473,22 @@ def delete_connection(channel, imc2_channel): conn.delete() return True + def connect_to_imc2(): "Create the imc instance and connect to the IMC2 network." # connect - imc = internet.TCPClient(IMC2_NETWORK, int(IMC2_PORT), IMC2Factory(IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME, - IMC2_CLIENT_PWD, IMC2_SERVER_PWD)) + imc = internet.TCPClient(IMC2_NETWORK, + int(IMC2_PORT), + IMC2Factory(IMC2_NETWORK, + IMC2_PORT, + IMC2_MUDNAME, + IMC2_CLIENT_PWD, + IMC2_SERVER_PWD)) imc.setName("imc2_%s:%s(%s)" % (IMC2_NETWORK, IMC2_PORT, IMC2_MUDNAME)) SESSIONS.server.services.addService(imc) + def connect_all(): """ Activates the imc2 system. Called by the server if IMC2_ENABLED=True. diff --git a/src/comms/imc2lib/__init__.py b/src/comms/imc2lib/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/src/comms/imc2lib/__init__.py +++ b/src/comms/imc2lib/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/comms/imc2lib/imc2_ansi.py b/src/comms/imc2lib/imc2_ansi.py index 25fe055bea..26815b7bba 100644 --- a/src/comms/imc2lib/imc2_ansi.py +++ b/src/comms/imc2lib/imc2_ansi.py @@ -1,59 +1,60 @@ -""" -ANSI parser - this adds colour to text according to -special markup strings. - -This is a IMC2 complacent version. -""" - -import re -from src.utils import ansi - -class IMCANSIParser(ansi.ANSIParser): - """ - This parser is per the IMC2 specification. - """ - def __init__(self): - normal = ansi.ANSI_NORMAL - hilite = ansi.ANSI_HILITE - self.ansi_map = [ - (r'~Z', normal), # Random - (r'~x', normal + ansi.ANSI_BLACK), # Black - (r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey - (r'~z', hilite + ansi.ANSI_BLACK), - (r'~w', normal + ansi.ANSI_WHITE), # Grey - (r'~W', hilite + ansi.ANSI_WHITE), # White - (r'~g', normal + ansi.ANSI_GREEN), # Dark Green - (r'~G', hilite + ansi.ANSI_GREEN), # Green - (r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta - (r'~m', normal + ansi.ANSI_MAGENTA), - (r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta - (r'~P', hilite + ansi.ANSI_MAGENTA), - (r'~c', normal + ansi.ANSI_CYAN), # Cyan - (r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown) - (r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow - (r'~b', normal + ansi.ANSI_BLUE), # Dark Blue - (r'~B', hilite + ansi.ANSI_BLUE), # Blue - (r'~C', hilite + ansi.ANSI_BLUE), - (r'~r', normal + ansi.ANSI_RED), # Dark Red - (r'~R', hilite + ansi.ANSI_RED), # Red - - ## Formatting - (r'~L', hilite), # Bold/hilite - (r'~!', normal), # reset - (r'\\r', normal), - (r'\\n', ansi.ANSI_RETURN), - ] - # prepare regex matching - self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1]) - for sub in self.ansi_map] - # prepare matching ansi codes overall - self.ansi_regex = re.compile("\033\[[0-9;]+m") - - -ANSI_PARSER = IMCANSIParser() - -def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER): - """ - Shortcut to use the IMC2 ANSI parser. - """ - return parser.parse_ansi(string, strip_ansi=strip_ansi) +""" +ANSI parser - this adds colour to text according to +special markup strings. + +This is a IMC2 complacent version. +""" + +import re +from src.utils import ansi + + +class IMCANSIParser(ansi.ANSIParser): + """ + This parser is per the IMC2 specification. + """ + def __init__(self): + normal = ansi.ANSI_NORMAL + hilite = ansi.ANSI_HILITE + self.ansi_map = [ + (r'~Z', normal), # Random + (r'~x', normal + ansi.ANSI_BLACK), # Black + (r'~D', hilite + ansi.ANSI_BLACK), # Dark Grey + (r'~z', hilite + ansi.ANSI_BLACK), + (r'~w', normal + ansi.ANSI_WHITE), # Grey + (r'~W', hilite + ansi.ANSI_WHITE), # White + (r'~g', normal + ansi.ANSI_GREEN), # Dark Green + (r'~G', hilite + ansi.ANSI_GREEN), # Green + (r'~p', normal + ansi.ANSI_MAGENTA), # Dark magenta + (r'~m', normal + ansi.ANSI_MAGENTA), + (r'~M', hilite + ansi.ANSI_MAGENTA), # Magenta + (r'~P', hilite + ansi.ANSI_MAGENTA), + (r'~c', normal + ansi.ANSI_CYAN), # Cyan + (r'~y', normal + ansi.ANSI_YELLOW), # Dark Yellow (brown) + (r'~Y', hilite + ansi.ANSI_YELLOW), # Yellow + (r'~b', normal + ansi.ANSI_BLUE), # Dark Blue + (r'~B', hilite + ansi.ANSI_BLUE), # Blue + (r'~C', hilite + ansi.ANSI_BLUE), + (r'~r', normal + ansi.ANSI_RED), # Dark Red + (r'~R', hilite + ansi.ANSI_RED), # Red + + ## Formatting + (r'~L', hilite), # Bold/hilite + (r'~!', normal), # reset + (r'\\r', normal), + (r'\\n', ansi.ANSI_RETURN), + ] + # prepare regex matching + self.ansi_sub = [(re.compile(sub[0], re.DOTALL), sub[1]) + for sub in self.ansi_map] + # prepare matching ansi codes overall + self.ansi_regex = re.compile("\033\[[0-9;]+m") + +ANSI_PARSER = IMCANSIParser() + + +def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER): + """ + Shortcut to use the IMC2 ANSI parser. + """ + return parser.parse_ansi(string, strip_ansi=strip_ansi) diff --git a/src/comms/imc2lib/imc2_listeners.py b/src/comms/imc2lib/imc2_listeners.py index bef1b4ae04..ab31076f49 100644 --- a/src/comms/imc2lib/imc2_listeners.py +++ b/src/comms/imc2lib/imc2_listeners.py @@ -1,24 +1,24 @@ -""" -This module handles some of the -reply packets like whois-reply. - -""" -from src.objects.models import ObjectDB -from src.comms.imc2lib import imc2_ansi - -from django.utils.translation import ugettext as _ - -def handle_whois_reply(packet): - """ - When the player sends an imcwhois request, the outgoing - packet contains the id of the one asking. This handler catches the - (possible) reply from the server, parses the id back to the - original asker and tells them the result. - """ - try: - pobject = ObjectDB.objects.get(id=packet.target) - response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown')) - string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text} - pobject.msg(string.strip()) - except ObjectDB.DoesNotExist: - # No match found for whois sender. Ignore it. - pass +""" +This module handles some of the -reply packets like whois-reply. + +""" +from src.objects.models import ObjectDB +from src.comms.imc2lib import imc2_ansi + +from django.utils.translation import ugettext as _ + +def handle_whois_reply(packet): + """ + When the player sends an imcwhois request, the outgoing + packet contains the id of the one asking. This handler catches the + (possible) reply from the server, parses the id back to the + original asker and tells them the result. + """ + try: + pobject = ObjectDB.objects.get(id=packet.target) + response_text = imc2_ansi.parse_ansi(packet.optional_data.get('text', 'Unknown')) + string = _('Whois reply from %(origin)s: %(msg)s') % {"origin":packet.origin, "msg":response_text} + pobject.msg(string.strip()) + except ObjectDB.DoesNotExist: + # No match found for whois sender. Ignore it. + pass diff --git a/src/comms/imc2lib/imc2_packets.py b/src/comms/imc2lib/imc2_packets.py index b427f14a6c..f7df12e1e7 100644 --- a/src/comms/imc2lib/imc2_packets.py +++ b/src/comms/imc2lib/imc2_packets.py @@ -1,766 +1,795 @@ -""" -IMC2 packets. These are pretty well documented at: -http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol - -""" -import shlex -from django.conf import settings - -class Lexxer(shlex.shlex): - """ - A lexical parser for interpreting IMC2 packets. - """ - def __init__(self, packet_str, posix=True): - shlex.shlex.__init__(self, packet_str, posix=True) - # Single-quotes are notably not present. This is important! - self.quotes = '"' - self.commenters = '' - # This helps denote what constitutes a continuous token. - self.wordchars += "~`!@#$%^&*()-_+=[{]}|\\;:',<.>/?" - -class IMC2Packet(object): - """ - Base IMC2 packet class. This is generally sub-classed, aside from using it - to parse incoming packets from the IMC2 network server. - """ - def __init__(self, mudname=None, packet_str=None): - """ - Optionally, parse a packet and load it up. - """ - # The following fields are all according to the basic packet format of: - # @ @ - self.sender = None - if not mudname: - mudname = settings.SERVERNAME - self.origin = mudname - self.sequence = None - self.route = mudname - self.packet_type = None - self.target = None - self.destination = None - # Optional data. - self.optional_data = {} - # Reference to the IMC2Protocol object doing the sending. - self.imc2_protocol = None - - if packet_str: - # The lexxer handles the double quotes correctly, unlike just - # splitting. Spaces throw things off, so shlex handles it - # gracefully, ala POSIX shell-style parsing. - lex = Lexxer(packet_str) - - # Token counter. - counter = 0 - for token in lex: - if counter == 0: - # This is the sender@origin token. - sender_origin = token - split_sender_origin = sender_origin.split('@') - self.sender = split_sender_origin[0].strip() - self.origin = split_sender_origin[1] - elif counter == 1: - # Numeric time-based sequence. - self.sequence = token - elif counter == 2: - # Packet routing info. - self.route = token - elif counter == 3: - # Packet type string. - self.packet_type = token - elif counter == 4: - # Get values for the target and destination attributes. - target_destination = token - split_target_destination = target_destination.split('@') - self.target = split_target_destination[0] - try: - self.destination = split_target_destination[1] - except IndexError: - # There is only one element to the target@dest segment - # of the packet. Wipe the target and move the captured - # value to the destination attrib. - self.target = '*' - self.destination = split_target_destination[0] - elif counter > 4: - # Populate optional data. - try: - key, value = token.split('=', 1) - self.optional_data[key] = value - except ValueError: - # Failed to split on equal sign, disregard. - pass - # Increment and continue to the next token (if applicable) - counter += 1 - - def __str__(self): - retval = """ - --IMC2 package (%s) - Sender: %s - Origin: %s - Sequence: %s - Route: %s - Type: %s - Target: %s - Dest.: %s - Data: - %s - ------------------------""" % (self.packet_type, self.sender, - self.origin, self.sequence, - self.route, self.packet_type, - self.target, self.destination, - "\n ".join(["%s: %s" % items for items in self.optional_data.items()])) - return retval.strip() - - def _get_optional_data_string(self): - """ - Generates the optional data string to tack on to the end of the packet. - """ - if self.optional_data: - data_string = '' - for key, value in self.optional_data.items(): - # Determine the number of words in this value. - words = len(str(value).split(' ')) - # Anything over 1 word needs double quotes. - if words > 1: - value = '"%s"' % (value,) - data_string += '%s=%s ' % (key, value) - return data_string.strip() - else: - return '' - - def _get_sender_name(self): - """ - Calculates the sender name to be sent with the packet. - """ - if self.sender == '*': - # Some packets have no sender. - return '*' - elif str(self.sender).isdigit(): - return self.sender - elif type(self.sender) in [type(u""),type(str())]: - #this is used by e.g. IRC where no user object is present. - return self.sender.strip().replace(' ', '_') - elif self.sender: - # Player object. - name = self.sender.get_name(fullname=False, show_dbref=False, - show_flags=False, - no_ansi=True) - # IMC2 does not allow for spaces. - return name.strip().replace(' ', '_') - else: - # None value. Do something or other. - return 'Unknown' - - def assemble(self, mudname=None, client_pwd=None, server_pwd=None): - """ - Assembles the packet and returns the ready-to-send string. - Note that the arguments are not used, they are there for - consistency across all packets. - """ - self.sequence = self.imc2_protocol.sequence - packet = "%s@%s %s %s %s %s@%s %s\n" % ( - self._get_sender_name(), - self.origin, - self.sequence, - self.route, - self.packet_type, - self.target, - self.destination, - self._get_optional_data_string()) - return packet.strip() - -class IMC2PacketAuthPlaintext(object): - """ - IMC2 plain-text authentication packet. Auth packets are strangely - formatted, so this does not sub-class IMC2Packet. The SHA and plain text - auth packets are the two only non-conformers. - - CLIENT Sends: - PW version= autosetup (SHA256) - - Optional Arguments( required if using the specified authentication method: - (SHA256) The literal string: SHA256. This is sent to notify the server - that the MUD is SHA256-Enabled. All future logins from this - client will be expected in SHA256-AUTH format if the server - supports it. - """ - def assemble(self, mudname=None, client_pwd=None, server_pwd=None): - """ - This is one of two strange packets, just assemble the packet manually - and go. - """ - return 'PW %s %s version=2 autosetup %s\n' %(mudname, client_pwd, server_pwd) - -class IMC2PacketKeepAliveRequest(IMC2Packet): - """ - Description: - This packet is sent by a MUD to trigger is-alive packets from other MUDs. - This packet is usually followed by the sending MUD's own is-alive packet. - It is used in the filling of a client's MUD list, thus any MUD that doesn't - respond with an is-alive isn't marked as online on the sending MUD's mudlist. - - Data: - (none) - - Example of a received keepalive-request: - *@YourMUD 1234567890 YourMUD!Hub1 keepalive-request *@* - - Example of a sent keepalive-request: - *@YourMUD 1234567890 YourMUD keepalive-request *@* - """ - def __init__(self): - super(IMC2PacketKeepAliveRequest, self).__init__() - self.sender = '*' - self.packet_type = 'keepalive-request' - self.target = '*' - self.destination = '*' - -class IMC2PacketIsAlive(IMC2Packet): - """ - Description: - This packet is the reply to a keepalive-request packet. It is responsible - for filling a client's mudlist with the information about other MUDs on the - network. - - Data: - versionid= - Where is the text version ID of the client. ("IMC2 4.5 MUD-Net") - - url= - Where is the proper URL of the client. (http://www.domain.com) - - host= - Where is the telnet address of the MUD. (telnet://domain.com) - - port= - Where is the telnet port of the MUD. - - (These data fields are not sent by the MUD, they are added by the server.) - networkname= - Where is the network name that the MUD/server is on. ("MyNetwork") - - sha256= - This is an optional tag that denotes the SHA-256 capabilities of a - MUD or server. - - Example of a received is-alive: - *@SomeMUD 1234567890 SomeMUD!Hub2 is-alive *@YourMUD versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" networkname="MyNetwork" sha256=1 host=domain.com port=5500 - - Example of a sent is-alive: - *@YourMUD 1234567890 YourMUD is-alive *@* versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" host=domain.com port=5500 - """ - def __init__(self): - super(IMC2PacketIsAlive, self).__init__() - self.sender = '*' - self.packet_type = 'is-alive' - self.target = '*' - self.destination = '*' - self.optional_data = {'versionid': 'Evennia IMC2', - 'url': '"http://www.evennia.com"', - 'host': 'test.com', - 'port': '5555'} - -class IMC2PacketIceRefresh(IMC2Packet): - """ - Description: - This packet is sent by the MUD to request data about the channels on the - network. Servers with channels reply with an ice-update packet for each - channel they control. The usual target for this packet is IMC@$. - - Data: - (none) - - Example: - *@YourMUD 1234567890 YourMUD!Hub1 ice-refresh IMC@$ - """ - def __init__(self): - super(IMC2PacketIceRefresh, self).__init__() - self.sender = '*' - self.packet_type = 'ice-refresh' - self.target = 'IMC' - self.destination = '$' - -class IMC2PacketIceUpdate(IMC2Packet): - """ - Description: - A server returns this packet with the data of a channel when prompted with - an ice-refresh request. - - Data: - channel= - The channel's network name in the format of ServerName:ChannelName - - owner= - The Name@MUD of the channel's owner - - operators= - A space-seperated list of the Channel's operators, in the format of Person@MUD - - policy= - The policy is either "open" or "private" with no quotes. - - invited= - The space-seperated list of invited User@MUDs, only valid for a - "private" channel. - - excluded= - The space-seperated list of banned User@MUDs, only valid for "open" - channels. - - level= The default level of the channel: Admin, Imp, Imm, - Mort, or None - - localname= The suggested local name of the channel. - - Examples: - - Open Policy: - ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:ichat owner=Imm@SomeMUD operators=Other@SomeMUD policy=open excluded="Flamer@badMUD Jerk@dirtyMUD" level=Imm localname=ichat - - Private Policy: - ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:secretchat owner=Imm@SomeMUD operators=Other@SomeMUD policy=private invited="SpecialDude@OtherMUD CoolDude@WeirdMUD" level=Mort localname=schat - """ - pass - -class IMC2PacketIceMsgRelayed(IMC2Packet): - """ - Description: - The -r in this ice-msg packet means it was relayed. This, along with the - ice-msg-p packet, are used with private policy channels. The 'r' stands - for 'relay'. All incoming channel messages are from ICE@, where - is the server hosting the channel. - - Data: - realfrom= - The User@MUD the message came from. - - channel= - The Server:Channel the message is intended to be displayed on. - - text= - The message text. - - emote= - An integer value designating emotes. 0 for no emote, 1 for an emote, - and 2 for a social. - - Examples: - ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="Aha! I got it!" emote=0 - - ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text=Ahh emote=0 - - ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="grins evilly." emote=1 - - ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="You@YourMUD grins evilly!" emote=2 - """ - pass - -class IMC2PacketIceMsgPrivate(IMC2Packet): - """ - Description: - This packet is sent when a player sends a message to a private channel. - This packet should never be seen as incoming to a client. The target of - this packet should be IMC@ of the server hosting the channel. - - Data: - channel= - The Server:Channel the message is intended to be displayed on. - - text= - The message text. - - emote= - An integer value designating emotes. 0 for no emote, 1 for an emote, - and 2 for a social. - - echo= - Tells the server to echo the message back to the sending MUD. This is only - seen on out-going messages. - - Examples: - You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="Ahh! I got it!" emote=0 echo=1 - You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text=Ahh! emote=0 echo=1 - You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="grins evilly." emote=1 echo=1 - You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="You@YourMUD grins evilly." emote=2 echo=1 - """ - pass - -class IMC2PacketIceMsgBroadcasted(IMC2Packet): - """ - Description: - This is the packet used to chat on open policy channels. When sent from a - MUD, it is broadcasted across the network. Other MUDs receive it in-tact - as it was sent by the originating MUD. The server that hosts the channel - sends the packet back to the originating MUD as an 'echo' by removing the - "echo=1" and attaching the "sender=Person@MUD" data field. - - Data: - channel= - The Server:Channel the message is intended to be displayed on. - - text= - The message text. - - emote= - An integer value designating emotes. 0 for no emote, 1 for an emote, - and 2 for a social. - - *echo= - This stays on broadcasted messages. It tells the channel's server to - relay an echo back. - - *sender= - The hosting server replaces "echo=1" with this when sending the echo back - to the originating MUD. - - Examples: - (See above for emote/social examples as they are pretty much the same) - - Return Echo Packet: - You-YourMUD@Hub1 1234567890 Hub1 ice-msg-b *@YourMUD text=Hi! channel=Hub1:ichat sender=You@YourMUD emote=0 - - Broadcasted Packet: - You@YourMUD 1234567890 YourMUD!Hub1 ice-msg-b *@* channel=Hub1:ichat text=Hi! emote=0 echo=1 - """ - def __init__(self, server, channel, pobject, message): - """ - Args: - server: (String) Server name the channel resides on (obs - this is e.g. Server01, not the full network name!) - channel: (String) Name of the IMC2 channel. - pobject: (Object) Object sending the message. - message: (String) Message to send. - """ - super(IMC2PacketIceMsgBroadcasted, self).__init__() - self.sender = pobject - self.packet_type = 'ice-msg-b' - self.target = '*' - self.destination = '*' - self.optional_data = {'channel': '%s:%s' % (server, channel), - 'text': message, - 'emote': 0, - 'echo': 1} - -class IMC2PacketUserCache(IMC2Packet): - """ - Description: - Sent by a MUD with a new IMC2-able player or when a player's gender changes, - this packet contains only the gender for data. The packet's origination - should be the Player@MUD. - - Data: - gender= 0 is male, 1 is female, 2 is anything else such as neuter. - Will be referred to as "it". - - Example: - Dude@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache *@* gender=0 - """ - pass - -class IMC2PacketUserCacheRequest(IMC2Packet): - """ - Description: - The MUD sends this packet out when making a request for the user-cache - information of the user included in the data part of the packet. - - Data: - user= The Person@MUD whose data the MUD is seeking. - - Example: - *@YourMUD 1234567890 YourMUD user-cache-request *@SomeMUD user=Dude@SomeMUD - """ - pass - -class IMC2PacketUserCacheReply(IMC2Packet): - """ - Description: - A reply to the user-cache-request packet. It contains the user and gender - for the user. - - Data: - user= - The Person@MUD whose data the MUD requested. - - gender= - The gender of the Person@MUD in the 'user' field. - - Example: - *@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache-reply *@YourMUD user=Dude@SomeMUD gender=0 - """ - pass - -class IMC2PacketTell(IMC2Packet): - """ - Description: - This packet is used to communicate private messages between users on MUDs - across the network. - - Data: - text= Message text - isreply= Two settings: 1 denotes a reply, 2 denotes a tell social. - - Example: - - Originating: - You@YourMUD 1234567890 YourMUD tell Dude@SomeMUD text="Having fun?" - - Reply from Dude: - Dude@SomeMUD 1234567890 SomeMUD!Hub1 tell You@YourMUD text="Yeah, this is cool!" isreply=1 - """ - def __init__(self, pobject, target, destination, message): - super(IMC2PacketTell, self).__init__() - self.sender = pobject - self.packet_type = "tell" - self.target = target - self.destination = destination - self.optional_data = {"text": message, - "isreply":None} - - def assemble(self, mudname=None, client_pwd=None, server_pwd=None): - self.sequence = self.imc2_protocol.sequence - #self.route = "%s!%s" % (self.origin, self.imc2_protocol.factory.servername.capitalize()) - return '''"%s@%s %s %s tell %s@%s text="%s"''' % (self.sender, self.origin, self.sequence, - self.route, self.target, self.destination, - self.optional_data.get("text","NO TEXT GIVEN")) - -class IMC2PacketEmote(IMC2Packet): - """ - Description: - This packet seems to be sent by servers when notifying the network of a new - channel or the destruction of a channel. - - Data: - channel= - Unsure of what this means. The channel seen in both creation and - destruction packets is 15. - - level= - I am assuming this is the permission level of the sender. In both - creation and destruction messages, this is -1. - - text= - This is the message to be sent to the users. - - Examples: - ICE@Hub1 1234567890 Hub1 emote *@* channel=15 level=-1 text="the channel called hub1:test has been destroyed by You@YourMUD." - """ - pass - -class IMC2PacketRemoteAdmin(IMC2Packet): - """ - Description: - This packet is used in remote server administration. Please note that - SHA-256 Support is *required* for a client to use this feature. The command - can vary, in fact this very packet is highly dependant on the server it's - being directed to. In most cases, sending the 'list' command will have a - remote-admin enabled server send you the list of commands it will accept. - - Data: - command= - The command being sent to the server for processing. - - data= - Data associated with the command. This is not always required. - - hash= - The SHA-256 hash that is verified by the server. This hash is generated in - the same manner as an authentication packet. - - Example: - You@YourMUD 1234567890 YourMUD remote-admin IMC@Hub1 command=list hash= - """ - pass - -class IMC2PacketIceCmd(IMC2Packet): - """ - Description: - Used for remote channel administration. In most cases, one must be listed - as a channel creator on the target server in order to do much with this - packet. Other cases include channel operators. - - Data: - channel= - The target server:channel for the command. - - command= - The command to be processed. - - data= - Data associated with the command. This is not always required. - - Example: - You@YourMUD 1234567890 YourMUD ice-cmd IMC@hub1 channel=hub1:ichat command=list - """ - pass - -class IMC2PacketDestroy(IMC2Packet): - """ - Description: - Sent by a server to indicate the destruction of a channel it hosted. - The mud should remove this channel from its local configuration. - - Data: - channel= The server:channel being destroyed. - """ - pass - -class IMC2PacketWho(IMC2Packet): - """ - Description: - A seemingly mutli-purpose information-requesting packet. The istats - packet currently only works on servers, or at least that's the case on - MUD-Net servers. The 'finger' type takes a player name in addition to the - type name. - - Example: "finger Dude". The 'who' and 'info' types take no argument. - The MUD is responsible for building the reply text sent in the who-reply - packet. - - Data: - type= Types: who, info, "finger ", istats (server only) - - Example: - Dude@SomeMUD 1234567890 SomeMUD!Hub1 who *@YourMUD type=who - """ - pass - -class IMC2PacketWhoReply(IMC2Packet): - """ - Description: - The multi-purpose reply to the multi-purpose information-requesting 'who' - packet. The MUD is responsible for building the return data, including the - format of it. The mud can use the permission level sent in the original who - packet to filter the output. The example below is the MUD-Net format. - - Data: - text= The formatted reply to a 'who' packet. - - Additional Notes: - The example below is for the who list packet. The same construction would - go into formatting the other types of who packets. - - Example: - *@YourMUD 1234567890 YourMUD who-reply Dude@SomeMUD text="\n\r~R-=< ~WPlayers on YourMUD ~R>=-\n\r ~Y-=< ~Wtelnet://yourmud.domain.com:1234 ~Y>=-\n\r\n\r~B--------------------------------=< ~WPlayers ~B>=---------------------------------\n\r\n\r ~BPlayer ~z<--->~G Mortal the Toy\n\r\n\r~R-------------------------------=< ~WImmortals ~R>=--------------------------------\n\r\n\r ~YStaff ~z<--->~G You the Immortal\n\r\n\r~Y<~W2 Players~Y> ~Y<~WHomepage: http://www.yourmud.com~Y> <~W 2 Max Since Reboot~Y>\n\r~Y<~W3 logins since last reboot on Tue Feb 24, 2004 6:55:59 PM EST~Y>" - """ - pass - -class IMC2PacketWhois(IMC2Packet): - """ - Description: - Sends a request to the network for the location of the specified player. - - Data: - level= The permission level of the person making the request. - - Example: - You@YourMUD 1234567890 YourMUD whois dude@* level=5 - """ - def __init__(self, pobject_id, whois_target): - super(IMC2PacketWhois, self).__init__() - self.sender = pobject_id # Use the dbref, it's easier to trace back for the whois-reply. - self.packet_type = 'whois' - self.target = whois_target - self.destination = '*' - self.optional_data = {'level': '5'} - -class IMC2PacketWhoisReply(IMC2Packet): - """ - Description: - The reply to a whois packet. The MUD is responsible for building and formatting - the text sent back to the requesting player, and can use the permission level - sent in the original whois packet to filter or block the response. - - Data: - text= The whois text. - - Example: - *@SomeMUD 1234567890 SomeMUD!Hub1 whois-reply You@YourMUD text="~RIMC Locate: ~YDude@SomeMUD: ~cOnline.\n\r" - """ - pass - -class IMC2PacketBeep(IMC2Packet): - """ - Description: - Sends out a beep packet to the Player@MUD. The client receiving this should - then send a bell-character to the target player to 'beep' them. - - Example: - You@YourMUD 1234567890 YourMUD beep dude@somemud - """ - pass - -class IMC2PacketIceChanWho(IMC2Packet): - """ - Description: - Sends a request to the specified MUD or * to list all the users listening - to the specified channel. - - Data: - level= - Sender's permission level. - - channel= - The server:chan name of the channel. - - lname= - The localname of the channel. - - Example: - You@YourMUD 1234567890 YourMUD ice-chan-who somemud level=5 channel=Hub1:ichat lname=ichat - """ - pass - -class IMC2PacketIceChanWhoReply(IMC2Packet): - """ - Description: - This is the reply packet for an ice-chan-who. The MUD is responsible for - creating and formatting the list sent back in the 'list' field. The - permission level sent in the original ice-chan-who packet can be used to - filter or block the response. - - Data: - channel= - The server:chan of the requested channel. - - list= - The formatted list of local listeners for that MUD. - - Example: - *@SomeMUD 1234567890 SomeMUD!Hub1 ice-chan-whoreply You@YourMUD channel=Hub1:ichat list="The following people are listening to ichat on SomeMUD:\n\r\n\rDude\n\r" - """ - pass - -class IMC2PacketLaston(IMC2Packet): - """ - Description: - This packet queries the server the mud is connected to to find out when a - specified user was last seen by the network on a public channel. - - Data: - username= The user, user@mud, or "all" being queried. Responses - to this packet will be sent by the server in the form of a series of tells. - - Example: User@MUD 1234567890 MUD imc-laston SERVER username=somenamehere - """ - pass - -class IMC2PacketCloseNotify(IMC2Packet): - """ - Description: - This packet alerts the network when a server or MUD has disconnected. The - server hosting the server or MUD is responsible for sending this packet - out across the network. Clients need only process the packet to remove the - disconnected MUD from their MUD list (or mark it as Disconnected). - - Data: - host= - The MUD or server that has disconnected from the network. - - Example: - *@Hub2 1234567890 Hub2!Hub1 close-notify *@* host=DisconnMUD - """ - pass - -if __name__ == "__main__": - packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1" - packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0" - print IMC2Packet(packstr) - +""" +IMC2 packets. These are pretty well documented at: +http://www.mudbytes.net/index.php?a=articles&s=imc2_protocol + +""" +import shlex +from django.conf import settings + +class Lexxer(shlex.shlex): + """ + A lexical parser for interpreting IMC2 packets. + """ + def __init__(self, packet_str, posix=True): + shlex.shlex.__init__(self, packet_str, posix=True) + # Single-quotes are notably not present. This is important! + self.quotes = '"' + self.commenters = '' + # This helps denote what constitutes a continuous token. + self.wordchars += "~`!@#$%^&*()-_+=[{]}|\\;:',<.>/?" + +class IMC2Packet(object): + """ + Base IMC2 packet class. This is generally sub-classed, aside from using it + to parse incoming packets from the IMC2 network server. + """ + def __init__(self, mudname=None, packet_str=None): + """ + Optionally, parse a packet and load it up. + """ + # The following fields are all according to the basic packet format of: + # @ @ + self.sender = None + if not mudname: + mudname = settings.SERVERNAME + self.origin = mudname + self.sequence = None + self.route = mudname + self.packet_type = None + self.target = None + self.destination = None + # Optional data. + self.optional_data = {} + # Reference to the IMC2Protocol object doing the sending. + self.imc2_protocol = None + + if packet_str: + # The lexxer handles the double quotes correctly, unlike just + # splitting. Spaces throw things off, so shlex handles it + # gracefully, ala POSIX shell-style parsing. + lex = Lexxer(packet_str) + + # Token counter. + counter = 0 + for token in lex: + if counter == 0: + # This is the sender@origin token. + sender_origin = token + split_sender_origin = sender_origin.split('@') + self.sender = split_sender_origin[0].strip() + self.origin = split_sender_origin[1] + elif counter == 1: + # Numeric time-based sequence. + self.sequence = token + elif counter == 2: + # Packet routing info. + self.route = token + elif counter == 3: + # Packet type string. + self.packet_type = token + elif counter == 4: + # Get values for the target and destination attributes. + target_destination = token + split_target_destination = target_destination.split('@') + self.target = split_target_destination[0] + try: + self.destination = split_target_destination[1] + except IndexError: + # There is only one element to the target@dest segment + # of the packet. Wipe the target and move the captured + # value to the destination attrib. + self.target = '*' + self.destination = split_target_destination[0] + elif counter > 4: + # Populate optional data. + try: + key, value = token.split('=', 1) + self.optional_data[key] = value + except ValueError: + # Failed to split on equal sign, disregard. + pass + # Increment and continue to the next token (if applicable) + counter += 1 + + def __str__(self): + retval = """ + --IMC2 package (%s) + Sender: %s + Origin: %s + Sequence: %s + Route: %s + Type: %s + Target: %s + Dest.: %s + Data: + %s + ------------------------""" % (self.packet_type, self.sender, + self.origin, self.sequence, + self.route, self.packet_type, + self.target, self.destination, + "\n ".join(["%s: %s" % items for items in self.optional_data.items()])) + return retval.strip() + + def _get_optional_data_string(self): + """ + Generates the optional data string to tack on to the end of the packet. + """ + if self.optional_data: + data_string = '' + for key, value in self.optional_data.items(): + # Determine the number of words in this value. + words = len(str(value).split(' ')) + # Anything over 1 word needs double quotes. + if words > 1: + value = '"%s"' % (value,) + data_string += '%s=%s ' % (key, value) + return data_string.strip() + else: + return '' + + def _get_sender_name(self): + """ + Calculates the sender name to be sent with the packet. + """ + if self.sender == '*': + # Some packets have no sender. + return '*' + elif str(self.sender).isdigit(): + return self.sender + elif type(self.sender) in [type(u""),type(str())]: + #this is used by e.g. IRC where no user object is present. + return self.sender.strip().replace(' ', '_') + elif self.sender: + # Player object. + name = self.sender.get_name(fullname=False, show_dbref=False, + show_flags=False, + no_ansi=True) + # IMC2 does not allow for spaces. + return name.strip().replace(' ', '_') + else: + # None value. Do something or other. + return 'Unknown' + + def assemble(self, mudname=None, client_pwd=None, server_pwd=None): + """ + Assembles the packet and returns the ready-to-send string. + Note that the arguments are not used, they are there for + consistency across all packets. + """ + self.sequence = self.imc2_protocol.sequence + packet = "%s@%s %s %s %s %s@%s %s\n" % ( + self._get_sender_name(), + self.origin, + self.sequence, + self.route, + self.packet_type, + self.target, + self.destination, + self._get_optional_data_string()) + return packet.strip() + + +class IMC2PacketAuthPlaintext(object): + """ + IMC2 plain-text authentication packet. Auth packets are strangely + formatted, so this does not sub-class IMC2Packet. The SHA and plain text + auth packets are the two only non-conformers. + + CLIENT Sends: + PW version= autosetup (SHA256) + + Optional Arguments( required if using the specified authentication method: + (SHA256) The literal string: SHA256. This is sent to notify the server + that the MUD is SHA256-Enabled. All future logins from this + client will be expected in SHA256-AUTH format if the server + supports it. + """ + def assemble(self, mudname=None, client_pwd=None, server_pwd=None): + """ + This is one of two strange packets, just assemble the packet manually + and go. + """ + return 'PW %s %s version=2 autosetup %s\n' %(mudname, client_pwd, server_pwd) + + +class IMC2PacketKeepAliveRequest(IMC2Packet): + """ + Description: + This packet is sent by a MUD to trigger is-alive packets from other MUDs. + This packet is usually followed by the sending MUD's own is-alive packet. + It is used in the filling of a client's MUD list, thus any MUD that doesn't + respond with an is-alive isn't marked as online on the sending MUD's + mudlist. + + Data: + (none) + + Example of a received keepalive-request: + *@YourMUD 1234567890 YourMUD!Hub1 keepalive-request *@* + + Example of a sent keepalive-request: + *@YourMUD 1234567890 YourMUD keepalive-request *@* + """ + def __init__(self): + super(IMC2PacketKeepAliveRequest, self).__init__() + self.sender = '*' + self.packet_type = 'keepalive-request' + self.target = '*' + self.destination = '*' + + +class IMC2PacketIsAlive(IMC2Packet): + """ + Description: + This packet is the reply to a keepalive-request packet. It is responsible + for filling a client's mudlist with the information about other MUDs on the + network. + + Data: + versionid= + Where is the text version ID of the client. ("IMC2 4.5 MUD-Net") + + url= + Where is the proper URL of the client. (http://www.domain.com) + + host= + Where is the telnet address of the MUD. (telnet://domain.com) + + port= + Where is the telnet port of the MUD. + + (These data fields are not sent by the MUD, they are added by the server.) + networkname= + Where is the network name that the MUD/server is on. ("MyNetwork") + + sha256= + This is an optional tag that denotes the SHA-256 capabilities of a + MUD or server. + + Example of a received is-alive: + *@SomeMUD 1234567890 SomeMUD!Hub2 is-alive *@YourMUD versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" networkname="MyNetwork" sha256=1 host=domain.com port=5500 + + Example of a sent is-alive: + *@YourMUD 1234567890 YourMUD is-alive *@* versionid="IMC2 4.5 MUD-Net" url="http://www.domain.com" host=domain.com port=5500 + """ + def __init__(self): + super(IMC2PacketIsAlive, self).__init__() + self.sender = '*' + self.packet_type = 'is-alive' + self.target = '*' + self.destination = '*' + self.optional_data = {'versionid': 'Evennia IMC2', + 'url': '"http://www.evennia.com"', + 'host': 'test.com', + 'port': '5555'} + + +class IMC2PacketIceRefresh(IMC2Packet): + """ + Description: + This packet is sent by the MUD to request data about the channels on the + network. Servers with channels reply with an ice-update packet for each + channel they control. The usual target for this packet is IMC@$. + + Data: + (none) + + Example: + *@YourMUD 1234567890 YourMUD!Hub1 ice-refresh IMC@$ + """ + def __init__(self): + super(IMC2PacketIceRefresh, self).__init__() + self.sender = '*' + self.packet_type = 'ice-refresh' + self.target = 'IMC' + self.destination = '$' + + +class IMC2PacketIceUpdate(IMC2Packet): + """ + Description: + A server returns this packet with the data of a channel when prompted with + an ice-refresh request. + + Data: + channel= + The channel's network name in the format of ServerName:ChannelName + + owner= + The Name@MUD of the channel's owner + + operators= + A space-seperated list of the Channel's operators, (format: Person@MUD) + + policy= + The policy is either "open" or "private" with no quotes. + + invited= + The space-seperated list of invited User@MUDs, only valid for a + "private" channel. + + excluded= + The space-seperated list of banned User@MUDs, only valid for "open" + channels. + + level= The default level of the channel: Admin, Imp, Imm, + Mort, or None + + localname= The suggested local name of the channel. + + Examples: + + Open Policy: + ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:ichat owner=Imm@SomeMUD operators=Other@SomeMUD policy=open excluded="Flamer@badMUD Jerk@dirtyMUD" level=Imm localname=ichat + + Private Policy: + ICE@Hub1 1234567890 Hub1!Hub2 ice-update *@YourMUD channel=Hub1:secretchat owner=Imm@SomeMUD operators=Other@SomeMUD policy=private invited="SpecialDude@OtherMUD CoolDude@WeirdMUD" level=Mort localname=schat + """ + pass + + +class IMC2PacketIceMsgRelayed(IMC2Packet): + """ + Description: + The -r in this ice-msg packet means it was relayed. This, along with the + ice-msg-p packet, are used with private policy channels. The 'r' stands + for 'relay'. All incoming channel messages are from ICE@, where + is the server hosting the channel. + + Data: + realfrom= + The User@MUD the message came from. + + channel= + The Server:Channel the message is intended to be displayed on. + + text= + The message text. + + emote= + An integer value designating emotes. 0 for no emote, 1 for an emote, + and 2 for a social. + + Examples: + ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="Aha! I got it!" emote=0 + + ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text=Ahh emote=0 + + ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="grins evilly." emote=1 + + ICE@Hub1 1234567890 Hub1!Hub2 ice-msg-r *@YourMUD realfrom=You@YourMUD channel=hub1:secret text="You@YourMUD grins evilly!" emote=2 + """ + pass + + +class IMC2PacketIceMsgPrivate(IMC2Packet): + """ + Description: + This packet is sent when a player sends a message to a private channel. + This packet should never be seen as incoming to a client. The target of + this packet should be IMC@ of the server hosting the channel. + + Data: + channel= + The Server:Channel the message is intended to be displayed on. + + text= + The message text. + + emote= + An integer value designating emotes. 0 for no emote, 1 for an emote, + and 2 for a social. + + echo= + Tells the server to echo the message back to the sending MUD. This is only + seen on out-going messages. + + Examples: + You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="Ahh! I got it!" emote=0 echo=1 + You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text=Ahh! emote=0 echo=1 + You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="grins evilly." emote=1 echo=1 + You@YourMUD 1234567890 YourMUD ice-msg-p IMC@Hub1 channel=Hub1:secret text="You@YourMUD grins evilly." emote=2 echo=1 + """ + pass + + +class IMC2PacketIceMsgBroadcasted(IMC2Packet): + """ + Description: + This is the packet used to chat on open policy channels. When sent from a + MUD, it is broadcasted across the network. Other MUDs receive it in-tact + as it was sent by the originating MUD. The server that hosts the channel + sends the packet back to the originating MUD as an 'echo' by removing the + "echo=1" and attaching the "sender=Person@MUD" data field. + + Data: + channel= + The Server:Channel the message is intended to be displayed on. + + text= + The message text. + + emote= + An integer value designating emotes. 0 for no emote, 1 for an emote, + and 2 for a social. + + *echo= + This stays on broadcasted messages. It tells the channel's server to + relay an echo back. + + *sender= + The hosting server replaces "echo=1" with this when sending the echo back + to the originating MUD. + + Examples: + (See above for emote/social examples as they are pretty much the same) + + Return Echo Packet: + You-YourMUD@Hub1 1234567890 Hub1 ice-msg-b *@YourMUD text=Hi! channel=Hub1:ichat sender=You@YourMUD emote=0 + + Broadcasted Packet: + You@YourMUD 1234567890 YourMUD!Hub1 ice-msg-b *@* channel=Hub1:ichat text=Hi! emote=0 echo=1 + """ + def __init__(self, server, channel, pobject, message): + """ + Args: + server: (String) Server name the channel resides on (obs - this is + e.g. Server01, not the full network name!) + channel: (String) Name of the IMC2 channel. + pobject: (Object) Object sending the message. + message: (String) Message to send. + """ + super(IMC2PacketIceMsgBroadcasted, self).__init__() + self.sender = pobject + self.packet_type = 'ice-msg-b' + self.target = '*' + self.destination = '*' + self.optional_data = {'channel': '%s:%s' % (server, channel), + 'text': message, + 'emote': 0, + 'echo': 1} + + +class IMC2PacketUserCache(IMC2Packet): + """ + Description: + Sent by a MUD with a new IMC2-able player or when a player's gender changes, + this packet contains only the gender for data. The packet's origination + should be the Player@MUD. + + Data: + gender= 0 is male, 1 is female, 2 is anything else such as neuter. + Will be referred to as "it". + + Example: + Dude@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache *@* gender=0 + """ + pass + + +class IMC2PacketUserCacheRequest(IMC2Packet): + """ + Description: + The MUD sends this packet out when making a request for the user-cache + information of the user included in the data part of the packet. + + Data: + user= The Person@MUD whose data the MUD is seeking. + + Example: + *@YourMUD 1234567890 YourMUD user-cache-request *@SomeMUD user=Dude@SomeMUD + """ + pass + + +class IMC2PacketUserCacheReply(IMC2Packet): + """ + Description: + A reply to the user-cache-request packet. It contains the user and gender + for the user. + + Data: + user= + The Person@MUD whose data the MUD requested. + + gender= + The gender of the Person@MUD in the 'user' field. + + Example: + *@someMUD 1234567890 SomeMUD!Hub2!Hub1 user-cache-reply *@YourMUD user=Dude@SomeMUD gender=0 + """ + pass + + +class IMC2PacketTell(IMC2Packet): + """ + Description: + This packet is used to communicate private messages between users on MUDs + across the network. + + Data: + text= Message text + isreply= Two settings: 1 denotes a reply, 2 denotes a tell social. + + Example: + + Originating: + You@YourMUD 1234567890 YourMUD tell Dude@SomeMUD text="Having fun?" + + Reply from Dude: + Dude@SomeMUD 1234567890 SomeMUD!Hub1 tell You@YourMUD text="Yeah, this is cool!" isreply=1 + """ + def __init__(self, pobject, target, destination, message): + super(IMC2PacketTell, self).__init__() + self.sender = pobject + self.packet_type = "tell" + self.target = target + self.destination = destination + self.optional_data = {"text": message, + "isreply":None} + + def assemble(self, mudname=None, client_pwd=None, server_pwd=None): + self.sequence = self.imc2_protocol.sequence + #self.route = "%s!%s" % (self.origin, self.imc2_protocol.factory.servername.capitalize()) + return '''"%s@%s %s %s tell %s@%s text="%s"''' % (self.sender, self.origin, self.sequence, + self.route, self.target, self.destination, + self.optional_data.get("text","NO TEXT GIVEN")) + + +class IMC2PacketEmote(IMC2Packet): + """ + Description: + This packet seems to be sent by servers when notifying the network of a new + channel or the destruction of a channel. + + Data: + channel= + Unsure of what this means. The channel seen in both creation and + destruction packets is 15. + + level= + I am assuming this is the permission level of the sender. In both + creation and destruction messages, this is -1. + + text= + This is the message to be sent to the users. + + Examples: + ICE@Hub1 1234567890 Hub1 emote *@* channel=15 level=-1 text="the + channel called hub1:test has been destroyed by You@YourMUD." + """ + pass + + +class IMC2PacketRemoteAdmin(IMC2Packet): + """ + Description: + This packet is used in remote server administration. Please note that + SHA-256 Support is *required* for a client to use this feature. The command + can vary, in fact this very packet is highly dependant on the server it's + being directed to. In most cases, sending the 'list' command will have a + remote-admin enabled server send you the list of commands it will accept. + + Data: + command= + The command being sent to the server for processing. + + data= + Data associated with the command. This is not always required. + + hash= + The SHA-256 hash that is verified by the server. This hash is generated in + the same manner as an authentication packet. + + Example: + You@YourMUD 1234567890 YourMUD remote-admin IMC@Hub1 command=list hash= + """ + pass + + +class IMC2PacketIceCmd(IMC2Packet): + """ + Description: + Used for remote channel administration. In most cases, one must be listed + as a channel creator on the target server in order to do much with this + packet. Other cases include channel operators. + + Data: + channel= + The target server:channel for the command. + + command= + The command to be processed. + + data= + Data associated with the command. This is not always required. + + Example: + You@YourMUD 1234567890 YourMUD ice-cmd IMC@hub1 channel=hub1:ichat command=list + """ + pass + + +class IMC2PacketDestroy(IMC2Packet): + """ + Description: + Sent by a server to indicate the destruction of a channel it hosted. + The mud should remove this channel from its local configuration. + + Data: + channel= The server:channel being destroyed. + """ + pass + + +class IMC2PacketWho(IMC2Packet): + """ + Description: + A seemingly mutli-purpose information-requesting packet. The istats + packet currently only works on servers, or at least that's the case on + MUD-Net servers. The 'finger' type takes a player name in addition to the + type name. + + Example: "finger Dude". The 'who' and 'info' types take no argument. + The MUD is responsible for building the reply text sent in the who-reply + packet. + + Data: + type= Types: who, info, "finger ", istats (server only) + + Example: + Dude@SomeMUD 1234567890 SomeMUD!Hub1 who *@YourMUD type=who + """ + pass + + +class IMC2PacketWhoReply(IMC2Packet): + """ + Description: + The multi-purpose reply to the multi-purpose information-requesting 'who' + packet. The MUD is responsible for building the return data, including the + format of it. The mud can use the permission level sent in the original who + packet to filter the output. The example below is the MUD-Net format. + + Data: + text= The formatted reply to a 'who' packet. + + Additional Notes: + The example below is for the who list packet. The same construction would + go into formatting the other types of who packets. + + Example: + *@YourMUD 1234567890 YourMUD who-reply Dude@SomeMUD text="\n\r~R-=< ~WPlayers on YourMUD ~R>=-\n\r ~Y-=< ~Wtelnet://yourmud.domain.com:1234 ~Y>=-\n\r\n\r~B--------------------------------=< ~WPlayers ~B>=---------------------------------\n\r\n\r ~BPlayer ~z<--->~G Mortal the Toy\n\r\n\r~R-------------------------------=< ~WImmortals ~R>=--------------------------------\n\r\n\r ~YStaff ~z<--->~G You the Immortal\n\r\n\r~Y<~W2 Players~Y> ~Y<~WHomepage: http://www.yourmud.com~Y> <~W 2 Max Since Reboot~Y>\n\r~Y<~W3 logins since last reboot on Tue Feb 24, 2004 6:55:59 PM EST~Y>" + """ + pass + + +class IMC2PacketWhois(IMC2Packet): + """ + Description: + Sends a request to the network for the location of the specified player. + + Data: + level= The permission level of the person making the request. + + Example: + You@YourMUD 1234567890 YourMUD whois dude@* level=5 + """ + def __init__(self, pobject_id, whois_target): + super(IMC2PacketWhois, self).__init__() + # Use the dbref, it's easier to trace back for the whois-reply. + self.sender = pobject_id + self.packet_type = 'whois' + self.target = whois_target + self.destination = '*' + self.optional_data = {'level': '5'} + + +class IMC2PacketWhoisReply(IMC2Packet): + """ + Description: + The reply to a whois packet. The MUD is responsible for building and formatting + the text sent back to the requesting player, and can use the permission level + sent in the original whois packet to filter or block the response. + + Data: + text= The whois text. + + Example: + *@SomeMUD 1234567890 SomeMUD!Hub1 whois-reply You@YourMUD text="~RIMC Locate: ~YDude@SomeMUD: ~cOnline.\n\r" + """ + pass + + +class IMC2PacketBeep(IMC2Packet): + """ + Description: + Sends out a beep packet to the Player@MUD. The client receiving this should + then send a bell-character to the target player to 'beep' them. + + Example: + You@YourMUD 1234567890 YourMUD beep dude@somemud + """ + pass + + +class IMC2PacketIceChanWho(IMC2Packet): + """ + Description: + Sends a request to the specified MUD or * to list all the users listening + to the specified channel. + + Data: + level= + Sender's permission level. + + channel= + The server:chan name of the channel. + + lname= + The localname of the channel. + + Example: + You@YourMUD 1234567890 YourMUD ice-chan-who somemud level=5 channel=Hub1:ichat lname=ichat + """ + pass + + +class IMC2PacketIceChanWhoReply(IMC2Packet): + """ + Description: + This is the reply packet for an ice-chan-who. The MUD is responsible for + creating and formatting the list sent back in the 'list' field. The + permission level sent in the original ice-chan-who packet can be used to + filter or block the response. + + Data: + channel= + The server:chan of the requested channel. + + list= + The formatted list of local listeners for that MUD. + + Example: + *@SomeMUD 1234567890 SomeMUD!Hub1 ice-chan-whoreply You@YourMUD channel=Hub1:ichat list="The following people are listening to ichat on SomeMUD:\n\r\n\rDude\n\r" + """ + pass + + +class IMC2PacketLaston(IMC2Packet): + """ + Description: + This packet queries the server the mud is connected to to find out when a + specified user was last seen by the network on a public channel. + + Data: + username= The user, user@mud, or "all" being queried. Responses + to this packet will be sent by the server in the form of a series of tells. + + Example: User@MUD 1234567890 MUD imc-laston SERVER username=somenamehere + """ + pass + + +class IMC2PacketCloseNotify(IMC2Packet): + """ + Description: + This packet alerts the network when a server or MUD has disconnected. The + server hosting the server or MUD is responsible for sending this packet + out across the network. Clients need only process the packet to remove the + disconnected MUD from their MUD list (or mark it as Disconnected). + + Data: + host= + The MUD or server that has disconnected from the network. + + Example: + *@Hub2 1234567890 Hub2!Hub1 close-notify *@* host=DisconnMUD + """ + pass + +if __name__ == "__main__": + packstr = "Kayle@MW 1234567 MW!Server02!Server01 ice-msg-b *@* channel=Server01:ichat text=\"*they're going woot\" emote=0 echo=1" + packstr = "*@Lythelian 1234567 Lythelian!Server01 is-alive *@* versionid=\"Tim's LPC IMC2 client 30-Jan-05 / Dead Souls integrated\" networkname=Mudbytes url=http://dead-souls.net host=70.32.76.142 port=6666 sha256=0" + print IMC2Packet(packstr) + diff --git a/src/comms/imc2lib/imc2_trackers.py b/src/comms/imc2lib/imc2_trackers.py index f38d1fdfd7..cd053fde50 100644 --- a/src/comms/imc2lib/imc2_trackers.py +++ b/src/comms/imc2lib/imc2_trackers.py @@ -1,104 +1,108 @@ -""" -Certain periodic packets are sent by connected MUDs (is-alive, user-cache, -etc). The IMC2 protocol assumes that each connected MUD will capture these and -populate/maintain their own lists of other servers connected. This module -contains stuff like this. -""" -from time import time - -class IMC2Mud(object): - """ - Stores information about other games connected to our current IMC2 network. - """ - def __init__(self, packet): - self.name = packet.origin - self.versionid = packet.optional_data.get('versionid', None) - self.networkname = packet.optional_data.get('networkname', None) - self.url = packet.optional_data.get('url', None) - self.host = packet.optional_data.get('host', None) - self.port = packet.optional_data.get('port', None) - self.sha256 = packet.optional_data.get('sha256', None) - # This is used to determine when a Mud has fallen into inactive status. - self.last_updated = time() - -class IMC2MudList(object): - """ - Keeps track of other MUDs connected to the IMC network. - """ - def __init__(self): - # Mud list is stored in a dict, key being the IMC Mud name. - self.mud_list = {} - - def get_mud_list(self): - """ - Returns a sorted list of connected Muds. - """ - muds = self.mud_list.items() - muds.sort() - return [value for key, value in muds] - - def update_mud_from_packet(self, packet): - """ - This grabs relevant info from the packet and stuffs it in the - Mud list for later retrieval. - """ - mud = IMC2Mud(packet) - self.mud_list[mud.name] = mud - - def remove_mud_from_packet(self, packet): - """ - Removes a mud from the Mud list when given a packet. - """ - mud = IMC2Mud(packet) - try: - del self.mud_list[mud.name] - except KeyError: - # No matching entry, no big deal. - pass - -class IMC2Channel(object): - """ - Stores information about channels available on the network. - """ - def __init__(self, packet): - self.localname = packet.optional_data.get('localname', None) - self.name = packet.optional_data.get('channel', None) - self.level = packet.optional_data.get('level', None) - self.owner = packet.optional_data.get('owner', None) - self.policy = packet.optional_data.get('policy', None) - self.last_updated = time() - -class IMC2ChanList(object): - """ - Keeps track of other MUDs connected to the IMC network. - """ - def __init__(self): - # Chan list is stored in a dict, key being the IMC Mud name. - self.chan_list = {} - - def get_channel_list(self): - """ - Returns a sorted list of cached channels. - """ - channels = self.chan_list.items() - channels.sort() - return [value for key, value in channels] - - def update_channel_from_packet(self, packet): - """ - This grabs relevant info from the packet and stuffs it in the - channel list for later retrieval. - """ - channel = IMC2Channel(packet) - self.chan_list[channel.name] = channel - - def remove_channel_from_packet(self, packet): - """ - Removes a channel from the Channel list when given a packet. - """ - channel = IMC2Channel(packet) - try: - del self.chan_list[channel.name] - except KeyError: - # No matching entry, no big deal. - pass +""" +Certain periodic packets are sent by connected MUDs (is-alive, user-cache, +etc). The IMC2 protocol assumes that each connected MUD will capture these and +populate/maintain their own lists of other servers connected. This module +contains stuff like this. +""" +from time import time + + +class IMC2Mud(object): + """ + Stores information about other games connected to our current IMC2 network. + """ + def __init__(self, packet): + self.name = packet.origin + self.versionid = packet.optional_data.get('versionid', None) + self.networkname = packet.optional_data.get('networkname', None) + self.url = packet.optional_data.get('url', None) + self.host = packet.optional_data.get('host', None) + self.port = packet.optional_data.get('port', None) + self.sha256 = packet.optional_data.get('sha256', None) + # This is used to determine when a Mud has fallen into inactive status. + self.last_updated = time() + + +class IMC2MudList(object): + """ + Keeps track of other MUDs connected to the IMC network. + """ + def __init__(self): + # Mud list is stored in a dict, key being the IMC Mud name. + self.mud_list = {} + + def get_mud_list(self): + """ + Returns a sorted list of connected Muds. + """ + muds = self.mud_list.items() + muds.sort() + return [value for key, value in muds] + + def update_mud_from_packet(self, packet): + """ + This grabs relevant info from the packet and stuffs it in the + Mud list for later retrieval. + """ + mud = IMC2Mud(packet) + self.mud_list[mud.name] = mud + + def remove_mud_from_packet(self, packet): + """ + Removes a mud from the Mud list when given a packet. + """ + mud = IMC2Mud(packet) + try: + del self.mud_list[mud.name] + except KeyError: + # No matching entry, no big deal. + pass + + +class IMC2Channel(object): + """ + Stores information about channels available on the network. + """ + def __init__(self, packet): + self.localname = packet.optional_data.get('localname', None) + self.name = packet.optional_data.get('channel', None) + self.level = packet.optional_data.get('level', None) + self.owner = packet.optional_data.get('owner', None) + self.policy = packet.optional_data.get('policy', None) + self.last_updated = time() + + +class IMC2ChanList(object): + """ + Keeps track of other MUDs connected to the IMC network. + """ + def __init__(self): + # Chan list is stored in a dict, key being the IMC Mud name. + self.chan_list = {} + + def get_channel_list(self): + """ + Returns a sorted list of cached channels. + """ + channels = self.chan_list.items() + channels.sort() + return [value for key, value in channels] + + def update_channel_from_packet(self, packet): + """ + This grabs relevant info from the packet and stuffs it in the + channel list for later retrieval. + """ + channel = IMC2Channel(packet) + self.chan_list[channel.name] = channel + + def remove_channel_from_packet(self, packet): + """ + Removes a channel from the Channel list when given a packet. + """ + channel = IMC2Channel(packet) + try: + del self.chan_list[channel.name] + except KeyError: + # No matching entry, no big deal. + pass diff --git a/src/comms/irc.py b/src/comms/irc.py index 6915a60e30..1f2f65decc 100644 --- a/src/comms/irc.py +++ b/src/comms/irc.py @@ -1,205 +1,218 @@ -""" -This connects to an IRC network/channel and launches an 'bot' onto it. -The bot then pipes what is being said between the IRC channel and one or -more Evennia channels. -""" -from twisted.application import internet -from twisted.words.protocols import irc -from twisted.internet import protocol -from django.conf import settings -from src.comms.models import ExternalChannelConnection, ChannelDB -from src.utils import logger, utils -from src.server.sessionhandler import SESSIONS - -from django.utils.translation import ugettext as _ - -INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0]) -IRC_CHANNELS = [] - -def msg_info(message): - """ - Send info to default info channel - """ - message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message) - try: - INFOCHANNEL[0].msg(message) - except AttributeError: - logger.log_infomsg("MUDinfo (irc): %s" % message) - -class IRC_Bot(irc.IRCClient): - """ - This defines an IRC bot that connects to an IRC channel - and relays data to and from an evennia game. - """ - - def _get_nickname(self): - "required for correct nickname setting" - return self.factory.nickname - nickname = property(_get_nickname) - - def signedOn(self): - # This is the first point the protocol is instantiated. - # add this protocol instance to the global list so we - # can access it later to send data. - global IRC_CHANNELS - self.join(self.factory.channel) - - IRC_CHANNELS.append(self) - #msg_info("Client connecting to %s.'" % (self.factory.channel)) - - def joined(self, channel): - msg = _("joined %s.") % self.factory.pretty_key - msg_info(msg) - logger.log_infomsg(msg) - - def get_mesg_info(self, user, irc_channel, msg): - """ - Get basic information about a message posted in IRC. - """ - #find irc->evennia channel mappings - conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) - if not conns: - return - #format message: - user = user.split("!")[0] - if user: - user.strip() - else: - user = _("Unknown") - msg = msg.strip() - sender_strings = ["%s@%s" % (user, irc_channel)] - return conns, msg, sender_strings - - def privmsg(self, user, irc_channel, msg): - "Someone has written something in irc channel. Echo it to the evennia channel" - conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) - #logger.log_infomsg("" - #find irc->evennia channel mappings - conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) - if not conns: - return - conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) - # Transform this into a pose. - msg = ':' + msg - #logger.log_infomsg("%s" % (irc_network, irc_port, irc_channel, irc_bot_nick, channel) - -def build_service_key(key): - return "IRCbot:%s" % key - -def create_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick): - """ - This will create a new IRC<->channel connection. - """ - if not type(channel) == ChannelDB: - new_channel = ChannelDB.objects.filter(db_key=channel) - if not new_channel: - logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel) - return False - channel = new_channel[0] - key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick) - - old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) - if old_conns: - return False - config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick) - # how the channel will be able to contact this protocol - send_code = "from src.comms.irc import IRC_CHANNELS\n" - send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key - send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n" - conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_send_code=send_code, - db_external_config=config) - conn.save() - - # connect - connect_to_irc(conn) - return True - -def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick): - "Destroy a connection" - if hasattr(channel, 'key'): - channel = channel.key - - key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick) - service_key = build_service_key(key) - try: - conn = ExternalChannelConnection.objects.get(db_external_key=key) - except Exception: - return False - conn.delete() - - try: - service = SESSIONS.server.services.getServiceNamed(service_key) - except Exception: - return True - if service.running: - SESSIONS.server.services.removeService(service) - return True - -def connect_to_irc(connection): - "Create the bot instance and connect to the IRC network and channel." - # get config - key = utils.to_str(connection.external_key) - service_key = build_service_key(key) - irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')] - # connect - bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick, - connection.channel.key)) - bot.setName(service_key) - SESSIONS.server.services.addService(bot) - -def connect_all(): - """ - Activate all irc bots. - """ - for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'): - connect_to_irc(connection) - - +""" +This connects to an IRC network/channel and launches an 'bot' onto it. +The bot then pipes what is being said between the IRC channel and one or +more Evennia channels. +""" +from twisted.application import internet +from twisted.words.protocols import irc +from twisted.internet import protocol +from django.conf import settings +from src.comms.models import ExternalChannelConnection, ChannelDB +from src.utils import logger, utils +from src.server.sessionhandler import SESSIONS + +from django.utils.translation import ugettext as _ + +INFOCHANNEL = ChannelDB.objects.channel_search(settings.CHANNEL_MUDINFO[0]) +IRC_CHANNELS = [] + + +def msg_info(message): + """ + Send info to default info channel + """ + message = '[%s][IRC]: %s' % (INFOCHANNEL[0].key, message) + try: + INFOCHANNEL[0].msg(message) + except AttributeError: + logger.log_infomsg("MUDinfo (irc): %s" % message) + + +class IRC_Bot(irc.IRCClient): + """ + This defines an IRC bot that connects to an IRC channel + and relays data to and from an evennia game. + """ + + def _get_nickname(self): + "required for correct nickname setting" + return self.factory.nickname + nickname = property(_get_nickname) + + def signedOn(self): + # This is the first point the protocol is instantiated. + # add this protocol instance to the global list so we + # can access it later to send data. + global IRC_CHANNELS + self.join(self.factory.channel) + + IRC_CHANNELS.append(self) + #msg_info("Client connecting to %s.'" % (self.factory.channel)) + + def joined(self, channel): + msg = _("joined %s.") % self.factory.pretty_key + msg_info(msg) + logger.log_infomsg(msg) + + def get_mesg_info(self, user, irc_channel, msg): + """ + Get basic information about a message posted in IRC. + """ + #find irc->evennia channel mappings + conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) + if not conns: + return + #format message: + user = user.split("!")[0] + if user: + user.strip() + else: + user = _("Unknown") + msg = msg.strip() + sender_strings = ["%s@%s" % (user, irc_channel)] + return conns, msg, sender_strings + + def privmsg(self, user, irc_channel, msg): + "Someone has written something in irc channel. Echo it to the evennia channel" + conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) + #logger.log_infomsg("" + #find irc->evennia channel mappings + conns = ExternalChannelConnection.objects.filter(db_external_key=self.factory.key) + if not conns: + return + conns, msg, sender_strings = self.get_mesg_info(user, irc_channel, msg) + # Transform this into a pose. + msg = ':' + msg + #logger.log_infomsg("%s" % (irc_network, irc_port, + irc_channel, irc_bot_nick, channel) + + +def build_service_key(key): + return "IRCbot:%s" % key + + +def create_connection(channel, irc_network, irc_port, + irc_channel, irc_bot_nick): + """ + This will create a new IRC<->channel connection. + """ + if not type(channel) == ChannelDB: + new_channel = ChannelDB.objects.filter(db_key=channel) + if not new_channel: + logger.log_errmsg(_("Cannot attach IRC<->Evennia: Evennia Channel '%s' not found") % channel) + return False + channel = new_channel[0] + key = build_connection_key(channel, irc_network, irc_port, + irc_channel, irc_bot_nick) + + old_conns = ExternalChannelConnection.objects.filter(db_external_key=key) + if old_conns: + return False + config = "%s|%s|%s|%s" % (irc_network, irc_port, irc_channel, irc_bot_nick) + # how the channel will be able to contact this protocol + send_code = "from src.comms.irc import IRC_CHANNELS\n" + send_code += "matched_ircs = [irc for irc in IRC_CHANNELS if irc.factory.key == '%s']\n" % key + send_code += "[irc.msg_irc(message, senders=[self]) for irc in matched_ircs]\n" + conn = ExternalChannelConnection(db_channel=channel, + db_external_key=key, + db_external_send_code=send_code, + db_external_config=config) + conn.save() + + # connect + connect_to_irc(conn) + return True + +def delete_connection(channel, irc_network, irc_port, irc_channel, irc_bot_nick): + "Destroy a connection" + if hasattr(channel, 'key'): + channel = channel.key + + key = build_connection_key(channel, irc_network, irc_port, irc_channel, irc_bot_nick) + service_key = build_service_key(key) + try: + conn = ExternalChannelConnection.objects.get(db_external_key=key) + except Exception: + return False + conn.delete() + + try: + service = SESSIONS.server.services.getServiceNamed(service_key) + except Exception: + return True + if service.running: + SESSIONS.server.services.removeService(service) + return True + +def connect_to_irc(connection): + "Create the bot instance and connect to the IRC network and channel." + # get config + key = utils.to_str(connection.external_key) + service_key = build_service_key(key) + irc_network, irc_port, irc_channel, irc_bot_nick = [utils.to_str(conf) for conf in connection.external_config.split('|')] + # connect + bot = internet.TCPClient(irc_network, int(irc_port), IRCbotFactory(key, irc_channel, irc_network, irc_port, irc_bot_nick, + connection.channel.key)) + bot.setName(service_key) + SESSIONS.server.services.addService(bot) + +def connect_all(): + """ + Activate all irc bots. + """ + for connection in ExternalChannelConnection.objects.filter(db_external_key__startswith='irc_'): + connect_to_irc(connection) + + diff --git a/src/comms/managers.py b/src/comms/managers.py index 7991a80604..5b4c5f55fb 100644 --- a/src/comms/managers.py +++ b/src/comms/managers.py @@ -18,10 +18,12 @@ _User = None # error class + class CommError(Exception): "Raise by comm system, to allow feedback to player when caught." pass + # # helper functions # @@ -43,6 +45,7 @@ def dbref(dbref, reqhash=True): return None return dbref + def identify_object(inp): "identify if an object is a player or an object; return its database model" # load global stores @@ -61,18 +64,25 @@ def identify_object(inp): return inp, None # try to identify the type try: - obj = _GA(inp, "dbobj") # this works for all typeclassed entities + obj = _GA(inp, "dbobj") # this works for all typeclassed entities except AttributeError: obj = inp typ = type(obj) - if typ == _PlayerDB: return obj, "player" - elif typ == _ObjectDB: return obj, "object" - elif typ == _ChannelDB: return obj, "channel" - elif dbref(obj): return dbref(obj), "dbref" - elif typ == basestring: return obj, "string" - elif typ == _ExternalConnection: return obj, "external" + if typ == _PlayerDB: + return obj, "player" + elif typ == _ObjectDB: + return obj, "object" + elif typ == _ChannelDB: + return obj, "channel" + elif dbref(obj): + return dbref(obj), "dbref" + elif typ == basestring: + return obj, "string" + elif typ == _ExternalConnection: + return obj, "external" return obj, None # Something else + def to_object(inp, objtype='player'): """ Locates the object related to the given @@ -85,28 +95,39 @@ def to_object(inp, objtype='player'): if typ == objtype: return obj if objtype == 'player': - if typ == 'object': return obj.player - if typ == 'string': return _PlayerDB.objects.get(user_username__iexact=obj) - if typ == 'dbref': return _PlayerDB.objects.get(id=obj) + if typ == 'object': + return obj.player + if typ == 'string': + return _PlayerDB.objects.get(user_username__iexact=obj) + if typ == 'dbref': + return _PlayerDB.objects.get(id=obj) print objtype, inp, obj, typ, type(inp) raise CommError() elif objtype == 'object': - if typ == 'player': return obj.obj - if typ == 'string': return _ObjectDB.objects.get(db_key__iexact=obj) - if typ == 'dbref': return _ObjectDB.objects.get(id=obj) + if typ == 'player': + return obj.obj + if typ == 'string': + return _ObjectDB.objects.get(db_key__iexact=obj) + if typ == 'dbref': + return _ObjectDB.objects.get(id=obj) print objtype, inp, obj, typ, type(inp) raise CommError() elif objtype == 'channel': - if typ == 'string': return _ChannelDB.objects.get(db_key__iexact=obj) - if typ == 'dbref': return _ChannelDB.objects.get(id=obj) + if typ == 'string': + return _ChannelDB.objects.get(db_key__iexact=obj) + if typ == 'dbref': + return _ChannelDB.objects.get(id=obj) print objtype, inp, obj, typ, type(inp) raise CommError() elif objtype == 'external': - if typ == 'string': return _ExternalConnection.objects.get(db_key=inp) - if typ == 'dbref': return _ExternalConnection.objects.get(id=obj) + if typ == 'string': + return _ExternalConnection.objects.get(db_key=inp) + if typ == 'dbref': + return _ExternalConnection.objects.get(id=obj) print objtype, inp, obj, typ, type(inp) raise CommError() + # # Msg manager # @@ -146,17 +167,21 @@ class MsgManager(models.Manager): def get_messages_by_sender(self, obj, exclude_channel_messages=False): """ - Get all messages sent by one entity - this could be either a player or an object + Get all messages sent by one entity - this could be either a + player or an object - only_non_channel: only return messages -not- aimed at a channel (e.g. private tells) + only_non_channel: only return messages -not- aimed at a channel + (e.g. private tells) """ obj, typ = identify_object(obj) if exclude_channel_messages: # explicitly exclude channel recipients if typ == 'player': - return list(self.filter(db_sender_players=obj, db_receivers_channels__isnull=True).exclude(db_hide_from_players=obj)) + return list(self.filter(db_sender_players=obj, + db_receivers_channels__isnull=True).exclude(db_hide_from_players=obj)) elif typ == 'object': - return list(self.filter(db_sender_objects=obj, db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj)) + return list(self.filter(db_sender_objects=obj, + db_receivers_channels__isnull=True).exclude(db_hide_from_objects=obj)) else: raise CommError else: @@ -208,9 +233,10 @@ class MsgManager(models.Manager): if msg: return msg[0] - # We use Q objects to gradually build up the query - this way we only need to do one - # database lookup at the end rather than gradually refining with multiple filter:s. - # Django Note: Q objects can be combined with & and | (=AND,OR). ~ negates the queryset + # We use Q objects to gradually build up the query - this way we only + # need to do one database lookup at the end rather than gradually + # refining with multiple filter:s. Django Note: Q objects can be + # combined with & and | (=AND,OR). ~ negates the queryset # filter by sender sender, styp = identify_object(sender) @@ -238,6 +264,7 @@ class MsgManager(models.Manager): # execute the query return list(self.filter(sender_restrict & receiver_restrict & fulltext_restrict)) + # # Channel manager # @@ -350,9 +377,12 @@ class ChannelManager(models.Manager): channels = self.filter(db_key__iexact=ostring) if not channels: # still no match. Search by alias. - channels = [channel for channel in self.all() if ostring.lower() in [a.lower for a in channel.aliases.all()]] + channels = [channel for channel in self.all() + if ostring.lower() in [a.lower + for a in channel.aliases.all()]] return channels + # # PlayerChannelConnection manager # @@ -419,6 +449,7 @@ class PlayerChannelConnectionManager(models.Manager): for conn in conns: conn.delete() + class ExternalChannelConnectionManager(models.Manager): """ This ExternalChannelConnectionManager implements methods for searching diff --git a/src/comms/models.py b/src/comms/models.py index cb8f1cc2c4..2f96d4a7ad 100644 --- a/src/comms/models.py +++ b/src/comms/models.py @@ -30,12 +30,14 @@ from src.locks.lockhandler import LockHandler from src.utils import logger from src.utils.utils import is_iter, to_str, crop, make_iter -__all__ = ("Msg", "TempMsg", "ChannelDB", "PlayerChannelConnection", "ExternalChannelConnection") +__all__ = ("Msg", "TempMsg", "ChannelDB", + "PlayerChannelConnection", "ExternalChannelConnection") _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ + #------------------------------------------------------------ # # Msg @@ -66,9 +68,9 @@ class Msg(SharedMemoryModel): # These databse fields are all set using their corresponding properties, # named same as the field, but withtout the db_* prefix. - # Sender is either a player, an object or an external sender, like an IRC channel - # normally there is only one, but if co-modification of a message is allowed, there - # may be more than one "author" + # Sender is either a player, an object or an external sender, like + # an IRC channel; normally there is only one, but if co-modification of + # a message is allowed, there may be more than one "author" db_sender_players = models.ManyToManyField("players.PlayerDB", related_name='sender_player_set', null=True, verbose_name='sender(player)', db_index=True) db_sender_objects = models.ManyToManyField("objects.ObjectDB", related_name='sender_object_set', null=True, verbose_name='sender(object)', db_index=True) db_sender_external = models.CharField('external sender', max_length=255, null=True, db_index=True, @@ -80,8 +82,8 @@ class Msg(SharedMemoryModel): db_receivers_objects = models.ManyToManyField('objects.ObjectDB', related_name='receiver_object_set', null=True, help_text="object receivers") db_receivers_channels = models.ManyToManyField("ChannelDB", related_name='channel_set', null=True, help_text="channel recievers") - # header could be used for meta-info about the message if your system needs it, or as a separate - # store for the mail subject line maybe. + # header could be used for meta-info about the message if your system needs + # it, or as a separate store for the mail subject line maybe. db_header = models.TextField('header', null=True, blank=True) # the message body itself db_message = models.TextField('messsage') @@ -124,6 +126,7 @@ class Msg(SharedMemoryModel): list(self.db_sender_players.all()) + list(self.db_sender_objects.all()) + self.extra_senders] + #@sender.setter def __senders_set(self, value): "Setter. Allows for self.sender = value" @@ -143,6 +146,7 @@ class Msg(SharedMemoryModel): else: raise ValueError(obj) self.save() + #@sender.deleter def __senders_del(self): "Deleter. Clears all senders" @@ -173,12 +177,19 @@ class Msg(SharedMemoryModel): # receivers property #@property def __receivers_get(self): - "Getter. Allows for value = self.receivers. Returns three lists of receivers: players, objects and channels." + """ + Getter. Allows for value = self.receivers. + Returns three lists of receivers: players, objects and channels. + """ return [hasattr(o, "typeclass") and o.typeclass or o for o in list(self.db_receivers_players.all()) + list(self.db_receivers_objects.all())] + #@receivers.setter def __receivers_set(self, value): - "Setter. Allows for self.receivers = value. This appends a new receiver to the message." + """ + Setter. Allows for self.receivers = value. + This appends a new receiver to the message. + """ for val in (v for v in make_iter(value) if v): obj, typ = identify_object(val) if typ == 'player': @@ -190,6 +201,7 @@ class Msg(SharedMemoryModel): else: raise ValueError self.save() + #@receivers.deleter def __receivers_del(self): "Deleter. Clears all receivers" @@ -215,11 +227,15 @@ class Msg(SharedMemoryModel): def __channels_get(self): "Getter. Allows for value = self.channels. Returns a list of channels." return self.db_receivers_channels.all() + #@channels.setter def __channels_set(self, value): - "Setter. Allows for self.channels = value. Requires a channel to be added." + """ + Setter. Allows for self.channels = value. + Requires a channel to be added.""" for val in (v.dbobj for v in make_iter(value) if v): self.db_receivers_channels.add(val) + #@channels.deleter def __channels_del(self): "Deleter. Allows for del self.channels" @@ -228,8 +244,12 @@ class Msg(SharedMemoryModel): channels = property(__channels_get, __channels_set, __channels_del) def __hide_from_get(self): - "Getter. Allows for value = self.hide_from. Returns 3 lists of players, objects and channels" + """ + Getter. Allows for value = self.hide_from. + Returns 3 lists of players, objects and channels + """ return self.db_hide_from_players.all(), self.db_hide_from_objects.all(), self.db_hide_from_channels.all() + #@hide_from_sender.setter def __hide_from_set(self, value): "Setter. Allows for self.hide_from = value. Will append to hiders" @@ -243,6 +263,7 @@ class Msg(SharedMemoryModel): else: raise ValueError self.save() + #@hide_from_sender.deleter def __hide_from_del(self): "Deleter. Allows for del self.hide_from_senders" @@ -275,7 +296,6 @@ class TempMsg(object): temporary messages that will not be stored. It mimics the "real" Msg object, but don't require sender to be given. - """ def __init__(self, senders=None, receivers=None, channels=None, message="", header="", type="", lockstring="", hide_from=None): self.senders = senders and make_iter(senders) or [] @@ -301,7 +321,7 @@ class TempMsg(object): try: self.senders.remove(o) except ValueError: - pass # nothing to remove + pass # nothing to remove def remove_receiver(self, obj): "Remove a sender or a list of senders" @@ -309,11 +329,13 @@ class TempMsg(object): try: self.senders.remove(o) except ValueError: - pass # nothing to remove + pass # nothing to remove def access(self, accessing_obj, access_type='read', default=False): "checks lock access" - return self.locks.check(accessing_obj, access_type=access_type, default=default) + return self.locks.check(accessing_obj, + access_type=access_type, default=default) + #------------------------------------------------------------ # @@ -347,7 +369,6 @@ class ChannelDB(TypedObject): _SA(self, "aliases", AliasHandler(self, category_prefix="comm_")) _SA(self, "attributes", AttributeHandler(self)) - class Meta: "Define Django meta options" verbose_name = "Channel" @@ -415,6 +436,7 @@ class ChannelDB(TypedObject): """ return self.locks.check(accessing_obj, access_type=access_type, default=default) + class PlayerChannelConnection(SharedMemoryModel): """ This connects a player object to a particular comm channel. @@ -435,11 +457,13 @@ class PlayerChannelConnection(SharedMemoryModel): def player_get(self): "Getter. Allows for value = self.player" return self.db_player + #@player.setter def player_set(self, value): "Setter. Allows for self.player = value" self.db_player = value self.save() + #@player.deleter def player_del(self): "Deleter. Allows for del self.player. Deletes connection." @@ -451,11 +475,13 @@ class PlayerChannelConnection(SharedMemoryModel): def channel_get(self): "Getter. Allows for value = self.channel" return self.db_channel.typeclass + #@channel.setter def channel_set(self, value): "Setter. Allows for self.channel = value" self.db_channel = value.dbobj self.save() + #@channel.deleter def channel_del(self): "Deleter. Allows for del self.channel. Deletes connection." @@ -507,10 +533,12 @@ class ExternalChannelConnection(SharedMemoryModel): "Getter. Allows for value = self.channel" return self.db_channel #@channel.setter + def channel_set(self, value): "Setter. Allows for self.channel = value" self.db_channel = value self.save() + #@channel.deleter def channel_del(self): "Deleter. Allows for del self.channel. Deletes connection." @@ -522,11 +550,13 @@ class ExternalChannelConnection(SharedMemoryModel): def external_key_get(self): "Getter. Allows for value = self.external_key" return self.db_external_key + #@external_key.setter def external_key_set(self, value): "Setter. Allows for self.external_key = value" self.db_external_key = value self.save() + #@external_key.deleter def external_key_del(self): "Deleter. Allows for del self.external_key. Deletes connection." @@ -538,11 +568,13 @@ class ExternalChannelConnection(SharedMemoryModel): def external_send_code_get(self): "Getter. Allows for value = self.external_send_code" return self.db_external_send_code + #@external_send_code.setter def external_send_code_set(self, value): "Setter. Allows for self.external_send_code = value" self.db_external_send_code = value self.save() + #@external_send_code.deleter def external_send_code_del(self): "Deleter. Allows for del self.external_send_code. Deletes connection." @@ -555,11 +587,13 @@ class ExternalChannelConnection(SharedMemoryModel): def external_config_get(self): "Getter. Allows for value = self.external_config" return self.db_external_config + #@external_config.setter def external_config_set(self, value): "Setter. Allows for self.external_config = value" self.db_external_config = value self.save() + #@external_config.deleter def external_config_del(self): "Deleter. Allows for del self.external_config. Deletes connection." @@ -572,11 +606,13 @@ class ExternalChannelConnection(SharedMemoryModel): def is_enabled_get(self): "Getter. Allows for value = self.is_enabled" return self.db_is_enabled + #@is_enabled.setter def is_enabled_set(self, value): "Setter. Allows for self.is_enabled = value" self.db_is_enabled = value self.save() + #@is_enabled.deleter def is_enabled_del(self): "Deleter. Allows for del self.is_enabled. Deletes connection." @@ -589,8 +625,8 @@ class ExternalChannelConnection(SharedMemoryModel): def to_channel(self, message, *args, **kwargs): "Send external -> channel" - if 'from_obj' in kwargs and kwargs.pop('from_obj'): - from_obj = self.external_key + #if 'from_obj' in kwargs and kwargs.pop('from_obj'): + # from_obj = self.external_key self.channel.msg(message, senders=[self], *args, **kwargs) def to_external(self, message, senders=None, from_channel=None): @@ -599,12 +635,13 @@ class ExternalChannelConnection(SharedMemoryModel): # make sure we are not echoing back our own message to ourselves # (this would result in a nasty infinite loop) #print senders - if self in make_iter(senders):#.external_key: + if self in make_iter(senders): #.external_key: return try: # we execute the code snippet that should make it possible for the - # connection to contact the protocol correctly (as set by the protocol). + # connection to contact the protocol correctly (as set by the + # protocol). # Note that the code block has access to the variables here, such # as message, from_obj and from_channel. exec(to_str(self.external_send_code)) diff --git a/src/comms/rss.py b/src/comms/rss.py index 0852368fc7..212c800185 100644 --- a/src/comms/rss.py +++ b/src/comms/rss.py @@ -21,6 +21,7 @@ RETAG = re.compile(r'<[^>]*?>') # holds rss readers they can be shut down at will. RSS_READERS = {} + def msg_info(message): """ Send info to default info channel @@ -37,6 +38,7 @@ if RSS_ENABLED: except ImportError: raise ImportError("RSS requires python-feedparser to be installed. Install or set RSS_ENABLED=False.") + class RSSReader(object): """ Reader script used to connect to each individual RSS feed @@ -50,7 +52,7 @@ class RSSReader(object): self.key = key self.url = url self.interval = interval - self.entries = {} # stored feeds + self.entries = {} # stored feeds self.task = None # first we do is to load the feed so we don't resend # old entries whenever the reader starts. @@ -63,7 +65,8 @@ class RSSReader(object): feed = feedparser.parse(self.url) new = [] for entry in (e for e in feed['entries'] if e['id'] not in self.entries): - txt = "[RSS] %s: %s" % (RETAG.sub("", entry['title']), entry['link'].replace('\n','').encode('utf-8')) + txt = "[RSS] %s: %s" % (RETAG.sub("", entry['title']), + entry['link'].replace('\n','').encode('utf-8')) self.entries[entry['id']] = txt new.append(txt) return new @@ -92,12 +95,14 @@ class RSSReader(object): self.task.start(self.interval, now=False) RSS_READERS[self.key] = self + def build_connection_key(channel, url): "This is used to id the connection" if hasattr(channel, 'key'): channel = channel.key return "rss_%s>%s" % (url, channel) + def create_connection(channel, url, interval): """ This will create a new RSS->channel connection @@ -113,13 +118,17 @@ def create_connection(channel, url, interval): if old_conns: return False config = "%s|%i" % (url, interval) - # There is no sendback from evennia to the rss, so we need not define any sendback code. - conn = ExternalChannelConnection(db_channel=channel, db_external_key=key, db_external_config=config) + # There is no sendback from evennia to the rss, so we need not define + # any sendback code. + conn = ExternalChannelConnection(db_channel=channel, + db_external_key=key, + db_external_config=config) conn.save() connect_to_rss(conn) return True + def delete_connection(channel, url): """ Delete rss connection between channel and url @@ -135,6 +144,7 @@ def delete_connection(channel, url): reader.task.stop() return True + def connect_to_rss(connection): """ Create the parser instance and connect to RSS feed and channel @@ -145,6 +155,7 @@ def connect_to_rss(connection): # Create reader (this starts the running task and stores a reference in RSS_TASKS) RSSReader(key, url, int(interval)) + def connect_all(): """ Activate all rss feed parsers diff --git a/src/help/__init__.py b/src/help/__init__.py index 95dd700232..e1f630d05d 100644 --- a/src/help/__init__.py +++ b/src/help/__init__.py @@ -1,11 +1,11 @@ """ -Makes it easier to import by grouping all relevant things already at this level. +Makes it easier to import by grouping all relevant things already at this level. You can henceforth import most things directly from src.help Also, the initiated object manager is available as src.help.manager. """ -from src.help.models import * +from src.help.models import * manager = HelpEntry.objects diff --git a/src/help/admin.py b/src/help/admin.py index 8194f96a6b..20fe50ef46 100644 --- a/src/help/admin.py +++ b/src/help/admin.py @@ -1,36 +1,37 @@ -""" -This defines how to edit help entries in Admin. -""" -from django import forms -from django.contrib import admin -from src.help.models import HelpEntry - - - -class HelpEntryForm(forms.ModelForm): - "Defines how to display the help entry" - class Meta: - model = HelpEntry - db_help_category = forms.CharField(label="Help category", initial='General', - help_text="organizes help entries in lists") - db_lock_storage = forms.CharField(label="Locks", initial='view:all()',required=False, - widget=forms.TextInput(attrs={'size':'40'}),) - -class HelpEntryAdmin(admin.ModelAdmin): - "Sets up the admin manaager for help entries" - - list_display = ('id', 'db_key', 'db_help_category', 'db_lock_storage') - list_display_links = ('id', 'db_key') - search_fields = ['^db_key', 'db_entrytext'] - ordering = ['db_help_category', 'db_key'] - save_as = True - save_on_top = True - list_select_related = True - - form = HelpEntryForm - fieldsets = ( - (None, {'fields':(('db_key', 'db_help_category'), 'db_entrytext', 'db_lock_storage'), - 'description':"Sets a Help entry. Set lock to view:all() unless you want to restrict it."}),) - - -admin.site.register(HelpEntry, HelpEntryAdmin) +""" +This defines how to edit help entries in Admin. +""" +from django import forms +from django.contrib import admin +from src.help.models import HelpEntry + + + +class HelpEntryForm(forms.ModelForm): + "Defines how to display the help entry" + class Meta: + model = HelpEntry + db_help_category = forms.CharField(label="Help category", initial='General', + help_text="organizes help entries in lists") + db_lock_storage = forms.CharField(label="Locks", initial='view:all()',required=False, + widget=forms.TextInput(attrs={'size':'40'}),) + +class HelpEntryAdmin(admin.ModelAdmin): + "Sets up the admin manaager for help entries" + + list_display = ('id', 'db_key', 'db_help_category', 'db_lock_storage') + list_display_links = ('id', 'db_key') + search_fields = ['^db_key', 'db_entrytext'] + ordering = ['db_help_category', 'db_key'] + save_as = True + save_on_top = True + list_select_related = True + + form = HelpEntryForm + fieldsets = ( + (None, {'fields':(('db_key', 'db_help_category'), + 'db_entrytext', 'db_lock_storage'), + 'description':"Sets a Help entry. Set lock to view:all() unless you want to restrict it."}),) + + +admin.site.register(HelpEntry, HelpEntryAdmin) diff --git a/src/help/manager.py b/src/help/manager.py index 61bf5a1c68..5233285fac 100644 --- a/src/help/manager.py +++ b/src/help/manager.py @@ -5,6 +5,7 @@ from django.db import models from src.utils import logger, utils __all__ = ("HelpEntryManager",) + class HelpEntryManager(models.Manager): """ This HelpEntryManager implements methods for searching @@ -48,7 +49,7 @@ class HelpEntryManager(models.Manager): Do a fuzzy match, preferably within the category of the current topic. """ - return self.filter(db_key__icontains=topicstring).exclude(db_key__iexact=topicstring) + return self.filter(db_key__icontains=topicstr).exclude(db_key__iexact=topicstr) def find_topics_with_category(self, help_category): """ @@ -92,6 +93,7 @@ class HelpEntryManager(models.Manager): """ ostring = ostring.strip().lower() if help_category: - return self.filter(db_key__iexact=ostring, db_help_category__iexact=help_category) + return self.filter(db_key__iexact=ostring, + db_help_category__iexact=help_category) else: return self.filter(db_key__iexact=ostring) diff --git a/src/help/models.py b/src/help/models.py index 1c3880b536..287b06e7f3 100644 --- a/src/help/models.py +++ b/src/help/models.py @@ -12,12 +12,11 @@ game world, policy info, rules and similar. from django.db import models from src.utils.idmapper.models import SharedMemoryModel from src.help.manager import HelpEntryManager -from src.utils import ansi from src.typeclasses.models import Tag, TagHandler from src.locks.lockhandler import LockHandler -from src.utils.utils import is_iter __all__ = ("HelpEntry",) + #------------------------------------------------------------ # # HelpEntry diff --git a/src/locks/__init__.py b/src/locks/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/src/locks/__init__.py +++ b/src/locks/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/locks/lockfuncs.py b/src/locks/lockfuncs.py index 832537bf73..00ec8ea526 100644 --- a/src/locks/lockfuncs.py +++ b/src/locks/lockfuncs.py @@ -86,6 +86,7 @@ from src.utils import utils _PERMISSION_HIERARCHY = [p.lower() for p in settings.PERMISSION_HIERARCHY] + def _to_player(accessing_obj): "Helper function. Makes sure an accessing object is a player object" if utils.inherits_from(accessing_obj, "src.objects.objects.Object"): @@ -99,14 +100,21 @@ def _to_player(accessing_obj): def true(*args, **kwargs): "Always returns True." return True + + def all(*args, **kwargs): return True + + def false(*args, **kwargs): "Always returns False" return False + + def none(*args, **kwargs): return False + def self(accessing_obj, accessed_obj, *args, **kwargs): """ Check if accessing_obj is the same as accessed_obj @@ -172,7 +180,8 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs): else: return hpos_target <= hpos_player elif not is_quell and perm in perms_player: - # if we get here, check player perms first, otherwise continue as normal + # if we get here, check player perms first, otherwise + # continue as normal return True if perm in perms_object: @@ -185,6 +194,7 @@ def perm(accessing_obj, accessed_obj, *args, **kwargs): if hperm in perms_object and hpos_target < hpos) return False + def perm_above(accessing_obj, accessed_obj, *args, **kwargs): """ Only allow objects with a permission *higher* in the permission @@ -193,7 +203,8 @@ def perm_above(accessing_obj, accessed_obj, *args, **kwargs): this function has no meaning and returns False. """ kwargs["_greater_than"] = True - return perm(accessing_obj,accessed_obj, *args, **kwargs) + return perm(accessing_obj, accessed_obj, *args, **kwargs) + def pperm(accessing_obj, accessed_obj, *args, **kwargs): """ @@ -209,6 +220,7 @@ def pperm(accessing_obj, accessed_obj, *args, **kwargs): """ return perm(_to_player(accessing_obj), accessed_obj, *args, **kwargs) + def pperm_above(accessing_obj, accessed_obj, *args, **kwargs): """ Only allow Player objects with a permission *higher* in the permission @@ -218,6 +230,7 @@ def pperm_above(accessing_obj, accessed_obj, *args, **kwargs): """ return perm_above(_to_player(accessing_obj), accessed_obj, *args, **kwargs) + def dbref(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -238,16 +251,19 @@ def dbref(accessing_obj, accessed_obj, *args, **kwargs): return dbref == accessing_obj.dbid return False + def pdbref(accessing_obj, accessed_obj, *args, **kwargs): """ Same as dbref, but making sure accessing_obj is a player. """ return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs) + def id(accessing_obj, accessed_obj, *args, **kwargs): "Alias to dbref" return dbref(accessing_obj, accessed_obj, *args, **kwargs) + def pid(accessing_obj, accessed_obj, *args, **kwargs): "Alias to dbref, for Players" return dbref(_to_player(accessing_obj), accessed_obj, *args, **kwargs) @@ -262,6 +278,7 @@ CF_MAPPING = {'eq': lambda val1, val2: val1 == val2 or int(val1) == int(val2), 'ne': lambda val1, val2: int(val1) != int(val2), 'default': lambda val1, val2: False} + def attr(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -297,22 +314,26 @@ def attr(accessing_obj, accessed_obj, *args, **kwargs): try: return CF_MAPPING.get(typ, 'default')(val1, val2) except Exception: - # this might happen if we try to compare two things that cannot be compared + # this might happen if we try to compare two things + # that cannot be compared return False # first, look for normal properties on the object trying to gain access if hasattr(accessing_obj, attrname): if value: return valcompare(str(getattr(accessing_obj, attrname)), value, compare) - return bool(getattr(accessing_obj, attrname)) # will return Fail on False value etc + # will return Fail on False value etc + return bool(getattr(accessing_obj, attrname)) # check attributes, if they exist if (hasattr(accessing_obj, 'attributes') and accessing_obj.attributes.has(attrname)): if value: return (hasattr(accessing_obj, 'attributes') and valcompare(accessing_obj.attributes.get(attrname), value, compare)) - return bool(accessing_obj.attributes.get(attrname)) # fails on False/None values + # fails on False/None values + return bool(accessing_obj.attributes.get(attrname)) return False + def objattr(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -328,6 +349,7 @@ def objattr(accessing_obj, accessed_obj, *args, **kwargs): if hasattr(accessing_obj, "obj"): return attr(accessing_obj.obj, accessed_obj, *args, **kwargs) + def locattr(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -350,6 +372,7 @@ def attr_eq(accessing_obj, accessed_obj, *args, **kwargs): """ return attr(accessing_obj, accessed_obj, *args, **kwargs) + def attr_gt(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -357,7 +380,9 @@ def attr_gt(accessing_obj, accessed_obj, *args, **kwargs): Only true if access_obj's attribute > the value given. """ - return attr(accessing_obj, accessed_obj, *args, **{'compare':'gt'}) + return attr(accessing_obj, accessed_obj, *args, **{'compare': 'gt'}) + + def attr_ge(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -365,7 +390,9 @@ def attr_ge(accessing_obj, accessed_obj, *args, **kwargs): Only true if access_obj's attribute >= the value given. """ - return attr(accessing_obj, accessed_obj, *args, **{'compare':'ge'}) + return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ge'}) + + def attr_lt(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -373,7 +400,9 @@ def attr_lt(accessing_obj, accessed_obj, *args, **kwargs): Only true if access_obj's attribute < the value given. """ - return attr(accessing_obj, accessed_obj, *args, **{'compare':'lt'}) + return attr(accessing_obj, accessed_obj, *args, **{'compare': 'lt'}) + + def attr_le(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -381,7 +410,9 @@ def attr_le(accessing_obj, accessed_obj, *args, **kwargs): Only true if access_obj's attribute <= the value given. """ - return attr(accessing_obj, accessed_obj, *args, **{'compare':'le'}) + return attr(accessing_obj, accessed_obj, *args, **{'compare': 'le'}) + + def attr_ne(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -389,18 +420,22 @@ def attr_ne(accessing_obj, accessed_obj, *args, **kwargs): Only true if access_obj's attribute != the value given. """ - return attr(accessing_obj, accessed_obj, *args, **{'compare':'ne'}) + return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ne'}) + def holds(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: - holds() # checks if accessed_obj or accessed_obj.obj is held by accessing_obj - holds(key/dbref) # checks if accessing_obj holds an object with given key/dbref - holds(attrname, value) # checks if accessing_obj holds an object with the given attrname and value + holds() checks if accessed_obj or accessed_obj.obj + is held by accessing_obj + holds(key/dbref) checks if accessing_obj holds an object + with given key/dbref + holds(attrname, value) checks if accessing_obj holds an + object with the given attrname and value This is passed if accessed_obj is carried by accessing_obj (that is, - accessed_obj.location == accessing_obj), or if accessing_obj itself holds an - object matching the given key. + accessed_obj.location == accessing_obj), or if accessing_obj itself holds + an object matching the given key. """ try: # commands and scripts don't have contents, so we are usually looking @@ -412,6 +447,7 @@ def holds(accessing_obj, accessed_obj, *args, **kwargs): contents = accessing_obj.obj.contents except AttributeError: return False + def check_holds(objid): # helper function. Compares both dbrefs and keys/aliases. objid = str(objid) @@ -449,9 +485,11 @@ def superuser(*args, **kwargs): """ return False + def serversetting(accessing_obj, accessed_obj, *args, **kwargs): """ - Only returns true if the Evennia settings exists, alternatively has a certain value. + Only returns true if the Evennia settings exists, alternatively has + a certain value. Usage: serversetting(IRC_ENABLED) diff --git a/src/locks/lockhandler.py b/src/locks/lockhandler.py index 2119d8ff3f..b2c9da731b 100644 --- a/src/locks/lockhandler.py +++ b/src/locks/lockhandler.py @@ -67,14 +67,14 @@ Here, the lock-function perm() will be called with the string 'Builders' (accessing_obj and accessed_obj are added automatically, you only need to add the args/kwargs, if any). -If we wanted to make sure the accessing object was BOTH a Builders and a GoodGuy, we -could use AND: +If we wanted to make sure the accessing object was BOTH a Builders and a +GoodGuy, we could use AND: 'edit:perm(Builders) AND perm(GoodGuy)' -To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just one example, -the lock function can do anything and compare any properties of the calling object to -decide if the lock is passed or not. +To allow EITHER Builders and GoodGuys, we replace AND with OR. perm() is just +one example, the lock function can do anything and compare any properties of +the calling object to decide if the lock is passed or not. 'lift:attrib(very_strong) AND NOT attrib(bad_back)' @@ -89,7 +89,8 @@ object would do something like this: if not target_obj.lockhandler.has_perm(caller, 'edit'): caller.msg("Sorry, you cannot edit that.") -All objects also has a shortcut called 'access' that is recommended to use instead: +All objects also has a shortcut called 'access' that is recommended to +use instead: if not target_obj.access(caller, 'edit'): caller.msg("Sorry, you cannot edit that.") @@ -104,13 +105,15 @@ to any other identifier you can use. """ -import re, inspect +import re +import inspect from django.conf import settings from src.utils import logger, utils from django.utils.translation import ugettext as _ __all__ = ("LockHandler", "LockException") + # # Exception class # @@ -119,6 +122,7 @@ class LockException(Exception): "raised during an error in a lock." pass + # # Cached lock functions # @@ -186,15 +190,16 @@ class LockHandler(object): """ Helper function. This is normally only called when the lockstring is cached and does preliminary checking. locks are - stored as a string 'atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype... + stored as a string + 'atype:[NOT] lock()[[ AND|OR [NOT] lock()[...]];atype... """ locks = {} if not storage_lockstring: return locks duplicates = 0 - elist = [] # errors - wlist = [] # warnings + elist = [] # errors + wlist = [] # warnings for raw_lockstring in storage_lockstring.split(';'): lock_funcs = [] try: @@ -234,7 +239,8 @@ class LockHandler(object): {"access_type":access_type, "source":locks[access_type][2], "goal":raw_lockstring})) locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring) if wlist and self.log_obj: - # a warning text was set, it's not an error, so only report if log_obj is available. + # a warning text was set, it's not an error, so only report + # if log_obj is available. self._log_error("\n".join(wlist)) if elist: # an error text was set, raise exception. @@ -252,10 +258,12 @@ class LockHandler(object): def cache_lock_bypass(self, obj): """ - We cache superuser bypass checks here for efficiency. This needs to be re-run when a player is assigned to a character. - We need to grant access to superusers. We need to check both directly on the object (players), through obj.player and using the - get_player method (this sits on serversessions, in some rare cases where a check is done - before the login process has yet been fully finalized) + We cache superuser bypass checks here for efficiency. This needs to + be re-run when a player is assigned to a character. + We need to grant access to superusers. We need to check both directly + on the object (players), through obj.player and using the get_player() + method (this sits on serversessions, in some rare cases where a + check is done before the login process has yet been fully finalized) """ self.lock_bypass = hasattr(obj, "is_superuser") and obj.is_superuser @@ -308,7 +316,7 @@ class LockHandler(object): def get(self, access_type=None): "get the full lockstring or the lockstring of a particular access type." if access_type: - return self.locks.get(access_type, ["","",""])[2] + return self.locks.get(access_type, ["", "", ""])[2] return str(self) def delete(self, access_type): @@ -342,7 +350,7 @@ class LockHandler(object): access_type - the type of access wanted default - if no suitable lock type is found, use this no_superuser_bypass - don't use this unless you really, really need to, - it makes supersusers susceptible to the lock check. + it makes supersusers susceptible to the lock check. A lock is executed in the follwoing way: @@ -403,9 +411,11 @@ class LockHandler(object): locks = self._parse_lockstring(lockstring) for access_type in locks: evalstring, func_tup, raw_string = locks[access_type] - true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1], **tup[2]) for tup in func_tup) + true_false = tuple(tup[0](accessing_obj, self.obj, *tup[1],**tup[2]) + for tup in func_tup) return eval(evalstring % true_false) + def _test(): # testing diff --git a/src/locks/tests.py b/src/locks/tests.py index cf60894222..1247598b3f 100644 --- a/src/locks/tests.py +++ b/src/locks/tests.py @@ -15,7 +15,7 @@ except ImportError: from django.test import TestCase from django.conf import settings -from src.locks import lockhandler, lockfuncs +from src.locks import lockfuncs from src.utils import create #------------------------------------------------------------ diff --git a/src/objects/admin.py b/src/objects/admin.py index fa096539ba..3d4adeb6a1 100644 --- a/src/objects/admin.py +++ b/src/objects/admin.py @@ -1,140 +1,138 @@ -# -# This sets up how models are displayed -# in the web admin interface. -# - -from django import forms -from django.conf import settings -from django.contrib import admin -from src.typeclasses.models import Attribute, Tag -from src.objects.models import ObjectDB - -class AttributeInline(admin.TabularInline): - # This class is currently not used, because PickleField objects are not editable. - # It's here for us to ponder making a way that allows them to be edited. - model = Attribute - fields = ('db_key', 'db_value') - extra = 0 - -class TagInline(admin.TabularInline): - model = ObjectDB.db_tags.through - raw_id_fields = ('tag',) - extra = 0 - -class TagAdmin(admin.ModelAdmin): - fields = ('db_key', 'db_category', 'db_data') - -class ObjectCreateForm(forms.ModelForm): - "This form details the look of the fields" - class Meta: - model = ObjectDB - db_key = forms.CharField(label="Name/Key", - widget=forms.TextInput(attrs={'size':'78'}), - help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. If creating a Character, check so the name is unique among characters!",) - db_typeclass_path = forms.CharField(label="Typeclass", - initial=settings.BASE_OBJECT_TYPECLASS, - widget=forms.TextInput(attrs={'size':'78'}), - help_text="This defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. If you are creating a Character you should use the typeclass defined by settings.BASE_CHARACTER_TYPECLASS or one derived from that.") - #db_permissions = forms.CharField(label="Permissions", - # initial=settings.PERMISSION_PLAYER_DEFAULT, - # required=False, - # widget=forms.TextInput(attrs={'size':'78'}), - # help_text="a comma-separated list of text strings checked by certain locks. They are mainly of use for Character objects. Character permissions overload permissions defined on a controlling Player. Most objects normally don't have any permissions defined.") - db_cmdset_storage = forms.CharField(label="CmdSet", - initial="", - required=False, - widget=forms.TextInput(attrs={'size':'78'}), - help_text="Most non-character objects don't need a cmdset and can leave this field blank.") - raw_id_fields = ('db_destination', 'db_location', 'db_home') - - - -class ObjectEditForm(ObjectCreateForm): - "Form used for editing. Extends the create one with more fields" - - db_lock_storage = forms.CharField(label="Locks", - required=False, - widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}), - help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form type:lockfunction(args);type2:lockfunction2(args);...") - - -class ObjectDBAdmin(admin.ModelAdmin): - - list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path') - list_display_links = ('id', 'db_key') - ordering = ['db_player', 'db_typeclass_path', 'id'] - search_fields = ['^db_key', 'db_typeclass_path'] - raw_id_fields = ('db_destination', 'db_location', 'db_home') - - save_as = True - save_on_top = True - list_select_related = True - list_filter = ('db_typeclass_path',) - #list_filter = ('db_permissions', 'db_typeclass_path') - - # editing fields setup - - form = ObjectEditForm - fieldsets = ( - (None, { - 'fields': (('db_key','db_typeclass_path'), ('db_lock_storage', ), - ('db_location', 'db_home'), 'db_destination','db_cmdset_storage' - )}), - ) - #fieldsets = ( - # (None, { - # 'fields': (('db_key','db_typeclass_path'), ('db_permissions', 'db_lock_storage'), - # ('db_location', 'db_home'), 'db_destination','db_cmdset_storage' - # )}), - # ) - - #deactivated temporarily, they cause empty objects to be created in admin - inlines = [TagInline] - - - # Custom modification to give two different forms wether adding or not. - - add_form = ObjectCreateForm - add_fieldsets = ( - (None, { - 'fields': (('db_key','db_typeclass_path'), - ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage' - )}), - ) - #add_fieldsets = ( - # (None, { - # 'fields': (('db_key','db_typeclass_path'), 'db_permissions', - # ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage' - # )}), - # ) - def get_fieldsets(self, request, obj=None): - if not obj: - return self.add_fieldsets - return super(ObjectDBAdmin, self).get_fieldsets(request, obj) - - def get_form(self, request, obj=None, **kwargs): - """ - Use special form during creation - """ - defaults = {} - if obj is None: - defaults.update({ - 'form': self.add_form, - 'fields': admin.util.flatten_fieldsets(self.add_fieldsets), - }) - defaults.update(kwargs) - return super(ObjectDBAdmin, self).get_form(request, obj, **defaults) - - def save_model(self, request, obj, form, change): - obj.save() - if not change: - # adding a new object - obj = obj.typeclass - obj.basetype_setup() - obj.basetype_posthook_setup() - obj.at_object_creation() - obj.at_init() - - -admin.site.register(ObjectDB, ObjectDBAdmin) -admin.site.register(Tag, TagAdmin) +# +# This sets up how models are displayed +# in the web admin interface. +# + +from django import forms +from django.conf import settings +from django.contrib import admin +from src.typeclasses.models import Attribute, Tag +from src.objects.models import ObjectDB + + +class AttributeInline(admin.TabularInline): + # This class is currently not used, because PickleField objects are + # not editable. It's here for us to ponder making a way that allows + # them to be edited. + model = Attribute + fields = ('db_key', 'db_value') + extra = 0 + + +class TagInline(admin.TabularInline): + model = ObjectDB.db_tags.through + raw_id_fields = ('tag',) + extra = 0 + + +class TagAdmin(admin.ModelAdmin): + fields = ('db_key', 'db_category', 'db_data') + + +class ObjectCreateForm(forms.ModelForm): + "This form details the look of the fields" + class Meta: + model = ObjectDB + db_key = forms.CharField(label="Name/Key", + widget=forms.TextInput(attrs={'size': '78'}), + help_text="Main identifier, like 'apple', 'strong guy', 'Elizabeth' etc. If creating a Character, check so the name is unique among characters!",) + db_typeclass_path = forms.CharField(label="Typeclass", + initial=settings.BASE_OBJECT_TYPECLASS, + widget=forms.TextInput(attrs={'size': '78'}), + help_text="This defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass. If you are creating a Character you should use the typeclass defined by settings.BASE_CHARACTER_TYPECLASS or one derived from that.") + db_cmdset_storage = forms.CharField(label="CmdSet", + initial="", + required=False, + widget=forms.TextInput(attrs={'size': '78'}), + help_text="Most non-character objects don't need a cmdset and can leave this field blank.") + raw_id_fields = ('db_destination', 'db_location', 'db_home') + + +class ObjectEditForm(ObjectCreateForm): + "Form used for editing. Extends the create one with more fields" + + db_lock_storage = forms.CharField(label="Locks", + required=False, + widget=forms.Textarea(attrs={'cols':'100', 'rows':'2'}), + help_text="In-game lock definition string. If not given, defaults will be used. This string should be on the form type:lockfunction(args);type2:lockfunction2(args);...") + + +class ObjectDBAdmin(admin.ModelAdmin): + + list_display = ('id', 'db_key', 'db_player', 'db_typeclass_path') + list_display_links = ('id', 'db_key') + ordering = ['db_player', 'db_typeclass_path', 'id'] + search_fields = ['^db_key', 'db_typeclass_path'] + raw_id_fields = ('db_destination', 'db_location', 'db_home') + + save_as = True + save_on_top = True + list_select_related = True + list_filter = ('db_typeclass_path',) + #list_filter = ('db_permissions', 'db_typeclass_path') + + # editing fields setup + + form = ObjectEditForm + fieldsets = ( + (None, { + 'fields': (('db_key','db_typeclass_path'), ('db_lock_storage', ), + ('db_location', 'db_home'), 'db_destination','db_cmdset_storage' + )}), + ) + #fieldsets = ( + # (None, { + # 'fields': (('db_key','db_typeclass_path'), ('db_permissions', 'db_lock_storage'), + # ('db_location', 'db_home'), 'db_destination','db_cmdset_storage' + # )}), + # ) + + #deactivated temporarily, they cause empty objects to be created in admin + inlines = [TagInline] + + # Custom modification to give two different forms wether adding or not. + add_form = ObjectCreateForm + add_fieldsets = ( + (None, { + 'fields': (('db_key','db_typeclass_path'), + ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage' + )}), + ) + + #add_fieldsets = ( + # (None, { + # 'fields': (('db_key','db_typeclass_path'), 'db_permissions', + # ('db_location', 'db_home'), 'db_destination', 'db_cmdset_storage' + # )}), + # ) + def get_fieldsets(self, request, obj=None): + if not obj: + return self.add_fieldsets + return super(ObjectDBAdmin, self).get_fieldsets(request, obj) + + def get_form(self, request, obj=None, **kwargs): + """ + Use special form during creation + """ + defaults = {} + if obj is None: + defaults.update({ + 'form': self.add_form, + 'fields': admin.util.flatten_fieldsets(self.add_fieldsets), + }) + defaults.update(kwargs) + return super(ObjectDBAdmin, self).get_form(request, obj, **defaults) + + def save_model(self, request, obj, form, change): + obj.save() + if not change: + # adding a new object + obj = obj.typeclass + obj.basetype_setup() + obj.basetype_posthook_setup() + obj.at_object_creation() + obj.at_init() + + +admin.site.register(ObjectDB, ObjectDBAdmin) +admin.site.register(Tag, TagAdmin) diff --git a/src/objects/manager.py b/src/objects/manager.py index 3bce33c878..e98c0c9812 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -21,6 +21,7 @@ _ATTR = None _AT_MULTIMATCH_INPUT = utils.variable_from_module(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) + class ObjectManager(TypedObjectManager): """ This ObjectManager implementes methods for searching @@ -43,7 +44,8 @@ class ObjectManager(TypedObjectManager): get_objs_with_db_property_match get_objs_with_key_or_alias get_contents - object_search (interface to many of the above methods, equivalent to ev.search_object) + object_search (interface to many of the above methods, + equivalent to ev.search_object) copy_object """ @@ -93,8 +95,8 @@ class ObjectManager(TypedObjectManager): @returns_typeclass_list def get_objs_with_attr(self, attribute_name, candidates=None): """ - Returns all objects having the given attribute_name defined at all. Location - should be a valid location object. + Returns all objects having the given attribute_name defined at all. + Location should be a valid location object. """ cand_restriction = candidates != None and Q(objattribute__db_obj__pk__in=[_GA(obj, "id") for obj in make_iter(candidates) if obj]) or Q() return list(self.filter(cand_restriction & Q(objattribute__db_key=attribute_name))) @@ -178,10 +180,11 @@ class ObjectManager(TypedObjectManager): return self.filter(db_location=location).exclude(exclude_restriction) @returns_typeclass_list - def get_objs_with_key_or_alias(self, ostring, exact=True, candidates=None, typeclasses=None): + def get_objs_with_key_or_alias(self, ostring, exact=True, + candidates=None, typeclasses=None): """ - Returns objects based on key or alias match. Will also do fuzzy matching based on - the utils.string_partial_matching function. + Returns objects based on key or alias match. Will also do fuzzy + matching based on the utils.string_partial_matching function. candidates - list of candidate objects to restrict on typeclasses - list of typeclass path strings to restrict on """ @@ -191,7 +194,8 @@ class ObjectManager(TypedObjectManager): else: return [] if is_iter(candidates) and not len(candidates): - # if candidates is an empty iterable there can be no matches. Exit early. + # if candidates is an empty iterable there can be no matches + # Exit early. return [] # build query objects @@ -231,33 +235,42 @@ class ObjectManager(TypedObjectManager): candidates=None, exact=True): """ - Search as an object globally or in a list of candidates and return results. The result is always an Object. - Always returns a list. + Search as an object globally or in a list of candidates and return + results. The result is always an Object. Always returns a list. Arguments: - searchdata: (str or obj) The entity to match for. This is usually a key string but may also be an object itself. - By default (if not attribute_name is set), this will search object.key and object.aliases in order. Can also - be on the form #dbref, which will, if exact=True be matched against primary key. - attribute_name: (str): Use this named ObjectAttribute to match searchdata against, instead - of the defaults. If this is the name of a database field (with or without the db_ prefix), that - will be matched too. - typeclass (str or TypeClass): restrict matches to objects having this typeclass. This will help - speed up global searches. - candidates (list obj ObjectDBs): If supplied, search will only be performed among the candidates - in this list. A common list of candidates is the contents of the current location searched. - exact (bool): Match names/aliases exactly or partially. Partial matching matches the - beginning of words in the names/aliases, using a matching routine to separate - multiple matches in names with multiple components (so "bi sw" will match - "Big sword"). Since this is more expensive than exact matching, it is - recommended to be used together with the objlist keyword to limit the number - of possibilities. This value has no meaning if searching for attributes/properties. + searchdata: (str or obj) The entity to match for. This is usually a + key string but may also be an object itself. By default (if + not attribute_name is set), this will search object.key and + object.aliases in order. Can also be on the form #dbref, + which will, if exact=True be matched against primary key. + attribute_name: (str): Use this named ObjectAttribute to match + searchdata against, instead of the defaults. If this is + the name of a database field (with or without the db_ prefix), + that will be matched too. + typeclass (str or TypeClass): restrict matches to objects having this + typeclass. This will help speed up global searches. + candidates (list obj ObjectDBs): If supplied, search will only be + performed among the candidates in this list. A common list + of candidates is the contents of the current location + searched. + exact (bool): Match names/aliases exactly or partially. Partial + matching matches the beginning of words in the names/aliases, + using a matching routine to separate multiple matches in + names with multiple components (so "bi sw" will match + "Big sword"). Since this is more expensive than exact + matching, it is recommended to be used together with the + objlist keyword to limit the number of possibilities. This + value has no meaning if searching for attributes/properties. Returns: A list of matching objects (or a list with one unique match) - """ def _searcher(searchdata, candidates, typeclass, exact=False): - "Helper method for searching objects. typeclass is only used for global searching (no candidates)" + """ + Helper method for searching objects. typeclass is only used + for global searching (no candidates) + """ if attribute_name: # attribute/property search (always exact). matches = self.get_objs_with_db_property_value(attribute_name, searchdata, candidates=candidates, typeclasses=typeclass) @@ -285,10 +298,11 @@ class ObjectManager(TypedObjectManager): # Convenience check to make sure candidates are really dbobjs candidates = [cand.dbobj for cand in make_iter(candidates) if cand] if typeclass: - candidates = [cand for cand in candidates if _GA(cand, "db_typeclass_path") in typeclass] + candidates = [cand for cand in candidates + if _GA(cand, "db_typeclass_path") in typeclass] dbref = not attribute_name and exact and self.dbref(searchdata) - if dbref != None: + if dbref is not None: # Easiest case - dbref matching (always exact) dbref_match = self.dbref_search(dbref) if dbref_match: @@ -299,17 +313,19 @@ class ObjectManager(TypedObjectManager): # Search through all possibilities. match_number = None - # always run first check exact - we don't want partial matches if on the form of 1-keyword etc. + # always run first check exact - we don't want partial matches + # if on the form of 1-keyword etc. matches = _searcher(searchdata, candidates, typeclass, exact=True) if not matches: - # no matches found - check if we are dealing with N-keyword query - if so, strip it. + # no matches found - check if we are dealing with N-keyword + # query - if so, strip it. match_number, searchdata = _AT_MULTIMATCH_INPUT(searchdata) # run search again, with the exactness set by call - if match_number != None or not exact: + if match_number is not None or not exact: matches = _searcher(searchdata, candidates, typeclass, exact=exact) # deal with result - if len(matches) > 1 and match_number != None: + if len(matches) > 1 and match_number is not None: # multiple matches, but a number was given to separate them try: matches = [matches[match_number]] @@ -324,11 +340,12 @@ class ObjectManager(TypedObjectManager): def copy_object(self, original_object, new_key=None, new_location=None, new_home=None, - new_permissions=None, new_locks=None, new_aliases=None, new_destination=None): + new_permissions=None, new_locks=None, + new_aliases=None, new_destination=None): """ - Create and return a new object as a copy of the original object. All will - be identical to the original except for the arguments given specifically - to this method. + Create and return a new object as a copy of the original object. All + will be identical to the original except for the arguments given + specifically to this method. original_object (obj) - the object to make a copy from new_key (str) - name the copy differently from the original. @@ -358,9 +375,14 @@ class ObjectManager(TypedObjectManager): # create new object from src.utils import create from src.scripts.models import ScriptDB - new_object = create.create_object(typeclass_path, key=new_key, location=new_location, - home=new_home, permissions=new_permissions, - locks=new_locks, aliases=new_aliases, destination=new_destination) + new_object = create.create_object(typeclass_path, + key=new_key, + location=new_location, + home=new_home, + permissions=new_permissions, + locks=new_locks, + aliases=new_aliases, + destination=new_destination) if not new_object: return None @@ -381,7 +403,6 @@ class ObjectManager(TypedObjectManager): return new_object - def clear_all_sessids(self): """ Clear the db_sessid field of all objects having also the db_player field diff --git a/src/objects/models.py b/src/objects/models.py index 2ce82accc4..e9cdfb17a2 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -18,17 +18,15 @@ import traceback from django.db import models from django.conf import settings -from src.typeclasses.models import TypedObject, TagHandler, NickHandler, AliasHandler, AttributeHandler -from src.server.caches import get_prop_cache, set_prop_cache - -from src.typeclasses.typeclass import TypeClass +from src.typeclasses.models import (TypedObject, TagHandler, NickHandler, + AliasHandler, AttributeHandler) from src.objects.manager import ObjectManager from src.players.models import PlayerDB from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler from src.scripts.scripthandler import ScriptHandler from src.utils import logger -from src.utils.utils import make_iter, to_str, to_unicode, variable_from_module, inherits_from +from src.utils.utils import make_iter, to_str, to_unicode, variable_from_module from django.utils.translation import ugettext as _ @@ -46,6 +44,7 @@ _ME = _("me") _SELF = _("self") _HERE = _("here") + #------------------------------------------------------------ # # ObjectDB @@ -98,9 +97,10 @@ class ObjectDB(TypedObject): # db_key (also 'name' works), db_typeclass_path, db_date_created, # db_permissions # - # These databse fields (including the inherited ones) should normally be set - # using their corresponding wrapper properties, named same as the field, but without - # the db_* prefix (e.g. the db_key field is set with self.key instead). The wrappers + # These databse fields (including the inherited ones) should normally be + # managed by their corresponding wrapper properties, named same as the + # field, but without the db_* prefix (e.g. the db_key field is set with + # self.key instead). The wrappers are created at the metaclass level and # will automatically save and cache the data more efficiently. # If this is a character object, the player is connected here. @@ -112,7 +112,7 @@ class ObjectDB(TypedObject): # The location in the game world. Since this one is likely # to change often, we set this with the 'location' property # to transparently handle Typeclassing. - db_location = models.ForeignKey('self', related_name="locations_set",db_index=True, + db_location = models.ForeignKey('self', related_name="locations_set", db_index=True, blank=True, null=True, verbose_name='game location') # a safety location, this usually don't change much. db_home = models.ForeignKey('self', related_name="homes_set", @@ -158,13 +158,19 @@ class ObjectDB(TypedObject): # seems very sensitive to caching, so leaving it be for now. /Griatch #@property def __cmdset_storage_get(self): - "Getter. Allows for value = self.name. Returns a list of cmdset_storage." + """ + Getter. Allows for value = self.name. + Returns a list of cmdset_storage. + """ storage = _GA(self, "db_cmdset_storage") # we need to check so storage is not None - return [path.strip() for path in storage.split(',')] if storage else [] + return [path.strip() for path in storage.split(',')] if storage else [] #@cmdset_storage.setter def __cmdset_storage_set(self, value): - "Setter. Allows for self.name = value. Stores as a comma-separated string." + """ + Setter. Allows for self.name = value. + Stores as a comma-separated string. + """ _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) _GA(self, "save")() #@cmdset_storage.deleter @@ -236,14 +242,13 @@ class ObjectDB(TypedObject): if exi.destination] exits = property(__exits_get) - # # Main Search method # def search(self, searchdata, global_search=False, - use_nicks=True, # should this default to off? + use_nicks=True, # should this default to off? typeclass=None, location=None, attribute_name=None, @@ -259,33 +264,46 @@ class ObjectDB(TypedObject): Inputs: - searchdata (str or obj): Primary search criterion. Will be matched against object.key (with object.aliases second) - unless the keyword attribute_name specifies otherwise. Special strings: - # - search by unique dbref. This is always a global search. + searchdata (str or obj): Primary search criterion. Will be matched + against object.key (with object.aliases second) unless + the keyword attribute_name specifies otherwise. + Special strings: + # - search by unique dbref. This is always + a global search. me,self - self-reference to this object - - - can be used to differentiate between multiple same-named matches - global_search (bool): Search all objects globally. This is overruled by "location" keyword. - use_nicks (bool): Use nickname-replace (nicktype "object") on the search string - typeclass (str or Typeclass, or list of either): Limit search only to Objects with this typeclass. May - be a list of typeclasses for a broader search. - location (Object): Specify a location to search, if different from the self's given location - plus its contents. This can also be a list of locations. - attribute_name (str): Define which property to search. If set, no key+alias search will be performed. This can be used to - search database fields (db_ will be automatically appended), and if that fails, it will try to - return objects having Attributes with this name and value equal to searchdata. A special - use is to search for "key" here if you want to do a key-search without including aliases. - quiet (bool) - don't display default error messages - return multiple matches as a list and - no matches as None. If not set (default), will echo error messages and return None. - exact (bool) - if unset (default) - prefers to match to beginning of string rather than not matching - at all. If set, requires exact mathing of entire string. + - - can be used to differentiate + between multiple same-named matches + global_search (bool): Search all objects globally. This is overruled + by "location" keyword. + use_nicks (bool): Use nickname-replace (nicktype "object") on the + search string + typeclass (str or Typeclass, or list of either): Limit search only + to Objects with this typeclass. May be a list of typeclasses + for a broader search. + location (Object): Specify a location to search, if different from the + self's given location plus its contents. This can also + be a list of locations. + attribute_name (str): Define which property to search. If set, no + key+alias search will be performed. This can be used to + search database fields (db_ will be automatically + appended), and if that fails, it will try to return + objects having Attributes with this name and value + equal to searchdata. A special use is to search for + "key" here if you want to do a key-search without + including aliases. + quiet (bool) - don't display default error messages - return multiple + matches as a list and no matches as None. If not + set (default), will echo error messages and return None. + exact (bool) - if unset (default) - prefers to match to beginning of + string rather than not matching at all. If set, requires + exact mathing of entire string. Returns: - quiet=False (default): no match or multimatch: - auto-echoes errors to self.msg, then returns None - (results are handled by modules set by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) + auto-echoes errors to self.msg, then returns None + (results are handled by settings.SEARCH_AT_RESULT + and settings.SEARCH_AT_MULTIMATCH_INPUT) match: a unique object match quiet=True: @@ -315,8 +333,10 @@ class ObjectDB(TypedObject): break candidates=None - if global_search or (is_string and searchdata.startswith("#") and len(searchdata) > 1 and searchdata[1:].isdigit()): - # only allow exact matching if searching the entire database or unique #dbrefs + if(global_search or (is_string and searchdata.startswith("#") and + len(searchdata) > 1 and searchdata[1:].isdigit())): + # only allow exact matching if searching the entire database + # or unique #dbrefs exact = True elif location: # location(s) were given @@ -324,13 +344,15 @@ class ObjectDB(TypedObject): for obj in make_iter(location): candidates.extend([o.dbobj for o in obj.contents]) else: - # local search. Candidates are self.contents, self.location and self.location.contents + # local search. Candidates are self.contents, self.location + # and self.location.contents location = self.location candidates = self.contents if location: candidates = candidates + [location] + location.contents else: - candidates.append(self) # normally we are included in location.contents + # normally we are included in location.contents + candidates.append(self) # db manager expects database objects candidates = [obj.dbobj for obj in candidates] @@ -360,23 +382,24 @@ class ObjectDB(TypedObject): def execute_cmd(self, raw_string, sessid=None): """ - Do something as this object. This method is a copy of the execute_cmd method on the - session. This is never called normally, it's only used when wanting specifically to - let an object be the caller of a command. It makes use of nicks of eventual connected - players as well. + Do something as this object. This method is a copy of the execute_ + cmd method on the session. This is never called normally, it's only + used when wanting specifically to let an object be the caller of a + command. It makes use of nicks of eventual connected players as well. Argument: raw_string (string) - raw command input sessid (int) - optional session id to return results to Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To overload - this one needs to attach callback functions to it, with addCallback(function). - This function will be called with an eventual return value from the command - execution. + not fire until the command has actually finished executing. To + overload this one needs to attach callback functions to it, with + addCallback(function). This function will be called with an + eventual return value from the command execution. - This return is not used at all by Evennia by default, but might be useful - for coders intending to implement some sort of nested command structure. + This return is not used at all by Evennia by default, but might + be useful for coders intending to implement some sort of nested + command structure. """ # nick replacement - we require full-word matching. @@ -384,7 +407,8 @@ class ObjectDB(TypedObject): raw_string = to_unicode(raw_string) raw_list = raw_string.split(None) - raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]] + raw_list = [" ".join(raw_list[:i + 1]) for i in range(len(raw_list)) + if raw_list[:i + 1]] # fetch the nick data efficiently nicks = self.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel")) if self.has_player: @@ -416,7 +440,6 @@ class ObjectDB(TypedObject): if "data" in kwargs: # deprecation warning - from src.utils import logger logger.log_depmsg("ObjectDB.msg(): 'data'-dict keyword is deprecated. Use **kwargs instead.") data = kwargs.pop("data") if isinstance(data, dict): @@ -437,7 +460,8 @@ class ObjectDB(TypedObject): """ Emits something to all objects inside an object. - exclude is a list of objects not to send to. See self.msg() for more info. + exclude is a list of objects not to send to. See self.msg() for + more info. """ contents = _GA(self, "contents") if exclude: @@ -454,22 +478,27 @@ class ObjectDB(TypedObject): Moves this object to a new location. Note that if is an exit object (i.e. it has "destination"!=None), the move_to will happen to this destination and -not- into the exit object itself, unless - use_destination=False. Note that no lock checks are done by this function, - such things are assumed to have been handled before calling move_to. + use_destination=False. Note that no lock checks are done by this + function, such things are assumed to have been handled before calling + move_to. destination: (Object) Reference to the object to move to. This can also be an exit object, in which case the destination property is used as destination. quiet: (bool) If true, don't emit left/arrived messages. emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" property - of destinations as the target to move to. Turning off this - keyword allows objects to move "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run when moving - to a None location. If you want to run hooks, run them manually. + use_destination (bool): Default is for objects to use the "destination" + property of destinations as the target to move to. + Turning off this keyword allows objects to move + "inside" exit objects. + to_none - allow destination to be None. Note that no hooks are run when + moving to a None location. If you want to run hooks, + run them manually (and make sure they can manage None + locations). - Returns True/False depending on if there were problems with the move. This method - may also return various error messages to the emit_to_obj. + Returns True/False depending on if there were problems with the move. + This method may also return various error messages to the + emit_to_obj. """ def logerr(string=""): trc = traceback.format_exc() @@ -633,11 +662,13 @@ class ObjectDB(TypedObject): def copy(self, new_key=None): """ - Makes an identical copy of this object. If you want to customize the copy by - changing some settings, use ObjectDB.object.copy_object() directly. + Makes an identical copy of this object. If you want to customize the + copy by changing some settings, use ObjectDB.object.copy_object() + directly. - new_key (string) - new key/name of copied object. If new_key is not specified, the copy will be named - _copy by default. + new_key (string) - new key/name of copied object. If new_key is not + specified, the copy will be named _copy + by default. Returns: Object (copy of this one) """ def find_clone_key(): @@ -650,7 +681,8 @@ class ObjectDB(TypedObject): key = _GA(self, "key") num = 1 for obj in (obj for obj in self.location.contents - if obj.key.startswith(key) and obj.key.lstrip(key).isdigit()): + if obj.key.startswith(key) and + obj.key.lstrip(key).isdigit()): num += 1 return "%s%03i" % (key, num) new_key = new_key or find_clone_key() @@ -705,7 +737,7 @@ class ObjectDB(TypedObject): _GA(self, "clear_exits")() # Clear out any non-exit objects located within the object _GA(self, "clear_contents")() - old_loc = _GA(self, "location") + #old_loc = _GA(self, "location") # Perform the deletion of the object super(ObjectDB, self).delete() # clear object's old location's content cache of this object diff --git a/src/objects/objects.py b/src/objects/objects.py index d3cf7f634f..9f93a79a17 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -25,6 +25,7 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ + # # Base class to inherit from. # @@ -55,41 +56,53 @@ class Object(TypeClass): key (string) - name of object name (string) - same as key - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. + dbobj (Object, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings - player (Player) - controlling player (if any, only set together with sessid below) - sessid (int, read-only) - session id (if any, only set together with player above) + player (Player) - controlling player (if any, only set together with + sessid below) + sessid (int, read-only) - session id (if any, only set together with + player above) location (Object) - current location. Is None if this is a room home (Object) - safety start-location - sessions (list of Sessions, read-only) - returns all sessions connected to this object + sessions (list of Sessions, read-only) - returns all sessions + connected to this object has_player (bool, read-only)- will only return *connected* players - contents (list of Objects, read-only) - returns all objects inside this object (including exits) - exits (list of Objects, read-only) - returns all exits from this object, if any + contents (list of Objects, read-only) - returns all objects inside + this object (including exits) + exits (list of Objects, read-only) - returns all exits from this + object, if any destination (Object) - only set if this object is an exit. is_superuser (bool, read-only) - True/False if this user is a superuser * Handlers available locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data scripts - script-handler. Add new scripts to object with scripts.add() cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object nicks - nick-handler. New nicks with nicks.add(). * Helper methods (see src.objects.objects.py for full headers) - search(ostring, global_search=False, global_dbref=False, attribute_name=None, - use_nicks=True, location=None, ignore_errors=False, player=False) + search(ostring, global_search=False, global_dbref=False, + attribute_name=None, use_nicks=True, location=None, + ignore_errors=False, player=False) execute_cmd(raw_string) msg(message, **kwargs) msg_contents(message, exclude=None, from_obj=None, **kwargs) - move_to(destination, quiet=False, emit_to_obj=None, use_destination=True, to_none=False) + move_to(destination, quiet=False, emit_to_obj=None, + use_destination=True, to_none=False) copy(new_key=None) delete() is_typeclass(typeclass, exact=False) @@ -99,42 +112,73 @@ class Object(TypeClass): * Hook methods - basetype_setup() - only called once, used for behind-the-scenes setup. Normally not modified. - basetype_posthook_setup() - customization in basetype, after the object has been created; Normally not modified. + basetype_setup() - only called once, used for behind-the-scenes + setup. Normally not modified. + basetype_posthook_setup() - customization in basetype, after the + object has been created; Normally not modified. - at_object_creation() - only called once, when object is first created. Object customizations go here. - at_object_delete() - called just before deleting an object. If returning False, deletion is aborted. Note that all objects - inside a deleted object are automatically moved to their , they don't need to be removed here. + at_object_creation() - only called once, when object is first created. + Object customizations go here. + at_object_delete() - called just before deleting an object. If + returning False, deletion is aborted. Note that + all objects inside a deleted object are + automatically moved to their , they don't + need to be removed here. - at_init() - called whenever typeclass is cached from memory, at least once every server restart/reload - at_cmdset_get() - this is called just before the command handler requests a cmdset from this objecth - at_pre_puppet(player)- (player-controlled objects only) called just before puppeting - at_post_puppet() - (player-controlled objects only) called just after completing connection player<->object - at_pre_unpuppet() - (player-controlled objects only) called just before un-puppeting - at_post_unpuppet(player) - (player-controlled objects only) called just after disconnecting player<->object link + at_init() called whenever typeclass is cached from + memory, at least once every server restart/reload + at_cmdset_get() - this is called just before the command + handler requests a cmdset from this objecth + at_pre_puppet(player)- (player-controlled objects only) called just + before puppeting + at_post_puppet() - (player-controlled objects only) called just + after completing connection player<->object + at_pre_unpuppet() - (player-controlled objects only) called just + before un-puppeting + at_post_unpuppet(player) (player-controlled objects only) called + just after disconnecting player<->object link at_server_reload() - called before server is reloaded at_server_shutdown() - called just before server is fully shut down - at_before_move(destination) - called just before moving object to the destination. If returns False, move is cancelled. - announce_move_from(destination) - called in old location, just before move, if obj.move_to() has quiet=False - announce_move_to(source_location) - called in new location, just after move, if obj.move_to() has quiet=False - at_after_move(source_location) - always called after a move has been successfully performed. - at_object_leave(obj, target_location) - called when an object leaves this object in any fashion - at_object_receive(obj, source_location) - called when this object receives another object + at_before_move(destination) called just before moving + object to the destination. If returns + False, move is cancelled. + announce_move_from(destination) - called in old location, just before + move, if obj.move_to() has + quiet=False + announce_move_to(source_location) - called in new location, + just after move, if obj.move_to() + has quiet=False + at_after_move(source_location) - always called after a move + has been successfully performed. + at_object_leave(obj, target_location) - called when an object leaves + this object in any fashion + at_object_receive(obj, source_location) - called when this object + receives another object - at_before_traverse(traversing_object) - (exit-objects only) called just before an object traverses this object - at_after_traverse(traversing_object, source_location) - (exit-objects only) called just after a traversal has happened. - at_failed_traverse(traversing_object) - (exit-objects only) called if traversal fails and property err_traverse is not defined. + at_before_traverse(traversing_object) - (exit-objects only) called + just before an object + traverses this object + at_after_traverse(traversing_object, source_location) - (exit-objects + only) called just after a traversal has happened. + at_failed_traverse(traversing_object) - (exit-objects only) called + if traversal fails and property err_traverse is not defined. - at_msg_receive(self, msg, from_obj=None, data=None) - called when a message (via self.msg()) is sent to this obj. - If returns false, aborts send. - at_msg_send(self, msg, to_obj=None, data=None) - called when this objects sends a message to someone via self.msg(). + at_msg_receive(self, msg, from_obj=None, data=None) - called when a + message (via self.msg()) is sent to this obj. + If returns false, aborts send. + at_msg_send(self, msg, to_obj=None, data=None) - called when this + objects sends a message to someone via self.msg(). - return_appearance(looker) - describes this object. Used by "look" command by default - at_desc(looker=None) - called by 'look' whenever the appearance is requested. - at_get(getter) - called after object has been picked up. Does not stop pickup. + return_appearance(looker) - describes this object. Used by "look" + command by default + at_desc(looker=None) - called by 'look' whenever the appearance + is requested. + at_get(getter) - called after object has been picked up. + Does not stop pickup. at_drop(dropper) - called when this object has been dropped. - at_say(speaker, message) - by default, called if an object inside this object speaks + at_say(speaker, message) - by default, called if an object inside + this object speaks """ super(Object, self).__init__(dbobj) @@ -159,30 +203,41 @@ class Object(TypeClass): Inputs: - ostring (str): Primary search criterion. Will be matched against object.key (with object.aliases second) - unless the keyword attribute_name specifies otherwise. Special strings: - # - search by unique dbref. This is always a global search. + ostring (str): Primary search criterion. Will be matched against + object.key (with object.aliases second) + unless the keyword attribute_name specifies otherwise. + Special strings: + # - search by unique dbref. This is always a + global search. me,self - self-reference to this object - - - can be used to differentiate between multiple same-named matches - global_search (bool): Search all objects globally. This is overruled by "location" keyword. - use_nicks (bool): Use nickname-replace (nicktype "object") on the search string - typeclass (str or Typeclass): Limit search only to Objects with this typeclass. May be a list of typeclasses - for a broader search. - location (Object): Specify a location to search, if different from the self's given location + - - can be used to differentiate between + multiple same-named matches + global_search (bool): Search all objects globally. This is overruled + by "location" keyword. + use_nicks (bool): Use nickname-replace (nicktype "object") on the + search string + typeclass (str or Typeclass): Limit search only to Objects with this + typeclass. May be a list of typeclasses for a + broader search. + location (Object): Specify a location to search, if different from the + self's given location plus its contents. This can also be a list of locations. - attribute_name (str): Use this named Attribute to match ostring against, instead of object.key. - quiet (bool) - don't display default error messages - return multiple matches as a list and - no matches as None. If not set (default), will echo error messages and return None. - exact (bool) - if unset (default) - prefers to match to beginning of string rather than not matching - at all. If set, requires exact mathing of entire string. + attribute_name (str): Use this named Attribute to match ostring against, + instead of object.key. + quiet (bool) - don't display default error messages - return multiple + matches as a list and no matches as None. If not + set (default), will echo error messages and return None. + exact (bool) - if unset (default) - prefers to match to beginning of + string rather than not matching at all. If set, + requires exact mathing of entire string. Returns: quiet=False (default): no match or multimatch: auto-echoes errors to self.msg, then returns None - (results are handled by modules set by settings.SEARCH_AT_RESULT - and settings.SEARCH_AT_MULTIMATCH_INPUT) + (results are handled by settings.SEARCH_AT_RESULT + and settings.SEARCH_AT_MULTIMATCH_INPUT) match: a unique object match quiet=True: @@ -209,16 +264,18 @@ class Object(TypeClass): Argument: raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the sessid property on the command. + sessid (int) - id of session executing the command. This sets the + sessid property on the command. Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To overload - this one needs to attach callback functions to it, with addCallback(function). - This function will be called with an eventual return value from the command - execution. + not fire until the command has actually finished executing. To + overload this one needs to attach callback functions to it, with + addCallback(function). This function will be called with an + eventual return value from the command execution. - This return is not used at all by Evennia by default, but might be useful - for coders intending to implement some sort of nested command structure. + This return is not used at all by Evennia by default, but might be + useful for coders intending to implement some sort of nested + command structure. """ return self.dbobj.execute_cmd(raw_string, sessid=sessid) @@ -233,48 +290,59 @@ class Object(TypeClass): sessid: optional session target. If sessid=0, the session will default to self.sessid or from_obj.sessid. """ - self.dbobj.msg(text=text, **kwargs)#message, from_obj=from_obj, data=data, sessid=0) + + self.dbobj.msg(text=text, **kwargs) def msg_contents(self, text=None, exclude=None, from_obj=None, **kwargs): """ Emits something to all objects inside an object. - exclude is a list of objects not to send to. See self.msg() for more info. + exclude is a list of objects not to send to. See self.msg() for + more info. """ - self.dbobj.msg_contents(text, exclude=exclude, from_obj=from_obj, **kwargs) + self.dbobj.msg_contents(text, exclude=exclude, + from_obj=from_obj, **kwargs) def move_to(self, destination, quiet=False, emit_to_obj=None, use_destination=True, to_none=False): """ Moves this object to a new location. Note that if is an exit object (i.e. it has "destination"!=None), the move_to will - happen to this destination and -not- into the exit object itself, unless - use_destination=False. Note that no lock checks are done by this function, - such things are assumed to have been handled before calling move_to. + happen to this destination and -not- into the exit object itself, + unless use_destination=False. Note that no lock checks are done by + this function, such things are assumed to have been handled before + calling move_to. destination: (Object) Reference to the object to move to. This can also be an exit object, in which case the destination property is used as destination. quiet: (bool) If true, don't emit left/arrived messages. emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" property - of destinations as the target to move to. Turning off this - keyword allows objects to move "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run when moving - to a None location. If you want to run hooks, run them manually. - Returns True/False depending on if there were problems with the move. This method - may also return various error messages to the emit_to_obj. + use_destination (bool): Default is for objects to use the "destination" + property of destinations as the target to move to. + Turning off this keyword allows objects to move + "inside" exit objects. + to_none - allow destination to be None. Note that no hooks are run + when moving to a None location. If you want to run hooks, run + them manually (and make sure the hooks can handle a None + location). + Returns True/False depending on if there were problems with the move. + This method may also return various error messages to the + emit_to_obj. """ return self.dbobj.move_to(destination, quiet=quiet, - emit_to_obj=emit_to_obj, use_destination=use_destination) + emit_to_obj=emit_to_obj, + use_destination=use_destination) def copy(self, new_key=None): """ - Makes an identical copy of this object. If you want to customize the copy by - changing some settings, use ObjectDB.object.copy_object() directly. + Makes an identical copy of this object. If you want to customize the + copy by changing some settings, use ObjectDB.object.copy_object() + directly. - new_key (string) - new key/name of copied object. If new_key is not specified, the copy will be named + new_key (string) - new key/name of copied object. If new_key is not + specified, the copy will be named _copy by default. Returns: Object (copy of this one) """ @@ -293,7 +361,6 @@ class Object(TypeClass): """ return self.dbobj.delete() - # methods inherited from the typeclass system def is_typeclass(self, typeclass, exact=False): @@ -347,18 +414,20 @@ class Object(TypeClass): """ - return self.dbobj.swap_typeclass(new_typeclass, clean_attributes=clean_attributes, no_default=no_default) + return self.dbobj.swap_typeclass(new_typeclass, + clean_attributes=clean_attributes, no_default=no_default) def access(self, accessing_obj, access_type='read', default=False): """ - Determines if another object has permission to access this object in whatever way. + Determines if another object has permission to access this object in + whatever way. accessing_obj (Object)- object trying to access this one access_type (string) - type of access sought default (bool) - what to return if no lock of access_type was found - This function will call at_access_success or at_access_failure depending on the - outcome of the access check. + This function will call at_access_success or at_access_failure + depending on the outcome of the access check. """ if self.dbobj.access(accessing_obj, access_type=access_type, default=default): @@ -373,12 +442,12 @@ class Object(TypeClass): This explicitly checks the given string against this object's 'permissions' property without involving any locks. - permstring (string) - permission string that need to match a permission on the object. + permstring (string) - permission string that need to match a + permission on the object. (example: 'Builders') """ return self.dbobj.check_permstring(permstring) - def __eq__(self, other): """ Checks for equality against an id string or another object or user. @@ -387,24 +456,23 @@ class Object(TypeClass): parent doesn't work. """ try: - return _GA(_GA(self, "dbobj"),"dbid") == _GA(_GA(other,"dbobj"),"dbid") + return _GA(_GA(self, "dbobj"), "dbid") == _GA(_GA(other, "dbobj"), "dbid") except AttributeError: # compare players instead try: - return _GA(_GA(_GA(self, "dbobj"),"player"),"uid") == _GA(_GA(other, "player"),"uid") + return _GA(_GA(_GA(self, "dbobj"), "player"), "uid") == _GA(_GA(other, "player"), "uid") except AttributeError: return False ## hooks called by the game engine - def basetype_setup(self): """ This sets up the default properties of an Object, just before the more general at_object_creation. - You normally don't need to change this unless you change some fundamental - things like names of permission groups. + You normally don't need to change this unless you change some + fundamental things like names of permission groups. """ # the default security setup fallback for a generic # object. Overload in child for a custom setup. Also creation @@ -412,22 +480,25 @@ class Object(TypeClass): # controller, for example) dbref = self.dbobj.dbref - self.locks.add(";".join(["control:perm(Immortals)", # edit locks/permissions, delete - "examine:perm(Builders)", # examine properties - "view:all()", # look at object (visibility) - "edit:perm(Wizards)", # edit properties/attributes - "delete:perm(Wizards)", # delete object - "get:all()", # pick up object - "call:true()", # allow to call commands on this object - "tell:perm(Wizards)", # allow emits to this object - "puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref])) # restricts puppeting of this object + self.locks.add(";".join([ + "control:perm(Immortals)", # edit locks/permissions, delete + "examine:perm(Builders)", # examine properties + "view:all()", # look at object (visibility) + "edit:perm(Wizards)", # edit properties/attributes + "delete:perm(Wizards)", # delete object + "get:all()", # pick up object + "call:true()", # allow to call commands on this object + "tell:perm(Wizards)", # allow emits to this object + # restricts puppeting of this object + "puppet:pid(%s) or perm(Immortals) or pperm(Immortals)" % dbref])) def basetype_posthook_setup(self): """ - Called once, after basetype_setup and at_object_creation. This should generally not be overloaded unless - you are redefining how a room/exit/object works. It allows for basetype-like setup - after the object is created. An example of this is EXITs, who need to know keys, aliases, locks - etc to set up their exit-cmdsets. + Called once, after basetype_setup and at_object_creation. This should + generally not be overloaded unless you are redefining how a + room/exit/object works. It allows for basetype-like setup after the + object is created. An example of this is EXITs, who need to know keys, + aliases, locks etc to set up their exit-cmdsets. """ pass @@ -437,7 +508,6 @@ class Object(TypeClass): """ pass - def at_object_delete(self): """ Called just before the database object is @@ -502,34 +572,34 @@ class Object(TypeClass): def at_server_reload(self): """ - This hook is called whenever the server is shutting down for restart/reboot. - If you want to, for example, save non-persistent properties across a restart, - this is the place to do it. + This hook is called whenever the server is shutting down for + restart/reboot. If you want to, for example, save non-persistent + properties across a restart, this is the place to do it. """ pass def at_server_shutdown(self): """ - This hook is called whenever the server is shutting down fully (i.e. not for - a restart). + This hook is called whenever the server is shutting down fully + (i.e. not for a restart). """ pass def at_access_success(self, accessing_obj, access_type): """ - This hook is called whenever accessing_obj succeed a lock check of type access_type - on this object, for whatever reason. The return value of this hook is not used, - the lock will still pass regardless of what this hook does (use lockstring/funcs to tweak - the lock result). + This hook is called whenever accessing_obj succeed a lock check of + type access_type on this object, for whatever reason. The return value + of this hook is not used, the lock will still pass regardless of what + this hook does (use lockstring/funcs to tweak the lock result). """ pass def at_access_failure(self, accessing_obj, access_type): """ - This hook is called whenever accessing_obj fails a lock check of type access_type - on this object, for whatever reason. The return value of this hook is not used, the - lock will still fail regardless of what this hook does (use lockstring/funcs to tweak the - lock result). + This hook is called whenever accessing_obj fails a lock check of type + access_type on this object, for whatever reason. The return value of + this hook is not used, the lock will still fail regardless of what + this hook does (use lockstring/funcs to tweak the lock result). """ pass @@ -588,7 +658,6 @@ class Object(TypeClass): string = "%s arrives to %s from %s." self.location.msg_contents(string % (name, loc_name, src_name), exclude=self) - def at_after_move(self, source_location): """ Called after move has completed, regardless of quiet mode or not. @@ -598,7 +667,6 @@ class Object(TypeClass): """ pass - def at_object_leave(self, moved_obj, target_location): """ Called just before an object leaves from inside this object @@ -630,9 +698,9 @@ class Object(TypeClass): """ This hook is responsible for handling the actual traversal, normally by calling traversing_object.move_to(target_location). It is normally - only implemented by Exit objects. If it returns False (usually because move_to - returned False), at_after_traverse below should not be called and - instead at_failed_traverse should be called. + only implemented by Exit objects. If it returns False (usually because + move_to returned False), at_after_traverse below should not be called + and instead at_failed_traverse should be called. """ pass @@ -687,7 +755,6 @@ class Object(TypeClass): """ pass - # hooks called by the default cmdset. def return_appearance(self, pobject): @@ -698,7 +765,8 @@ class Object(TypeClass): if not pobject: return # get and identify all objects - visible = (con for con in self.contents if con != pobject and con.access(pobject, "view")) + visible = (con for con in self.contents if con != pobject and + con.access(pobject, "view")) exits, users, things = [], [], [] for con in visible: key = con.key @@ -744,6 +812,7 @@ class Object(TypeClass): dropper - the object which just dropped this object. """ pass + def at_say(self, speaker, message): """ Called on this object if an object inside this object speaks. @@ -797,12 +866,13 @@ class Character(Object): def at_pre_puppet(self, player, sessid=None): """ - This recovers the character again after having been "stoved away" at the unpuppet + This recovers the character again after having been "stoved away" + at the unpuppet """ if self.db.prelogout_location: # try to recover self.location = self.db.prelogout_location - if self.location == None: + if self.location is None: # make sure location is never None (home should always exist) self.location = self.home if self.location: @@ -824,8 +894,9 @@ class Character(Object): def at_post_unpuppet(self, player, sessid=None): """ - We stove away the character when the player goes ooc/logs off, otherwise the character object will - remain in the room also after the player logged off ("headless", so to say). + We stove away the character when the player goes ooc/logs off, + otherwise the character object will remain in the room also after the + player logged off ("headless", so to say). """ if self.location: # have to check, in case of multiple connections closing self.location.msg_contents("%s has left the game." % self.name, exclude=[self]) @@ -852,6 +923,7 @@ class Room(Object): "puppet:false()"])) # would be weird to puppet a room ... self.location = None + # # Base Exit object # @@ -905,7 +977,8 @@ class Exit(Object): # No shorthand error message. Call hook. self.obj.at_failed_traverse(self.caller) - # create an exit command. We give the properties here, to always trigger metaclass preparations + # create an exit command. We give the properties here, + # to always trigger metaclass preparations cmd = ExitCommand(key=exidbobj.db_key.strip().lower(), aliases=exidbobj.aliases.all(), locks=str(exidbobj.locks), @@ -944,8 +1017,9 @@ class Exit(Object): def at_cmdset_get(self): """ - Called when the cmdset is requested from this object, just before the cmdset is - actually extracted. If no Exit-cmdset is cached, create it now. + Called when the cmdset is requested from this object, just before the + cmdset is actually extracted. If no Exit-cmdset is cached, create + it now. """ if self.ndb.exit_reset or not self.cmdset.has_cmdset("_exitset", must_be_default=True): @@ -984,8 +1058,9 @@ class Exit(Object): def at_failed_traverse(self, traversing_object): """ This is called if an object fails to traverse this object for some - reason. It will not be called if the attribute "err_traverse" is defined, - that attribute will then be echoed back instead as a convenient shortcut. + reason. It will not be called if the attribute "err_traverse" is + defined, that attribute will then be echoed back instead as a + convenient shortcut. (See also hooks at_before_traverse and at_after_traverse). """ diff --git a/src/objects/tests.py b/src/objects/tests.py index dbf45d209e..9d1bff6327 100644 --- a/src/objects/tests.py +++ b/src/objects/tests.py @@ -8,10 +8,12 @@ Runs as part of the Evennia's test suite with 'manage.py test" Please add new tests to this module as needed. Guidelines: - A 'test case' is testing a specific component and is defined as a class inheriting from unittest.TestCase. - The test case class can have a method setUp() that creates and sets up the testing environment. - All methods inside the test case class whose names start with 'test' are used as test methods by the runner. - Inside the test methods, special member methods assert*() are used to test the behaviour. + A 'test case' is testing a specific component and is defined as a class + inheriting from unittest.TestCase. The test case class can have a method + setUp() that creates and sets up the testing environment. + All methods inside the test case class whose names start with 'test' are + used as test methods by the runner. Inside the test methods, special member + methods assert*() are used to test the behaviour. """ import sys @@ -24,12 +26,10 @@ try: except ImportError: import unittest -from django.conf import settings -from src.objects import models, objects -from src.utils import create from src.commands.default import tests as commandtests from src.locks import tests as locktests + class TestObjAttrs(TestCase): """ Test aspects of ObjAttributes @@ -49,6 +49,7 @@ class TestObjAttrs(TestCase): # self.assertEqual(self.obj2 ,self.obj1.db.testattr) # self.assertEqual(self.obj2.location, self.obj1.db.testattr.location) + def suite(): """ This function is called automatically by the django test runner. diff --git a/src/players/__init__.py b/src/players/__init__.py index fb882aa549..b49e80468c 100644 --- a/src/players/__init__.py +++ b/src/players/__init__.py @@ -1,12 +1,13 @@ """ -Makes it easier to import by grouping all relevant things already at this level. +Makes it easier to import by grouping all relevant things already at this +level. You can henceforth import most things directly from src.player Also, the initiated object manager is available as src.players.manager. """ -from src.players.player import * +from src.players.player import * from src.players.models import PlayerDB manager = PlayerDB.objects diff --git a/src/players/admin.py b/src/players/admin.py index 3e5de18090..279d1fff01 100644 --- a/src/players/admin.py +++ b/src/players/admin.py @@ -4,16 +4,16 @@ # from django import forms -from django.db import models +#from django.db import models from django.conf import settings from django.contrib import admin from django.contrib.auth.admin import UserAdmin as BaseUserAdmin -from django.contrib.admin import widgets +#from django.contrib.admin import widgets from django.contrib.auth.forms import UserChangeForm, UserCreationForm -from django.contrib.auth.models import User +#from django.contrib.auth.models import User from src.players.models import PlayerDB -from src.typeclasses.models import Attribute -from src.utils import logger, create +#from src.typeclasses.models import Attribute +from src.utils import create # handle the custom User editor @@ -109,6 +109,7 @@ class PlayerForm(forms.ModelForm): required=False, help_text="python path to player cmdset class (set in settings.CMDSET_PLAYER by default)") + class PlayerInline(admin.StackedInline): "Inline creation of Player" model = PlayerDB @@ -127,6 +128,7 @@ class PlayerInline(admin.StackedInline): extra = 1 max_num = 1 + class PlayerDBAdmin(BaseUserAdmin): "This is the main creation screen for Users/players" @@ -136,17 +138,17 @@ class PlayerDBAdmin(BaseUserAdmin): fieldsets = ( (None, {'fields': ('username', 'password', 'email')}), ('Website profile', {'fields': ('first_name', 'last_name'), - 'description':"These are not used in the default system."}), + 'description': "These are not used in the default system."}), ('Website dates', {'fields': ('last_login', 'date_joined'), - 'description':'Relevant only to the website.'}), - ('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', 'user_permissions','groups'), + 'description': 'Relevant only to the website.'}), + ('Website Permissions', {'fields': ('is_active', 'is_staff', 'is_superuser', + 'user_permissions', 'groups'), 'description': "These are permissions/permission groups for accessing the admin site. They are unrelated to in-game access rights."}), ('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_lock_storage'), 'description': 'These are attributes that are more relevant to gameplay.'})) #('Game Options', {'fields': ('db_typeclass_path', 'db_cmdset_storage', 'db_permissions', 'db_lock_storage'), # 'description': 'These are attributes that are more relevant to gameplay.'})) - add_fieldsets = ( (None, {'fields': ('username', 'password1', 'password2', 'email'), @@ -162,7 +164,7 @@ class PlayerDBAdmin(BaseUserAdmin): #uname, passwd, email = str(request.POST.get(u"username")), \ # str(request.POST.get(u"password1")), str(request.POST.get(u"email")) typeclass = str(request.POST.get(u"playerdb_set-0-db_typeclass_path")) - create.create_player("","","", + create.create_player("", "", "", user=userobj, typeclass=typeclass, player_dbobj=userobj) diff --git a/src/players/manager.py b/src/players/manager.py index 4bfc9605b0..8e07829034 100644 --- a/src/players/manager.py +++ b/src/players/manager.py @@ -4,11 +4,12 @@ The managers for the custom Player object and permissions. import datetime from django.contrib.auth.models import UserManager -from functools import update_wrapper +#from functools import update_wrapper from src.typeclasses.managers import returns_typeclass_list, returns_typeclass, TypedObjectManager -from src.utils import logger +#from src.utils import logger __all__ = ("PlayerManager",) + # # Player Manager # @@ -118,8 +119,8 @@ class PlayerManager(TypedObjectManager, UserManager): def swap_character(self, player, new_character, delete_old_character=False): """ - This disconnects a player from the current character (if any) and connects - to a new character object. + This disconnects a player from the current character (if any) and + connects to a new character object. """ diff --git a/src/players/models.py b/src/players/models.py index 52ef0d84ae..d16e1330d0 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -23,15 +23,16 @@ from django.utils.encoding import smart_str from src.players import manager from src.scripts.models import ScriptDB -from src.typeclasses.models import TypedObject, TagHandler, NickHandler, AliasHandler, AttributeHandler +from src.typeclasses.models import (TypedObject, TagHandler, NickHandler, + AliasHandler, AttributeHandler) from src.commands.cmdsethandler import CmdSetHandler from src.commands import cmdhandler -from src.utils import utils +from src.utils import utils, logger from src.utils.utils import to_str, make_iter from django.utils.translation import ugettext as _ -__all__ = ("PlayerDB",) +__all__ = ("PlayerDB",) _ME = _("me") _SELF = _("self") @@ -47,14 +48,12 @@ _DA = object.__delattr__ _TYPECLASS = None - #------------------------------------------------------------ # # PlayerDB # #------------------------------------------------------------ - class PlayerDB(TypedObject, AbstractUser): """ This is a special model using Django's 'profile' functionality @@ -90,7 +89,9 @@ class PlayerDB(TypedObject, AbstractUser): # store a connected flag here too, not just in sessionhandler. # This makes it easier to track from various out-of-process locations - db_is_connected = models.BooleanField(default=False, verbose_name="is_connected", help_text="If player is connected to game or not") + db_is_connected = models.BooleanField(default=False, + verbose_name="is_connected", + help_text="If player is connected to game or not") # database storage of persistant cmdsets. db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, help_text="optional python path to a cmdset class. If creating a Character, this will default to settings.CMDSET_CHARACTER.") @@ -118,24 +119,36 @@ class PlayerDB(TypedObject, AbstractUser): _SA(self, "nicks", NickHandler(self)) # alias to the objs property - def __characters_get(self): return self.objs - def __characters_set(self, value): self.objs = value - def __characters_del(self): raise Exception("Cannot delete name") + def __characters_get(self): + return self.objs + + def __characters_set(self, value): + self.objs = value + + def __characters_del(self): + raise Exception("Cannot delete name") characters = property(__characters_get, __characters_set, __characters_del) # cmdset_storage property # This seems very sensitive to caching, so leaving it be for now /Griatch #@property def cmdset_storage_get(self): - "Getter. Allows for value = self.name. Returns a list of cmdset_storage." + """ + Getter. Allows for value = self.name. Returns a list of cmdset_storage. + """ storage = _GA(self, "db_cmdset_storage") # we need to check so storage is not None - return [path.strip() for path in storage.split(',')] if storage else [] + return [path.strip() for path in storage.split(',')] if storage else [] + #@cmdset_storage.setter def cmdset_storage_set(self, value): - "Setter. Allows for self.name = value. Stores as a comma-separated string." + """ + Setter. Allows for self.name = value. Stores as a comma-separated + string. + """ _SA(self, "db_cmdset_storage", ",".join(str(val).strip() for val in make_iter(value))) _GA(self, "save")() + #@cmdset_storage.deleter def cmdset_storage_del(self): "Deleter. Allows for del self.name" @@ -161,10 +174,12 @@ class PlayerDB(TypedObject, AbstractUser): #@property def __username_get(self): return _GA(self, "username") + def __username_set(self, value): _SA(self, "username", value) + def __username_del(self): - _DA(self, "username", value) + _DA(self, "username") # aliases name = property(__username_get, __username_set, __username_del) key = property(__username_get, __username_set, __username_del) @@ -173,8 +188,10 @@ class PlayerDB(TypedObject, AbstractUser): def __uid_get(self): "Getter. Retrieves the user id" return self.id + def __uid_set(self, value): raise Exception("User id cannot be set!") + def __uid_del(self): raise Exception("User id cannot be deleted!") uid = property(__uid_get, __uid_set, __uid_del) @@ -197,21 +214,21 @@ class PlayerDB(TypedObject, AbstractUser): def msg(self, text=None, from_obj=None, sessid=None, **kwargs): """ Evennia -> User - This is the main route for sending data back to the user from the server. + This is the main route for sending data back to the user from the + server. outgoing_string (string) - text data to send - from_obj (Object/Player) - source object of message to send. Its at_msg_send - hook will be called. - sessid - the session id of the session to send to. If not given, return to - all sessions connected to this player. This is usually only + from_obj (Object/Player) - source object of message to send. Its + at_msg_send() hook will be called. + sessid - the session id of the session to send to. If not given, return + to all sessions connected to this player. This is usually only relevant when using msg() directly from a player-command (from - a command on a Character, the character automatically stores and - handles the sessid). + a command on a Character, the character automatically stores + and handles the sessid). kwargs (dict) - All other keywords are parsed as extra data. """ if "data" in kwargs: # deprecation warning - from src.utils import logger logger.log_depmsg("PlayerDB:msg() 'data'-dict keyword is deprecated. Use **kwargs instead.") data = kwargs.pop("data") if isinstance(data, dict): @@ -253,16 +270,17 @@ class PlayerDB(TypedObject, AbstractUser): if not _SESSIONS: from src.server.sessionhandler import SESSIONS as _SESSIONS return _SESSIONS.sessions_from_player(self) - sessions = property(get_all_sessions) # alias shortcut - + sessions = property(get_all_sessions) # alias shortcut def disconnect_session_from_player(self, sessid): """ Access method for disconnecting a given session from the player (connection happens automatically in the sessionhandler) """ - # this should only be one value, loop just to make sure to clean everything - sessions = (session for session in self.get_all_sessions() if session.sessid == sessid) + # this should only be one value, loop just to make sure to + # clean everything + sessions = (session for session in self.get_all_sessions() + if session.sessid == sessid) for session in sessions: # this will also trigger unpuppeting session.sessionhandler.disconnect(session) @@ -292,8 +310,9 @@ class PlayerDB(TypedObject, AbstractUser): # we don't allow to puppet an object already controlled by an active # player. To kick a player, call unpuppet_object on them explicitly. return - # if we get to this point the character is ready to puppet or it was left - # with a lingering player/sessid reference from an unclean server kill or similar + # if we get to this point the character is ready to puppet or it + # was left with a lingering player/sessid reference from an unclean + # server kill or similar if normal_mode: _GA(obj.typeclass, "at_pre_puppet")(self.typeclass, sessid=sessid) @@ -341,8 +360,9 @@ class PlayerDB(TypedObject, AbstractUser): def get_puppet(self, sessid, return_dbobj=False): """ - Get an object puppeted by this session through this player. This is the main - method for retrieving the puppeted object from the player's end. + Get an object puppeted by this session through this player. This is + the main method for retrieving the puppeted object from the + player's end. sessid - return character connected to this sessid, character - return character if connected to this player, else None. @@ -359,7 +379,8 @@ class PlayerDB(TypedObject, AbstractUser): """ Get all currently puppeted objects as a list """ - puppets = [session.puppet for session in self.get_all_sessions() if session.puppet] + puppets = [session.puppet for session in self.get_all_sessions() + if session.puppet] if return_dbobj: return puppets return [puppet.typeclass for puppet in puppets] @@ -379,30 +400,25 @@ class PlayerDB(TypedObject, AbstractUser): # utility methods - def delete(self, *args, **kwargs): """ Deletes the player permanently. - Makes sure to delete user also when deleting player - the two may never exist separately. """ for session in self.get_all_sessions(): # unpuppeting all objects and disconnecting the user, if any - # sessions remain (should usually be handled from the deleting command) + # sessions remain (should usually be handled from the + # deleting command) self.unpuppet_object(session.sessid) session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) - #try: - # if _GA(self, "user"): - # _GA(_GA(self, "user"), "delete")() - #except AssertionError: - # pass super(PlayerDB, self).delete(*args, **kwargs) def execute_cmd(self, raw_string, sessid=None): """ Do something as this player. This method is never called normally, - but only when the player object itself is supposed to execute the command. It - does not take nicks on eventual puppets into account. + but only when the player object itself is supposed to execute the + command. It does not take nicks on eventual puppets into account. + raw_string - raw command input coming from the command line. """ # nick replacement - we require full-word matching. @@ -410,8 +426,9 @@ class PlayerDB(TypedObject, AbstractUser): raw_string = utils.to_unicode(raw_string) raw_list = raw_string.split(None) - raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]] - # get the nick replacement data directly from the database to be able to use db_category__in + raw_list = [" ".join(raw_list[:i + 1]) for i in range(len(raw_list)) if raw_list[:i + 1]] + # get the nick replacement data directly from the database to be + # able to use db_category__in nicks = self.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel")) for nick in nicks: if nick.db_key in raw_list: @@ -419,22 +436,30 @@ class PlayerDB(TypedObject, AbstractUser): break if not sessid and _MULTISESSION_MODE in (0, 1): # in this case, we should either have only one sessid, or the sessid - # should not matter (since the return goes to all of them we can just - # use the first one as the source) + # should not matter (since the return goes to all of them we can + # just use the first one as the source) sessid = self.get_all_sessions()[0].sessid - return cmdhandler.cmdhandler(self.typeclass, raw_string, callertype="player", sessid=sessid) + return cmdhandler.cmdhandler(self.typeclass, raw_string, + callertype="player", sessid=sessid) - def search(self, ostring, return_character=False, **kwargs): + def search(self, ostring, return_puppet=False, + return_character=False, **kwargs): """ - This is similar to the ObjectDB search method but will search for Players only. Errors - will be echoed, and None returned if no Player is found. + This is similar to the ObjectDB search method but will search for + Players only. Errors will be echoed, and None returned if no Player + is found. - return_character - will try to return the character the player controls instead of - the Player object itself. If no Character exists (since Player is - OOC), None will be returned. - Extra keywords are ignored, but are allowed in call in order to make API more consistent - with objects.models.TypedObject.search. + return_character - will try to return the character the player controls + instead of the Player object itself. If no + Character exists (since Player is OOC), None will + be returned. + Extra keywords are ignored, but are allowed in call in order to make + API more consistent with objects.models. + TypedObject.search. """ + if return_character: + logger.log_depmsg("Player.search's 'return_character' keyword is deprecated. Use the return_puppet keyword instead.") + #return_puppet = return_character # handle me, self if ostring in (_ME, _SELF, '*' + _ME, '*' + _SELF): return self diff --git a/src/players/player.py b/src/players/player.py index 4f3e026675..6d8042ab5c 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -41,23 +41,29 @@ class Player(TypeClass): key (string) - name of player name (string)- wrapper for user.username - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Player, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Player, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. + dbobj (Player, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Player, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings user (User, read-only) - django User authorization object - obj (Object) - game object controlled by player. 'character' can also be used. + obj (Object) - game object controlled by player. 'character' can also + be used. sessions (list of Sessions) - sessions connected to this player is_superuser (bool, read-only) - if the connected user is a superuser * Handlers locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data scripts - script-handler. Add new scripts to object with scripts.add() cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object nicks - nick-handler. New nicks with nicks.add(). @@ -67,7 +73,9 @@ class Player(TypeClass): msg(outgoing_string, from_obj=None, **kwargs) swap_character(new_character, delete_old_character=False) execute_cmd(raw_string) - search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, ignore_errors=False, player=False) + search(ostring, global_search=False, attribute_name=None, + use_nicks=False, location=None, + ignore_errors=False, player=False) is_typeclass(typeclass, exact=False) swap_typeclass(new_typeclass, clean_attributes=False, no_default=True) access(accessing_obj, access_type='read', default=False) @@ -99,15 +107,16 @@ class Player(TypeClass): def msg(self, text=None, from_obj=None, sessid=None, **kwargs): """ Evennia -> User - This is the main route for sending data back to the user from the server. + This is the main route for sending data back to the user from + the server. text (string) - text data to send from_obj (Object/Player) - source object of message to send - sessid - the session id of the session to send to. If not given, return to - all sessions connected to this player. This is usually only - relevant when using msg() directly from a player-command (from - a command on a Character, the character automatically stores and - handles the sessid). + sessid - the session id of the session to send to. If not given, + return to all sessions connected to this player. This is usually only + relevant when using msg() directly from a player-command (from + a command on a Character, the character automatically stores and + handles the sessid). kwargs - extra data to send through protocol """ self.dbobj.msg(text=text, from_obj=from_obj, sessid=sessid, **kwargs) @@ -131,16 +140,18 @@ class Player(TypeClass): Argument: raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the sessid property on the command + sessid (int) - id of session executing the command. This sets the + sessid property on the command Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To overload - this one needs to attach callback functions to it, with addCallback(function). - This function will be called with an eventual return value from the command - execution. + not fire until the command has actually finished executing. To + overload this one needs to attach callback functions to it, with + addCallback(function). This function will be called with an + eventual return value from the command execution. - This return is not used at all by Evennia by default, but might be useful - for coders intending to implement some sort of nested command structure. + This return is not used at all by Evennia by default, but might + be useful for coders intending to implement some sort of nested + command structure. """ return self.dbobj.execute_cmd(raw_string, sessid=sessid) @@ -204,11 +215,13 @@ class Player(TypeClass): """ - self.dbobj.swap_typeclass(new_typeclass, clean_attributes=clean_attributes, no_default=no_default) + self.dbobj.swap_typeclass(new_typeclass, + clean_attributes=clean_attributes, no_default=no_default) def access(self, accessing_obj, access_type='read', default=False): """ - Determines if another object has permission to access this object in whatever way. + Determines if another object has permission to access this object + in whatever way. accessing_obj (Object)- object trying to access this one access_type (string) - type of access sought @@ -221,8 +234,8 @@ class Player(TypeClass): This explicitly checks the given string against this object's 'permissions' property without involving any locks. - permstring (string) - permission string that need to match a permission on the object. - (example: 'Builders') + permstring (string) - permission string that need to match a permission + on the object. (example: 'Builders') """ return self.dbobj.check_permstring(permstring) @@ -307,7 +320,8 @@ class Player(TypeClass): except Exception: logger.log_trace() now = datetime.datetime.now() - now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, now.day, now.hour, now.minute) + now = "%02i-%02i-%02i(%02i:%02i)" % (now.year, now.month, + now.day, now.hour, now.minute) if _CONNECT_CHANNEL: _CONNECT_CHANNEL.tempmsg("[%s, %s]: %s" % (_CONNECT_CHANNEL.key, now, message)) else: @@ -361,15 +375,15 @@ class Player(TypeClass): def at_server_reload(self): """ - This hook is called whenever the server is shutting down for restart/reboot. - If you want to, for example, save non-persistent properties across a restart, - this is the place to do it. + This hook is called whenever the server is shutting down for + restart/reboot. If you want to, for example, save non-persistent + properties across a restart, this is the place to do it. """ pass def at_server_shutdown(self): """ - This hook is called whenever the server is shutting down fully (i.e. not for - a restart). + This hook is called whenever the server is shutting down fully + (i.e. not for a restart). """ pass diff --git a/src/scripts/__init__.py b/src/scripts/__init__.py index 5f15860a10..54d7d20b3f 100644 --- a/src/scripts/__init__.py +++ b/src/scripts/__init__.py @@ -1,12 +1,13 @@ """ -Makes it easier to import by grouping all relevant things already at this level. +Makes it easier to import by grouping all relevant things already at this +level. You can henceforth import most things directly from src.scripts Also, the initiated object manager is available as src.scripts.manager. """ -from src.scripts.scripts import * +from src.scripts.scripts import * from src.scripts.models import ScriptDB manager = ScriptDB.objects diff --git a/src/scripts/admin.py b/src/scripts/admin.py index 89e1413e76..13ff1cbdf7 100644 --- a/src/scripts/admin.py +++ b/src/scripts/admin.py @@ -7,14 +7,17 @@ from src.typeclasses.models import Attribute from src.scripts.models import ScriptDB from django.contrib import admin + class AttributeInline(admin.TabularInline): model = Attribute fields = ('db_key', 'db_value') max_num = 1 + class ScriptDBAdmin(admin.ModelAdmin): - list_display = ('id', 'db_key', 'db_typeclass_path', 'db_obj', 'db_interval', 'db_repeats', 'db_persistent') + list_display = ('id', 'db_key', 'db_typeclass_path', + 'db_obj', 'db_interval', 'db_repeats', 'db_persistent') list_display_links = ('id', 'db_key') ordering = ['db_obj', 'db_typeclass_path'] search_fields = ['^db_key', 'db_typeclass_path'] @@ -25,7 +28,9 @@ class ScriptDBAdmin(admin.ModelAdmin): fieldsets = ( (None, { - 'fields':(('db_key', 'db_typeclass_path'), 'db_interval', 'db_repeats', 'db_start_delay', 'db_persistent', 'db_obj')}), + 'fields': (('db_key', 'db_typeclass_path'), 'db_interval', + 'db_repeats', 'db_start_delay', 'db_persistent', + 'db_obj')}), ) #inlines = [AttributeInline] diff --git a/src/scripts/manager.py b/src/scripts/manager.py index ab48bb191e..5bc41487cb 100644 --- a/src/scripts/manager.py +++ b/src/scripts/manager.py @@ -10,6 +10,7 @@ __all__ = ("ScriptManager",) VALIDATE_ITERATION = 0 + class ScriptManager(TypedObjectManager): """ This Scriptmanager implements methods for searching @@ -82,8 +83,8 @@ class ScriptManager(TypedObjectManager): def remove_non_persistent(self, obj=None): """ This cleans up the script database of all non-persistent - scripts, or only those on obj. It is called every time the server restarts - and + scripts, or only those on obj. It is called every time the server + restarts. """ if obj: to_stop = self.filter(db_obj=obj, db_persistent=False, db_is_active=True) @@ -211,10 +212,11 @@ class ScriptManager(TypedObjectManager): Make an identical copy of the original_script """ typeclass = original_script.typeclass_path - new_key = new_key if new_key!=None else original_script.key - new_obj = new_obj if new_obj!=None else original_script.obj - new_locks = new_locks if new_locks!=None else original_script.db_lock_storage + new_key = new_key if new_key is not None else original_script.key + new_obj = new_obj if new_obj is not None else original_script.obj + new_locks = new_locks if new_locks is not None else original_script.db_lock_storage from src.utils import create - new_script = create.create_script(typeclass, key=new_key, obj=new_obj, locks=new_locks, autostart=True) + new_script = create.create_script(typeclass, key=new_key, obj=new_obj, + locks=new_locks, autostart=True) return new_script diff --git a/src/scripts/models.py b/src/scripts/models.py index c208a271bd..22945ddf9f 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -26,10 +26,9 @@ Common examples of uses of Scripts: """ from django.conf import settings from django.db import models -from django.db.models.signals import post_init, pre_delete -from src.typeclasses.models import Attribute, TypedObject, TagHandler, AttributeHandler#, AliasHandler, NickHandler -from django.contrib.contenttypes.models import ContentType +from src.typeclasses.models import (TypedObject, TagHandler, + AttributeHandler) from src.scripts.manager import ScriptManager __all__ = ("ScriptDB",) diff --git a/src/scripts/scripthandler.py b/src/scripts/scripthandler.py index b91cf48750..bc53f143e2 100644 --- a/src/scripts/scripthandler.py +++ b/src/scripts/scripthandler.py @@ -38,10 +38,13 @@ class ScriptHandler(object): interval = script.interval if script.repeats: repeats = script.repeats - try: next_repeat = script.time_until_next_repeat() - except: next_repeat = "?" + try: + next_repeat = script.time_until_next_repeat() + except: + next_repeat = "?" string += _("\n '%(key)s' (%(next_repeat)s/%(interval)s, %(repeats)s repeats): %(desc)s") % \ - {"key":script.key, "next_repeat":next_repeat, "interval":interval,"repeats":repeats,"desc":script.desc} + {"key": script.key, "next_repeat": next_repeat, + "interval": interval, "repeats": repeats, "desc": script.desc} return string.strip() def add(self, scriptclass, key=None, autostart=True): @@ -51,10 +54,12 @@ class ScriptHandler(object): scriptclass - either a class object inheriting from Script, an instantiated script object or a python path to such a class object. - key - optional identifier for the script (often set in script definition) + key - optional identifier for the script (often set in script + definition) autostart - start the script upon adding it """ - script = create.create_script(scriptclass, key=key, obj=self.obj, autostart=autostart) + script = create.create_script(scriptclass, key=key, obj=self.obj, + autostart=autostart) if not script: logger.log_errmsg("Script %s could not be created and/or started." % scriptclass) return False diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index b150326b70..392233abca 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -5,30 +5,31 @@ scripts are inheriting from. It also defines a few common scripts. """ -from sys import getsizeof from time import time -from collections import defaultdict from twisted.internet.defer import maybeDeferred from twisted.internet.task import LoopingCall from django.conf import settings -from src.server import caches +from django.utils.translation import ugettext as _ from src.typeclasses.typeclass import TypeClass from src.scripts.models import ScriptDB from src.comms import channelhandler -from src.utils import logger, is_pypy -from django.utils.translation import ugettext as _ +from src.utils import logger -__all__ = ["Script", "DoNothing", "CheckSessions", "ValidateScripts", "ValidateChannelHandler"] +__all__ = ["Script", "DoNothing", "CheckSessions", + "ValidateScripts", "ValidateChannelHandler"] _SESSIONS = None -_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE # attr-cache size in MB. +# attr-cache size in MB +_ATTRIBUTE_CACHE_MAXSIZE = settings.ATTRIBUTE_CACHE_MAXSIZE + # # Base script, inherit from Script below instead. # class ScriptClass(TypeClass): """ - Base class for scripts. Don't inherit from this, inherit from Script instead. + Base class for scripts. Don't inherit from this, inherit + from the class 'Script' instead. """ # private methods @@ -53,7 +54,8 @@ class ScriptClass(TypeClass): else: # starting script anew. #print "_start_task: self.interval:", self.key, self.dbobj.interval - self.ndb.twisted_task.start(self.dbobj.interval, now=start_now and not self.start_delay) + self.ndb.twisted_task.start(self.dbobj.interval, + now=start_now and not self.start_delay) self.ndb.time_last_called = int(time()) def _stop_task(self): @@ -64,16 +66,19 @@ class ScriptClass(TypeClass): self.ndb.twisted_task.stop() except Exception: logger.log_trace() + def _step_err_callback(self, e): "callback for runner errors" cname = self.__class__.__name__ estring = _("Script %(key)s(#%(dbid)i) of type '%(cname)s': at_repeat() error '%(err)s'.") % \ - {"key":self.key, "dbid":self.dbid, "cname":cname, "err":e.getErrorMessage()} + {"key": self.key, "dbid": self.dbid, "cname": cname, + "err": e.getErrorMessage()} try: self.dbobj.db_obj.msg(estring) except Exception: pass logger.log_errmsg(estring) + def _step_succ_callback(self): "step task runner. No try..except needed due to defer wrap." if not self.is_valid(): @@ -82,7 +87,7 @@ class ScriptClass(TypeClass): self.at_repeat() repeats = self.dbobj.db_repeats if repeats <= 0: - pass # infinite repeat + pass # infinite repeat elif repeats == 1: self.stop() return @@ -92,14 +97,14 @@ class ScriptClass(TypeClass): self.save() if self.ndb._paused_time: - # this means we were running an unpaused script, for the time remaining - # after the pause. Now we start a normal-running timer again. - #print "switching to normal run:", self.key + # this means we were running an unpaused script, for the + # time remaining after the pause. Now we start a normal-running + # timer again. + # print "switching to normal run:", self.key del self.ndb._paused_time self._stop_task() self._start_task(start_now=False) - def _step_task(self): "step task" try: @@ -109,7 +114,6 @@ class ScriptClass(TypeClass): except Exception: logger.log_trace() - # Public methods def time_until_next_repeat(self): @@ -136,7 +140,8 @@ class ScriptClass(TypeClass): force_restart - if True, will always restart the script, regardless of if it has started before. - returns 0 or 1 to indicated the script has been started or not. Used in counting. + returns 0 or 1 to indicated the script has been started or not. + Used in counting. """ #print "Script %s (%s) start (active:%s, force:%s) ..." % (self.key, id(self.dbobj), # self.is_active, force_restart) @@ -205,7 +210,7 @@ class ScriptClass(TypeClass): """ #print "pausing", self.key, self.time_until_next_repeat() dt = self.time_until_next_repeat() - if dt == None: + if dt is None: return self.db._paused_time = dt self._stop_task() @@ -216,7 +221,7 @@ class ScriptClass(TypeClass): """ #print "unpausing", self.key, self.db._paused_time dt = self.db._paused_time - if dt == None: + if dt is None: return False try: self.dbobj.is_active = True @@ -234,18 +239,23 @@ class ScriptClass(TypeClass): def at_script_creation(self): "placeholder" pass + def is_valid(self): "placeholder" pass + def at_start(self): "placeholder." pass + def at_stop(self): "placeholder" pass + def at_repeat(self): "placeholder" pass + def at_init(self): "called when typeclass re-caches. Usually not used for scripts." pass @@ -263,8 +273,9 @@ class Script(ScriptClass): def __init__(self, dbobj): """ - This is the base TypeClass for all Scripts. Scripts describe events, timers and states in game, - they can have a time component or describe a state that changes under certain conditions. + This is the base TypeClass for all Scripts. Scripts describe events, + timers and states in game, they can have a time component or describe + a state that changes under certain conditions. Script API: @@ -272,57 +283,73 @@ class Script(ScriptClass): key (string) - name of object name (string)- same as key - aliases (list of strings) - aliases to the object. Will be saved to database as AliasDB entries but returned as strings. + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass points back to this class - typeclass (Object, read-only) - this links back to this class as an identified only. Use self.swap_typeclass() to switch. + dbobj (Object, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. date_created (string) - time stamp of object creation permissions (list of strings) - list of permission strings desc (string) - optional description of script, shown in listings - obj (Object) - optional object that this script is connected to and acts on (set automatically by obj.scripts.add()) - interval (int) - how often script should run, in seconds. <=0 turns off ticker - start_delay (bool) - if the script should start repeating right away or wait self.interval seconds - repeats (int) - how many times the script should repeat before stopping. <=0 means infinite repeats + obj (Object) - optional object that this script is connected to + and acts on (set automatically + by obj.scripts.add()) + interval (int) - how often script should run, in seconds. + <=0 turns off ticker + start_delay (bool) - if the script should start repeating right + away or wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. <=0 means infinite repeats persistent (bool) - if script should survive a server shutdown or not is_active (bool) - if script is currently running * Handlers locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not create a database entry when storing data + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data * Helper methods - start() - start script (this usually happens automatically at creation and obj.script.add() etc) + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) stop() - stop script, and delete it - pause() - put the script on hold, until unpause() is called. If script is persistent, the pause state will survive a shutdown. - unpause() - restart a previously paused script. The script will continue as if it was never paused. - time_until_next_repeat() - if a timed script (interval>0), returns time until next tick + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will + continue as if it was never paused. + time_until_next_repeat() - if a timed script (interval>0), returns + time until next tick * Hook methods at_script_creation() - called only once, when an object of this class is first created. is_valid() - is called to check if the script is valid to be running - at the current time. If is_valid() returns False, the running - script is stopped and removed from the game. You can use this - to check state changes (i.e. an script tracking some combat - stats at regular intervals is only valid to run while there is - actual combat going on). - at_start() - Called every time the script is started, which for persistent - scripts is at least once every server start. Note that this is - unaffected by self.delay_start, which only delays the first call - to at_repeat(). - at_repeat() - Called every self.interval seconds. It will be called immediately - upon launch unless self.delay_start is True, which will delay - the first call of this method by self.interval seconds. If - self.interval<=0, this method will never be called. - at_stop() - Called as the script object is stopped and is about to be removed from - the game, e.g. because is_valid() returned False. - at_server_reload() - Called when server reloads. Can be used to save temporary - variables you want should survive a reload. + at the current time. If is_valid() returns False, the + running script is stopped and removed from the game. You + can use this to check state changes (i.e. an script + tracking some combat stats at regular intervals is only + valid to run while there is actual combat going on). + at_start() - Called every time the script is started, which for + persistent scripts is at least once every server start. + Note that this is unaffected by self.delay_start, which + only delays the first call to at_repeat(). + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, + which will delay the first call of this method by + self.interval seconds. If self.interval<=0, this method + will never be called. + at_stop() - Called as the script object is stopped and is about to + be removed from the game, e.g. because is_valid() + returned False or self.stop() was called manually. + at_server_reload() - Called when server reloads. Can be used to save + temporary variables you want should survive a reload. at_server_shutdown() - called at a full server shutdown. @@ -335,7 +362,7 @@ class Script(ScriptClass): """ self.key = "" self.desc = "" - self.interval = 0 # infinite + self.interval = 0 # infinite self.start_delay = False self.repeats = 0 # infinite self.persistent = False @@ -372,29 +399,29 @@ class Script(ScriptClass): def at_server_reload(self): """ - This hook is called whenever the server is shutting down for restart/reboot. - If you want to, for example, save non-persistent properties across a restart, - this is the place to do it. + This hook is called whenever the server is shutting down for + restart/reboot. If you want to, for example, save non-persistent + properties across a restart, this is the place to do it. """ pass def at_server_shutdown(self): """ - This hook is called whenever the server is shutting down fully (i.e. not for - a restart). + This hook is called whenever the server is shutting down fully + (i.e. not for a restart). """ pass - # Some useful default Script types used by Evennia. class DoNothing(Script): "An script that does nothing. Used as default fallback." def at_script_creation(self): - "Setup the script" - self.key = "sys_do_nothing" - self.desc = _("This is an empty placeholder script.") + "Setup the script" + self.key = "sys_do_nothing" + self.desc = _("This is an empty placeholder script.") + class Store(Script): "Simple storage script" @@ -403,6 +430,7 @@ class Store(Script): self.key = "sys_storage" self.desc = _("This is a generic storage container.") + class CheckSessions(Script): "Check sessions regularly." def at_script_creation(self): @@ -421,13 +449,14 @@ class CheckSessions(Script): #print "ValidateSessions run" _SESSIONS.validate_sessions() + class ValidateScripts(Script): "Check script validation regularly" def at_script_creation(self): "Setup the script" self.key = "sys_scripts_validate" self.desc = _("Validates all scripts regularly.") - self.interval = 3600 # validate every hour. + self.interval = 3600 # validate every hour. self.persistent = True def at_repeat(self): @@ -435,13 +464,14 @@ class ValidateScripts(Script): #print "ValidateScripts run." ScriptDB.objects.validate() + class ValidateChannelHandler(Script): "Update the channelhandler to make sure it's in sync." def at_script_creation(self): "Setup the script" self.key = "sys_channels_validate" self.desc = _("Updates the channel handler") - self.interval = 3700 # validate a little later than ValidateScripts + self.interval = 3700 # validate a little later than ValidateScripts self.persistent = True def at_repeat(self): diff --git a/src/server/admin.py b/src/server/admin.py index 32194015b5..3a043dc698 100644 --- a/src/server/admin.py +++ b/src/server/admin.py @@ -1,18 +1,19 @@ -# -# This sets up how models are displayed -# in the web admin interface. -# - -from django.contrib import admin -from src.server.models import ServerConfig - -class ServerConfigAdmin(admin.ModelAdmin): - "Custom admin for server configs" - list_display = ('db_key', 'db_value') - list_display_links = ('db_key',) - ordering = ['db_key', 'db_value'] - search_fields = ['db_key'] - save_as = True - save_on_top = True - list_select_related = True -admin.site.register(ServerConfig, ServerConfigAdmin) +# +# This sets up how models are displayed +# in the web admin interface. +# + +from django.contrib import admin +from src.server.models import ServerConfig + + +class ServerConfigAdmin(admin.ModelAdmin): + "Custom admin for server configs" + list_display = ('db_key', 'db_value') + list_display_links = ('db_key',) + ordering = ['db_key', 'db_value'] + search_fields = ['db_key'] + save_as = True + save_on_top = True + list_select_related = True +admin.site.register(ServerConfig, ServerConfigAdmin) diff --git a/src/server/amp.py b/src/server/amp.py index f6ec7b86e3..e3537f01df 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -1,17 +1,18 @@ """ -Contains the protocols, commands, and client factory needed for the Server and Portal -to communicate with each other, letting Portal work as a proxy. Both sides use this -same protocol. +Contains the protocols, commands, and client factory needed for the Server +and Portal to communicate with each other, letting Portal work as a proxy. +Both sides use this same protocol. The separation works like this: -Portal - (AMP client) handles protocols. It contains a list of connected sessions in a - dictionary for identifying the respective player connected. If it looses the AMP connection - it will automatically try to reconnect. +Portal - (AMP client) handles protocols. It contains a list of connected + sessions in a dictionary for identifying the respective player + connected. If it looses the AMP connection it will automatically + try to reconnect. Server - (AMP server) Handles all mud operations. The server holds its own list - of sessions tied to player objects. This is synced against the portal at startup - and when a session connects/disconnects + of sessions tied to player objects. This is synced against the portal + at startup and when a session connects/disconnects """ @@ -29,16 +30,17 @@ from src.utils.utils import to_str, variable_from_module # communication bits -PCONN = chr(1) # portal session connect -PDISCONN = chr(2) # portal session disconnect -PSYNC = chr(3) # portal session sync -SLOGIN = chr(4) # server session login -SDISCONN = chr(5) # server session disconnect -SDISCONNALL = chr(6) # server session disconnect all -SSHUTD = chr(7) # server shutdown -SSYNC = chr(8) # server sessigon sync +PCONN = chr(1) # portal session connect +PDISCONN = chr(2) # portal session disconnect +PSYNC = chr(3) # portal session sync +SLOGIN = chr(4) # server session login +SDISCONN = chr(5) # server session disconnect +SDISCONNALL = chr(6) # server session disconnect all +SSHUTD = chr(7) # server shutdown +SSYNC = chr(8) # server sessigon sync + +MAXLEN = 65535 # max allowed data length in AMP protocol -MAXLEN = 65535 # max allowed data length in AMP protocol def get_restart_mode(restart_file): """ @@ -49,6 +51,7 @@ def get_restart_mode(restart_file): return flag == "True" return False + class AmpServerFactory(protocol.ServerFactory): """ This factory creates the Server as a new AMPProtocol instance for accepting @@ -71,9 +74,11 @@ class AmpServerFactory(protocol.ServerFactory): self.server.amp_protocol.factory = self return self.server.amp_protocol + class AmpClientFactory(protocol.ReconnectingClientFactory): """ - This factory creates an instance of the Portal, an AMPProtocol instances to use to connect + This factory creates an instance of the Portal, an AMPProtocol + instances to use to connect """ # Initial reconnect delay in seconds. initialDelay = 1 @@ -139,6 +144,7 @@ class MsgPortal2Server(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] + class MsgServer2Portal(amp.Command): """ Message server -> portal @@ -151,6 +157,7 @@ class MsgServer2Portal(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] + class ServerAdmin(amp.Command): """ Portal -> Server @@ -165,6 +172,7 @@ class ServerAdmin(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] + class PortalAdmin(amp.Command): """ Server -> Portal @@ -178,6 +186,7 @@ class PortalAdmin(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] + class FunctionCall(amp.Command): """ Bidirectional @@ -202,6 +211,7 @@ loads = lambda data: pickle.loads(to_str(data)) MSGBUFFER = defaultdict(list) + #------------------------------------------------------------ # Core AMP protocol for communication Server <-> Portal #------------------------------------------------------------ @@ -241,7 +251,8 @@ class AMPProtocol(amp.AMP): def errback(self, e, info): "error handler, to avoid dropping connections on server tracebacks." f = e.trap(Exception) - print "AMP Error for %(info)s: %(e)s" % {'info': info, 'e': e.getErrorMessage()} + print "AMP Error for %(info)s: %(e)s" % {'info': info, + 'e': e.getErrorMessage()} def send_split_msg(self, sessid, msg, data, command): """ @@ -258,14 +269,16 @@ class AMPProtocol(amp.AMP): datastr = dumps(data) nmsg, ndata = len(msg), len(datastr) if nmsg > MAXLEN or ndata > MAXLEN: - msglist = [msg[i:i+MAXLEN] for i in range(0, len(msg), MAXLEN)] - datalist = [datastr[i:i+MAXLEN] for i in range(0, len(datastr), MAXLEN)] + msglist = [msg[i:i + MAXLEN] for i in range(0, len(msg), MAXLEN)] + datalist = [datastr[i:i + MAXLEN] + for i in range(0, len(datastr), MAXLEN)] nmsglist, ndatalist = len(msglist), len(datalist) if ndatalist < nmsglist: - datalist.extend("" for i in range(nmsglist-ndatalist)) + datalist.extend("" for i in range(nmsglist - ndatalist)) if nmsglist < ndatalist: - msglist.extend("" for i in range(ndatalist-nmsglist)) - # we have split the msg/data into right-size chunks. Now we send it in sequence + msglist.extend("" for i in range(ndatalist - nmsglist)) + # we have split the msg/data into right-size chunks. Now we + # send it in sequence return [self.callRemote(command, sessid=sessid, msg=to_str(msg), @@ -295,8 +308,8 @@ class AMPProtocol(amp.AMP): return {} else: # we have all parts. Put it all together in the right order. - msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0])) - data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0])) + msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0])) + data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0])) del MSGBUFFER[sessid] # call session hook with the data self.factory.server.sessions.data_in(sessid, text=msg, **loads(data)) @@ -311,13 +324,14 @@ class AMPProtocol(amp.AMP): try: return self.callRemote(MsgPortal2Server, sessid=sessid, - msg=to_str(msg) if msg!=None else "", + msg=to_str(msg) if msg is not None else "", ipart=0, nparts=1, data=dumps(data)).addErrback(self.errback, "MsgPortal2Server") except amp.TooLong: - # the msg (or data) was too long for AMP to send. We need to send in blocks. - return self.send_split_msg(sessid, msg, kwargs, MsgPortal2Server) + # the msg (or data) was too long for AMP to send. + # We need to send in blocks. + return self.send_split_msg(sessid, msg, data, MsgPortal2Server) # Server -> Portal message @@ -335,8 +349,8 @@ class AMPProtocol(amp.AMP): return {} else: # we have all parts. Put it all together in the right order. - msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0])) - data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o:o[0])) + msg = "".join(t[1] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0])) + data = "".join(t[2] for t in sorted(MSGBUFFER[sessid], key=lambda o: o[0])) del MSGBUFFER[sessid] # call session hook with the data self.factory.portal.sessions.data_out(sessid, text=msg, **loads(data)) @@ -351,14 +365,14 @@ class AMPProtocol(amp.AMP): try: return self.callRemote(MsgServer2Portal, sessid=sessid, - msg=to_str(msg) if msg!=None else "", + msg=to_str(msg) if msg is not None else "", ipart=0, nparts=1, data=dumps(data)).addErrback(self.errback, "MsgServer2Portal") except amp.TooLong: - # the msg (or data) was too long for AMP to send. We need to send in blocks. - return self.send_split_msg(sessid, msg, kwargs, MsgServer2Portal) - + # the msg (or data) was too long for AMP to send. + # We need to send in blocks. + return self.send_split_msg(sessid, msg, data, MsgServer2Portal) # Server administration from the Portal side def amp_server_admin(self, sessid, operation, data): @@ -372,17 +386,18 @@ class AMPProtocol(amp.AMP): #print "serveradmin (server side):", sessid, ord(operation), data - if operation == PCONN: #portal_session_connect + if operation == PCONN: # portal_session_connect # create a new session and sync it server_sessionhandler.portal_connect(data) - elif operation == PDISCONN: #'portal_session_disconnect' + elif operation == PDISCONN: # portal_session_disconnect # session closed from portal side self.factory.server.sessions.portal_disconnect(sessid) - elif operation == PSYNC: #'portal_session_sync' - # force a resync of sessions when portal reconnects to server (e.g. after a server reboot) - # the data kwarg contains a dict {sessid: {arg1:val1,...}} representing the attributes + elif operation == PSYNC: # portal_session_sync + # force a resync of sessions when portal reconnects to server + # (e.g. after a server reboot) the data kwarg contains a dict + # {sessid: {arg1:val1,...}} representing the attributes # to sync for each session. server_sessionhandler.portal_session_sync(data) else: @@ -414,23 +429,23 @@ class AMPProtocol(amp.AMP): portal_sessionhandler = self.factory.portal.sessions #print "portaladmin (portal side):", sessid, ord(operation), data - if operation == SLOGIN: # 'server_session_login' + if operation == SLOGIN: # server_session_login # a session has authenticated; sync it. portal_sessionhandler.server_logged_in(sessid, data) - elif operation == SDISCONN: #'server_session_disconnect' + elif operation == SDISCONN: # server_session_disconnect # the server is ordering to disconnect the session portal_sessionhandler.server_disconnect(sessid, reason=data) - elif operation == SDISCONNALL: #'server_session_disconnect_all' + elif operation == SDISCONNALL: # server_session_disconnect_all # server orders all sessions to disconnect portal_sessionhandler.server_disconnect_all(reason=data) - elif operation == SSHUTD: #server_shutdown' + elif operation == SSHUTD: # server_shutdown # the server orders the portal to shut down self.factory.portal.shutdown(restart=False) - elif operation == SSYNC: #'server_session_sync' + elif operation == SSYNC: # server_session_sync # server wants to save session data to the portal, maybe because # it's about to shut down. portal_sessionhandler.server_session_sync(data) @@ -465,25 +480,28 @@ class AMPProtocol(amp.AMP): result = variable_from_module(module, function)(*args, **kwargs) if isinstance(result, Deferred): - # if result is a deferred, attach handler to properly wrap the return value - result.addCallback(lambda r: {"result":dumps(r)}) + # if result is a deferred, attach handler to properly + # wrap the return value + result.addCallback(lambda r: {"result": dumps(r)}) return result else: - return {'result':dumps(result)} + return {'result': dumps(result)} FunctionCall.responder(amp_function_call) - def call_remote_FunctionCall(self, modulepath, functionname, *args, **kwargs): """ - Access method called by either process. This will call an arbitrary function - on the other process (On Portal if calling from Server and vice versa). + Access method called by either process. This will call an arbitrary + function on the other process (On Portal if calling from Server and + vice versa). Inputs: modulepath (str) - python path to module holding function to call functionname (str) - name of function in given module - *args, **kwargs will be used as arguments/keyword args for the remote function call + *args, **kwargs will be used as arguments/keyword args for the + remote function call Returns: - A deferred that fires with the return value of the remote function call + A deferred that fires with the return value of the remote + function call """ return self.callRemote(FunctionCall, module=modulepath, diff --git a/src/server/caches.py b/src/server/caches.py index c1ea0086d6..f604c50c7b 100644 --- a/src/server/caches.py +++ b/src/server/caches.py @@ -4,10 +4,10 @@ Central caching module. """ from sys import getsizeof -import os, threading +import os +import threading from collections import defaultdict -from django.core.cache import get_cache from src.server.models import ServerConfig from src.utils.utils import uses_database, to_str, get_evennia_pids @@ -35,6 +35,7 @@ if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6 else: _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f" + def hashid(obj, suffix=""): """ Returns a per-class unique hash that combines the object's @@ -59,11 +60,15 @@ def hashid(obj, suffix=""): # rely on memory adressing in this case. date, idnum = "InMemory", id(obj) if not idnum or not date: - # this will happen if setting properties on an object which is not yet saved + # this will happen if setting properties on an object which + # is not yet saved return None + # we have to remove the class-name's space, for eventual use + # of memcached hid = "%s-%s-#%s" % (_GA(obj, "__class__"), date, idnum) - hid = hid.replace(" ", "") # we have to remove the class-name's space, for memcached's sake - # we cache the object part of the hashid to avoid too many object lookups + hid = hid.replace(" ", "") + # we cache the object part of the hashid to avoid too many + # object lookups _SA(obj, "_hashid", hid) # build the complete hashid hid = "%s%s" % (hid, suffix) @@ -84,8 +89,9 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg """ Called at the beginning of the field save operation. The save method must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this function. - Its main purpose is to allow to plug-in a save handler and oob handlers. + This method should NOT save; rather it is the save() that triggers this + function. Its main purpose is to allow to plug-in a save handler and oob + handlers. """ if raw: return @@ -102,12 +108,14 @@ def field_pre_save(sender, instance=None, update_fields=None, raw=False, **kwarg if callable(handler): handler() + def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwargs): """ Called at the beginning of the field save operation. The save method must be called with the update_fields keyword in order to be most efficient. - This method should NOT save; rather it is the save() that triggers this function. - Its main purpose is to allow to plug-in a save handler and oob handlers. + This method should NOT save; rather it is the save() that triggers this + function. Its main purpose is to allow to plug-in a save handler and oob + handlers. """ if raw: return @@ -127,70 +135,6 @@ def field_post_save(sender, instance=None, update_fields=None, raw=False, **kwar if trackerhandler: trackerhandler.update(fieldname, _GA(instance, fieldname)) -#------------------------------------------------------------ -# Attr cache - caching the attribute objects related to a given object to -# avoid lookups more than necessary (this makes Attributes en par in speed -# to any property). -#------------------------------------------------------------ - -## connected to m2m_changed signal in respective model class -#def post_attr_update(sender, **kwargs): -# "Called when the many2many relation changes (NOT when updating the value of an Attribute!)" -# obj = kwargs['instance'] -# model = kwargs['model'] -# action = kwargs['action'] -# if kwargs['reverse']: -# # the reverse relation changed (the Attribute itself was acted on) -# pass -# else: -# # forward relation changed (the Object holding the Attribute m2m field) -# if not kwargs["pk_set"]: -# return -# if action == "post_add": -# # cache all added objects -# for attr_id in kwargs["pk_set"]: -# attr_obj = model.objects.get(pk=attr_id) -# set_attr_cache(obj, _GA(attr_obj, "db_key"), attr_obj) -# elif action == "post_remove": -# # obj.db_attributes.remove(attr) was called -# for attr_id in kwargs["pk_set"]: -# attr_obj = model.objects.get(pk=attr_id) -# del_attr_cache(obj, _GA(attr_obj, "db_key")) -# attr_obj.delete() -# elif action == "post_clear": -# # obj.db_attributes.clear() was called -# clear_obj_attr_cache(obj) -# -# -## attr cache - this is only left as deprecated cache -# -#def get_attr_cache(obj, attrname): -# "Called by getting attribute" -# hid = hashid(obj, "-%s" % attrname) -# return _ATTR_CACHE.get(hid, None) -# -#def set_attr_cache(obj, attrname, attrobj): -# "Set the attr cache manually; this can be used to update" -# global _ATTR_CACHE -# hid = hashid(obj, "-%s" % attrname) -# _ATTR_CACHE[hid] = attrobj -# -#def del_attr_cache(obj, attrname): -# "Del attribute cache" -# global _ATTR_CACHE -# hid = hashid(obj, "-%s" % attrname) -# if hid in _ATTR_CACHE: -# del _ATTR_CACHE[hid] -# -#def flush_attr_cache(): -# "Clear attribute cache" -# global _ATTR_CACHE -# _ATTR_CACHE = {} -# -#def clear_obj_attr_cache(obj): -# global _ATTR_CACHE -# hid = hashid(obj) -# _ATTR_CACHE = {key:value for key, value in _ATTR_CACHE if not key.startswith(hid)} #------------------------------------------------------------ # Property cache - this is a generic cache for properties stored on models. @@ -203,12 +147,14 @@ def get_prop_cache(obj, propname): hid = hashid(obj, "-%s" % propname) return _PROP_CACHE[hid].get(propname, None) if hid else None + def set_prop_cache(obj, propname, propvalue): "Set property cache" hid = hashid(obj, "-%s" % propname) if hid: _PROP_CACHE[hid][propname] = propvalue + def del_prop_cache(obj, propname): "Delete element from property cache" hid = hashid(obj, "-%s" % propname) @@ -216,11 +162,12 @@ def del_prop_cache(obj, propname): if propname in _PROP_CACHE[hid]: del _PROP_CACHE[hid][propname] + def flush_prop_cache(): "Clear property cache" global _PROP_CACHE _PROP_CACHE = defaultdict(dict) - #_PROP_CACHE.clear() + def get_cache_sizes(): """ @@ -229,8 +176,8 @@ def get_cache_sizes(): global _ATTR_CACHE, _PROP_CACHE attr_n = len(_ATTR_CACHE) attr_mb = sum(getsizeof(obj) for obj in _ATTR_CACHE) / 1024.0 - field_n = 0 #sum(len(dic) for dic in _FIELD_CACHE.values()) - field_mb = 0 # sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _FIELD_CACHE.values()) / 1024.0 + field_n = 0 # sum(len(dic) for dic in _FIELD_CACHE.values()) + field_mb = 0 # sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _FIELD_CACHE.values()) / 1024.0 prop_n = sum(len(dic) for dic in _PROP_CACHE.values()) prop_mb = sum(sum([getsizeof(obj) for obj in dic.values()]) for dic in _PROP_CACHE.values()) / 1024.0 return (attr_n, attr_mb), (field_n, field_mb), (prop_n, prop_mb) diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index b6f3f73ec3..63b75897a7 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -7,16 +7,13 @@ Everything starts at handle_setup() """ import django -from django.core import management from django.conf import settings from django.contrib.auth import get_user_model from src.server.models import ServerConfig -from src.help.models import HelpEntry from src.utils import create - - from django.utils.translation import ugettext as _ + def create_config_values(): """ Creates the initial config values. @@ -24,6 +21,7 @@ def create_config_values(): ServerConfig.objects.conf("site_name", settings.SERVERNAME) ServerConfig.objects.conf("idle_timeout", settings.IDLE_TIMEOUT) + def get_god_player(): """ Creates the god user. @@ -32,13 +30,15 @@ def get_god_player(): try: god_player = PlayerDB.objects.get(id=1) except PlayerDB.DoesNotExist: - txt = "\n\nNo superuser exists yet. The superuser is the 'owner' account on the" - txt += "\nEvennia server. Create a new superuser using the command" + txt = "\n\nNo superuser exists yet. The superuser is the 'owner'" + txt += "\account on the Evennia server. Create a new superuser using" + txt += "\nthe command" txt += "\n\n python manage.py createsuperuser" txt += "\n\nFollow the prompts, then restart the server." raise Exception(txt) return god_player + def create_objects(): """ Creates the #1 player and Limbo room. @@ -54,18 +54,23 @@ def create_objects(): # mud-specific settings for the PlayerDB object. player_typeclass = settings.BASE_PLAYER_TYPECLASS - # run all creation hooks on god_player (we must do so manually since the manage.py command does not) + # run all creation hooks on god_player (we must do so manually + # since the manage.py command does not) god_player.typeclass_path = player_typeclass god_player.basetype_setup() god_player.at_player_creation() god_player.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all()") - god_player.permissions.add("Immortals") # this is necessary for quelling to work correctly. + # this is necessary for quelling to work correctly. + god_player.permissions.add("Immortals") # Limbo is the default "nowhere" starting room - # Create the in-game god-character for player #1 and set it to exist in Limbo. + # Create the in-game god-character for player #1 and set + # it to exist in Limbo. character_typeclass = settings.BASE_CHARACTER_TYPECLASS - god_character = create.create_object(character_typeclass, key=god_player.username, nohome=True) + god_character = create.create_object(character_typeclass, + key=god_player.username, nohome=True) + print "god_character:", character_typeclass, god_character, god_character.cmdset.all() god_character.id = 1 god_character.db.desc = _('This is User #1.') @@ -81,10 +86,13 @@ def create_objects(): limbo_obj = create.create_object(room_typeclass, _('Limbo'), nohome=True) limbo_obj.id = 2 string = " ".join([ - "Welcome to your new {wEvennia{n-based game. From here you are ready to begin development.", - "Visit http://evennia.com if you should need help or would like to participate in community discussions.", - "If you are logged in as User #1 you can create a demo/tutorial area with '@batchcommand contrib.tutorial_world.build'.", - "Log out and create a new non-admin account at the login screen to play the tutorial properly."]) + "Welcome to your new {wEvennia{n-based game. From here you are ready", + "to begin development. Visit http://evennia.com if you should need", + "help or would like to participate in community discussions. If you", + "are logged in as User #1 you can create a demo/tutorial area with", + "'@batchcommand contrib.tutorial_world.build'. Log out and create", + "a new non-admin account at the login screen to play the tutorial", + "properly."]) string = _(string) limbo_obj.db.desc = string limbo_obj.save() @@ -96,6 +104,7 @@ def create_objects(): if not god_character.home: god_character.home = limbo_obj + def create_channels(): """ Creates some sensible default channels. @@ -112,20 +121,21 @@ def create_channels(): key3, aliases, desc, locks = settings.CHANNEL_CONNECTINFO cchan = create.create_channel(key3, aliases, desc, locks=locks) - - # TODO: postgresql-psycopg2 has a strange error when trying to connect the user - # to the default channels. It works fine from inside the game, but not from - # the initial startup. We are temporarily bypassing the problem with the following - # fix. See Evennia Issue 151. - if ((".".join(str(i) for i in django.VERSION) < "1.2" and settings.DATABASE_ENGINE == "postgresql_psycopg2") + # TODO: postgresql-psycopg2 has a strange error when trying to + # connect the user to the default channels. It works fine from inside + # the game, but not from the initial startup. We are temporarily bypassing + # the problem with the following fix. See Evennia Issue 151. + if ((".".join(str(i) for i in django.VERSION) < "1.2" + and settings.DATABASE_ENGINE == "postgresql_psycopg2") or (hasattr(settings, 'DATABASES') and settings.DATABASES.get("default", {}).get('ENGINE', None) == 'django.db.backends.postgresql_psycopg2')): warning = """ PostgreSQL-psycopg2 compatability fix: The in-game channels %s, %s and %s were created, - but the superuser was not yet connected to them. Please use in-game commands to - connect Player #1 to those channels when first logging in. + but the superuser was not yet connected to them. Please use in + game commands to onnect Player #1 to those channels when first + logging in. """ % (key1, key2, key3) print warning return @@ -137,6 +147,7 @@ def create_channels(): PlayerChannelConnection.objects.create_connection(goduser, ichan) PlayerChannelConnection.objects.create_connection(goduser, cchan) + def create_system_scripts(): """ Setup the system repeat scripts. They are automatically started @@ -152,11 +163,10 @@ def create_system_scripts(): script2 = create.create_script(scripts.ValidateScripts) # update the channel handler to make sure it's in sync script3 = create.create_script(scripts.ValidateChannelHandler) - # clear the attribute cache regularly - #script4 = create.create_script(scripts.ClearAttributeCache) - if not script1 or not script2 or not script3:# or not script4: + if not script1 or not script2 or not script3: print " Error creating system scripts." + def start_game_time(): """ This starts a persistent script that keeps track of the @@ -168,6 +178,7 @@ def start_game_time(): from src.utils import gametime gametime.init_gametime() + def create_admin_media_links(): """ This traverses to src/web/media and tries to create a symbolic @@ -179,7 +190,8 @@ def create_admin_media_links(): since the django install may be at different locations depending on system. """ - import django, os + import django + import os if django.get_version() < 1.4: dpath = os.path.join(django.__path__[0], 'contrib', 'admin', 'media') @@ -202,6 +214,7 @@ def create_admin_media_links(): else: print " Admin-media files should be copied manually to ADMIN_MEDIA_ROOT." + def at_initial_setup(): """ Custom hook for users to overload some or all parts of the initial @@ -220,6 +233,7 @@ def at_initial_setup(): if mod.__dict__.get("at_initial_setup", None): mod.at_initial_setup() + def reset_server(): """ We end the initialization by resetting the server. This @@ -231,6 +245,7 @@ def reset_server(): print " Initial setup complete. Restarting Server once." SESSIONS.server.shutdown(mode='reset') + def handle_setup(last_step): """ Main logic for the module. It allows for restarting @@ -242,7 +257,7 @@ def handle_setup(last_step): # this means we don't need to handle setup since # it already ran sucessfully once. return - elif last_step == None: + elif last_step is None: # config doesn't exist yet. First start of server last_step = 0 diff --git a/src/server/manager.py b/src/server/manager.py index cdd3821433..527f727775 100644 --- a/src/server/manager.py +++ b/src/server/manager.py @@ -3,6 +3,7 @@ Custom manager for ServerConfig objects. """ from django.db import models + class ServerConfigManager(models.Manager): """ This ServerConfigManager implements methods for searching @@ -24,16 +25,16 @@ class ServerConfigManager(models.Manager): """ if not key: return self.all() - elif delete == True: + elif delete is True: for conf in self.filter(db_key=key): conf.delete() - elif value != None: + elif value is not None: conf = self.filter(db_key=key) if conf: conf = conf[0] else: conf = self.model(db_key=key) - conf.value = value # this will pickle + conf.value = value # this will pickle else: conf = self.filter(db_key=key) if not conf: @@ -42,7 +43,8 @@ class ServerConfigManager(models.Manager): def get_mysql_db_version(self): """ - This is a helper method for getting the version string of a mysql database. + This is a helper method for getting the version string + of a mysql database. """ from django.db import connection conn = connection.cursor() diff --git a/src/server/models.py b/src/server/models.py index da4ae08016..b2093e43c9 100644 --- a/src/server/models.py +++ b/src/server/models.py @@ -18,6 +18,7 @@ from src.utils.idmapper.models import SharedMemoryModel from src.utils import logger, utils from src.server.manager import ServerConfigManager + #------------------------------------------------------------ # # ServerConfig @@ -61,11 +62,13 @@ class ServerConfig(SharedMemoryModel): def __key_get(self): "Getter. Allows for value = self.key" return self.db_key + #@key.setter def __key_set(self, value): "Setter. Allows for self.key = value" self.db_key = value self.save() + #@key.deleter def __key_del(self): "Deleter. Allows for del self.key. Deletes entry." @@ -77,6 +80,7 @@ class ServerConfig(SharedMemoryModel): def __value_get(self): "Getter. Allows for value = self.value" return pickle.loads(str(self.db_value)) + #@value.setter def __value_set(self, value): "Setter. Allows for self.value = value" @@ -86,6 +90,7 @@ class ServerConfig(SharedMemoryModel): return self.db_value = pickle.dumps(value) self.save() + #@value.deleter def __value_del(self): "Deleter. Allows for del self.value. Deletes entry." diff --git a/src/server/oob_msdp.py b/src/server/oob_msdp.py index 30cd5cc8a9..3abbfb7387 100644 --- a/src/server/oob_msdp.py +++ b/src/server/oob_msdp.py @@ -8,13 +8,14 @@ from django.conf import settings from src.utils.utils import to_str _GA = object.__getattribute__ _SA = object.__setattr__ -_NA = lambda o: (None, "N/A") # not implemented +_NA = lambda o: (None, "N/A") # not implemented -# mapper for which properties may be requested/sent to the client and how to do so. -# Each entry should define a function that returns two values - the name of the -# propertye being returned (a string) and the value. If tracking database fields, -# make sure to enter the full database field name (e.g. db_key rather than just key) -# since the db_ prefix is used by trackers to know which tracking mechanism to activate. +# mapper for which properties may be requested/sent to the client and how +# to do so. Each entry should define a function that returns two values - the +# name of the property being returned (a string) and the value. If tracking +# database fields, make sure to enter the full database field name (e.g. +# db_key rather than just key) since the db_ prefix is used by trackers +# to know which tracking mechanism to activate. OOB_SENDABLE = { ## General @@ -97,13 +98,16 @@ class TrackerBase(object): """ def __init__(self, oobhandler, *args, **kwargs): self.oobhandler = oobhandler + def update(self, *args, **kwargs): "Called by tracked objects" pass + def at_remove(self, *args, **kwargs): "Called when tracker is removed" pass + class OOBFieldTracker(TrackerBase): """ Tracker that passively sends data to a stored sessid whenever @@ -127,7 +131,9 @@ class OOBFieldTracker(TrackerBase): new_value = new_value.key except AttributeError: new_value = to_str(new_value, force_string=True) - self.oobhandler.msg(self.sessid, "report", self.fieldname, new_value, *args, **kwargs) + self.oobhandler.msg(self.sessid, "report", self.fieldname, + new_value, *args, **kwargs) + class OOBAttributeTracker(TrackerBase): """ @@ -136,13 +142,13 @@ class OOBAttributeTracker(TrackerBase): we instead store the name of the attribute to return. """ def __init__(self, oobhandler, fieldname, sessid, attrname, *args, **kwargs): - """ - attrname - name of attribute to track - sessid - sessid of session to report to - """ - self.oobhandler = oobhandler - self.attrname = attrname - self.sessid = sessid + """ + attrname - name of attribute to track + sessid - sessid of session to report to + """ + self.oobhandler = oobhandler + self.attrname = attrname + self.sessid = sessid def update(self, new_value, *args, **kwargs): "Called by cache when attribute's db_value field updates" @@ -152,6 +158,7 @@ class OOBAttributeTracker(TrackerBase): new_value = to_str(new_value, force_string=True) self.oobhandler.msg(self.sessid, "report", self.attrname, new_value, *args, **kwargs) + #------------------------------------------------------------ # OOB commands # This defines which internal server commands the OOB handler @@ -173,31 +180,51 @@ def oob_error(oobhandler, session, errmsg, *args, **kwargs): occurs already at the execution stage (such as the oob function not being recognized or having the wrong args etc). """ - session.msg(oob=("send", {"ERROR":errmsg})) + session.msg(oob=("send", {"ERROR": errmsg})) + def LIST(oobhandler, session, mode, *args, **kwargs): """ List available properties. Mode is the type of information desired: - "COMMANDS" Request an array of commands supported by the server. - "LISTS" Request an array of lists supported by the server. - "CONFIGURABLE_VARIABLES" Request an array of variables the client can configure. - "REPORTABLE_VARIABLES" Request an array of variables the server will report. - "REPORTED_VARIABLES" Request an array of variables currently being reported. - "SENDABLE_VARIABLES" Request an array of variables the server will send. + "COMMANDS" Request an array of commands supported + by the server. + "LISTS" Request an array of lists supported + by the server. + "CONFIGURABLE_VARIABLES" Request an array of variables the client + can configure. + "REPORTABLE_VARIABLES" Request an array of variables the server + will report. + "REPORTED_VARIABLES" Request an array of variables currently + being reported. + "SENDABLE_VARIABLES" Request an array of variables the server + will send. """ mode = mode.upper() - # the first return argument is treated by the msdp protocol as the name of the msdp array to return + # the first return argument is treated by the msdp protocol as the + # name of the msdp array to return if mode == "COMMANDS": - session.msg(oob=("list", ("COMMANDS", "LIST", "REPORT", "UNREPORT", "SEND"))) # RESET + session.msg(oob=("list", ("COMMANDS", + "LIST", + "REPORT", + "UNREPORT", + # "RESET", + "SEND"))) elif mode == "LISTS": - session.msg(oob=("list", ("LISTS", "REPORTABLE_VARIABLES","REPORTED_VARIABLES", "SENDABLE_VARIABLES"))) #CONFIGURABLE_VARIABLES + session.msg(oob=("list", ("LISTS", + "REPORTABLE_VARIABLES", + "REPORTED_VARIABLES", + # "CONFIGURABLE_VARIABLES", + "SENDABLE_VARIABLES"))) elif mode == "REPORTABLE_VARIABLES": - session.msg(oob=("list", ("REPORTABLE_VARIABLES",) + tuple(key for key in OOB_REPORTABLE.keys()))) + session.msg(oob=("list", ("REPORTABLE_VARIABLES",) + + tuple(key for key in OOB_REPORTABLE.keys()))) elif mode == "REPORTED_VARIABLES": - session.msg(oob=("list", ("REPORTED_VARIABLES",) + tuple(oobhandler.get_all_tracked(session)))) + session.msg(oob=("list", ("REPORTED_VARIABLES",) + + tuple(oobhandler.get_all_tracked(session)))) elif mode == "SENDABLE_VARIABLES": - session.msg(oob=("list", ("SENDABLE_VARIABLES",) + tuple(key for key in OOB_REPORTABLE.keys()))) + session.msg(oob=("list", ("SENDABLE_VARIABLES",) + + tuple(key for key in OOB_REPORTABLE.keys()))) #elif mode == "CONFIGURABLE_VARIABLES": # pass else: @@ -221,6 +248,7 @@ def send(oobhandler, session, *args, **kwargs): # return result session.msg(oob=("send", ret)) + def report(oobhandler, session, *args, **kwargs): """ This creates a tracker instance to track the data given in *args. @@ -232,9 +260,12 @@ def report(oobhandler, session, *args, **kwargs): key, val = OOB_REPORTABLE.get(name, _NA)(obj) if key: if key.startswith("db_"): - oobhandler.track_field(obj, session.sessid, key, OOBFieldTracker) - else: # assume attribute - oobhandler.track_attribute(obj, session.sessid, key, OOBAttributeTracker) + oobhandler.track_field(obj, session.sessid, + key, OOBFieldTracker) + else: # assume attribute + oobhandler.track_attribute(obj, session.sessid, + key, OOBAttributeTracker) + def unreport(oobhandler, session, vartype="prop", *args, **kwargs): """ @@ -248,6 +279,6 @@ def unreport(oobhandler, session, vartype="prop", *args, **kwargs): if key: if key.startswith("db_"): oobhandler.untrack_field(obj, session.sessid, key) - else: # assume attribute + else: # assume attribute oobhandler.untrack_attribute(obj, session.sessid, key) diff --git a/src/server/oobhandler.py b/src/server/oobhandler.py index 2dd062beac..b92865f6a2 100644 --- a/src/server/oobhandler.py +++ b/src/server/oobhandler.py @@ -5,14 +5,16 @@ The OOBHandler is called directly by out-of-band protocols. It supplies three pieces of functionality: function execution - the oob protocol can execute a function directly on - the server. Only functions specified in settings.OOB_PLUGIN_MODULE.OOB_FUNCS - are valid for this use. - repeat func execution - the oob protocol can request a given function be executed repeatedly - at a regular interval. - tracking - the oob protocol can request Evennia to track changes to fields on - objects, as well as changes in Attributes. This is done by dynamically adding - tracker-objects on entities. The behaviour of those objects can be customized - via settings.OOB_PLUGIN_MODULE + the server. Only functions specified in + settings.OOB_PLUGIN_MODULE.OOB_FUNCS are valid + for this use. + repeat func execution - the oob protocol can request a given function be + executed repeatedly at a regular interval. + tracking - the oob protocol can request Evennia to track changes to + fields on objects, as well as changes in Attributes. This is + done by dynamically adding tracker-objects on entities. The + behaviour of those objects can be customized via + settings.OOB_PLUGIN_MODULE oob functions have the following call signature: function(caller, *args, **kwargs) @@ -30,7 +32,7 @@ from src.scripts.scripts import Script from src.utils.create import create_script from src.utils.dbserialize import dbserialize, dbunserialize, pack_dbobj, unpack_dbobj from src.utils import logger -from src.utils.utils import all_from_module, to_str, is_iter, make_iter +from src.utils.utils import all_from_module _SA = object.__setattr__ _GA = object.__getattribute__ @@ -50,14 +52,18 @@ class TrackerHandler(object): """ def __init__(self, obj): """ - This is initiated and stored on the object as a property _trackerhandler. + This is initiated and stored on the object as a + property _trackerhandler. """ - try: obj = obj.dbobj - except AttributeError: pass + try: + obj = obj.dbobj + except AttributeError: + pass self.obj = obj self.ntrackers = 0 # initiate store only with valid on-object fieldnames - self.tracktargets = dict((key, {}) for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) + self.tracktargets = dict((key, {}) + for key in _GA(_GA(self.obj, "_meta"), "get_all_field_names")()) def add(self, fieldname, tracker): """ @@ -95,19 +101,23 @@ class TrackerHandler(object): except Exception: logger.log_trace() + class TrackerBase(object): """ Base class for OOB Tracker objects. """ def __init__(self, *args, **kwargs): pass + def update(self, *args, **kwargs): "Called by tracked objects" pass + def at_remove(self, *args, **kwargs): "Called when tracker is removed" pass + class _RepeaterScript(Script): """ Repeating and subscription-enabled script for triggering OOB @@ -117,7 +127,7 @@ class _RepeaterScript(Script): "Called when script is initialized" self.key = "oob_func" self.desc = "OOB functionality script" - self.persistent = False #oob scripts should always be non-persistent + self.persistent = False # oob scripts should always be non-persistent self.ndb.subscriptions = {} def at_repeat(self): @@ -142,11 +152,12 @@ class _RepeaterScript(Script): """ self.ndb.subscriptions.pop(store_key, None) + class _RepeaterPool(object): """ - This maintains a pool of _RepeaterScript scripts, ordered one per interval. It - will automatically cull itself once a given interval's script has no more - subscriptions. + This maintains a pool of _RepeaterScript scripts, ordered one per + interval. It will automatically cull itself once a given interval's + script has no more subscriptions. This is used and accessed from oobhandler.repeat/unrepeat """ @@ -160,9 +171,11 @@ class _RepeaterPool(object): """ if interval not in self.scripts: # if no existing interval exists, create new script to fill the gap - new_tracker = create_script(_RepeaterScript, key="oob_repeater_%is" % interval, interval=interval) + new_tracker = create_script(_RepeaterScript, + key="oob_repeater_%is" % interval, interval=interval) self.scripts[interval] = new_tracker - self.scripts[interval].subscribe(store_key, sessid, func_key, interval, *args, **kwargs) + self.scripts[interval].subscribe(store_key, sessid, func_key, + interval, *args, **kwargs) def remove(self, store_key, interval): """ @@ -176,8 +189,8 @@ class _RepeaterPool(object): def stop(self): """ - Stop all scripts in pool. This is done at server reload since restoring the pool - will automatically re-populate the pool. + Stop all scripts in pool. This is done at server reload since + restoring the pool will automatically re-populate the pool. """ for script in self.scripts.values(): script.stop() @@ -188,8 +201,8 @@ class _RepeaterPool(object): class OOBHandler(object): """ The OOBHandler maintains all dynamic on-object oob hooks. It will store the - creation instructions and and re-apply them at a server reload (but not after - a server shutdown) + creation instructions and and re-apply them at a server reload (but + not after a server shutdown) """ def __init__(self): """ @@ -207,10 +220,12 @@ class OOBHandler(object): """ if self.oob_tracker_storage: #print "saved tracker_storage:", self.oob_tracker_storage - ServerConfig.objects.conf(key="oob_tracker_storage", value=dbserialize(self.oob_tracker_storage)) + ServerConfig.objects.conf(key="oob_tracker_storage", + value=dbserialize(self.oob_tracker_storage)) if self.oob_repeat_storage: #print "saved repeat_storage:", self.oob_repeat_storage - ServerConfig.objects.conf(key="oob_repeat_storage", value=dbserialize(self.oob_repeat_storage)) + ServerConfig.objects.conf(key="oob_repeat_storage", + value=dbserialize(self.oob_repeat_storage)) self.oob_tracker_pool.stop() def restore(self): @@ -242,12 +257,14 @@ class OOBHandler(object): Create an OOB obj of class _oob_MAPPING[tracker_key] on obj. args, kwargs will be used to initialize the OOB hook before adding it to obj. - If property_key is not given, but the OOB has a class property property_name, this - will be used as the property name when assigning the OOB to - obj, otherwise tracker_key is used as the property name. + If property_key is not given, but the OOB has a class property + property_name, this will be used as the property name when assigning + the OOB to obj, otherwise tracker_key is used as the property name. """ - try: obj = obj.dbobj - except AttributeError: pass + try: + obj = obj.dbobj + except AttributeError: + pass if not "_trackerhandler" in _GA(obj, "__dict__"): # assign trackerhandler to object @@ -266,8 +283,10 @@ class OOBHandler(object): Remove the OOB from obj. If oob implements an at_delete hook, this will be called with args, kwargs """ - try: obj = obj.dbobj - except AttributeError: pass + try: + obj = obj.dbobj + except AttributeError: + pass try: # call at_delete hook @@ -278,7 +297,7 @@ class OOBHandler(object): store_key = (pack_dbobj(obj), sessid, fieldname) self.oob_tracker_storage.pop(store_key, None) - def get_all_tracked(session): + def get_all_tracked(self, session): """ Get the names of all variables this session is tracking. """ @@ -304,12 +323,14 @@ class OOBHandler(object): def track_attribute(self, obj, sessid, attr_name, trackerclass): """ Shortcut wrapper method for specifically tracking the changes of an - Attribute on an object. Will create a tracker on the Attribute Object and - name in a way the Attribute expects. + Attribute on an object. Will create a tracker on the Attribute + Object and name in a way the Attribute expects. """ # get the attribute object if we can - try: obj = obj.dbobj - except AttributeError: pass + try: + obj = obj.dbobj + except AttributeError: + pass attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: self.track(attrobj, sessid, "db_value", trackerclass, attr_name) @@ -318,8 +339,10 @@ class OOBHandler(object): """ Shortcut for deactivating tracking for a given attribute. """ - try: obj = obj.dbobj - except AttributeError: pass + try: + obj = obj.dbobj + except AttributeError: + pass attrobj = _GA(obj, "attributes").get(attr_name, return_obj=True) if attrobj: self.untrack(attrobj, sessid, attr_name, trackerclass) @@ -335,7 +358,7 @@ class OOBHandler(object): try: obj = obj.dbobj except AttributeError: - passj + pass store_obj = pack_dbobj(obj) store_key = (store_obj, sessid, func_key, interval) # prepare to store @@ -363,7 +386,6 @@ class OOBHandler(object): # access method - called from session.msg() - def execute_cmd(self, session, func_key, *args, **kwargs): """ Retrieve oobfunc from OOB_FUNCS and execute it immediately @@ -371,7 +393,7 @@ class OOBHandler(object): """ try: #print "OOB execute_cmd:", session, func_key, args, kwargs, _OOB_FUNCS.keys() - oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found + oobfunc = _OOB_FUNCS[func_key] # raise traceback if not found oobfunc(self, session, *args, **kwargs) except KeyError,e: errmsg = "OOB Error: function '%s' not recognized: %s" % (func_key, e) diff --git a/src/server/portal/__init__.py b/src/server/portal/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/src/server/portal/__init__.py +++ b/src/server/portal/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/server/portal/mccp.py b/src/server/portal/mccp.py index 7625bbe607..0e07f23e97 100644 --- a/src/server/portal/mccp.py +++ b/src/server/portal/mccp.py @@ -20,12 +20,14 @@ import zlib MCCP = chr(86) FLUSH = zlib.Z_SYNC_FLUSH + def mccp_compress(protocol, data): "Handles zlib compression, if applicable" if hasattr(protocol, 'zlib'): return protocol.zlib.compress(data) + protocol.zlib.flush(FLUSH) return data + class Mccp(object): """ Implements the MCCP protocol. Add this to a diff --git a/src/server/portal/msdp.py b/src/server/portal/msdp.py index 08b73da480..d605a4c37f 100644 --- a/src/server/portal/msdp.py +++ b/src/server/portal/msdp.py @@ -9,9 +9,7 @@ etc. """ import re -from django.conf import settings -from src.utils.utils import make_iter, mod_import, to_str -from src.utils import logger +from src.utils.utils import to_str # MSDP-relevant telnet cmd/opt-codes MSDP = chr(69) @@ -29,11 +27,18 @@ SE = chr(240) force_str = lambda inp: to_str(inp, force_string=True) # pre-compiled regexes -regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_ARRAY_OPEN, MSDP_ARRAY_CLOSE)) # return 2-tuple -regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, MSDP_TABLE_OPEN, MSDP_TABLE_CLOSE)) # return 2-tuple (may be nested) +# returns 2-tuple +regex_array = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_ARRAY_OPEN, + MSDP_ARRAY_CLOSE)) +# returns 2-tuple (may be nested) +regex_table = re.compile(r"%s(.*?)%s%s(.*?)%s" % (MSDP_VAR, MSDP_VAL, + MSDP_TABLE_OPEN, + MSDP_TABLE_CLOSE)) regex_var = re.compile(MSDP_VAR) regex_val = re.compile(MSDP_VAL) + # Msdp object handler class Msdp(object): @@ -90,7 +95,7 @@ class Msdp(object): else: string += MSDP_VAR + force_str(key) + MSDP_VAL + force_str(val) string += MSDP_TABLE_CLOSE - return stringk + return string def make_array(name, *args): "build a array. Arrays may not nest tables by definition." @@ -169,7 +174,7 @@ class Msdp(object): tables[key] = {} for varval in regex_var.split(table): parts = regex_val.split(varval) - tables[key].expand({parts[0] : tuple(parts[1:]) if len(parts)>1 else ("",)}) + tables[key].expand({parts[0]: tuple(parts[1:]) if len(parts) > 1 else ("",)}) for key, array in regex_array.findall(data): arrays[key] = [] for val in regex_val.split(array): @@ -178,16 +183,18 @@ class Msdp(object): for varval in regex_var.split(regex_array.sub("", regex_table.sub("", data))): # get remaining varvals after cleaning away tables/arrays parts = regex_val.split(varval) - variables[parts[0].upper()] = tuple(parts[1:]) if len(parts)>1 else ("", ) + variables[parts[0].upper()] = tuple(parts[1:]) if len(parts) > 1 else ("", ) #print "MSDP: table, array, variables:", tables, arrays, variables - # all variables sent through msdp to Evennia are considered commands with arguments. - # there are three forms of commands possible through msdp: + # all variables sent through msdp to Evennia are considered commands + # with arguments. There are three forms of commands possible + # through msdp: # # VARNAME VAR -> varname(var) # ARRAYNAME VAR VAL VAR VAL VAR VAL ENDARRAY -> arrayname(val,val,val) - # TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> tablename(varname=val, varname=val) + # TABLENAME TABLE VARNAME VAL VARNAME VAL ENDTABLE -> + # tablename(varname=val, varname=val) # # default MSDP functions @@ -232,82 +239,4 @@ class Msdp(object): Send oob data to Evennia """ #print "msdp data_in:", funcname, args, kwargs - self.protocol.data_in(text=None, oob=(funcname, args, kwargs)) - - # # MSDP Commands - # # Some given MSDP (varname, value) pairs can also be treated as command + argument. - # # Generic msdp command map. The argument will be sent to the given command. - # # See http://tintin.sourceforge.net/msdp/ for definitions of each command. - # # These are client->server commands. - # def msdp_cmd_list(self, arg): - # """ - # The List command allows for retrieving various info about the server/client - # """ - # if arg == 'COMMANDS': - # return self.evennia_to_msdp(arg, MSDP_COMMANDS) - # elif arg == 'LISTS': - # return self.evennia_to_msdp(arg, ("COMMANDS", "LISTS", "CONFIGURABLE_VARIABLES", - # "REPORTED_VARIABLES", "SENDABLE_VARIABLES")) - # elif arg == 'CONFIGURABLE_VARIABLES': - # return self.evennia_to_msdp(arg, ("CLIENT_NAME", "CLIENT_VERSION", "PLUGIN_ID")) - # elif arg == 'REPORTABLE_VARIABLES': - # return self.evennia_to_msdp(arg, MSDP_REPORTABLE.keys()) - # elif arg == 'REPORTED_VARIABLES': - # # the dynamically set items to report - # return self.evennia_to_msdp(arg, self.msdp_reported.keys()) - # elif arg == 'SENDABLE_VARIABLES': - # return self.evennia_to_msdp(arg, MSDP_SENDABLE.keys()) - # else: - # return self.evennia_to_msdp("LIST", arg) - - # # default msdp commands - - # def msdp_cmd_report(self, *arg): - # """ - # The report command instructs the server to start reporting a - # reportable variable to the client. - # """ - # try: - # return MSDP_REPORTABLE[arg](report=True) - # except Exception: - # logger.log_trace() - - # def msdp_cmd_unreport(self, arg): - # """ - # Unreport a previously reported variable - # """ - # try: - # MSDP_REPORTABLE[arg](report=False) - # except Exception: - # self.logger.log_trace() - - # def msdp_cmd_reset(self, arg): - # """ - # The reset command resets a variable to its initial state. - # """ - # try: - # MSDP_REPORTABLE[arg](reset=True) - # except Exception: - # logger.log_trace() - - # def msdp_cmd_send(self, *args): - # """ - # Request the server to send a particular variable - # to the client. - - # arg - this is a list of variables the client wants. - # """ - # ret = [] - # for var in make_iter(arg) - - - - - # for var in make_iter(arg): - # try: - # ret.append(MSDP_REPORTABLE[var.upper()])# (send=True)) - # except Exception: - # ret.append("ERROR")#logger.log_trace() - # return ret - - + self.protocol.data_in(text=None, oob=(funcname, args, kwargs)) \ No newline at end of file diff --git a/src/server/portal/portal.py b/src/server/portal/portal.py index f35ae4e6db..53fb5253f7 100644 --- a/src/server/portal/portal.py +++ b/src/server/portal/portal.py @@ -83,7 +83,7 @@ class Portal(object): # create a store of services self.services = service.IServiceCollection(application) - self.amp_protocol = None # set by amp factory + self.amp_protocol = None # set by amp factory self.sessions = PORTAL_SESSIONS self.sessions.portal = self @@ -99,7 +99,7 @@ class Portal(object): be restarted or is shutting down. Valid modes are True/False and None. If mode is None, no change will be done to the flag file. """ - if mode == None: + if mode is None: return f = open(PORTAL_RESTART, 'w') print "writing mode=%(mode)s to %(portal_restart)s" % {'mode': mode, 'portal_restart': PORTAL_RESTART} @@ -211,17 +211,20 @@ if SSL_ENABLED: factory = protocol.ServerFactory() factory.sessionhandler = PORTAL_SESSIONS factory.protocol = ssl.SSLProtocol - ssl_service = internet.SSLServer(port, factory, ssl.getSSLContext(), interface=interface) + ssl_service = internet.SSLServer(port, + factory, + ssl.getSSLContext(), + interface=interface) ssl_service.setName('EvenniaSSL%s' % pstring) PORTAL.services.addService(ssl_service) print " ssl%s: %s" % (ifacestr, port) - if SSH_ENABLED: - # Start SSH game connections. Will create a keypair in evennia/game if necessary. + # Start SSH game connections. Will create a keypair in + # evennia/game if necessary. from src.server.portal import ssh @@ -234,9 +237,9 @@ if SSH_ENABLED: ifacestr = "-%s" % interface for port in SSH_PORTS: pstring = "%s:%s" % (ifacestr, port) - factory = ssh.makeFactory({'protocolFactory':ssh.SshProtocol, - 'protocolArgs':(), - 'sessions':PORTAL_SESSIONS}) + factory = ssh.makeFactory({'protocolFactory': ssh.SshProtocol, + 'protocolArgs': (), + 'sessions': PORTAL_SESSIONS}) ssh_service = internet.TCPServer(port, factory, interface=interface) ssh_service.setName('EvenniaSSH%s' % pstring) PORTAL.services.addService(ssh_service) @@ -247,8 +250,6 @@ if WEBSERVER_ENABLED: # Start a reverse proxy to relay data to the Server-side webserver - from twisted.web import proxy - for interface in WEBSERVER_INTERFACES: if ":" in interface: print " iPv6 interfaces not yet supported" @@ -269,7 +270,9 @@ if WEBSERVER_ENABLED: webclientstr = "/client" web_root = server.Site(web_root, logPath=settings.HTTP_LOG_FILE) - proxy_service = internet.TCPServer(proxyport, web_root, interface=interface) + proxy_service = internet.TCPServer(proxyport, + web_root, + interface=interface) proxy_service.setName('EvenniaWebProxy%s' % pstring) PORTAL.services.addService(proxy_service) print " webproxy%s%s:%s (<-> %s)" % (webclientstr, ifacestr, proxyport, serverport) @@ -278,7 +281,7 @@ for plugin_module in PORTAL_SERVICES_PLUGIN_MODULES: # external plugin services to start plugin_module.start_plugin_services(PORTAL) -print '-' * 50 # end of terminal output +print '-' * 50 # end of terminal output if os.name == 'nt': # Windows only: Set PID file manually diff --git a/src/server/portal/portalsessionhandler.py b/src/server/portal/portalsessionhandler.py index 369f85e96b..e90164b365 100644 --- a/src/server/portal/portalsessionhandler.py +++ b/src/server/portal/portalsessionhandler.py @@ -4,6 +4,7 @@ Sessionhandler for portal sessions import time from src.server.sessionhandler import SessionHandler, PCONN, PDISCONN + #------------------------------------------------------------ # Portal-SessionHandler class #------------------------------------------------------------ @@ -39,8 +40,8 @@ class PortalSessionHandler(SessionHandler): def connect(self, session): """ - Called by protocol at first connect. This adds a not-yet authenticated session - using an ever-increasing counter for sessid. + Called by protocol at first connect. This adds a not-yet + authenticated session using an ever-increasing counter for sessid. """ self.latest_sessid += 1 sessid = self.latest_sessid @@ -48,13 +49,15 @@ class PortalSessionHandler(SessionHandler): sessdata = session.get_sync_data() self.sessions[sessid] = session # sync with server-side - if self.portal.amp_protocol: # this is a timing issue + if self.portal.amp_protocol: # this is a timing issue self.portal.amp_protocol.call_remote_ServerAdmin(sessid, operation=PCONN, data=sessdata) + def disconnect(self, session): """ - Called from portal side when the connection is closed from the portal side. + Called from portal side when the connection is closed + from the portal side. """ sessid = session.sessid if sessid in self.sessions: @@ -86,18 +89,22 @@ class PortalSessionHandler(SessionHandler): self.sessions = {} def server_logged_in(self, sessid, data): - "The server tells us that the session has been authenticated. Updated it." + """ + The server tells us that the session has been + authenticated. Updated it. + """ sess = self.get_session(sessid) sess.load_sync_data(data) def server_session_sync(self, serversessions): """ - Server wants to save data to the portal, maybe because it's about to shut down. - We don't overwrite any sessions here, just update them in-place and remove - any that are out of sync (which should normally not be the case) + Server wants to save data to the portal, maybe because it's about + to shut down. We don't overwrite any sessions here, just update + them in-place and remove any that are out of sync (which should + normally not be the case) - serversessions - dictionary {sessid:{property:value},...} describing the properties - to sync on all sessions + serversessions - dictionary {sessid:{property:value},...} describing + the properties to sync on all sessions """ to_save = [sessid for sessid in serversessions if sessid in self.sessions] to_delete = [sessid for sessid in self.sessions if sessid not in to_save] @@ -131,6 +138,7 @@ class PortalSessionHandler(SessionHandler): self.portal.amp_protocol.call_remote_MsgPortal2Server(session.sessid, msg=text, data=kwargs) + def announce_all(self, message): """ Send message to all connection sessions @@ -138,7 +146,6 @@ class PortalSessionHandler(SessionHandler): for session in self.sessions.values(): session.data_out(message) - def data_out(self, sessid, text=None, **kwargs): """ Called by server for having the portal relay messages and data diff --git a/src/server/portal/ssh.py b/src/server/portal/ssh.py index d179526ff6..66d284492e 100644 --- a/src/server/portal/ssh.py +++ b/src/server/portal/ssh.py @@ -26,7 +26,7 @@ from twisted.python import components from django.conf import settings from src.server import session from src.players.models import PlayerDB -from src.utils import ansi, utils, logger +from src.utils import ansi, utils ENCODINGS = settings.ENCODINGS @@ -35,6 +35,7 @@ CTRL_D = '\x04' CTRL_BACKSLASH = '\x1c' CTRL_L = '\x0c' + class SshProtocol(Manhole, session.Session): """ Each player connecting over ssh gets this protocol assigned to @@ -47,7 +48,8 @@ class SshProtocol(Manhole, session.Session): login automatically. """ self.authenticated_player = starttuple[0] - self.cfactory = starttuple[1] # obs may not be called self.factory, it gets overwritten! + # obs must not be called self.factory, that gets overwritten! + self.cfactory = starttuple[1] def terminalSize(self, width, height): """ @@ -95,7 +97,6 @@ class SshProtocol(Manhole, session.Session): self.terminal.write("KeyboardInterrupt") self.terminal.nextLine() - def handle_EOF(self): """ Handles EOF generally used to exit. @@ -105,7 +106,6 @@ class SshProtocol(Manhole, session.Session): else: self.handle_QUIT() - def handle_FF(self): """ Handle a 'form feed' byte - generally used to request a screen @@ -114,14 +114,12 @@ class SshProtocol(Manhole, session.Session): self.terminal.eraseDisplay() self.terminal.cursorHome() - def handle_QUIT(self): """ Quit, end, and lose the connection. """ self.terminal.loseConnection() - def connectionLost(self, reason=None): """ This is executed when the connection is lost for @@ -140,9 +138,7 @@ class SshProtocol(Manhole, session.Session): """ return self.terminal.transport.getPeer() - def lineReceived(self, string): - """ Communication Player -> Evennia. Any line return indicates a command for the purpose of the MUD. So we take the user input @@ -159,10 +155,10 @@ class SshProtocol(Manhole, session.Session): """ for line in string.split('\n'): - self.terminal.write(line) #this is the telnet-specific method for sending + #this is the telnet-specific method for sending + self.terminal.write(line) self.terminal.nextLine() - # session-general method hooks def disconnect(self, reason="Connection closed. Goodbye for now."): @@ -175,7 +171,8 @@ class SshProtocol(Manhole, session.Session): def data_out(self, text=None, **kwargs): """ - Data Evennia -> Player access hook. 'data' argument is a dict parsed for string settings. + Data Evennia -> Player access hook. 'data' argument is a dict + parsed for string settings. ssh flags: raw=True - leave all ansi markup and tokens unparsed @@ -190,9 +187,9 @@ class SshProtocol(Manhole, session.Session): raw = kwargs.get("raw", False) nomarkup = kwargs.get("nomarkup", False) if raw: - self.lineSend(string) + self.lineSend(text) else: - self.lineSend(ansi.parse_ansi(string.strip("{r") + "{r", strip_ansi=nomarkup)) + self.lineSend(ansi.parse_ansi(text.strip("{r") + "{r", strip_ansi=nomarkup)) class ExtraInfoAuthServer(SSHUserAuthServer): @@ -209,6 +206,7 @@ class ExtraInfoAuthServer(SSHUserAuthServer): return self.portal.login(c, None, IConchUser).addErrback( self._ebPassword) + class PlayerDBPasswordChecker(object): """ Checks the django db for the correct credentials for @@ -232,6 +230,7 @@ class PlayerDBPasswordChecker(object): res = (player, self.factory) return defer.succeed(res) + class PassAvatarIdTerminalRealm(TerminalRealm): """ Returns an avatar that passes the avatarId through to the @@ -244,7 +243,7 @@ class PassAvatarIdTerminalRealm(TerminalRealm): sess = self.sessionFactory(comp) sess.transportFactory = self.transportFactory - sess.chainedProtocolFactory = lambda : self.chainedProtocolFactory(avatarId) + sess.chainedProtocolFactory = lambda: self.chainedProtocolFactory(avatarId) comp.setComponent(iconch.IConchUser, user) comp.setComponent(iconch.ISession, sess) @@ -252,7 +251,6 @@ class PassAvatarIdTerminalRealm(TerminalRealm): return user - class TerminalSessionTransport_getPeer: """ Taken from twisted's TerminalSessionTransport which doesn't @@ -345,4 +343,4 @@ def makeFactory(configdict): factory.portal.registerChecker(PlayerDBPasswordChecker(factory)) - return factory + return factory \ No newline at end of file diff --git a/src/server/portal/ssl.py b/src/server/portal/ssl.py index 238d3bfd8b..0ab5357791 100644 --- a/src/server/portal/ssl.py +++ b/src/server/portal/ssl.py @@ -3,7 +3,8 @@ This is a simple context factory for auto-creating SSL keys and certificates. """ -import os, sys +import os +import sys from twisted.internet import ssl as twisted_ssl try: import OpenSSL @@ -13,6 +14,7 @@ except ImportError: from src.server.portal.telnet import TelnetProtocol + class SSLProtocol(TelnetProtocol): """ Communication is the same as telnet, except data transfer @@ -20,6 +22,7 @@ class SSLProtocol(TelnetProtocol): """ pass + def verify_SSL_key_and_cert(keyfile, certfile): """ This function looks for RSA key and certificate in the current @@ -41,24 +44,27 @@ def verify_SSL_key_and_cert(keyfile, certfile): rsaKey = Key(RSA.generate(KEY_LENGTH)) keyString = rsaKey.toString(type="OPENSSH") file(keyfile, 'w+b').write(keyString) - except Exception,e: + except Exception, e: print "rsaKey error: %(e)s\n WARNING: Evennia could not auto-generate SSL private key." % {'e': e} print "If this error persists, create game/%(keyfile)s yourself using third-party tools." % {'keyfile': keyfile} sys.exit(5) # try to create the certificate - CERT_EXPIRE = 365 * 20 # twenty years validity + CERT_EXPIRE = 365 * 20 # twenty years validity # default: #openssl req -new -x509 -key ssl.key -out ssl.cert -days 7300 exestring = "openssl req -new -x509 -key %s -out %s -days %s" % (keyfile, certfile, CERT_EXPIRE) #print "exestring:", exestring try: - err = subprocess.call(exestring)#, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + #, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT) + subprocess.call(exestring) except OSError, e: string = "\n".join([ " %s\n" % e, - " Evennia's SSL context factory could not automatically create an SSL certificate game/%(cert)s." % {'cert': certfile}, - " A private key 'ssl.key' was already created. Please create %(cert)s manually using the commands valid" % {'cert': certfile}, + " Evennia's SSL context factory could not automatically", + " create an SSL certificate game/%(cert)s." % {'cert': certfile}, + " A private key 'ssl.key' was already created. Please", + " create %(cert)s manually using the commands valid" % {'cert': certfile}, " for your operating system.", " Example (linux, using the openssl program): ", " %s" % exestring]) @@ -66,6 +72,7 @@ def verify_SSL_key_and_cert(keyfile, certfile): sys.exit(5) print "done." + def getSSLContext(): """ Returns an SSL context (key and certificate). This function diff --git a/src/server/portal/telnet.py b/src/server/portal/telnet.py index 58a03b359a..d0a074c7eb 100644 --- a/src/server/portal/telnet.py +++ b/src/server/portal/telnet.py @@ -9,15 +9,14 @@ sessions etc. import re from twisted.conch.telnet import Telnet, StatefulTelnetProtocol, IAC, LINEMODE -from twisted.internet.defer import inlineCallbacks, returnValue from src.server.session import Session from src.server.portal import ttype, mssp, msdp from src.server.portal.mccp import Mccp, mccp_compress, MCCP from src.utils import utils, ansi, logger -from src.utils.utils import make_iter, is_iter _RE_N = re.compile(r"\{n$") + class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ Each player connecting over telnet (ie using most traditional mud @@ -127,7 +126,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): def _write(self, data): "hook overloading the one used in plain telnet" - #print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data)) + # print "_write (%s): %s" % (self.state, " ".join(str(ord(c)) for c in data)) data = data.replace('\n', '\r\n').replace('\r\r\n', '\r\n') #data = data.replace('\n', '\r\n') super(TelnetProtocol, self)._write(mccp_compress(self, data)) @@ -147,7 +146,6 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): """ self.data_in(text=string) - # Session hooks def disconnect(self, reason=None): @@ -172,11 +170,13 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): through the telnet connection. valid telnet kwargs: - raw=True - pass string through without any ansi processing (i.e. include Evennia - ansi markers but do not convert them into ansi tokens) + raw=True - pass string through without any ansi + processing (i.e. include Evennia ansi markers but do + not convert them into ansi tokens) nomarkup=True - strip all ansi markup - The telnet ttype negotiation flags, if any, are used if no kwargs are given. + The telnet ttype negotiation flags, if any, are used if no kwargs + are given. """ try: text = utils.to_str(text if text else "", encoding=self.encoding) @@ -200,6 +200,7 @@ class TelnetProtocol(Telnet, StatefulTelnetProtocol, Session): # no processing whatsoever self.sendLine(text) else: - # we need to make sure to kill the color at the end in order to match the webclient output. - #print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self) + # we need to make sure to kill the color at the end in order + # to match the webclient output. + # print "telnet data out:", self.protocol_flags, id(self.protocol_flags), id(self) self.sendLine(ansi.parse_ansi(_RE_N.sub("", text) + "{n", strip_ansi=nomarkup, xterm256=ttype.get('256 COLORS'))) diff --git a/src/server/portal/ttype.py b/src/server/portal/ttype.py index ffd7a2a94e..622a84e2d1 100644 --- a/src/server/portal/ttype.py +++ b/src/server/portal/ttype.py @@ -12,12 +12,12 @@ under the 'TTYPE' key. """ # telnet option codes -TTYPE = chr(24) +TTYPE = chr(24) IS = chr(0) SEND = chr(1) # terminal capabilities and their codes -MTTS = [(128,'PROXY'), +MTTS = [(128, 'PROXY'), (64, 'SCREEN READER'), (32, 'OSC COLOR PALETTE'), (16, 'MOUSE TRACKING'), @@ -27,8 +27,8 @@ MTTS = [(128,'PROXY'), (1, 'ANSI')] # some clients sends erroneous strings instead # of capability numbers. We try to convert back. -MTTS_invert = {"PROXY":128, - "SCREEN COLOR PALETTE":64, +MTTS_invert = {"PROXY": 128, + "SCREEN COLOR PALETTE": 64, "OSC COLOR PALETTE": 32, "MOUSE TRACKING": 16, "256 COLORS": 8, @@ -36,6 +36,7 @@ MTTS_invert = {"PROXY":128, "VT100": 2, "ANSI": 1} + class Ttype(object): """ Handles ttype negotiations. Called and initiated by the @@ -51,7 +52,7 @@ class Ttype(object): """ self.ttype_step = 0 self.protocol = protocol - self.protocol.protocol_flags['TTYPE'] = {"init_done":False} + self.protocol.protocol_flags['TTYPE'] = {"init_done": False} # setup protocol to handle ttype initialization and negotiation self.protocol.negotiationMap[TTYPE] = self.do_ttype # ask if client will ttype, connect callback if it does. @@ -61,7 +62,7 @@ class Ttype(object): """ Callback if ttype is not supported by client. """ - self.protocol.protocol_flags['TTYPE'] = {"init_done":True} + self.protocol.protocol_flags['TTYPE'] = {"init_done": True} def do_ttype(self, option): """ @@ -95,9 +96,9 @@ class Ttype(object): try: option = int(option.strip('MTTS ')) except ValueError: - # it seems some clients don't send MTTS according to protocol - # specification, but instead just sends the data as plain - # strings. We try to convert back. + # it seems some clients don't send MTTS according to + # protocol specification, but instead just sends + # the data as plain strings. We try to convert back. option = MTTS_invert.get(option.strip('MTTS ').upper()) if not option: # no conversion possible. Give up. diff --git a/src/server/portal/webclient.py b/src/server/portal/webclient.py index 14d87d8c42..cc3f319750 100644 --- a/src/server/portal/webclient.py +++ b/src/server/portal/webclient.py @@ -20,19 +20,19 @@ import time from hashlib import md5 from twisted.web import server, resource -from twisted.internet import defer, reactor from django.utils import simplejson from django.utils.functional import Promise from django.utils.encoding import force_unicode from django.conf import settings -from src.utils import utils, logger, ansi +from src.utils import utils, logger from src.utils.text2html import parse_html from src.server import session SERVERNAME = settings.SERVERNAME ENCODINGS = settings.ENCODINGS + # defining a simple json encoder for returning # django data to the client. Might need to # extend this if one wants to send more @@ -43,6 +43,8 @@ class LazyEncoder(simplejson.JSONEncoder): if isinstance(obj, Promise): return force_unicode(obj) return super(LazyEncoder, self).default(obj) + + def jsonify(obj): return utils.to_str(simplejson.dumps(obj, ensure_ascii=False, cls=LazyEncoder)) @@ -84,23 +86,23 @@ class WebClient(resource.Resource): request = self.requests.get(suid) if request: # we have a request waiting. Return immediately. - request.write(jsonify({'msg':string, 'data':data})) + request.write(jsonify({'msg': string, 'data': data})) request.finish() del self.requests[suid] else: # no waiting request. Store data in buffer dataentries = self.databuffer.get(suid, []) - dataentries.append(jsonify({'msg':string, 'data':data})) + dataentries.append(jsonify({'msg': string, 'data': data})) self.databuffer[suid] = dataentries def client_disconnect(self, suid): """ Disconnect session with given suid. """ - if self.requests.has_key(suid): + if suid in self.requests: self.requests[suid].finish() del self.requests[suid] - if self.databuffer.has_key(suid): + if suid in self.databuffer: del self.databuffer[suid] def mode_init(self, request): @@ -108,7 +110,8 @@ class WebClient(resource.Resource): This is called by render_POST when the client requests an init mode operation (at startup) """ - #csess = request.getSession() # obs, this is a cookie, not an evennia session! + #csess = request.getSession() # obs, this is a cookie, not + # an evennia session! #csees.expireCallbacks.append(lambda : ) suid = request.args.get('suid', ['0'])[0] @@ -124,7 +127,7 @@ class WebClient(resource.Resource): sess.init_session("webclient", remote_addr, self.sessionhandler) sess.suid = suid sess.sessionhandler.connect(sess) - return jsonify({'msg':host_string, 'suid':suid}) + return jsonify({'msg': host_string, 'suid': suid}) def mode_input(self, request): """ @@ -158,8 +161,8 @@ class WebClient(resource.Resource): if dataentries: return dataentries.pop(0) request.notifyFinish().addErrback(self._responseFailed, suid, request) - if self.requests.has_key(suid): - self.requests[suid].finish() # Clear any stale request. + if suid in self.requests: + self.requests[suid].finish() # Clear any stale request. self.requests[suid] = request return server.NOT_DONE_YET @@ -206,6 +209,7 @@ class WebClient(resource.Resource): # this should not happen if client sends valid data. return '' + # # A session type handling communication over the # web client interface. @@ -241,7 +245,8 @@ class WebClientSession(session.Session): if raw: self.client.lineSend(self.suid, text) else: - self.client.lineSend(self.suid, parse_html(text, strip_ansi=nomarkup)) + self.client.lineSend(self.suid, + parse_html(text, strip_ansi=nomarkup)) return except Exception: logger.log_trace() diff --git a/src/server/server.py b/src/server/server.py index 825f4b91ce..f86e046aff 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -32,12 +32,11 @@ from src.server.sessionhandler import SESSIONS # setting up server-side field cache -from django.db.models.signals import pre_save, post_save +from django.db.models.signals import post_save from src.server.caches import field_pre_save #pre_save.connect(field_pre_save, dispatch_uid="fieldcache") post_save.connect(field_pre_save, dispatch_uid="fieldcache") -from src.typeclasses.models import TypedObject #from src.server.caches import post_attr_update #from django.db.models.signals import m2m_changed @@ -104,7 +103,7 @@ class Evennia(object): # create a store of services self.services = service.IServiceCollection(application) - self.amp_protocol = None # set by amp factory + self.amp_protocol = None # set by amp factory self.sessions = SESSIONS self.sessions.server = self @@ -121,7 +120,8 @@ class Evennia(object): # set a callback if the server is killed abruptly, # by Ctrl-C, reboot etc. - reactor.addSystemEventTrigger('before', 'shutdown', self.shutdown, _reactor_stopping=True) + reactor.addSystemEventTrigger('before', 'shutdown', + self.shutdown, _reactor_stopping=True) self.game_running = True @@ -146,38 +146,48 @@ class Evennia(object): def update_defaults(self): """ - We make sure to store the most important object defaults here, so we can catch if they - change and update them on-objects automatically. This allows for changing default cmdset locations - and default typeclasses in the settings file and have them auto-update all already existing - objects. + We make sure to store the most important object defaults here, so + we can catch if they change and update them on-objects automatically. + This allows for changing default cmdset locations and default + typeclasses in the settings file and have them auto-update all + already existing objects. """ # setting names - settings_names = ("CMDSET_CHARACTER", "CMDSET_PLAYER", "BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS", - "BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS", "BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS") + settings_names = ("CMDSET_CHARACTER", "CMDSET_PLAYER", + "BASE_PLAYER_TYPECLASS", "BASE_OBJECT_TYPECLASS", + "BASE_CHARACTER_TYPECLASS", "BASE_ROOM_TYPECLASS", + "BASE_EXIT_TYPECLASS", "BASE_SCRIPT_TYPECLASS") # get previous and current settings so they can be compared settings_compare = zip([ServerConfig.objects.conf(name) for name in settings_names], [settings.__getattr__(name) for name in settings_names]) mismatches = [i for i, tup in enumerate(settings_compare) if tup[0] and tup[1] and tup[0] != tup[1]] - if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any() - # we have a changed default. Import relevant objects and run the update + if len(mismatches): # can't use any() since mismatches may be [0] which reads as False for any() + # we have a changed default. Import relevant objects and + # run the update from src.objects.models import ObjectDB #from src.players.models import PlayerDB for i, prev, curr in ((i, tup[0], tup[1]) for i, tup in enumerate(settings_compare) if i in mismatches): # update the database print " %s:\n '%s' changed to '%s'. Updating unchanged entries in database ..." % (settings_names[i], prev, curr) - if i == 0: [obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)] - if i == 1: [ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)] - if i == 2: [ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)] - if i in (3,4,5,6): [obj.__setattr__("typeclass_path",curr) - for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)] - if i == 7: [scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)] + if i == 0: + [obj.__setattr__("cmdset_storage", curr) for obj in ObjectDB.objects.filter(db_cmdset_storage__exact=prev)] + if i == 1: + [ply.__setattr__("cmdset_storage", curr) for ply in PlayerDB.objects.filter(db_cmdset_storage__exact=prev)] + if i == 2: + [ply.__setattr__("typeclass_path", curr) for ply in PlayerDB.objects.filter(db_typeclass_path__exact=prev)] + if i in (3, 4, 5, 6): + [obj.__setattr__("typeclass_path", curr) for obj in ObjectDB.objects.filter(db_typeclass_path__exact=prev)] + if i == 7: + [scr.__setattr__("typeclass_path", curr) for scr in ScriptDB.objects.filter(db_typeclass_path__exact=prev)] # store the new default and clean caches ServerConfig.objects.conf(settings_names[i], curr) ObjectDB.flush_instance_cache() PlayerDB.flush_instance_cache() ScriptDB.flush_instance_cache() - # if this is the first start we might not have a "previous" setup saved. Store it now. - [ServerConfig.objects.conf(settings_names[i], tup[1]) for i, tup in enumerate(settings_compare) if not tup[0]] + # if this is the first start we might not have a "previous" + # setup saved. Store it now. + [ServerConfig.objects.conf(settings_names[i], tup[1]) + for i, tup in enumerate(settings_compare) if not tup[0]] def run_initial_setup(self): """ @@ -191,7 +201,7 @@ class Evennia(object): # i.e. this is an empty DB that needs populating. print ' Server started for the first time. Setting defaults.' initial_setup.handle_setup(0) - print '-'*50 + print '-' * 50 elif int(last_initial_setup_step) >= 0: # a positive value means the setup crashed on one of its # modules and setup will resume from this step, retrying @@ -200,7 +210,7 @@ class Evennia(object): print ' Resuming initial setup from step %(last)s.' % \ {'last': last_initial_setup_step} initial_setup.handle_setup(int(last_initial_setup_step)) - print '-'*50 + print '-' * 50 def run_init_hooks(self): """ @@ -244,7 +254,7 @@ class Evennia(object): Either way, the active restart setting (Restart=True/False) is returned so the server knows which more it's in. """ - if mode == None: + if mode is None: with open(SERVER_RESTART, 'r') as f: # mode is either shutdown, reset or reload mode = f.read() @@ -259,16 +269,20 @@ class Evennia(object): Shuts down the server from inside it. mode - sets the server restart mode. - 'reload' - server restarts, no "persistent" scripts are stopped, at_reload hooks called. - 'reset' - server restarts, non-persistent scripts stopped, at_shutdown hooks called. + 'reload' - server restarts, no "persistent" scripts + are stopped, at_reload hooks called. + 'reset' - server restarts, non-persistent scripts stopped, + at_shutdown hooks called. 'shutdown' - like reset, but server will not auto-restart. None - keep currently set flag from flag file. - _reactor_stopping - this is set if server is stopped by a kill command OR this method was already called - once - in both cases the reactor is dead/stopping already. + _reactor_stopping - this is set if server is stopped by a kill + command OR this method was already called + once - in both cases the reactor is + dead/stopping already. """ if _reactor_stopping and hasattr(self, "shutdown_complete"): - # this means we have already passed through this method once; we don't need - # to run the shutdown procedure again. + # this means we have already passed through this method + # once; we don't need to run the shutdown procedure again. defer.returnValue(None) mode = self.set_restart_mode(mode) @@ -280,9 +294,12 @@ class Evennia(object): if mode == 'reload': # call restart hooks - yield [(o.typeclass, o.at_server_reload()) for o in ObjectDB.get_all_cached_instances()] - yield [(p.typeclass, p.at_server_reload()) for p in PlayerDB.get_all_cached_instances()] - yield [(s.typeclass, s.pause(), s.at_server_reload()) for s in ScriptDB.get_all_cached_instances()] + yield [(o.typeclass, o.at_server_reload()) + for o in ObjectDB.get_all_cached_instances()] + yield [(p.typeclass, p.at_server_reload()) + for p in PlayerDB.get_all_cached_instances()] + yield [(s.typeclass, s.pause(), s.at_server_reload()) + for s in ScriptDB.get_all_cached_instances()] yield self.sessions.all_sessions_portal_sync() ServerConfig.objects.conf("server_restart_mode", "reload") @@ -294,14 +311,20 @@ class Evennia(object): else: if mode == 'reset': - # don't unset the is_connected flag on reset, otherwise same as shutdown - yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] - else: # shutdown - yield [_SA(p, "is_connected", False) for p in PlayerDB.get_all_cached_instances()] - yield [(o.typeclass, o.at_server_shutdown()) for o in ObjectDB.get_all_cached_instances()] + # don't unset the is_connected flag on reset, otherwise + # same as shutdown + yield [(o.typeclass, o.at_server_shutdown()) + for o in ObjectDB.get_all_cached_instances()] + else: # shutdown + yield [_SA(p, "is_connected", False) + for p in PlayerDB.get_all_cached_instances()] + yield [(o.typeclass, o.at_server_shutdown()) + for o in ObjectDB.get_all_cached_instances()] - yield [(p.typeclass, p.unpuppet_all(), p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()] - yield [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()] + yield [(p.typeclass, p.unpuppet_all(), p.at_server_shutdown()) + for p in PlayerDB.get_all_cached_instances()] + yield [(s.typeclass, s.at_server_shutdown()) + for s in ScriptDB.get_all_cached_instances()] yield ObjectDB.objects.clear_all_sessids() ServerConfig.objects.conf("server_restart_mode", "reset") @@ -310,12 +333,14 @@ class Evennia(object): if SERVER_STARTSTOP_MODULE: SERVER_STARTSTOP_MODULE.at_server_stop() - # if _reactor_stopping is true, reactor does not need to be stopped again. + # if _reactor_stopping is true, reactor does not need to + # be stopped again. if os.name == 'nt' and os.path.exists(SERVER_PIDFILE): # for Windows we need to remove pid files manually os.remove(SERVER_PIDFILE) if not _reactor_stopping: - # this will also send a reactor.stop signal, so we set a flag to avoid loops. + # this will also send a reactor.stop signal, so we set a + # flag to avoid loops. self.shutdown_complete = True reactor.callLater(0, reactor.stop) @@ -415,7 +440,7 @@ for plugin_module in SERVER_SERVICES_PLUGIN_MODULES: # external plugin protocols plugin_module.start_plugin_services(EVENNIA) -print '-' * 50 # end of terminal output +print '-' * 50 # end of terminal output # clear server startup mode ServerConfig.objects.conf("server_starting_mode", delete=True) diff --git a/src/server/serversession.py b/src/server/serversession.py index be11328d08..23c38b2bb1 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -10,7 +10,7 @@ are stored on the Portal side) import time from datetime import datetime from django.conf import settings -from src.scripts.models import ScriptDB +#from src.scripts.models import ScriptDB from src.comms.models import ChannelDB from src.utils import logger, utils from src.utils.utils import make_iter, to_str @@ -53,7 +53,8 @@ class ServerSession(Session): self.cmdset = cmdsethandler.CmdSetHandler(self) def __cmdset_storage_get(self): - return [path.strip() for path in self.cmdset_storage_string.split(',')] + return [path.strip() for path in self.cmdset_storage_string.split(',')] + def __cmdset_storage_set(self, value): self.cmdset_storage_string = ",".join(str(val).strip() for val in make_iter(value)) cmdset_storage = property(__cmdset_storage_get, __cmdset_storage_set) @@ -61,8 +62,8 @@ class ServerSession(Session): def at_sync(self): """ This is called whenever a session has been resynced with the portal. - At this point all relevant attributes have already been set and self.player - been assigned (if applicable). + At this point all relevant attributes have already been set and + self.player been assigned (if applicable). Since this is often called after a server restart we need to set up the session as it was. @@ -78,7 +79,8 @@ class ServerSession(Session): self.cmdset.update(init_mode=True) if self.puid: - # reconnect puppet (puid is only set if we are coming back from a server reload) + # reconnect puppet (puid is only set if we are coming + # back from a server reload) obj = _ObjectDB.objects.get(id=self.puid) self.player.puppet_object(self.sessid, obj, normal_mode=False) @@ -139,7 +141,8 @@ class ServerSession(Session): def get_puppet_or_player(self): """ - Returns session if not logged in; puppet if one exists, otherwise return the player. + Returns session if not logged in; puppet if one exists, + otherwise return the player. """ if self.logged_in: return self.puppet if self.puppet else self.player @@ -192,7 +195,8 @@ class ServerSession(Session): # merge, give prio to the lowest level (puppet) nicks = list(puppet.db_attributes.filter(db_category__in=("nick_inputline", "nick_channel"))) + list(nicks) raw_list = text.split(None) - raw_list = [" ".join(raw_list[:i+1]) for i in range(len(raw_list)) if raw_list[:i+1]] + raw_list = [" ".join(raw_list[:i + 1]) + for i in range(len(raw_list)) if raw_list[:i + 1]] for nick in nicks: if nick.db_key in raw_list: text = text.replace(nick.db_key, nick.db_strvalue, 1) @@ -209,7 +213,7 @@ class ServerSession(Session): if funcname: _OOB_HANDLER.execute_cmd(self, funcname, *args, **kwargs) - execute_cmd = data_in # alias + execute_cmd = data_in # alias def data_out(self, text=None, **kwargs): """ @@ -255,7 +259,6 @@ class ServerSession(Session): "alias for at_data_out" self.data_out(text=text, **kwargs) - # Dummy API hooks for use during non-loggedin operation def at_cmdset_get(self): @@ -282,6 +285,7 @@ class ServerSession(Session): def all(self): return [val for val in self.__dict__.keys() if not val.startswith['_']] + def __getattribute__(self, key): # return None if no matching attribute was found. try: @@ -290,12 +294,14 @@ class ServerSession(Session): return None self._ndb_holder = NdbHolder() return self._ndb_holder + #@ndb.setter def ndb_set(self, value): "Stop accidentally replacing the db object" string = "Cannot assign directly to ndb object! " string = "Use ndb.attr=value instead." raise Exception(string) + #@ndb.deleter def ndb_del(self): "Stop accidental deletion." diff --git a/src/server/session.py b/src/server/session.py index 536177a8a5..a0e8c81ca4 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -6,6 +6,7 @@ on Portal and Server side) should inherit from this class. import time + #------------------------------------------------------------ # Server Session #------------------------------------------------------------ @@ -18,23 +19,24 @@ class Session(object): Each connection will see two session instances created: 1) A Portal session. This is customized for the respective connection - protocols that Evennia supports, like Telnet, SSH etc. The Portal session - must call init_session() as part of its initialization. The respective - hook methods should be connected to the methods unique for the respective - protocol so that there is a unified interface to Evennia. - 2) A Server session. This is the same for all connected players, regardless - of how they connect. + protocols that Evennia supports, like Telnet, SSH etc. The Portal + session must call init_session() as part of its initialization. The + respective hook methods should be connected to the methods unique + for the respective protocol so that there is a unified interface + to Evennia. + 2) A Server session. This is the same for all connected players, + regardless of how they connect. - The Portal and Server have their own respective sessionhandlers. These are synced - whenever new connections happen or the Server restarts etc, which means much of the - same information must be stored in both places e.g. the portal can re-sync with the - server when the server reboots. + The Portal and Server have their own respective sessionhandlers. These + are synced whenever new connections happen or the Server restarts etc, + which means much of the same information must be stored in both places + e.g. the portal can re-sync with the server when the server reboots. """ # names of attributes that should be affected by syncing. - _attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', 'uname', - 'logged_in', 'puid', 'encoding', + _attrs_to_sync = ('protocol_key', 'address', 'suid', 'sessid', 'uid', + 'uname', 'logged_in', 'puid', 'encoding', 'conn_time', 'cmd_last', 'cmd_last_visible', 'cmd_total', 'protocol_flags', 'server_data', "cmdset_storage_string") @@ -55,7 +57,7 @@ class Session(object): self.suid = None # unique id for this session - self.sessid = 0 # no sessid yet + self.sessid = 0 # no sessid yet # database id for the user connected to this session self.uid = None # user name, for easier tracking of sessions @@ -84,7 +86,8 @@ class Session(object): """ Return all data relevant to sync the session """ - return dict((key, value) for key, value in self.__dict__.items() if key in self._attrs_to_sync) + return dict((key, value) for key, value in self.__dict__.items() + if key in self._attrs_to_sync) def load_sync_data(self, sessdata): """ @@ -124,4 +127,3 @@ class Session(object): hook for protocols to send incoming data to the engine. """ pass - diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 0b5b4dddd1..f7ac098457 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -15,7 +15,7 @@ There are two similar but separate stores of sessions: import time from django.conf import settings from src.commands.cmdhandler import CMD_LOGINSTART -from src.utils.utils import variable_from_module, to_str +from src.utils.utils import variable_from_module try: import cPickle as pickle except ImportError: @@ -29,14 +29,14 @@ _ScriptDB = None # AMP signals -PCONN = chr(1) # portal session connect -PDISCONN = chr(2) # portal session disconnect -PSYNC = chr(3) # portal session sync -SLOGIN = chr(4) # server session login -SDISCONN = chr(5) # server session disconnect -SDISCONNALL = chr(6) # server session disconnect all -SSHUTD = chr(7) # server shutdown -SSYNC = chr(8) # server session sync +PCONN = chr(1) # portal session connect +PDISCONN = chr(2) # portal session disconnect +PSYNC = chr(3) # portal session sync +SLOGIN = chr(4) # server session login +SDISCONN = chr(5) # server session disconnect +SDISCONNALL = chr(6) # server session disconnect all +SSHUTD = chr(7) # server shutdown +SSYNC = chr(8) # server session sync # i18n from django.utils.translation import ugettext as _ @@ -45,6 +45,7 @@ SERVERNAME = settings.SERVERNAME MULTISESSION_MODE = settings.MULTISESSION_MODE IDLE_TIMEOUT = settings.IDLE_TIMEOUT + def delayed_import(): "Helper method for delayed import of all needed entities" global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB @@ -61,6 +62,7 @@ def delayed_import(): # including once to avoid warnings in Python syntax checkers _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB + #----------------------------------------------------------- # SessionHandler base class #------------------------------------------------------------ @@ -110,7 +112,8 @@ class SessionHandler(object): (cmdname, (args,), {kwargs}) ((cmdname, (arg1,arg2)), cmdname, (cmdname, (arg1,))) outputs an ordered structure on the form - ((cmdname, (args,), {kwargs}), ...), where the two last parts of each tuple may be empty + ((cmdname, (args,), {kwargs}), ...), where the two last + parts of each tuple may be empty """ def _parse(oobstruct): slen = len(oobstruct) @@ -119,8 +122,9 @@ class SessionHandler(object): elif not hasattr(oobstruct, "__iter__"): # a singular command name, without arguments or kwargs return (oobstruct.lower(), (), {}) - # regardless of number of args/kwargs, the first element must be the function name. - # we will not catch this error if not, but allow it to propagate. + # regardless of number of args/kwargs, the first element must be + # the function name. We will not catch this error if not, but + # allow it to propagate. if slen == 1: return (oobstruct[0].lower(), (), {}) elif slen == 2: @@ -135,7 +139,9 @@ class SessionHandler(object): return (oobstruct[0].lower(), tuple(oobstruct[1]), dict(oobstruct[2])) if hasattr(oobstruct, "__iter__"): - # differentiate between (cmdname, cmdname), (cmdname, args, kwargs) and ((cmdname,args,kwargs), (cmdname,args,kwargs), ...) + # differentiate between (cmdname, cmdname), + # (cmdname, args, kwargs) and ((cmdname,args,kwargs), + # (cmdname,args,kwargs), ...) if oobstruct and isinstance(oobstruct[0], basestring): return (tuple(_parse(oobstruct)),) @@ -146,6 +152,7 @@ class SessionHandler(object): return (tuple(out),) return (_parse(oobstruct),) + #------------------------------------------------------------ # Server-SessionHandler class #------------------------------------------------------------ @@ -171,7 +178,7 @@ class ServerSessionHandler(SessionHandler): """ self.sessions = {} self.server = None - self.server_data = {"servername":SERVERNAME} + self.server_data = {"servername": SERVERNAME} def portal_connect(self, portalsession): """ @@ -189,7 +196,8 @@ class ServerSessionHandler(SessionHandler): sess.sessionhandler = self sess.load_sync_data(portalsession) if sess.logged_in and sess.uid: - # this can happen in the case of auto-authenticating protocols like SSH + # this can happen in the case of auto-authenticating + # protocols like SSH sess.player = _PlayerDB.objects.get_player_from_uid(sess.uid) sess.at_sync() # validate all script @@ -216,17 +224,19 @@ class ServerSessionHandler(SessionHandler): def portal_session_sync(self, portalsessions): """ - Syncing all session ids of the portal with the ones of the server. This is instantiated - by the portal when reconnecting. + Syncing all session ids of the portal with the ones of the + server. This is instantiated by the portal when reconnecting. portalsessions is a dictionary {sessid: {property:value},...} defining - each session and the properties in it which should be synced. + each session and the properties in it which should + be synced. """ delayed_import() global _ServerSession, _PlayerDB, _ServerConfig, _ScriptDB for sess in self.sessions.values(): - # we delete the old session to make sure to catch eventual lingering references. + # we delete the old session to make sure to catch eventual + # lingering references. del sess for sessid, sessdict in portalsessions.items(): @@ -238,7 +248,8 @@ class ServerSessionHandler(SessionHandler): self.sessions[sessid] = sess sess.at_sync() - # after sync is complete we force-validate all scripts (this also starts them) + # after sync is complete we force-validate all scripts + # (this also starts them) init_mode = _ServerConfig.objects.conf("server_restart_mode", default=None) _ScriptDB.objects.validate(init_mode=init_mode) _ServerConfig.objects.conf("server_restart_mode", delete=True) @@ -333,7 +344,6 @@ class ServerSessionHandler(SessionHandler): operation=SSYNC, data=sessdata) - def disconnect_all_sessions(self, reason=_("You have been disconnected.")): """ Cleanly disconnect all of the connected sessions. @@ -346,7 +356,8 @@ class ServerSessionHandler(SessionHandler): operation=SDISCONNALL, data=reason) - def disconnect_duplicate_sessions(self, curr_session, reason = _("Logged in from elsewhere. Disconnecting.") ): + def disconnect_duplicate_sessions(self, curr_session, + reason=_("Logged in from elsewhere. Disconnecting.")): """ Disconnects any existing sessions with the same user. """ @@ -364,7 +375,7 @@ class ServerSessionHandler(SessionHandler): and see if any are dead. """ tcurr = time.time() - reason= _("Idle timeout exceeded, disconnecting.") + reason = _("Idle timeout exceeded, disconnecting.") for session in (session for session in self.sessions.values() if session.logged_in and IDLE_TIMEOUT > 0 and (tcurr - session.cmd_last) > IDLE_TIMEOUT): @@ -407,7 +418,6 @@ class ServerSessionHandler(SessionHandler): return self.sessions.get(sessid) return None - def announce_all(self, message): """ Send message to all connected sessions @@ -422,6 +432,7 @@ class ServerSessionHandler(SessionHandler): self.server.amp_protocol.call_remote_MsgServer2Portal(sessid=session.sessid, msg=text, data=kwargs) + def data_in(self, sessid, text="", **kwargs): """ Data Portal -> Server diff --git a/src/server/webserver.py b/src/server/webserver.py index fdea83bb99..defde838be 100644 --- a/src/server/webserver.py +++ b/src/server/webserver.py @@ -15,7 +15,7 @@ import urlparse from urllib import quote as urlquote from twisted.web import resource, http from twisted.internet import reactor -from twisted.application import service, internet +from twisted.application import internet from twisted.web.proxy import ReverseProxyResource from twisted.web.server import NOT_DONE_YET @@ -24,6 +24,7 @@ from django.core.handlers.wsgi import WSGIHandler from settings import UPSTREAM_IPS + # # X-Forwarded-For Handler # @@ -40,13 +41,14 @@ class HTTPChannelWithXForwardedFor(http.HTTPChannel): proxy_chain = req.getHeader('X-FORWARDED-FOR') if proxy_chain and client_ip in UPSTREAM_IPS: forwarded = proxy_chain.split(', ', 1)[CLIENT] - self.transport.client = (forwarded, port) + self.transport.client = (forwarded, port) # Monkey-patch Twisted to handle X-Forwarded-For. http.HTTPFactory.protocol = HTTPChannelWithXForwardedFor + class EvenniaReverseProxyResource(ReverseProxyResource): def getChild(self, path, request): """ @@ -58,7 +60,6 @@ class EvenniaReverseProxyResource(ReverseProxyResource): self.host, self.port, self.path + '/' + urlquote(path, safe=""), self.reactor) - def render(self, request): """ Render a request by forwarding it to the proxied server. @@ -77,6 +78,7 @@ class EvenniaReverseProxyResource(ReverseProxyResource): self.reactor.connectTCP(self.host, self.port, clientFactory) return NOT_DONE_YET + # # Website server resource # @@ -92,7 +94,7 @@ class DjangoWebRoot(resource.Resource): Setup the django+twisted resource """ resource.Resource.__init__(self) - self.wsgi_resource = WSGIResource(reactor, pool , WSGIHandler()) + self.wsgi_resource = WSGIResource(reactor, pool, WSGIHandler()) def getChild(self, path, request): """ @@ -102,6 +104,8 @@ class DjangoWebRoot(resource.Resource): path0 = request.prepath.pop(0) request.postpath.insert(0, path0) return self.wsgi_resource + + # # Threaded Webserver # @@ -114,14 +118,16 @@ class WSGIWebServer(internet.TCPServer): call with WSGIWebServer(threadpool, port, wsgi_resource) """ - def __init__(self, pool, *args, **kwargs ): + def __init__(self, pool, *args, **kwargs): "This just stores the threadpool" self.pool = pool internet.TCPServer.__init__(self, *args, **kwargs) + def startService(self): "Start the pool after the service" internet.TCPServer.startService(self) self.pool.start() + def stopService(self): "Safely stop the pool after service stop." internet.TCPServer.stopService(self) diff --git a/src/settings_default.py b/src/settings_default.py index 008be07f5e..1d3be18f4f 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -34,7 +34,7 @@ TELNET_INTERFACES = ['0.0.0.0'] # full use of OOB, you need to prepare functions to handle the data # server-side (see OOB_FUNC_MODULE). TELNET_ENABLED is required for this # to work. -TELNET_OOB_ENABLED = False # OBS - currently not fully implemented - do not use! +TELNET_OOB_ENABLED = False # Start the evennia django+twisted webserver so you can # browse the evennia website and the admin interface # (Obs - further web configuration can be found below @@ -85,8 +85,9 @@ LOG_DIR = os.path.join(GAME_DIR, 'logs') SERVER_LOG_FILE = os.path.join(LOG_DIR, 'server.log') PORTAL_LOG_FILE = os.path.join(LOG_DIR, 'portal.log') HTTP_LOG_FILE = os.path.join(LOG_DIR, 'http_requests.log') -# Rotate log files when server and/or portal stops. This will keep log file sizes down. -# Turn off to get ever growing log files and never loose log info. +# Rotate log files when server and/or portal stops. This will keep log +# file sizes down. Turn off to get ever growing log files and never +# loose log info. CYCLE_LOGFILES = True # Local time zone for this installation. All choices can be found here: # http://www.postgresql.org/docs/8.0/interactive/datetime-keywords.html#DATETIME-TIMEZONE-SET-TABLE @@ -114,20 +115,22 @@ ENCODINGS = ["utf-8", "latin-1", "ISO-8859-1"] # The game server opens an AMP port so that the portal can # communicate with it. This is an internal functionality of Evennia, usually # operating between two processes on the same machine. You usually don't need to -# change this unless you cannot use the default AMP port/host for whatever reason. +# change this unless you cannot use the default AMP port/host for +# whatever reason. AMP_HOST = 'localhost' AMP_PORT = 5000 AMP_INTERFACE = '127.0.0.1' # Caching speeds up all forms of database access, often considerably. There # are (currently) only two settings, "local" or None, the latter of which turns -# off all caching completely. Local caching stores data in the process. It's very -# fast but will go out of sync if more than one process writes to the database (such -# as when using procpool or an extensice web precense). +# off all caching completely. Local caching stores data in the process. It's +# very fast but will go out of sync if more than one process writes to the +# database (such as when using procpool or an extensice web precense). GAME_CACHE_TYPE = "local" # Attributes on objects are cached aggressively for speed. If the number of -# objects is large (and their attributes are often accessed) this can use up a lot of -# memory. So every now and then Evennia checks the size of this cache and resets -# it if it's too big. This variable sets the maximum size (in MB). +# objects is large (and their attributes are often accessed) this can use up +# a lot of memory. So every now and then Evennia checks the size of this +# cache and resets it if it's too big. This variable sets the maximum +# size (in MB). ATTRIBUTE_CACHE_MAXSIZE = 100 ###################################################################### @@ -146,13 +149,13 @@ ATTRIBUTE_CACHE_MAXSIZE = 100 # HOST - empty string is localhost (unused in sqlite3) # PORT - empty string defaults to localhost (unused in sqlite3) DATABASES = { - 'default':{ - 'ENGINE':'django.db.backends.sqlite3', - 'NAME':os.path.join(GAME_DIR, 'evennia.db3'), - 'USER':'', - 'PASSWORD':'', - 'HOST':'', - 'PORT':'' + 'default': { + 'ENGINE': 'django.db.backends.sqlite3', + 'NAME': os.path.join(GAME_DIR, 'evennia.db3'), + 'USER': '', + 'PASSWORD': '', + 'HOST': '', + 'PORT': '' }} ###################################################################### @@ -185,12 +188,14 @@ AT_INITIAL_SETUP_HOOK_MODULE = "" # at_server_stop() methods. These methods will be called every time # the server starts, reloads and resets/stops respectively. AT_SERVER_STARTSTOP_MODULE = "" -# List of one or more module paths to modules containing a function start_plugin_services(application). This module -# will be called with the main Evennia Server application when the Server is initiated. +# List of one or more module paths to modules containing a function start_ +# plugin_services(application). This module will be called with the main +# Evennia Server application when the Server is initiated. # It will be called last in the startup sequence. SERVER_SERVICES_PLUGIN_MODULES = [] -# List of one or more module paths to modules containing a function start_plugin_services(application). This module -# will be called with the main Evennia Portal application when the Portal is initiated. +# List of one or more module paths to modules containing a function +# start_plugin_services(application). This module will be called with the +# main Evennia Portal application when the Portal is initiated. # It will be called last in the startup sequence. PORTAL_SERVICES_PLUGIN_MODULES = [] # Module holding MSSP meta data. This is used by MUD-crawlers to determine @@ -208,9 +213,9 @@ OOB_PLUGIN_MODULE = "src.server.oob_msdp" # Default command sets ###################################################################### # Note that with the exception of the unloggedin set (which is not -# stored anywhere in the databse), changing these paths will only affect NEW created -# characters/objects, not those already in play. So if you plan to change -# this, it's recommended you do it before having created a lot of objects +# stored anywhere in the databse), changing these paths will only affect +# NEW created characters/objects, not those already in play. So if you plan to +# change this, it's recommended you do it before having created a lot of objects # (or simply reset the database after the change for simplicity). Remember # that you should never edit things in src/. Instead copy out the examples # in game/gamesrc/commands/examples up one level and re-point these settings @@ -236,8 +241,12 @@ SERVER_SESSION_CLASS = "src.server.serversession.ServerSession" # Base paths for typeclassed object classes. These paths must be # defined relative evennia's root directory. They will be searched in # order to find relative typeclass paths. -OBJECT_TYPECLASS_PATHS = ["game.gamesrc.objects", "game.gamesrc.objects.examples", "contrib"] -SCRIPT_TYPECLASS_PATHS = ["game.gamesrc.scripts", "game.gamesrc.scripts.examples", "contrib"] +OBJECT_TYPECLASS_PATHS = ["game.gamesrc.objects", + "game.gamesrc.objects.examples", + "contrib"] +SCRIPT_TYPECLASS_PATHS = ["game.gamesrc.scripts", + "game.gamesrc.scripts.examples", + "contrib"] PLAYER_TYPECLASS_PATHS = ["game.gamesrc.objects", "contrib"] COMM_TYPECLASS_PATHS = ["game.gamesrc.objects", "contrib"] @@ -304,20 +313,31 @@ TIME_MONTH_PER_YEAR = 12 # Default Player setup and access ###################################################################### -# Different Multisession modes allow a player (=account) to connect to the game simultaneously -# with multiple clients (=sessions). In modes 0,1 there is only one character created to the same -# name as the account at first login. In modes 1,2 no default character will be created and -# the MAX_NR_CHARACTERS value (below) defines how many characters are allowed. -# 0 - single session, one player, one character, when a new session is connected, the old one is disconnected -# 1 - multiple sessions, one player, one character, each session getting the same data -# 2 - multiple sessions, one player, many characters, each session getting data from different characters +# Different Multisession modes allow a player (=account) to connect to the +# game simultaneously with multiple clients (=sessions). In modes 0,1 there is +# only one character created to the same name as the account at first login. +# In modes 1,2 no default character will be created and the MAX_NR_CHARACTERS +# value (below) defines how many characters are allowed. +# 0 - single session, one player, one character, when a new session is +# connected, the old one is disconnected +# 1 - multiple sessions, one player, one character, each session getting +# the same data +# 2 - multiple sessions, one player, many characters, each session getting +# data from different characters MULTISESSION_MODE = 0 -# The maximum number of characters allowed for MULTISESSION_MODE 2. This is checked -# by the default ooc char-creation command. Forced to 1 for MULTISESSION_MODE 0 and 1. +# The maximum number of characters allowed for MULTISESSION_MODE 2. This is +# checked +# by the default ooc char-creation command. Forced to 1 for +# MULTISESSION_MODE 0 and 1. MAX_NR_CHARACTERS = 1 # The access hiearchy, in climbing order. A higher permission in the -# hierarchy includes access of all levels below it. Used by the perm()/pperm() lock functions. -PERMISSION_HIERARCHY = ("Players","PlayerHelpers","Builders", "Wizards", "Immortals") +# hierarchy includes access of all levels below it. Used by the perm()/pperm() +# lock functions. +PERMISSION_HIERARCHY = ("Players", + "PlayerHelpers", + "Builders", + "Wizards", + "Immortals") # The default permission given to all new players PERMISSION_PLAYER_DEFAULT = "Players" @@ -334,10 +354,10 @@ CHANNEL_PUBLIC = ("Public", ('ooc',), 'Public discussion', "control:perm(Wizards);listen:all();send:all()") # General info about the server CHANNEL_MUDINFO = ("MUDinfo", '', 'Informative messages', - "control:perm(Immortals);listen:perm(Immortals);send:false()") + "control:perm(Immortals);listen:perm(Immortals);send:false()") # Channel showing when new people connecting CHANNEL_CONNECTINFO = ("MUDconnections", '', 'Connection log', - "control:perm(Immortals);listen:perm(Wizards);send:false()") + "control:perm(Immortals);listen:perm(Wizards);send:false()") ###################################################################### # External Channel connections @@ -461,7 +481,7 @@ TEMPLATE_LOADERS = ( MIDDLEWARE_CLASSES = ( 'django.middleware.common.CommonMiddleware', 'django.contrib.sessions.middleware.SessionMiddleware', - 'django.contrib.messages.middleware.MessageMiddleware', # 1.4? + 'django.contrib.messages.middleware.MessageMiddleware', # 1.4? 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.middleware.csrf.CsrfViewMiddleware', 'django.middleware.doc.XViewMiddleware', diff --git a/src/typeclasses/__init__.py b/src/typeclasses/__init__.py index e69de29bb2..40a96afc6f 100644 --- a/src/typeclasses/__init__.py +++ b/src/typeclasses/__init__.py @@ -0,0 +1 @@ +# -*- coding: utf-8 -*- diff --git a/src/typeclasses/managers.py b/src/typeclasses/managers.py index 0a87fbd716..004d0351f3 100644 --- a/src/typeclasses/managers.py +++ b/src/typeclasses/managers.py @@ -13,6 +13,7 @@ from src.utils.dbserialize import to_pickle __all__ = ("AttributeManager", "TypedObjectManager") _GA = object.__getattribute__ + # Managers def _attr_pickled(method): @@ -29,24 +30,30 @@ def _attr_pickled(method): return method(self, *args, **kwargs) return update_wrapper(wrapper, method) + class AttributeManager(models.Manager): "Manager for handling Attributes." @_attr_pickled def get(self, *args, **kwargs): return super(AttributeManager, self).get(*args, **kwargs) @_attr_pickled + def filter(self,*args, **kwargs): return super(AttributeManager, self).filter(*args, **kwargs) @_attr_pickled + def exclude(self,*args, **kwargs): return super(AttributeManager, self).exclude(*args, **kwargs) @_attr_pickled + def values(self,*args, **kwargs): return super(AttributeManager, self).values(*args, **kwargs) @_attr_pickled + def values_list(self,*args, **kwargs): return super(AttributeManager, self).values_list(*args, **kwargs) @_attr_pickled + def exists(self,*args, **kwargs): return super(AttributeManager, self).exists(*args, **kwargs) @@ -113,11 +120,11 @@ class TagManager(models.Manager): search_key (string) - the tag identifier category (string) - the tag category - Returns a single Tag (or None) if both key and category is given, otherwise - it will return a list. + Returns a single Tag (or None) if both key and category is given, + otherwise it will return a list. """ - key_cands = Q(db_key__iexact=key.lower().strip()) if key!=None else Q() - cat_cands = Q(db_category__iexact=category.lower().strip()) if category!=None else Q() + key_cands = Q(db_key__iexact=key.lower().strip()) if key is not None else Q() + cat_cands = Q(db_category__iexact=category.lower().strip()) if category is not None else Q() tags = self.filter(key_cands & cat_cands) if key and category: return tags[0] if tags else None @@ -132,8 +139,8 @@ class TagManager(models.Manager): key (string) - the tag identifier category (string) - the tag category """ - key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if search_key!=None else Q() - cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category!=None else Q() + key_cands = Q(db_tags__db_key__iexact=key.lower().strip()) if key is not None else Q() + cat_cands = Q(db_tags__db_category__iexact=category.lower().strip()) if category is not None else Q() return objclass.objects.filter(key_cands & cat_cands) def create_tag(self, key=None, category=None, data=None): @@ -144,19 +151,21 @@ class TagManager(models.Manager): data on a tag (it is not part of what makes the tag unique). """ - data = str(data) if data!=None else None + data = str(data) if data is not None else None tag = self.get_tag(key=key, category=category) - if tag and data != None: + if tag and data is not None: tag.db_data = data tag.save() elif not tag: - tag = self.create(db_key=key.lower().strip() if key!=None else None, - db_category=category.lower().strip() if category and key!=None else None, - db_data=str(data) if data!=None else None) + tag = self.create(db_key=key.lower().strip() if key is not None else None, + db_category=category.lower().strip() + if category and key is not None else None, + db_data=str(data) if data is not None else None) tag.save() return make_iter(tag)[0] + # # helper functions for the TypedObjectManager. # @@ -171,9 +180,11 @@ def returns_typeclass_list(method): "decorator. Returns a list." self.__doc__ = method.__doc__ matches = make_iter(method(self, *args, **kwargs)) - return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj for dbobj in make_iter(matches)] + return [(hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj + for dbobj in make_iter(matches)] return update_wrapper(func, method) + def returns_typeclass(method): """ Decorator: Will always return a single typeclassed result or None. @@ -184,7 +195,7 @@ def returns_typeclass(method): matches = method(self, *args, **kwargs) dbobj = matches and make_iter(matches)[0] or None if dbobj: - return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj + return (hasattr(dbobj, "typeclass") and dbobj.typeclass) or dbobj return None return update_wrapper(func, method) @@ -240,9 +251,9 @@ class TypedObjectManager(idmapper.manager.SharedMemoryManager): given boundaries. """ retval = super(TypedObjectManager, self).all() - if min_dbref != None: + if min_dbref is not None: retval = retval.filter(id__gte=self.dbref(min_dbref, reqhash=False)) - if max_dbref != None: + if max_dbref is not None: retval = retval.filter(id__lte=self.dbref(max_dbref, reqhash=False)) return retval diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 1ce01179d4..f052d70cb5 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -147,6 +147,7 @@ class Attribute(SharedMemoryModel): # self.cached_value = value # self.no_cache = False #return self.cached_value + #@value.setter def __value_set(self, new_value): """ @@ -168,13 +169,13 @@ class Attribute(SharedMemoryModel): # self._track_db_value_change.update(self.cached_value) #except AttributeError: # pass + #@value.deleter def __value_del(self): "Deleter. Allows for del attr.value. This removes the entire attribute." self.delete() value = property(__value_get, __value_set, __value_del) - # # # Attribute methods @@ -202,6 +203,7 @@ class Attribute(SharedMemoryModel): """ pass + # # Handlers making use of the Attribute model # @@ -226,19 +228,21 @@ class AttributeHandler(object): def has(self, key, category=None): """ - Checks if the given Attribute (or list of Attributes) exists on the object. + Checks if the given Attribute (or list of Attributes) exists on + the object. If an iterable is given, returns list of booleans. """ - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() catkey = to_str(category, force_string=True).lower() searchkeys = ["%s_%s" % (k.lower(), catkey) for k in make_iter(key)] ret = [self._cache[skey] for skey in searchkeys if skey in self._cache] return ret[0] if len(ret) == 1 else ret - def get(self, key=None, category=None, default=None, return_obj=False, strattr=False, - raise_exception=False, accessing_obj=None, default_access=True): + def get(self, key=None, category=None, default=None, return_obj=False, + strattr=False, raise_exception=False, accessing_obj=None, + default_access=True): """ Returns the value of the given Attribute or list of Attributes. strattr will cause the string-only value field instead of the normal @@ -253,7 +257,7 @@ class AttributeHandler(object): checked before displaying each looked-after Attribute. If no accessing_obj is given, no check will be done. """ - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() ret = [] catkey = to_str(category, force_string=True).lower() @@ -280,32 +284,36 @@ class AttributeHandler(object): ret = ret if return_obj else [attr.value if attr else None for attr in ret] return ret[0] if len(ret)==1 else ret - def add(self, key, value, category=None, lockstring="", strattr=False, accessing_obj=None, default_access=True): + def add(self, key, value, category=None, lockstring="", + strattr=False, accessing_obj=None, default_access=True): """ Add attribute to object, with optional lockstring. - If strattr is set, the db_strvalue field will be used (no pickling). Use the get() method - with the strattr keyword to get it back. + If strattr is set, the db_strvalue field will be used (no pickling). + Use the get() method with the strattr keyword to get it back. If accessing_obj is given, self.obj's 'attrcreate' lock access - will be checked against it. If no accessing_obj is given, no check will be done. + will be checked against it. If no accessing_obj is given, no check + will be done. """ - if accessing_obj and not self.obj.access(accessing_obj, self._attrcreate, default=default_access): + if accessing_obj and not self.obj.access(accessing_obj, + self._attrcreate, default=default_access): # check create access return - if self._cache == None: + if self._cache is None: self._recache() cachekey = "%s_%s" % (key.lower(), to_str(category, force_string=True).lower()) attr_obj = self._cache.get(cachekey) if not attr_obj: # no old attr available; create new. attr_obj = Attribute(db_key=key, db_category=category) - attr_obj.save() # important + attr_obj.save() # important _GA(self.obj, self._m2m_fieldname).add(attr_obj) self._cache[cachekey] = attr_obj if lockstring: attr_obj.locks.add(lockstring) - # we shouldn't need to fear stale objects, the field signalling should catch all cases + # we shouldn't need to fear stale objects, the field signalling + # should catch all cases if strattr: # store as a simple string attr_obj.strvalue = value @@ -315,18 +323,21 @@ class AttributeHandler(object): attr_obj.value = value attr_obj.strvalue = None - def remove(self, key, raise_exception=False, category=None, accessing_obj=None, default_access=True): + def remove(self, key, raise_exception=False, category=None, + accessing_obj=None, default_access=True): """Remove attribute or a list of attributes from object. - If accessing_obj is given, will check against the 'attredit' lock. If not given, this check is skipped. + If accessing_obj is given, will check against the 'attredit' lock. + If not given, this check is skipped. """ - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() catkey = to_str(category, force_string=True).lower() for keystr in ("%s_%s" % (k.lower(), catkey) for k in make_iter(key)): attr_obj = self._cache.get(keystr) if attr_obj: - if accessing_obj and not attr_obj.access(accessing_obj, self._attredit, default=default_access): + if accessing_obj and not attr_obj.access(accessing_obj, + self._attredit, default=default_access): continue attr_obj.delete() elif not attr_obj and raise_exception: @@ -353,7 +364,7 @@ class AttributeHandler(object): each attribute before returning them. If not given, this check is skipped. """ - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() catkey = "_%s" % to_str(category, force_string=True).lower() return [attr for key, attr in self._cache.items() if key.endswith(catkey)] @@ -367,6 +378,7 @@ class AttributeHandler(object): #else: # return list(all_attrs) + class NickHandler(AttributeHandler): """ Handles the addition and removal of Nicks @@ -401,27 +413,33 @@ class NickHandler(AttributeHandler): return super(NickHandler, self).all(category=category) return _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith="nick_") + class NAttributeHandler(object): """ - This stand-alone handler manages non-database saved properties by storing them - as properties on obj.ndb. It has the same methods as AttributeHandler, but they - are much simplified. + This stand-alone handler manages non-database saved properties by storing + them as properties on obj.ndb. It has the same methods as AttributeHandler, + but they are much simplified. """ def __init__(self, obj): "initialized on the object" self.ndb = _GA(obj, "ndb") + def has(self, key): "Check if object has this attribute or not" - return _GA(self.ndb, key) # ndb returns None if not found + return _GA(self.ndb, key) # ndb returns None if not found + def get(self, key): "Returns named key value" return _GA(self.ndb, key) + def add(self, key, value): "Add new key and value" _SA(self.ndb, key, value) + def remove(self, key): "Remove key from storage" _DA(self.ndb, key) + def all(self): "List all keys stored" if callable(self.ndb.all): @@ -429,6 +447,7 @@ class NAttributeHandler(object): else: return [val for val in self.ndb.__dict__.keys() if not val.startswith('_')] + #------------------------------------------------------------ # # Tags @@ -456,18 +475,25 @@ class Tag(models.Model): this uses the 'aliases' tag category, which is also checked by the default search functions of Evennia to allow quick searches by alias. """ - db_key = models.CharField('key', max_length=255, null=True, help_text="tag identifier", db_index=True) - db_category = models.CharField('category', max_length=64, null=True, help_text="tag category", db_index=True) - db_data = models.TextField('data', null=True, blank=True, help_text="optional data field with extra information. This is not searched for.") + db_key = models.CharField('key', max_length=255, null=True, + help_text="tag identifier", db_index=True) + db_category = models.CharField('category', max_length=64, null=True, + help_text="tag category", db_index=True) + db_data = models.TextField('data', null=True, blank=True, + help_text="optional data field with extra information. This is not searched for.") objects = managers.TagManager() + + class Meta: "Define Django meta options" verbose_name = "Tag" - unique_together =(('db_key', 'db_category'),) + unique_together = (('db_key', 'db_category'),) index_together = (('db_key', 'db_category'),) + def __unicode__(self): return u"%s" % self.db_key + def __str__(self): return str(self.db_key) @@ -489,7 +515,8 @@ class TagHandler(object): using the category """ self.obj = obj - self.prefix = "%s%s" % (category_prefix.strip().lower() if category_prefix else "", self._base_category) + self.prefix = "%s%s" % (category_prefix.strip().lower() + if category_prefix else "", self._base_category) self._cache = None def _recache(self): @@ -499,38 +526,46 @@ class TagHandler(object): def add(self, tag, category=None, data=None): "Add a new tag to the handler. Tag is a string or a list of strings." for tagstr in make_iter(tag): - tagstr = tagstr.strip().lower() if tagstr!=None else None - category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "") - data = str(data) if data!=None else None - # this will only create tag if no matches existed beforehand (it will overload - # data on an existing tag since that is not considered part of making the tag unique) + tagstr = tagstr.strip().lower() if tagstr is not None else None + category = "%s%s" % (self.prefix, category.strip().lower() if category is not None else "") + data = str(data) if data is not None else None + # this will only create tag if no matches existed beforehand (it + # will overload data on an existing tag since that is not + # considered part of making the tag unique) tagobj = Tag.objects.create_tag(key=tagstr, category=category, data=data) _GA(self.obj, self._m2m_fieldname).add(tagobj) - if self._cache == None: + if self._cache is None: self._recache() self._cache[tagstr] = True def get(self, key, category="", return_obj=False): - "Get the data field for the given tag or list of tags. If return_obj=True, return the matching Tag objects instead." - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + """ + Get the data field for the given tag or list of tags. If + return_obj=True, return the matching Tag objects instead. + """ + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() ret = [] - category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "") - ret = [val for val in (self._cache.get(keystr.strip().lower()) for keystr in make_iter(key)) if val] + category = "%s%s" % (self.prefix, category.strip().lower() + if category is not None else "") + ret = [val for val in (self._cache.get(keystr.strip().lower()) + for keystr in make_iter(key)) if val] ret = ret if return_obj else [to_str(tag.db_data) for tag in ret if tag] - return ret[0] if len(ret)==1 else ret + return ret[0] if len(ret) == 1 else ret def remove(self, tag, category=None): "Remove a tag from the handler" - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() for tag in make_iter(tag): - if not (tag or tag.strip()): # we don't allow empty tags + if not (tag or tag.strip()): # we don't allow empty tags continue - tagstr = tag.strip().lower() if tag!=None else None - category = "%s%s" % (self.prefix, category.strip().lower() if category!=None else "") - #TODO This does not delete the tag object itself. Maybe it should do that when no - # objects reference the tag anymore? + tagstr = tag.strip().lower() if tag is not None else None + category = "%s%s" % (self.prefix, category.strip().lower() + if category is not None else "") + + # This does not delete the tag object itself. Maybe it should do + # that when no objects reference the tag anymore (how to check)? tagobj = self.obj.db_tags.filter(db_key=tagstr, db_category=category) if tagobj: _GA(self.obj, self._m2m_fieldname).remove(tagobj[0]) @@ -543,19 +578,22 @@ class TagHandler(object): def all(self): "Get all tags in this handler" - if self._cache == None or not _TYPECLASS_AGGRESSIVE_CACHE: + if self._cache is None or not _TYPECLASS_AGGRESSIVE_CACHE: self._recache() return self._cache.keys() #return [to_str(p[0]) for p in _GA(self.obj, self._m2m_fieldname).filter(db_category__startswith=self.prefix).values_list("db_key") if p[0]] def __str__(self): return ",".join(self.all()) + def __unicode(self): return u",".join(self.all()) + class AliasHandler(TagHandler): _base_category = "alias" + class PermissionHandler(TagHandler): _base_category = "permission" @@ -591,19 +629,23 @@ class TypedObject(SharedMemoryModel): # TypedObject Database Model setup # # - # These databse fields are all accessed and set using their corresponding properties, - # named same as the field, but without the db_* prefix (no separate save() call is needed) + # These databse fields are all accessed and set using their corresponding + # properties, named same as the field, but without the db_* prefix + # (no separate save() call is needed) - # Main identifier of the object, for searching. Is accessed with self.key or self.name + # Main identifier of the object, for searching. Is accessed with self.key + # or self.name db_key = models.CharField('key', max_length=255, db_index=True) - # This is the python path to the type class this object is tied to the type class is what defines what kind of Object this is) + # This is the python path to the type class this object is tied to the + # typeclass is what defines what kind of Object this is) db_typeclass_path = models.CharField('typeclass', max_length=255, null=True, help_text="this defines what 'type' of entity this is. This variable holds a Python path to a module with a valid Evennia Typeclass.") # Creation date. This is not changed once the object is created. db_date_created = models.DateTimeField('creation date', editable=False, auto_now_add=True) # Permissions (access these through the 'permissions' property) #db_permissions = models.CharField('permissions', max_length=255, blank=True, - # help_text="a comma-separated list of text strings checked by in-game locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. Character objects use 'Players' by default. Most other objects don't have any permissions.") + # help_text="a comma-separated list of text strings checked by + # in-game locks. They are often used for hierarchies, such as letting a Player have permission 'Wizards', 'Builders' etc. Character objects use 'Players' by default. Most other objects don't have any permissions.") # Lock storage db_lock_storage = models.TextField('locks', blank=True, help_text="locks limit access to an entity. A lock is defined as a 'lock string' on the form 'type:lockfunctions', defining what functionality is locked and how to determine access. Not defining a lock means no access is granted.") @@ -647,9 +689,14 @@ class TypedObject(SharedMemoryModel): # is the object in question). # name property (alias to self.key) - def __name_get(self): return self.key - def __name_set(self, value): self.key = value - def __name_del(self): raise Exception("Cannot delete name") + def __name_get(self): + return self.key + + def __name_set(self, value): + self.key = value + + def __name_del(self): + raise Exception("Cannot delete name") name = property(__name_get, __name_set, __name_del) # @@ -711,8 +758,10 @@ class TypedObject(SharedMemoryModel): dbid = _GA(self, "id") set_prop_cache(self, "_dbid", dbid) return dbid + def __dbid_set(self, value): raise Exception("dbid cannot be set!") + def __dbid_del(self): raise Exception("dbid cannot be deleted!") dbid = property(__dbid_get, __dbid_set, __dbid_del) @@ -723,13 +772,14 @@ class TypedObject(SharedMemoryModel): Returns the object's dbref on the form #NN. """ return "#%s" % _GA(self, "_TypedObject__dbid_get")() + def __dbref_set(self): raise Exception("dbref cannot be set!") + def __dbref_del(self): raise Exception("dbref cannot be deleted!") dbref = property(__dbref_get, __dbref_set, __dbref_del) - # typeclass property #@property def __typeclass_get(self): @@ -760,9 +810,10 @@ class TypedObject(SharedMemoryModel): else: # handle loading/importing of typeclasses, searching all paths. # (self._typeclass_paths is a shortcut to settings.TYPECLASS_*_PATHS - # where '*' is either OBJECT, SCRIPT or PLAYER depending on the typed - # entities). - typeclass_paths = [path] + ["%s.%s" % (prefix, path) for prefix in _GA(self, '_typeclass_paths')] + # where '*' is either OBJECT, SCRIPT or PLAYER depending on the + # typed entities). + typeclass_paths = [path] + ["%s.%s" % (prefix, path) + for prefix in _GA(self, '_typeclass_paths')] for tpath in typeclass_paths: @@ -787,8 +838,9 @@ class TypedObject(SharedMemoryModel): errstring += " to specify the actual typeclass name inside the module too." else: errstring += "\n%s" % typeclass # this will hold a growing error message. - # If we reach this point we couldn't import any typeclasses. Return default. It's up to the calling - # method to use e.g. self.is_typeclass() to detect that the result is not the one asked for. + # If we reach this point we couldn't import any typeclasses. Return + # default. It's up to the calling method to use e.g. self.is_typeclass() + # to detect that the result is not the one asked for. _GA(self, "_display_errmsg")(errstring) _SA(self, "typeclass_lasterrmsg", errstring) return _GA(self, "_get_default_typeclass")(cache=False, silent=False, save=False) @@ -818,12 +870,13 @@ class TypedObject(SharedMemoryModel): return None try: modpath, class_name = path.rsplit('.', 1) - module = __import__(modpath, fromlist=["none"]) + module = __import__(modpath, fromlist=["none"]) return module.__dict__[class_name] except ImportError: trc = sys.exc_traceback if not trc.tb_next: - # we separate between not finding the module, and finding a buggy one. + # we separate between not finding the module, and finding + # a buggy one. errstring = "Typeclass not found trying path '%s'." % path else: # a bug in the module is reported normally. @@ -866,7 +919,8 @@ class TypedObject(SharedMemoryModel): if not callable(typeclass): # if typeclass still doesn't exist at this point, we're in trouble. - # fall back to hardcoded core class which is wrong for e.g. scripts/players etc. + # fall back to hardcoded core class which is wrong for e.g. + # scripts/players etc. failpath = defpath defpath = "src.objects.objects.Object" typeclass = _GA(self, "_path_import")(defpath) @@ -876,7 +930,8 @@ class TypedObject(SharedMemoryModel): errstring += "\n Using Evennia's default class '%s'." % defpath _GA(self, "_display_errmsg")(errstring) if not callable(typeclass): - # if this is still giving an error, Evennia is wrongly configured or buggy + # if this is still giving an error, Evennia is wrongly + # configured or buggy raise Exception("CRITICAL ERROR: The final fallback typeclass %s cannot load!!" % defpath) typeclass = typeclass(self) if save: @@ -911,15 +966,17 @@ class TypedObject(SharedMemoryModel): typeclass = _GA(typeclass, "path") except AttributeError: pass - typeclasses = [typeclass] + ["%s.%s" % (path, typeclass) for path in _GA(self, "_typeclass_paths")] + typeclasses = [typeclass] + ["%s.%s" % (path, typeclass) + for path in _GA(self, "_typeclass_paths")] if exact: current_path = _GA(self.typeclass, "path") #"_GA(self, "_cached_db_typeclass_path") return typeclass and any((current_path == typec for typec in typeclasses)) else: # check parent chain return any((cls for cls in self.typeclass.__class__.mro() - if any(("%s.%s" % (_GA(cls,"__module__"), _GA(cls,"__name__")) == typec for typec in typeclasses)))) - + if any(("%s.%s" % (_GA(cls, "__module__"), + _GA(cls, "__name__")) == typec + for typec in typeclasses)))) def delete(self, *args, **kwargs): """ @@ -927,7 +984,6 @@ class TypedObject(SharedMemoryModel): """ super(TypedObject, self).delete(*args, **kwargs) - # # Object manipulation methods # @@ -1020,13 +1076,15 @@ class TypedObject(SharedMemoryModel): def check_permstring(self, permstring): """ - This explicitly checks if we hold particular permission without involving - any locks. + This explicitly checks if we hold particular permission without + involving any locks. """ if hasattr(self, "player"): - if self.player and self.player.is_superuser: return True + if self.player and self.player.is_superuser: + return True else: - if self.is_superuser: return True + if self.is_superuser: + return True if not permstring: return False @@ -1048,9 +1106,9 @@ class TypedObject(SharedMemoryModel): def flush_from_cache(self): """ - Flush this object instance from cache, forcing an object reload. Note that this - will kill all temporary attributes on this object since it will be recreated - as a new Typeclass instance. + Flush this object instance from cache, forcing an object reload. + Note that this will kill all temporary attributes on this object + since it will be recreated as a new Typeclass instance. """ self.__class__.flush_cached_instance(self) @@ -1068,8 +1126,8 @@ class TypedObject(SharedMemoryModel): and del obj.db.attrname and - all_attr = obj.db.all (unless there is no attribute named 'all', in which - case that will be returned instead). + all_attr = obj.db.all() (unless there is an attribute + named 'all', in which case that will be returned instead). """ try: return self._db_holder @@ -1079,6 +1137,7 @@ class TypedObject(SharedMemoryModel): def __init__(self, obj): _SA(self, 'obj', obj) _SA(self, "attrhandler", _GA(_GA(self, "obj"), "attributes")) + def __getattribute__(self, attrname): if attrname == 'all': # we allow to overload our default .all @@ -1087,21 +1146,26 @@ class TypedObject(SharedMemoryModel): return attr return _GA(self, 'all') return _GA(self, "attrhandler").get(attrname) + def __setattr__(self, attrname, value): _GA(self, "attrhandler").add(attrname, value) + def __delattr__(self, attrname): _GA(self, "attrhandler").remove(attrname) + def get_all(self): return _GA(self, "attrhandler").all() all = property(get_all) self._db_holder = DbHolder(self) return self._db_holder + #@db.setter def __db_set(self, value): "Stop accidentally replacing the db object" string = "Cannot assign directly to db object! " string += "Use db.attr=value instead." raise Exception(string) + #@db.deleter def __db_del(self): "Stop accidental deletion." @@ -1129,24 +1193,28 @@ class TypedObject(SharedMemoryModel): return [val for val in self.__dict__.keys() if not val.startswith('_')] all = property(get_all) + def __getattribute__(self, key): # return None if no matching attribute was found. try: return _GA(self, key) except AttributeError: return None + def __setattr__(self, key, value): # hook the oob handler here #call_ndb_hooks(self, key, value) _SA(self, key, value) self._ndb_holder = NdbHolder() return self._ndb_holder + #@ndb.setter def __ndb_set(self, value): "Stop accidentally replacing the db object" string = "Cannot assign directly to ndb object! " string = "Use ndb.attr=value instead." raise Exception(string) + #@ndb.deleter def __ndb_del(self): "Stop accidental deletion." @@ -1239,12 +1307,12 @@ class TypedObject(SharedMemoryModel): value to None using this method. Use set_attribute. """ logger.log_depmsg("obj.attr() is deprecated. Use handlers obj.db or obj.attributes.") - if attribute_name == None: + if attribute_name is None: # act as a list method return _GA(self, "attributes").all() - elif delete == True: + elif delete is True: _GA(self, "attributes").remove(attribute_name) - elif value == None: + elif value is None: # act as a getter. return _GA(self, "attributes").get(attribute_name) else: @@ -1272,12 +1340,12 @@ class TypedObject(SharedMemoryModel): """ logger.log_depmsg("obj.secure_attr() is deprecated. Use obj.attributes methods, giving accessing_obj keyword.") - if attribute_name == None: + if attribute_name is None: return _GA(self, "attributes").all(accessing_obj=accessing_object, default_access=default_access_read) - elif delete == True: + elif delete is True: # act as deleter _GA(self, "attributes").remove(attribute_name, accessing_obj=accessing_object, default_access=default_access_edit) - elif value == None: + elif value is None: # act as getter return _GA(self, "attributes").get(attribute_name, accessing_obj=accessing_object, default_access=default_access_read) else: @@ -1296,17 +1364,17 @@ class TypedObject(SharedMemoryModel): a method call. Will return None if trying to access a non-existing property. """ logger.log_depmsg("obj.nattr() is deprecated. Use obj.nattributes instead.") - if attribute_name == None: + if attribute_name is None: # act as a list method if callable(self.ndb.all): return self.ndb.all() else: return [val for val in self.ndb.__dict__.keys() if not val.startswith['_']] - elif delete == True: + elif delete is True: if hasattr(self.ndb, attribute_name): _DA(_GA(self, "ndb"), attribute_name) - elif value == None: + elif value is None: # act as a getter. if hasattr(self.ndb, attribute_name): _GA(_GA(self, "ndb"), attribute_name) diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py index 8bd1c2854b..3920cbe422 100644 --- a/src/typeclasses/typeclass.py +++ b/src/typeclasses/typeclass.py @@ -29,6 +29,7 @@ PROTECTED = ('id', 'dbobj', 'db', 'ndb', 'objects', 'typeclass', 'db_player', 'attr', 'save', 'delete', 'db_model_name','attribute_class', 'typeclass_paths') + # If this is true, all non-protected property assignments # are directly stored to a database attribute @@ -49,6 +50,7 @@ class MetaTypeClass(type): def __str__(cls): return "%s" % cls.__name__ + class TypeClass(object): """ This class implements a 'typeclass' object. This is connected @@ -70,8 +72,10 @@ class TypeClass(object): def __init__(self, dbobj): """ Initialize the object class. There are two ways to call this class. - o = object_class(dbobj) : this is used to initialize dbobj with the class name - o = dbobj.object_class(dbobj) : this is used when dbobj.object_class is already set. + o = object_class(dbobj) : this is used to initialize dbobj with the + class name + o = dbobj.object_class(dbobj) : this is used when dbobj.object_class + is already set. """ # typecheck of dbobj - we can't allow it to be added here @@ -145,11 +149,10 @@ class TypeClass(object): except AttributeError: return id(self) == id(other) - def __delattr__(self, propname): """ - Transparently deletes data from the typeclass or dbobj by first searching on the typeclass, - secondly on the dbobj.db. + Transparently deletes data from the typeclass or dbobj by first + searching on the typeclass, secondly on the dbobj.db. Will not allow deletion of properties stored directly on dbobj. """ if propname in PROTECTED: @@ -166,7 +169,7 @@ class TypeClass(object): dbobj = _GA(self, 'dbobj') except AttributeError: log_trace("This is probably due to an unsafe reload.") - return # ignore delete + return # ignore delete try: dbobj.del_attribute(propname, raise_exception=True) except AttributeError: @@ -178,5 +181,6 @@ class TypeClass(object): def __str__(self): "represent the object" return self.key + def __unicode__(self): return u"%s" % self.key diff --git a/src/utils/ansi.py b/src/utils/ansi.py index a70b7b02be..54d29fe342 100644 --- a/src/utils/ansi.py +++ b/src/utils/ansi.py @@ -59,6 +59,7 @@ ANSI_SPACE = " " # Escapes ANSI_ESCAPES = ("{{", "%%", "\\\\") + class ANSIParser(object): """ A class that parses ansi markup @@ -75,10 +76,11 @@ class ANSIParser(object): # MUX-style mappings %cr %cn etc self.mux_ansi_map = [ - # commented out by default; they (especially blink) are potentially annoying - (r'%r', ANSI_RETURN), - (r'%t', ANSI_TAB), - (r'%b', ANSI_SPACE), + # commented out by default; they (especially blink) are + # potentially annoying + (r'%r', ANSI_RETURN), + (r'%t', ANSI_TAB), + (r'%b', ANSI_SPACE), #(r'%cf', ANSI_BLINK), #(r'%ci', ANSI_INVERSE), (r'%cr', ANSI_RED), @@ -118,18 +120,18 @@ class ANSIParser(object): (r'{M', normal + ANSI_MAGENTA), (r'{c', hilite + ANSI_CYAN), (r'{C', normal + ANSI_CYAN), - (r'{w', hilite + ANSI_WHITE), # pure white - (r'{W', normal + ANSI_WHITE), #light grey - (r'{x', hilite + ANSI_BLACK), #dark grey - (r'{X', normal + ANSI_BLACK), #pure black - (r'{n', normal) #reset + (r'{w', hilite + ANSI_WHITE), # pure white + (r'{W', normal + ANSI_WHITE), # light grey + (r'{x', hilite + ANSI_BLACK), # dark grey + (r'{X', normal + ANSI_BLACK), # pure black + (r'{n', normal) # reset ] # xterm256 {123, %c134, self.xterm256_map = [ (r'%c([0-5]{3})', self.parse_rgb), # %c123 - foreground colour - (r'%c(b[0-5]{3})', self.parse_rgb), # %cb123 - background colour + (r'%c(b[0-5]{3})', self.parse_rgb), # %cb123 - background colour (r'{([0-5]{3})', self.parse_rgb), # {123 - foreground colour (r'{(b[0-5]{3})', self.parse_rgb) # {b123 - background colour ] @@ -145,7 +147,8 @@ class ANSIParser(object): # prepare matching ansi codes overall self.ansi_regex = re.compile("\033\[[0-9;]+m") - # escapes - these double-chars will be replaced with a single instance of each + # escapes - these double-chars will be replaced with a single + # instance of each self.ansi_escapes = re.compile(r"(%s)" % "|".join(ANSI_ESCAPES), re.DOTALL) def parse_rgb(self, rgbmatch): @@ -174,37 +177,61 @@ class ANSIParser(object): #print "ANSI convert:", red, green, blue # xterm256 not supported, convert the rgb value to ansi instead if red == green and red == blue and red < 2: - if background: return ANSI_BACK_BLACK - elif red >= 1: return ANSI_HILITE + ANSI_BLACK - else: return ANSI_NORMAL + ANSI_BLACK + if background: + return ANSI_BACK_BLACK + elif red >= 1: + return ANSI_HILITE + ANSI_BLACK + else: + return ANSI_NORMAL + ANSI_BLACK elif red == green and red == blue: - if background: return ANSI_BACK_WHITE - elif red >= 4: return ANSI_HILITE + ANSI_WHITE - else: return ANSI_NORMAL + ANSI_WHITE + if background: + return ANSI_BACK_WHITE + elif red >= 4: + return ANSI_HILITE + ANSI_WHITE + else: + return ANSI_NORMAL + ANSI_WHITE elif red > green and red > blue: - if background: return ANSI_BACK_RED - elif red >= 3: return ANSI_HILITE + ANSI_RED - else: return ANSI_NORMAL + ANSI_RED + if background: + return ANSI_BACK_RED + elif red >= 3: + return ANSI_HILITE + ANSI_RED + else: + return ANSI_NORMAL + ANSI_RED elif red == green and red > blue: - if background: return ANSI_BACK_YELLOW - elif red >= 3: return ANSI_HILITE + ANSI_YELLOW - else: return ANSI_NORMAL + ANSI_YELLOW + if background: + return ANSI_BACK_YELLOW + elif red >= 3: + return ANSI_HILITE + ANSI_YELLOW + else: + return ANSI_NORMAL + ANSI_YELLOW elif red == blue and red > green: - if background: return ANSI_BACK_MAGENTA - elif red >= 3: return ANSI_HILITE + ANSI_MAGENTA - else: return ANSI_NORMAL + ANSI_MAGENTA + if background: + return ANSI_BACK_MAGENTA + elif red >= 3: + return ANSI_HILITE + ANSI_MAGENTA + else: + return ANSI_NORMAL + ANSI_MAGENTA elif green > blue: - if background: return ANSI_BACK_GREEN - elif green >= 3: return ANSI_HILITE + ANSI_GREEN - else: return ANSI_NORMAL + ANSI_GREEN + if background: + return ANSI_BACK_GREEN + elif green >= 3: + return ANSI_HILITE + ANSI_GREEN + else: + return ANSI_NORMAL + ANSI_GREEN elif green == blue: - if background: return ANSI_BACK_CYAN - elif green >= 3: return ANSI_HILITE + ANSI_CYAN - else: return ANSI_NORMAL + ANSI_CYAN + if background: + return ANSI_BACK_CYAN + elif green >= 3: + return ANSI_HILITE + ANSI_CYAN + else: + return ANSI_NORMAL + ANSI_CYAN else: # mostly blue - if background: return ANSI_BACK_BLUE - elif blue >= 3: return ANSI_HILITE + ANSI_BLUE - else: return ANSI_NORMAL + ANSI_BLUE + if background: + return ANSI_BACK_BLUE + elif blue >= 3: + return ANSI_HILITE + ANSI_BLUE + else: + return ANSI_NORMAL + ANSI_BLUE def parse_ansi(self, string, strip_ansi=False, xterm256=False): """ @@ -227,13 +254,14 @@ class ANSIParser(object): part = sub[0].sub(sub[1], part) string += "%s%s" % (part, sep[0].strip()) if strip_ansi: - # remove all ansi codes (including those manually inserted in string) + # remove all ansi codes (including those manually + # inserted in string) string = self.ansi_regex.sub("", string) return string - ANSI_PARSER = ANSIParser() + # # Access function # @@ -245,8 +273,9 @@ def parse_ansi(string, strip_ansi=False, parser=ANSI_PARSER, xterm256=False): """ return parser.parse_ansi(string, strip_ansi=strip_ansi, xterm256=xterm256) + def raw(string): """ Escapes a string into a form which won't be colorized by the ansi parser. """ - return string.replace('{','{{').replace('%','%%') + return string.replace('{', '{{').replace('%', '%%') diff --git a/src/utils/batchprocessors.py b/src/utils/batchprocessors.py index 436418b44d..770bec0894 100644 --- a/src/utils/batchprocessors.py +++ b/src/utils/batchprocessors.py @@ -167,16 +167,18 @@ script = create.create_script() import re import codecs -import traceback, sys -from traceback import format_exc +import traceback +import sys +#from traceback import format_exc from django.conf import settings from src.utils import logger from src.utils import utils -from game import settings as settings_module +#from game import settings as settings_module ENCODINGS = settings.ENCODINGS CODE_INFO_HEADER = re.compile(r"\(.*?\)") + #------------------------------------------------------------ # Helper function #------------------------------------------------------------ @@ -216,7 +218,7 @@ def read_batchfile(pythonpath, file_ending='.py'): continue load_errors = [] - err =None + err = None # We have successfully found and opened the file. Now actually # try to decode it using the given protocol. try: @@ -246,6 +248,7 @@ def read_batchfile(pythonpath, file_ending='.py'): else: return lines + #------------------------------------------------------------ # # Batch-command processor @@ -261,9 +264,9 @@ class BatchCommandProcessor(object): """ This parses the lines of a batchfile according to the following rules: - 1) # at the beginning of a line marks the end of the command before it. - It is also a comment and any number of # can exist on subsequent - lines (but not inside comments). + 1) # at the beginning of a line marks the end of the command before + it. It is also a comment and any number of # can exist on + subsequent lines (but not inside comments). 2) #INSERT at the beginning of a line imports another batch-cmd file file and pastes it into the batch file as if it was written there. @@ -323,10 +326,10 @@ class BatchCommandProcessor(object): curr_cmd = "" filename = line.lstrip("#INSERT").strip() insert_commands = self.parse_file(filename) - if insert_commands == None: + if insert_commands is None: insert_commands = ["{rINSERT ERROR: %s{n" % filename] commands.extend(insert_commands) - else: #comment + else: #comment if curr_cmd: commands.append(curr_cmd.strip()) curr_cmd = "" @@ -341,6 +344,7 @@ class BatchCommandProcessor(object): commands = [c.strip('\r\n') for c in commands] return commands + #------------------------------------------------------------ # # Batch-code processor @@ -350,11 +354,14 @@ class BatchCommandProcessor(object): def tb_filename(tb): "Helper to get filename from traceback" return tb.tb_frame.f_code.co_filename + + def tb_iter(tb): while tb is not None: yield tb tb = tb.tb_next + class BatchCodeProcessor(object): """ This implements a batch-code processor @@ -368,7 +375,8 @@ class BatchCodeProcessor(object): 1) Lines starting with #HEADER starts a header block (ends other blocks) 2) Lines starting with #CODE begins a code block (ends other blocks) - 3) #CODE headers may be of the following form: #CODE (info) objname, objname2, ... + 3) #CODE headers may be of the following form: + #CODE (info) objname, objname2, ... 4) Lines starting with #INSERT are on form #INSERT filename. 3) All lines outside blocks are stripped. 4) All excess whitespace beginning/ending a block is stripped. @@ -378,8 +386,8 @@ class BatchCodeProcessor(object): # helper function def parse_line(line): """ - Identifies the line type: block command, comment, empty or normal code. - + Identifies the line type: + block command, comment, empty or normal code. """ parseline = line.strip() @@ -432,7 +440,7 @@ class BatchCodeProcessor(object): # are not checking for cyclic imports! in_header = False in_code = False - inserted_codes = self.parse_file(line) or [{'objs':"", 'info':line, 'code':""}] + inserted_codes = self.parse_file(line) or [{'objs': "", 'info': line, 'code': ""}] for codedict in inserted_codes: codedict["inserted"] = True codes.extend(inserted_codes) @@ -444,7 +452,7 @@ class BatchCodeProcessor(object): in_code = True # the line is a list of object variable names # (or an empty list) at this point. - codedict = {'objs':line, 'info':info, 'code':""} + codedict = {'objs': line, 'info': info, 'code': ""} codes.append(codedict) elif mode == 'comment' and in_header: continue @@ -485,7 +493,7 @@ class BatchCodeProcessor(object): """ # define the execution environment environ = "settings_module.configure()" - environdict = {"settings_module":settings} + environdict = {"settings_module": settings} if extra_environ: for key, value in extra_environ.items(): environdict[key] = value diff --git a/src/utils/create.py b/src/utils/create.py index 5751f0fa54..ecf4b7c7b8 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -43,10 +43,12 @@ _channelhandler = None # limit symbol import from API -__all__ = ("create_object", "create_script", "create_help_entry", "create_message", "create_channel", "create_player") +__all__ = ("create_object", "create_script", "create_help_entry", + "create_message", "create_channel", "create_player") _GA = object.__getattribute__ + # # Game Object creation # @@ -105,7 +107,8 @@ def create_object(typeclass, key=None, location=None, new_object = new_db_object.typeclass if not _GA(new_object, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still gave us a default + # this will fail if we gave a typeclass as input and it still + # gave us a default SharedMemoryModel.delete(new_db_object) if report_to: _GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_object.key, typeclass, @@ -122,14 +125,14 @@ def create_object(typeclass, key=None, location=None, # call the hook method. This is where all at_creation # customization happens as the typeclass stores custom # things on its database object. - new_object.basetype_setup() # setup the basics of Exits, Characters etc. + new_object.basetype_setup() # setup the basics of Exits, Characters etc. new_object.at_object_creation() # custom-given perms/locks overwrite hooks if permissions: new_object.permissions.add(permissions) if locks: - new_object.locks.add(locks) + new_object.locks.add(locks) if aliases: new_object.aliases.add(aliases) @@ -137,7 +140,7 @@ def create_object(typeclass, key=None, location=None, if home: new_object.home = home else: - new_object.home = settings.CHARACTER_DEFAULT_HOME if not nohome else None + new_object.home = settings.CHARACTER_DEFAULT_HOME if not nohome else None if location: new_object.move_to(location, quiet=True) @@ -154,6 +157,7 @@ def create_object(typeclass, key=None, location=None, #alias for create_object object = create_object + # # Script creation # @@ -218,7 +222,8 @@ def create_script(typeclass, key=None, obj=None, locks=None, new_script = new_db_script.typeclass if not _GA(new_db_script, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still gave us a default + # this will fail if we gave a typeclass as input and it still + # gave us a default SharedMemoryModel.delete(new_db_script) if report_to: _GA(report_to, "msg")("Error creating %s (%s): %s" % (new_db_script.key, typeclass, @@ -243,13 +248,13 @@ def create_script(typeclass, key=None, obj=None, locks=None, new_script.key = key if locks: new_script.locks.add(locks) - if interval != None: + if interval is not None: new_script.interval = interval - if start_delay != None: + if start_delay is not None: new_script.start_delay = start_delay - if repeats != None: + if repeats is not None: new_script.repeats = repeats - if persistent != None: + if persistent is not None: new_script.persistent = persistent # a new created script should usually be started. @@ -261,6 +266,7 @@ def create_script(typeclass, key=None, obj=None, locks=None, #alias script = create_script + # # Help entry creation # @@ -296,6 +302,7 @@ def create_help_entry(key, entrytext, category="General", locks=None): # alias help_entry = create_help_entry + # # Comm system methods # @@ -343,6 +350,7 @@ def create_message(senderobj, message, channels=None, return new_message message = create_message + def create_channel(key, aliases=None, desc=None, locks=None, keep_log=True, typeclass=None): @@ -389,6 +397,7 @@ def create_channel(key, aliases=None, desc=None, channel = create_channel + # # Player creation methods # @@ -456,7 +465,8 @@ def create_player(key, email, password, new_player = new_db_player.typeclass if not _GA(new_db_player, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still gave us a default + # this will fail if we gave a typeclass as input + # and it still gave us a default SharedMemoryModel.delete(new_db_player) if report_to: _GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_player.key, typeclass, @@ -465,7 +475,7 @@ def create_player(key, email, password, else: raise Exception(_GA(new_db_player, "typeclass_last_errmsg")) - new_player.basetype_setup() # setup the basic locks and cmdset + new_player.basetype_setup() # setup the basic locks and cmdset # call hook method (may override default permissions) new_player.at_player_creation() diff --git a/src/utils/dbserialize.py b/src/utils/dbserialize.py index 61e402010d..bb45b9065d 100644 --- a/src/utils/dbserialize.py +++ b/src/utils/dbserialize.py @@ -50,8 +50,12 @@ if uses_database("mysql") and ServerConfig.objects.get_mysql_db_version() < '5.6 else: _DATESTRING = "%Y:%m:%d-%H:%M:%S:%f" + def _TO_DATESTRING(obj): - "this will only be called with valid database objects. Returns datestring on correct form." + """ + this will only be called with valid database objects. Returns datestring + on correct form. + """ try: return _GA(obj, "db_date_created").strftime(_DATESTRING) except AttributeError: @@ -74,6 +78,7 @@ def _init_globals(): # SaverList, SaverDict, SaverSet - Attribute-specific helper classes and functions # + def _save(method): "method decorator that saves data to Attribute" def save_wrapper(self, *args, **kwargs): @@ -83,6 +88,7 @@ def _save(method): return ret return update_wrapper(save_wrapper, method) + class _SaverMutable(object): """ Parent class for properly handling of nested mutables in @@ -95,6 +101,7 @@ class _SaverMutable(object): self._parent = kwargs.pop("parent", None) self._db_obj = kwargs.pop("db_obj", None) self._data = None + def _save_tree(self): "recursively traverse back up the tree, save when we reach the root" if self._parent: @@ -103,6 +110,7 @@ class _SaverMutable(object): self._db_obj.value = self else: logger.log_errmsg("_SaverMutable %s has no root Attribute to save to." % self) + def _convert_mutables(self, data): "converts mutables to Saver* variants and assigns .parent property" def process_tree(item, parent): @@ -127,19 +135,25 @@ class _SaverMutable(object): def __repr__(self): return self._data.__repr__() + def __len__(self): return self._data.__len__() + def __iter__(self): return self._data.__iter__() + def __getitem__(self, key): return self._data.__getitem__(key) + @_save def __setitem__(self, key, value): self._data.__setitem__(key, self._convert_mutables(value)) + @_save def __delitem__(self, key): self._data.__delitem__(key) + class _SaverList(_SaverMutable, MutableSequence): """ A list that saves itself to an Attribute when updated. @@ -147,14 +161,17 @@ class _SaverList(_SaverMutable, MutableSequence): def __init__(self, *args, **kwargs): super(_SaverList, self).__init__(*args, **kwargs) self._data = list(*args) + @_save def __add__(self, otherlist): self._data = self._data.__add__(otherlist) return self._data + @_save def insert(self, index, value): self._data.insert(index, self._convert_mutables(value)) + class _SaverDict(_SaverMutable, MutableMapping): """ A dict that stores changes to an Attribute when updated @@ -163,6 +180,7 @@ class _SaverDict(_SaverMutable, MutableMapping): super(_SaverDict, self).__init__(*args, **kwargs) self._data = dict(*args) + class _SaverSet(_SaverMutable, MutableSet): """ A set that saves to an Attribute when updated @@ -170,11 +188,14 @@ class _SaverSet(_SaverMutable, MutableSet): def __init__(self, *args, **kwargs): super(_SaverSet, self).__init__(*args, **kwargs) self._data = set(*args) + def __contains__(self, value): return self._data.__contains__(value) + @_save def add(self, value): self._data.add(self._convert_mutables(value)) + @_save def discard(self, value): self._data.discard(value) @@ -187,14 +208,18 @@ class _SaverSet(_SaverMutable, MutableSet): def pack_dbobj(item): """ Check and convert django database objects to an internal representation. - This either returns the original input item or a tuple ("__packed_dbobj__", key, creation_time, id) + This either returns the original input item or a tuple + ("__packed_dbobj__", key, creation_time, id) """ _init_globals() - obj = hasattr(item, 'dbobj') and item.dbobj or item + obj = hasattr(item, 'dbobj') and item.dbobj or item natural_key = _FROM_MODEL_MAP[hasattr(obj, "id") and hasattr(obj, "db_date_created") and hasattr(obj, '__class__') and obj.__class__.__name__.lower()] - # build the internal representation as a tuple ("__packed_dbobj__", key, creation_time, id) - return natural_key and ('__packed_dbobj__', natural_key, _TO_DATESTRING(obj), _GA(obj, "id")) or item + # build the internal representation as a tuple + # ("__packed_dbobj__", key, creation_time, id) + return natural_key and ('__packed_dbobj__', natural_key, + _TO_DATESTRING(obj), _GA(obj, "id")) or item + def unpack_dbobj(item): """ @@ -208,21 +233,26 @@ def unpack_dbobj(item): obj = item[3] and _TO_TYPECLASS(_TO_MODEL_MAP[item[1]].objects.get(id=item[3])) except ObjectDoesNotExist: return None - # even if we got back a match, check the sanity of the date (some databases may 're-use' the id) - try: dbobj = obj.dbobj - except AttributeError: dbobj = obj + # even if we got back a match, check the sanity of the date (some + # databases may 're-use' the id) + try: + dbobj = obj.dbobj + except AttributeError: + dbobj = obj return _TO_DATESTRING(dbobj) == item[2] and obj or None + # # Access methods # def to_pickle(data): """ - This prepares data on arbitrary form to be pickled. It handles any nested structure - and returns data on a form that is safe to pickle (including having converted any - database models to their internal representation). We also convert any Saver*-type - objects back to their normal representations, they are not pickle-safe. + This prepares data on arbitrary form to be pickled. It handles any nested + structure and returns data on a form that is safe to pickle (including + having converted any database models to their internal representation). + We also convert any Saver*-type objects back to their normal + representations, they are not pickle-safe. """ def process_item(item): "Recursive processor and identification of data" @@ -246,20 +276,22 @@ def to_pickle(data): return pack_dbobj(item) return process_item(data) + @transaction.autocommit def from_pickle(data, db_obj=None): """ This should be fed a just de-pickled data object. It will be converted back to a form that may contain database objects again. Note that if a database - object was removed (or changed in-place) in the database, None will be returned. + object was removed (or changed in-place) in the database, None will be + returned. - db_obj - this is the model instance (normally an Attribute) that _Saver*-type - iterables (_SaverList etc) will save to when they update. It must have a 'value' - property that saves assigned data to the database. Skip if not serializing onto - a given object. + db_obj - this is the model instance (normally an Attribute) that + _Saver*-type iterables (_SaverList etc) will save to when they + update. It must have a 'value' property that saves assigned data + to the database. Skip if not serializing onto a given object. - If db_obj is given, this function will convert lists, dicts and sets to their - _SaverList, _SaverDict and _SaverSet counterparts. + If db_obj is given, this function will convert lists, dicts and sets + to their _SaverList, _SaverDict and _SaverSet counterparts. """ def process_item(item): @@ -278,7 +310,8 @@ def from_pickle(data, db_obj=None): return set(process_item(val) for val in item) elif hasattr(item, '__iter__'): try: - # we try to conserve the iterable class if it accepts an iterator + # we try to conserve the iterable class if + # it accepts an iterator return item.__class__(process_item(val) for val in item) except (AttributeError, TypeError): return [process_item(val) for val in item] @@ -300,7 +333,8 @@ def from_pickle(data, db_obj=None): return dat elif dtype == dict: dat = _SaverDict(parent=parent) - dat._data.update(dict((key, process_tree(val, dat)) for key, val in item.items())) + dat._data.update(dict((key, process_tree(val, dat)) + for key, val in item.items())) return dat elif dtype == set: dat = _SaverSet(parent=parent) @@ -308,7 +342,8 @@ def from_pickle(data, db_obj=None): return dat elif hasattr(item, '__iter__'): try: - # we try to conserve the iterable class if it accepts an iterator + # we try to conserve the iterable class if it + # accepts an iterator return item.__class__(process_tree(val, parent) for val in item) except (AttributeError, TypeError): dat = _SaverList(parent=parent) @@ -326,7 +361,8 @@ def from_pickle(data, db_obj=None): return dat elif dtype == dict: dat = _SaverDict(db_obj=db_obj) - dat._data.update((key, process_tree(val, parent=dat)) for key, val in data.items()) + dat._data.update((key, process_tree(val, parent=dat)) + for key, val in data.items()) return dat elif dtype == set: dat = _SaverSet(db_obj=db_obj) @@ -334,17 +370,22 @@ def from_pickle(data, db_obj=None): return dat return process_item(data) + def do_pickle(data): "Perform pickle to string" return to_str(dumps(data, protocol=PICKLE_PROTOCOL)) + def do_unpickle(data): "Retrieve pickle from pickled string" return loads(to_str(data)) + def dbserialize(data): "Serialize to pickled form in one step" return do_pickle(to_pickle(data)) + + def dbunserialize(data, db_obj=None): "Un-serialize in one step. See from_pickle for help db_obj." return do_unpickle(from_pickle(data, db_obj=db_obj)) diff --git a/src/utils/gametime.py b/src/utils/gametime.py index 02d0a14e2f..8c5dc65812 100644 --- a/src/utils/gametime.py +++ b/src/utils/gametime.py @@ -24,8 +24,8 @@ TIMEFACTOR = settings.TIME_FACTOR # Common real-life time measures, in seconds. # You should not change these. -REAL_TICK = max(1.0, settings.TIME_TICK) #Smallest time unit (min 1s) -REAL_MIN = 60.0 # seconds per minute in real world +REAL_TICK = max(1.0, settings.TIME_TICK) # Smallest time unit (min 1s) +REAL_MIN = 60.0 # seconds per minute in real world # Game-time units, in real-life seconds. These are supplied as # a convenient measure for determining the current in-game time, @@ -40,6 +40,7 @@ WEEK = DAY * settings.TIME_DAY_PER_WEEK MONTH = WEEK * settings.TIME_WEEK_PER_MONTH YEAR = MONTH * settings.TIME_MONTH_PER_YEAR + class GameTime(Script): """ This sets up an script that keeps track of the @@ -51,12 +52,12 @@ class GameTime(Script): """ self.key = "sys_game_time" self.desc = "Keeps track of the game time" - self.interval = REAL_MIN # update every minute + self.interval = REAL_MIN # update every minute self.persistent = True self.start_delay = True - self.attributes.add("game_time", 0.0) #IC time - self.attributes.add("run_time", 0.0) #OOC time - self.attributes.add("up_time", 0.0) #OOC time + self.attributes.add("game_time", 0.0) # IC time + self.attributes.add("run_time", 0.0) # OOC time + self.attributes.add("up_time", 0.0) # OOC time def at_repeat(self): """ @@ -77,6 +78,7 @@ class GameTime(Script): """ self.attributes.add("up_time", 0.0) + # Access routines def gametime_format(seconds): @@ -94,27 +96,29 @@ def gametime_format(seconds): # do this or we cancel the already counted # timefactor in the timer script... sec = int(seconds * TIMEFACTOR) - years, sec = sec/YEAR, sec % YEAR - months, sec = sec/MONTH, sec % MONTH - weeks, sec = sec/WEEK, sec % WEEK - days, sec = sec/DAY, sec % DAY - hours, sec = sec/HOUR, sec % HOUR - minutes, sec = sec/MIN, sec % MIN + years, sec = sec / YEAR, sec % YEAR + months, sec = sec / MONTH, sec % MONTH + weeks, sec = sec / WEEK, sec % WEEK + days, sec = sec / DAY, sec % DAY + hours, sec = sec / HOUR, sec % HOUR + minutes, sec = sec / MIN, sec % MIN return (years, months, weeks, days, hours, minutes, sec) + def realtime_format(seconds): """ As gametime format, but with real time units """ sec = int(seconds) - years, sec = sec/29030400, sec % 29030400 - months, sec = sec/2419200, sec % 2419200 - weeks, sec = sec/604800, sec % 604800 - days, sec = sec/86400, sec % 86400 - hours, sec = sec/3600, sec % 3600 - minutes, sec = sec/60, sec % 60 + years, sec = sec / 29030400, sec % 29030400 + months, sec = sec / 2419200, sec % 2419200 + weeks, sec = sec / 604800, sec % 604800 + days, sec = sec / 86400, sec % 86400 + hours, sec = sec / 3600, sec % 3600 + minutes, sec = sec / 60, sec % 60 return (years, months, weeks, days, hours, minutes, sec) + def gametime(format=False): """ Find the current in-game time (in seconds) since the start of the mud. @@ -136,6 +140,7 @@ def gametime(format=False): return gametime_format(game_time) return game_time + def runtime(format=False): """ Get the total actual time the server has been running (minus downtimes) @@ -151,6 +156,7 @@ def runtime(format=False): return realtime_format(run_time) return run_time + def uptime(format=False): """ Get the actual time the server has been running since last downtime. @@ -170,31 +176,33 @@ def uptime(format=False): def gametime_to_realtime(secs=0, mins=0, hrs=0, days=0, weeks=0, months=0, yrs=0): """ - This method helps to figure out the real-world time it will take until a in-game time - has passed. E.g. if an event should take place a month later in-game, you will be able - to find the number of real-world seconds this corresponds to (hint: Interval events deal - with real life seconds). + This method helps to figure out the real-world time it will take until an + in-game time has passed. E.g. if an event should take place a month later + in-game, you will be able to find the number of real-world seconds this + corresponds to (hint: Interval events deal with real life seconds). Example: - gametime_to_realtime(days=2) -> number of seconds in real life from now after which - 2 in-game days will have passed. + gametime_to_realtime(days=2) -> number of seconds in real life from + now after which 2 in-game days will have passed. """ - real_time = secs/TIMEFACTOR + mins*MIN + hrs*HOUR + \ - days*DAY + weeks*WEEK + months*MONTH + yrs*YEAR + real_time = secs / TIMEFACTOR + mins * MIN + hrs * HOUR + \ + days * DAY + weeks * WEEK + months * MONTH + yrs * YEAR return real_time + def realtime_to_gametime(secs=0, mins=0, hrs=0, days=0, weeks=0, months=0, yrs=0): """ - This method calculates how large an in-game time a real-world time interval would - correspond to. This is usually a lot less interesting than the other way around. + This method calculates how large an in-game time a real-world time + interval would correspond to. This is usually a lot less interesting + than the other way around. Example: realtime_to_gametime(days=2) -> number of game-world seconds corresponding to 2 real days. """ - game_time = TIMEFACTOR * (secs + mins*60 + hrs*3600 + days*86400 + \ - weeks*604800 + months*2419200 + yrs*29030400) + game_time = TIMEFACTOR * (secs + mins * 60 + hrs * 3600 + days * 86400 + + weeks * 604800 + months * 2419200 + yrs * 29030400) return game_time diff --git a/src/utils/logger.py b/src/utils/logger.py index 9325eb8ccc..a0c93f4ecf 100644 --- a/src/utils/logger.py +++ b/src/utils/logger.py @@ -8,6 +8,7 @@ a higher layer module. from traceback import format_exc from twisted.python import log + def log_trace(errmsg=None): """ Log a traceback to the log. This should be called @@ -27,7 +28,8 @@ def log_trace(errmsg=None): for line in errmsg.splitlines(): log.msg('[EE] %s' % line) except Exception: - log.msg('[EE] %s' % errmsg ) + log.msg('[EE] %s' % errmsg) + def log_errmsg(errmsg): """ @@ -43,6 +45,7 @@ def log_errmsg(errmsg): log.msg('[EE] %s' % line) #log.err('ERROR: %s' % (errormsg,)) + def log_warnmsg(warnmsg): """ Prints/logs any warnings that aren't critical but should be noted. @@ -57,6 +60,7 @@ def log_warnmsg(warnmsg): log.msg('[WW] %s' % line) #log.msg('WARNING: %s' % (warnmsg,)) + def log_infomsg(infomsg): """ Prints any generic debugging/informative info that should appear in the log. @@ -70,6 +74,7 @@ def log_infomsg(infomsg): for line in infomsg.splitlines(): log.msg('[..] %s' % line) + def log_depmsg(depmsg): """ Prints a deprecation message diff --git a/src/utils/search.py b/src/utils/search.py index 2af471730f..c50b6c4d55 100644 --- a/src/utils/search.py +++ b/src/utils/search.py @@ -30,7 +30,8 @@ Example: To reach the search method 'get_object_with_player' from django.contrib.contenttypes.models import ContentType # limit symbol import from API -__all__ = ("search_object", "search_player", "search_script", "search_message", "search_channel", "search_help_entry") +__all__ = ("search_object", "search_player", "search_script", + "search_message", "search_channel", "search_help_entry") # import objects this way to avoid circular import problems @@ -54,25 +55,29 @@ HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_c # candidates=None, # exact=True): # -# Search globally or in a list of candidates and return results. The result is always an Object. -# Always returns a list. +# Search globally or in a list of candidates and return results. +# The result is always a list of Objects (or the empty list) # # Arguments: -# ostring: (str) The string to compare names against. By default (if not attribute_name -# is set), this will search object.key and object.aliases in order. Can also -# be on the form #dbref, which will, if exact=True be matched against primary key. -# attribute_name: (str): Use this named ObjectAttribute to match ostring against, instead -# of the defaults. -# typeclass (str or TypeClass): restrict matches to objects having this typeclass. This will help -# speed up global searches. -# candidates (list obj ObjectDBs): If supplied, search will only be performed among the candidates -# in this list. A common list of candidates is the contents of the current location searched. -# exact (bool): Match names/aliases exactly or partially. Partial matching matches the -# beginning of words in the names/aliases, using a matching routine to separate -# multiple matches in names with multiple components (so "bi sw" will match -# "Big sword"). Since this is more expensive than exact matching, it is -# recommended to be used together with the objlist keyword to limit the number -# of possibilities. This value has no meaning if searching for attributes/properties. +# ostring: (str) The string to compare names against. By default (if +# not attribute_name is set), this will search object.key +# and object.aliases in order. Can also be on the form #dbref, +# which will, if exact=True be matched against primary key. +# attribute_name: (str): Use this named ObjectAttribute to match ostring +# against, instead of the defaults. +# typeclass (str or TypeClass): restrict matches to objects having +# this typeclass. This will help speed up global searches. +# candidates (list obj ObjectDBs): If supplied, search will only be +# performed among the candidates in this list. A common list +# of candidates is the contents of the current location. +# exact (bool): Match names/aliases exactly or partially. Partial +# matching matches the beginning of words in the names/aliases, +# using a matching routine to separate multiple matches in +# names with multiple components (so "bi sw" will match +# "Big sword"). Since this is more expensive than exact +# matching, it is recommended to be used together with +# the objlist keyword to limit the number of possibilities. +# This keyword has no meaning if attribute_name is set. # # Returns: # A list of matching objects (or a list with one unique match) diff --git a/src/utils/test_utils.py b/src/utils/test_utils.py index 1da08837d0..2968c4ed94 100644 --- a/src/utils/test_utils.py +++ b/src/utils/test_utils.py @@ -1,11 +1,12 @@ """ -Test runner for Evennia test suite. Run with "game/manage.py test". +Test runner for Evennia test suite. Run with "game/manage.py test". """ from django.conf import settings from django.test.simple import DjangoTestSuiteRunner + class EvenniaTestSuiteRunner(DjangoTestSuiteRunner): """ This test runner only runs tests on the apps specified in src/ and game/ to @@ -13,11 +14,11 @@ class EvenniaTestSuiteRunner(DjangoTestSuiteRunner): """ def build_suite(self, test_labels, extra_tests=None, **kwargs): """ - Build a test suite for Evennia. test_labels is a list of apps to test. + Build a test suite for Evennia. test_labels is a list of apps to test. If not given, a subset of settings.INSTALLED_APPS will be used. """ if not test_labels: - test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS + test_labels = [applabel.rsplit('.', 1)[1] for applabel in settings.INSTALLED_APPS if (applabel.startswith('src.') or applabel.startswith('game.'))] return super(EvenniaTestSuiteRunner, self).build_suite(test_labels, extra_tests=extra_tests, **kwargs) diff --git a/src/utils/text2html.py b/src/utils/text2html.py index 9425222a2d..5429b35c68 100644 --- a/src/utils/text2html.py +++ b/src/utils/text2html.py @@ -13,6 +13,7 @@ import re import cgi from ansi import * + class TextToHTMLparser(object): """ This class describes a parser for converting from ansi to html. @@ -36,10 +37,10 @@ class TextToHTMLparser(object): ('purple', ANSI_MAGENTA), ('cyan', hilite + ANSI_CYAN), ('teal', ANSI_CYAN), - ('white', hilite + ANSI_WHITE), # pure white - ('gray', ANSI_WHITE), #light grey - ('dimgray', hilite + ANSI_BLACK), #dark grey - ('black', ANSI_BLACK), #pure black + ('white', hilite + ANSI_WHITE), # pure white + ('gray', ANSI_WHITE), # light grey + ('dimgray', hilite + ANSI_BLACK), # dark grey + ('black', ANSI_BLACK), # pure black ] colorback = [ ('bgred', hilite + ANSI_BACK_RED), @@ -61,8 +62,8 @@ class TextToHTMLparser(object): ] # make sure to escape [ - colorcodes = [(c, code.replace("[",r"\[")) for c, code in colorcodes] - colorback = [(c, code.replace("[",r"\[")) for c, code in colorback] + colorcodes = [(c, code.replace("[", r"\[")) for c, code in colorcodes] + colorback = [(c, code.replace("[", r"\[")) for c, code in colorback] # create stop markers fgstop = [("", c.replace("[", r"\[")) for c in (normal, hilite, underline)] bgstop = [("", c.replace("[", r"\[")) for c in (normal,)] @@ -74,7 +75,7 @@ class TextToHTMLparser(object): re_bgs = [(cname, re.compile("(?:%s)(.*?)(?=%s)" % (code, bgstop))) for cname, code in colorback] re_normal = re.compile(normal.replace("[", r"\[")) re_hilite = re.compile("(?:%s)(.*)(?=%s)" % (hilite.replace("[", r"\["), fgstop)) - re_uline = re.compile("(?:%s)(.*?)(?=%s)" % (ANSI_UNDERLINE.replace("[",r"\["), fgstop)) + re_uline = re.compile("(?:%s)(.*?)(?=%s)" % (ANSI_UNDERLINE.replace("[", r"\["), fgstop)) re_string = re.compile(r'(?P[<&>])|(?P^[ \t]+)|(?P\r\n|\r|\n)', re.S|re.M|re.I) def re_color(self, text): @@ -126,9 +127,9 @@ class TextToHTMLparser(object): if c['lineend']: return '
    ' elif c['space'] == '\t': - return ' '*self.tabstop + return ' ' * self.tabstop elif c['space']: - t = m.group().replace('\t', ' '*self.tabstop) + t = m.group().replace('\t', ' ' * self.tabstop) t = t.replace(' ', ' ') return t @@ -155,6 +156,7 @@ class TextToHTMLparser(object): HTML_PARSER = TextToHTMLparser() + # # Access function # diff --git a/src/utils/utils.py b/src/utils/utils.py index c4f2b7ffed..3de278c198 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -6,12 +6,19 @@ be of use when designing your own game. """ -import os, sys, imp, types, math, re -import textwrap, datetime, random, traceback, inspect +import os +import sys +import imp +import types +import math +import re +import textwrap +import datetime +import random +import traceback from inspect import ismodule from collections import defaultdict from twisted.internet import threads, defer, reactor -from django.contrib.contenttypes.models import ContentType from django.conf import settings try: @@ -24,6 +31,7 @@ _GA = object.__getattribute__ _SA = object.__setattr__ _DA = object.__delattr__ + def is_iter(iterable): """ Checks if an object behaves iterably. However, @@ -39,10 +47,12 @@ def is_iter(iterable): except AttributeError: return False + def make_iter(obj): "Makes sure that the object is always iterable." return not hasattr(obj, '__iter__') and [obj] or obj + def fill(text, width=78, indent=0): """ Safely wrap text to a certain number of characters. @@ -69,7 +79,8 @@ def crop(text, width=78, suffix="[...]"): return text else: lsuffix = len(suffix) - return "%s%s" % (text[:width-lsuffix], suffix) + return "%s%s" % (text[:width - lsuffix], suffix) + def dedent(text): """ @@ -83,6 +94,7 @@ def dedent(text): return "" return textwrap.dedent(text) + def list_to_string(inlist, endsep="and", addquote=False): """ This pretty-formats a list as string output, adding @@ -108,6 +120,7 @@ def list_to_string(inlist, endsep="and", addquote=False): return str(inlist[0]) return ", ".join(str(v) for v in inlist[:-1]) + " %s %s" % (endsep, inlist[-1]) + def wildcard_to_regexp(instring): """ Converts a player-supplied string that may have wildcards in it to regular @@ -123,7 +136,7 @@ def wildcard_to_regexp(instring): regexp_string += "^" # Replace any occurances of * or ? with the appropriate groups. - regexp_string += instring.replace("*","(.*)").replace("?", "(.{1})") + regexp_string += instring.replace("*", "(.*)").replace("?", "(.{1})") # If there's an asterisk at the end of the string, we can't impose the # end of string ($) limiter. @@ -132,6 +145,7 @@ def wildcard_to_regexp(instring): return regexp_string + def time_format(seconds, style=0): """ Function to return a 'prettified' version of a value in seconds. @@ -146,11 +160,11 @@ def time_format(seconds, style=0): # We'll just use integer math, no need for decimal precision. seconds = int(seconds) - days = seconds / 86400 + days = seconds / 86400 seconds -= days * 86400 - hours = seconds / 3600 + hours = seconds / 3600 seconds -= hours * 3600 - minutes = seconds / 60 + minutes = seconds / 60 seconds -= minutes * 60 if style is 0: @@ -225,6 +239,7 @@ def time_format(seconds, style=0): return retval + def datetime_format(dtobj): """ Takes a datetime object instance (e.g. from django's DateTimeField) @@ -250,6 +265,7 @@ def datetime_format(dtobj): timestring = "%02i:%02i:%02i" % (hour, minute, second) return timestring + def host_os_is(osname): """ Check to see if the host OS matches the query. @@ -258,6 +274,7 @@ def host_os_is(osname): return True return False + def get_evennia_version(): """ Check for the evennia version info. @@ -268,6 +285,7 @@ def get_evennia_version(): except IOError: return "Unknown version" + def pypath_to_realpath(python_path, file_ending='.py'): """ Converts a path on dot python form (e.g. 'src.objects.models') to @@ -284,11 +302,13 @@ def pypath_to_realpath(python_path, file_ending='.py'): return "%s%s" % (path, file_ending) return path + def dbref(dbref, reqhash=True): """ Converts/checks if input is a valid dbref. - If reqhash is set, only input strings on the form '#N', where N is an integer - is accepted. Otherwise strings '#N', 'N' and integers N are all accepted. + If reqhash is set, only input strings on the form '#N', where N is an + integer is accepted. Otherwise strings '#N', 'N' and integers N are all + accepted. Output is the integer part. """ if reqhash: @@ -301,6 +321,7 @@ def dbref(dbref, reqhash=True): return int(dbref) if dbref.isdigit() else None return dbref if isinstance(dbref, int) else None + def to_unicode(obj, encoding='utf-8', force_string=False): """ This decodes a suitable object to the unicode format. Note that @@ -335,6 +356,7 @@ def to_unicode(obj, encoding='utf-8', force_string=False): raise Exception("Error: '%s' contains invalid character(s) not in %s." % (obj, encoding)) return obj + def to_str(obj, encoding='utf-8', force_string=False): """ This encodes a unicode string back to byte-representation, @@ -366,6 +388,7 @@ def to_str(obj, encoding='utf-8', force_string=False): raise Exception("Error: Unicode could not encode unicode string '%s'(%s) to a bytestring. " % (obj, encoding)) return obj + def validate_email_address(emailaddress): """ Checks if an email address is syntactically correct. @@ -382,18 +405,18 @@ def validate_email_address(emailaddress): # Email address must be more than 7 characters in total. if len(emailaddress) < 7: - return False # Address too short. + return False # Address too short. # Split up email address into parts. try: localpart, domainname = emailaddress.rsplit('@', 1) host, toplevel = domainname.rsplit('.', 1) except ValueError: - return False # Address does not have enough parts. + return False # Address does not have enough parts. # Check for Country code or Generic Domain. if len(toplevel) != 2 and toplevel not in domains: - return False # Not a domain name. + return False # Not a domain name. for i in '-_.%+.': localpart = localpart.replace(i, "") @@ -401,9 +424,9 @@ def validate_email_address(emailaddress): host = host.replace(i, "") if localpart.isalnum() and host.isalnum(): - return True # Email address is fine. + return True # Email address is fine. else: - return False # Email address has funny characters. + return False # Email address has funny characters. def inherits_from(obj, parent): @@ -447,6 +470,7 @@ def server_services(): del SESSIONS return server + def uses_database(name="sqlite3"): """ Checks if the game is currently using a given database. This is a @@ -460,13 +484,15 @@ def uses_database(name="sqlite3"): engine = settings.DATABASE_ENGINE return engine == "django.db.backends.%s" % name + def delay(delay=2, retval=None, callback=None): """ Delay the return of a value. Inputs: delay (int) - the delay in seconds retval (any) - this will be returned by this function after a delay - callback (func(retval)) - if given, this will be called with retval after delay seconds + callback (func(retval)) - if given, this will be called with retval + after delay seconds Returns: deferred that will fire with to_return after delay seconds """ @@ -499,14 +525,15 @@ def clean_object_caches(obj): pass # on-object property cache - [_DA(obj, cname) for cname in obj.__dict__.keys() if cname.startswith("_cached_db_")] + [_DA(obj, cname) for cname in obj.__dict__.keys() + if cname.startswith("_cached_db_")] try: hashid = _GA(obj, "hashid") - hasid = obj.hashid _TYPECLASSMODELS._ATTRIBUTE_CACHE[hashid] = {} except AttributeError: pass + _PPOOL = None _PCMD = None _PROC_ERR = "A process has ended with a probable error condition: process ended by signal 9." @@ -574,7 +601,6 @@ def run_async(to_execute, *args, **kwargs): deferred.addCallback(callback, **callback_kwargs) deferred.addErrback(errback, **errback_kwargs) -# def check_evennia_dependencies(): """ @@ -631,7 +657,7 @@ def check_evennia_dependencies(): if settings.IRC_ENABLED: try: import twisted.words - twisted.words # set to avoid debug info about not-used import + twisted.words # set to avoid debug info about not-used import except ImportError: errstring += "\n ERROR: IRC is enabled, but twisted.words is not installed. Please install it." errstring += "\n Linux Debian/Ubuntu users should install package 'python-twisted-words', others" @@ -642,6 +668,7 @@ def check_evennia_dependencies(): print "%s\n %s\n%s" % ("-"*78, errstring, '-'*78) return no_error + def has_parent(basepath, obj): "Checks if basepath is somewhere in objs parent tree." try: @@ -652,17 +679,19 @@ def has_parent(basepath, obj): # instance. Not sure if one should defend against this. return False + def mod_import(module): """ A generic Python module loader. Args: - module - this can be either a Python path (dot-notation like src.objects.models), - an absolute path (e.g. /home/eve/evennia/src/objects.models.py) + module - this can be either a Python path (dot-notation like + src.objects.models), an absolute path + (e.g. /home/eve/evennia/src/objects.models.py) or an already import module object (e.g. models) Returns: - an imported module. If the input argument was already a model, this is returned as-is, - otherwise the path is parsed and imported. + an imported module. If the input argument was already a model, + this is returned as-is, otherwise the path is parsed and imported. Error: returns None. The error is also logged. """ @@ -691,7 +720,7 @@ def mod_import(module): if not module: return None - if type(module) == types.ModuleType: + if isinstance(module, types.ModuleType): # if this is already a module, we are done mod = module else: @@ -699,8 +728,9 @@ def mod_import(module): try: mod = __import__(module, fromlist=["None"]) except ImportError, ex: - # check just where the ImportError happened (it could have been an erroneous - # import inside the module as well). This is the trivial way to do it ... + # check just where the ImportError happened (it could have been + # an erroneous import inside the module as well). This is the + # trivial way to do it ... if str(ex) != "Import by filename is not supported.": #log_trace("ImportError inside module '%s': '%s'" % (module, str(ex))) raise @@ -726,29 +756,36 @@ def mod_import(module): result[0].close() return mod + def all_from_module(module): """ Return all global-level variables from a module as a dict """ mod = mod_import(module) - return dict((key, val) for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))) + return dict((key, val) for key, val in mod.__dict__.items() + if not (key.startswith("_") or ismodule(val))) + def variable_from_module(module, variable=None, default=None): """ - Retrieve a variable or list of variables from a module. The variable(s) must be defined - globally in the module. If no variable is given (or a list entry is None), a random variable - is extracted from the module. + Retrieve a variable or list of variables from a module. The variable(s) + must be defined globally in the module. If no variable is given (or a + list entry is None), a random variable is extracted from the module. If module cannot be imported or given variable not found, default is returned. Args: module (string or module)- python path, absolute path or a module - variable (string or iterable) - single variable name or iterable of variable names to extract - default (string) - default value to use if a variable fails to be extracted. + variable (string or iterable) - single variable name or iterable of + variable names to extract + default (string) - default value to use if a variable fails + to be extracted. Returns: - a single value or a list of values depending on the type of 'variable' argument. Errors in lists - are replaced by the 'default' argument.""" + a single value or a list of values depending on the type of + 'variable' argument. Errors in lists are replaced by the + 'default' argument. + """ if not module: return default @@ -761,12 +798,14 @@ def variable_from_module(module, variable=None, default=None): result.append(mod.__dict__.get(var, default)) else: # random selection - mvars = [val for key, val in mod.__dict__.items() if not (key.startswith("_") or ismodule(val))] + mvars = [val for key, val in mod.__dict__.items() + if not (key.startswith("_") or ismodule(val))] result.append((mvars and random.choice(mvars)) or default) if len(result) == 1: return result[0] return result + def string_from_module(module, variable=None, default=None): """ This is a wrapper for variable_from_module that requires return @@ -779,6 +818,7 @@ def string_from_module(module, variable=None, default=None): return [(isinstance(v, basestring) and v or default) for v in val] return default + def init_new_player(player): """ Helper method to call all hooks, set flags etc on a newly created @@ -790,41 +830,50 @@ def init_new_player(player): # player.character.db.FIRST_LOGIN = True player.db.FIRST_LOGIN = True + def string_similarity(string1, string2): """ This implements a "cosine-similarity" algorithm as described for example in - Proceedings of the 22nd International Conference on Computation Linguistics - (Coling 2008), pages 593-600, Manchester, August 2008 - The measure-vectors used is simply a "bag of words" type histogram (but for letters). + Proceedings of the 22nd International Conference on Computation + Linguistics (Coling 2008), pages 593-600, Manchester, August 2008 + The measure-vectors used is simply a "bag of words" type histogram + (but for letters). - The function returns a value 0...1 rating how similar the two strings are. The strings can - contain multiple words. + The function returns a value 0...1 rating how similar the two strings + are. The strings can contain multiple words. """ vocabulary = set(list(string1 + string2)) vec1 = [string1.count(v) for v in vocabulary] vec2 = [string2.count(v) for v in vocabulary] try: - return float(sum(vec1[i]*vec2[i] for i in range(len(vocabulary)))) / \ + return float(sum(vec1[i] * vec2[i] for i in range(len(vocabulary)))) / \ (math.sqrt(sum(v1**2 for v1 in vec1)) * math.sqrt(sum(v2**2 for v2 in vec2))) except ZeroDivisionError: - # can happen if empty-string cmdnames appear for some reason. This is a no-match. + # can happen if empty-string cmdnames appear for some reason. + # This is a no-match. return 0 + def string_suggestions(string, vocabulary, cutoff=0.6, maxnum=3): """ - Given a string and a vocabulary, return a match or a list of suggestsion based on - string similarity. + Given a string and a vocabulary, return a match or a list of suggestsion + based on string similarity. Args: string (str)- a string to search for vocabulary (iterable) - a list of available strings - cutoff (int, 0-1) - limit the similarity matches (higher, the more exact is required) + cutoff (int, 0-1) - limit the similarity matches (higher, the more + exact is required) maxnum (int) - maximum number of suggestions to return Returns: - list of suggestions from vocabulary (could be empty if there are no matches) + list of suggestions from vocabulary (could be empty if there are + no matches) """ - return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg) for sugg in vocabulary], - key=lambda tup: tup[0], reverse=True) if tup[0] >= cutoff][:maxnum] + return [tup[1] for tup in sorted([(string_similarity(string, sugg), sugg) + for sugg in vocabulary], + key=lambda tup: tup[0], reverse=True) + if tup[0] >= cutoff][:maxnum] + def string_partial_matching(alternatives, inp, ret_index=True): """ @@ -837,7 +886,8 @@ def string_partial_matching(alternatives, inp, ret_index=True): Input: alternatives (list of str) - list of possible strings to match inp (str) - search criterion - ret_index (bool) - return list of indices (from alternatives array) or strings + ret_index (bool) - return list of indices (from alternatives + array) or strings Returns: list of matching indices or strings, or an empty list @@ -855,7 +905,8 @@ def string_partial_matching(alternatives, inp, ret_index=True): # loop over parts, making sure only to visit each part once # (this will invalidate input in the wrong word order) submatch = [last_index + alt_num for alt_num, alt_word - in enumerate(alt_words[last_index:]) if alt_word.startswith(inp_word)] + in enumerate(alt_words[last_index:]) + if alt_word.startswith(inp_word)] if submatch: last_index = min(submatch) + 1 score += 1 @@ -871,6 +922,7 @@ def string_partial_matching(alternatives, inp, ret_index=True): return matches[max(matches)] return [] + def format_table(table, extra_space=1): """ Note: src.utils.prettytable is more powerful than this, but this @@ -909,6 +961,7 @@ def format_table(table, extra_space=1): for icol, col in enumerate(table)]) return ftable + def get_evennia_pids(): """ Get the currently valids PIDs (Process IDs) of the Portal and Server