From 9af9f94fa0aae61b99b06ecc0c536569786e4135 Mon Sep 17 00:00:00 2001 From: Griatch Date: Thu, 25 Dec 2014 14:43:43 +0100 Subject: [PATCH] Moved Players over to the new proxy system, made the start-hook called by the save-signal system into at_first_save() --- src/objects/objects.py | 4 +- src/players/manager.py | 56 ++++---- src/players/models.py | 238 +--------------------------------- src/players/player.py | 266 ++++++++++++++++++++++++++++++++------ src/scripts/models.py | 40 +++--- src/scripts/scripts.py | 219 ++++++++++++++++++------------- src/typeclasses/models.py | 7 +- src/utils/create.py | 214 +++++++----------------------- src/utils/spawner.py | 5 +- 9 files changed, 465 insertions(+), 584 deletions(-) diff --git a/src/objects/objects.py b/src/objects/objects.py index 5d1818755e..416d3d69d4 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -840,7 +840,7 @@ class DefaultObject(ObjectDB): except AttributeError: return False - def at_instance_creation(self): + def at_first_save(self): """ This is called by the typeclass system whenever an instance of this class is saved for the first time. It is a generic hook @@ -874,6 +874,7 @@ class DefaultObject(ObjectDB): updates.append("db_destination") if updates: self.save(update_fields=updates) + if cdict["permissions"]: self.permissions.add(cdict["permissions"]) if cdict["locks"]: @@ -883,6 +884,7 @@ class DefaultObject(ObjectDB): if cdict["location"]: cdict["location"].at_object_receive(self, None) self.at_after_move(None) + del self._createdict self.basetype_posthook_setup() diff --git a/src/players/manager.py b/src/players/manager.py index 67c72bd147..4525c8bd19 100644 --- a/src/players/manager.py +++ b/src/players/manager.py @@ -37,7 +37,7 @@ class PlayerDBManager(TypedObjectManager, UserManager): get_player_from_uid get_player_from_name player_search (equivalent to ev.search_player) - swap_character + #swap_character """ def num_total_players(self): @@ -123,33 +123,33 @@ class PlayerDBManager(TypedObjectManager, UserManager): else: return self.filter(username__icontains=ostring) - def swap_character(self, player, new_character, delete_old_character=False): - """ - This disconnects a player from the current character (if any) and - connects to a new character object. - - """ - - if new_character.player: - # the new character is already linked to a player! - return False - - # do the swap - old_character = player.character - if old_character: - old_character.player = None - try: - player.character = new_character - new_character.player = player - except Exception: - # recover old setup - if old_character: - old_character.player = player - player.character = old_character - return False - if old_character and delete_old_character: - old_character.delete() - return True +# def swap_character(self, player, new_character, delete_old_character=False): +# """ +# This disconnects a player from the current character (if any) and +# connects to a new character object. +# +# """ +# +# if new_character.player: +# # the new character is already linked to a player! +# return False +# +# # do the swap +# old_character = player.character +# if old_character: +# old_character.player = None +# try: +# player.character = new_character +# new_character.player = player +# except Exception: +# # recover old setup +# if old_character: +# old_character.player = player +# player.character = old_character +# return False +# if old_character and delete_old_character: +# old_character.delete() +# return True class PlayerManager(PlayerDBManager, TypeclassManager): pass diff --git a/src/players/models.py b/src/players/models.py index 951c097126..d1ae752664 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -22,21 +22,14 @@ from django.contrib.auth.models import AbstractUser from django.utils.encoding import smart_str from src.players.manager import PlayerDBManager -from src.scripts.models import ScriptDB from src.typeclasses.models import TypedObject -from src.commands import cmdhandler -from src.utils import utils, logger -from src.utils.utils import to_str, make_iter - -from django.utils.translation import ugettext as _ +from src.utils.utils import make_iter __all__ = ("PlayerDB",) #_ME = _("me") #_SELF = _("self") -_SESSIONS = None -_AT_SEARCH_RESULT = utils.variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) _MULTISESSION_MODE = settings.MULTISESSION_MODE _GA = object.__getattribute__ @@ -147,7 +140,7 @@ class PlayerDB(TypedObject, AbstractUser): cmdset_storage = property(cmdset_storage_get, cmdset_storage_set, cmdset_storage_del) # - # PlayerDB main class properties and methods + # property/field access # def __str__(self): @@ -182,230 +175,3 @@ class PlayerDB(TypedObject, AbstractUser): def __uid_del(self): raise Exception("User id cannot be deleted!") uid = property(__uid_get, __uid_set, __uid_del) - - # - # PlayerDB class access methods - # - - # session-related methods - - def get_session(self, sessid): - """ - Return session with given sessid connected to this player. - note that the sessionhandler also accepts sessid as an iterable. - """ - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - return _SESSIONS.session_from_player(self, sessid) - - def get_all_sessions(self): - "Return all sessions connected to this player" - global _SESSIONS - if not _SESSIONS: - from src.server.sessionhandler import SESSIONS as _SESSIONS - return _SESSIONS.sessions_from_player(self) - sessions = property(get_all_sessions) # alias shortcut - - def disconnect_session_from_player(self, sessid): - """ - Access method for disconnecting a given session from the player - (connection happens automatically in the sessionhandler) - """ - # this should only be one value, loop just to make sure to - # clean everything - sessions = (session for session in self.get_all_sessions() - if session.sessid == sessid) - for session in sessions: - # this will also trigger unpuppeting - session.sessionhandler.disconnect(session) - - # puppeting operations - - def puppet_object(self, sessid, obj, normal_mode=True): - """ - Use the given session to control (puppet) the given object (usually - a Character type). Note that we make no puppet checks here, that must - have been done before calling this method. - - sessid - session id of session to connect - obj - the object to connect to - normal_mode - trigger hooks and extra checks - this is turned off when - the server reloads, to quickly re-connect puppets. - - returns True if successful, False otherwise - """ - session = self.get_session(sessid) - if not session: - return False - if normal_mode and session.puppet: - # cleanly unpuppet eventual previous object puppeted by this session - self.unpuppet_object(sessid) - if obj.player and obj.player.is_connected and obj.player != self: - # we don't allow to puppet an object already controlled by an active - # player. To kick a player, call unpuppet_object on them explicitly. - return - # if we get to this point the character is ready to puppet or it - # was left with a lingering player/sessid reference from an unclean - # server kill or similar - - if normal_mode: - obj.at_pre_puppet(self, sessid=sessid) - # do the connection - obj.sessid.add(sessid) - obj.player = self - session.puid = obj.id - session.puppet = obj - # validate/start persistent scripts on object - ScriptDB.objects.validate(obj=obj) - if normal_mode: - obj.at_post_puppet() - return True - - def unpuppet_object(self, sessid): - """ - Disengage control over an object - - sessid - the session id to disengage - - returns True if successful - """ - session = self.get_session(sessid) - if not session: - return False - obj = hasattr(session, "puppet") and session.puppet or None - if not obj: - return False - # do the disconnect, but only if we are the last session to puppet - obj.at_pre_unpuppet() - obj.dbobj.sessid.remove(sessid) - if not obj.dbobj.sessid.count(): - del obj.dbobj.player - obj.at_post_unpuppet(self, sessid=sessid) - session.puppet = None - session.puid = None - return True - - def unpuppet_all(self): - """ - Disconnect all puppets. This is called by server - before a reset/shutdown. - """ - for session in self.get_all_sessions(): - self.unpuppet_object(session.sessid) - - def get_puppet(self, sessid, return_dbobj=False): - """ - Get an object puppeted by this session through this player. This is - the main method for retrieving the puppeted object from the - player's end. - - sessid - return character connected to this sessid, - character - return character if connected to this player, else None. - - """ - session = self.get_session(sessid) - if not session: - return None - if return_dbobj: - return session.puppet - return session.puppet and session.puppet or None - - def get_all_puppets(self, return_dbobj=False): - """ - Get all currently puppeted objects as a list - """ - puppets = [session.puppet for session in self.get_all_sessions() - if session.puppet] - if return_dbobj: - return puppets - return [puppet for puppet in puppets] - - def __get_single_puppet(self): - """ - This is a legacy convenience link for users of - MULTISESSION_MODE 0 or 1. It will return - only the first puppet. For mode 2, this returns - a list of all characters. - """ - puppets = self.get_all_puppets() - if _MULTISESSION_MODE in (0, 1): - return puppets and puppets[0] or None - return puppets - character = property(__get_single_puppet) - puppet = property(__get_single_puppet) - - # utility methods - - def delete(self, *args, **kwargs): - """ - Deletes the player permanently. - """ - for session in self.get_all_sessions(): - # unpuppeting all objects and disconnecting the user, if any - # sessions remain (should usually be handled from the - # deleting command) - self.unpuppet_object(session.sessid) - session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) - self.scripts.stop() - _GA(self, "attributes").clear() - _GA(self, "nicks").clear() - _GA(self, "aliases").clear() - super(PlayerDB, self).delete(*args, **kwargs) - - def execute_cmd(self, raw_string, sessid=None, **kwargs): - """ - Do something as this player. This method is never called normally, - but only when the player object itself is supposed to execute the - command. It takes player nicks into account, but not nicks of - eventual puppets. - - raw_string - raw command input coming from the command line. - sessid - the optional session id to be responsible for the command-send - **kwargs - other keyword arguments will be added to the found command - object instace as variables before it executes. This is - unused by default Evennia but may be used to set flags and - change operating paramaters for commands at run-time. - """ - raw_string = utils.to_unicode(raw_string) - raw_string = self.nicks.nickreplace(raw_string, - categories=("inputline", "channel"), include_player=False) - if not sessid and _MULTISESSION_MODE in (0, 1): - # in this case, we should either have only one sessid, or the sessid - # should not matter (since the return goes to all of them we can - # just use the first one as the source) - try: - sessid = self.get_all_sessions()[0].sessid - except IndexError: - # this can happen for bots - sessid = None - return cmdhandler.cmdhandler(self, raw_string, - callertype="player", sessid=sessid, **kwargs) - - def search(self, searchdata, return_puppet=False, **kwargs): - """ - This is similar to the ObjectDB search method but will search for - Players only. Errors will be echoed, and None returned if no Player - is found. - searchdata - search criterion, the Player's key or dbref to search for - return_puppet - will try to return the object the player controls - instead of the Player object itself. If no - puppeted object exists (since Player is OOC), None will - be returned. - Extra keywords are ignored, but are allowed in call in order to make - API more consistent with objects.models.TypedObject.search. - """ - #TODO deprecation - if "return_character" in kwargs: - logger.log_depmsg("Player.search's 'return_character' keyword is deprecated. Use the return_puppet keyword instead.") - return_puppet = kwargs.get("return_character") - - matches = _GA(self, "__class__").objects.player_search(searchdata) - matches = _AT_SEARCH_RESULT(self, searchdata, matches, global_search=True) - if matches and return_puppet: - try: - return _GA(matches, "puppet") - except AttributeError: - return None - return matches - diff --git a/src/players/player.py b/src/players/player.py index dfcb90671f..4a160a4d08 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -17,15 +17,23 @@ from src.typeclasses.models import TypeclassBase from src.players.manager import PlayerManager from src.players.models import PlayerDB from src.comms.models import ChannelDB +from src.commands import cmdhandler +from src.scripts.models import ScriptDB from src.utils import logger -from src.utils.utils import lazy_property, to_str, make_iter +from src.utils.utils import (lazy_property, to_str, + make_iter, to_unicode, + variable_from_module) from src.typeclasses.attributes import NickHandler from src.scripts.scripthandler import ScriptHandler from src.commands.cmdsethandler import CmdSetHandler +from django.utils.translation import ugettext as _ __all__ = ("DefaultPlayer",) +_SESSIONS = None + +_AT_SEARCH_RESULT = variable_from_module(*settings.SEARCH_AT_RESULT.rsplit('.', 1)) _MULTISESSION_MODE = settings.MULTISESSION_MODE _CMDSET_PLAYER = settings.CMDSET_PLAYER _CONNECT_CHANNEL = None @@ -74,7 +82,7 @@ class DefaultPlayer(PlayerDB): * Helper methods msg(outgoing_string, from_obj=None, **kwargs) - swap_character(new_character, delete_old_character=False) + #swap_character(new_character, delete_old_character=False) execute_cmd(raw_string) search(ostring, global_search=False, attribute_name=None, use_nicks=False, location=None, @@ -122,6 +130,171 @@ class DefaultPlayer(PlayerDB): return NickHandler(self) + # session-related methods + + def get_session(self, sessid): + """ + Return session with given sessid connected to this player. + note that the sessionhandler also accepts sessid as an iterable. + """ + global _SESSIONS + if not _SESSIONS: + from src.server.sessionhandler import SESSIONS as _SESSIONS + return _SESSIONS.session_from_player(self, sessid) + + def get_all_sessions(self): + "Return all sessions connected to this player" + global _SESSIONS + if not _SESSIONS: + from src.server.sessionhandler import SESSIONS as _SESSIONS + return _SESSIONS.sessions_from_player(self) + sessions = property(get_all_sessions) # alias shortcut + + def disconnect_session_from_player(self, sessid): + """ + Access method for disconnecting a given session from the player + (connection happens automatically in the sessionhandler) + """ + # this should only be one value, loop just to make sure to + # clean everything + sessions = (session for session in self.get_all_sessions() + if session.sessid == sessid) + for session in sessions: + # this will also trigger unpuppeting + session.sessionhandler.disconnect(session) + + # puppeting operations + + def puppet_object(self, sessid, obj, normal_mode=True): + """ + Use the given session to control (puppet) the given object (usually + a Character type). Note that we make no puppet checks here, that must + have been done before calling this method. + + sessid - session id of session to connect + obj - the object to connect to + normal_mode - trigger hooks and extra checks - this is turned off when + the server reloads, to quickly re-connect puppets. + + returns True if successful, False otherwise + """ + session = self.get_session(sessid) + if not session: + return False + if normal_mode and session.puppet: + # cleanly unpuppet eventual previous object puppeted by this session + self.unpuppet_object(sessid) + if obj.player and obj.player.is_connected and obj.player != self: + # we don't allow to puppet an object already controlled by an active + # player. To kick a player, call unpuppet_object on them explicitly. + return + # if we get to this point the character is ready to puppet or it + # was left with a lingering player/sessid reference from an unclean + # server kill or similar + + if normal_mode: + obj.at_pre_puppet(self, sessid=sessid) + # do the connection + obj.sessid.add(sessid) + obj.player = self + session.puid = obj.id + session.puppet = obj + # validate/start persistent scripts on object + ScriptDB.objects.validate(obj=obj) + if normal_mode: + obj.at_post_puppet() + return True + + def unpuppet_object(self, sessid): + """ + Disengage control over an object + + sessid - the session id to disengage + + returns True if successful + """ + session = self.get_session(sessid) + if not session: + return False + obj = hasattr(session, "puppet") and session.puppet or None + if not obj: + return False + # do the disconnect, but only if we are the last session to puppet + obj.at_pre_unpuppet() + obj.dbobj.sessid.remove(sessid) + if not obj.dbobj.sessid.count(): + del obj.dbobj.player + obj.at_post_unpuppet(self, sessid=sessid) + session.puppet = None + session.puid = None + return True + + def unpuppet_all(self): + """ + Disconnect all puppets. This is called by server + before a reset/shutdown. + """ + for session in self.get_all_sessions(): + self.unpuppet_object(session.sessid) + + def get_puppet(self, sessid, return_dbobj=False): + """ + Get an object puppeted by this session through this player. This is + the main method for retrieving the puppeted object from the + player's end. + + sessid - return character connected to this sessid, + character - return character if connected to this player, else None. + + """ + session = self.get_session(sessid) + if not session: + return None + if return_dbobj: + return session.puppet + return session.puppet and session.puppet or None + + def get_all_puppets(self, return_dbobj=False): + """ + Get all currently puppeted objects as a list + """ + puppets = [session.puppet for session in self.get_all_sessions() + if session.puppet] + if return_dbobj: + return puppets + return [puppet for puppet in puppets] + + def __get_single_puppet(self): + """ + This is a legacy convenience link for users of + MULTISESSION_MODE 0 or 1. It will return + only the first puppet. For mode 2, this returns + a list of all characters. + """ + puppets = self.get_all_puppets() + if _MULTISESSION_MODE in (0, 1): + return puppets and puppets[0] or None + return puppets + character = property(__get_single_puppet) + puppet = property(__get_single_puppet) + + # utility methods + + def delete(self, *args, **kwargs): + """ + Deletes the player permanently. + """ + for session in self.get_all_sessions(): + # unpuppeting all objects and disconnecting the user, if any + # sessions remain (should usually be handled from the + # deleting command) + self.unpuppet_object(session.sessid) + session.sessionhandler.disconnect(session, reason=_("Player being deleted.")) + self.scripts.stop() + self.attributes.clear() + self.nicks.clear() + self.aliases.clear() + super(PlayerDB, self).delete(*args, **kwargs) ## methods inherited from database model def msg(self, text=None, from_obj=None, sessid=None, **kwargs): @@ -167,48 +340,38 @@ class DefaultPlayer(PlayerDB): for sess in self.get_all_sessions(): sess.msg(text=text, **kwargs) - def swap_character(self, new_character, delete_old_character=False): - """ - Swaps the character controlled by this Player, if possible. - - new_character (Object) - character/object to swap to - delete_old_character (bool) - delete the old character when swapping - - Returns: True/False depending on if swap suceeded or not. - """ - return super(DefaultPlayer, self).swap_character(new_character, delete_old_character=delete_old_character) - def execute_cmd(self, raw_string, sessid=None, **kwargs): """ - Do something as this object. This command transparently - lets its typeclass execute the command. This method - is -not- called by Evennia normally, it is here to be - called explicitly in code. + Do something as this player. This method is never called normally, + but only when the player object itself is supposed to execute the + command. It takes player nicks into account, but not nicks of + eventual puppets. - Argument: - raw_string (string) - raw command input - sessid (int) - id of session executing the command. This sets the - sessid property on the command + raw_string - raw command input coming from the command line. + sessid - the optional session id to be responsible for the command-send **kwargs - other keyword arguments will be added to the found command object instace as variables before it executes. This is unused by default Evennia but may be used to set flags and change operating paramaters for commands at run-time. - - Returns Deferred - this is an asynchronous Twisted object that will - not fire until the command has actually finished executing. To - overload this one needs to attach callback functions to it, with - addCallback(function). This function will be called with an - eventual return value from the command execution. - - This return is not used at all by Evennia by default, but might - be useful for coders intending to implement some sort of nested - command structure. """ - return super(DefaultPlayer, self).execute_cmd(raw_string, sessid=sessid, **kwargs) + raw_string = to_unicode(raw_string) + raw_string = self.nicks.nickreplace(raw_string, + categories=("inputline", "channel"), include_player=False) + if not sessid and _MULTISESSION_MODE in (0, 1): + # in this case, we should either have only one sessid, or the sessid + # should not matter (since the return goes to all of them we can + # just use the first one as the source) + try: + sessid = self.get_all_sessions()[0].sessid + except IndexError: + # this can happen for bots + sessid = None + return cmdhandler.cmdhandler(self, raw_string, + callertype="player", sessid=sessid, **kwargs) def search(self, searchdata, return_puppet=False, **kwargs): """ - This is similar to the Object search method but will search for + This is similar to the ObjectDB search method but will search for Players only. Errors will be echoed, and None returned if no Player is found. searchdata - search criterion, the Player's key or dbref to search for @@ -224,7 +387,14 @@ class DefaultPlayer(PlayerDB): # handle wrapping of common terms if searchdata.lower() in ("me", "*me", "self", "*self",): return self - return super(DefaultPlayer, self).search(searchdata, return_puppet=return_puppet, **kwargs) + matches = self.__class__.objects.player_search(searchdata) + matches = _AT_SEARCH_RESULT(self, searchdata, matches, global_search=True) + if matches and return_puppet: + try: + return matches.puppet + except AttributeError: + return None + return matches def is_typeclass(self, typeclass, exact=False): """ @@ -332,6 +502,7 @@ class DefaultPlayer(PlayerDB): lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins)" self.attributes.add("_playable_characters", [], lockstring=lockstring) + # TODO - handle this in __init__ instead. def at_init(self): """ This is always called whenever this object is initiated -- @@ -344,12 +515,35 @@ class DefaultPlayer(PlayerDB): """ pass + # Note that the hooks below also exist in the character object's # typeclass. You can often ignore these and rely on the character # ones instead, unless you are implementing a multi-character game # and have some things that should be done regardless of which # character is currently connected to this player. + def at_first_save(self): + """ + This is a generic hook called by Evennia when this object is + saved to the database the very first time. You generally + don't override this method but the hooks called by it. + """ + self.basetype_setup() + self.at_player_creation() + + permissions = settings.PERMISSION_PLAYER_DEFAULT + if hasattr(self, "_createdict"): + # this will only be set if the utils.create_player + # function was used to create the object. + cdict = self._createdict + if "locks" in cdict: + self.locks.add(cdict["locks"]) + if "permissions" in cdict: + permissions = cdict["permissions"] + del self._createdict + + self.permissions.add(permissions) + def at_access(self, result, accessing_obj, access_type, **kwargs): """ This is called with the result of an access call, along with @@ -373,8 +567,7 @@ class DefaultPlayer(PlayerDB): def at_first_login(self): """ - Only called once, the very first - time the user logs in. + Called the very first time this player logs into the game. """ pass @@ -472,6 +665,7 @@ class DefaultPlayer(PlayerDB): """ pass + class Guest(DefaultPlayer): """ This class is used for guest logins. Unlike Players, Guests and their diff --git a/src/scripts/models.py b/src/scripts/models.py index 99ec793f4b..1d73aa8eb9 100644 --- a/src/scripts/models.py +++ b/src/scripts/models.py @@ -158,23 +158,23 @@ class ScriptDB(TypedObject): object = property(__get_obj, __set_obj) - def at_typeclass_error(self): - """ - If this is called, it means the typeclass has a critical - error and cannot even be loaded. We don't allow a script - to be created under those circumstances. Already created, - permanent scripts are set to already be active so they - won't get activated now (next reboot the bug might be fixed) - """ - # By setting is_active=True, we trick the script not to run "again". - self.is_active = True - return super(ScriptDB, self).at_typeclass_error() - - delete_iter = 0 - def delete(self): - "Delete script" - if self.delete_iter > 0: - return - self.delete_iter += 1 - _GA(self, "attributes").clear() - super(ScriptDB, self).delete() +# def at_typeclass_error(self): +# """ +# If this is called, it means the typeclass has a critical +# error and cannot even be loaded. We don't allow a script +# to be created under those circumstances. Already created, +# permanent scripts are set to already be active so they +# won't get activated now (next reboot the bug might be fixed) +# """ +# # By setting is_active=True, we trick the script not to run "again". +# self.is_active = True +# return super(ScriptDB, self).at_typeclass_error() +# +# delete_iter = 0 +# def delete(self): +# "Delete script" +# if self.delete_iter > 0: +# return +# self.delete_iter += 1 +# _GA(self, "attributes").clear() +# super(ScriptDB, self).delete() diff --git a/src/scripts/scripts.py b/src/scripts/scripts.py index 5cf0e81cd9..cb4e5eefd9 100644 --- a/src/scripts/scripts.py +++ b/src/scripts/scripts.py @@ -351,109 +351,146 @@ class ScriptBase(ScriptDB): class Script(ScriptBase): """ - This is the class you should inherit from, it implements - the hooks called by the script machinery. + This is the base TypeClass for all Scripts. Scripts describe events, + timers and states in game, they can have a time component or describe + a state that changes under certain conditions. + + Script API: + + * Available properties (only available on initiated Typeclass objects) + + key (string) - name of object + name (string)- same as key + aliases (list of strings) - aliases to the object. Will be saved to + database as AliasDB entries but returned as strings. + dbref (int, read-only) - unique #id-number. Also "id" can be used. + dbobj (Object, read-only) - link to database model. dbobj.typeclass + points back to this class + typeclass (Object, read-only) - this links back to this class as an + identified only. Use self.swap_typeclass() to switch. + date_created (string) - time stamp of object creation + permissions (list of strings) - list of permission strings + + desc (string) - optional description of script, shown in listings + obj (Object) - optional object that this script is connected to + and acts on (set automatically + by obj.scripts.add()) + interval (int) - how often script should run, in seconds. + <=0 turns off ticker + start_delay (bool) - if the script should start repeating right + away or wait self.interval seconds + repeats (int) - how many times the script should repeat before + stopping. <=0 means infinite repeats + persistent (bool) - if script should survive a server shutdown or not + is_active (bool) - if script is currently running + + * Handlers + + locks - lock-handler: use locks.add() to add new lock strings + db - attribute-handler: store/retrieve database attributes on this + self.db.myattr=val, val=self.db.myattr + ndb - non-persistent attribute handler: same as db but does not + create a database entry when storing data + + * Helper methods + + start() - start script (this usually happens automatically at creation + and obj.script.add() etc) + stop() - stop script, and delete it + pause() - put the script on hold, until unpause() is called. If script + is persistent, the pause state will survive a shutdown. + unpause() - restart a previously paused script. The script will + continue as if it was never paused. + force_repeat() - force-step the script, regardless of how much remains + until next step. This counts like a normal firing in all ways. + time_until_next_repeat() - if a timed script (interval>0), returns + time until next tick + remaining_repeats() - number of repeats remaining, if limited + + * Hook methods + + at_script_creation() - called only once, when an object of this + class is first created. + is_valid() - is called to check if the script is valid to be running + at the current time. If is_valid() returns False, the + running script is stopped and removed from the game. You + can use this to check state changes (i.e. an script + tracking some combat stats at regular intervals is only + valid to run while there is actual combat going on). + at_start() - Called every time the script is started, which for + persistent scripts is at least once every server start. + Note that this is unaffected by self.delay_start, which + only delays the first call to at_repeat(). It will also + be called after a pause, to allow for setting up the script. + at_repeat() - Called every self.interval seconds. It will be called + immediately upon launch unless self.delay_start is True, + which will delay the first call of this method by + self.interval seconds. If self.interval<=0, this method + will never be called. + at_stop() - Called as the script object is stopped and is about to + be removed from the game, e.g. because is_valid() + returned False or self.stop() was called manually. + at_server_reload() - Called when server reloads. Can be used to save + temporary variables you want should survive a reload. + at_server_shutdown() - called at a full server shutdown. + """ - - def __init__(self, *args, **kwargs): + def at_first_save(self): """ - This is the base TypeClass for all Scripts. Scripts describe events, - timers and states in game, they can have a time component or describe - a state that changes under certain conditions. + This is called after very first time this object is saved. + Generally, you don't need to overload this, but only the hooks + called by this method. + """ + self.at_script_creation(self) - Script API: + if hasattr(self, "_createdict"): + # this will only be set if the utils.create_script + # function was used to create the object. We want + # the create call's kwargs to override the values + # set by hooks. + cdict = self._createdict + updates = [] + if not cdict["key"]: + self.db_key = "#%i" % self.dbid + updates.append("db_key") + elif self.key != cdict["db_key"]: + self.db_key = cdict["key"] + updates.append("db_key") + if cdict["interval"] and self.interval != cdict["interval"]: + self.db_interval = cdict["interval"] + updates.append("db_interval") + if cdict["start_delay"] and self.start_delay != cdict["start_delay"]: + self.db_start_delay = cdict["start_delay"] + updates.append("db_start_delay") + if cdict["repeats"] and self.repeats != cdict["repeats"]: + self.db_repeats = cdict["repeats"] + updates.append("db_repeats") + if cdict["persistent"] and self.persistent != cdict["persistent"]: + self.db_persistent = cdict["persistent"] + updates.append("db_persistent") + if updates: + self.save(update_fields=updates) - * Available properties (only available on initiated Typeclass objects) + if cdict["permissions"]: + self.permissions.add(cdict["permissions"]) + if cdict["locks"]: + self.locks.add(cdict["locks"]) + if cdict["aliases"]: + self.aliases.add(cdict["aliases"]) - key (string) - name of object - name (string)- same as key - aliases (list of strings) - aliases to the object. Will be saved to - database as AliasDB entries but returned as strings. - dbref (int, read-only) - unique #id-number. Also "id" can be used. - dbobj (Object, read-only) - link to database model. dbobj.typeclass - points back to this class - typeclass (Object, read-only) - this links back to this class as an - identified only. Use self.swap_typeclass() to switch. - date_created (string) - time stamp of object creation - permissions (list of strings) - list of permission strings + if not cdict["autostart"]: + # don't auto-start the script + return - desc (string) - optional description of script, shown in listings - obj (Object) - optional object that this script is connected to - and acts on (set automatically - by obj.scripts.add()) - interval (int) - how often script should run, in seconds. - <=0 turns off ticker - start_delay (bool) - if the script should start repeating right - away or wait self.interval seconds - repeats (int) - how many times the script should repeat before - stopping. <=0 means infinite repeats - persistent (bool) - if script should survive a server shutdown or not - is_active (bool) - if script is currently running + # auto-start script (default) + self.start() - * Handlers - - locks - lock-handler: use locks.add() to add new lock strings - db - attribute-handler: store/retrieve database attributes on this - self.db.myattr=val, val=self.db.myattr - ndb - non-persistent attribute handler: same as db but does not - create a database entry when storing data - - * Helper methods - - start() - start script (this usually happens automatically at creation - and obj.script.add() etc) - stop() - stop script, and delete it - pause() - put the script on hold, until unpause() is called. If script - is persistent, the pause state will survive a shutdown. - unpause() - restart a previously paused script. The script will - continue as if it was never paused. - force_repeat() - force-step the script, regardless of how much remains - until next step. This counts like a normal firing in all ways. - time_until_next_repeat() - if a timed script (interval>0), returns - time until next tick - remaining_repeats() - number of repeats remaining, if limited - - * Hook methods - - at_script_creation() - called only once, when an object of this - class is first created. - is_valid() - is called to check if the script is valid to be running - at the current time. If is_valid() returns False, the - running script is stopped and removed from the game. You - can use this to check state changes (i.e. an script - tracking some combat stats at regular intervals is only - valid to run while there is actual combat going on). - at_start() - Called every time the script is started, which for - persistent scripts is at least once every server start. - Note that this is unaffected by self.delay_start, which - only delays the first call to at_repeat(). It will also - be called after a pause, to allow for setting up the script. - at_repeat() - Called every self.interval seconds. It will be called - immediately upon launch unless self.delay_start is True, - which will delay the first call of this method by - self.interval seconds. If self.interval<=0, this method - will never be called. - at_stop() - Called as the script object is stopped and is about to - be removed from the game, e.g. because is_valid() - returned False or self.stop() was called manually. - at_server_reload() - Called when server reloads. Can be used to save - temporary variables you want should survive a reload. - at_server_shutdown() - called at a full server shutdown. - - - """ - super(Script, self).__init__(*args, **kwargs) def at_script_creation(self): """ Only called once, by the create function. """ - self.key = "" - self.desc = "" - self.interval = 0 # infinite - self.start_delay = False - self.repeats = 0 # infinite - self.persistent = False + pass def is_valid(self): """ diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 588cc962a0..874e3dab8c 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -74,10 +74,10 @@ _SA = object.__setattr__ # signal receivers. Assigned in __new__ def post_save(sender, instance, created, **kwargs): """ - Is called Receive a signal just after the object is saved. + Receives a signal just after the object is saved. """ if created: - instance.at_instance_creation() + instance.at_first_save() #TODO - put OOB handler here? @@ -363,7 +363,8 @@ class TypedObject(SharedMemoryModel): self.nattributes.clear() if run_start_hooks: - self.at_instance_creation() + # fake this call to mimic the first save + self.at_first_save() # # Lock / permission methods diff --git a/src/utils/create.py b/src/utils/create.py index 4ab9492eae..92a428b116 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -48,33 +48,6 @@ __all__ = ("create_object", "create_script", "create_help_entry", _GA = object.__getattribute__ -# Helper function - -def handle_dbref(inp, objclass, raise_errors=True): - """ - Convert a #dbid to a valid object of objclass. objclass - should be a valid object class to filter against (objclass.filter ...) - If not raise_errors is set, this will swallow errors of non-existing - objects. - """ - if not (isinstance(inp, basestring) and inp.startswith("#")): - return inp - # a string, analyze it - inp = inp.lstrip('#') - try: - if int(inp) < 0: - return None - except ValueError: - return None - - # if we get to this point, inp is an integer dbref; get the matching object - try: - return objclass.objects.get(id=inp) - except Exception: - if raise_errors: - raise - return inp - # # Game Object creation # @@ -128,7 +101,7 @@ def create_object(typeclass=None, key=None, location=None, "locks":locks, "aliases":aliases, "destination":destination, "report_to":report_to, "nohome":nohome} # this will trigger the save signal which in turn calls the - # at_instance_creation hook on the typeclass, where the _createdict can be + # at_first_save hook on the typeclass, where the _createdict can be # used. new_object.save() return new_object @@ -168,83 +141,33 @@ def create_script(typeclass, key=None, obj=None, player=None, locks=None, error will be raised. If set, this method will return None upon errors. """ - global _Script, _ScriptDB - if not _Script: - from src.scripts.scripts import Script as _Script - if not _ScriptDB: - from src.scripts.models import ScriptDB as _ScriptDB + typeclass = typeclass if typeclass else settings.BASE_SCRIPT_TYPECLASS - if not typeclass: - typeclass = settings.BASE_SCRIPT_TYPECLASS - elif isinstance(typeclass, _ScriptDB): - # this is already an scriptdb instance, extract its typeclass - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Script) or utils.inherits_from(typeclass, _Script): - # this is already an object typeclass, extract its path - typeclass = typeclass.path + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass + typeclass = class_from_module(typeclass, settings.SCRIPT_TYPECLASS_PATHS) - # create new database script - new_db_script = _ScriptDB() + # validate input + player = dbid_to_obj(player) + obj = dbid_to_obj(obj) - # assign the typeclass - typeclass = utils.to_unicode(typeclass) - new_db_script.typeclass_path = typeclass - - # the name/key is often set later in the typeclass. This - # is set here as a failsafe. - if key: - new_db_script.key = key - else: - new_db_script.key = "#%i" % new_db_script.id - - # this will either load the typeclass or the default one - new_script = new_db_script.typeclass - - if not _GA(new_db_script, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input and it still - # gave us a default - SharedMemoryModel.delete(new_db_script) - if report_to: - _GA(report_to, "msg")("Error creating %s (%s): %s" % (new_db_script.key, typeclass, - _GA(new_db_script, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_script, "typeclass_last_errmsg")) - - if obj: - new_script.obj = obj - if player: - new_script.player = player - - # call the hook method. This is where all at_creation - # customization happens as the typeclass stores custom - # things on its database object. - new_script.at_script_creation() - - # custom-given variables override the hook - if key: - new_script.key = key - if locks: - new_script.locks.add(locks) - if interval is not None: - new_script.interval = interval - if start_delay is not None: - new_script.start_delay = start_delay - if repeats is not None: - new_script.repeats = repeats - if persistent is not None: - new_script.persistent = persistent - - # must do this before starting the script since some - # scripts may otherwise run for a very short time and - # try to delete itself before we have a time to save it. - new_db_script.save() - - # a new created script should usually be started. - if autostart: - new_script.start() + # create new instance + new_script = typeclass(db_key=key, db_obj=obj, db_player=player, + db_interval=interval, db_start_delay=start_delay, + db_repeats=repeats, db_peristent=persistent) + # store the call signature for the signal + new_script._createdict = {"key":key, "obj":obj, "player":player, + "locks":locks, "interval":interval, + "start_delay":start_delay, "repeats":repeats, + "persistent":persistent, "autostart":autostart, + "report_to":report_to} + # this will trigger the save signal which in turn calls the + # at_first_save hook on the tyepclass, where the _createdict + # can be used. + new_script.save() return new_script + #alias script = create_script @@ -413,11 +336,16 @@ def create_player(key, email, password, operations and is thus not suitable for play-testing the game. """ - global _PlayerDB, _Player - if not _PlayerDB: - from src.players.models import PlayerDB as _PlayerDB - if not _Player: - from src.players.player import Player as _Player + typeclass = typeclass if typeclass else settings.BASE_PLAYER_TYPECLASS + + if isinstance(typeclass, basestring): + # a path is given. Load the actual typeclass. + typeclass = class_from_module(typeclass, settings.OBJECT_TYPECLASS_PATHS) + typeclass_path = typeclass.path + + # setup input for the create command. We use PlayerDB as baseclass + # here to give us maximum freedom (the typeclasses will load + # correctly when each object is recovered). if not email: email = "dummy@dummy.com" @@ -425,69 +353,23 @@ def create_player(key, email, password, raise ValueError("A Player with the name '%s' already exists." % key) # this handles a given dbref-relocate to a player. - report_to = handle_dbref(report_to, _PlayerDB) + report_to = dbid_to_obj(report_to, _PlayerDB) - try: + # create the correct player object + if is_superuser: + new_player = _PlayerDB.objects.create_superuser(key, email, password) + else: + new_player = _PlayerDB.objects.create_user(key, email, password) + new_player.db_typeclass_path = typeclass_path + # store the call signature for the signal + new_player._createdict = {"locks":locks, "permissions":permissions, + "report_to":report_to} - # create the correct Player object - if is_superuser: - new_db_player = _PlayerDB.objects.create_superuser(key, email, password) - else: - new_db_player = _PlayerDB.objects.create_user(key, email, password) - - if not typeclass: - typeclass = settings.BASE_PLAYER_TYPECLASS - elif isinstance(typeclass, _PlayerDB): - # this is an PlayerDB instance, extract its typeclass path - typeclass = typeclass.typeclass.path - elif isinstance(typeclass, _Player) or utils.inherits_from(typeclass, _Player): - # this is Player object typeclass, extract its path - typeclass = typeclass.path - - # assign the typeclass - typeclass = utils.to_unicode(typeclass) - new_db_player.typeclass_path = typeclass - - # this will either load the typeclass or the default one - new_player = new_db_player.typeclass - - if not _GA(new_db_player, "is_typeclass")(typeclass, exact=True): - # this will fail if we gave a typeclass as input - # and it still gave us a default - SharedMemoryModel.delete(new_db_player) - if report_to: - _GA(report_to, "msg")("Error creating %s (%s):\n%s" % (new_db_player.key, typeclass, - _GA(new_db_player, "typeclass_last_errmsg"))) - return None - else: - raise Exception(_GA(new_db_player, "typeclass_last_errmsg")) - - new_player.basetype_setup() # setup the basic locks and cmdset - # call hook method (may override default permissions) - new_player.at_player_creation() - - # custom given arguments potentially overrides the hook - if permissions: - new_player.permissions.add(permissions) - elif not new_player.permissions: - new_player.permissions.add(settings.PERMISSION_PLAYER_DEFAULT) - if locks: - new_player.locks.add(locks) - return new_player - - except Exception: - # a failure in creating the player; we try to clean - # up as much as we can - logger.log_trace() - try: - new_player.delete() - except Exception: - pass - try: - del new_player - except Exception: - pass - raise + # saving will trigger the signal that calls the + # at_first_save hook on the typeclass, where the _createdict + # can be used. + new_player.save() + return new_player # alias player = create_player diff --git a/src/utils/spawner.py b/src/utils/spawner.py index b4bbdc3113..cddaab2743 100644 --- a/src/utils/spawner.py +++ b/src/utils/spawner.py @@ -76,12 +76,11 @@ os.environ['DJANGO_SETTINGS_MODULE'] = 'game.settings' from django.conf import settings from random import randint from src.objects.models import ObjectDB -from src.utils.create import handle_dbref -from src.utils.utils import make_iter, all_from_module +from src.utils.utils import make_iter, all_from_module, dbid_to_obj _CREATE_OBJECT_KWARGS = ("key", "location", "home", "destination") -_handle_dbref = lambda inp: handle_dbref(inp, ObjectDB) +_handle_dbref = lambda inp: dbid_to_obj(inp, ObjectDB) def _validate_prototype(key, prototype, protparents, visited):