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/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 cd4d7a2f5..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,151 +649,6 @@ class CmdAccess(MuxCommand): string += "\nPlayer {c%s{n: %s" % (caller.player.key, pperms) caller.msg(string) -# OOC commands - -class CmdOOCLook(MuxCommandOOC, CmdLook): - """ - ooc look - - Usage: - look - - This is an OOC version of the look command. Since a - Player doesn't have an in-game existence, there is no - concept of location or "self". If we are controlling - a character, pass control over to normal look. - - """ - - key = "look" - aliases = ["l", "ls"] - locks = "cmd:all()" - help_category = "General" - - 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. - super(CmdOOCLook, self).func() - -class CmdIC(MuxCommandOOC): - """ - Switch control to an object - - Usage: - @ic - - Go in-character (IC) as a given Character. - - This will attempt to "become" a different object assuming you have - the right to do so. Note that it's the PLAYER character that puppets - characters/objects and which needs to have the correct permission! - - You cannot become an object that is already controlled by another - player. In principle can be any in-game object as long - as you the player have access right to puppet it. - """ - - key = "@ic" - locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it. - aliases = "@puppet" - help_category = "General" - - def func(self): - """ - Simple puppet method - """ - caller = self.caller - old_character = self.character - - new_character = None - if not self.args: - new_character = caller.db.last_puppet - if not new_character: - caller.msg("Usage: @ic ") - return - if not new_character: - # search for a matching character - new_character = search.objects(self.args, caller) - if new_character: - new_character = new_character[0] - 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) - 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): - new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name) - caller.db.last_puppet = old_character - if not new_character.location: - # this might be due to being hidden away at logout; check - loc = new_character.db.prelogout_location - if not loc: # still no location; use home - loc = new_character.home - new_character.location = loc - if new_character.location: - new_character.location.msg_contents("%s has entered the game." % new_character.key, exclude=[new_character]) - new_character.location.at_object_receive(new_character, new_character.location) - new_character.execute_cmd("look") - else: - caller.msg("{rYou cannot become {C%s{n." % new_character.name) - -class CmdOOC(MuxCommandOOC): - """ - @ooc - go ooc - - Usage: - @ooc - - Go out-of-character (OOC). - - This will leave your current character and put you in a incorporeal OOC state. - """ - - key = "@ooc" - locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it. - aliases = "@unpuppet" - help_category = "General" - - def func(self): - "Implement function" - - caller = self.caller - - if utils.inherits_from(caller, "src.objects.objects.Object"): - caller = self.caller.player - - if not caller.character: - string = "You are already OOC." - caller.msg(string) - return - - caller.db.last_puppet = caller.character - # 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 - - # disconnect - caller.character.player = None - caller.character = None - - caller.msg("\n{GYou go OOC.{n\n") - caller.execute_cmd("look") - class CmdColorTest(MuxCommand): """ testing colors @@ -851,3 +706,239 @@ class CmdColorTest(MuxCommand): self.caller.msg(string) self.caller.msg("(e.g. %%c123 and %%cb123 also work)") + +#------------------------------------------------------------ +# OOC commands +#------------------------------------------------------------ + +class CmdOOCLook(MuxCommandOOC, CmdLook): + """ + ooc look + + Usage: + 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:" % (len(characters) > 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 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 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): + """ + Switch control to an object + + Usage: + @ic + + Go in-character (IC) as a given Character. + + This will attempt to "become" a different object assuming you have + the right to do so. Note that it's the PLAYER character that puppets + characters/objects and which needs to have the correct permission! + + You cannot become an object that is already controlled by another + player. In principle can be any in-game object as long + as you the player have access right to puppet it. + """ + + key = "@ic" + locks = "cmd:all()" # must be all() or different puppeted objects won't be able to access it. + aliases = "@puppet" + help_category = "General" + + def func(self): + """ + Simple puppet method + """ + caller = self.caller + sessid = self.sessid + old_character = self.character + + new_character = None + if not self.args: + new_character = caller.db.last_puppet + if not new_character: + caller.msg("Usage: @ic ") + return + if not new_character: + # search for a matching character + new_character = search.objects(self.args, caller) + if new_character: + new_character = new_character[0] + else: + # the search method handles error messages etc. + return + # 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.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: + # this might be due to being hidden away at logout; check + loc = new_character.db.prelogout_location + if not loc: # still no location; use home + loc = new_character.home + new_character.location = loc + if new_character.location: + new_character.location.msg_contents("%s has entered the game." % new_character.key, exclude=[new_character]) + new_character.location.at_object_receive(new_character, new_character.location) + new_character.execute_cmd("look") + else: + caller.msg("{rYou cannot become {C%s{n." % new_character.name) + +class CmdOOC(MuxCommandOOC): + """ + @ooc - go ooc + + Usage: + @ooc + + Go out-of-character (OOC). + + This will leave your current character and put you in a incorporeal OOC state. + """ + + key = "@ooc" + locks = "cmd:all()" # this must be all(), or different puppeted objects won't be able to access it. + aliases = "@unpuppet" + help_category = "General" + + def func(self): + "Implement function" + + caller = self.caller + + if utils.inherits_from(caller, "src.objects.objects.Object"): + caller = self.caller.player + + 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 = old_char + # save location as if we were disconnecting from the game entirely. + 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.disconnect_character(caller) + + caller.msg("\n{GYou go OOC.{n\n") + caller.execute_cmd("look") + 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/migrations/0016_adding_sessid.py b/src/objects/migrations/0016_adding_sessid.py new file mode 100644 index 000000000..a0ec0ae0d --- /dev/null +++ b/src/objects/migrations/0016_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/objects/models.py b/src/objects/models.py index 21bcdda2d..1fa86cd99 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 @@ -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,28 @@ 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, "sessid"): + del_field_cache(self, "sessid") + return get_field_cache(self, "sessid") + #@sessid.setter + def __sessid_set(self, sessid): + "Setter. Allows for self.player = value" + set_field_cache(self, "sessid", sessid) + #@sessid.deleter + def __sessid_del(self): + "Deleter. Allows for del self.player" + del_field_cache(self, "sessid") + sessid = property(__sessid_get, __sessid_set, __sessid_del) + # location property (wraps db_location) #@property def __location_get(self): @@ -612,7 +636,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 @@ -644,11 +668,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. @@ -656,8 +680,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/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/migrations/0014_adding_objs_m2m.py b/src/players/migrations/0014_adding_objs_m2m.py new file mode 100644 index 000000000..6e5d575b3 --- /dev/null +++ b/src/players/migrations/0014_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/0015_removing_obj.py b/src/players/migrations/0015_removing_obj.py new file mode 100644 index 000000000..8261ec5da --- /dev/null +++ b/src/players/migrations/0015_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 4306a8476..9464b1ab3 100644 --- a/src/players/models.py +++ b/src/players/models.py @@ -161,8 +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", 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") @@ -195,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: @@ -208,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 @@ -236,6 +242,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,32 +365,220 @@ 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. + + 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 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=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_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. + + ingoing_string - text string (i.e. command string) + data - dictionary of optional data + session - session sending this data + """ + character = _GA(self, "get_character")(sessid=sessid) + if character: + # execute command on character + _GA(character, "execute_cmd")(ingoing_string, sessid=sessid) + else: + # a non-character session; this goes to player directly + _GA(self, "execute_cmd")(ingoing_string, sessid=sessid) + + 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(). + + 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, return_dbobj=True) + 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: + # 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 + 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 + """ + if not sessid: + return + char = _GA(self, "get_character")(sessid=sessid, return_dbobj=True) + if char: + # call hook before disconnecting + _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) + return char + + def get_session(self, sessid): + """ + Return session with given sessid connected to this player. + """ + return SESSIONS.sessions_from_player(self, sessid=sessid) + + def get_all_sessions(self): + "Return all sessions connected to this player" + return SESSIONS.sessions_from_player(self) + + def get_character(self, sessid=None, character=None, return_dbobj=False): + """ + 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. + + 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 = _GA(self, "db_objs").filter(db_player=self, db_sessid=sessid) or None + if char: + char = char[0] + cache[sessid] = char + set_prop_cache(self, "_characters", cache) + if character: + 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(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(return_dbobj and o or o.typeclass for o in self.db_objs.all()) + + def get_all_characters(self): + """ + Readability-wrapper for getting all characters + """ + return _GA(self, "get_character")(sessid=None, character=None) + + 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 = character.dbobj + _GA(self, "disconnect_character")(char) + 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, character): + """ + Disconnect a character from this player, either based + on sessid or by giving the character object directly + + Returns newly disconnected character. + """ + 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 + self.save() + # clear cache + 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 swap_character(self, new_character, delete_old_character=False): + def disconnect_all_characters(self): + for char in self.db_objs.all(): + _GA(self, "disconnect_character")(char) + + 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." @@ -401,7 +596,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. @@ -417,7 +612,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/players/player.py b/src/players/player.py index d10c33091..58b289999 100644 --- a/src/players/player.py +++ b/src/players/player.py @@ -88,17 +88,21 @@ 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 - - """ - self.dbobj.msg(outgoing_string, from_obj=from_obj, data=data) + 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, sessid=sessid) def swap_character(self, new_character, delete_old_character=False): """ @@ -111,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 @@ -119,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 @@ -129,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): """ @@ -293,8 +298,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/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 f1303e8d8..5fb3d6496 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): """ @@ -91,38 +87,24 @@ 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}) # 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) - # post-login hooks player.at_post_login() - if character: - character.at_post_login() def session_disconnect(self): """ @@ -131,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() @@ -154,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): """ @@ -193,20 +172,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, sessid=self.sessid) def data_out(self, msg, data=None): """ @@ -243,11 +215,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 46a5e58ee..7b469895c 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 @@ -222,26 +223,30 @@ 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)) - 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: + 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] def sessions_from_character(self, character): """ 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): 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. diff --git a/src/typeclasses/models.py b/src/typeclasses/models.py index d5d23bcd6..e98000b61 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 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) 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