From 261454ff0a165924ac6cc4935f1c400223a20080 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 26 Jan 2013 21:20:31 +0100 Subject: [PATCH 1/9] First steps towards a full account system (multiple Character with one Player); added the Player-level methods. --- src/players/models.py | 93 ++++++++++++++++++++++++++++++++---- src/server/sessionhandler.py | 7 ++- 2 files changed, 90 insertions(+), 10 deletions(-) diff --git a/src/players/models.py b/src/players/models.py index 4306a8476..53f7769aa 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -163,6 +163,7 @@ class PlayerDB(TypedObject): # Use the property 'obj' to access. db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True, verbose_name="character", help_text='In-game object.') + db_objs = models.ManyToManyField("objects.ObjectDB", null=True, verbose_name="characters", help_text="In-game objects.") # store a connected flag here too, not just in sessionhandler. # This makes it easier to track from various out-of-process locations db_is_connected = models.BooleanField(default=False, verbose_name="is_connected", help_text="If player is connected to game or not") @@ -224,7 +225,7 @@ class PlayerDB(TypedObject): #@property def character_get(self): "Getter. Allows for value = self.character" - return get_field_cache(self, "obj") + return get_field_cache(self, "obj) #@character.setter def character_set(self, character): "Setter. Allows for self.character = value" @@ -236,6 +237,7 @@ class PlayerDB(TypedObject): "Deleter. Allows for del self.character" del_field_cache(self, "obj") character = property(character_get, character_set, character_del) + # cmdset_storage property # This seems very sensitive to caching, so leaving it be for now /Griatch #@property @@ -358,26 +360,101 @@ class PlayerDB(TypedObject): # PlayerDB class access methods # - def msg(self, outgoing_string, from_obj=None, data=None): + def msg(self, outgoing_string, from_obj=None, data=None, sessid=None): """ Evennia -> User This is the main route for sending data back to the user from the server. """ if from_obj: + # call hook try: _GA(from_obj, "at_msg_send")(outgoing_string, to_obj=self, data=data) except Exception: pass - if (_GA(self, "character") and not - _GA(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 _GA(self, 'sessions'): + session = None + if sessid: + session = _GA(self, "get_session")(sessid) + if session: + char = _GA(self, "get_character")(sessid) + if char and not char.at_msg_receive(outgoing_string, from_obj=from_obj, data=data)): + # if hook returns false, cancel send + return session.msg(outgoing_string, data) + else: + # if no session was specified, send to them all + for sess in _GA(self, 'get_sessions'): + sess.msg(outgoing_string, data) + def inmsg(self, ingoing_string, data, session): + """ + This is the reverse of msg - used by sessions to relay + messages/data back into the game. It is normally not called + from inside game code but only by the serversessions directly. + + ingoing_string - text string (i.e. command string) + data - dictionary of optional data + sessid - session sending this data + """ + character = _GA(self, "get_character")(session.sessid) + if character: + # execute command on character + character.execute_cmd(ingoing_string) + else: + # a non-character session; this goes to player directly + self.execute_cmd(ingoing_message) + + + def get_sessions(self, sessid=None): + """ + Return session with given sessid connected to this player. If sessid is + not given, return all connected sessions. + Note that this method will always return a list, even if it only has one + (or zero) element(s). + """ + return SESSIONS.get_session_from_player(self, sessid=sessid) + + def get_character(self, sessid): + """ + Get the character connected through this sessid, if any + """ + if not sessid: # sessid is always > 0 + return None + try: + char = get_prop_cache(self, "_characters").get(sessid) + except AttributeError: + set_prop_cache(self, "_characters", {}) + char = None + if not char: + char = self.db_objs.filter(player=self, sessid=sessid) + if char.count(): + chars_cache = get_prop_cache(self, "_characters") + chars_cache[sessid] = char[0] + set_prop_cache(self, "_characters") + return char + + def connect_character(self, character, sessid): + """ + Use the Player to connect a Character to a session. Note + that we don't do any access checks at this point. + """ + character = character.dbobj + character.player = self + character.sessid = sessid + self.db_objs.add(character) + self.save() + + def disconnect_character(self, character): + """ + Disconnect a character from this player. + """ + character = character.dbobj + if self.db_objs.filter(id=_GA(character,"id")): + self.db_objs.remove(character) + character.player = None + character.sessid = None + self.save() def swap_character(self, new_character, delete_old_character=False): """ diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 46a5e58ee..e17134309 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -228,12 +228,15 @@ class ServerSessionHandler(SessionHandler): """ return len(set(session.uid for session in self.sessions.values() if session.logged_in)) - def sessions_from_player(self, player): + def sessions_from_player(self, player, sessid=None): """ Given a player, return any matching sessions. """ uid = player.uid - return [session for session in self.sessions.values() if session.logged_in and session.uid == uid] + if sessid: + return [session for session in self.sessions.values() if session.logged_in and session.sessid == sessid and session.uid == uid] + else + return [session for session in self.sessions.values() if session.logged_in and session.uid == uid] def sessions_from_character(self, character): """ From 8b5938ddd0f433c27bab05458fe198f91931c16f Mon Sep 17 00:00:00 2001 From: Griatch Date: Fri, 1 Feb 2013 22:03:55 +0100 Subject: [PATCH 2/9] Non-working intermediary commit. --- src/objects/models.py | 28 +++++++++++++++++++++++++++- src/players/models.py | 24 +++++++++++++++++------- 2 files changed, 44 insertions(+), 8 deletions(-) diff --git a/src/objects/models.py b/src/objects/models.py index c3c3c6d24..da41f715d 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -176,6 +176,9 @@ class ObjectDB(TypedObject): # If this is a character object, the player is connected here. db_player = models.ForeignKey("players.PlayerDB", blank=True, null=True, verbose_name='player', help_text='a Player connected to this object, if any.') + # the session id associated with this player, if any + db_sessid = models.IntegerField(null=True, verbose_name="session id", + help_text="unique session id of connected Player, if any." # The location in the game world. Since this one is likely # to change often, we set this with the 'location' property # to transparently handle Typeclassing. @@ -191,7 +194,6 @@ class ObjectDB(TypedObject): # database storage of persistant cmdsets. db_cmdset_storage = models.CharField('cmdset', max_length=255, null=True, blank=True, help_text="optional python path to a cmdset class.") - # Database manager objects = ObjectManager() @@ -260,6 +262,30 @@ class ObjectDB(TypedObject): del_field_cache(self, "player") player = property(__player_get, __player_set, __player_del) + # sessid property (wraps db_sessid) + #@property + def __sessid_get(self): + """ + Getter. Allows for value = self.sessid. Since sessid + is directly related to self.player, we cannot have + a sessid without a player being connected (but the + opposite could be true). + """ + if not get_field_cache(self, "player"): + del_field_cache(self, "sessid") + return get_field_cache(self, "sessid") + + #@player.setter + def __sessid_set(self, player): + "Setter. Allows for self.player = value" + if inherits_from(player, TypeClass): + player = player.dbobj + set_field_cache(self, "player", player) + #@player.deleter + def __player_del(self): + "Deleter. Allows for del self.player" + del_field_cache(self, "player") + player = property(__player_get, __player_set, __player_del) # location property (wraps db_location) #@property def __location_get(self): diff --git a/src/players/models.py b/src/players/models.py index 53f7769aa..59ac2c4aa 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -387,7 +387,7 @@ class PlayerDB(TypedObject): for sess in _GA(self, 'get_sessions'): sess.msg(outgoing_string, data) - def inmsg(self, ingoing_string, data, session): + def inmsg(self, ingoing_string, data, sessid): """ This is the reverse of msg - used by sessions to relay messages/data back into the game. It is normally not called @@ -395,16 +395,21 @@ class PlayerDB(TypedObject): ingoing_string - text string (i.e. command string) data - dictionary of optional data - sessid - session sending this data + session - session sending this data """ - character = _GA(self, "get_character")(session.sessid) + character = _GA(self, "get_character")(sessid) if character: # execute command on character - character.execute_cmd(ingoing_string) + _GA(character, "execute_cmd")(ingoing_string) else: # a non-character session; this goes to player directly - self.execute_cmd(ingoing_message) + _GA(self, "execute_cmd")(ingoing_string) + def connect_session(self, sessid): + """ + Connect session to this player to a session through + its session id. + """ def get_sessions(self, sessid=None): """ @@ -436,8 +441,10 @@ class PlayerDB(TypedObject): def connect_character(self, character, sessid): """ - Use the Player to connect a Character to a session. Note - that we don't do any access checks at this point. + Use the Player to connect a Character to a session. Note that + we don't do any access checks at this point. Note that if the + game was fully restarted (including the Portal), this must be + used, since sessids will have changed as players reconnect. """ character = character.dbobj character.player = self @@ -456,6 +463,9 @@ class PlayerDB(TypedObject): character.sessid = None self.save() + def disconnect_all_characters(self): + for char in self.db_objs.all(): + def swap_character(self, new_character, delete_old_character=False): """ Swaps character, if possible From 231af4a35108c76e1f543e313ef8222e556d68ae Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 2 Feb 2013 15:55:42 +0100 Subject: [PATCH 3/9] Continued work on multi-char-per-account. Added a new default login point. Still need to add sessid to commands. --- src/commands/default/general.py | 66 +++++++++++++++++++++++----- src/objects/models.py | 5 +-- src/players/models.py | 78 ++++++++++++++++++++++----------- src/server/sessionhandler.py | 9 ++-- src/settings_default.py | 12 ++--- 5 files changed, 120 insertions(+), 50 deletions(-) diff --git a/src/commands/default/general.py b/src/commands/default/general.py index cd4d7a2f5..79b8122d1 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -649,7 +649,12 @@ class CmdAccess(MuxCommand): string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms) caller.msg(string) + + + +#------------------------------------------------------------ # OOC commands +#------------------------------------------------------------ class CmdOOCLook(MuxCommandOOC, CmdLook): """ @@ -658,28 +663,67 @@ class CmdOOCLook(MuxCommandOOC, CmdLook): 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. - + Look in the ooc state. """ + #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_category = "General" + def look_target(self): + "Hook method for when an argument is given." + # caller is assumed to be a player object here. + caller = self.caller + looktarget = caller.get_character(key=self.args) + if looktarget: + caller.msg(looktarget.return_appearance()) + else: + caller.msg("No such character.") + return + + def no_look_target(self): + "Hook method for default look without a specified target" + # caller is always a player at this point. + player = self.caller + sessid = self.sessid + # get all our characters + characters = player.get_all_characters() # get all characters + string = "You are logged in as {g%s{n." % player.key + string += " Use {w@ic {n to enter the game." + string += "\n\nAvailable character%s:" % (characters.count() > 1 and "s" or "") + for char in characters: + csessid = char.sessid + if csessid: + # character is already puppeted + if csessid == sessid: + # this should not happen. + string += "\n - {r%s{n (sessid not properly cleared! Contact an admin!)" % char.key + elif player.get_session(csessid): + string += "\n - {G%s{n (played by you in another session)" + else: + string += "\n - {R%s{n (played by someone else)" % char.key + else: + # character is "free to puppet" + string += "\n - %s" % char.key + player.msg(string) + def func(self): "implement the ooc look command" - 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. + if utils.inherits_from(self.caller, "src.objects.objects.Object"): + # An object of some type is calling. Use default look instead. super(CmdOOCLook, self).func() + elif self.args: + self.look_target() + else: + self.no_look_target() + class CmdIC(MuxCommandOOC): """ diff --git a/src/objects/models.py b/src/objects/models.py index f0dd76a16..97b0eb74e 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -274,14 +274,13 @@ class ObjectDB(TypedObject): if not get_field_cache(self, "player"): del_field_cache(self, "sessid") return get_field_cache(self, "sessid") - - #@player.setter + #@sessid.setter def __sessid_set(self, player): "Setter. Allows for self.player = value" if inherits_from(player, TypeClass): player = player.dbobj set_field_cache(self, "player", player) - #@player.deleter + #@sessid.deleter def __player_del(self): "Deleter. Allows for del self.player" del_field_cache(self, "player") diff --git a/src/players/models.py b/src/players/models.py index 59ac2c4aa..66d03e333 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -225,7 +225,7 @@ class PlayerDB(TypedObject): #@property def character_get(self): "Getter. Allows for value = self.character" - return get_field_cache(self, "obj) + return get_field_cache(self, "obj") #@character.setter def character_set(self, character): "Setter. Allows for self.character = value" @@ -378,7 +378,7 @@ class PlayerDB(TypedObject): session = _GA(self, "get_session")(sessid) if session: char = _GA(self, "get_character")(sessid) - if char and not char.at_msg_receive(outgoing_string, from_obj=from_obj, data=data)): + if char and not char.at_msg_receive(outgoing_string, from_obj=from_obj, data=data): # if hook returns false, cancel send return session.msg(outgoing_string, data) @@ -411,7 +411,7 @@ class PlayerDB(TypedObject): its session id. """ - def get_sessions(self, sessid=None): + def get_session(self, sessid=None): """ Return session with given sessid connected to this player. If sessid is not given, return all connected sessions. @@ -420,24 +420,36 @@ class PlayerDB(TypedObject): """ return SESSIONS.get_session_from_player(self, sessid=sessid) - def get_character(self, sessid): + def get_character(self, sessid=None, key=None): """ - Get the character connected through this sessid, if any + Get the character connected through this sessid, if any. May also + try to return a character based on key. If neither sessid nor key + is given, return all characters connected to this player """ - if not sessid: # sessid is always > 0 - return None - try: - char = get_prop_cache(self, "_characters").get(sessid) - except AttributeError: - set_prop_cache(self, "_characters", {}) - char = None - if not char: - char = self.db_objs.filter(player=self, sessid=sessid) - if char.count(): - chars_cache = get_prop_cache(self, "_characters") - chars_cache[sessid] = char[0] - set_prop_cache(self, "_characters") - return char + cache = get_prop_cache(self, "_characters") or {} + if sessid: + # try to return a character with a given sessid + char = cache.get(sessid) + if not char: + char = self.db_objs.filter(player=self, sessid=sessid) or None + if char: + cache[sessid] = char[0] + set_prop_cache(self, "_characters", cache) + if key: + return char.key.lower() == key.lower() and char or None + return char + elif key: + char = self.db_objs.filter(player=self, db_key__iexact=key) + return char and char[0] or None + else: + # no sessid given - return all available characters + return list(self.db_objs.filter(player=self, sessid=sessid)) + + def get_all_characters(self): + """ + Readability-wrapper for getting all characters + """ + return self.get_character(sessid=None) def connect_character(self, character, sessid): """ @@ -446,25 +458,39 @@ class PlayerDB(TypedObject): game was fully restarted (including the Portal), this must be used, since sessids will have changed as players reconnect. """ + # first disconnect any other character from this session + self.disconnect_character(sessid=sessid) character = character.dbobj character.player = self character.sessid = sessid self.db_objs.add(character) self.save() + # update cache + cache = get_prop_cache(self, "_characters") or {} + cache[sessid] = character + set_prop_cache(self, "_characters", cache) - def disconnect_character(self, character): + def disconnect_character(self, sessid=None, char=None): """ - Disconnect a character from this player. + Disconnect a character from this player, either based + on sessid or by giving the character object directly """ - character = character.dbobj - if self.db_objs.filter(id=_GA(character,"id")): - self.db_objs.remove(character) - character.player = None - character.sessid = None + key = char and char.key or None + char = self.get_character(sessid=sessid, key=key) + if char: + self.db_objs.remove(char) + del char.player + del char.sessid self.save() + # clear cache + cache = get_prop_cache(self, "_characters") or {} + if cache and sessid in cache: + del cache[sessid] + set_prop_cache(self, "_characters", cache) def disconnect_all_characters(self): for char in self.db_objs.all(): + self.disconnect_character(char) def swap_character(self, new_character, delete_old_character=False): """ diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index e17134309..dcd1d6c49 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -32,7 +32,8 @@ SSYNC = chr(8) # server session sync from django.utils.translation import ugettext as _ SERVERNAME = settings.SERVERNAME -ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION +#ALLOW_MULTISESSION = settings.ALLOW_MULTISESSION +MULTISESSION_MODE = settings.MULTISESSION_MODE IDLE_TIMEOUT = settings.IDLE_TIMEOUT #----------------------------------------------------------- @@ -163,8 +164,8 @@ class ServerSessionHandler(SessionHandler): """ # prep the session with player/user info - if not ALLOW_MULTISESSION: - # disconnect previous sessions. + if MULTISESSION_MODE == 0: + # disconnect all previous sessions. self.disconnect_duplicate_sessions(session) session.logged_in = True # sync the portal to this session @@ -235,7 +236,7 @@ class ServerSessionHandler(SessionHandler): uid = player.uid if sessid: return [session for session in self.sessions.values() if session.logged_in and session.sessid == sessid and session.uid == uid] - else + else: return [session for session in self.sessions.values() if session.logged_in and session.uid == uid] def sessions_from_character(self, character): diff --git a/src/settings_default.py b/src/settings_default.py index abab55983..8dd69a004 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -59,12 +59,12 @@ SSL_ENABLED = False SSL_PORTS = [4001] # Interface addresses to listen to. If 0.0.0.0, listen to all. SSL_INTERFACES = ['0.0.0.0'] -# If multisessions are allowed, a user can log into the game -# from several different computers/clients at the same time. -# All feedback from the game will be echoed to all sessions. -# If false, only one session is allowed, all other are logged off -# when a new connects. -ALLOW_MULTISESSION = False +# Multisession modes allow a player (=account) to connect to the game simultaneously +# with multiple clients in various ways according to the set mode: +# 0 - no multisession - when a new session is connected, the old one is disconnected +# 1 - multiple sessions, one player, one character, each session getting the same data +# 2 - multiple sessions, one player, each session controlling different characters +MULTISESSION_MODE = 0 # The path that contains this settings.py file (no trailing slash). BASE_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) # Path to the src directory containing the bulk of the codebase's code. From 00584365ae4b1addf41a8aa35948006c074d2622 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 2 Feb 2013 22:41:56 +0100 Subject: [PATCH 4/9] Added sessids to command objects and changed how the "login"-hooks are called. Those will probably have to be changed to better names, at least for characters. --- src/commands/cmdhandler.py | 4 +- src/objects/models.py | 28 ++++---- src/players/models.py | 125 +++++++++++++++++++++++------------ src/server/serversession.py | 39 +++-------- src/server/sessionhandler.py | 13 ++-- 5 files changed, 113 insertions(+), 96 deletions(-) diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index f182f5e5a..05fcf2d7a 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -41,7 +41,6 @@ from twisted.internet.defer import inlineCallbacks, returnValue from django.conf import settings from src.comms.channelhandler import CHANNELHANDLER from src.utils import logger, utils -from src.commands.cmdset import CmdSet from src.commands.cmdparser import at_multimatch_cmd from src.utils.utils import string_suggestions @@ -165,7 +164,7 @@ def get_and_merge_cmdsets(caller): # Main command-handler function @inlineCallbacks -def cmdhandler(caller, raw_string, testing=False): +def cmdhandler(caller, raw_string, testing=False, sessid=None): """ This is the main function to handle any string sent to the engine. @@ -251,6 +250,7 @@ def cmdhandler(caller, raw_string, testing=False): cmd.cmdstring = cmdname cmd.args = args cmd.cmdset = cmdset + cmd.sessid = sessid cmd.raw_string = unformatted_raw_string if hasattr(cmd, 'obj') and hasattr(cmd.obj, 'scripts'): diff --git a/src/objects/models.py b/src/objects/models.py index 97b0eb74e..2b48734b6 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -21,7 +21,7 @@ from django.conf import settings from src.utils.idmapper.models import SharedMemoryModel from src.typeclasses.models import Attribute, TypedObject, TypeNick, TypeNickHandler from src.server.caches import get_field_cache, set_field_cache, del_field_cache -from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache, hashid +from src.server.caches import get_prop_cache, set_prop_cache, del_prop_cache from src.typeclasses.typeclass import TypeClass from src.players.models import PlayerNick from src.objects.manager import ObjectManager @@ -178,7 +178,7 @@ class ObjectDB(TypedObject): help_text='a Player connected to this object, if any.') # the session id associated with this player, if any db_sessid = models.IntegerField(null=True, verbose_name="session id", - help_text="unique session id of connected Player, if any." + help_text="unique session id of connected Player, if any.") # The location in the game world. Since this one is likely # to change often, we set this with the 'location' property # to transparently handle Typeclassing. @@ -271,20 +271,18 @@ class ObjectDB(TypedObject): a sessid without a player being connected (but the opposite could be true). """ - if not get_field_cache(self, "player"): + if not get_field_cache(self, "sessid"): del_field_cache(self, "sessid") return get_field_cache(self, "sessid") #@sessid.setter - def __sessid_set(self, player): + def __sessid_set(self, sessid): "Setter. Allows for self.player = value" - if inherits_from(player, TypeClass): - player = player.dbobj - set_field_cache(self, "player", player) + set_field_cache(self, "sessid", sessid) #@sessid.deleter - def __player_del(self): + def __sessid_del(self): "Deleter. Allows for del self.player" - del_field_cache(self, "player") - player = property(__player_get, __player_set, __player_del) + del_field_cache(self, "sessid") + player = property(__sessid_get, __sessid_set, __sessid_del) # location property (wraps db_location) #@property def __location_get(self): @@ -637,7 +635,7 @@ class ObjectDB(TypedObject): # Execution/action methods # - def execute_cmd(self, raw_string): + def execute_cmd(self, raw_string, sessid=None): """ Do something as this object. This command transparently lets its typeclass execute the command. Evennia also calls @@ -669,11 +667,11 @@ class ObjectDB(TypedObject): if nick.db_nick in raw_list: raw_string = raw_string.replace(nick.db_nick, nick.db_real, 1) break - return cmdhandler.cmdhandler(_GA(self, "typeclass"), raw_string) + return cmdhandler.cmdhandler(_GA(self, "typeclass"), raw_string, sessid=sessid) def msg(self, message, from_obj=None, data=None): """ - Emits something to any sessions attached to the object. + Emits something to a session attached to the object. message (str): The message to send from_obj (obj): object that is sending. @@ -681,8 +679,8 @@ class ObjectDB(TypedObject): be used by the protocol. """ if _GA(self, 'player'): - # note that we check the typeclass' msg, otherwise one couldn't overload it. - _GA(_GA(self, 'player'), "typeclass").msg(message, from_obj=from_obj, data=data) + # note that we must call the player *typeclass'* msg(), otherwise one couldn't overload it. + _GA(_GA(self, 'player'), "typeclass").msg(message, from_obj=from_obj, data=data, sessid=_GA(self, "sessid")) def emit_to(self, message, from_obj=None, data=None): "Deprecated. Alias for msg" diff --git a/src/players/models.py b/src/players/models.py index 66d03e333..a59850da4 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -387,7 +387,7 @@ class PlayerDB(TypedObject): for sess in _GA(self, 'get_sessions'): sess.msg(outgoing_string, data) - def inmsg(self, ingoing_string, data, sessid): + def inmsg(self, ingoing_string, sessid): """ This is the reverse of msg - used by sessions to relay messages/data back into the game. It is normally not called @@ -400,58 +400,101 @@ class PlayerDB(TypedObject): character = _GA(self, "get_character")(sessid) if character: # execute command on character - _GA(character, "execute_cmd")(ingoing_string) + _GA(character, "execute_cmd")(ingoing_string, sessid=sessid) else: # a non-character session; this goes to player directly - _GA(self, "execute_cmd")(ingoing_string) + _GA(self, "execute_cmd")(ingoing_string, sessid=sessid) - def connect_session(self, sessid): - """ - Connect session to this player to a session through - its session id. + def connect_session_to_character(self, sessid, character, force=False): """ + Connect the given session to a character through this player. + Note that this assumes the character has previously been + linked to the player using self.connect_character(). - def get_session(self, sessid=None): + force - drop existing connection to other character """ - Return session with given sessid connected to this player. If sessid is - not given, return all connected sessions. - Note that this method will always return a list, even if it only has one - (or zero) element(s). + # first check if we already have a character tied to this session + char = _GA(self, "get_character")(sessid=sessid) + if char: + if force and char != character: + _GA(self, "disconnect_session_from_character")(sessid) + else: + return + # do the connection + character.sessid = sessid + # update cache + cache = get_prop_cache(self, "_characters") or {} + cache[sessid] = character + set_prop_cache(self, "_characters", cache) + # call hooks + character.at_init() + if character.db.FIRST_LOGIN: + character.at_first_login() + del character.db.FIRST_LOGIN + character.at_pre_login() + character.at_post_login() + + def disconnect_session_from_character(self, sessid): + """ + Disconnect a session from the characterm (still keeping the + connection to the Player) + """ + char = _GA(self, "get_character")(sessid=sessid) + if char: + del char.sessid + # update cache + cache = get_prop_cache(self, "_characters") or {} + if sessid in cache: + del cache[sessid] + set_prop_cache(self, "_characters", cache) + + def get_session(self, sessid): + """ + Return session with given sessid connected to this player. """ return SESSIONS.get_session_from_player(self, sessid=sessid) - def get_character(self, sessid=None, key=None): + def get_all_sessions(self): + "Return all sessions connected to this player" + return SESSIONS.get_session_from_player(self) + + def get_character(self, sessid=None, character=None): """ - Get the character connected through this sessid, if any. May also - try to return a character based on key. If neither sessid nor key - is given, return all characters connected to this player + Get the character connected to this player + + sessid - return character connected to this sessid, + character - return character if connected to this player, else None. + + Combining both keywords will check the entire connection - if the + given session is currently connected to the given char. If no + keywords are given, returns all connected characters. """ cache = get_prop_cache(self, "_characters") or {} if sessid: # try to return a character with a given sessid char = cache.get(sessid) if not char: - char = self.db_objs.filter(player=self, sessid=sessid) or None + char = _GA(self, "db_objs").filter(player=self, sessid=sessid) or None if char: cache[sessid] = char[0] set_prop_cache(self, "_characters", cache) - if key: - return char.key.lower() == key.lower() and char or None + if character: + return char == character.dbobj or None return char - elif key: - char = self.db_objs.filter(player=self, db_key__iexact=key) + elif character: + char = _GA(self, "db_objs").filter(character) return char and char[0] or None else: # no sessid given - return all available characters - return list(self.db_objs.filter(player=self, sessid=sessid)) + return list(self.db_objs.all()) def get_all_characters(self): """ Readability-wrapper for getting all characters """ - return self.get_character(sessid=None) + return _GA(self, "get_character")(sessid=None, character=None) - def connect_character(self, character, sessid): + def connect_character(self, char): """ Use the Player to connect a Character to a session. Note that we don't do any access checks at this point. Note that if the @@ -459,38 +502,34 @@ class PlayerDB(TypedObject): used, since sessids will have changed as players reconnect. """ # first disconnect any other character from this session - self.disconnect_character(sessid=sessid) - character = character.dbobj - character.player = self - character.sessid = sessid - self.db_objs.add(character) - self.save() - # update cache - cache = get_prop_cache(self, "_characters") or {} - cache[sessid] = character - set_prop_cache(self, "_characters", cache) + char = char.dbobj + _GA(self, "disconnect_character")(char) + char = char.dbobj + char.player = self + _GA(self, "db_objs").add(char) + _GA(self, "save")() - def disconnect_character(self, sessid=None, char=None): + def disconnect_character(self, char): """ Disconnect a character from this player, either based on sessid or by giving the character object directly """ + char = char.dbobj key = char and char.key or None - char = self.get_character(sessid=sessid, key=key) + char = _GA(self, "get_character")(key=key) if char: - self.db_objs.remove(char) + _GA(self, "db_objs").remove(char) del char.player del char.sessid self.save() # clear cache cache = get_prop_cache(self, "_characters") or {} - if cache and sessid in cache: - del cache[sessid] - set_prop_cache(self, "_characters", cache) + [cache.pop(sessid) for sessid,stored_char in cache.items() if stored_char==char] + set_prop_cache(self, "_characters", cache) def disconnect_all_characters(self): for char in self.db_objs.all(): - self.disconnect_character(char) + _GA(self, "disconnect_character")(char) def swap_character(self, new_character, delete_old_character=False): """ @@ -514,7 +553,7 @@ class PlayerDB(TypedObject): # Execution/action methods # - def execute_cmd(self, raw_string): + def execute_cmd(self, raw_string, sessid=None): """ Do something as this player. This command transparently lets its typeclass execute the command. @@ -530,7 +569,7 @@ class PlayerDB(TypedObject): if nick.db_nick in raw_list: raw_string = raw_string.replace(nick.db_nick, nick.db_real, 1) break - return cmdhandler.cmdhandler(self.typeclass, raw_string) + return cmdhandler.cmdhandler(self.typeclass, raw_string, sessid=sessid) def search(self, ostring, return_character=False): """ diff --git a/src/server/serversession.py b/src/server/serversession.py index f1303e8d8..46afb6818 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -91,25 +91,14 @@ class ServerSession(Session): self.user.save() # player init - #print "at_init() - player" player.at_init() # Check if this is the first time the *player* logs in if player.db.FIRST_LOGIN: player.at_first_login() del player.db.FIRST_LOGIN - player.at_pre_login() - character = player.character - if character: - # this player has a character. Check if it's the - # first time *this character* logs in - character.at_init() - if character.db.FIRST_LOGIN: - character.at_first_login() - del character.db.FIRST_LOGIN - # run character login hook - character.at_pre_login() + player.at_pre_login() self.log(_('Logged in: %(self)s') % {'self': self}) @@ -119,10 +108,7 @@ class ServerSession(Session): #add session to connected list self.sessionhandler.login(self) - # post-login hooks player.at_post_login() - if character: - character.at_post_login() def session_disconnect(self): """ @@ -193,20 +179,13 @@ class ServerSession(Session): if str(command_string).strip() == IDLE_COMMAND: self.update_session_counters(idle=True) return - - # all other inputs, including empty inputs - character = self.get_character() - if character: - character.execute_cmd(command_string) + if self.logged_in: + # the inmsg handler will relay to the right place + self.player.inmsg(command_string, self.sessid) else: - 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 the session directly - # (it uses the settings.UNLOGGEDIN cmdset) - cmdhandler.cmdhandler(self, command_string) - self.update_session_counters() + # we are not logged in. Use the session directly + # (it uses the settings.UNLOGGEDIN cmdset) + cmdhandler.cmdhandler(self, command_string) def data_out(self, msg, data=None): """ @@ -243,11 +222,11 @@ class ServerSession(Session): func = OOB_FUNC_MODULE.__dict__.get(functuple[0]) if func: try: - outdata[funcname] = func(oobkey, self, *functuple[1], **functuple[2]) + outdata[functuple[0]] = func(oobkey, self, *functuple[1], **functuple[2]) except Exception: logger.log_trace() else: - logger.log_errmsg("oob_data_in error: funcname '%s' not found in OOB_FUNC_MODULE." % funcname) + logger.log_errmsg("oob_data_in error: funcname '%s' not found in OOB_FUNC_MODULE." % functuple[0]) if outdata: # we have a direct result - send it back right away self.oob_data_out(outdata) diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index dcd1d6c49..7b469895c 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -223,8 +223,8 @@ class ServerSessionHandler(SessionHandler): def player_count(self): """ - Get the number of connected players (not sessions since a player - may have more than one session connected if ALLOW_MULTISESSION is True) + Get the number of connected players (not sessions since a + player may have more than one session depending on settings). Only logged-in players are counted here. """ return len(set(session.uid for session in self.sessions.values() if session.logged_in)) @@ -235,7 +235,8 @@ class ServerSessionHandler(SessionHandler): """ uid = player.uid if sessid: - return [session for session in self.sessions.values() if session.logged_in and session.sessid == sessid and session.uid == uid] + session = self.sessions.get(sessid) + return session and session.logged_in and session.uid == uid and session or None else: return [session for session in self.sessions.values() if session.logged_in and session.uid == uid] @@ -243,9 +244,9 @@ class ServerSessionHandler(SessionHandler): """ Given a game character, return any matching sessions. """ - player = character.player - if player: - return self.sessions_from_player(player) + sessid = character.sessid + if sessid: + return self.sessions.get(sessid) return None def announce_all(self, message): From 7ba0683d9d08b89073da36e6ac844c91ddd48b00 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sat, 2 Feb 2013 23:25:46 +0100 Subject: [PATCH 5/9] Added migrations for the new sessid and multiple-character-related fields along with some bugfixes. --- src/objects/migrations/0014_adding_sessid.py | 113 +++++++++++++++++ .../migrations/0012_adding_objs_m2m.py | 116 ++++++++++++++++++ src/players/migrations/0013_removing_obj.py | 106 ++++++++++++++++ src/players/models.py | 25 ++-- 4 files changed, 350 insertions(+), 10 deletions(-) create mode 100644 src/objects/migrations/0014_adding_sessid.py create mode 100644 src/players/migrations/0012_adding_objs_m2m.py create mode 100644 src/players/migrations/0013_removing_obj.py diff --git a/src/objects/migrations/0014_adding_sessid.py b/src/objects/migrations/0014_adding_sessid.py new file mode 100644 index 000000000..a0ec0ae0d --- /dev/null +++ b/src/objects/migrations/0014_adding_sessid.py @@ -0,0 +1,113 @@ +# -*- coding: 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 'ObjectDB.db_sessid' + db.add_column('objects_objectdb', 'db_sessid', + self.gf('django.db.models.fields.IntegerField')(null=True), + keep_default=False) + + + def backwards(self, orm): + # Deleting field 'ObjectDB.db_sessid' + db.delete_column('objects_objectdb', 'db_sessid') + + + 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_index': 'True'}), + '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_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', '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.CharField', [], {'max_length': '255', 'null': 'True', 'blank': '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_index': 'True'}), + '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.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_sessid': ('django.db.models.fields.IntegerField', [], {'null': '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_is_connected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'obj_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_objs': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'objs_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'] \ No newline at end of file diff --git a/src/players/migrations/0012_adding_objs_m2m.py b/src/players/migrations/0012_adding_objs_m2m.py new file mode 100644 index 000000000..6e5d575b3 --- /dev/null +++ b/src/players/migrations/0012_adding_objs_m2m.py @@ -0,0 +1,116 @@ +# -*- coding: 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 M2M table for field db_objs on 'PlayerDB' + db.create_table('players_playerdb_db_objs', ( + ('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True)), + ('playerdb', models.ForeignKey(orm['players.playerdb'], null=False)), + ('objectdb', models.ForeignKey(orm['objects.objectdb'], null=False)) + )) + db.create_unique('players_playerdb_db_objs', ['playerdb_id', 'objectdb_id']) + + if not db.dry_run: + for player in orm.PlayerDB.objects.all(): + # move data from old field to new + if player.db_obj: + player.db_objs.add(player.db_obj) + player.save() + + def backwards(self, orm): + # Removing M2M table for field db_objs on 'PlayerDB' + db.delete_table('players_playerdb_db_objs') + + + 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.CharField', [], {'max_length': '255', 'null': 'True', 'blank': '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_index': 'True'}), + '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.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_sessid': ('django.db.models.fields.IntegerField', [], {'null': '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_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_is_connected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_obj': ('django.db.models.fields.related.ForeignKey', [], {'blank': 'True', 'related_name': "'obj_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_objs': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'objs_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', '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/migrations/0013_removing_obj.py b/src/players/migrations/0013_removing_obj.py new file mode 100644 index 000000000..8261ec5da --- /dev/null +++ b/src/players/migrations/0013_removing_obj.py @@ -0,0 +1,106 @@ +# -*- coding: 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): + # Deleting field 'PlayerDB.db_obj' + db.delete_column('players_playerdb', 'db_obj_id') + + + def backwards(self, orm): + # Adding field 'PlayerDB.db_obj' + db.add_column('players_playerdb', 'db_obj', + self.gf('django.db.models.fields.related.ForeignKey')(related_name='obj_set', null=True, to=orm['objects.ObjectDB'], blank=True), + keep_default=False) + + + 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.CharField', [], {'max_length': '255', 'null': 'True', 'blank': '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_index': 'True'}), + '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.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', 'blank': 'True'}), + 'db_player': ('django.db.models.fields.related.ForeignKey', [], {'to': "orm['players.PlayerDB']", 'null': 'True', 'blank': 'True'}), + 'db_sessid': ('django.db.models.fields.IntegerField', [], {'null': '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_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', '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.CharField', [], {'max_length': '255', 'null': 'True'}), + 'db_date_created': ('django.db.models.fields.DateTimeField', [], {'auto_now_add': 'True', 'blank': 'True'}), + 'db_is_connected': ('django.db.models.fields.BooleanField', [], {'default': 'False'}), + 'db_key': ('django.db.models.fields.CharField', [], {'max_length': '255', 'db_index': 'True'}), + 'db_lock_storage': ('django.db.models.fields.CharField', [], {'max_length': '512', 'blank': 'True'}), + 'db_objs': ('django.db.models.fields.related.ManyToManyField', [], {'symmetrical': 'False', 'related_name': "'objs_set'", 'null': 'True', 'to': "orm['objects.ObjectDB']"}), + 'db_permissions': ('django.db.models.fields.CharField', [], {'max_length': '255', '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'] \ No newline at end of file diff --git a/src/players/models.py b/src/players/models.py index a59850da4..6a4978aae 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -161,9 +161,9 @@ class PlayerDB(TypedObject): help_text="The User object holds django-specific authentication for each Player. A unique User should be created and tied to each Player, the two should never be switched or changed around. The User will be deleted automatically when the Player is.") # the in-game object connected to this player (if any). # Use the property 'obj' to access. - db_obj = models.ForeignKey("objects.ObjectDB", null=True, blank=True, - verbose_name="character", help_text='In-game object.') - db_objs = models.ManyToManyField("objects.ObjectDB", null=True, verbose_name="characters", help_text="In-game objects.") + db_objs = models.ManyToManyField("objects.ObjectDB", null=True, + verbose_name="characters", related_name="objs_set", + help_text="In-game objects.") # store a connected flag here too, not just in sessionhandler. # This makes it easier to track from various out-of-process locations db_is_connected = models.BooleanField(default=False, verbose_name="is_connected", help_text="If player is connected to game or not") @@ -196,11 +196,12 @@ class PlayerDB(TypedObject): # obj property (wraps db_obj) #@property - def obj_get(self): + def objs_get(self): "Getter. Allows for value = self.obj" - return get_field_cache(self, "obj") + return self.db_objs.all() + #return get_field_cache(self, "objs").all() #@obj.setter - def obj_set(self, value): + def objs_set(self, value): "Setter. Allows for self.obj = value" global _TYPECLASS if not _TYPECLASS: @@ -209,15 +210,19 @@ class PlayerDB(TypedObject): if isinstance(value, _TYPECLASS): value = value.dbobj try: - set_field_cache(self, "obj", value) + self.db_objs.add(value) + self.save() + #set_field_cache(self, "obj", value) except Exception: logger.log_trace() raise Exception("Cannot assign %s as a player object!" % value) #@obj.deleter - def obj_del(self): + def objs_del(self): "Deleter. Allows for del self.obj" - del_field_cache(self, "obj") - obj = property(obj_get, obj_set, obj_del) + self.db_objs.clear() + self.save() + #del_field_cache(self, "obj") + objs = property(objs_get, objs_set, objs_del) # whereas the name 'obj' is consistent with the rest of the code, # 'character' is a more intuitive property name, so we From c0d634fe8ceb3bf35a4fc1b11db0f11e24f44128 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 3 Feb 2013 00:23:58 +0100 Subject: [PATCH 6/9] Added "typeclass" as a shortcut variable name of __getattribute__. This allows for obj.typeclass to always cleanly return typeclass regardless of if obj is already a typeclass or is a database object. I.e. the same functionality of using obj.dbobj to always get the database object. --- src/typeclasses/typeclass.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/typeclasses/typeclass.py b/src/typeclasses/typeclass.py index f708ede3a..87c5c9307 100644 --- a/src/typeclasses/typeclass.py +++ b/src/typeclasses/typeclass.py @@ -97,6 +97,8 @@ class TypeClass(object): """ if propname == 'dbobj': return _GA(self, 'dbobj') + if propname == 'typeclass': + return self if propname.startswith('__') and propname.endswith('__'): # python specials are parsed as-is (otherwise things like # isinstance() fail to identify the typeclass) From f1767251c6cb310b3d08f20dde5acb26514ee609 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 3 Feb 2013 00:25:06 +0100 Subject: [PATCH 7/9] Can now log in to a character selection screen. Lots more testing required before one-char-per-session works. --- src/commands/default/general.py | 2 +- src/objects/models.py | 3 ++- src/players/models.py | 24 ++++++++++++++++++++---- src/players/player.py | 15 +++++++++------ src/server/serversession.py | 19 ++++++------------- src/utils/create.py | 5 +++-- 6 files changed, 41 insertions(+), 27 deletions(-) diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 79b8122d1..75445aacb 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -696,7 +696,7 @@ class CmdOOCLook(MuxCommandOOC, CmdLook): characters = player.get_all_characters() # get all characters string = "You are logged in as {g%s{n." % player.key string += " Use {w@ic {n to enter the game." - string += "\n\nAvailable character%s:" % (characters.count() > 1 and "s" or "") + string += "\n\nAvailable character%s:" % (len(characters) > 1 and "s" or "") for char in characters: csessid = char.sessid if csessid: diff --git a/src/objects/models.py b/src/objects/models.py index 2b48734b6..1fa86cd99 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -282,7 +282,8 @@ class ObjectDB(TypedObject): def __sessid_del(self): "Deleter. Allows for del self.player" del_field_cache(self, "sessid") - player = property(__sessid_get, __sessid_set, __sessid_del) + sessid = property(__sessid_get, __sessid_set, __sessid_del) + # location property (wraps db_location) #@property def __location_get(self): diff --git a/src/players/models.py b/src/players/models.py index 6a4978aae..4f29a2e02 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -369,6 +369,15 @@ class PlayerDB(TypedObject): """ Evennia -> User This is the main route for sending data back to the user from the server. + + outgoing_string (string) - text data to send + from_obj (Object/Player) - source object of message to send + data (dict) - arbitrary data object containing eventual protocol-specific options + sessid - the session id of the session to send to. If not given, return to + all sessions connected to this player. This is usually only + relevant when using msg() directly from a player-command (from + a command on a Character, the character automatically stores and + handles the sessid). """ if from_obj: # call hook @@ -389,11 +398,12 @@ class PlayerDB(TypedObject): session.msg(outgoing_string, data) else: # if no session was specified, send to them all - for sess in _GA(self, 'get_sessions'): + for sess in _GA(self, 'get_all_sessions')(): sess.msg(outgoing_string, data) def inmsg(self, ingoing_string, sessid): """ + User -> Evennia This is the reverse of msg - used by sessions to relay messages/data back into the game. It is normally not called from inside game code but only by the serversessions directly. @@ -433,6 +443,10 @@ class PlayerDB(TypedObject): set_prop_cache(self, "_characters", cache) # call hooks character.at_init() + if character: + # start (persistent) scripts on this object + #ScriptDB.objects.validate(obj=character) + pass if character.db.FIRST_LOGIN: character.at_first_login() del character.db.FIRST_LOGIN @@ -446,6 +460,8 @@ class PlayerDB(TypedObject): """ char = _GA(self, "get_character")(sessid=sessid) if char: + # call hook before disconnecting + character.at_disconnect() del char.sessid # update cache cache = get_prop_cache(self, "_characters") or {} @@ -457,11 +473,11 @@ class PlayerDB(TypedObject): """ Return session with given sessid connected to this player. """ - return SESSIONS.get_session_from_player(self, sessid=sessid) + return SESSIONS.sessions_from_player(self, sessid=sessid) def get_all_sessions(self): "Return all sessions connected to this player" - return SESSIONS.get_session_from_player(self) + return SESSIONS.sessions_from_player(self) def get_character(self, sessid=None, character=None): """ @@ -479,7 +495,7 @@ class PlayerDB(TypedObject): # try to return a character with a given sessid char = cache.get(sessid) if not char: - char = _GA(self, "db_objs").filter(player=self, sessid=sessid) or None + char = _GA(self, "db_objs").filter(db_player=self, db_sessid=sessid) or None if char: cache[sessid] = char[0] set_prop_cache(self, "_characters", cache) diff --git a/src/players/player.py b/src/players/player.py index d10c33091..e330a5de7 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -88,16 +88,20 @@ class Player(TypeClass): ## methods inherited from database model - def msg(self, outgoing_string, from_obj=None, data=None): + def msg(self, outgoing_string, from_obj=None, data=None, sessid=None): """ Evennia -> User This is the main route for sending data back to the user from the server. outgoing_string (string) - text data to send from_obj (Object/Player) - source object of message to send - data (?) - arbitrary data object containing eventual protocol-specific options - - """ + data (dict) - arbitrary data object containing eventual protocol-specific options + sessid - the session id of the session to send to. If not given, return to + all sessions connected to this player. This is usually only + relevant when using msg() directly from a player-command (from + a command on a Character, the character automatically stores and + handles the sessid). + """ self.dbobj.msg(outgoing_string, from_obj=from_obj, data=data) def swap_character(self, new_character, delete_old_character=False): @@ -293,8 +297,7 @@ class Player(TypeClass): """ # Character.at_post_login also looks around. Only use # this as a backup when logging in without a character - if not self.character: - self.execute_cmd("look") + self.execute_cmd("look") def at_disconnect(self, reason=None): """ diff --git a/src/server/serversession.py b/src/server/serversession.py index 46afb6818..57e9c13c6 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -57,10 +57,6 @@ class ServerSession(Session): self.cmdset.update(init_mode=True) return - character = self.get_character() - if character: - # start (persistent) scripts on this object - ScriptDB.objects.validate(obj=character) def session_login(self, player): """ @@ -103,7 +99,7 @@ class ServerSession(Session): self.log(_('Logged in: %(self)s') % {'self': self}) # start (persistent) scripts on this object - ScriptDB.objects.validate(obj=self.player.character) + #ScriptDB.objects.validate(obj=self.player.character) #add session to connected list self.sessionhandler.login(self) @@ -117,10 +113,10 @@ class ServerSession(Session): accounts. """ if self.logged_in: - player = self.get_player() - character = self.get_character() - if character: - character.at_disconnect() + sessid = self.sessid + player = self.player + if player.get_character(sessid): + player.disconnect_session_from_character(sessid) uaccount = player.user uaccount.last_login = datetime.now() uaccount.save() @@ -140,10 +136,7 @@ class ServerSession(Session): Returns the in-game character associated with this session. This returns the typeclass of the object. """ - player = self.get_player() - if player: - return player.character - return None + return self.logged_in and self.player.get_character(self.sessid) or None def log(self, message, channel=True): """ diff --git a/src/utils/create.py b/src/utils/create.py index 1c1ff7eeb..49cf593b8 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -500,6 +500,7 @@ def create_player(name, email, password, # call hook method (may override default permissions) new_player.at_player_creation() + print # custom given arguments potentially overrides the hook if permissions: new_player.permissions = permissions @@ -521,7 +522,7 @@ def create_player(name, email, password, player=new_player, report_to=report_to) return new_character return new_player - except Exception, e: + except Exception: # a failure in creating the character if not user: # in there was a failure we clean up everything we can @@ -538,7 +539,7 @@ def create_player(name, email, password, del new_character except Exception: pass - raise e + raise # alias player = create_player From b26c3ab8724873d4375cd663858957cd4262dc76 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 3 Feb 2013 17:00:46 +0100 Subject: [PATCH 8/9] Fixed bugs and allowed for logging in using one character. Added a simple command for creating a new character. --- src/commands/default/cmdset_ooc.py | 2 + src/commands/default/general.py | 193 ++++++++++++++++++----------- src/commands/default/muxcommand.py | 2 +- src/commands/default/system.py | 4 +- src/objects/manager.py | 9 ++ src/objects/objects.py | 5 +- src/players/models.py | 78 +++++++----- src/players/player.py | 7 +- src/server/server.py | 5 +- src/server/serversession.py | 2 +- src/typeclasses/models.py | 8 +- 11 files changed, 204 insertions(+), 111 deletions(-) diff --git a/src/commands/default/cmdset_ooc.py b/src/commands/default/cmdset_ooc.py index f28eb04db..63e9cfcd5 100644 --- a/src/commands/default/cmdset_ooc.py +++ b/src/commands/default/cmdset_ooc.py @@ -23,6 +23,7 @@ class OOCCmdSet(CmdSet): self.add(general.CmdOOCLook()) self.add(general.CmdIC()) self.add(general.CmdOOC()) + self.add(general.CmdCharCreate()) self.add(general.CmdEncoding()) self.add(general.CmdQuit()) self.add(general.CmdPassword()) @@ -34,6 +35,7 @@ class OOCCmdSet(CmdSet): self.add(system.CmdReload()) self.add(system.CmdReset()) self.add(system.CmdShutdown()) + self.add(system.CmdPy()) # Admin commands self.add(admin.CmdDelPlayer()) diff --git a/src/commands/default/general.py b/src/commands/default/general.py index 75445aacb..3714b2205 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -5,7 +5,7 @@ now. import time from django.conf import settings from src.server.sessionhandler import SESSIONS -from src.utils import utils, search +from src.utils import utils, search, create from src.objects.models import ObjectNick as Nick from src.commands.default.muxcommand import MuxCommand, MuxCommandOOC @@ -573,7 +573,7 @@ class CmdEncoding(MuxCommand): Common encodings are utf-8 (default), latin-1, ISO-8859-1 etc. If you don't submit an encoding, the current encoding will be displayed instead. - """ + """ key = "@encoding" aliases = "@encode" @@ -649,7 +649,62 @@ class CmdAccess(MuxCommand): string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms) caller.msg(string) +class CmdColorTest(MuxCommand): + """ + testing colors + Usage: + @color ansi|xterm256 + + Print a color map along with in-mud color codes, while testing what is supported in your client. + Choices are 16-color ansi (supported in most muds) or the 256-color xterm256 standard. + No checking is done to determine your client supports color - if not you will + see rubbish appear. + """ + key = "@color" + locks = "cmd:all()" + help_category = "General" + + def func(self): + "Show color tables" + + if not self.args or not self.args in ("ansi", "xterm256"): + self.caller.msg("Usage: @color ansi|xterm256") + return + + if self.args == "ansi": + from src.utils import ansi + ap = ansi.ANSI_PARSER + # ansi colors + # show all ansi color-related codes + col1 = ["%s%s{n" % (code, code.replace("{","{{")) for code, _ in ap.ext_ansi_map[:-1]] + hi = "%ch" + col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[:-2]] + col3 = ["%s%s{n" % (hi+code, (hi+code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[:-2]] + table = utils.format_table([col1, col2, col3], extra_space=1) + string = "ANSI colors:" + for row in table: + string += "\n" + "".join(row) + print string + self.caller.msg(string) + self.caller.msg("{{X and %%cx are black-on-black)") + elif self.args == "xterm256": + table = [[],[],[],[],[],[],[],[],[],[],[],[]] + for ir in range(6): + for ig in range(6): + for ib in range(6): + # foreground table + table[ir].append("%%c%i%i%i%s{n" % (ir,ig,ib, "{{%i%i%i" % (ir,ig,ib))) + # background table + table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib, + 5-ir,5-ig,5-ib, + "{{b%i%i%i" % (ir,ig,ib))) + table = utils.format_table(table) + string = "Xterm256 colors:" + for row in table: + string += "\n" + "".join(row) + self.caller.msg(string) + self.caller.msg("(e.g. %%c123 and %%cb123 also work)") #------------------------------------------------------------ @@ -724,6 +779,49 @@ class CmdOOCLook(MuxCommandOOC, CmdLook): else: self.no_look_target() +class CmdCharCreate(MuxCommandOOC): + """ + Create a character + + Usage: + @charcreate [= desc] + + Create a new character, optionally giving it a description. + """ + key = "@charcreate" + locks = "cmd:all()" + help_category = "General" + + MAX_NR_CHARACTERS = 2 + + def func(self): + "create the new character" + player = self.caller + if not self.args: + player.msg("Usage: @charcreate [= description]") + return + key = self.lhs + desc = self.rhs + if not player.db._created_chars: + lockstring = "attrread:perm(Admins);attredit:perm(Admins);attrcreate:perm(Admins)" + player.set_attribute("_created_chars", [], lockstring=lockstring) + if len(player.db._created_chars) >= self.MAX_NR_CHARACTERS: + player.msg("You may only create a maximum of %i characters." % self.MAX_NR_CHARACTERS) + return + # create the character + from src.objects.models import ObjectDB + + default_home = ObjectDB.objects.get_id(settings.CHARACTER_DEFAULT_HOME) + typeclass = settings.BASE_CHARACTER_TYPECLASS + permissions = settings.PERMISSION_PLAYER_DEFAULT + + new_character = create.create_object(typeclass, key=key, location=default_home, + home=default_home, permissions=permissions) + player.db._created_chars.append(new_character) + if desc: + new_character.db.desc = desc + player.msg("Created new character %s." % new_character.key) + class CmdIC(MuxCommandOOC): """ @@ -753,6 +851,7 @@ class CmdIC(MuxCommandOOC): Simple puppet method """ caller = self.caller + sessid = self.sessid old_character = self.character new_character = None @@ -769,16 +868,21 @@ class CmdIC(MuxCommandOOC): else: # 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) + # permission checks + if caller.get_character(sessid=sessid, character=new_character): + caller.msg("{RYou already act as {c%s{n." % new_character.name) return + if new_character.player: + if new_character.sessid == sessid: + caller.msg("{RYou already act as {c%s{n from another session." % new_character.name) + return + elif not caller.get_character(character=new_character): + 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 - if caller.swap_character(new_character): + if caller.connect_character(new_character, sessid=sessid): new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name) caller.db.last_puppet = old_character if not new_character.location: @@ -819,79 +923,22 @@ class CmdOOC(MuxCommandOOC): if utils.inherits_from(caller, "src.objects.objects.Object"): caller = self.caller.player - if not caller.character: + old_char = caller.get_character(sessid=self.sessid) + if not old_char: string = "You are already OOC." caller.msg(string) return - caller.db.last_puppet = caller.character + caller.db.last_puppet = old_char # save location as if we were disconnecting from the game entirely. - if caller.character.location: - caller.character.location.msg_contents("%s has left the game." % caller.character.key, exclude=[caller.character]) - caller.character.db.prelogout_location = caller.character.location - caller.character.location = None + if old_char.location: + old_char.location.msg_contents("%s has left the game." % old_char.key, exclude=[old_char]) + old_char.db.prelogout_location = old_char.location + old_char.location = None # disconnect - caller.character.player = None - caller.character = None + caller.disconnect_character(caller) caller.msg("\n{GYou go OOC.{n\n") caller.execute_cmd("look") -class CmdColorTest(MuxCommand): - """ - testing colors - - Usage: - @color ansi|xterm256 - - Print a color map along with in-mud color codes, while testing what is supported in your client. - Choices are 16-color ansi (supported in most muds) or the 256-color xterm256 standard. - No checking is done to determine your client supports color - if not you will - see rubbish appear. - """ - key = "@color" - locks = "cmd:all()" - help_category = "General" - - def func(self): - "Show color tables" - - if not self.args or not self.args in ("ansi", "xterm256"): - self.caller.msg("Usage: @color ansi|xterm256") - return - - if self.args == "ansi": - from src.utils import ansi - ap = ansi.ANSI_PARSER - # ansi colors - # show all ansi color-related codes - col1 = ["%s%s{n" % (code, code.replace("{","{{")) for code, _ in ap.ext_ansi_map[:-1]] - hi = "%ch" - col2 = ["%s%s{n" % (code, code.replace("%", "%%")) for code, _ in ap.mux_ansi_map[:-2]] - col3 = ["%s%s{n" % (hi+code, (hi+code).replace("%", "%%")) for code, _ in ap.mux_ansi_map[:-2]] - table = utils.format_table([col1, col2, col3], extra_space=1) - string = "ANSI colors:" - for row in table: - string += "\n" + "".join(row) - print string - self.caller.msg(string) - self.caller.msg("{{X and %%cx are black-on-black)") - elif self.args == "xterm256": - table = [[],[],[],[],[],[],[],[],[],[],[],[]] - for ir in range(6): - for ig in range(6): - for ib in range(6): - # foreground table - table[ir].append("%%c%i%i%i%s{n" % (ir,ig,ib, "{{%i%i%i" % (ir,ig,ib))) - # background table - table[6+ir].append("%%cb%i%i%i%%c%i%i%i%s{n" % (ir,ig,ib, - 5-ir,5-ig,5-ib, - "{{b%i%i%i" % (ir,ig,ib))) - table = utils.format_table(table) - string = "Xterm256 colors:" - for row in table: - string += "\n" + "".join(row) - self.caller.msg(string) - self.caller.msg("(e.g. %%c123 and %%cb123 also work)") - diff --git a/src/commands/default/muxcommand.py b/src/commands/default/muxcommand.py index 705701611..d4adb1ab3 100644 --- a/src/commands/default/muxcommand.py +++ b/src/commands/default/muxcommand.py @@ -189,6 +189,6 @@ class MuxCommandOOC(MuxCommand): self.caller = self.caller.player elif hasattr(self.caller, "character"): # caller was already a Player - self.character = self.caller.character + self.character = self.caller.get_character(sessid=self.sessid) else: self.character = None diff --git a/src/commands/default/system.py b/src/commands/default/system.py index 5ddba48cb..8cb1aa383 100644 --- a/src/commands/default/system.py +++ b/src/commands/default/system.py @@ -146,11 +146,13 @@ class CmdPy(MuxCommand): caller.msg(string) return + # check if caller is a player + # import useful variables import ev available_vars = {'self':caller, 'me':caller, - 'here':caller.location, + 'here':hasattr(caller, "location") and caller.location or None, 'ev':ev, 'inherits_from':utils.inherits_from} diff --git a/src/objects/manager.py b/src/objects/manager.py index a60425f60..ec7f107ab 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -365,3 +365,12 @@ class ObjectManager(TypedObjectManager): ScriptDB.objects.copy_script(script, new_obj=new_object.dbobj) return new_object + + + def clear_all_sessids(self): + """ + Clear the db_sessid field of all objects having also the db_player field + set. + """ + self.filter(db_sessid__isnull=False).update(db_sessid=None) + diff --git a/src/objects/objects.py b/src/objects/objects.py index 6b6f0efe6..93c56f848 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -201,7 +201,7 @@ class Object(TypeClass): ignore_errors=ignore_errors, player=player) - def execute_cmd(self, raw_string): + def execute_cmd(self, raw_string, sessid=None): """ Do something as this object. This command transparently lets its typeclass execute the command. Evennia also calls @@ -209,6 +209,7 @@ class Object(TypeClass): Argument: raw_string (string) - raw command input + sessid (int) - id of session executing the command. This sets the sessid property on the command. Returns Deferred - this is an asynchronous Twisted object that will not fire until the command has actually finished executing. To overload @@ -219,7 +220,7 @@ class Object(TypeClass): This return is not used at all by Evennia by default, but might be useful for coders intending to implement some sort of nested command structure. """ - return self.dbobj.execute_cmd(raw_string) + return self.dbobj.execute_cmd(raw_string, sessid=sessid) def msg(self, message, from_obj=None, data=None): """ diff --git a/src/players/models.py b/src/players/models.py index 4f29a2e02..9464b1ab3 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -391,7 +391,7 @@ class PlayerDB(TypedObject): if sessid: session = _GA(self, "get_session")(sessid) if session: - char = _GA(self, "get_character")(sessid) + char = _GA(self, "get_character")(sessid=sessid) if char and not char.at_msg_receive(outgoing_string, from_obj=from_obj, data=data): # if hook returns false, cancel send return @@ -412,7 +412,7 @@ class PlayerDB(TypedObject): data - dictionary of optional data session - session sending this data """ - character = _GA(self, "get_character")(sessid) + character = _GA(self, "get_character")(sessid=sessid) if character: # execute command on character _GA(character, "execute_cmd")(ingoing_string, sessid=sessid) @@ -427,9 +427,11 @@ class PlayerDB(TypedObject): linked to the player using self.connect_character(). force - drop existing connection to other character + + Returns True if connection was successful, False otherwise """ # first check if we already have a character tied to this session - char = _GA(self, "get_character")(sessid=sessid) + char = _GA(self, "get_character")(sessid=sessid, return_dbobj=True) if char: if force and char != character: _GA(self, "disconnect_session_from_character")(sessid) @@ -452,22 +454,27 @@ class PlayerDB(TypedObject): del character.db.FIRST_LOGIN character.at_pre_login() character.at_post_login() + return True def disconnect_session_from_character(self, sessid): """ Disconnect a session from the characterm (still keeping the connection to the Player) + returns the newly disconnected character, if it existed """ - char = _GA(self, "get_character")(sessid=sessid) + if not sessid: + return + char = _GA(self, "get_character")(sessid=sessid, return_dbobj=True) if char: # call hook before disconnecting - character.at_disconnect() + _GA(char.typeclass, "at_disconnect")() del char.sessid - # update cache - cache = get_prop_cache(self, "_characters") or {} - if sessid in cache: - del cache[sessid] - set_prop_cache(self, "_characters", cache) + # update cache + cache = get_prop_cache(self, "_characters") or {} + if sessid in cache: + del cache[sessid] + set_prop_cache(self, "_characters", cache) + return char def get_session(self, sessid): """ @@ -479,9 +486,9 @@ class PlayerDB(TypedObject): "Return all sessions connected to this player" return SESSIONS.sessions_from_player(self) - def get_character(self, sessid=None, character=None): + def get_character(self, sessid=None, character=None, return_dbobj=False): """ - Get the character connected to this player + Get the character connected to this player and sessid sessid - return character connected to this sessid, character - return character if connected to this player, else None. @@ -497,17 +504,18 @@ class PlayerDB(TypedObject): if not char: char = _GA(self, "db_objs").filter(db_player=self, db_sessid=sessid) or None if char: - cache[sessid] = char[0] + char = char[0] + cache[sessid] = char set_prop_cache(self, "_characters", cache) if character: - return char == character.dbobj or None - return char + return char and (char == character.dbobj and (return_dbobj and char or char.typeclass)) or None + return char and (return_dbobj and char or char.typeclass) or None elif character: - char = _GA(self, "db_objs").filter(character) - return char and char[0] or None + char = _GA(self, "db_objs").filter(id=_GA(character.dbobj, "id")) + return char and (return_dbobj and char[0] or char[0].typeclass) or None else: # no sessid given - return all available characters - return list(self.db_objs.all()) + return list(return_dbobj and o or o.typeclass for o in self.db_objs.all()) def get_all_characters(self): """ @@ -515,30 +523,37 @@ class PlayerDB(TypedObject): """ return _GA(self, "get_character")(sessid=None, character=None) - def connect_character(self, char): + def connect_character(self, character, sessid=None): """ Use the Player to connect a Character to a session. Note that we don't do any access checks at this point. Note that if the game was fully restarted (including the Portal), this must be used, since sessids will have changed as players reconnect. + + if sessid is given, also connect the sessid to the character. """ # first disconnect any other character from this session - char = char.dbobj + char = character.dbobj _GA(self, "disconnect_character")(char) - char = char.dbobj char.player = self _GA(self, "db_objs").add(char) _GA(self, "save")() + if sessid: + return _GA(self, "connect_session_to_character")(sessid=sessid, character=char) + return True - def disconnect_character(self, char): + def disconnect_character(self, character): """ Disconnect a character from this player, either based on sessid or by giving the character object directly + + Returns newly disconnected character. """ - char = char.dbobj - key = char and char.key or None - char = _GA(self, "get_character")(key=key) + if not character: + return + char = _GA(self, "get_character")(character=character, return_dbobj=True) if char: + _GA(self, "disconnect_session_from_character")(char.sessid) _GA(self, "db_objs").remove(char) del char.player del char.sessid @@ -547,16 +562,23 @@ class PlayerDB(TypedObject): cache = get_prop_cache(self, "_characters") or {} [cache.pop(sessid) for sessid,stored_char in cache.items() if stored_char==char] set_prop_cache(self, "_characters", cache) + return char + def disconnect_all_characters(self): for char in self.db_objs.all(): _GA(self, "disconnect_character")(char) - def swap_character(self, new_character, delete_old_character=False): + def swap_character(self, old_character, new_character): """ - Swaps character, if possible + Swaps character between sessions, if possible """ - return _GA(self, "__class__").objects.swap_character(self, new_character, delete_old_character=delete_old_character) + this_sessid = old_character.sessid + other_sessd = new_character.sessid + this_char = _GA(self, "disconnect_session_from_character")(this_sessid) + other_char = _GA(self, "disconnect_session_from_character")(other_sessid) + _GA(self, "connect_session_to_character")(this_sessid, other_char) + _GA(self, "connect_session_to_character")(other_sessid, this_char) def delete(self, *args, **kwargs): "Make sure to delete user also when deleting player - the two may never exist separately." diff --git a/src/players/player.py b/src/players/player.py index e330a5de7..58b289999 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -102,7 +102,7 @@ class Player(TypeClass): a command on a Character, the character automatically stores and handles the sessid). """ - self.dbobj.msg(outgoing_string, from_obj=from_obj, data=data) + self.dbobj.msg(outgoing_string, from_obj=from_obj, data=data, sessid=sessid) def swap_character(self, new_character, delete_old_character=False): """ @@ -115,7 +115,7 @@ class Player(TypeClass): """ return self.dbobj.swap_character(new_character, delete_old_character=delete_old_character) - def execute_cmd(self, raw_string): + def execute_cmd(self, raw_string, sessid=None): """ Do something as this object. This command transparently lets its typeclass execute the command. Evennia also calls @@ -123,6 +123,7 @@ class Player(TypeClass): Argument: raw_string (string) - raw command input + sessid (int) - id of session executing the command. This sets the sessid property on the command Returns Deferred - this is an asynchronous Twisted object that will not fire until the command has actually finished executing. To overload @@ -133,7 +134,7 @@ class Player(TypeClass): This return is not used at all by Evennia by default, but might be useful for coders intending to implement some sort of nested command structure. """ - return self.dbobj.execute_cmd(raw_string) + return self.dbobj.execute_cmd(raw_string, sessid=sessid) def search(self, ostring, return_character=False): """ diff --git a/src/server/server.py b/src/server/server.py index 25d3849a9..d45dc50fc 100644 --- a/src/server/server.py +++ b/src/server/server.py @@ -190,6 +190,9 @@ class Evennia(object): from src.objects.models import ObjectDB #from src.players.models import PlayerDB + # clear eventual lingering session storages + ObjectDB.objects.clear_all_sessids() + #update eventual changed defaults self.update_defaults() @@ -288,7 +291,7 @@ class Evennia(object): yield [(p.typeclass, p.at_server_shutdown()) for p in PlayerDB.get_all_cached_instances()] yield [(s.typeclass, s.at_server_shutdown()) for s in ScriptDB.get_all_cached_instances()] - + yield ObjectDB.objects.clear_all_sessids() ServerConfig.objects.conf("server_restart_mode", "reset") if SERVER_STARTSTOP_MODULE: diff --git a/src/server/serversession.py b/src/server/serversession.py index 57e9c13c6..5fb3d6496 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -178,7 +178,7 @@ class ServerSession(Session): else: # we are not logged in. Use the session directly # (it uses the settings.UNLOGGEDIN cmdset) - cmdhandler.cmdhandler(self, command_string) + cmdhandler.cmdhandler(self, command_string, sessid=self.sessid) def data_out(self, msg, data=None): """ diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index 21baa2522..286bbafc9 100644 --- a/src/typeclasses/models.py +++ b/src/typeclasses/models.py @@ -1311,7 +1311,7 @@ class TypedObject(SharedMemoryModel): return False return True - def set_attribute(self, attribute_name, new_value=None): + def set_attribute(self, attribute_name, new_value=None, lockstring=""): """ Sets an attribute on an object. Creates the attribute if need be. @@ -1319,6 +1319,10 @@ class TypedObject(SharedMemoryModel): attribute_name: (str) The attribute's name. new_value: (python obj) The value to set the attribute to. If this is not a str, the object will be stored as a pickle. + lockstring - this sets an access restriction on the attribute object. Note that + this is normally NOT checked - use the secureattr() access method + below to perform access-checked modification of attributes. Lock + types checked by secureattr are 'attrread','attredit','attrcreate'. """ attrib_obj = get_attr_cache(self, attribute_name) if not attrib_obj: @@ -1332,6 +1336,8 @@ class TypedObject(SharedMemoryModel): else: # no match; create new attribute attrib_obj = attrclass(db_key=attribute_name, db_obj=self) + if lockstring: + attrib_obj.locks.add(lockstring) # re-set an old attribute value try: attrib_obj.value = new_value From f3addf9cf9c2686f6345f76ec4b604cbcebe2e97 Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 3 Feb 2013 17:21:12 +0100 Subject: [PATCH 9/9] Shuffled migration id numbers around to account for changes in trunk. --- .../migrations/{0014_adding_sessid.py => 0016_adding_sessid.py} | 0 .../{0012_adding_objs_m2m.py => 0014_adding_objs_m2m.py} | 0 .../migrations/{0013_removing_obj.py => 0015_removing_obj.py} | 0 3 files changed, 0 insertions(+), 0 deletions(-) rename src/objects/migrations/{0014_adding_sessid.py => 0016_adding_sessid.py} (100%) rename src/players/migrations/{0012_adding_objs_m2m.py => 0014_adding_objs_m2m.py} (100%) rename src/players/migrations/{0013_removing_obj.py => 0015_removing_obj.py} (100%) diff --git a/src/objects/migrations/0014_adding_sessid.py b/src/objects/migrations/0016_adding_sessid.py similarity index 100% rename from src/objects/migrations/0014_adding_sessid.py rename to src/objects/migrations/0016_adding_sessid.py diff --git a/src/players/migrations/0012_adding_objs_m2m.py b/src/players/migrations/0014_adding_objs_m2m.py similarity index 100% rename from src/players/migrations/0012_adding_objs_m2m.py rename to src/players/migrations/0014_adding_objs_m2m.py diff --git a/src/players/migrations/0013_removing_obj.py b/src/players/migrations/0015_removing_obj.py similarity index 100% rename from src/players/migrations/0013_removing_obj.py rename to src/players/migrations/0015_removing_obj.py