diff --git a/game/gamesrc/commands/basecmdset.py b/game/gamesrc/commands/basecmdset.py index 5d68ab8f36..483c616ec0 100644 --- a/game/gamesrc/commands/basecmdset.py +++ b/game/gamesrc/commands/basecmdset.py @@ -19,7 +19,7 @@ new cmdset class. """ from src.commands.cmdset import CmdSet -from src.commands.default import cmdset_default, cmdset_unloggedin +from src.commands.default import cmdset_default, cmdset_unloggedin, cmdset_ooc from game.gamesrc.commands.basecommand import Command @@ -40,7 +40,7 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet): Populates the cmdset """ super(DefaultCmdSet, self).at_cmdset_creation() - + # # any commands you add below will overload the default ones. # @@ -70,6 +70,24 @@ class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet): # +class OOCCmdSet(cmdset_ooc.OOCCmdSet): + """ + This is set is available to the player when they have no + character connected to them (i.e. they are out-of-character, ooc). + """ + key = "OOC" + + def at_cmdset_creation(self): + """ + Populates the cmdset + """ + super(OOCCmdSet, self).at_cmdset_creation() + + # + # any commands you add below will overload the default ones. + # + + class BaseCmdSet(CmdSet): """ Implements an empty, example cmdset. diff --git a/game/gamesrc/objects/baseobjects.py b/game/gamesrc/objects/baseobjects.py index aa13e72bb4..2157c9ddb2 100644 --- a/game/gamesrc/objects/baseobjects.py +++ b/game/gamesrc/objects/baseobjects.py @@ -44,12 +44,6 @@ class Object(BaseObject): Hooks (these are class methods, so their arguments should also start with self): - basetype_setup() - only called once, when object is first created, before - at_object_creation(). Sets up semi-engine-specific details such as - the default lock policy, what defines a Room, Exit etc. - Usually not modified unless you want to overload the default - security restriction policies. - at_object_creation() - only called once, when object is first created. Almost all object customizations go here. at_first_login() - only called once, the very first time user logs in. diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index dc1e4507c0..33146def18 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -54,14 +54,11 @@ from django.conf import settings from src.comms.channelhandler import CHANNELHANDLER from src.commands.cmdsethandler import import_cmdset from src.objects.exithandler import EXITHANDLER -from src.utils import logger +from src.utils import logger, utils #This switches the command parser to a user-defined one. # You have to restart the server for this to take effect. -try: - CMDPARSER = __import__(settings.ALTERNATE_PARSER, fromlist=[True]).cmdparser -except Exception: - from src.commands.cmdparser import cmdparser as CMDPARSER +COMMAND_PARSER = utils.mod_import(*settings.COMMAND_PARSER.rsplit('.', 1)) # There are a few system-hardcoded command names. These # allow for custom behaviour when the command handler hits @@ -101,13 +98,21 @@ def get_and_merge_cmdsets(caller): exit_cmdset = None local_objects_cmdsets = [None] + # Player object's commandsets + try: + player_cmdset = caller.player.cmdset.current + except AttributeError: + player_cmdset = None + if not caller_cmdset.no_channels: # Make cmdsets out of all valid channels channel_cmdset = CHANNELHANDLER.get_cmdset(caller) if not caller_cmdset.no_exits: # Make cmdsets out of all valid exits in the room exit_cmdset = EXITHANDLER.get_cmdset(caller) - location = caller.location + location = None + if hasattr(caller, "location"): + location = caller.location if location and not caller_cmdset.no_objs: # Gather all cmdsets stored on objects in the room and # also in the caller's inventory @@ -138,6 +143,12 @@ def get_and_merge_cmdsets(caller): cmdset = channel_cmdset + cmdset except TypeError: pass + # finally merge on the player cmdset. This should have a low priority + try: + cmdset = player_cmdset + cmdset + except TypeError: + pass + return cmdset def match_command(cmd_candidates, cmdset, logged_caller=None): @@ -295,7 +306,7 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False): raise ExecSystemCommand(syscmd, sysarg) # Parse the input string into command candidates - cmd_candidates = CMDPARSER(raw_string) + cmd_candidates = COMMAND_PARSER(raw_string) #string ="Command candidates" #for cand in cmd_candidates: diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index b0e967ea07..ba30c8e11d 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -174,3 +174,128 @@ def cmdparser(raw_string): wordlist = raw_string.split(" ") candidates.extend(produce_candidates(nr_candidates, wordlist)) return candidates + + +#------------------------------------------------------------ +# Search parsers and support methods +#------------------------------------------------------------ +# +# Default functions for formatting and processing searches. +# +# This is in its own module due to them being possible to +# replace from the settings file by setting the variables +# +# SEARCH_AT_RESULTERROR_HANDLER +# SEARCH_MULTIMATCH_PARSER +# +# 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): + """ + Called by search methods after a result of any type has been found. + + Takes a search result (a list) and + formats eventual errors. + + msg_obj - object to receive feedback. + ostring - original search string + results - list of found matches (0, 1 or more) + global_search - if this was a global_search or not + (if it is, there might be an idea of supplying + dbrefs instead of only numbers) + + Multiple matches are returned to the searching object + as + 1-object + 2-object + 3-object + etc + + """ + string = "" + if not results: + # no results. + string = "Could not find '%s'." % ostring + results = None + + elif len(results) > 1: + # we have more than one match. We will display a + # list of the form 1-objname, 2-objname etc. + + # check if the msg_object may se dbrefs + show_dbref = global_search + + string += "More than one match for '%s'" % ostring + string += " (please narrow target):" + for num, result in enumerate(results): + invtext = "" + dbreftext = "" + if hasattr(result, "location") and result.location == msg_obj: + invtext = " (carried)" + if show_dbref: + dbreftext = "(#%i)" % result.id + string += "\n %i-%s%s%s" % (num+1, result.name, + dbreftext, invtext) + results = None + else: + # we have exactly one match. + results = results[0] + + if string: + msg_obj.msg(string.strip()) + return results + +def at_multimatch_input(ostring): + """ + Parse number-identifiers. + + This parser will be called by the engine when a user supplies + a search term. The search term must be analyzed to determine + if the user wants to differentiate between multiple matches + (usually found during a previous search). + + This method should separate out any identifiers from the search + string used to differentiate between same-named objects. The + result should be a tuple (index, search_string) where the index + gives which match among multiple matches should be used (1 being + the lowest number, rather than 0 as in Python). + + This parser version will identify search strings on the following + forms + + 2-object + + This will be parsed to (2, "object") and, if applicable, will tell + the engine to pick the second from a list of same-named matches of + objects called "object". + + Ex for use in a game session: + + > look + You see: ball, ball, ball and ball. + > get ball + There where multiple matches for ball: + 1-ball + 2-ball + 3-ball + 4-ball + > get 3-ball + You get the ball. + + """ + + if not isinstance(ostring, basestring): + return (None, ostring) + if not '-' in ostring: + return (None, ostring) + try: + 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) + except IndexError: + return (None, ostring) diff --git a/src/commands/cmdset.py b/src/commands/cmdset.py index ccb8f3f107..f334f8c83d 100644 --- a/src/commands/cmdset.py +++ b/src/commands/cmdset.py @@ -15,6 +15,9 @@ together to create interesting in-game effects. """ import copy +from src.utils.utils import inherits_from, is_iter + +RECURSIVE_PROTECTION = False class CmdSetMeta(type): """ @@ -204,16 +207,40 @@ class CmdSet(object): def add(self, cmd): """ - Add a command to this cmdset. + Add a command, a list of commands or a cmdset to this cmdset. Note that if cmd already exists in set, it will replace the old one (no priority checking etc at this point; this is often used to overload default commands). - """ - cmd = instantiate(cmd) - if cmd: + If cmd is another cmdset class or -instance, the commands + of that command set is added to this one, as if they were part + of the original cmdset definition. No merging or priority checks + are made, rather later added commands will simply replace + existing ones to make a unique set. + """ + + if inherits_from(cmd, "src.commands.cmdset.CmdSet"): + # this is a command set so merge all commands in that set + # to this one. We are not protecting against recursive + # loops (adding a cmdset that itself adds this cmdset here + # since such an error will be very visible and lead to a + # traceback at startup anyway. + try: + cmd = instantiate(cmd) + except RuntimeError, e: + string = "Adding cmdset %s to %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, self.__class__)) + cmds = cmd.commands + elif not is_iter(cmd): + cmds = [instantiate(cmd)] + else: + cmds = instantiate(cmd) + + for cmd in cmds: + # add all commands if not hasattr(cmd, 'obj'): cmd.obj = self.cmdsetobj try: diff --git a/src/commands/cmdsethandler.py b/src/commands/cmdsethandler.py index 216666d293..7c2cf1d840 100644 --- a/src/commands/cmdsethandler.py +++ b/src/commands/cmdsethandler.py @@ -179,7 +179,7 @@ class CmdSetHandler(object): string += "\n" merged = True - # Display the currently active cmdset + # Display the currently active cmdset, limited by self.obj's permissions mergetype = self.mergetype_stack[-1] if mergetype != self.current.mergetype: merged_on = self.cmdset_stack[-2].key @@ -187,7 +187,8 @@ class CmdSetHandler(object): if merged: string += " : %s" % (mergetype, self.current) else: - string += " <%s (%s)>: %s" % (self.current.key, mergetype, self.current) + string += " <%s (%s)>: %s" % (self.current.key, mergetype, + ", ".join(cmd.key for cmd in self.current if cmd.access(self.obj, "cmd"))) return string.strip() def update(self, init_mode=False): diff --git a/src/commands/default/admin.py b/src/commands/default/admin.py index 0bc76723b2..523b5591af 100644 --- a/src/commands/default/admin.py +++ b/src/commands/default/admin.py @@ -58,7 +58,7 @@ class CmdBoot(MuxCommand): break else: # Boot by player object - pobj = caller.search("*%s" % args, global_search=True) + pobj = caller.search("*%s" % args, global_search=True, player=True) if not pobj: return if pobj.character.has_player: @@ -118,6 +118,9 @@ class CmdDelPlayer(MuxCommand): caller = self.caller args = self.args + if hasattr(caller, 'player'): + caller = caller.player + if not args: caller.msg("Usage: @delplayer[/delobj] [: reason]") return @@ -128,7 +131,7 @@ class CmdDelPlayer(MuxCommand): # We use player_search since we want to be sure to find also players # that lack characters. - players = caller.search("*%s" % args) + players = caller.search("*%s" % args, player=True) if not players: try: players = PlayerDB.objects.filter(id=args) @@ -304,7 +307,7 @@ class CmdNewPassword(MuxCommand): return # the player search also matches 'me' etc. - player = caller.search("*%s" % self.lhs, global_search=True) + player = caller.search("*%s" % self.lhs, global_search=True, player=True) if not player: return player.user.set_password(self.rhs) @@ -320,12 +323,14 @@ class CmdPerm(MuxCommand): Usage: @perm[/switch] [= [,,...]] + @perm[/switch] * [= [,,...]] Switches: - del : delete the given permission from . - - This command sets/clears individual permission strings on an object. - If no permission is given, list all permissions on + del : delete the given permission from or . + player : set permission on a player (same as adding * to name) + + This command sets/clears individual permission strings on an object + or player. If no permission is given, list all permissions on . """ key = "@perm" aliases = "@setperm" @@ -340,17 +345,18 @@ class CmdPerm(MuxCommand): lhs, rhs = self.lhs, self.rhs if not self.args: - string = "Usage: @perm[/switch] object [ = permission, permission, ...]\n" + string = "Usage: @perm[/switch] object [ = permission, permission, ...]" caller.msg(string) return - - # locate the object - obj = caller.search(lhs, global_search=True) + + playermode = 'player' in self.switches or lhs.startswith('*') + + # locate the object + obj = caller.search(lhs, global_search=True, player=playermode) if not obj: return if not rhs: - if not obj.access(caller, 'examine'): caller.msg("You are not allowed to examine this object.") return @@ -396,12 +402,10 @@ class CmdPerm(MuxCommand): for perm in self.rhslist: - - perm = perm.lower() # don't allow to set a permission higher in the hierarchy than the one the # caller has (to prevent self-escalation) - if perm in PERMISSION_HIERARCHY and not obj.locks.check_lockstring(caller, "dummy:perm(%s)" % perm): + 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 @@ -416,45 +420,6 @@ class CmdPerm(MuxCommand): if tstring: obj.msg(tstring.strip()) - -class CmdPuppet(MuxCommand): - """ - Switch control to an object - - Usage: - @puppet - - This will attempt to "become" a different character. Note that this command does not check so that - the target object has the appropriate cmdset. You cannot puppet a character that is already "taken". - """ - - key = "@puppet" - locks = "cmd:perm(puppet) or perm(Builders)" - help_category = "Admin" - - def func(self): - """ - Simple puppet method (does not check permissions) - """ - caller = self.caller - if not self.args: - caller.msg("Usage: @puppet ") - return - - player = caller.player - new_character = caller.search(self.args) - if not new_character: - return - if not utils.inherits_from(new_character, settings.BASE_CHARACTER_TYPECLASS): - caller.msg("%s is not a Character." % self.args) - return - if new_character.player: - caller.msg("This character is already under the control of a player.") - if player.swap_character(new_character): - new_character.msg("You now control %s." % new_character.name) - else: - caller.msg("You cannot control %s." % new_character.name) - class CmdWall(MuxCommand): """ @wall diff --git a/src/commands/default/building.py b/src/commands/default/building.py index c5bc9dcaae..1a8680e402 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -6,6 +6,7 @@ Building and world design commands from django.conf import settings from src.objects.models import ObjectDB, ObjAttribute +from src.players.models import PlayerAttribute from src.utils import create, utils, debug from src.commands.default.muxcommand import MuxCommand @@ -122,7 +123,7 @@ class CmdSetObjAlias(MuxCommand): old_aliases = obj.aliases if old_aliases: caller.msg("Cleared aliases from %s: %s" % (obj.key, ", ".join(old_aliases))) - del obj.dbobj.aliases # TODO: del does not understand indirect typeclass reference! + del obj.dbobj.aliases else: caller.msg("No aliases to clear.") return @@ -1389,16 +1390,25 @@ class CmdExamine(ObjManipCommand): Usage: examine [[/attrname]] + examine [*[/attrname]] + + Switch: + player - examine a Player (same as adding *) The examine command shows detailed game info about an object and optionally a specific attribute on it. If object is not specified, the current location is examined. + + Append a * before the search string to examine a player. + """ key = "@examine" aliases = ["@ex","ex", "exam", "examine"] locks = "cmd:perm(examine) or perm(Builders)" help_category = "Building" + player_mode = False + def format_attributes(self, obj, attrname=None, crop=True): """ Helper function that returns info about attributes and/or @@ -1411,7 +1421,10 @@ class CmdExamine(ObjManipCommand): except Exception: ndb_attr = None else: - db_attr = [(attr.key, attr.value) for attr in ObjAttribute.objects.filter(db_obj=obj)] + if player_mode: + db_attr = [(attr.key, attr.value) for attr in PlayerAttribute.objects.filter(db_obj=obj)] + else: + db_attr = [(attr.key, attr.value) for attr in ObjAttribute.objects.filter(db_obj=obj)] try: ndb_attr = [(aname, avalue) for aname, avalue in obj.ndb.__dict__.items()] except Exception: @@ -1439,24 +1452,25 @@ class CmdExamine(ObjManipCommand): returns a string. """ - if obj.has_player: + if hasattr(obj, "has_player") and obj.has_player: string = "\n{wName/key{n: {c%s{n (%s)" % (obj.name, obj.dbref) else: string = "\n{wName/key{n: {C%s{n (%s)" % (obj.name, obj.dbref) - if obj.aliases: + if hasattr(obj, "aliases") and obj.aliases: string += "\n{wAliases{n: %s" % (", ".join(obj.aliases)) - if obj.has_player: + if hasattr(obj, "has_player") and obj.has_player: string += "\n{wPlayer{n: {c%s{n" % obj.player.name perms = obj.player.permissions if obj.player.is_superuser: perms = [""] elif not perms: perms = [""] - string += "\n{wPlayer Perms{n: %s" % (", ".join(perms)) - + string += "\n{wPlayer Perms{n: %s" % (", ".join(perms)) string += "\n{wTypeclass{n: %s (%s)" % (obj.typeclass, obj.typeclass_path) - string += "\n{wLocation{n: %s" % obj.location - if obj.destination: + + if hasattr(obj, "location"): + string += "\n{wLocation{n: %s" % obj.location + if hasattr(obj, "destination") and obj.destination: string += "\n{wDestination{n: %s" % obj.destination perms = obj.permissions if perms: @@ -1467,8 +1481,8 @@ class CmdExamine(ObjManipCommand): if not (len(obj.cmdset.all()) == 1 and obj.cmdset.current.key == "Empty"): cmdsetstr = "\n".join([utils.fill(cmdset, indent=2) for cmdset in str(obj.cmdset).split("\n")]) - string += "\n{wCurrent Cmdset (before permission checks){n:\n %s" % cmdsetstr - if obj.scripts.all(): + string += "\n{wCurrent Cmdset (including permission checks){n:\n %s" % cmdsetstr + if hasattr(obj, "scripts") and hasattr(obj.scripts, "all") and obj.scripts.all(): string += "\n{wScripts{n:\n %s" % obj.scripts # add the attributes string += self.format_attributes(obj) @@ -1476,21 +1490,21 @@ class CmdExamine(ObjManipCommand): exits = [] pobjs = [] things = [] - for content in obj.contents: - if content.destination: - # an exit - exits.append(content) - elif content.player: - pobjs.append(content) - else: - things.append(content) - if exits: - string += "\n{wExits{n: " + ", ".join([exit.name for exit in exits]) - if pobjs: - string += "\n{wCharacters{n: " + ", ".join(["{c%s{n" % pobj.name for pobj in pobjs]) - if things: - string += "\n{wContents{n: " + ", ".join([cont.name for cont in obj.contents - if cont not in exits and cont not in pobjs]) + if hasattr(obj, "contents"): + for content in obj.contents: + if content.destination: + exits.append(content) + elif content.player: + pobjs.append(content) + else: + things.append(content) + if exits: + string += "\n{wExits{n: " + ", ".join([exit.name for exit in exits]) + if pobjs: + string += "\n{wCharacters{n: " + ", ".join(["{c%s{n" % pobj.name for pobj in pobjs]) + if things: + string += "\n{wContents{n: " + ", ".join([cont.name for cont in obj.contents + if cont not in exits and cont not in pobjs]) #output info return "-"*78 + '\n' + string.strip() + "\n" + '-'*78 @@ -1506,36 +1520,35 @@ class CmdExamine(ObjManipCommand): caller.execute_cmd('look %s' % obj.name) return string = self.format_output(obj) - - else: - # we have given a specific target object - - string = "" - - for objdef in self.lhs_objattr: - - obj_name = objdef['name'] - obj_attrs = objdef['attrs'] - - obj = caller.search(obj_name) - if not obj: - continue - - if not obj.access(caller, 'examine'): - #If we don't have special info access, just look at the object instead. - caller.execute_cmd('look %s' % obj_name) - continue - - if obj_attrs: - for attrname in obj_attrs: - # we are only interested in specific attributes - string += self.format_attributes(obj, attrname, crop=False) - else: - string += self.format_output(obj) - string = string.strip() - # Send it all - if string: caller.msg(string.strip()) + return + + # we have given a specific target object + string = "" + for objdef in self.lhs_objattr: + + obj_name = objdef['name'] + obj_attrs = objdef['attrs'] + + global player_mode + player_mode = "player" in self.switches or obj_name.startswith('*') + + obj = caller.search(obj_name, player=player_mode) + if not obj: + continue + + if not obj.access(caller, 'examine'): + #If we don't have special info access, just look at the object instead. + caller.execute_cmd('look %s' % obj_name) + continue + + if obj_attrs: + for attrname in obj_attrs: + # we are only interested in specific attributes + string += self.format_attributes(obj, attrname, crop=False) + else: + string += self.format_output(obj) + caller.msg(string.strip()) class CmdFind(MuxCommand): diff --git a/src/commands/default/cmdset_default.py b/src/commands/default/cmdset_default.py index 5b23b6a1d6..7ed73b6023 100644 --- a/src/commands/default/cmdset_default.py +++ b/src/commands/default/cmdset_default.py @@ -18,17 +18,14 @@ class DefaultCmdSet(CmdSet): # The general commands self.add(general.CmdLook()) self.add(general.CmdHome()) - self.add(general.CmdPassword()) + self.add(general.CmdWho()) self.add(general.CmdInventory()) - self.add(general.CmdQuit()) self.add(general.CmdPose()) self.add(general.CmdNick()) self.add(general.CmdGet()) self.add(general.CmdDrop()) - self.add(general.CmdWho()) self.add(general.CmdSay()) self.add(general.CmdAccess()) - self.add(general.CmdEncoding()) # The help system self.add(help.CmdHelp()) @@ -52,7 +49,6 @@ class DefaultCmdSet(CmdSet): self.add(admin.CmdEmit()) self.add(admin.CmdNewPassword()) self.add(admin.CmdPerm()) - self.add(admin.CmdPuppet()) self.add(admin.CmdWall()) # Building and world manipulation @@ -79,24 +75,6 @@ class DefaultCmdSet(CmdSet): self.add(building.CmdLock()) self.add(building.CmdScript()) self.add(building.CmdHome()) - - # Comm commands - self.add(comms.CmdAddCom()) - self.add(comms.CmdDelCom()) - self.add(comms.CmdAllCom()) - self.add(comms.CmdChannels()) - self.add(comms.CmdCdestroy()) - self.add(comms.CmdChannelCreate()) - self.add(comms.CmdCset()) - self.add(comms.CmdCBoot()) - self.add(comms.CmdCemit()) - self.add(comms.CmdCWho()) - self.add(comms.CmdCdesc()) - self.add(comms.CmdPage()) - self.add(comms.CmdIRC2Chan()) - self.add(comms.CmdIMC2Chan()) - self.add(comms.CmdIMCInfo()) - self.add(comms.CmdIMCTell()) # Batchprocessor commands self.add(batchprocess.CmdBatchCommands()) diff --git a/src/commands/default/cmdset_ooc.py b/src/commands/default/cmdset_ooc.py new file mode 100644 index 0000000000..d6ca8439d2 --- /dev/null +++ b/src/commands/default/cmdset_ooc.py @@ -0,0 +1,55 @@ +""" + +This is the cmdset for OutOfCharacter (OOC) commands. +These are stored on the Player object and should +thus be able to handle getting a Player object +as caller rather than a Character. + +""" +from src.commands.cmdset import CmdSet +from src.commands.default import help, comms, general, admin + +class OOCCmdSet(CmdSet): + """ + Implements the player command set. + """ + + key = "DefaultOOC" + priority = -5 + + def at_cmdset_creation(self): + "Populates the cmdset" + + # general commands + self.add(general.CmdOOCLook()) + self.add(general.CmdIC()) + self.add(general.CmdOOC()) + self.add(general.CmdEncoding()) + self.add(general.CmdQuit()) + self.add(general.CmdPassword()) + + # help command + self.add(help.CmdHelp()) + + # admin commands + self.add(admin.CmdBoot()) + self.add(admin.CmdDelPlayer()) + self.add(admin.CmdNewPassword()) + + # Comm commands + self.add(comms.CmdAddCom()) + self.add(comms.CmdDelCom()) + self.add(comms.CmdAllCom()) + self.add(comms.CmdChannels()) + self.add(comms.CmdCdestroy()) + self.add(comms.CmdChannelCreate()) + self.add(comms.CmdCset()) + self.add(comms.CmdCBoot()) + self.add(comms.CmdCemit()) + self.add(comms.CmdCWho()) + self.add(comms.CmdCdesc()) + self.add(comms.CmdPage()) + self.add(comms.CmdIRC2Chan()) + self.add(comms.CmdIMC2Chan()) + self.add(comms.CmdIMCInfo()) + self.add(comms.CmdIMCTell()) diff --git a/src/commands/default/comms.py b/src/commands/default/comms.py index 2c77617b22..afa90a82f5 100644 --- a/src/commands/default/comms.py +++ b/src/commands/default/comms.py @@ -1,5 +1,11 @@ """ -Comsys command module. +Comsystem command module. + +Comm commands are OOC commands and intended to be made available to +the Player at all times (they go into the PlayerCmdSet). So we +make sure to homogenize self.caller to always be the player object +for easy handling. + """ from django.conf import settings from src.comms.models import Channel, Msg, PlayerChannelConnection, ExternalChannelConnection @@ -29,6 +35,25 @@ def find_channel(caller, channelname, silent=False, noaliases=False): caller.msg("Multiple channels match (be more specific): \n%s" % matches) return None return channels[0] + +class CommCommand(MuxCommand): + """ + This is a parent for comm-commands. Since + These commands are to be available to the + Player, we make sure to homogenize the caller + here, so it's always seen as a player to the + command body. + """ + + def parse(self): + "overload parts of parse" + + # run parent + super(CommCommand, self).parse() + # fix obj->player + if utils.inherits_from(self.caller, "src.objects.objects.Object"): + # an object. Convert it to its player. + self.caller = self.caller.player class CmdAddCom(MuxCommand): """ @@ -46,14 +71,14 @@ class CmdAddCom(MuxCommand): key = "addcom" aliases = ["aliaschan","chanalias"] help_category = "Comms" - locks = "cmd:not perm(channel_banned)" + locks = "cmd:not pperm(channel_banned)" def func(self): "Implement the command" caller = self.caller args = self.args - player = caller.player + player = caller if not args: caller.msg("Usage: addcom [alias =] channelname.") @@ -120,7 +145,7 @@ class CmdDelCom(MuxCommand): "Implementing the command. " caller = self.caller - player = caller.player + player = caller if not self.args: caller.msg("Usage: delcom ") @@ -169,7 +194,7 @@ class CmdAllCom(MuxCommand): """ key = "allcom" - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -189,7 +214,7 @@ class CmdAllCom(MuxCommand): caller.execute_cmd("addcom %s" % channel.key) elif args == "off": #get names all subscribed channels and disconnect from them all - channels = [conn.channel for conn in PlayerChannelConnection.objects.get_all_player_connections(caller.player)] + channels = [conn.channel for conn in PlayerChannelConnection.objects.get_all_player_connections(caller)] for channel in channels: caller.execute_cmd("delcom %s" % channel.key) elif args == "destroy": @@ -230,7 +255,7 @@ class CmdChannels(MuxCommand): key = "@channels" aliases = ["@clist", "channels", "comlist", "chanlist", "channellist", "all channels"] help_category = "Comms" - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" def func(self): "Implement function" @@ -243,7 +268,7 @@ class CmdChannels(MuxCommand): caller.msg("No channels available.") return # all channel we are already subscribed to - subs = [conn.channel for conn in PlayerChannelConnection.objects.get_all_player_connections(caller.player)] + subs = [conn.channel for conn in PlayerChannelConnection.objects.get_all_player_connections(caller)] if self.cmdstring != "comlist": @@ -298,7 +323,7 @@ class CmdCdestroy(MuxCommand): key = "@cdestroy" help_category = "Comms" - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" def func(self): "Destroy objects cleanly." @@ -337,7 +362,7 @@ class CmdCBoot(MuxCommand): """ key = "@cboot" - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -352,12 +377,12 @@ class CmdCBoot(MuxCommand): if not channel: return reason = "" - player = None if ":" in self.rhs: playername, reason = self.rhs.rsplit(":", 1) - player = self.caller.search("*%s" % playername.lstrip('*')) - if not player: - player = self.caller.search("*%s" % self.rhs.lstrip('*')) + searchstring = playername.lstrip('*') + else: + searchstring = self.rhs.lstrip('*') + player = self.caller.search(searchstring, player=True) if not player: return if reason: @@ -400,7 +425,7 @@ class CmdCemit(MuxCommand): key = "@cemit" aliases = ["@cmsg"] - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -437,7 +462,7 @@ class CmdCWho(MuxCommand): List who is connected to a given channel you have access to. """ key = "@cwho" - locks = "cmd: not perm(channel_banned)" + locks = "cmd: not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -475,7 +500,7 @@ class CmdChannelCreate(MuxCommand): key = "@ccreate" aliases = "channelcreate" - locks = "cmd:not perm(channel_banned)" + locks = "cmd:not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -522,7 +547,7 @@ class CmdCset(MuxCommand): """ key = "@cset" - locks = "cmd:not perm(channel_banned)" + locks = "cmd:not pperm(channel_banned)" aliases = ["@cclock"] help_category = "Comms" @@ -568,7 +593,7 @@ class CmdCdesc(MuxCommand): """ key = "@cdesc" - locks = "cmd:not perm(channel_banned)" + locks = "cmd:not pperm(channel_banned)" help_category = "Comms" def func(self): @@ -611,7 +636,7 @@ class CmdPage(MuxCommand): key = "page" aliases = ['tell'] - locks = "cmd:not perm(page_banned)" + locks = "cmd:not pperm(page_banned)" help_category = "Comms" def func(self): @@ -619,7 +644,7 @@ class CmdPage(MuxCommand): "Implement function using the Msg methods" caller = self.caller - player = caller.player + player = caller # get the messages we've sent messages_we_sent = list(Msg.objects.get_messages_by_sender(player)) @@ -751,7 +776,7 @@ class CmdIRC2Chan(MuxCommand): """ key = "@irc2chan" - locks = "cmd:serversetting(IRC_ENABLED) and perm(Immortals)" + locks = "cmd:serversetting(IRC_ENABLED) and pperm(Immortals)" help_category = "Comms" def func(self): @@ -840,7 +865,7 @@ class CmdIMC2Chan(MuxCommand): """ key = "@imc2chan" - locks = "cmd:serversetting(IMC2_ENABLED) and perm(Immortals)" + locks = "cmd:serversetting(IMC2_ENABLED) and pperm(Immortals)" help_category = "Comms" def func(self): @@ -923,7 +948,7 @@ class CmdIMCInfo(MuxCommand): key = "@imcinfo" aliases = ["@imcchanlist", "@imclist", "@imcwhois"] - locks = "cmd: serversetting(IMC2_ENABLED) and perm(Wizards)" + locks = "cmd: serversetting(IMC2_ENABLED) and pperm(Wizards)" help_category = "Comms" def func(self): @@ -982,7 +1007,7 @@ class CmdIMCInfo(MuxCommand): return from src.comms.imc2 import IMC2_CLIENT self.caller.msg("Sending IMC whois request. If you receive no response, no matches were found.") - IMC2_CLIENT.msg_imc2(None, from_obj=self.caller.player, packet_type="imcwhois", data={"target":self.args}) + IMC2_CLIENT.msg_imc2(None, from_obj=self.caller, packet_type="imcwhois", data={"target":self.args}) elif not self.switches or "channels" in self.switches or self.cmdstring == "@imcchanlist": # show channels @@ -1051,6 +1076,6 @@ class CmdIMCTell(MuxCommand): data = {"target":target, "destination":destination} # send to imc2 - IMC2_CLIENT.msg_imc2(message, from_obj=self.caller.player, packet_type="imctell", data=data) + IMC2_CLIENT.msg_imc2(message, from_obj=self.caller, packet_type="imctell", data=data) self.caller.msg("You paged {c%s@%s{n (over IMC): '%s'." % (target, destination, message)) diff --git a/src/commands/default/general.py b/src/commands/default/general.py index d2f0157c8d..534311aeee 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -5,11 +5,13 @@ now. import time from django.conf import settings from src.server.sessionhandler import SESSIONS -from src.objects.models import HANDLE_SEARCH_ERRORS from src.utils import utils -from src.objects.models import Nick +from src.objects.models import ObjectNick as Nick from src.commands.default.muxcommand import MuxCommand +AT_SEARCH_RESULT = utils.mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) +BASE_PLAYER_TYPECLASS = settings.BASE_PLAYER_TYPECLASS + class CmdHome(MuxCommand): """ home @@ -45,7 +47,7 @@ class CmdLook(MuxCommand): Observes your location or objects in your vicinity. """ key = "look" - aliases = ["l"] + aliases = ["l", "ls"] locks = "cmd:all()" def func(self): @@ -53,18 +55,19 @@ class CmdLook(MuxCommand): Handle the looking. """ caller = self.caller - args = self.args # caller.msg(inp) + args = self.args if args: # Use search to handle duplicate/nonexistant results. looking_at_obj = caller.search(args, use_nicks=True) if not looking_at_obj: - return + return else: looking_at_obj = caller.location if not looking_at_obj: - caller.msg("Location: None") + caller.msg("You have no location to look at!") return + if not hasattr(looking_at_obj, 'return_appearance'): # this is likely due to us having a player instead looking_at_obj = looking_at_obj.character @@ -89,6 +92,8 @@ class CmdPassword(MuxCommand): "hook function." caller = self.caller + if hasattr(caller, "player"): + caller = caller.player if not self.rhs: caller.msg("Usage: @password = ") @@ -96,7 +101,7 @@ class CmdPassword(MuxCommand): oldpass = self.lhslist[0] # this is already stripped by parse() newpass = self.rhslist[0] # '' try: - uaccount = caller.player.user + uaccount = caller.user except AttributeError: caller.msg("This is only applicable for players.") return @@ -309,7 +314,7 @@ class CmdDrop(MuxCommand): # those in our inventory. results = [obj for obj in results if obj in caller.contents] # now we send it into the handler. - obj = HANDLE_SEARCH_ERRORS(caller, self.args, results, False) + obj = AT_SEARCH_RESULT(caller, self.args, results, False) if not obj: return @@ -348,13 +353,13 @@ class CmdWho(MuxCommand): who doing - Shows who is currently online. Doing is an - alias that limits info also for those with - all permissions. + Shows who is currently online. Doing is an alias that limits info + also for those with all permissions. """ key = "who" aliases = "doing" + locks = "cmd:all()" def func(self): """ @@ -518,17 +523,20 @@ class CmdEncoding(MuxCommand): Sets the encoding. """ caller = self.caller + if hasattr(caller, 'player'): + caller = caller.player + if 'clear' in self.switches: # remove customization - old_encoding = caller.player.db.encoding + old_encoding = caller.db.encoding if old_encoding: string = "Your custom text encoding ('%s') was cleared." % old_encoding else: string = "No custom encoding was set." - del caller.player.db.encoding + del caller.db.encoding elif not self.args: # just list the encodings supported - pencoding = caller.player.db.encoding + pencoding = caller.db.encoding string = "" if pencoding: string += "Default encoding: {g%s{n (change with {w@encoding {n)" % pencoding @@ -539,9 +547,9 @@ class CmdEncoding(MuxCommand): string = "No encodings found." else: # change encoding - old_encoding = caller.player.db.encoding + old_encoding = caller.db.encoding encoding = self.args - caller.player.db.encoding = encoding + caller.db.encoding = encoding string = "Your custom text encoding was changed from '%s' to '%s'." % (old_encoding, encoding) caller.msg(string.strip()) @@ -579,3 +587,141 @@ class CmdAccess(MuxCommand): if hasattr(caller, 'player'): string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms) caller.msg(string) + +# OOC commands + +class CmdOOCLook(CmdLook): + """ + ooc look + + Usage: + look + + This is an OOC version of the look command. Since a + Player doesn't have an in-game existence, there is no + concept of location or "self". If we are controlling + a character, pass control over to normal look. + + """ + + key = "look" + aliases = ["l", "ls"] + locks = "cmd:all()" + help_cateogory = "General" + + def func(self): + "implement the command" + + self.character = None + if utils.inherits_from(self.caller, "src.objects.objects.Object"): + # An object of some type is calling. Convert to player. + self.character = self.caller + self.caller = self.caller.player + + if not self.character: + string = "You are out-of-character (OOC). " + string += "Use {w@ic{n to get back to the game, {whelp{n for more info." + self.caller.msg(string) + else: + self.caller = self.character # we have to put this back for normal look to work. + super(CmdLook, self).func() + +class CmdIC(MuxCommand): + """ + Switch control to an object + + Usage: + @ic + + Go in-character (IC) as a given Character. + + This will attempt to "become" a different object assuming you have + the right to do so. You cannot become an object that is already + controlled by another player. In principle can be + any in-game object as long as you have access right to puppet it. + """ + + key = "@ic" + locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it. + aliases = "@puppet" + help_category = "General" + + def func(self): + """ + Simple puppet method + """ + caller = self.caller + if utils.inherits_from(caller, "src.objects.objects.Object"): + caller = caller.player + + new_character = None + if not self.args: + new_character = caller.db.last_puppet + if not new_character: + caller.msg("Usage: @ic ") + return + if not new_character: + # search for a matching character + new_character = caller.search(self.args) + if not new_character: + # the search method handles error messages etc. + return + if new_character.player: + if new_character.player == caller: + caller.msg("{RYou already are {c%s{n." % new_character.name) + else: + caller.msg("{c%s{r is already acted by another player.{n" % new_character.name) + return + if not new_character.access(caller, "puppet"): + caller.msg("{rYou may not become %s.{n" % new_character.name) + return + old_char = None + if caller.character: + # save the old character. We only assign this to last_puppet if swap is successful. + old_char = caller.character + if caller.swap_character(new_character): + new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name) + caller.db.last_puppet = old_char + new_character.execute_cmd("look") + else: + caller.msg("{rYou cannot become {C%s{n." % new_character.name) + +class CmdOOC(MuxCommand): + """ + @ooc - go ooc + + Usage: + @ooc + + Go out-of-character (OOC). + + This will leave your current character and put you in a incorporeal OOC state. + """ + + key = "@ooc" + locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it. + aliases = "@unpuppet" + help_category = "General" + + def func(self): + "Implement function" + + caller = self.caller + + if utils.inherits_from(caller, "src.objects.objects.Object"): + caller = self.caller.player + + if not caller.character: + string = "You are already OOC." + caller.msg(string) + return + + caller.db.last_puppet = caller.character + + # disconnect + caller.character.player = None + caller.character = None + + + caller.msg("\n{GYou go OOC.{n\n") + caller.execute_cmd("look") diff --git a/src/commands/default/tests.py b/src/commands/default/tests.py index 02c390a47d..b9f6d19d03 100644 --- a/src/commands/default/tests.py +++ b/src/commands/default/tests.py @@ -441,5 +441,12 @@ class TestCwho(CommandTest): self.execute_cmd("@ccreate testchannel1;testchan1;testchan1b = This is a test channel") self.execute_cmd("@cwho testchan1b", "Channel subscriptions") +# OOC commands + +#class TestOOC_and_IC(CommandTest): # can't be tested it seems, causes errors in other commands (?) +# def test_call(self): +# self.execute_cmd("@ooc", "\nYou go OOC.") +# self.execute_cmd("@ic", "\nYou become TestChar") + # Unloggedin commands # these cannot be tested from here. diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index be8749a36c..3c4d52d512 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -62,49 +62,81 @@ class CmdConnect(MuxCommand): # We are logging in, get/setup the player object controlled by player - character = player.character - if not character: - # Create a new character object to tie the player to. This should - # usually not be needed unless the old character object was manually - # deleted. - default_home_id = ServerConfig.objects.conf("default_home") - default_home = ObjectDB.objects.get_id(default_home_id) - typeclass = settings.BASE_CHARACTER_TYPECLASS - character = create.create_object(typeclass=typeclass, - key=player.name, - location=default_home, - home=default_home, - player=player) - - character.db.FIRST_LOGIN = "True" - - # Getting ready to log the player in. - # Check if this is the first time the # *player* connects if player.db.FIRST_LOGIN: player.at_first_login() del player.db.FIRST_LOGIN - - # check if this is the first time the *character* - # character (needs not be the first time the player - # does so, e.g. if the player has several characters) - if character.db.FIRST_LOGIN: - character.at_first_login() - del character.db.FIRST_LOGIN - - # actually do the login, calling - # customization hooks before and after. player.at_pre_login() - character.at_pre_login() + character = player.character + if character: + # this player has a character. Check if it's the + # first time *this character* logs in + if character.db.FIRST_LOGIN: + character.at_first_login() + del character.db.FIRST_LOGIN + # run character login hook + character.at_pre_login() + + # actually do the login session.session_login(player) + + # post-login hooks + player.at_post_login() + if character: + character.at_post_login() + character.execute_cmd('look') + else: + player.execute_cmd('look') - player.at_post_login() - character.at_post_login() # run look #print "character:", character, character.scripts.all(), character.cmdset.current - character.execute_cmd('look') + + # + # character = player.character + # if not character: + # # Create a new character object to tie the player to. This should + # # usually not be needed unless the old character object was manually + # # deleted. + # default_home_id = ServerConfig.objects.conf("default_home") + # default_home = ObjectDB.objects.get_id(default_home_id) + # typeclass = settings.BASE_CHARACTER_TYPECLASS + # character = create.create_object(typeclass=typeclass, + # key=player.name, + # location=default_home, + # home=default_home, + # player=player) + + # character.db.FIRST_LOGIN = "True" + + # # Getting ready to log the player in. + + # # Check if this is the first time the + # # *player* connects + # if player.db.FIRST_LOGIN: + # player.at_first_login() + # del player.db.FIRST_LOGIN + + # # check if this is the first time the *character* + # # character (needs not be the first time the player + # # does so, e.g. if the player has several characters) + # if character.db.FIRST_LOGIN: + # character.at_first_login() + # del character.db.FIRST_LOGIN + + # # actually do the login, calling + # # customization hooks before and after. + # player.at_pre_login() + # character.at_pre_login() + + # session.session_login(player) + + # player.at_post_login() + # character.at_post_login() + # # run look + # #print "character:", character, character.scripts.all(), character.cmdset.current + # character.execute_cmd('look') class CmdCreate(MuxCommand): """ @@ -169,61 +201,69 @@ class CmdCreate(MuxCommand): return # Run sanity and security checks - + if PlayerDB.objects.get_player_from_name(playername) or User.objects.filter(username=playername): # player already exists session.msg("Sorry, there is already a player with the name '%s'." % playername) - elif PlayerDB.objects.get_player_from_email(email): + return + if PlayerDB.objects.get_player_from_email(email): # email already set on a player session.msg("Sorry, there is already a player with that email address.") - elif len(password) < 3: + return + if len(password) < 3: # too short password string = "Your password must be at least 3 characters or longer." string += "\n\rFor best security, make it at least 8 characters long, " string += "avoid making it a real word and mix numbers into it." session.msg(string) - else: - # everything's ok. Create the new player account - try: - default_home_id = ServerConfig.objects.conf("default_home") - default_home = ObjectDB.objects.get_id(default_home_id) - - typeclass = settings.BASE_CHARACTER_TYPECLASS - permissions = settings.PERMISSION_PLAYER_DEFAULT + return - new_character = create.create_player(playername, email, password, - permissions=permissions, - location=default_home, - typeclass=typeclass, - home=default_home) - # character safety features - new_character.locks.delete("get") - new_character.locks.add("get:perm(Wizards)") + # everything's ok. Create the new player account. + try: + default_home_id = ServerConfig.objects.conf("default_home") + default_home = ObjectDB.objects.get_id(default_home_id) - # set a default description - new_character.db.desc = "This is a Player." + typeclass = settings.BASE_CHARACTER_TYPECLASS + permissions = settings.PERMISSION_PLAYER_DEFAULT - new_character.db.FIRST_LOGIN = True - new_player = new_character.player - new_player.db.FIRST_LOGIN = True - - # join the new player to the public channel - pchanneldef = settings.CHANNEL_PUBLIC - if pchanneldef: - pchannel = Channel.objects.get_channel(pchanneldef[0]) - if not pchannel.connect_to(new_player): - string = "New player '%s' could not connect to public channel!" % new_player.key - logger.log_errmsg(string) + new_character = create.create_player(playername, email, password, + permissions=permissions, + location=default_home, + typeclass=typeclass, + home=default_home) + new_player = new_character.player + + # character safety features + new_character.locks.delete("get") + new_character.locks.add("get:perm(Wizards)") + # allow the character itself and the player to puppet this character. + new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % + (new_character.id, new_player.id)) - string = "A new account '%s' was created with the email address %s. Welcome!" - string += "\n\nYou can now log with the command 'connect %s '." - session.msg(string % (playername, email, email)) - except Exception: - # we have to handle traceback ourselves at this point, if - # we don't, errors will give no feedback. - 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()) + # set a default description + new_character.db.desc = "This is a Player." + + new_character.db.FIRST_LOGIN = True + new_player = new_character.player + new_player.db.FIRST_LOGIN = True + + # join the new player to the public channel + pchanneldef = settings.CHANNEL_PUBLIC + if pchanneldef: + pchannel = Channel.objects.get_channel(pchanneldef[0]) + if not pchannel.connect_to(new_player): + string = "New player '%s' could not connect to public channel!" % new_player.key + logger.log_errmsg(string) + + string = "A new account '%s' was created with the email address %s. Welcome!" + string += "\n\nYou can now log with the command 'connect %s '." + 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. + 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 CmdQuit(MuxCommand): """ diff --git a/src/comms/channelhandler.py b/src/comms/channelhandler.py index b63548b1ef..669f63b3db 100644 --- a/src/comms/channelhandler.py +++ b/src/comms/channelhandler.py @@ -80,11 +80,16 @@ class ChannelCommand(command.Command): msg = "[%s] %s: %s" % (channel.key, caller.name, msg) # we can't use the utils.create function to make the Msg, # since that creates an import recursive loop. - msgobj = Msg(db_sender=caller.player, db_message=msg) + try: + sender = caller.player + except AttributeError: + # this could happen if a player is calling directly. + sender = caller.dbobj + msgobj = Msg(db_sender=sender, db_message=msg) msgobj.save() msgobj.channels = channel # send new message object to channel - channel.msg(msgobj, from_obj=caller.player) + channel.msg(msgobj, from_obj=sender) class ChannelHandler(object): """ diff --git a/src/locks/lockfuncs.py b/src/locks/lockfuncs.py index 586c76c455..8660613baf 100644 --- a/src/locks/lockfuncs.py +++ b/src/locks/lockfuncs.py @@ -106,6 +106,16 @@ 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"): + # an object. Convert to player. + accessing_obj = accessing_obj.player + return accessing_obj + + +# lock functions + def true(*args, **kwargs): "Always returns True." return True @@ -155,6 +165,29 @@ def perm_above(accessing_obj, accessed_obj, *args, **kwargs): return any(True for hpos, hperm in enumerate(PERMISSION_HIERARCHY) if hperm in [p.lower() for p in accessing_obj.permissions] and hpos > ppos) +def pperm(accessing_obj, accessed_obj, *args, **kwargs): + """ + The basic permission-checker for Player objects. Ignores case. + + Usage: + pperm() + + where is the permission accessing_obj must + have in order to pass the lock. If the given permission + is part of PERMISSION_HIERARCHY, permission is also granted + to all ranks higher up in the hierarchy. + """ + 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 + hierarchy than the one given. If there is no such higher rank, + it's assumed we refer to superuser. If no hierarchy is defined, + this function has no meaning and returns False. + """ + return perm_above(_to_player(accessing_obj), accessed_obj, *args, **kwargs) + def dbref(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: @@ -175,10 +208,21 @@ def dbref(accessing_obj, accessed_obj, *args, **kwargs): return dbref == accessing_obj.id 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) + + def attr(accessing_obj, accessed_obj, *args, **kwargs): """ Usage: diff --git a/src/locks/lockhandler.py b/src/locks/lockhandler.py index ef483439fb..7d05331af4 100644 --- a/src/locks/lockhandler.py +++ b/src/locks/lockhandler.py @@ -223,7 +223,8 @@ class LockHandler(object): wlist.append("Lock: access type '%s' changed from '%s' to '%s' " % \ (access_type, locks[access_type][2], raw_lockstring)) locks[access_type] = (evalstring, tuple(lock_funcs), raw_lockstring) - if wlist: + if wlist and self.log_obj: + # not an error, so only report if log_obj is available. self._log_error("\n".join(wlist)) if elist: raise LockException("\n".join(elist)) @@ -343,10 +344,11 @@ class LockHandler(object): to None if the lock functions called don't access it). atype can also be put to a dummy value since no lock selection is made. """ - if (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'user') - and hasattr(accessing_obj.player.user, 'is_superuser') - and accessing_obj.player.user.is_superuser): - return True # always grant access to the superuser. + if ((hasattr(accessing_obj, 'is_superuser') and accessing_obj.is_superuser) + or (hasattr(accessing_obj, 'player') and hasattr(accessing_obj.player, 'is_superuser') and accessing_obj.player.is_superuser) + or (hasattr(accessing_obj, 'get_player') and (accessing_obj.get_player()==None or accessing_obj.get_player().is_superuser))): + return True + locks = self. _parse_lockstring(lockstring) for access_type in locks: evalstring, func_tup, raw_string = locks[access_type] diff --git a/src/objects/manager.py b/src/objects/manager.py index ff7bb3215e..e418cf518e 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -9,11 +9,8 @@ from src.typeclasses.managers import returns_typeclass, returns_typeclass_list from src.utils import utils # Try to use a custom way to parse id-tagged multimatches. -IDPARSER_PATH = getattr(settings, 'ALTERNATE_OBJECT_SEARCH_MULTIMATCH_PARSER', 'src.objects.object_search_funcs') -if not IDPARSER_PATH: - # can happen if variable is set to "" in settings - IDPARSER_PATH = 'src.objects.object_search_funcs' -exec("from %s import object_multimatch_parser as IDPARSER" % IDPARSER_PATH) + +AT_MULTIMATCH_INPUT = utils.mod_import(*settings.SEARCH_AT_MULTIMATCH_INPUT.rsplit('.', 1)) class ObjectManager(TypedObjectManager): """ @@ -172,7 +169,10 @@ class ObjectManager(TypedObjectManager): global_search=False, attribute_name=None, location=None): """ - Search as an object and return results. + Search as an object and return results. The result is always an Object. + If * is appended (player search, a Character controlled by this Player + is looked for. The Character is returned, not the Player. Use player_search + to find Player objects. character: (Object) The object performing the search. ostring: (string) The string to compare names against. @@ -187,7 +187,7 @@ class ObjectManager(TypedObjectManager): if not ostring or not character: return None - if not location: + if not location and hasattr(character, "location"): location = character.location # Easiest case - dbref matching (always exact) @@ -204,16 +204,17 @@ class ObjectManager(TypedObjectManager): if character and ostring in ['me', 'self']: return [character] if character and ostring in ['*me', '*self']: - return [character.player] + return [character] - # Test if we are looking for a player object + # Test if we are looking for an object controlled by a + # specific player if utils.to_unicode(ostring).startswith("*"): # Player search - try to find obj by its player's name player_match = self.get_object_with_player(ostring) if player_match is not None: - return [player_match.player] - + return [player_match] + # Search for keys, aliases or other attributes search_locations = [None] # this means a global search @@ -246,7 +247,7 @@ class ObjectManager(TypedObjectManager): matches = local_and_global_search(ostring, exact=True) if not matches: # if we have no match, check if we are dealing with an "N-keyword" query - if so, strip it. - match_number, ostring = IDPARSER(ostring) + match_number, ostring = AT_MULTIMATCH_INPUT(ostring) if match_number != None and ostring: # Run search again, without match number: matches = local_and_global_search(ostring, exact=True) diff --git a/src/objects/migrations/0004_rename_nick_to_objectnick.py b/src/objects/migrations/0004_rename_nick_to_objectnick.py new file mode 100644 index 0000000000..ec185397a7 --- /dev/null +++ b/src/objects/migrations/0004_rename_nick_to_objectnick.py @@ -0,0 +1,124 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models, utils + +class Migration(SchemaMigration): + + def forwards(self, orm): + + try: + # if we migrate, we just rename the table. This will move over all values too. + db.rename_table("objects_nick", "objects_objectnick") + except utils.DatabaseError: + # this happens if we start from scratch. In that case the old + # database table doesn't exist, so we just create the new one. + + # Adding model 'ObjectNick' + db.create_table('objects_objectnick', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('db_nick', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('db_real', self.gf('django.db.models.fields.TextField')()), + ('db_type', self.gf('django.db.models.fields.CharField')(default='inputline', max_length=16, null=True, blank=True)), + ('db_obj', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['objects.ObjectDB'])), + )) + db.send_create_signal('objects', ['ObjectNick']) + + # Adding unique constraint on 'ObjectNick', fields ['db_nick', 'db_type', 'db_obj'] + db.create_unique('objects_objectnick', ['db_nick', 'db_type', 'db_obj_id']) + + + def backwards(self, orm): + raise RuntimeError("This migration cannot be reversed.") + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'objects.alias': { + 'Meta': {'object_name': 'Alias'}, + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objattribute': { + 'Meta': {'object_name': 'ObjAttribute'}, + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'db_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objectdb': { + 'Meta': {'object_name': 'ObjectDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_destination': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'destinations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_home': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'homes_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objectnick': { + 'Meta': {'unique_together': "(('db_nick', 'db_type', 'db_obj'),)", 'object_name': 'ObjectNick'}, + 'db_nick': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'db_real': ('django.db.models.fields.TextField', [], {}), + 'db_type': ('django.db.models.fields.CharField', [], {'default': "'inputline'", 'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerdb': { + 'Meta': {'object_name': 'PlayerDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']", 'null': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['objects'] diff --git a/src/objects/migrations/0005_add_object_default_locks.py b/src/objects/migrations/0005_add_object_default_locks.py new file mode 100644 index 0000000000..8dccaf25b5 --- /dev/null +++ b/src/objects/migrations/0005_add_object_default_locks.py @@ -0,0 +1,121 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models, utils + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + + # we need to add a default lock string to all objects, then a separate set to Characters. + + lockstring1 = 'control:id(1);get:all();edit:perm(Wizards);examine:perm(Builders);call:true();puppet:id(#4) or perm(Immortals) or pperm(Immortals);delete:id(1) or perm(Wizards)' + lockstring2 = 'control:id(#3) or perm(Immortals);get:perm(Wizards);edit:perm(Wizards);examine:perm(Builders);call:false();puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals);delete:perm(Wizards)' + + try: + for obj in orm.ObjectDB.objects.all().exclude(db_player__isnull=False): + obj.db_lock_storage = lockstring1 + obj.save() + for obj in orm.ObjectDB.objects.filter(db_player__isnull=False): + obj.db_lock_storage = lockstring2 % (obj.id, obj.db_player.id) + obj.save() + + except utils.DatabaseError: + # running from scatch. In this case we just ignore this. + pass + + def backwards(self, orm): + "Write your backwards methods here." + raise RuntimeError("You cannot reverse this migration.") + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'objects.alias': { + 'Meta': {'object_name': 'Alias'}, + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objattribute': { + 'Meta': {'object_name': 'ObjAttribute'}, + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'db_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objectdb': { + 'Meta': {'object_name': 'ObjectDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_destination': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'destinations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_home': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'homes_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'objects.objectnick': { + 'Meta': {'unique_together': "(('db_nick', 'db_type', 'db_obj'),)", 'object_name': 'ObjectNick'}, + 'db_nick': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']"}), + 'db_real': ('django.db.models.fields.TextField', [], {}), + 'db_type': ('django.db.models.fields.CharField', [], {'default': "'inputline'", 'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerdb': { + 'Meta': {'object_name': 'PlayerDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']", 'null': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['objects'] diff --git a/src/objects/models.py b/src/objects/models.py index 34f9f3ee11..bcaac75789 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -16,25 +16,24 @@ transparently through the decorating TypeClass. from django.db import models from django.conf import settings +from django.contrib.contenttypes.models import ContentType from src.utils.idmapper.models import SharedMemoryModel -from src.typeclasses.models import Attribute, TypedObject +from src.typeclasses.models import Attribute, TypedObject, TypeNick, TypeNickHandler from src.typeclasses.typeclass import TypeClass from src.objects.manager import ObjectManager +from src.players.models import PlayerDB from src.server.models import ServerConfig 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 is_iter, to_unicode +from src.utils.utils import is_iter, to_unicode, to_str, mod_import + +#PlayerDB = ContentType.objects.get(app_label="players", model="playerdb").model_class() FULL_PERSISTENCE = settings.FULL_PERSISTENCE - -try: - HANDLE_SEARCH_ERRORS = __import__( - settings.ALTERNATE_OBJECT_SEARCH_ERROR_HANDLER).handle_search_errors, fromlist=[None] -except Exception: - from src.objects.object_search_funcs \ - import handle_search_errors as HANDLE_SEARCH_ERRORS +AT_SEARCH_RESULT = mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) #------------------------------------------------------------ # @@ -80,80 +79,33 @@ class Alias(SharedMemoryModel): #------------------------------------------------------------ # -# Nick +# Object Nicks # #------------------------------------------------------------ -class Nick(SharedMemoryModel): +class ObjectNick(TypeNick): """ - This model holds whichever alternate names this object - has for OTHER objects, but also for arbitrary strings, - channels, players etc. Setting a nick does not affect - the nicknamed object at all (as opposed to Aliases above), - and only this object will be able to refer to the nicknamed - object by the given nick. - + The default nick types used by Evennia are: inputline (default) - match against all input player - match against player searches obj - match against object searches channel - used to store own names for channels - """ - db_nick = models.CharField(max_length=255, db_index=True) # the nick - db_real = models.TextField() # the aliased string - db_type = models.CharField(default="inputline", max_length=16, null=True, blank=True) # the type of nick db_obj = models.ForeignKey("ObjectDB") class Meta: "Define Django meta options" - verbose_name = "Nickname" - verbose_name_plural = "Nicknames" + verbose_name = "Nickname for Objects" + verbose_name_plural = "Nicknames Objects" unique_together = ("db_nick", "db_type", "db_obj") -class NickHandler(object): +class ObjectNickHandler(TypeNickHandler): """ Handles nick access and setting. Accessed through ObjectDB.nicks """ + NickClass = ObjectNick - def __init__(self, obj): - "Setup" - self.obj = obj - - def add(self, nick, realname, nick_type="inputline"): - "We want to assign a new nick" - if not nick or not nick.strip(): - return - nick = nick.strip() - real = realname.strip() - query = Nick.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) - if query.count(): - old_nick = query[0] - old_nick.db_real = real - old_nick.save() - else: - new_nick = Nick(db_nick=nick, db_real=real, db_type=nick_type, db_obj=self.obj) - new_nick.save() - def delete(self, nick, nick_type="inputline"): - "Removes a nick" - nick = nick.strip() - query = Nick.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) - if query.count(): - # remove the found nick(s) - query.delete() - def get(self, nick=None, nick_type="inputline"): - if nick: - query = Nick.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) - query = query.values_list("db_real", flat=True) - if query.count(): - return query[0] - else: - return nick - else: - return Nick.objects.filter(db_obj=self.obj) - def has(self, nick, nick_type="inputline"): - "Returns true/false if this nick is defined or not" - return Nick.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type).count() #------------------------------------------------------------ # @@ -239,7 +191,7 @@ class ObjectDB(TypedObject): self.cmdset.update(init_mode=True) self.scripts = ScriptHandler(self) self.scripts.validate(init_mode=True) - self.nicks = NickHandler(self) + self.nicks = ObjectNickHandler(self) # Wrapper properties to easily set database fields. These are # @property decorators that allows to access these fields using @@ -312,7 +264,7 @@ class ObjectDB(TypedObject): loc = location elif ObjectDB.objects.dbref(location): # location is a dbref; search - loc = ObjectDB.objects.dbref_search(location) + loc = ObjectDB.objects.dbref_search(ocation) if loc and hasattr(loc,'dbobj'): loc = loc.dbobj else: @@ -524,14 +476,11 @@ class ObjectDB(TypedObject): global_search=False, attribute_name=None, use_nicks=False, location=None, - ignore_errors=False): + ignore_errors=False, player=False): """ Perform a standard object search in the database, handling multiple results and lack thereof gracefully. - if local_only AND search_self are both false, a global - search is done instead. - ostring: (str) The string to match object names against. Obs - To find a player, append * to the start of ostring. @@ -544,7 +493,16 @@ class ObjectDB(TypedObject): ignore_errors : Don't display any error messages even if there are none/multiple matches - just return the result as a list. + player : Don't search for an Object but a Player. + This will also find players that don't + currently have a character. + Use * to search for objects controlled by a specific + player. Note that the object controlled by the player will be + returned, not the player object itself. This also means that + this will not find Players without a character. Use the keyword + player=True to find player objects. + Note - for multiple matches, the engine accepts a number linked to the key in order to separate the matches from each other without showing the dbref explicitly. Default @@ -554,22 +512,30 @@ class ObjectDB(TypedObject): etc. """ if use_nicks: - if ostring.startswith('*'): + if ostring.startswith('*') or player: # player nick replace - ostring = "*%s" % self.nicks.get(ostring.lstrip('*'), nick_type="player") + ostring = self.nicks.get(ostring.lstrip('*'), nick_type="player") + if not player: + ostring = "*%s" % ostring else: # object nick replace ostring = self.nicks.get(ostring, nick_type="object") - results = ObjectDB.objects.object_search(self, ostring, - global_search=global_search, - attribute_name=attribute_name, - location=location) + if player: + if ostring in ("me", "self", "*me", "*self"): + results = [self.player] + else: + results = PlayerDB.objects.player_search(ostring.lstrip('*')) + else: + results = ObjectDB.objects.object_search(self, ostring, + global_search=global_search, + attribute_name=attribute_name, + location=location) if ignore_errors: return results - return HANDLE_SEARCH_ERRORS(self, ostring, results, global_search) - + # this import is cache after the first call. + return AT_SEARCH_RESULT(self, ostring, results, global_search) # # Execution/action methods @@ -588,7 +554,7 @@ class ObjectDB(TypedObject): 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]] - for nick in Nick.objects.filter(db_obj=self, db_type__in=("inputline","channel")): + for nick in ObjectNick.objects.filter(db_obj=self, db_type__in=("inputline","channel")): if nick.db_nick in raw_list: raw_string = raw_string.replace(nick.db_nick, nick.db_real, 1) break @@ -605,7 +571,7 @@ class ObjectDB(TypedObject): """ # This is an important function that must always work. # we use a different __getattribute__ to avoid recursive loops. - + if object.__getattribute__(self, 'player'): object.__getattribute__(self, 'player').msg(message, from_obj, data) @@ -796,9 +762,15 @@ class ObjectDB(TypedObject): return False # See if we need to kick the player off. + for session in self.sessions: - session.msg("Your character %s has been destroyed. Goodbye." % self.name) - session.session_disconnect() + session.msg("Your character %s has been destroyed." % self.name) + #session.session_disconnect() + + # sever the connection (important!) + if object.__getattribute__(self, 'player') and self.player: + self.player.character = None + self.player = None # if self.player: # self.player.user.is_active = False @@ -811,6 +783,3 @@ class ObjectDB(TypedObject): # Perform the deletion of the object super(ObjectDB, self).delete() return True - -# Deferred import to avoid circular import errors. -from src.commands import cmdhandler diff --git a/src/objects/object_search_funcs.py b/src/objects/object_search_funcs.py deleted file mode 100644 index c5678eeb24..0000000000 --- a/src/objects/object_search_funcs.py +++ /dev/null @@ -1,102 +0,0 @@ -""" -Default functions for formatting and processing object searches. - -This is in its own module due to them being possible to -replace from the settings file by use of setting the variables - -ALTERNATE_OBJECT_SEARCH_ERROR_HANDLER -ALTERNATE_OBJECT_SEARCH_MULTIMATCH_PARSER - -Both the replacing functions must have the same name and same input/output -as the ones in this module. -""" - -def handle_search_errors(emit_to_obj, ostring, results, global_search=False): - """ - Takes a search result (a list) and - formats eventual errors. - - emit_to_obj - object to receive feedback. - ostring - original search string - results - list of object matches, if any - global_search - if this was a global_search or not - (if it is, there might be an idea of supplying - dbrefs instead of only numbers) - """ - if not results: - emit_to_obj.msg("Could not find '%s'." % ostring) - return None - if len(results) > 1: - # we have more than one match. We will display a - # list of the form 1-objname, 2-objname etc. - - # check if the emit_to_object may se dbrefs - show_dbref = global_search and \ - emit_to_obj.check_permstring('Builders') - - string = "More than one match for '%s'" % ostring - string += " (please narrow target):" - for num, result in enumerate(results): - invtext = "" - dbreftext = "" - if result.location == emit_to_obj: - invtext = " (carried)" - if show_dbref: - dbreftext = "(#%i)" % result.id - string += "\n %i-%s%s%s" % (num+1, result.name, - dbreftext, invtext) - emit_to_obj.msg(string.strip()) - return None - else: - return results[0] - -def object_multimatch_parser(ostring): - """ - Parse number-identifiers. - - Sometimes it can happen that there are several objects in the room - all with exactly the same key/identifier. Showing dbrefs to - separate them is not suitable for all types of games since it's - unique to that object (and e.g. in rp-games the object might not - want to be identified like that). Instead Evennia allows for - dbref-free matching by letting the user number which of the - objects in a multi-match they want. - - Ex for use in game session: - - > look - You see: ball, ball, ball and ball. - > get ball - There where multiple matches for ball: - 1-ball - 2-ball - 3-ball - 4-ball - > get 3-ball - You get the ball. - - The actual feedback upon multiple matches has to be - handled by the searching command. The syntax shown above is the - default. - - For replacing, the method must be named the same and - take the searchstring as argument and - return a tuple (int, string) where int is the identifier - matching which of the results (in order) should be used to - pick out the right match from the multimatch). Note - that the engine assumes this number to start with 1 (i.e. not - zero as in normal Python). - """ - if not isinstance(ostring, basestring): - return (None, ostring) - if not '-' in ostring: - return (None, ostring) - try: - 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) - except IndexError: - return (None, ostring) diff --git a/src/objects/objects.py b/src/objects/objects.py index b275042041..f5fe331e19 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -49,6 +49,9 @@ class Object(TypeClass): """ This sets up the default properties of an Object, just before the more general at_object_creation. + + Don't change this, instead edit at_object_creation() to + overload the defaults (it is called after this one). """ # the default security setup fallback for a generic # object. Overload in child for a custom setup. Also creation @@ -63,6 +66,7 @@ class Object(TypeClass): self.locks.add("delete:perm(Wizards)") # delete object self.locks.add("get:all()") # pick up object self.locks.add("call:true()") # allow to call commands on this object + self.locks.add("puppet:id(%s) or perm(Immortals) or pperm(Immortals)" % dbref) # restricts puppeting of this object def at_object_creation(self): """ @@ -310,9 +314,11 @@ class Character(Object): def basetype_setup(self): """ Setup character-specific security + + Don't change this, instead edit at_object_creation() to + overload the defaults (it is called after this one). """ super(Character, self).basetype_setup() - self.locks.add("puppet:id(%s) or perm(Immortals)" % self.dbobj.dbref) # who may become this object's player self.locks.add("get:false()") # noone can pick up the character self.locks.add("call:false()") # no commands can be called on character @@ -348,9 +354,13 @@ class Room(Object): """ Simple setup, shown as an example (since default is None anyway) + + Don't change this, instead edit at_object_creation() to + overload the defaults (it is called after this one). """ super(Room, self).basetype_setup() + self.locks.add("puppet:false()") # would be weird to puppet a room ... self.locks.add("get:false()") super(Room, self).basetype_setup() @@ -371,9 +381,13 @@ class Exit(Object): def basetype_setup(self): """ Setup exit-security + + Don't change this, instead edit at_object_creation() to + overload the defaults (it is called after this one). """ # the lock is open to all by default super(Exit, self).basetype_setup() + self.locks.add("puppet:false()") # would be weird to puppet an exit ... self.locks.add("traverse:all()") # who can pass through exit self.locks.add("get:false()") # noone can pick up the exit diff --git a/src/players/manager.py b/src/players/manager.py index 6748288ac3..a3947c1f60 100644 --- a/src/players/manager.py +++ b/src/players/manager.py @@ -146,17 +146,13 @@ class PlayerManager(TypedObjectManager): ostring = a string or database id. """ - players = [] - try: - # try dbref match - dbref = int(ostring.strip('#')) - players = self.filter(id=dbref) - except Exception: - pass - if not players: - players = self.filter(user__username=ostring) - return players - + ostring = ostring.lstrip("*") + dbref = self.dbref(ostring) + if dbref: + matches = self.filter(id=dbref) + if matches: + return matches + return self.filter(user__username__iexact=ostring) def swap_character(self, player, new_character, delete_old_character=False): """ diff --git a/src/players/migrations/0003_auto__add_field_playerdb_db_cmdset_storage.py b/src/players/migrations/0003_auto__add_field_playerdb_db_cmdset_storage.py new file mode 100644 index 0000000000..c089e1f8ec --- /dev/null +++ b/src/players/migrations/0003_auto__add_field_playerdb_db_cmdset_storage.py @@ -0,0 +1,95 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding field 'PlayerDB.db_cmdset_storage' + db.add_column('players_playerdb', 'db_cmdset_storage', self.gf('django.db.models.fields.TextField')(null=True), keep_default=False) + + + def backwards(self, orm): + + # Deleting field 'PlayerDB.db_cmdset_storage' + db.delete_column('players_playerdb', 'db_cmdset_storage') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'objects.objectdb': { + 'Meta': {'object_name': 'ObjectDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_destination': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'destinations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_home': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'homes_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerattribute': { + 'Meta': {'object_name': 'PlayerAttribute'}, + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']"}), + 'db_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerdb': { + 'Meta': {'object_name': 'PlayerDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']", 'null': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + } + } + + complete_apps = ['players'] diff --git a/src/players/migrations/0004_auto__add_playernick__add_unique_playernick_db_nick_db_type_db_obj.py b/src/players/migrations/0004_auto__add_playernick__add_unique_playernick_db_nick_db_type_db_obj.py new file mode 100644 index 0000000000..c9f3bf9661 --- /dev/null +++ b/src/players/migrations/0004_auto__add_playernick__add_unique_playernick_db_nick_db_type_db_obj.py @@ -0,0 +1,116 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import SchemaMigration +from django.db import models + +class Migration(SchemaMigration): + + def forwards(self, orm): + + # Adding model 'PlayerNick' + db.create_table('players_playernick', ( + ('id', self.gf('django.db.models.fields.AutoField')(primary_key=True)), + ('db_nick', self.gf('django.db.models.fields.CharField')(max_length=255, db_index=True)), + ('db_real', self.gf('django.db.models.fields.TextField')()), + ('db_type', self.gf('django.db.models.fields.CharField')(default='inputline', max_length=16, null=True, blank=True)), + ('db_obj', self.gf('django.db.models.fields.related.ForeignKey')(to=orm['players.PlayerDB'])), + )) + db.send_create_signal('players', ['PlayerNick']) + + # Adding unique constraint on 'PlayerNick', fields ['db_nick', 'db_type', 'db_obj'] + db.create_unique('players_playernick', ['db_nick', 'db_type', 'db_obj_id']) + + + def backwards(self, orm): + + # Removing unique constraint on 'PlayerNick', fields ['db_nick', 'db_type', 'db_obj'] + db.delete_unique('players_playernick', ['db_nick', 'db_type', 'db_obj_id']) + + # Deleting model 'PlayerNick' + db.delete_table('players_playernick') + + + models = { + 'auth.group': { + 'Meta': {'object_name': 'Group'}, + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '80'}), + 'permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}) + }, + 'auth.permission': { + 'Meta': {'ordering': "('content_type__app_label', 'content_type__model', 'codename')", 'unique_together': "(('content_type', 'codename'),)", 'object_name': 'Permission'}, + 'codename': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'content_type': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['contenttypes.ContentType']"}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '50'}) + }, + 'auth.user': { + 'Meta': {'object_name': 'User'}, + 'date_joined': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'email': ('django.db.models.fields.EmailField', [], {'max_length': '75', 'blank': 'True'}), + 'first_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'groups': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Group']", 'symmetrical': 'False', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'is_active': ('django.db.models.fields.BooleanField', [], {'default': 'True'}), + 'is_staff': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'is_superuser': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'last_login': ('django.db.models.fields.DateTimeField', [], {'default': 'datetime.datetime.now'}), + 'last_name': ('django.db.models.fields.CharField', [], {'max_length': '30', 'blank': 'True'}), + 'password': ('django.db.models.fields.CharField', [], {'max_length': '128'}), + 'user_permissions': ('django.db.models.fields.related.ManyToManyField', [], {'to': "orm['auth.Permission']", 'symmetrical': 'False', 'blank': 'True'}), + 'username': ('django.db.models.fields.CharField', [], {'unique': 'True', 'max_length': '30'}) + }, + 'contenttypes.contenttype': { + 'Meta': {'ordering': "('name',)", 'unique_together': "(('app_label', 'model'),)", 'object_name': 'ContentType', 'db_table': "'django_content_type'"}, + 'app_label': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'model': ('django.db.models.fields.CharField', [], {'max_length': '100'}), + 'name': ('django.db.models.fields.CharField', [], {'max_length': '100'}) + }, + 'objects.objectdb': { + 'Meta': {'object_name': 'ObjectDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_destination': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'destinations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_home': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'homes_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_location': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'locations_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerattribute': { + 'Meta': {'object_name': 'PlayerAttribute'}, + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']"}), + 'db_value': ('django.db.models.fields.TextField', [], {'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + }, + 'players.playerdb': { + 'Meta': {'object_name': 'PlayerDB'}, + 'db_cmdset_storage': ('django.db.models.fields.TextField', [], {'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255'}), + 'db_lock_storage': ('django.db.models.fields.TextField', [], {'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['objects.ObjectDB']", 'null': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_typeclass_path': ('django.db.models.fields.CharField', [], {'max_length': '255', 'null': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}), + 'user': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['auth.User']", 'unique': 'True'}) + }, + 'players.playernick': { + 'Meta': {'unique_together': "(('db_nick', 'db_type', 'db_obj'),)", 'object_name': 'PlayerNick'}, + 'db_nick': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']"}), + 'db_real': ('django.db.models.fields.TextField', [], {}), + 'db_type': ('django.db.models.fields.CharField', [], {'default': "'inputline'", 'max_length': '16', 'null': 'True', 'blank': 'True'}), + 'id': ('django.db.models.fields.AutoField', [], {'primary_key': 'True'}) + } + } + + complete_apps = ['players'] diff --git a/src/players/models.py b/src/players/models.py index a3abb3b3a7..a5ca57d6f3 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -44,10 +44,16 @@ from django.conf import settings from django.db import models from django.contrib.auth.models import User from django.utils.encoding import smart_str +from django.contrib.contenttypes.models import ContentType + from src.server.sessionhandler import SESSIONS from src.players import manager -from src.typeclasses.models import Attribute, TypedObject -from src.utils import logger +from src.typeclasses.models import Attribute, TypedObject, TypeNick, TypeNickHandler +from src.utils import logger, utils +from src.commands.cmdsethandler import CmdSetHandler +from src.commands import cmdhandler + +AT_SEARCH_RESULT = utils.mod_import(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) #------------------------------------------------------------ # @@ -68,6 +74,36 @@ class PlayerAttribute(Attribute): verbose_name = "Player Attribute" verbose_name_plural = "Player Attributes" +#------------------------------------------------------------ +# +# Player Nicks +# +#------------------------------------------------------------ + +class PlayerNick(TypeNick): + """ + + The default nick types used by Evennia are: + inputline (default) - match against all input + player - match against player searches + obj - match against object searches + channel - used to store own names for channels + """ + db_obj = models.ForeignKey("PlayerDB") + + class Meta: + "Define Django meta options" + verbose_name = "Nickname for Players" + verbose_name_plural = "Nicknames Players" + unique_together = ("db_nick", "db_type", "db_obj") + +class PlayerNickHandler(TypeNickHandler): + """ + Handles nick access and setting. Accessed through ObjectDB.nicks + """ + NickClass = PlayerNick + + #------------------------------------------------------------ # # PlayerDB @@ -116,6 +152,9 @@ class PlayerDB(TypedObject): # the in-game object connected to this player (if any). # Use the property 'obj' to access. db_obj = models.ForeignKey("objects.ObjectDB", null=True) + + # database storage of persistant cmdsets. + db_cmdset_storage = models.TextField(null=True) # Database manager objects = manager.PlayerManager() @@ -123,6 +162,14 @@ class PlayerDB(TypedObject): class Meta: app_label = 'players' + def __init__(self, *args, **kwargs): + "Parent must be initiated first" + TypedObject.__init__(self, *args, **kwargs) + # handlers + self.cmdset = CmdSetHandler(self) + self.cmdset.update(init_mode=True) + self.nicks = PlayerNickHandler(self) + # Wrapper properties to easily set database fields. These are # @property decorators that allows to access these fields using # normal python operations (without having to remember to save() @@ -172,6 +219,26 @@ class PlayerDB(TypedObject): self.db_obj = None self.save() character = property(character_get, character_set, character_del) + # cmdset_storage property + #@property + def cmdset_storage_get(self): + "Getter. Allows for value = self.name. Returns a list of cmdset_storage." + if self.db_cmdset_storage: + return [path.strip() for path in self.db_cmdset_storage.split(',')] + return [] + #@cmdset_storage.setter + def cmdset_storage_set(self, value): + "Setter. Allows for self.name = value. Stores as a comma-separated string." + if utils.is_iter(value): + value = ",".join([str(val).strip() for val in value]) + self.db_cmdset_storage = value + self.save() + #@cmdset_storage.deleter + def cmdset_storage_del(self): + "Deleter. Allows for del self.name" + self.db_cmdset_storage = "" + self.save() + cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del) class Meta: "Define Django meta options" @@ -245,15 +312,23 @@ class PlayerDB(TypedObject): Evennia -> User This is the main route for sending data back to the user from the server. """ + if from_obj: try: from_obj.at_msg_send(outgoing_string, to_obj=self, data=data) except Exception: pass - if object.__getattribute__(self, "character"): - if self.character.at_msg_receive(outgoing_string, from_obj=from_obj, data=data): - for session in object.__getattribute__(self, 'sessions'): - session.msg(outgoing_string, data) + + if (object.__getattribute__(self, "character") + and not self.character.at_msg_receive(outgoing_string, from_obj=from_obj, data=data)): + # the at_msg_receive() hook may block receiving of certain messages + return + + outgoing_string = utils.to_str(outgoing_string, force_string=True) + + for session in object.__getattribute__(self, 'sessions'): + session.msg(outgoing_string, data) + def swap_character(self, new_character, delete_old_character=False): """ @@ -261,3 +336,49 @@ class PlayerDB(TypedObject): """ return self.__class__.objects.swap_character(self, new_character, delete_old_character=delete_old_character) + + # + # Execution/action methods + # + + def execute_cmd(self, raw_string): + """ + Do something as this playe. This command transparently + lets its typeclass execute the command. + raw_string - raw command input coming from the command line. + """ + # nick replacement - we require full-word matching. + + 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]] + for nick in PlayerNick.objects.filter(db_obj=self, db_type__in=("inputline","channel")): + if nick.db_nick in raw_list: + raw_string = raw_string.replace(nick.db_nick, nick.db_real, 1) + break + cmdhandler.cmdhandler(self.typeclass(self), raw_string) + + def search(self, ostring, global_search=False, attribute_name=None, use_nicks=False, + location=None, ignore_errors=False, player=False): + """ + A shell method mimicking the ObjectDB equivalent, for easy inclusion from + commands regardless of if the command is run by a Player or an Object. + """ + + if self.character: + # run the normal search + return self.character.search(ostring, global_search=global_search, attribute_name=attribute_name, + use_nicks=use_nicks, location=location, + ignore_errors=ignore_errors, player=player) + if player: + # seach for players + matches = self.__class__.objects.player_search(ostring) + else: + # more limited player-only search. Still returns an Object. + ObjectDB = ContentType.objects.get(app_label="objects", model="objectdb").model_class() + matches = ObjectDB.objects.object_search(self, ostring, global_search=global_search) + # deal with results + matches = AT_SEARCH_RESULT(self, ostring, matches, global_search=global_search) + return matches + diff --git a/src/players/player.py b/src/players/player.py index 0c17f83354..7613d3bce9 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -12,19 +12,20 @@ instead for most things). """ from src.typeclasses.typeclass import TypeClass +from settings import CMDSET_OOC + class Player(TypeClass): """ Base typeclass for all Players. """ - def at_player_creation(self): + def basetype_setup(self): + """ + This sets up the basic properties for a player. + Overload this with at_player_creation rather than + changing this method. + """ - This is called once, the very first time - the player is created (i.e. first time they - register with the game). It's a good place - to store attributes all players should have, - like configuration values etc. - """ # the text encoding to use. self.db.encoding = "utf-8" @@ -35,6 +36,21 @@ class Player(TypeClass): self.locks.add("boot:perm(Wizards)") self.locks.add("msg:all()") + # The ooc player cmdset + self.cmdset.add_default(CMDSET_OOC, permanent=True) + self.cmdset.outside_access = False + + def at_player_creation(self): + """ + This is called once, the very first time + the player is created (i.e. first time they + register with the game). It's a good place + to store attributes all players should have, + like configuration values etc. + """ + pass + + # Note that the hooks below also exist # in the character object's typeclass. You # can often ignore these and rely on the diff --git a/src/server/server.py b/src/server/server.py index 27d0e0597d..94e8319f24 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -1,10 +1,12 @@ """ This module implements the main Evennia server process, the core of -the game engine. Only import this once! +the game engine. Don't import this module! If you need to access the +server processes from code, instead import sessionhandler.SESSIONS +and use its 'server' property. This module should be started with the 'twistd' executable since it -sets up all the networking features. (this is done by -game/evennia.py). +sets up all the networking features. (this is done by automatically +by game/evennia.py). """ import time diff --git a/src/server/session.py b/src/server/session.py index 446a1bf7cb..17447c3a17 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -145,13 +145,12 @@ class SessionBase(object): and have to be done right after this function! """ if self.logged_in: - character = self.get_character() - if character: - uaccount = character.player.user - uaccount.last_login = datetime.now() - uaccount.save() - self.at_disconnect() - self.logged_in = False + player = self.get_player() + uaccount = player.user + uaccount.last_login = datetime.now() + uaccount.save() + self.at_disconnect() + self.logged_in = False SESSIONS.remove_session(self) def session_validate(self): @@ -227,16 +226,18 @@ class SessionBase(object): return # all other inputs, including empty inputs - character = self.get_character() + character = self.get_character() if character: - #print "loggedin _execute_cmd: '%s' __ %s" % (command_string, character) # normal operation. character.execute_cmd(command_string) else: - #print "unloggedin _execute_cmd: '%s' __ %s" % (command_string, character) - # we are not logged in yet; call cmdhandler directly - cmdhandler.cmdhandler(self, command_string, unloggedin=True) + if self.logged_in: + # there is no character, but we are logged in. Use player instead. + self.get_player().execute_cmd(command_string) + else: + # we are not logged in. Use special unlogged-in call. + cmdhandler.cmdhandler(self, command_string, unloggedin=True) self.update_session_counters() def get_data_obj(self, **kwargs): diff --git a/src/settings_default.py b/src/settings_default.py index ac66f1606d..2a2e2eca34 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -136,21 +136,18 @@ DATABASE_PORT = '' ################################################### # An alternate command parser module to use -# (if not set, uses 'src.commands.cmdparser') -ALTERNATE_PARSER = "" +COMMAND_PARSER = "src.commands.cmdparser.cmdparser" # How many space-separated words a command name may have # and still be identified as one single command # (e.g. 'push button' instead of 'pushbutton') COMMAND_MAXLEN = 3 # The handler that outputs errors when searching -# objects using object.search(). (If not set, uses -# src.objects.object_search_funcs.handle_search_errors) -ALTERNATE_OBJECT_SEARCH_ERROR_HANDLER = "" +# objects using object.search(). +SEARCH_AT_RESULT = "src.commands.cmdparser.at_search_result" # The parser used in order to separate multiple # object matches (so you can separate between same-named -# objects without using dbrefs). (If not set, uses -# src.objects.object_search_funcs.object_multimatch_parser). -ALTERNATE_OBJECT_SEARCH_MULTIMATCH_PARSER = "" +# objects without using dbrefs). +SEARCH_AT_MULTIMATCH_INPUT = "src.commands.cmdparser.at_multimatch_input" # The module holding text strings for the connection screen. # This module should contain one or more variables # with strings defining the look of the screen. @@ -162,8 +159,10 @@ CONNECTION_SCREEN_MODULE = "game.gamesrc.world.connection_screens" # Command set used before player has logged in CMDSET_UNLOGGEDIN = "game.gamesrc.commands.basecmdset.UnloggedinCmdSet" -# Default set for logged in players (fallback) +# Default set for logged in player with characters (fallback) CMDSET_DEFAULT = "game.gamesrc.commands.basecmdset.DefaultCmdSet" +# Command set for players without a character (ooc) +CMDSET_OOC = "game.gamesrc.commands.basecmdset.OOCCmdSet" ################################################### # Default Object typeclasses diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 1ebba244ca..304c86386d 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -294,6 +294,87 @@ class Attribute(SharedMemoryModel): return self.locks.check(accessing_obj, access_type=access_type, default=default) +#------------------------------------------------------------ +# +# Nicks +# +#------------------------------------------------------------ + +class TypeNick(SharedMemoryModel): + """ + This model holds whichever alternate names this object + has for OTHER objects, but also for arbitrary strings, + channels, players etc. Setting a nick does not affect + the nicknamed object at all (as opposed to Aliases above), + and only this object will be able to refer to the nicknamed + object by the given nick. + + The default nick types used by Evennia are: + inputline (default) - match against all input + player - match against player searches + obj - match against object searches + channel - used to store own names for channels + + """ + db_nick = models.CharField(max_length=255, db_index=True) # the nick + db_real = models.TextField() # the aliased string + db_type = models.CharField(default="inputline", max_length=16, null=True, blank=True) # the type of nick + db_obj = None #models.ForeignKey("ObjectDB") + + class Meta: + "Define Django meta options" + abstract = True + verbose_name = "Nickname" + verbose_name_plural = "Nicknames" + unique_together = ("db_nick", "db_type", "db_obj") + +class TypeNickHandler(object): + """ + Handles nick access and setting. Accessed through ObjectDB.nicks + """ + + NickClass = TypeNick + + def __init__(self, obj): + "Setup" + self.obj = obj + + def add(self, nick, realname, nick_type="inputline"): + "We want to assign a new nick" + if not nick or not nick.strip(): + return + nick = nick.strip() + real = realname.strip() + query = self.NickClass.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) + if query.count(): + old_nick = query[0] + old_nick.db_real = real + old_nick.save() + else: + new_nick = self.NickClass(db_nick=nick, db_real=real, db_type=nick_type, db_obj=self.obj) + new_nick.save() + def delete(self, nick, nick_type="inputline"): + "Removes a nick" + nick = nick.strip() + query = self.NickClass.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) + if query.count(): + # remove the found nick(s) + query.delete() + def get(self, nick=None, nick_type="inputline"): + if nick: + query = self.NickClass.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type) + query = query.values_list("db_real", flat=True) + if query.count(): + return query[0] + else: + return nick + else: + return self.NickClass.objects.filter(db_obj=self.obj) + def has(self, nick, nick_type="inputline"): + "Returns true/false if this nick is defined or not" + return self.NickClass.objects.filter(db_obj=self.obj, db_nick__iexact=nick, db_type__iexact=nick_type).count() + + #------------------------------------------------------------ # # Typed Objects @@ -992,9 +1073,12 @@ class TypedObject(SharedMemoryModel): def check_permstring(self, permstring): """ - This explicitly checks for we hold particular permission without involving + This explicitly checks if we hold particular permission without involving any locks. """ + if self.player and self.player.is_superuser: + return True + if not permstring: return False perm = permstring.lower() diff --git a/src/utils/create.py b/src/utils/create.py index 69564efcbf..29b0a756b6 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -390,6 +390,7 @@ def create_player(name, email, password, new_player = PlayerDB(db_key=name, user=new_user) new_player.save() + 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/reloads.py b/src/utils/reloads.py index 4d13c0ac02..464c15f308 100644 --- a/src/utils/reloads.py +++ b/src/utils/reloads.py @@ -173,8 +173,8 @@ def reset_loop(): [m.locks.reset() for m in Msg.objects.all()] [c.locks.reset() for c in Channel.objects.all()] [s.locks.reset() for s in ScriptDB.objects.all()] - [p.locks.reset() for p in PlayerDB.objects.all()] [(o.typeclass(o), o.cmdset.reset(), o.locks.reset()) for o in ObjectDB.get_all_cached_instances()] + [(p.typeclass(p), p.cmdset.reset(), p.locks.reset()) for p in PlayerDB.get_all_cached_instances()] t2 = time.time() cemit_info(" ... Loop finished in %g seconds." % (t2-t1)) diff --git a/src/utils/search.py b/src/utils/search.py index 3bf7f8671f..e53ea08427 100644 --- a/src/utils/search.py +++ b/src/utils/search.py @@ -27,11 +27,15 @@ Example: To reach the search method 'get_object_with_user' # Import the manager methods to be wrapped -from src.objects.models import ObjectDB -from src.players.models import PlayerDB -from src.scripts.models import ScriptDB -from src.comms.models import Msg, Channel -from src.help.models import HelpEntry +from django.contrib.contenttypes.models import ContentType + +# import objects this way to avoid circular import problems +ObjectDB = ContentType.objects.get(app_label="objects", model="objectdb").model_class() +PlayerDB = ContentType.objects.get(app_label="players", model="playerdb").model_class() +ScriptDB = ContentType.objects.get(app_label="scripts", model="scriptdb").model_class() +Msg = ContentType.objects.get(app_label="comms", model="msg").model_class() +Channel = ContentType.objects.get(app_label="comms", model="channel").model_class() +HelpEntry = ContentType.objects.get(app_label="help", model="helpentry").model_class() # # Search objects as a character diff --git a/src/utils/test_utils.py b/src/utils/test_utils.py index 951da588a4..1da08837d0 100644 --- a/src/utils/test_utils.py +++ b/src/utils/test_utils.py @@ -20,3 +20,5 @@ class EvenniaTestSuiteRunner(DjangoTestSuiteRunner): 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/utils.py b/src/utils/utils.py index 529f01ee8d..60669fa4d8 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -235,14 +235,28 @@ def dbref(dbref): except Exception: return None return dbref + return None -def to_unicode(obj, encoding='utf-8'): +def to_unicode(obj, encoding='utf-8', force_string=False): """ - This decodes a suitable object to - the unicode format. Note that one - needs to encode it back to utf-8 - before writing to disk or printing. + This decodes a suitable object to the unicode format. Note that + one needs to encode it back to utf-8 before writing to disk or + printing. Note that non-string objects are let through without + conversion - this is important for e.g. Attributes. Use + force_string to enforce conversion of objects to string. . """ + + if force_string and not isinstance(obj, basestring): + # some sort of other object. Try to + # convert it to a string representation. + if hasattr(obj, '__str__'): + obj = obj.__str__() + elif hasattr(obj, '__unicode__'): + obj = obj.__unicode__() + else: + # last resort + obj = str(obj) + if isinstance(obj, basestring) and not isinstance(obj, unicode): try: obj = unicode(obj, encoding) @@ -257,11 +271,26 @@ def to_unicode(obj, encoding='utf-8'): raise Exception("Error: '%s' contains invalid character(s) not in %s." % (obj, encoding)) return obj -def to_str(obj, encoding='utf-8'): +def to_str(obj, encoding='utf-8', force_string=False): """ This encodes a unicode string back to byte-representation, - for printing, writing to disk etc. + for printing, writing to disk etc. Note that non-string + objects are let through without modification - this is + required e.g. for Attributes. Use force_string to force + conversion of objects to strings. """ + + if force_string and not isinstance(obj, basestring): + # some sort of other object. Try to + # convert it to a string representation. + if hasattr(obj, '__str__'): + obj = obj.__str__() + elif hasattr(obj, '__unicode__'): + obj = obj.__unicode__() + else: + # last resort + obj = str(obj) + if isinstance(obj, basestring) and isinstance(obj, unicode): try: obj = obj.encode(encoding) @@ -321,7 +350,7 @@ def inherits_from(obj, parent): Takes an object and tries to determine if it inherits at any distance from parent. What differs this function from e.g. isinstance() is that obj may be both an instance and a class, and parent - may be an instance, a class, or the python path to a class (counting +< may be an instance, a class, or the python path to a class (counting from the evennia root directory). """ @@ -480,10 +509,11 @@ def has_parent(basepath, obj): # instance. Not sure if one should defend against this. return False -def mod_import(mod_path): +def mod_import(mod_path, propname=None): """ Takes filename of a module, converts it to a python path - and imports it. + and imports it. If property is given, return the named + property from this module instead of the module itself. """ def log_trace(errmsg=None): @@ -494,6 +524,7 @@ def mod_import(mod_path): """ from traceback import format_exc from twisted.python import log + print errmsg tracestring = format_exc() if tracestring: @@ -507,23 +538,39 @@ def mod_import(mod_path): for line in errmsg.splitlines(): log.msg('[EE] %s' % line) - if not os.path.isabs(mod_path): - mod_path = os.path.abspath(mod_path) - path, filename = mod_path.rsplit(os.path.sep, 1) - modname = filename.rstrip('.py') + # first try to import as a python path + try: + mod = __import__(mod_path, fromlist=["None"]) + except ImportError: + + # try absolute path import instead - try: - result = imp.find_module(modname, [path]) - except ImportError: - log_trace("Could not find module '%s' (%s.py) at path '%s'" % (modname, modname, path)) - return - try: - mod = imp.load_module(modname, *result) - except ImportError: - log_trace("Could not find or import module %s at path '%s'" % (modname, path)) - mod = None - # we have to close the file handle manually - result[0].close() + if not os.path.isabs(mod_path): + mod_path = os.path.abspath(mod_path) + path, filename = mod_path.rsplit(os.path.sep, 1) + modname = filename.rstrip('.py') + + try: + result = imp.find_module(modname, [path]) + except ImportError: + log_trace("Could not find module '%s' (%s.py) at path '%s'" % (modname, modname, path)) + return + try: + mod = imp.load_module(modname, *result) + except ImportError: + log_trace("Could not find or import module %s at path '%s'" % (modname, path)) + mod = None + # we have to close the file handle manually + result[0].close() + + if mod and propname: + # we have a module, extract the sought property from it. + try: + mod_prop = mod.__dict__[to_str(propname)] + except KeyError: + log_trace("Could not import property '%s' from module %s." % (propname, mod_path)) + return None + return mod_prop return mod def string_from_module(modpath, variable=None):