diff --git a/contrib/chargen.py b/contrib/chargen.py new file mode 100644 index 0000000000..27d552b25c --- /dev/null +++ b/contrib/chargen.py @@ -0,0 +1,199 @@ +""" + +Contribution - Griatch 2011 + +This is a simple character creation commandset. A suggestion is to +test this together with menu_login, which doesn't create a Character +on its own. This shows some more info and gives the Player the option +to create a character without any more customizations than their name +(further options are unique for each game anyway). + +Since this extends the OOC cmdset, logging in from the menu will +automatically drop the Player into this cmdset unless they logged off +while puppeting a Character already before. + +Installation: + +Import this module in game.gamesrc.basecmdset and +add the following line to the end of OOCCmdSet's at_cmdset_creation(): + + self.add(character_creation.OOCCmdSetCharGen) + + +If you have a freshly installed database you could also instead add/edit +this line to your game/settings.py file: + +CMDSET_OOC = "contrib.character_creation.OOCCmdSetCharGen" + +This will replace the default OOCCmdset to look to this module +instead of the one in game.gamesrc.basecmdset. If you do this, uncomment +the super() statement in OOCCmdSetCharGen (end of this file) too. This will +however only affect NEWLY created players, not those already in the game, which i +s why you'd usually only do this if you are starting from scratch. + +""" + +from django.conf import settings +from src.commands.command import Command +from src.commands.default.general import CmdLook +from src.commands.default.cmdset_ooc import OOCCmdSet +from src.objects.models import ObjectDB +from src.utils import utils, create + +CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS + +class CmdOOCLook(CmdLook): + """ + ooc look + + Usage: + look + 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 any characters are available for you to control, you may look + at them with this command. + """ + + key = "look" + aliases = ["l", "ls"] + locks = "cmd:all()" + help_cateogory = "General" + + def func(self): + """ + Implements the ooc look command + + We use an attribute _character_dbrefs on the player in order + to figure out which characters are "theirs". A drawback of this + is that only the CmdCharacterCreate command adds this attribute, + and thus e.g. player #1 will not be listed (although it will work). + Existence in this list does not depend on puppeting rights though, + that is checked by the @ic command directly. + """ + + # making sure caller is really a player + self.character = None + if utils.inherits_from(self.caller, "src.objects.objects.Object"): + # An object of some type is calling. Convert to player. + #print self.caller, self.caller.__class__ + self.character = self.caller + if hasattr(self.caller, "player"): + self.caller = self.caller.player + + if not self.character: + # ooc mode, we are players + + avail_chars = self.caller.db._character_dbrefs + if self.args: + # Maybe the caller wants to look at a character + if not avail_chars: + self.caller.msg("You have no characters to look at. Why not create one?") + return + objs = ObjectDB.objects.get_objs_with_key_and_typeclass(self.args.strip(), CHARACTER_TYPECLASS) + objs = [obj for obj in objs if obj.id in avail_chars] + if not objs: + self.caller.msg("You cannot see this Character.") + return + self.caller.msg(objs[0].return_appearance(self.caller)) + return + + # not inspecting a character. Show the OOC info. + charobjs = [] + charnames = [] + if self.caller.db._character_dbrefs: + dbrefs = self.caller.db._character_dbrefs + charobjs = [ObjectDB.objects.get_id(dbref) for dbref in dbrefs] + charnames = [charobj.key for charobj in charobjs if charobj] + if charnames: + charlist = "The following Character(s) are available:\n\n" + charlist += "\n\r".join(["{w %s{n" % charname for charname in charnames]) + charlist += "\n\n Use {w@ic {n to switch to that Character." + else: + charlist = "You have no Characters." + string = \ +""" You, %s, are an {wOOC ghost{n without form. The world is hidden + from you and besides chatting on channels your options are limited. + You need to have a Character in order to interact with the world. + + %s + + Use {wcreate {n to create a new character and {whelp{n for a + list of available commands.""" % (self.caller.key, charlist) + self.caller.msg(string) + + else: + # not ooc mode - leave back to normal look + self.caller = self.character # we have to put this back for normal look to work. + super(CmdOOCLook, self).func() + +class CmdOOCCharacterCreate(Command): + """ + creates a character + + Usage: + create + + This will create a new character, assuming + the given character name does not already exist. + """ + + key = "create" + locks = "cmd:all()" + + def func(self): + """ + Tries to create the Character object. We also put an + attribute on ourselves to remember it. + """ + + # making sure caller is really a player + self.character = None + if utils.inherits_from(self.caller, "src.objects.objects.Object"): + # An object of some type is calling. Convert to player. + #print self.caller, self.caller.__class__ + self.character = self.caller + if hasattr(self.caller, "player"): + self.caller = self.caller.player + + if not self.args: + self.caller.msg("Usage: create ") + return + charname = self.args.strip() + old_char = ObjectDB.objects.get_objs_with_key_and_typeclass(charname, CHARACTER_TYPECLASS) + if old_char: + self.caller.msg("Character {c%s{n already exists." % charname) + return + # create the character + + new_character = create.create_object(CHARACTER_TYPECLASS, key=charname) + if not new_character: + self.caller.msg("{rThe Character couldn't be created. This is a bug. Please contact an admin.") + return + # make sure to lock the character to only be puppeted by this player + new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % + (new_character.id, self.caller.id)) + + # save dbref + avail_chars = self.caller.db._character_dbrefs + if avail_chars: + avail_chars.append(new_character.id) + else: + avail_chars = [new_character.id] + self.caller.db._character_dbrefs = avail_chars + + self.caller.msg("{gThe Character {c%s{g was successfully created!" % charname) + +class OOCCmdSetCharGen(OOCCmdSet): + """ + Extends the default OOC cmdset. + """ + def at_cmdset_creation(self): + "Install everything from the default set, then overload" + #super(OOCCmdSetCharGen, self).at_cmdset_creation() + self.add(CmdOOCLook()) + self.add(CmdOOCCharacterCreate()) + diff --git a/contrib/menu_login.py b/contrib/menu_login.py new file mode 100644 index 0000000000..a52407a1bd --- /dev/null +++ b/contrib/menu_login.py @@ -0,0 +1,332 @@ +""" +Menu-driven login system + +Contribution - Griatch 2011 + + +This is an alternative login system for Evennia, using the +contrib.menusystem module. As opposed to the default system it doesn't +use emails for authentication and also don't auto-creates a Character +with the same name as the Player (instead assuming some sort of +character-creation to come next). + + +Install is simple: + +To your settings file, add/edit the line: + +CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedInCmdSet" + +That's it. The cmdset in this module will now be used instead of the +default one. + +The initial login "graphic" is taken from strings in the module given +by settings.CONNECTION_SCREEN_MODULE. You will want to edit the string +in that module (at least comment out the default string that mentions +commands that are not available) and add something more suitable for +the initial splash screen. + +""" + +import re +import traceback +from django.conf import settings +from django.contrib.auth.models import User +from src.server import sessionhandler +from src.players.models import PlayerDB +from src.objects.models import ObjectDB +from src.server.models import ServerConfig +from src.comms.models import Channel + +from src.utils import create, logger, utils, ansi +from src.commands.command import Command +from src.commands.cmdset import CmdSet +from src.commands.cmdhandler import CMD_LOGINSTART +from contrib.menusystem import MenuNode, MenuTree, CMD_NOINPUT, CMD_NOMATCH + +CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE + + +# Commands run on the unloggedin screen. Note that this is not using settings.UNLOGGEDIN_CMDSET but +# the menu system, which is why some are named for the numbers in the menu. +# +# Also note that the menu system will automatically assign all +# commands used in its structure a property "menutree" holding a reference +# back to the menutree. This allows the commands to do direct manipulation +# for example by triggering a conditional jump to another node. +# + +# Menu entry 1a - Entering a Username + +class CmdBackToStart(Command): + """ + Step back to node0 + """ + key = CMD_NOINPUT + locks = "cmd:all()" + def func(self): + "Execute the command" + self.menutree.goto("START") + +class CmdUsernameSelect(Command): + """ + Handles the entering of a username and + checks if it exists. + """ + key = CMD_NOMATCH + locks = "cmd:all()" + def func(self): + "Execute the command" + player = PlayerDB.objects.get_player_from_name(self.args) + if not player: + self.caller.msg("{rThis account name couldn't be found. Did you create it? If you did, make sure you spelled it right (case doesn't matter).{n") + self.menutree.goto("node1a") + else: + self.menutree.player = player # store the player so next step can find it + self.menutree.goto("node1b") + +# Menu entry 1b - Entering a Password + +class CmdPasswordSelectBack(Command): + """ + Steps back from the Password selection + """ + key = CMD_NOINPUT + locks = "cmd:all()" + def func(self): + "Execute the command" + self.menutree.goto("node1a") + +class CmdPasswordSelect(Command): + """ + Handles the entering of a password and logs into the game. + """ + key = CMD_NOMATCH + locks = "cmd:all()" + + def func(self): + "Execute the command" + if not hasattr(self.menutree, "player"): + self.caller.msg("{rSomething went wrong! The player was not remembered from last step!{n") + self.menutree.goto("node1a") + return + player = self.menutree.player + if not player.user.check_password(self.args): + self.caller.msg("{rIncorrect password.{n") + self.menutree.goto("node1b") + return + + # we are ok, log us in. + self.caller.msg("{gWelcome %s! Logging in ...{n" % player.key) + self.caller.session_login(player) + + # abort menu, do cleanup. + self.menutree.goto("END") + + # we are logged in. Look around. + character = player.character + if character: + character.execute_cmd("look") + else: + # we have no character yet; use player's look, if it exists + player.execute_cmd("look") + +# Menu entry 2a - Creating a Username + +class CmdUsernameCreate(Command): + """ + Handle the creation of a valid username + """ + key = CMD_NOMATCH + locks = "cmd:all()" + + def func(self): + "Execute the command" + playername = self.args + + # sanity check on the name + if not re.findall('^[\w. @+-]+$', playername) or not (3 <= len(playername) <= 30): + self.caller.msg("\n\r {rAccount name should be between 3 and 30 characters. Letters, spaces, dig\ +its and @/./+/-/_ only.{n") # this echoes the restrictions made by django's auth module. + self.menutree.goto("node2a") + return + if PlayerDB.objects.get_player_from_name(playername): + self.caller.msg("\n\r {rAccount name %s already exists.{n" % playername) + self.menutree.goto("node2a") + return + # store the name for the next step + self.menutree.playername = playername + self.menutree.goto("node2b") + +# Menu entry 2b - Creating a Password + +class CmdPasswordCreateBack(Command): + "Step back from the password creation" + key = CMD_NOINPUT + locks = "cmd:all()" + def func(self): + "Execute the command" + self.menutree.goto("node2a") + +class CmdPasswordCreate(Command): + "Handle the creation of a password. This also creates the actual Player/User object." + key = CMD_NOMATCH + locks = "cmd:all()" + + def func(self): + "Execute the command" + password = self.args + if not hasattr(self.menutree, 'playername'): + self.caller.msg("{rSomething went wrong! Playername not remembered from previous step!{n") + self.menutree.goto("node2a") + return + playername = self.menutree.playername + if len(password) < 3: + # too short password + string = "{rYour password must be at least 3 characters or longer." + string += "\n\rFor best security, make it at least 8 characters long, " + string += "avoid making it a real word and mix numbers into it.{n" + self.caller.msg(string) + self.menutree.goto("node2b") + return + # everything's ok. Create the new player account. Don't create a Character here. + try: + permissions = settings.PERMISSION_PLAYER_DEFAULT + typeclass = settings.BASE_PLAYER_TYPECLASS + new_player = create.create_player(playername, None, password, + typeclass=typeclass, + permissions=permissions, + create_character=False) + if not new_player: + self.msg("There was an error creating the Player. This error was logged. Contact an admin.") + self.menutree.goto("START") + return + utils.init_new_player(new_player) + + # join the new player to the public channel + pchanneldef = settings.CHANNEL_PUBLIC + if pchanneldef: + pchannel = Channel.objects.get_channel(pchanneldef[0]) + if not pchannel.connect_to(new_player): + string = "New player '%s' could not connect to public channel!" % new_player.key + logger.log_errmsg(string) + + # tell the caller everything went well. + string = "{gA new account '%s' was created. Now go log in from the menu!{n" + self.caller.msg(string % (playername)) + self.menutree.goto("START") + except Exception: + # We are in the middle between logged in and -not, so we have to handle tracebacks + # ourselves at this point. If we don't, we won't see any errors at all. + string = "%s\nThis is a bug. Please e-mail an admin if the problem persists." + self.caller.msg(string % (traceback.format_exc())) + logger.log_errmsg(traceback.format_exc()) + + +# Menu entry 3 - help screen + +LOGIN_SCREEN_HELP = \ + """ + Welcome to %s! + + To login you need to first create an account. This is easy and + free to do: Choose option {w(1){n in the menu and enter an account + name and password when prompted. Obs- the account name is {wnot{n + the name of the Character you will play in the game! + + It's always a good idea (not only here, but everywhere on the net) + to not use a regular word for your password. Make it longer than 3 + characters (ideally 6 or more) and mix numbers and capitalization + into it. The password also handles whitespace, so why not make it + a small sentence - easy to remember, hard for a computer to crack. + + Once you have an account, use option {w(2){n to log in using the + account name and password you specified. + + Use the {whelp{n command once you're logged in to get more + aid. Hope you enjoy your stay! + + + (return to go back)""" % settings.SERVERNAME +# Menu entry 4 + +class CmdUnloggedinQuit(Command): + """ + We maintain a different version of the quit command + here for unconnected players for the sake of simplicity. The logged in + version is a bit more complicated. + """ + key = "4" + aliases = ["quit", "qu", "q"] + locks = "cmd:all()" + + def func(self): + "Simply close the connection." + self.menutree.goto("END") + self.caller.msg("Good bye! Disconnecting ...") + self.caller.session_disconnect() + + +# The login menu tree, using the commands above + +START = MenuNode("START", text=utils.string_from_module(CONNECTION_SCREEN_MODULE), + links=["node1a", "node2a", "node3", "END"], + linktexts=["Log in with an existing account", + "Create a new account", + "Help", + "Quit",], + selectcmds=[None, None, None, CmdUnloggedinQuit]) + +node1a = MenuNode("node1a", text="Please enter your account name (empty to abort).", + links=["START", "node1b"], + helptext=["Enter the account name you previously registered with."], + keywords=[CMD_NOINPUT, CMD_NOMATCH], + selectcmds=[CmdBackToStart, CmdUsernameSelect], + nodefaultcmds=True) # if we don't, default help/look will be triggered by names starting with l/h ... +node1b = MenuNode("node1b", text="Please enter your password (empty to go back).", + links=["node1a", "END"], + keywords=[CMD_NOINPUT, CMD_NOMATCH], + selectcmds=[CmdPasswordSelectBack, CmdPasswordSelect], + nodefaultcmds=True) + +node2a = MenuNode("node2a", text="Please enter your desired account name (empty to abort).", + links=["START", "node2b"], + helptext="Account name can max be 30 characters or fewer. Letters, spaces, digits and @/./+/-/_ only.", + keywords=[CMD_NOINPUT, CMD_NOMATCH], + selectcmds=[CmdBackToStart, CmdUsernameCreate], + nodefaultcmds=True) +node2b = MenuNode("node2b", text="Please enter your password (empty to go back).", + links=["node2a", "START"], + helptext="Your password cannot contain any characters.", + keywords=[CMD_NOINPUT, CMD_NOMATCH], + selectcmds=[CmdPasswordCreateBack, CmdPasswordCreate], + nodefaultcmds=True) +node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP, + links=["START"], + helptext="", + keywords=[CMD_NOINPUT], + selectcmds=[CmdBackToStart]) + + +# access commands + +class UnloggedInCmdSet(CmdSet): + "Cmdset for the unloggedin state" + key = "UnloggedinState" + priority = 0 + def at_cmdset_creation(self): + self.add(CmdUnloggedinLook()) + +class CmdUnloggedinLook(Command): + """ + An unloggedin version of the look command. This is called by the server when the player + first connects. It sets up the menu before handing off to the menu's own look command.. + """ + key = CMD_LOGINSTART + aliases = ["look", "l"] + locks = "cmd:all()" + + def func(self): + "Execute the menu" + menu = MenuTree(self.caller, nodes=(START, node1a, node1b, node2a, node2b, node3), exec_end=None) + menu.start() diff --git a/contrib/menusystem.py b/contrib/menusystem.py index 2d53fbbc06..6add273e66 100644 --- a/contrib/menusystem.py +++ b/contrib/menusystem.py @@ -48,7 +48,7 @@ class CmdMenuNode(Command): locks = "cmd:all()" help_category = "Menu" - menutree = None + menutree = None code = None def func(self): @@ -127,8 +127,7 @@ class MenuCmdSet(CmdSet): mergetype = "Replace" def at_cmdset_creation(self): "populate cmdset" - self.add(CmdMenuLook()) - self.add(CmdMenuHelp()) + pass # # Menu Node system @@ -150,15 +149,19 @@ class MenuTree(object): 'START' and 'END' respectively. """ - def __init__(self, caller, nodes=None, startnode="START", endnode="END"): + def __init__(self, caller, nodes=None, startnode="START", endnode="END", exec_end="look"): """ We specify startnode/endnode so that the system knows where to enter and where to exit the menu tree. If nodes is given, it shuld be a list of valid node objects to add to the tree. + + exec_end - if not None, will execute the given command string + directly after the menu system has been exited. """ self.tree = {} self.startnode = startnode self.endnode = endnode + self.exec_end = exec_end self.caller = caller if nodes and utils.is_iter(nodes): for node in nodes: @@ -187,7 +190,8 @@ class MenuTree(object): # if we was given the END node key, we clean up immediately. self.caller.cmdset.delete("menucmdset") del self.caller.db._menu_data - self.caller.execute_cmd("look") + if self.exec_end != None: + self.caller.execute_cmd(self.exec_end) return # not exiting, look for a valid code. node = self.tree.get(key, None) @@ -218,18 +222,28 @@ class MenuNode(object): """ def __init__(self, key, text="", links=None, linktexts=None, - keywords=None, cols=1, helptext=None, code=""): + keywords=None, cols=1, helptext=None, selectcmds=None, code="", nodefaultcmds=False, separator=""): """ key - the unique identifier of this node. text - is the text that will be displayed at top when viewing this node. - links - a list of keys for unique menunodes this is connected to. - linktexts - a list of texts to describe the links. If defined, need to match links list - keywords - a list of unique keys for choosing links. Must match links list. If not given, index numbers will be used. + links - a list of keys for unique menunodes this is connected to. The actual keys will not be + printed - keywords will be used (or a number) + linktexts - an optional list of texts to describe the links. Must match link list if defined. Entries can be None + to not generate any extra text for a particular link. + keywords - an optional list of unique keys for choosing links. Must match links list. If not given, index numbers + will be used. Also individual list entries can be None and will be replaed by indices. + If CMD_NOMATCH or CMD_NOENTRY, no text will be generated to indicate the option exists. cols - how many columns to use for displaying options. helptext - if defined, this is shown when using the help command instead of the normal help index. + selectcmds- a list of custom cmdclasses for handling each option. Must match links list, but some entries + may be set to None to use default menu cmds. The given command's key will be used for the menu + list entry unless it's CMD_NOMATCH or CMD_NOENTRY, in which case no text will be generated. These + commands have access to self.menutree and so can be used to select nodes. code - functional code. This will be executed just before this node is loaded (i.e. as soon after it's been selected from another node). self.caller is available to call from this code block, as well as ObjectDB and PlayerDB. + nodefaultcmds - if true, don't offer the default help and look commands in the node + separator - this string will be put on the line between menu nodes5B. """ self.key = key self.cmdset = None @@ -237,23 +251,32 @@ class MenuNode(object): self.linktexts = linktexts self.keywords = keywords self.cols = cols + self.selectcmds = selectcmds self.code = code - + self.nodefaultcmds = nodefaultcmds + self.separator = separator + Nlinks = len(self.links) + # validate the input if not self.links: self.links = [] - if not self.linktexts or (self.linktexts and len(self.linktexts) != len(self.links)): - self.linktexts = [] - if not self.keywords or (self.keywords and len(self.keywords) != len(self.links)): - self.keywords = [] + if not self.linktexts or (len(self.linktexts) != Nlinks): + self.linktexts = [None for i in range(Nlinks)] + if not self.keywords or (len(self.keywords) != Nlinks): + self.keywords = [None for i in range(Nlinks)] + if not selectcmds or (len(self.selectcmds) != Nlinks): + self.selectcmds = [None for i in range(Nlinks)] # Format default text for the menu-help command if not helptext: - helptext = "Select one of the valid options" - if self.keywords: - helptext += " (" + ", ".join(self.keywords) + ")" - elif self.links: - helptext += " (" + ", ".join([str(i + 1) for i in range(len(self.links))]) + ")" + helptext = "Select one of the valid options (" + for i in range(Nlinks): + if self.keywords[i]: + if self.keywords[i] not in (CMD_NOMATCH, CMD_NOINPUT): + helptext += "%s, " % self.keywords[i] + else: + helptext += "%s, " % (i + 1) + helptext = helptext.rstrip(", ") + ")" self.helptext = helptext # Format text display @@ -264,12 +287,14 @@ class MenuNode(object): # format the choices into as many collumns as specified choices = [] for ilink, link in enumerate(self.links): - if self.keywords: - choice = "{g%s{n" % self.keywords[ilink] + choice = "" + if self.keywords[ilink]: + if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT): + choice += "{g%s{n" % self.keywords[ilink] else: - choice = "{g%i{n" % (ilink + 1) - if self.linktexts: - choice += "-%s" % self.linktexts[ilink] + choice += "{g %i{n" % (ilink + 1) + if self.linktexts[ilink]: + choice += " - %s" % self.linktexts[ilink] choices.append(choice) cols = [[] for i in range(min(len(choices), cols))] while True: @@ -282,9 +307,9 @@ class MenuNode(object): break ftable = utils.format_table(cols) for row in ftable: - string += "\n" + "".join(row) + string +="\n" + "".join(row) # store text - self.text = 78*"-" + "\n" + string.strip() + self.text = self.separator + "\n" + string.rstrip() def init(self, menutree): """ @@ -292,15 +317,24 @@ class MenuNode(object): """ # Create the relevant cmdset self.cmdset = MenuCmdSet() - for i, link in enumerate(self.links): - cmd = CmdMenuNode() - cmd.key = str(i + 1) + if not self.nodefaultcmds: + # add default menu commands + self.cmdset.add(CmdMenuLook()) + self.cmdset.add(CmdMenuHelp()) + + for i, link in enumerate(self.links): + if self.selectcmds[i]: + cmd = self.selectcmds[i]() + else: + cmd = CmdMenuNode() + cmd.key = str(i + 1) + # this is the operable command, it moves us to the next node. + cmd.code = "self.menutree.goto('%s')" % link + # also custom commands get access to the menutree. cmd.menutree = menutree - # this is the operable command, it moves us to the next node. - cmd.code = "self.menutree.goto('%s')" % link - if self.keywords: + if self.keywords[i] and cmd.key not in (CMD_NOMATCH, CMD_NOINPUT): cmd.aliases = [self.keywords[i]] - self.cmdset.add(cmd) + self.cmdset.add(cmd) def __str__(self): "Returns the string representation." diff --git a/docs/sphinx/source/wiki/Commands.rst b/docs/sphinx/source/wiki/Commands.rst index e1a9b54518..9e9b8c8fa2 100644 --- a/docs/sphinx/source/wiki/Commands.rst +++ b/docs/sphinx/source/wiki/Commands.rst @@ -616,6 +616,29 @@ command must be added to a cmdset as well before it will work. def func(self): self.caller.msg("Don't just press return like that, talk to me!") +Exits +----- + +*Note: This is an advanced topic.* + +The functionality of `Exit `_ objects in Evennia is not +hard-coded in the engine. Instead Exits are normal typeclassed objects +that auto-creates a ``CmdSet`` on themselves when they are loaded. This +cmdset has a single command with the same name (and aliases) as the Exit +object itself. So what happens when a Player enters the name of the Exit +on the command line is simply that the command handler, in the process +of searching all available commands, also picks up the command from the +Exit object(s) in the same room. Having found the matching command, it +executes it. The command then makes sure to do all checks and eventually +move the Player across the exit as appropriate. This allows exits to be +extremely flexible - the functionality can be customized just like one +would edit any other command. + +Admittedly, you will usually be fine just using the appropriate +``traverse_*`` hooks. But if you are interested in really changing how +things work under the hood, check out ``src.objects.objects`` for how +the default ``Exit`` typeclass is set up. + How commands actually work -------------------------- @@ -684,3 +707,14 @@ Call ``func()`` on the command instance. This is the functional body of the command, actually doing useful things. Call ``at_post_command()`` on the command instance. + +Assorted notes +-------------- + +The return value of ``Command.func()`` *is* safely passed on should one +have some very specific use case in mind. So one could in principle do +``value = obj.execute_cmd(cmdname)``. Evennia does not use this +functionality at all by default (all default commands simply returns +``None``) and it's probably not relevant to any but the most +advanced/exotic designs (one might use it to create a "nested" command +structure for example). diff --git a/docs/sphinx/source/wiki/EvenniaIntroduction.rst b/docs/sphinx/source/wiki/EvenniaIntroduction.rst index 6b799b0502..5b70931941 100644 --- a/docs/sphinx/source/wiki/EvenniaIntroduction.rst +++ b/docs/sphinx/source/wiki/EvenniaIntroduction.rst @@ -17,7 +17,7 @@ and running a text-based massively-multiplayer game your very own. You might just be starting to think about it, or you might have lugged around that *perfect* game in your mind for years ... you know *just* how good it would be, if you could only make it come to -reality. We know how you feel. That is, after all why Evennia came to +reality. We know how you feel. That is, after all, why Evennia came to be. Evennia is in principle a MUD-building system: a bare-bones Python @@ -33,11 +33,11 @@ will in that case all be optional. What we *do* however, is to provide a solid foundation for all the boring database, networking, and behind-the-scenes administration stuff -that all online games need whether they like it or not. Evennia is by -default *fully persistent*, that means things you drop on the ground -somewhere will still be there a dozen server reboots later. Through -Django, we support a large variety of different database systems (the -default of which is created for you automatically). +that all online games need whether they like it or not. Evennia is +*fully persistent*, that means things you drop on the ground somewhere +will still be there a dozen server reboots later. Through Django we +support a large variety of different database systems (a database is +created for you automatically if you use the defaults). Using the full power of Python throughout the server offers some distinct advantages. All your coding, from object definitions and custom @@ -104,11 +104,11 @@ manual `_ with lots of examples. But while Python is a relatively easy programming language, it still represents a learning curve if you are new to programming. You should probably sit down with a Python beginner's -`tutorial `_ (there are plenty -of them on the web if you look around) so you at least know know what -you are seeing. To efficiently code your dream game in Evennia you don't -need to be a Python guru, but you do need to be able to read example -code containing at least these basic Python features: +`tutorial `_ (there are plenty of them +on the web if you look around) so you at least know what you are seeing. +To efficiently code your dream game in Evennia you don't need to be a +Python guru, but you do need to be able to read example code containing +at least these basic Python features: - Importing python modules - Using variables, `conditional diff --git a/docs/sphinx/source/wiki/GettingStarted.rst b/docs/sphinx/source/wiki/GettingStarted.rst index 87e6e06aeb..9c4fb02ed4 100644 --- a/docs/sphinx/source/wiki/GettingStarted.rst +++ b/docs/sphinx/source/wiki/GettingStarted.rst @@ -44,8 +44,7 @@ Evennia: **Python** (http://www.python.org) -- Version 2.5+ strongly recommended, although 2.3 or 2.4 **may** work. - Obs- Python3.x is not supported yet. +- Version 2.5+. Obs- Python3.x is not supported yet. - The default database system SQLite3 only comes as part of Python2.5 and later. - Windows users are recommended to use ActivePython @@ -97,8 +96,8 @@ Installing pre-requisites **Linux** package managers should usually handle all this for you. Python itself is definitely available through all distributions. On -Debian-derived systems you can do something like this (as root) to get -all you need: +Debian-derived systems (such as Ubuntu) you can do something like this +(as root) to get all you need: :: @@ -164,6 +163,7 @@ In the future, you just do :: hg pull + hg update from your ``evennia/`` directory to obtain the latest updates. diff --git a/docs/sphinx/source/wiki/Internationalization.rst b/docs/sphinx/source/wiki/Internationalization.rst index 8042498e00..a285354173 100644 --- a/docs/sphinx/source/wiki/Internationalization.rst +++ b/docs/sphinx/source/wiki/Internationalization.rst @@ -60,12 +60,12 @@ form. :: - django-admin.py compilemessages + django-admin compilemessages This will go through all languages and create/update compiled files (``*.mo``) for them. This needs to be done whenever a ``*.po`` file is updated. -When you are done, send the ``*.po`` and \*.mo file to the Evennia +When you are done, send the ``*.po`` and ``*.mo`` file to the Evennia developer list (or push it into your own repository clone) so we can integrate your translation into Evennia! diff --git a/docs/sphinx/source/wiki/SessionProtocols.rst b/docs/sphinx/source/wiki/SessionProtocols.rst index 0dac7ed152..268a89cb2a 100644 --- a/docs/sphinx/source/wiki/SessionProtocols.rst +++ b/docs/sphinx/source/wiki/SessionProtocols.rst @@ -99,6 +99,121 @@ be named exactly like this): parsed. From inside Evennia, ``data_out`` is often called with the alias ``msg`` instead. +Out-of-band communication +------------------------- + +Out-of-band communication (OOB) is data being sent to and fro the +player's client and the server on the protocol level, often due to the +request of the player's client software rather than any sort of active +input by the player. There are two main types: + +- Data requested by the client which the server responds to + immediately. This could for example be data that should go into a + window that the client just opened up. +- Data the server sends to the client to keep ut up-to-date. A common + example of this is something like a graphical health bar - *whenever* + the character's health status changes the server sends this data to + the client so it can update the bar graphic. This sending could also + be done on a timer, for example updating a weather map regularly. + +To communicate to the client, there are a range of protocols available +for MUDs, supported by different clients, such as MSDP and GMCP. They +basically implements custom telnet negotiation sequences and goes into a +custom Evennia Portal protocol so Evennia can understand it. + +It then needs to translate each protocol-specific function into an +Evennia function name - specifically a name of a module-level function +you define in the module given by ``settings.OOB_FUNC_MODULE``. These +function will get the session/character as first argument but is +otherwise completely free of form. The portal packs all function names +and eventual arguments they need in a dictionary and sends them off to +the Server by use of the ``sessionhandler.oob_data_in()`` method. On the +Server side, the dictionary is parsed, and the correct functions in +``settings.OOB_FUNC_MODULE`` are called with the given arguments. The +results from this function are again packed in a dictionary (keyed by +function name) and sent back to the portal. It will appear in the Portal +session's ``oob_data_out(data)`` method. + +So to summarize: To implement a Portal protocol with OOB communication +support, you need to first let your normal ``getData`` method somehow +parse out the special protocol format format coming in from the client +(MSDP, GMCP etc). It needs to translate what the client wants into +function names matching that in the ``OOB_FUNC_MODULE`` - these +functions need to be created to match too of course. The function name +and arguments are packed in a dictionary and sent off to the server via +``sessionhandler.oob_data_in()``. Finally, the portal session must +implement ``oob_data_out(data)`` to handle the data coming back from +Server. It will be a dictionary of return values keyed by the function +names. + +Example of out-of-band calling sequence +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Let's say we want our client to be able to request the character's +current health. In our Portal protocol we somehow parse the incoming +data stream and figure out what the request for health looks like. We +map this to the Evennia ``get_health`` function. + +We point ``settings.OOB_FUNC_MODULE`` to someplace in ``game/`` and +create a module there with the following function: + +:: + + # the caller is always added as first argument + # we also assume health is stored as a simple + # attribute on the character here. + def get_health(character): + return character.db.health + +Done, this function will do just what we want. Let's finish up the first +part of the portal protocol: + +:: + + # this method could be named differently depending on the + # protocol you are using (this is telnet) + def lineReceived(self, string): + # (does stuff to analyze the incoming string) outdict = + if GET_HEALTH: + # call get_health(char) + outdict["get_health"] = ([], ) + elif GET_MANA: + # call get_mana(char) + outdict["get_mana"] = ([], ) + elif GET_CONFIG: + # call get_config(char, 2, hidden=True) + outdict["get_config"] = ([2], 'hidden':True) [...] self.sessionhandler.oob_data_out(outdict) + +The server will properly accept this and call get\_health and get the +right value for the health. We need to define an ``oob_data_out(data)`` +in our portal protocol to catch the return value: + +:: + + def oob_data_out(self, data): + # the indata is a dicationary funcname:retval outstring = "" + for funcname, retval in data.items(): + if funcname == 'get_health': + # convert to the right format for sending back to client, store + # in outstring ... + [...] + +Above, once the dict is parsed and the return values properly put in a +format the client will understand, send the whole thing off using the +protocol's relevant send method. + +Implementing auto-sending +~~~~~~~~~~~~~~~~~~~~~~~~~ + +To have the Server update the client regularly, simply create a global +`Script `_ that upon each repeat creates the request +dictionary (basically faking a request from the portal) and sends it +directly to +``src.server.sessionhandler.oob_data_in(session.sessid, datadict)``. +Repeat for all sessions. All specified OOB functions are called as +normal and data will be sent back to be handled by the portal just as if +the portal initiated the request. + Assorted notes -------------- diff --git a/docs/sphinx/source/wiki/UpdatingYourGame.rst b/docs/sphinx/source/wiki/UpdatingYourGame.rst index 76d5d34d72..0f26289952 100644 --- a/docs/sphinx/source/wiki/UpdatingYourGame.rst +++ b/docs/sphinx/source/wiki/UpdatingYourGame.rst @@ -17,6 +17,7 @@ root directory and type: :: hg pull + hg update Assuming you've got the command line client. If you're using a graphical client, you will probably want to navigate to the ``evennia`` directory @@ -112,8 +113,8 @@ used (you have to give the ``mange.py migrate`` command as well as Once you have a database ready and using South, you work as normal. Whenever a new Evennia update tells you that the database schema has -changed (check ``hg log`` or the online list), you go to ``game/`` and -run this command: +changed (check ``hg log`` after you pulled the latest stuff, or read the +online list), you go to ``game/`` and run this command: :: diff --git a/game/evennia.py b/game/evennia.py index 9349920852..12d77e61dc 100755 --- a/game/evennia.py +++ b/game/evennia.py @@ -355,6 +355,10 @@ def handle_args(options, mode, service): errmsg = _("The %s does not seem to be running.") if mode == 'start': + + # launch the error checker. Best to catch the errors already here. + error_check_python_modules() + # starting one or many services if service == 'server': if inter: @@ -397,6 +401,42 @@ def handle_args(options, mode, service): kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False) return None +def error_check_python_modules(): + """ + Import settings modules in settings. This will raise exceptions on + pure python-syntax issues which are hard to catch gracefully + with exceptions in the engine (since they are formatting errors in + the python source files themselves). Best they fail already here + before we get any further. + """ + def imp(path, split=True): + mod, fromlist = path, "None" + if split: + mod, fromlist = path.rsplit('.', 1) + __import__(mod, fromlist=[fromlist]) + + # core modules + imp(settings.COMMAND_PARSER) + imp(settings.SEARCH_AT_RESULT) + imp(settings.SEARCH_AT_MULTIMATCH_INPUT) + imp(settings.CONNECTION_SCREEN_MODULE, split=False) + #imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False) + for path in settings.LOCK_FUNC_MODULES: + imp(path, split=False) + # cmdsets + from src.commands import cmdsethandler + cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None) + cmdsethandler.import_cmdset(settings.CMDSET_DEFAULT, None) + cmdsethandler.import_cmdset(settings.CMDSET_OOC, None) + # typeclasses + imp(settings.BASE_PLAYER_TYPECLASS) + imp(settings.BASE_OBJECT_TYPECLASS) + imp(settings.BASE_CHARACTER_TYPECLASS) + imp(settings.BASE_ROOM_TYPECLASS) + imp(settings.BASE_EXIT_TYPECLASS) + imp(settings.BASE_SCRIPT_TYPECLASS) + + def main(): """ This handles command line input. @@ -438,6 +478,7 @@ def main(): Popen(cmdstr) if __name__ == '__main__': + from src.utils.utils import check_evennia_dependencies if check_evennia_dependencies(): main() diff --git a/game/gamesrc/commands/basecmdset.py b/game/gamesrc/commands/basecmdset.py index 921d857e3d..3e95320848 100644 --- a/game/gamesrc/commands/basecmdset.py +++ b/game/gamesrc/commands/basecmdset.py @@ -24,6 +24,7 @@ from game.gamesrc.commands.basecommand import Command #from contrib import menusystem, lineeditor #from contrib import misc_commands +#from contrib import chargen, menu_login class DefaultCmdSet(cmdset_default.DefaultCmdSet): """ @@ -49,7 +50,7 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet): # #self.add(menusystem.CmdMenuTest()) #self.add(lineeditor.CmdEditor()) - #self.add(misc_commands.CmdQuell()) + #self.add(misc_commands.CmdQuell()) class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet): """ @@ -75,7 +76,6 @@ class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet): # any commands you add below will overload the default ones. # - class OOCCmdSet(cmdset_ooc.OOCCmdSet): """ This is set is available to the player when they have no @@ -92,9 +92,8 @@ class OOCCmdSet(cmdset_ooc.OOCCmdSet): # # any commands you add below will overload the default ones. - # + # - class BaseCmdSet(CmdSet): """ Implements an empty, example cmdset. diff --git a/game/gamesrc/commands/examples/cmdset_red_button.py b/game/gamesrc/commands/examples/cmdset_red_button.py index f9402ad9f9..111437f290 100644 --- a/game/gamesrc/commands/examples/cmdset_red_button.py +++ b/game/gamesrc/commands/examples/cmdset_red_button.py @@ -30,6 +30,7 @@ class CmdNudge(Command): key = "nudge lid" # two-word command name! aliases = ["nudge"] + locks = "cmd:all()" def func(self): """ @@ -54,6 +55,7 @@ class CmdPush(Command): """ key = "push button" aliases = ["push", "press button", "press"] + locks = "cmd:all()" def func(self): """ @@ -94,6 +96,7 @@ class CmdSmashGlass(Command): key = "smash glass" aliases = ["smash lid", "break lid", "smash"] + locks = "cmd:all()" def func(self): """ @@ -129,6 +132,7 @@ class CmdOpenLid(Command): key = "open lid" aliases = ["open button", 'open'] + locks = "cmd:all()" def func(self): "simply call the right function." @@ -159,6 +163,7 @@ class CmdCloseLid(Command): key = "close lid" aliases = ["close"] + locks = "cmd:all()" def func(self): "Close the lid" @@ -183,6 +188,8 @@ class CmdBlindLook(Command): key = "look" aliases = ["l", "get", "examine", "ex", "feel", "listen"] + locks = "cmd:all()" + def func(self): "This replaces all the senses when blinded." @@ -215,6 +222,8 @@ class CmdBlindHelp(Command): """ key = "help" aliases = "h" + locks = "cmd:all()" + def func(self): "Give a message." self.caller.msg("You are beyond help ... until you can see again.") diff --git a/game/gamesrc/objects/baseobjects.py b/game/gamesrc/objects/baseobjects.py index f4b9a9470f..689588f4cf 100644 --- a/game/gamesrc/objects/baseobjects.py +++ b/game/gamesrc/objects/baseobjects.py @@ -99,37 +99,13 @@ class Object(BaseObject): class Character(BaseCharacter): """ This is the default object created for a new user connecting - the - in-game player character representation. Note that it's important - that at_object_creation sets up an script that adds the Default - command set whenever the player logs in - otherwise they won't be - able to use any commands! + in-game player character representation. The basetype_setup always + assigns the default_cmdset as a fallback to objects of this type. + The default hooks also hide the character object away (by moving + it to a Null location whenever the player logs off (otherwise the + character would remain in the world, "headless" so to say). """ - - def at_disconnect(self): - """ - We stove away the character when logging off, otherwise the character object will - remain in the room also after the player logged off ("headless", so to say). - """ - if self.location: # have to check, in case of multiple connections closing - self.location.msg_contents("%s has left the game." % self.name) - self.db.prelogout_location = self.location - self.location = None - - def at_post_login(self): - """ - This recovers the character again after having been "stoved away" at disconnect. - """ - if self.db.prelogout_location: - # try to recover - self.location = self.db.prelogout_location - if self.location == None: - # make sure location is never None (home should always exist) - self.location = self.home - # save location again to be sure - self.db.prelogout_location = self.location - - self.location.msg_contents("%s has entered the game." % self.name) - self.location.at_object_receive(self, self.location) + pass class Room(BaseRoom): """ diff --git a/game/gamesrc/world/connection_screens.py b/game/gamesrc/world/connection_screens.py index 8698012828..79539b552c 100644 --- a/game/gamesrc/world/connection_screens.py +++ b/game/gamesrc/world/connection_screens.py @@ -7,16 +7,18 @@ # The names of the string variables doesn't matter (except they # shouldn't start with _), but each should hold a string defining a # connection screen - as seen when first connecting to the game -# (before having logged in). If there are more than one string -# variable defined, a random one is picked. +# (before having logged in). # -# After adding new connection screens to this module you must -# either reboot or reload the server to make them available. +# OBS - If there are more than one string variable viable in this +# module, a random one is picked! +# +# After adding new connection screens to this module you must either +# reboot or reload the server to make them available. # from src.commands.connection_screen import DEFAULT_SCREEN -# from src.utils import utils +#from src.utils import utils # # CUSTOM_SCREEN = \ # """{b=============================================================={n @@ -29,3 +31,12 @@ from src.commands.connection_screen import DEFAULT_SCREEN # # Enter {whelp{n for more info. {wlook{n will re-load this screen. #{b=============================================================={n""" % utils.get_evennia_version() + + +# # A suggested alternative screen for the Menu login system + +# from src.utils import utils +# MENU_SCREEN = \ +# """{b=============================================================={n +# Welcome to {gEvennnia{n, version %s! +# {b=============================================================={n""" % utils.get_evennia_version() diff --git a/src/commands/cmdhandler.py b/src/commands/cmdhandler.py index 2186ef2d69..c5cb40bf5b 100644 --- a/src/commands/cmdhandler.py +++ b/src/commands/cmdhandler.py @@ -8,9 +8,7 @@ command line. The process is as follows: 2) The system checks the state of the caller - loggedin or not 3) If no command string was supplied, we search the merged cmdset for system command CMD_NOINPUT and branches to execute that. --> Finished -4) Depending on the login/not state, it collects cmdsets from different sources: - not logged in - uses the single cmdset defined as settings.CMDSET_UNLOGGEDIN - normal - gathers command sets from many different sources (shown in dropping priority): +4) Cmdsets are gathered from different sources (in order of dropping priority): channels - all available channel names are auto-created into a cmdset, to allow for giving the channel name and have the following immediately sent to the channel. The sending is performed by the CMD_CHANNEL @@ -52,11 +50,17 @@ COMMAND_PARSER = utils.mod_import(*settings.COMMAND_PARSER.rsplit('.', 1)) # allow for custom behaviour when the command handler hits # special situations -- it then calls a normal Command # that you can customize! +# Import these variables and use them rather than trying +# to remember the actual string constants. CMD_NOINPUT = "__noinput_command" CMD_NOMATCH = "__nomatch_command" CMD_MULTIMATCH = "__multimatch_command" -CMD_CHANNEL = "__send_to_channel" +CMD_CHANNEL = "__send_to_channel_command" +# this is the name of the command the engine calls when the player +# connects. It is expected to show the login screen. +CMD_LOGINSTART = "__unloggedin_look_command" + class NoCmdSets(Exception): "No cmdsets found. Critical error." @@ -115,13 +119,14 @@ def get_and_merge_cmdsets(caller): try: player_cmdset = caller.player.cmdset.current except AttributeError: - player_cmdset = None + player_cmdset = None cmdsets = [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_objects_cmdsets # weed out all non-found sets cmdsets = [cmdset for cmdset in cmdsets if cmdset] # sort cmdsets after reverse priority (highest prio are merged in last) cmdsets = sorted(cmdsets, key=lambda x: x.priority) + if cmdsets: # Merge all command sets into one, beginning with the lowest-prio one cmdset = cmdsets.pop(0) @@ -131,7 +136,7 @@ def get_and_merge_cmdsets(caller): cmdset = merging_cmdset + cmdset else: cmdset = None - + for cset in (cset for cset in local_objects_cmdsets if cset): cset.duplicates = cset.old_duplicates @@ -140,27 +145,21 @@ def get_and_merge_cmdsets(caller): # Main command-handler function -def cmdhandler(caller, raw_string, unloggedin=False, testing=False): +def cmdhandler(caller, raw_string, testing=False): """ This is the main function to handle any string sent to the engine. caller - calling object raw_string - the command string given on the command line - unloggedin - if caller is an authenticated user or not testing - if we should actually execute the command or not. if True, the command instance will be returned instead. """ try: # catch bugs in cmdhandler itself try: # catch special-type commands - if unloggedin: - # not logged in, so it's just one cmdset we are interested in - cmdset = import_cmdset(settings.CMDSET_UNLOGGEDIN, caller) - else: - # We are logged in, collect all relevant cmdsets and merge - cmdset = get_and_merge_cmdsets(caller) + cmdset = get_and_merge_cmdsets(caller) - #print cmdset + # print cmdset if not cmdset: # this is bad and shouldn't happen. raise NoCmdSets @@ -171,12 +170,10 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False): syscmd = cmdset.get(CMD_NOINPUT) sysarg = "" raise ExecSystemCommand(syscmd, sysarg) - # Parse the input string and match to available cmdset. # This also checks for permissions, so all commands in match # are commands the caller is allowed to call. matches = COMMAND_PARSER(raw_string, cmdset, caller) - # Deal with matches if not matches: # No commands match our entered command diff --git a/src/commands/cmdparser.py b/src/commands/cmdparser.py index 4b4985f968..a71157954c 100644 --- a/src/commands/cmdparser.py +++ b/src/commands/cmdparser.py @@ -49,7 +49,6 @@ def cmdparser(raw_string, cmdset, caller, match_index=None): matches.extend([create_match(cmdname, raw_string, cmd) for cmdname in [cmd.key] + cmd.aliases if cmdname and l_raw_string.startswith(cmdname.lower())]) - if not matches: # no matches found. if '-' in raw_string: diff --git a/src/commands/cmdset.py b/src/commands/cmdset.py index fe61c08d48..b6e8b9cee9 100644 --- a/src/commands/cmdset.py +++ b/src/commands/cmdset.py @@ -221,7 +221,7 @@ class CmdSet(object): are made, rather later added commands will simply replace existing ones to make a unique set. """ - + if inherits_from(cmd, "src.commands.cmdset.CmdSet"): # this is a command set so merge all commands in that set # to this one. We are not protecting against recursive @@ -235,19 +235,19 @@ class CmdSet(object): string += "make sure they are not themself cyclically added to the new cmdset somewhere in the chain." raise RuntimeError(string % (cmd, self.__class__)) cmds = cmd.commands - elif not is_iter(cmd): - cmds = [instantiate(cmd)] + elif is_iter(cmd): + cmds = [instantiate(c) for c in cmd] else: - cmds = instantiate(cmd) + cmds = [instantiate(cmd)] for cmd in cmds: # add all commands if not hasattr(cmd, 'obj'): - cmd.obj = self.cmdsetobj + cmd.obj = self.cmdsetobj try: ic = self.commands.index(cmd) self.commands[ic] = cmd # replace except ValueError: - self.commands.append(cmd) + self.commands.append(cmd) # extra run to make sure to avoid doublets self.commands = list(set(self.commands)) #print "In cmdset.add(cmd):", self.key, cmd diff --git a/src/commands/cmdsethandler.py b/src/commands/cmdsethandler.py index 2395312da7..9025c3ca08 100644 --- a/src/commands/cmdsethandler.py +++ b/src/commands/cmdsethandler.py @@ -121,7 +121,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False): logger.log_trace() if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"): object.__getattribute__(emit_to_obj, "msg")(errstring) - #raise # have to raise, or we will not see any errors in some situations! + logger.log_errmsg("Error: %s" % errstring) + raise # have to raise, or we will not see any errors in some situations! # classes @@ -246,8 +247,8 @@ class CmdSetHandler(object): def add(self, cmdset, emit_to_obj=None, permanent=False): """ Add a cmdset to the handler, on top of the old ones. - Default is to not make this permanent (i.e. no script - will be added to add the cmdset every server start/login). + Default is to not make this permanent, i.e. the set + will not survive a server reset. cmdset - can be a cmdset object or the python path to such an object. diff --git a/src/commands/command.py b/src/commands/command.py index 2605586310..20eba4f9b9 100644 --- a/src/commands/command.py +++ b/src/commands/command.py @@ -129,7 +129,7 @@ class Command(object): previously extracted from the raw string by the system. cmdname is always lowercase when reaching this point. """ - return (cmdname == self.key) or (cmdname in self.aliases) + return cmdname and ((cmdname == self.key) or (cmdname in self.aliases)) def access(self, srcobj, access_type="cmd", default=False): """ diff --git a/src/commands/default/general.py b/src/commands/default/general.py index f48ab6bc78..026419d021 100644 --- a/src/commands/default/general.py +++ b/src/commands/default/general.py @@ -616,7 +616,7 @@ class CmdOOCLook(CmdLook): self.character = None if utils.inherits_from(self.caller, "src.objects.objects.Object"): # An object of some type is calling. Convert to player. - print self.caller, self.caller.__class__ + #print self.caller, self.caller.__class__ self.character = self.caller if hasattr(self.caller, "player"): self.caller = self.caller.player @@ -685,6 +685,15 @@ class CmdIC(MuxCommand): if caller.swap_character(new_character): new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name) caller.db.last_puppet = old_char + 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) @@ -720,11 +729,15 @@ class CmdOOC(MuxCommand): 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") diff --git a/src/commands/default/unloggedin.py b/src/commands/default/unloggedin.py index 3da12c66c4..e71f637c17 100644 --- a/src/commands/default/unloggedin.py +++ b/src/commands/default/unloggedin.py @@ -13,6 +13,7 @@ from src.comms.models import Channel from src.utils import create, logger, utils, ansi from src.commands.default.muxcommand import MuxCommand +from src.commands.cmdhandler import CMD_LOGINSTART CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE @@ -169,20 +170,10 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m session.msg("There was an error creating the default Character/Player. This error was logged. Contact an admin.") return new_player = new_character.player - - # character safety features - new_character.locks.delete("get") - new_character.locks.add("get:perm(Wizards)") - # allow the character itself and the player to puppet this character. - new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % - (new_character.id, new_player.id)) - # set a default description - new_character.db.desc = "This is a Player." - - new_character.db.FIRST_LOGIN = True - new_player = new_character.player - new_player.db.FIRST_LOGIN = True + # This needs to be called so the engine knows this player is logging in for the first time. + # (so it knows to call the right hooks during login later) + utils.init_new_player(new_player) # join the new player to the public channel pchanneldef = settings.CHANNEL_PUBLIC @@ -191,10 +182,20 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m if not pchannel.connect_to(new_player): string = "New player '%s' could not connect to public channel!" % new_player.key logger.log_errmsg(string) + + # allow only the character itself and the player to puppet this character (and Immortals). + new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" % + (new_character.id, new_player.id)) + + + # set a default description + new_character.db.desc = "This is a Player." + # tell the caller everything went well. string = "A new account '%s' was created with the email address %s. Welcome!" string += "\n\nYou can now log with the command 'connect %s '." session.msg(string % (playername, email, email)) + except Exception: # We are in the middle between logged in and -not, so we have to handle tracebacks # ourselves at this point. If we don't, we won't see any errors at all. @@ -221,10 +222,12 @@ class CmdQuit(MuxCommand): class CmdUnconnectedLook(MuxCommand): """ This is an unconnected version of the look command for simplicity. - All it does is re-show the connect screen. + + This is called by the server and kicks everything in gear. + All it does is display the connect screen. """ - key = "look" - aliases = "l" + key = CMD_LOGINSTART + aliases = ["look", "l"] locks = "cmd:all()" def func(self): diff --git a/src/objects/manager.py b/src/objects/manager.py index 51d98513e4..8a19129780 100644 --- a/src/objects/manager.py +++ b/src/objects/manager.py @@ -68,6 +68,13 @@ class ObjectManager(TypedObjectManager): # use the id to find the player return self.get_object_with_user(dbref) + @returns_typeclass_list + def get_objs_with_key_and_typeclass(self, oname, otypeclass_path): + """ + Returns objects based on simultaneous key and typeclass match. + """ + return self.filter(db_key__iexact=oname).filter(db_typeclass_path__exact=otypeclass_path) + # attr/property related @returns_typeclass_list diff --git a/src/objects/models.py b/src/objects/models.py index 10601deb45..d13555a292 100644 --- a/src/objects/models.py +++ b/src/objects/models.py @@ -316,7 +316,7 @@ class ObjectDB(TypedObject): string += "%s is not a valid home." self.msg(string % home) logger.log_trace(string) - raise + #raise self.save() #@home.deleter def home_del(self): diff --git a/src/objects/objects.py b/src/objects/objects.py index 160b379454..c99844115e 100644 --- a/src/objects/objects.py +++ b/src/objects/objects.py @@ -409,6 +409,34 @@ class Character(Object): def at_after_move(self, source_location): "Default is to look around after a move." self.execute_cmd('look') + + def at_disconnect(self): + """ + We stove away the character when logging off, otherwise the character object will + remain in the room also after the player logged off ("headless", so to say). + """ + if self.location: # have to check, in case of multiple connections closing + self.location.msg_contents("%s has left the game." % self.name, exclude=[self]) + self.db.prelogout_location = self.location + self.location = None + + def at_post_login(self): + """ + This recovers the character again after having been "stoved away" at disconnect. + """ + if self.db.prelogout_location: + # try to recover + self.location = self.db.prelogout_location + if self.location == None: + # make sure location is never None (home should always exist) + self.location = self.home + # save location again to be sure + self.db.prelogout_location = self.location + + self.location.msg_contents("%s has entered the game." % self.name, exclude=[self]) + self.location.at_object_receive(self, self.location) + + # # Base Room object diff --git a/src/server/amp.py b/src/server/amp.py index 7a740dd29e..6d2c21ff46 100644 --- a/src/server/amp.py +++ b/src/server/amp.py @@ -34,10 +34,6 @@ SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart") # i18n from django.utils.translation import ugettext as _ -# Signals - - - def get_restart_mode(restart_file): """ @@ -141,6 +137,24 @@ class MsgServer2Portal(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] +class OOBPortal2Server(amp.Command): + """ + OOB data portal -> server + """ + arguments = [('sessid', amp.Integer()), + ('data', amp.String())] + errors = [(Exception, "EXCEPTION")] + response = [] + +class OOBServer2Portal(amp.Command): + """ + OOB data server -> portal + """ + arguments = [('sessid', amp.Integer()), + ('data', amp.String())] + errors = [(Exception, "EXCEPTION")] + response = [] + class ServerAdmin(amp.Command): """ Portal -> Server @@ -168,6 +182,8 @@ class PortalAdmin(amp.Command): errors = [(Exception, 'EXCEPTION')] response = [] +dumps = lambda data: utils.to_str(pickle.dumps(data, pickle.HIGHEST_PROTOCOL)) +loads = lambda data: pickle.loads(utils.to_str(data)) #------------------------------------------------------------ # Core AMP protocol for communication Server <-> Portal @@ -220,7 +236,7 @@ class AMPProtocol(amp.AMP): Relays message to server. This method is executed on the Server. """ #print "msg portal -> server (server side):", sessid, msg - self.factory.server.sessions.data_in(sessid, msg, pickle.loads(utils.to_str(data))) + self.factory.server.sessions.data_in(sessid, msg, loads(data)) return {} MsgPortal2Server.responder(amp_msg_portal2server) @@ -232,7 +248,7 @@ class AMPProtocol(amp.AMP): self.callRemote(MsgPortal2Server, sessid=sessid, msg=msg, - data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server") + data=dumps(data)).addErrback(self.errback, "MsgPortal2Server") # Server -> Portal message @@ -241,7 +257,7 @@ class AMPProtocol(amp.AMP): Relays message to Portal. This method is executed on the Portal. """ #print "msg server->portal (portal side):", sessid, msg - self.factory.portal.sessions.data_out(sessid, msg, pickle.loads(utils.to_str(data))) + self.factory.portal.sessions.data_out(sessid, msg, loads(data)) return {} MsgServer2Portal.responder(amp_msg_server2portal) @@ -253,8 +269,50 @@ class AMPProtocol(amp.AMP): self.callRemote(MsgServer2Portal, sessid=sessid, msg=utils.to_str(msg), - data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgServer2Portal") + data=dumps(data)).addErrback(self.errback, "OOBServer2Portal") + # OOB Portal -> Server + + # Portal -> Server Msg + + def amp_oob_portal2server(self, sessid, data): + """ + Relays out-of-band data to server. This method is executed on the Server. + """ + #print "oob portal -> server (server side):", sessid, loads(data) + self.factory.server.sessions.oob_data_in(sessid, loads(data)) + return {} + OOBPortal2Server.responder(amp_oob_portal2server) + + def call_remote_OOBPortal2Server(self, sessid, data=""): + """ + Access method called by the Portal and executed on the Portal. + """ + #print "oob portal->server (portal side):", sessid, data + self.callRemote(OOBPortal2Server, + sessid=sessid, + data=dumps(data)).addErrback(self.errback, "OOBPortal2Server") + + # Server -> Portal message + + def amp_oob_server2portal(self, sessid, data): + """ + Relays out-of-band data to Portal. This method is executed on the Portal. + """ + #print "oob server->portal (portal side):", sessid, data + self.factory.portal.sessions.oob_data_out(sessid, loads(data)) + return {} + OOBServer2Portal.responder(amp_oob_server2portal) + + def call_remote_OOBServer2Portal(self, sessid, data=""): + """ + Access method called by the Server and executed on the Server. + """ + #print "oob server->portal (server side):", sessid, data + self.callRemote(OOBServer2Portal, + sessid=sessid, + data=dumps(data)).addErrback(self.errback, "OOBServer2Portal") + # Server administration from the Portal side @@ -264,7 +322,7 @@ class AMPProtocol(amp.AMP): operations on the server. This is executed on the Server. """ - data = pickle.loads(utils.to_str(data)) + data = loads(data) #print "serveradmin (server side):", sessid, operation, data @@ -276,7 +334,7 @@ class AMPProtocol(amp.AMP): if sess.logged_in and sess.uid: # this can happen in the case of auto-authenticating protocols like SSH sess.player = PlayerDB.objects.get_player_from_uid(sess.uid) - sess.at_sync() # this runs initialization without acr + sess.at_sync() # this runs initialization without acr self.factory.server.sessions.portal_connect(sessid, sess) @@ -315,7 +373,7 @@ class AMPProtocol(amp.AMP): Access method called by the Portal and Executed on the Portal. """ #print "serveradmin (portal side):", sessid, operation, data - data = utils.to_str(pickle.dumps(data)) + data = dumps(data) self.callRemote(ServerAdmin, sessid=sessid, @@ -329,7 +387,7 @@ class AMPProtocol(amp.AMP): This allows the server to perform admin operations on the portal. This is executed on the Portal. """ - data = pickle.loads(utils.to_str(data)) + data = loads(data) #print "portaladmin (portal side):", sessid, operation, data if operation == 'SLOGIN': # 'server_session_login' @@ -376,7 +434,7 @@ class AMPProtocol(amp.AMP): Access method called by the server side. """ #print "portaladmin (server side):", sessid, operation, data - data = utils.to_str(pickle.dumps(data)) + data = dumps(data) self.callRemote(PortalAdmin, sessid=sessid, diff --git a/src/server/initial_setup.py b/src/server/initial_setup.py index 0000d30989..fe62151e62 100644 --- a/src/server/initial_setup.py +++ b/src/server/initial_setup.py @@ -59,8 +59,7 @@ def create_objects(): character_typeclass=character_typeclass) if not god_character: - print _("#1 could not be created. Check the Player/Character typeclass for bugs.") - raise Exception + raise Exception(_("#1 could not be created. Check the Player/Character typeclass for bugs.")) god_character.id = 1 god_character.db.desc = _('This is User #1.') diff --git a/src/server/serversession.py b/src/server/serversession.py index e6eae3ac2f..51191ea108 100644 --- a/src/server/serversession.py +++ b/src/server/serversession.py @@ -12,12 +12,16 @@ from datetime import datetime from django.conf import settings from src.scripts.models import ScriptDB from src.comms.models import Channel -from src.utils import logger -from src.commands import cmdhandler +from src.utils import logger, utils +from src.commands import cmdhandler, cmdsethandler +from src.server.session import Session IDLE_COMMAND = settings.IDLE_COMMAND - -from src.server.session import Session + +# load optional out-of-band function module +OOB_FUNC_MODULE = settings.OOB_FUNC_MODULE +if OOB_FUNC_MODULE: + OOB_FUNC_MODULE = utils.mod_import(settings.OOB_FUNC_MODULE) # i18n from django.utils.translation import ugettext as _ @@ -37,7 +41,6 @@ class ServerSession(Session): through their session. """ - def at_sync(self): """ This is called whenever a session has been resynced with the portal. @@ -48,12 +51,18 @@ class ServerSession(Session): the session as it was. """ if not self.logged_in: + # assign the unloggedin-command set. + self.cmdset = cmdsethandler.CmdSetHandler(self) + self.cmdset_storage = [settings.CMDSET_UNLOGGEDIN] + self.cmdset.update(init_mode=True) + 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): """ Startup mechanisms that need to run at login. This is called @@ -87,11 +96,10 @@ class ServerSession(Session): player.at_pre_login() character = player.character - #print "at_init() - character" - character.at_init() 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 @@ -181,7 +189,7 @@ 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() @@ -193,8 +201,9 @@ class ServerSession(Session): # 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 special unlogged-in call. - cmdhandler.cmdhandler(self, command_string, unloggedin=True) + # we are not logged in. Use the session directly + # (it uses the settings.UNLOGGEDIN cmdset) + cmdhandler.cmdhandler(self, command_string) self.update_session_counters() def data_out(self, msg, data=None): @@ -203,9 +212,57 @@ class ServerSession(Session): """ self.sessionhandler.data_out(self, msg, data) + + def oob_data_in(self, data): + """ + This receives out-of-band data from the Portal. + + This method parses the data input (a dict) and uses + it to launch correct methods from those plugged into + the system. + + data = {funcname: ( [args], {kwargs]), + funcname: ( [args], {kwargs}), ...} + + example: + data = {"get_hp": ([], {}), + "update_counter", (["counter1"], {"now":True}) } + """ + + print "server: " + outdata = {} + + entity = self.get_character() + if not entity: + entity = self.get_player() + if not entity: + entity = self + + for funcname, argtuple in data.items(): + # loop through the data, calling available functions. + func = OOB_FUNC_MODULE.__dict__.get(funcname, None) + if func: + try: + outdata[funcname] = func(entity, *argtuple[0], **argtuple[1]) + except Exception: + logger.log_trace() + else: + logger.log_errmsg("oob_data_in error: funcname '%s' not found in OOB_FUNC_MODULE." % funcname) + if outdata: + self.oob_data_out(outdata) + + + def oob_data_out(self, data): + """ + This sends data from Server to the Portal across the AMP connection. + """ + self.sessionhandler.oob_data_out(self, data) + + def __eq__(self, other): return self.address == other.address + def __str__(self): """ String representation of the user session class. We use @@ -239,3 +296,57 @@ class ServerSession(Session): def msg(self, string='', data=None): "alias for at_data_out" self.data_out(string, data=data) + + + # Dummy API hooks for use a non-loggedin operation + + def at_cmdset_get(self): + "dummy hook all objects with cmdsets need to have" + pass + + # Mock db/ndb properties for allowing easy storage on the session + # (note that no databse is involved at all here. session.db.attr = + # value just saves a normal property in memory, just like ndb). + + #@property + def ndb_get(self): + """ + A non-persistent store (ndb: NonDataBase). Everything stored + to this is guaranteed to be cleared when a server is shutdown. + Syntax is same as for the _get_db_holder() method and + property, e.g. obj.ndb.attr = value etc. + """ + try: + return self._ndb_holder + except AttributeError: + class NdbHolder(object): + "Holder for storing non-persistent attributes." + def all(self): + return [val for val in self.__dict__.keys() + if not val.startswith['_']] + def __getattribute__(self, key): + # return None if no matching attribute was found. + try: + return object.__getattribute__(self, key) + except AttributeError: + return None + self._ndb_holder = NdbHolder() + return self._ndb_holder + #@ndb.setter + def ndb_set(self, value): + "Stop accidentally replacing the db object" + string = "Cannot assign directly to ndb object! " + string = "Use ndb.attr=value instead." + raise Exception(string) + #@ndb.deleter + def ndb_del(self): + "Stop accidental deletion." + raise Exception("Cannot delete the ndb object!") + ndb = property(ndb_get, ndb_set, ndb_del) + db = property(ndb_get, ndb_set, ndb_del) + + # Mock access method for the session (there is no lock info + # at this stage, so we just present a uniform API) + def access(self, *args, **kwargs): + "Dummy method." + return True diff --git a/src/server/session.py b/src/server/session.py index 519450058b..ed954c0bdf 100644 --- a/src/server/session.py +++ b/src/server/session.py @@ -123,3 +123,20 @@ class Session(object): """ pass + def oob_data_out(self, data): + """ + for Portal, this receives out-of-band data from Server across the AMP. + for Server, this sends out-of-band data to Portal. + + data is a dictionary + """ + pass + + def oob_data_in(self, data): + """ + for Portal, this sends out-of-band requests to Server over the AMP. + for Server, this receives data from Portal. + + data is a dictionary + """ + pass diff --git a/src/server/sessionhandler.py b/src/server/sessionhandler.py index 952458a8fa..9eb16bb014 100644 --- a/src/server/sessionhandler.py +++ b/src/server/sessionhandler.py @@ -19,6 +19,8 @@ from django.contrib.auth.models import User from src.server.models import ServerConfig from src.utils import utils +from src.commands.cmdhandler import CMD_LOGINSTART + # i18n from django.utils.translation import ugettext as _ @@ -94,7 +96,7 @@ class ServerSessionHandler(SessionHandler): Creates a new, unlogged-in game session. """ self.sessions[sessid] = session - session.execute_cmd('look') + session.execute_cmd(CMD_LOGINSTART) def portal_disconnect(self, sessid): """ @@ -293,6 +295,20 @@ class ServerSessionHandler(SessionHandler): # to put custom effects on the server due to data input, e.g. # from a custom client. + def oob_data_in(self, sessid, data): + """ + OOB (Out-of-band) Data Portal -> Server + """ + session = self.sessions.get(sessid, None) + if session: + session.oob_data_in(data) + + def oob_data_out(self, session, data): + """ + OOB (Out-of-band) Data Server -> Portal + """ + self.server.amp_protocol.call_remote_OOBServer2Portal(session.sessid, + data=data) #------------------------------------------------------------ # Portal-SessionHandler class @@ -390,5 +406,20 @@ class PortalSessionHandler(SessionHandler): if session: session.data_out(string, data=data) + def oob_data_in(self, session, data): + """ + OOB (Out-of-band) data Portal -> Server + """ + self.portal.amp_protocol.call_remote_OOBPortal2Server(session.sessid, + data=data) + + def oob_data_out(self, sessid, data): + """ + OOB (Out-of-band) data Server -> Portal + """ + session = self.sessions.get(sessid, None) + if session: + session.oob_data_out(data) + SESSIONS = ServerSessionHandler() PORTAL_SESSIONS = PortalSessionHandler() diff --git a/src/settings_default.py b/src/settings_default.py index 965224f3b9..2afef767c9 100644 --- a/src/settings_default.py +++ b/src/settings_default.py @@ -167,6 +167,12 @@ AT_INITIAL_SETUP_HOOK_MODULE = "game.gamesrc.world.at_initial_setup" ################################################### # Default command sets ################################################### +# Note that with the exception of the unloggedin set (which is not +# stored anywhere), changing these paths will only affect NEW created +# characters, not those already in play. So if you plan to change +# this, it's recommended you do it on a pristine setup only. To +# dynamically add new commands to a running server, extend/overload +# these existing sets instead. # Command set used before player has logged in CMDSET_UNLOGGEDIN = "game.gamesrc.commands.basecmdset.UnloggedinCmdSet" @@ -246,6 +252,8 @@ PERMISSION_PLAYER_DEFAULT = "Players" # Tuple of modules implementing lock functions. All callable functions # inside these modules will be available as lock functions. LOCK_FUNC_MODULES = ("src.locks.lockfuncs",) +# Module holding server-side functions for out-of-band protocols to call. +OOB_FUNC_MODULE = "" ################################################### diff --git a/src/utils/create.py b/src/utils/create.py index 05e90e8bba..2b57b5c704 100644 --- a/src/utils/create.py +++ b/src/utils/create.py @@ -73,6 +73,7 @@ def create_object(typeclass, key=None, location=None, # this will either load the typeclass or the default one new_object = new_db_object.typeclass + if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True): # this will fail if we gave a typeclass as input and it still gave us a default SharedMemoryModel.delete(new_db_object) @@ -105,6 +106,10 @@ def create_object(typeclass, key=None, location=None, # perform a move_to in order to display eventual messages. if home: new_object.home = home + else: + new_object.home = settings.CHARACTER_DEFAULT_HOME + + if location: new_object.move_to(location, quiet=True) else: @@ -389,6 +394,8 @@ def create_player(name, email, password, from src.players.models import PlayerDB from src.players.player import Player + if not email: + email = "dummy@dummy.com" if user: new_user = user else: diff --git a/src/utils/utils.py b/src/utils/utils.py index 3ec23a1f83..686963a968 100644 --- a/src/utils/utils.py +++ b/src/utils/utils.py @@ -542,7 +542,7 @@ def has_parent(basepath, obj): def mod_import(mod_path, propname=None): """ - Takes filename of a module, converts it to a python path + Takes filename of a module (a python path or a full pathname) and imports it. If property is given, return the named property from this module instead of the module itself. """ @@ -624,3 +624,15 @@ def string_from_module(modpath, variable=None): if not mvars: return None return mvars[random.randint(0, len(mvars)-1)] + +def init_new_player(player): + """ + Helper method to call all hooks, set flags etc on a newly created + player (and potentially their character, if it exists already) + """ + # the FIRST_LOGIN flags are necessary for the system to call + # the relevant first-login hooks. + if player.character: + player.character.db.FIRST_LOGIN = True + player.db.FIRST_LOGIN = True +