diff --git a/src/commands/default/cmdset_ooc.py b/src/commands/default/cmdset_ooc.py index f28eb04dbf..63e9cfcd5f 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 75445aacb3..3714b22053 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 7057016113..d4adb1ab39 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 5ddba48cb5..8cb1aa3835 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 a60425f607..ec7f107ab5 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 6b6f0efe65..93c56f8487 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 4f29a2e022..9464b1ab35 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 e330a5de7c..58b2899990 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 25d3849a93..d45dc50fce 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 57e9c13c68..5fb3d64964 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 21baa25222..286bbafc92 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