Changed the way unloggedin commands work. Rather than the cmdhander having a special state for unlogged-in commands, the session itself simply stores the cmdset from settings.CMDSET_UNLOGGEDIN. Clean and efficient and also gives a lot more freedom for creating custom login mechanisms (notably it opens the door to using menu systems).

This commit is contained in:
Griatch 2011-11-06 17:38:29 +01:00
parent 0ed692c19c
commit 2b2d27ed39
10 changed files with 119 additions and 77 deletions

View file

@ -48,7 +48,7 @@ class CmdMenuNode(Command):
locks = "cmd:all()" locks = "cmd:all()"
help_category = "Menu" help_category = "Menu"
menutree = None menutree = None
code = None code = None
def func(self): def func(self):
@ -127,8 +127,7 @@ class MenuCmdSet(CmdSet):
mergetype = "Replace" mergetype = "Replace"
def at_cmdset_creation(self): def at_cmdset_creation(self):
"populate cmdset" "populate cmdset"
self.add(CmdMenuLook()) pass
self.add(CmdMenuHelp())
# #
# Menu Node system # Menu Node system
@ -218,18 +217,27 @@ class MenuNode(object):
""" """
def __init__(self, key, text="", links=None, linktexts=None, 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):
""" """
key - the unique identifier of this node. key - the unique identifier of this node.
text - is the text that will be displayed at top when viewing 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. links - a list of keys for unique menunodes this is connected to. The actual keys will not be
linktexts - a list of texts to describe the links. If defined, need to match links list printed - keywords will be used (or a number)
keywords - a list of unique keys for choosing links. Must match links list. If not given, index numbers will be used. 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. 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. 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. 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 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. 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
""" """
self.key = key self.key = key
self.cmdset = None self.cmdset = None
@ -237,23 +245,31 @@ class MenuNode(object):
self.linktexts = linktexts self.linktexts = linktexts
self.keywords = keywords self.keywords = keywords
self.cols = cols self.cols = cols
self.selectcmds = selectcmds
self.code = code self.code = code
self.nodefaultcmds = nodefaultcmds
Nlinks = len(self.links)
# validate the input # validate the input
if not self.links: if not self.links:
self.links = [] self.links = []
if not self.linktexts or (self.linktexts and len(self.linktexts) != len(self.links)): if not self.linktexts or (len(self.linktexts) != Nlinks):
self.linktexts = [] self.linktexts = [None for i in range(Nlinks)]
if not self.keywords or (self.keywords and len(self.keywords) != len(self.links)): if not self.keywords or (len(self.keywords) != Nlinks):
self.keywords = [] 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 # Format default text for the menu-help command
if not helptext: if not helptext:
helptext = "Select one of the valid options" helptext = "Select one of the valid options ("
if self.keywords: for i in range(Nlinks):
helptext += " (" + ", ".join(self.keywords) + ")" if self.keywords[i]:
elif self.links: if self.keywords[i] not in (CMD_NOMATCH, CMD_NOINPUT):
helptext += " (" + ", ".join([str(i + 1) for i in range(len(self.links))]) + ")" helptext += "%s, " % self.keywords[i]
else:
helptext += "%s, " % (i + 1)
helptext = helptext.rstrip(", ") + ")"
self.helptext = helptext self.helptext = helptext
# Format text display # Format text display
@ -264,12 +280,14 @@ class MenuNode(object):
# format the choices into as many collumns as specified # format the choices into as many collumns as specified
choices = [] choices = []
for ilink, link in enumerate(self.links): for ilink, link in enumerate(self.links):
if self.keywords: choice = ""
choice = "{g%s{n" % self.keywords[ilink] if self.keywords[ilink]:
if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT):
choice += "{g%s{n" % self.keywords[ilink]
else: else:
choice = "{g%i{n" % (ilink + 1) choice += "{g %i{n" % (ilink + 1)
if self.linktexts: if self.linktexts[ilink]:
choice += "-%s" % self.linktexts[ilink] choice += " - %s" % self.linktexts[ilink]
choices.append(choice) choices.append(choice)
cols = [[] for i in range(min(len(choices), cols))] cols = [[] for i in range(min(len(choices), cols))]
while True: while True:
@ -282,9 +300,9 @@ class MenuNode(object):
break break
ftable = utils.format_table(cols) ftable = utils.format_table(cols)
for row in ftable: for row in ftable:
string += "\n" + "".join(row) string +="\n" + "".join(row)
# store text # store text
self.text = 78*"-" + "\n" + string.strip() self.text = 78*"-" + "\n" + string.rstrip()
def init(self, menutree): def init(self, menutree):
""" """
@ -292,15 +310,24 @@ class MenuNode(object):
""" """
# Create the relevant cmdset # Create the relevant cmdset
self.cmdset = MenuCmdSet() self.cmdset = MenuCmdSet()
for i, link in enumerate(self.links): if not self.nodefaultcmds:
cmd = CmdMenuNode() # add default menu commands
cmd.key = str(i + 1) 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 cmd.menutree = menutree
# this is the operable command, it moves us to the next node. if self.keywords[i] and cmd.key not in (CMD_NOMATCH, CMD_NOINPUT):
cmd.code = "self.menutree.goto('%s')" % link
if self.keywords:
cmd.aliases = [self.keywords[i]] cmd.aliases = [self.keywords[i]]
self.cmdset.add(cmd) self.cmdset.add(cmd)
def __str__(self): def __str__(self):
"Returns the string representation." "Returns the string representation."

