From b8a13a2389f4bb55a4fffdf939767d9c6350e458 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 1 May 2011 18:04:15 +0000 Subject: [PATCH] Migrate. Made Exits work differently, by use of commands directly instead of an exithandler assigning commands on-the-fly. This solution is a lot cleaner and also solves an issue where @reload would kill typeclasses in situations where an exit was painting to an object whose typeclass was reloaded (same issue occured if the exit typeclass itself was reloaded). As part of these fixes I cleaned up the merging of cmdsets to now merge in strict priority order, as one would expect them to do. Many small bug-fixes and cleanups all over. Resolves issue 164. Resolves issue 163. --- game/gamesrc/objects/baseobjects.py | 22 +-- src/commands/cmdhandler.py | 87 ++++-------- src/commands/cmdset.py | 4 +- src/commands/default/building.py | 27 ++-- src/commands/default/cmdset_unloggedin.py | 1 + src/commands/default/general.py | 10 +- src/commands/default/muxcommand.py | 2 +- src/commands/default/syscommands.py | 35 ----- src/objects/exithandler.py | 104 --------------- src/objects/manager.py | 23 ++-- src/objects/models.py | 15 ++- src/objects/objects.py | 126 ++++++++++++++---- .../migrations/0005_adding_player_cmdset.py | 108 +++++++++++++++ src/server/initial_setup.py | 2 +- src/utils/create.py | 5 +- src/utils/idmapper/base.py | 2 + src/utils/reloads.py | 48 +++---- 17 files changed, 323 insertions(+), 298 deletions(-) delete mode 100644 src/objects/exithandler.py create mode 100644 src/players/migrations/0005_adding_player_cmdset.py diff --git a/game/gamesrc/objects/baseobjects.py b/game/gamesrc/objects/baseobjects.py index 2157c9ddb2..0781d64822 100644 --- a/game/gamesrc/objects/baseobjects.py +++ b/game/gamesrc/objects/baseobjects.py @@ -133,18 +133,18 @@ class Room(BaseRoom): class Exit(BaseExit): """ - Exits are connectors between rooms. They are identified by the - engine by having an attribute "_destination" defined on themselves, - pointing to a valid room object. That is usually defined when - the exit is created (in, say, @dig or @link-type commands), not - hard-coded in their typeclass. Exits do have to make sure they - clean up a bit after themselves though, easiest accomplished - by letting by_object_delete() call the object's parent. - - Note that exits need to do cache-cleanups after they are - deleted, so if you re-implement at_object_delete() for some - reason, make sure to call the parent class-method too! + Exits are connectors between rooms. Exits defines the + 'destination' property and sets up a command on itself with the + same name as the Exit object - this command allows the player to + traverse the exit to the destination just by writing the name of + the object on the command line. + Relevant hooks: + at_before_traverse(traveller) - called just before traversing + at_after_traverse(traveller, source_loc) - called just after traversing + at_failed_traverse(traveller) - called if traversal failed for some reason. Will + not be called if the attribute 'err_traverse' is + defined, in which case that will simply be echoed. """ pass diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index 57437d86eb..b723d90993 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -53,7 +53,6 @@ from traceback import format_exc 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, utils #This switches the command parser to a user-defined one. @@ -70,7 +69,6 @@ CMD_NOMATCH = "__nomatch_command" CMD_MULTIMATCH = "__multimatch_command" CMD_NOPERM = "__noperm_command" CMD_CHANNEL = "__send_to_channel" -CMD_EXIT = "__move_to_exit" class NoCmdSets(Exception): "No cmdsets found. Critical error." @@ -92,63 +90,45 @@ def get_and_merge_cmdsets(caller): caller_cmdset = caller.cmdset.current except AttributeError: caller_cmdset = None - - # All surrounding cmdsets + + # Create cmdset for all player's available channels channel_cmdset = None - 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) + + # Gather cmdsets from location, objects in location or carried + local_objects_cmdsets = [None] 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 and the location itself - local_objlist = location.contents + caller.contents + [location] - local_objects_cmdsets = [obj.cmdset.current - for obj in local_objlist + local_objlist = location.contents_get(exclude=caller.dbobj) + caller.contents + [location] + local_objects_cmdsets = [obj.cmdset.current for obj in local_objlist if obj.locks.check(caller, 'call', no_superuser_bypass=True)] - # Merge all command sets into one - # (the order matters, the higher-prio cmdsets are merged last) - cmdset = caller_cmdset - for obj_cmdset in [obj_cmdset for obj_cmdset in local_objects_cmdsets if obj_cmdset]: - # Here only, object cmdsets are merged with duplicates=True - # (or we would never be able to differentiate between same-prio objects) - try: - old_duplicate_flag = obj_cmdset.duplicates - obj_cmdset.duplicates = True - cmdset = obj_cmdset + cmdset - obj_cmdset.duplicates = old_duplicate_flag - except TypeError: - pass - # Exits and channels automatically has duplicates=True. - try: - cmdset = exit_cmdset + cmdset - except TypeError: - pass - try: - 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 + # Player object's commandsets + try: + player_cmdset = caller.player.cmdset.current + except AttributeError: + player_cmdset = None + + cmdsets = [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_objects_cmdsets + # weed out all non-found sets + cmdsets = [cmdset for cmdset in cmdsets if cmdset] + # sort cmdsets after reverse priority (highest prio are merged in last) + cmdsets = sorted(cmdsets, key=lambda x: x.priority) + if cmdsets: + # Merge all command sets into one, beginning with the lowest-prio one + cmdset = cmdsets.pop(0) + for merging_cmdset in cmdsets: + #print "<%s(%s,%s)> onto <%s(%s,%s)>" % (merging_cmdset.key, merging_cmdset.priority, merging_cmdset.mergetype, + # cmdset.key, cmdset.priority, cmdset.mergetype) + cmdset = merging_cmdset + cmdset + else: + cmdset = None + return cmdset def match_command(cmd_candidates, cmdset, logged_caller=None): """ @@ -365,17 +345,6 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False): cmd_candidate.args) raise ExecSystemCommand(cmd, sysarg) - # Check if this is an Exit match. - if hasattr(cmd, 'is_exit') and cmd.is_exit: - # even if a user-defined syscmd is not defined, the - # found cmd is already a system command in its own right. - syscmd = cmdset.get(CMD_EXIT) - if syscmd: - # replace system command with custom version - cmd = syscmd - sysarg = raw_string - raise ExecSystemCommand(cmd, sysarg) - # A normal command. # Assign useful variables to the instance diff --git a/src/commands/cmdset.py b/src/commands/cmdset.py index 47b334d9d9..dd8dff5648 100644 --- a/src/commands/cmdset.py +++ b/src/commands/cmdset.py @@ -237,7 +237,6 @@ class CmdSet(object): cmds = [instantiate(cmd)] else: cmds = instantiate(cmd) - for cmd in cmds: # add all commands if not hasattr(cmd, 'obj'): @@ -247,8 +246,9 @@ class CmdSet(object): self.commands[ic] = cmd # replace except ValueError: self.commands.append(cmd) - # extra run to make sure to avoid doublets + # extra run to make sure to avoid doublets self.commands = list(set(self.commands)) + #print "In cmdset.add(cmd):", self.key, cmd def remove(self, cmd): """ diff --git a/src/commands/default/building.py b/src/commands/default/building.py index 40953f0dbe..f5762dceb6 100644 --- a/src/commands/default/building.py +++ b/src/commands/default/building.py @@ -136,6 +136,8 @@ class CmdSetObjAlias(MuxCommand): aliases = list(set(old_aliases)) # save back to object. obj.aliases = aliases + # we treat this as a re-caching (relevant for exits to re-build their exit commands with the correct aliases) + obj.at_cache() caller.msg("Aliases for '%s' are now set to %s." % (obj.key, ", ".join(obj.aliases))) class CmdCopy(ObjManipCommand): @@ -175,7 +177,7 @@ class CmdCopy(ObjManipCommand): return to_obj_name = "%s_copy" % from_obj_name to_obj_aliases = ["%s_copy" % alias for alias in from_obj.aliases] - copiedobj = ObjectDB.objects.copy_object(from_obj, new_name=to_obj_name, + copiedobj = ObjectDB.objects.copy_object(from_obj, new_key=to_obj_name, new_aliases=to_obj_aliases) if copiedobj: string = "Identical copy of %s, named '%s' was created." % (from_obj_name, to_obj_name) @@ -633,6 +635,8 @@ class CmdDig(ObjManipCommand): typeclass = "%s.%s" % (settings.BASE_TYPECLASS_PATH, typeclass) + # create room + lockstring = "control:id(%s) or perm(Immortal); delete:id(%s) or perm(Wizard); edit:id(%s) or perm(Wizard)" lockstring = lockstring % (caller.dbref, caller.dbref, caller.dbref) @@ -644,6 +648,9 @@ class CmdDig(ObjManipCommand): alias_string = " (%s)" % ", ".join(new_room.aliases) room_string = "Created room %s(%s)%s of type %s." % (new_room, new_room.dbref, alias_string, typeclass) + + # create exit to room + exit_to_string = "" exit_back_string = "" @@ -667,17 +674,17 @@ class CmdDig(ObjManipCommand): typeclass.startswith('game.')): typeclass = "%s.%s" % (settings.BASE_TYPECLASS_PATH, typeclass) - new_to_exit = create.create_object(typeclass, to_exit["name"], - location, - aliases=to_exit["aliases"]) - new_to_exit.destination = new_room - new_to_exit.locks.add(lockstring) + new_to_exit = create.create_object(typeclass, to_exit["name"], location, + aliases=to_exit["aliases"], + locks=lockstring, destination=new_room) alias_string = "" if new_to_exit.aliases: alias_string = " (%s)" % ", ".join(new_to_exit.aliases) exit_to_string = "\nCreated Exit from %s to %s: %s(%s)%s." exit_to_string = exit_to_string % (location.name, new_room.name, new_to_exit, new_to_exit.dbref, alias_string) + + # Create exit back from new room if len(self.rhs_objs) > 1: # Building the exit back to the current room @@ -700,10 +707,8 @@ class CmdDig(ObjManipCommand): typeclass = "%s.%s" % (settings.BASE_TYPECLASS_PATH, typeclass) new_back_exit = create.create_object(typeclass, back_exit["name"], - new_room, - aliases=back_exit["aliases"]) - new_back_exit.destination = location - new_back_exit.locks.add(lockstring) + new_room, aliases=back_exit["aliases"], + locks=lockstring, destination=location) alias_string = "" if new_back_exit.aliases: alias_string = " (%s)" % ", ".join(new_back_exit.aliases) @@ -1534,7 +1539,7 @@ class CmdExamine(ObjManipCommand): perms = [""] elif not perms: perms = [""] - string += headers["perms"] % (", ".join(perms)) + string += headers["playerperms"] % (", ".join(perms)) string += headers["typeclass"] % (obj.typeclass, obj.typeclass_path) if hasattr(obj, "location"): diff --git a/src/commands/default/cmdset_unloggedin.py b/src/commands/default/cmdset_unloggedin.py index 0a128da33c..1e189ed460 100644 --- a/src/commands/default/cmdset_unloggedin.py +++ b/src/commands/default/cmdset_unloggedin.py @@ -11,6 +11,7 @@ class UnloggedinCmdSet(CmdSet): Sets up the unlogged cmdset. """ key = "Unloggedin" + priority = 0 def at_cmdset_creation(self): "Populate the cmdset" diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 218f1508c3..85d7996ebe 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -613,13 +613,15 @@ class CmdOOCLook(CmdLook): help_cateogory = "General" def func(self): - "implement the command" + "implement the ooc look command" - self.character = None + self.character = None if utils.inherits_from(self.caller, "src.objects.objects.Object"): # An object of some type is calling. Convert to player. + print self.caller, self.caller.__class__ self.character = self.caller - self.caller = self.caller.player + if hasattr(self.caller, "player"): + self.caller = self.caller.player if not self.character: string = "You are out-of-character (OOC). " @@ -627,7 +629,7 @@ class CmdOOCLook(CmdLook): self.caller.msg(string) else: self.caller = self.character # we have to put this back for normal look to work. - super(CmdLook, self).func() + super(CmdOOCLook, self).func() class CmdIC(MuxCommand): """ diff --git a/src/commands/default/muxcommand.py b/src/commands/default/muxcommand.py index 2210d66f41..d1787eb1f9 100644 --- a/src/commands/default/muxcommand.py +++ b/src/commands/default/muxcommand.py @@ -136,7 +136,7 @@ class MuxCommand(Command): string += "-" * 50 string += "\nname of cmd (self.key): {w%s{n\n" % self.key string += "cmd aliases (self.aliases): {w%s{n\n" % self.aliases - string += "cmd perms (self.permissions): {w%s{n\n" % self.permissions + string += "cmd locks (self.locks): {w%s{n\n" % self.locks string += "help category (self.help_category): {w%s{n\n" % self.help_category string += "object calling (self.caller): {w%s{n\n" % self.caller string += "object storing cmdset (self.obj): {w%s{n\n" % self.obj diff --git a/src/commands/default/syscommands.py b/src/commands/default/syscommands.py index f41dbd8113..84b05dc493 100644 --- a/src/commands/default/syscommands.py +++ b/src/commands/default/syscommands.py @@ -28,7 +28,6 @@ from src.commands.cmdhandler import CMD_NOMATCH from src.commands.cmdhandler import CMD_MULTIMATCH from src.commands.cmdhandler import CMD_NOPERM from src.commands.cmdhandler import CMD_CHANNEL -from src.commands.cmdhandler import CMD_EXIT from src.commands.default.muxcommand import MuxCommand @@ -188,37 +187,3 @@ class SystemSendToChannel(MuxCommand): msg = "[%s] %s: %s" % (channel.key, caller.name, msg) msgobj = create.create_message(caller, msg, channels=[channel]) channel.msg(msgobj) - -# -# Command called when the system recognizes the command given -# as matching an exit from the room. E.g. if there is an exit called 'door' -# and the user gives the command -# > door -# the exit 'door' should be traversed to its destination. - -class SystemUseExit(MuxCommand): - """ - Handles what happens when user gives a valid exit - as a command. It receives the raw string as input. - """ - key = CMD_EXIT - locks = "cmd:all()" - - def func(self): - """ - Handle traversing an exit - """ - caller = self.caller - if not self.args: - return - exit_name = self.args - exi = caller.search(exit_name) - if not exi: - return - destination = exi.destination - if not destination: - return - if exit.access(caller, 'traverse'): - caller.move_to(destination) - else: - caller.msg("You cannot enter") diff --git a/src/objects/exithandler.py b/src/objects/exithandler.py deleted file mode 100644 index f4b2c8eaf2..0000000000 --- a/src/objects/exithandler.py +++ /dev/null @@ -1,104 +0,0 @@ -""" -This handler creates cmdsets on the fly, by searching -an object's location for valid exit objects. -""" - -from src.commands import cmdset, command - - -class ExitCommand(command.Command): - "Simple identifier command" - is_exit = True - locks = "cmd:all()" # should always be set to this. - destination = None - obj = None - - def func(self): - "Default exit traverse if no syscommand is defined." - - if self.obj.access(self.caller, 'traverse'): - # we may traverse the exit. - - old_location = None - if hasattr(self.caller, "location"): - old_location = self.caller.location - - # call pre/post hooks and move object. - self.obj.at_before_traverse(self.caller) - self.caller.move_to(self.destination) - self.obj.at_after_traverse(self.caller, old_location) - - else: - if self.obj.db.err_traverse: - # if exit has a better error message, let's use it. - self.caller.msg(self.obj.db.err_traverse) - else: - # No shorthand error message. Call hook. - self.obj.at_fail_traverse(self.caller) - -class ExitHandler(object): - """ - The exithandler auto-creates 'commands' to represent exits in the - room. It is called by cmdhandler when building its index of all - viable commands. This allows for exits to be processed along with - all other inputs the player gives to the game. The handler tries - to intelligently cache exit objects to cut down on processing. - - """ - - def __init__(self): - "Setup cache storage" - self.cached_exit_cmds = {} - - def clear(self, exitcmd=None): - """ - Reset cache storage. If obj is given, only remove - that object from cache. - """ - if exitcmd: - # delete only a certain object from cache - try: - del self.cached_exit_cmds[exitcmd.id] - except KeyError: - pass - return - # reset entire cache - self.cached_exit_cmds = {} - - def get_cmdset(self, srcobj): - """ - Search srcobjs location for valid exits, and - return objects as stored in command set - """ - # create a quick "throw away" cmdset - exit_cmdset = cmdset.CmdSet(None) - exit_cmdset.key = '_exitset' - exit_cmdset.priority = 9 - exit_cmdset.duplicates = True - try: - location = srcobj.location - except Exception: - location = None - if not location: - # there can be no exits if there's no location - return exit_cmdset - - # use exits to create searchable "commands" for the cmdhandler - for exi in location.exits: - if exi.id in self.cached_exit_cmds: - # retrieve from cache - exit_cmdset.add(self.cached_exit_cmds[exi.id]) - else: - # not in cache, create a new exit command - cmd = ExitCommand() - cmd.key = exi.name.strip().lower() - cmd.obj = exi - if exi.aliases: - cmd.aliases = exi.aliases - cmd.destination = exi.destination - exit_cmdset.add(cmd) - self.cached_exit_cmds[exi.id] = cmd - return exit_cmdset - -# The actual handler - call this to get exits -EXITHANDLER = ExitHandler() diff --git a/src/objects/manager.py b/src/objects/manager.py index 80a99b1052..40badec3e6 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -274,25 +274,26 @@ class ObjectManager(TypedObjectManager): # ObjectManager Copy method # - def copy_object(self, original_object, new_name=None, + def copy_object(self, original_object, new_key=None, new_location=None, new_player=None, new_home=None, - new_permissions=None, new_locks=None, new_aliases=None): + new_permissions=None, new_locks=None, new_aliases=None, new_destination=None): """ - Create and return a new object as a copy of the source object. All will + Create and return a new object as a copy of the original object. All will be identical to the original except for the arguments given specifically to this method. original_object (obj) - the object to make a copy from - new_name (str) - name the copy differently from the original. + new_key (str) - name the copy differently from the original. new_location (obj) - if not None, change the location new_home (obj) - if not None, change the Home new_aliases (list of strings) - if not None, change object aliases. + new_destination (obj) - if not None, change destination """ # get all the object's stats typeclass_path = original_object.typeclass_path - if not new_name: - new_name = original_object.key + if not new_key: + new_key = original_object.key if not new_location: new_location = original_object.location if not new_home: @@ -305,13 +306,15 @@ class ObjectManager(TypedObjectManager): new_locks = original_object.db_lock_storage if not new_permissions: new_permissions = original_object.permissions - + if not new_destination: + new_destination = original_object.destination + # create new object from src.utils import create from src.scripts.models import ScriptDB - new_object = create.create_object(typeclass_path, new_name, new_location, - new_home, new_player, new_permissions, - new_locks, new_aliases) + new_object = create.create_object(typeclass_path, key=new_key, location=new_location, + home=new_home, player=new_player, permissions=new_permissions, + locks=new_locks, aliases=new_aliases, destination=new_destination) if not new_object: return None diff --git a/src/objects/models.py b/src/objects/models.py index 1352118deb..2ee90ae2ef 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -449,12 +449,12 @@ class ObjectDB(TypedObject): has_player = property(has_player_get) #@property - def contents_get(self): + def contents_get(self, exclude=None): """ Returns the contents of this object, i.e. all objects that has this object set as its location. """ - return ObjectDB.objects.get_contents(self) + return ObjectDB.objects.get_contents(self, excludeobj=exclude) contents = property(contents_get) #@property @@ -748,6 +748,17 @@ class ObjectDB(TypedObject): obj.msg(string) obj.move_to(home) + def copy(self, new_key=None): + """ + Makes an identical copy of this object and returns + it. The copy will be named _copy by default. If you + want to customize the copy by changing some settings, use + the manager method copy_object directly. + """ + if not new_key: + new_key = "%s_copy" % self.key + return ObjectDB.objects.copy_object(self, new_key=new_key) + def delete(self): """ Deletes this object. diff --git a/src/objects/objects.py b/src/objects/objects.py index f057dc27c2..2adee59951 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -17,8 +17,6 @@ they control by simply linking to a new object's user property. from src.typeclasses.typeclass import TypeClass from src.commands import cmdset, command -from src.objects.exithandler import EXITHANDLER - # # Base class to inherit from. @@ -77,6 +75,12 @@ class Object(TypeClass): """ pass + def at_cache(self): + """ + Called whenever this object is cached or reloaded. + """ + pass + def at_first_login(self): """ Only called once, the very first @@ -401,49 +405,113 @@ class Room(Object): class Exit(Object): """ - This is the base exit object - it connects a location - to another. What separates it from other objects - is that it has the 'destination' property defined. - Note that property is the only identifier to - separate an exit from normal objects, so if the property - is removed, it will be treated like any other object. This - also means that any object can be made an exit by setting - the property destination to a valid location - ('Quack like a duck...' and so forth). + This is the base exit object - it connects a location to + another. This is done by the exit assigning a "command" on itself + with the same name as the exit object (to do this we need to + remember to re-create the command when the object is cached since it must be + created dynamically depending on what the exit is called). This + command (which has a high priority) will thus allow us to traverse exits + simply by giving the exit-object's name on its own. + """ + + # Helper classes and methods to implement the Exit. These need not + # be overloaded unless one want to change the foundation for how + # Exits work. See the end of the class for hook methods to overload. + + def create_exit_cmdset(self, exidbobj): + """ + Helper function for creating an exit command set + command. + + Note that exitdbobj is an ObjectDB instance. This is necessary for + handling reloads and avoid tracebacks while the typeclass system + is rebooting. + """ + + class ExitCommand(command.Command): + """ + This is a command that simply cause the caller + to traverse the object it is attached to. + """ + locks = "cmd:all()" # should always be set to this. + obj = None + + def func(self): + "Default exit traverse if no syscommand is defined." + + if self.obj.access(self.caller, 'traverse'): + # we may traverse the exit. + + old_location = None + if hasattr(self.caller, "location"): + old_location = self.caller.location + + # call pre/post hooks and move object. + self.obj.at_before_traverse(self.caller) + self.caller.move_to(self.obj.destination) + self.obj.at_after_traverse(self.caller, old_location) + + else: + if self.obj.db.err_traverse: + # if exit has a better error message, let's use it. + self.caller.msg(self.obj.db.err_traverse) + else: + # No shorthand error message. Call hook. + self.obj.at_failed_traverse(self.caller) + + # create an exit command. + cmd = ExitCommand() + cmd.key = exidbobj.db_key.strip().lower() + cmd.obj = exidbobj + cmd.aliases = exidbobj.aliases + cmd.destination = exidbobj.db_destination + # create a cmdset + exit_cmdset = cmdset.CmdSet(None) + exit_cmdset.key = '_exitset' + exit_cmdset.priority = 9 + exit_cmdset.duplicates = True + # add command to cmdset + exit_cmdset.add(cmd) + return exit_cmdset + + # Command hooks + 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). + overload the default locks (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 by default - self.locks.add("get:false()") # noone can pick up the exit - + + # this is the fundamental thing for making the Exit work: + self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False) # an exit should have a destination (this is replaced at creation time) if self.dbobj.location: self.destination = self.dbobj.location + + # setting default locks (overload these in at_object_creation() + self.locks.add("puppet:false()") # would be weird to puppet an exit ... + self.locks.add("traverse:all()") # who can pass through exit by default + self.locks.add("get:false()") # noone can pick up the exit - def at_object_delete(self): - """ - We have to make sure to clean the exithandler cache - when deleting the exit, or a new exit could be created - out of sync with the cache. You should do this also if - overloading this function in a child class. - """ - EXITHANDLER.clear(self.dbobj) - return True + def at_cache(self): + "Called when the typeclass is re-cached or reloaded. Should usually not be edited." + self.cmdset.add_default(self.create_exit_cmdset(self.dbobj), permanent=False) + + # this and other hooks are what usually can be modified safely. + + def at_object_creation(self): + "Called once, when object is first created (after basetype_setup)." + pass def at_failed_traverse(self, traversing_object): """ This is called if an object fails to traverse this object for some reason. It will not be called if the attribute "err_traverse" is defined, that attribute will then be echoed back instead as a convenient shortcut. + + (See also hooks at_before_traverse and at_after_traverse). """ - traversing_object.msg("You cannot enter %s." % self.key) - + traversing_object.msg("You cannot go there.") diff --git a/src/players/migrations/0005_adding_player_cmdset.py b/src/players/migrations/0005_adding_player_cmdset.py new file mode 100644 index 0000000000..b01490847d --- /dev/null +++ b/src/players/migrations/0005_adding_player_cmdset.py @@ -0,0 +1,108 @@ +# encoding: utf-8 +import datetime +from south.db import db +from south.v2 import DataMigration +from django.db import models, utils +from django.conf import settings + + +class Migration(DataMigration): + + def forwards(self, orm): + "Write your forwards methods here." + try: + for player in orm.PlayerDB.objects.all(): + if not player.db_cmdset_storage: + player.db_cmdset_storage = settings.CMDSET_OOC + player.save() + except utils.DatabaseError: + # this will happen if we start db from scratch (ignore in that case) + pass + + def backwards(self, orm): + "Write your backwards methods here." + raise RuntimeError("This migration cannot be reverted.") + + 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/server/initial_setup.py b/src/server/initial_setup.py index 39af49a0f6..79041ca0e3 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -57,7 +57,7 @@ def create_objects(): user=god_user) god_character.id = 1 god_character.db.desc = 'This is User #1.' - god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all()") + god_character.locks.add("examine:perm(Immortals);edit:false();delete:false();boot:false();msg:all();puppet:false()") god_character.save() diff --git a/src/utils/create.py b/src/utils/create.py index 29b0a756b6..2a51461f0e 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -29,7 +29,8 @@ from src.utils.utils import is_iter, has_parent # def create_object(typeclass, key=None, location=None, - home=None, player=None, permissions=None, locks=None, aliases=None): + home=None, player=None, permissions=None, locks=None, + aliases=None, destination=None): """ Create a new in-game object. Any game object is a combination of a database object that stores data persistently to @@ -89,6 +90,8 @@ def create_object(typeclass, key=None, location=None, # link a player and the object together new_object.player = player player.obj = new_object + + new_object.destination = destination # call the hook method. This is where all at_creation # customization happens as the typeclass stores custom diff --git a/src/utils/idmapper/base.py b/src/utils/idmapper/base.py index 7e00291846..2c363db5aa 100755 --- a/src/utils/idmapper/base.py +++ b/src/utils/idmapper/base.py @@ -101,6 +101,8 @@ class SharedMemoryModel(Model): """ if instance._get_pk_val() is not None: cls.__instance_cache__[instance._get_pk_val()] = instance + if hasattr(instance, 'at_cache') and callable(instance.at_cache): + instance.at_cache() #key = "%s-%s" % (cls, instance.pk) #TCACHE[key] = instance #print "cached: %s (%s: %s) (total cached: %s)" % (instance, cls.__name__, len(cls.__instance_cache__), len(TCACHE)) diff --git a/src/utils/reloads.py b/src/utils/reloads.py index 464c15f308..bd0db5312a 100644 --- a/src/utils/reloads.py +++ b/src/utils/reloads.py @@ -17,7 +17,6 @@ from src.comms.models import Channel, Msg from src.help.models import HelpEntry from src.typeclasses import models as typeclassmodels -from src.objects import exithandler from src.comms import channelhandler from src.comms.models import Channel from src.utils import reimport, utils, logger @@ -34,12 +33,7 @@ def start_reload_loop(): "" cemit_info('-'*50) cemit_info(" Starting asynchronous server reload.") - reload_modules() # this must be given time to finish - - wait_time = 5 - cemit_info(" Waiting %ss to give modules time to fully re-cache ..." % wait_time) - time.sleep(wait_time) - + reload_modules() reload_scripts() reload_commands() reset_loop() @@ -49,7 +43,7 @@ def start_reload_loop(): cemit_info(" Asynchronous server reload finished.\n" + '-'*50) def at_err(e): "error callback" - string = "Reload: Asynchronous reset exited with an error:\n{r%s{n" % e.getErrorMessage() + string = " Reload: Asynchronous reset exited with an error:\n {r%s{n" % e.getErrorMessage() cemit_info(string) utils.run_async(run_loop, at_return, at_err) @@ -89,13 +83,13 @@ def reload_modules(): "Check so modpath is not in an unsafe module" return not any(mpath.startswith(modpath) for mpath in unsafe_modules) - cemit_info(" Cleaning module caches ...") + #cemit_info(" Cleaning module caches ...") # clean as much of the caches as we can cache = AppCache() cache.app_store = SortedDict() #cache.app_models = SortedDict() # cannot clean this, it resets ContentTypes! - cache.app_errors = {} + cache.app_errors = {} cache.handled = {} cache.loaded = False @@ -108,29 +102,28 @@ def reload_modules(): string = "" if unsafe_dir_modified or unsafe_mod_modified: - if unsafe_dir_modified: - string += "\n-{rThe following changed module(s) can only be reloaded{n" - string += "\n {rby a server reboot:{n\n %s\n" - string = string % unsafe_dir_modified + if unsafe_mod_modified: - string += "\n-{rThe following modules contains at least one Script class with a timer{n" - string += "\n {rcomponent and has already spawned instances - these cannot be{n " - string += "\n {rsafely cleaned from memory on the fly. Stop all the affected scripts{n " - string += "\n {ror restart the server to safely reload:{n\n %s\n" - string = string % unsafe_mod_modified + string += "\n {rModules containing Script classes with a timer component" + string += "\n and which has already spawned instances cannot be reloaded safely.{n" + string += "\n {rThese module(s) can only be reloaded by server reboot:{n\n %s\n" + string = string % ", ".join(unsafe_dir_modified + unsafe_mod_modified) + if string: cemit_info(string) if safe_modified: - cemit_info(" Reloading module(s):\n %s ..." % safe_modified) + cemit_info(" Reloading safe module(s):{n\n %s" % "\n ".join(safe_modified)) reimport.reimport(*safe_modified) + wait_time = 5 + cemit_info(" Waiting %s secs to give modules time to re-cache ..." % wait_time) + time.sleep(wait_time) cemit_info(" ... all safe modules reloaded.") else: - cemit_info(" ... no modules could be (or needed to be) reloaded.") + cemit_info(" No modules reloaded.") # clean out cache dictionary of typeclasses, exits and channels - typeclassmodels.reset() - exithandler.EXITHANDLER.clear() + typeclassmodels.reset() channelhandler.CHANNELHANDLER.update() # run through all objects in database, forcing re-caching. @@ -161,23 +154,22 @@ def reload_scripts(scripts=None, obj=None, key=None, def reload_commands(): from src.commands import cmdsethandler cmdsethandler.CACHED_CMDSETS = {} - cemit_info(" Cleaned cmdset cache.") + #cemit_info(" Cleaned cmdset cache.") def reset_loop(): "Reload and restart all entities that can be reloaded." # run the reset loop on all objects - cemit_info(" Running resets on database entities ...") + cemit_info(" Resetting all cached database entities ...") t1 = time.time() - [h.locks.reset() for h in HelpEntry.objects.all()] [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()] - [(o.typeclass(o), o.cmdset.reset(), o.locks.reset()) for o in ObjectDB.get_all_cached_instances()] + [(o.typeclass(o), o.cmdset.reset(), o.locks.reset(), o.at_cache()) 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)) + cemit_info(" ... Reset finished in %g seconds." % (t2-t1)) def cemit_info(message): """