View file

@ -22,7 +22,7 @@ from src.commands.cmdset import CmdSet
from src.commands.default import cmdset_default, cmdset_unloggedin, cmdset_ooc from src.commands.default import cmdset_default, cmdset_unloggedin, cmdset_ooc
from game.gamesrc.commands.basecommand import Command from game.gamesrc.commands.basecommand import Command
#from contrib import menusystem, lineeditor from contrib import menusystem, lineeditor
#from contrib import misc_commands #from contrib import misc_commands
class DefaultCmdSet(cmdset_default.DefaultCmdSet): class DefaultCmdSet(cmdset_default.DefaultCmdSet):
@ -47,9 +47,9 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet):
# #
# any commands you add below will overload the default ones. # any commands you add below will overload the default ones.
# #
#self.add(menusystem.CmdMenuTest()) self.add(menusystem.CmdMenuTest())
#self.add(lineeditor.CmdEditor()) #self.add(lineeditor.CmdEditor())
#self.add(misc_commands.CmdQuell()) #self.add(misc_commands.CmdQuell())
class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet): class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
""" """
@ -75,7 +75,6 @@ class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
# any commands you add below will overload the default ones. # any commands you add below will overload the default ones.
# #
class OOCCmdSet(cmdset_ooc.OOCCmdSet): class OOCCmdSet(cmdset_ooc.OOCCmdSet):
""" """
This is set is available to the player when they have no This is set is available to the player when they have no

View file

@ -8,9 +8,7 @@ command line. The process is as follows:
2) The system checks the state of the caller - loggedin or not 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 3) If no command string was supplied, we search the merged cmdset for system command CMD_NOINPUT
and branches to execute that. --> Finished and branches to execute that. --> Finished
4) Depending on the login/not state, it collects cmdsets from different sources: 4) Cmdsets are gathered from different sources (in order of dropping priority):
not logged in - uses the single cmdset defined as settings.CMDSET_UNLOGGEDIN
normal - gathers command sets from many different sources (shown in dropping priority):
channels - all available channel names are auto-created into a cmdset, to allow channels - all available channel names are auto-created into a cmdset, to allow
for giving the channel name and have the following immediately for giving the channel name and have the following immediately
sent to the channel. The sending is performed by the CMD_CHANNEL sent to the channel. The sending is performed by the CMD_CHANNEL
@ -140,26 +138,20 @@ def get_and_merge_cmdsets(caller):
# Main command-handler function # 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. This is the main function to handle any string sent to the engine.
caller - calling object caller - calling object
raw_string - the command string given on the command line 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. testing - if we should actually execute the command or not.
if True, the command instance will be returned instead. if True, the command instance will be returned instead.
""" """
try: # catch bugs in cmdhandler itself try: # catch bugs in cmdhandler itself
try: # catch special-type commands try: # catch special-type commands
if unloggedin: cmdset = get_and_merge_cmdsets(caller)
# 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)
#print cmdset #print cmdset
if not cmdset: if not cmdset:
# this is bad and shouldn't happen. # this is bad and shouldn't happen.

View file

@ -221,7 +221,7 @@ class CmdSet(object):
are made, rather later added commands will simply replace are made, rather later added commands will simply replace
existing ones to make a unique set. existing ones to make a unique set.
""" """
if inherits_from(cmd, "src.commands.cmdset.CmdSet"): if inherits_from(cmd, "src.commands.cmdset.CmdSet"):
# this is a command set so merge all commands in that set # this is a command set so merge all commands in that set
# to this one. We are not protecting against recursive # 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." string += "make sure they are not themself cyclically added to the new cmdset somewhere in the chain."
raise RuntimeError(string % (cmd, self.__class__)) raise RuntimeError(string % (cmd, self.__class__))
cmds = cmd.commands cmds = cmd.commands
elif not is_iter(cmd): elif is_iter(cmd):
cmds = [instantiate(cmd)] cmds = [instantiate(c) for c in cmd]
else: else:
cmds = instantiate(cmd) cmds = [instantiate(cmd)]
for cmd in cmds: for cmd in cmds:
# add all commands # add all commands
if not hasattr(cmd, 'obj'): if not hasattr(cmd, 'obj'):
cmd.obj = self.cmdsetobj cmd.obj = self.cmdsetobj
try: ic = [i for i, oldcmd in enumerate(self.commands) if oldcmd.match(cmd)]
ic = self.commands.index(cmd) if ic:
self.commands[ic] = cmd # replace self.commands[ic[0]] = cmd # replace
except ValueError: else:
self.commands.append(cmd) self.commands.append(cmd)
# extra run to make sure to avoid doublets # extra run to make sure to avoid doublets
self.commands = list(set(self.commands)) self.commands = list(set(self.commands))
#print "In cmdset.add(cmd):", self.key, cmd #print "In cmdset.add(cmd):", self.key, cmd

View file

@ -129,7 +129,7 @@ class Command(object):
previously extracted from the raw string by the system. previously extracted from the raw string by the system.
cmdname is always lowercase when reaching this point. 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): def access(self, srcobj, access_type="cmd", default=False):
""" """

View file

@ -169,20 +169,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.") session.msg("There was an error creating the default Character/Player. This error was logged. Contact an admin.")
return return
new_player = new_character.player 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 # This needs to be called so the engine knows this player is logging in for the first time.
new_character.db.desc = "This is a Player." # (so it knows to call the right hooks during login later)
utils.init_new_player(player)
new_character.db.FIRST_LOGIN = True
new_player = new_character.player
new_player.db.FIRST_LOGIN = True
# join the new player to the public channel # join the new player to the public channel
pchanneldef = settings.CHANNEL_PUBLIC pchanneldef = settings.CHANNEL_PUBLIC
@ -191,10 +181,20 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
if not pchannel.connect_to(new_player): if not pchannel.connect_to(new_player):
string = "New player '%s' could not connect to public channel!" % new_player.key string = "New player '%s' could not connect to public channel!" % new_player.key
logger.log_errmsg(string) 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 = "A new account '%s' was created with the email address %s. Welcome!"
string += "\n\nYou can now log with the command 'connect %s <your password>'." string += "\n\nYou can now log with the command 'connect %s <your password>'."
session.msg(string % (playername, email, email)) session.msg(string % (playername, email, email))
except Exception: except Exception:
# We are in the middle between logged in and -not, so we have to handle tracebacks # 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. # ourselves at this point. If we don't, we won't see any errors at all.

View file

@ -276,7 +276,7 @@ class AMPProtocol(amp.AMP):
if sess.logged_in and sess.uid: if sess.logged_in and sess.uid:
# this can happen in the case of auto-authenticating protocols like SSH # this can happen in the case of auto-authenticating protocols like SSH
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid) 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) self.factory.server.sessions.portal_connect(sessid, sess)

View file

@ -13,7 +13,7 @@ from django.conf import settings
from src.scripts.models import ScriptDB from src.scripts.models import ScriptDB
from src.comms.models import Channel from src.comms.models import Channel
from src.utils import logger from src.utils import logger
from src.commands import cmdhandler from src.commands import cmdhandler, cmdsethandler
IDLE_COMMAND = settings.IDLE_COMMAND IDLE_COMMAND = settings.IDLE_COMMAND
@ -37,7 +37,6 @@ class ServerSession(Session):
through their session. through their session.
""" """
def at_sync(self): def at_sync(self):
""" """
This is called whenever a session has been resynced with the portal. This is called whenever a session has been resynced with the portal.
@ -48,12 +47,22 @@ class ServerSession(Session):
the session as it was. the session as it was.
""" """
if not self.logged_in: 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 return
character = self.get_character() character = self.get_character()
if character: if character:
# start (persistent) scripts on this object # start (persistent) scripts on this object
ScriptDB.objects.validate(obj=character) ScriptDB.objects.validate(obj=character)
def at_cmdset_get(self):
"dummy hook all objects with cmdsets need to have"
pass
def session_login(self, player): def session_login(self, player):
""" """
Startup mechanisms that need to run at login. This is called Startup mechanisms that need to run at login. This is called
@ -193,8 +202,9 @@ class ServerSession(Session):
# there is no character, but we are logged in. Use player instead. # there is no character, but we are logged in. Use player instead.
self.get_player().execute_cmd(command_string) self.get_player().execute_cmd(command_string)
else: else:
# we are not logged in. Use special unlogged-in call. # we are not logged in. Use the session directly
cmdhandler.cmdhandler(self, command_string, unloggedin=True) # (it uses the settings.UNLOGGEDIN cmdset)
cmdhandler.cmdhandler(self, command_string)
self.update_session_counters() self.update_session_counters()
def data_out(self, msg, data=None): def data_out(self, msg, data=None):

View file

@ -389,6 +389,8 @@ def create_player(name, email, password,
from src.players.models import PlayerDB from src.players.models import PlayerDB
from src.players.player import Player from src.players.player import Player
if not email:
email = "dummy@dummy.com"
if user: if user:
new_user = user new_user = user
else: else:

View file

@ -624,3 +624,15 @@ def string_from_module(modpath, variable=None):
if not mvars: if not mvars:
return None return None
return mvars[random.randint(0, len(mvars)-1)] 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