This commit is contained in:
Griatch 2011-11-11 01:20:51 +01:00
commit ce0e3c4857
32 changed files with 1208 additions and 166 deletions

199
contrib/chargen.py Normal file
View file

@ -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 <character>
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 <character name>{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 <name>{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 <character name>
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 <character name>")
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())

332
contrib/menu_login.py Normal file
View file

@ -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()

View file

@ -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
@ -150,15 +149,19 @@ class MenuTree(object):
'START' and 'END' respectively. '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 We specify startnode/endnode so that the system knows where to
enter and where to exit the menu tree. If nodes is given, it 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. 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.tree = {}
self.startnode = startnode self.startnode = startnode
self.endnode = endnode self.endnode = endnode
self.exec_end = exec_end
self.caller = caller self.caller = caller
if nodes and utils.is_iter(nodes): if nodes and utils.is_iter(nodes):
for node in nodes: for node in nodes:
@ -187,7 +190,8 @@ class MenuTree(object):
# if we was given the END node key, we clean up immediately. # if we was given the END node key, we clean up immediately.
self.caller.cmdset.delete("menucmdset") self.caller.cmdset.delete("menucmdset")
del self.caller.db._menu_data del self.caller.db._menu_data
self.caller.execute_cmd("look") if self.exec_end != None:
self.caller.execute_cmd(self.exec_end)
return return
# not exiting, look for a valid code. # not exiting, look for a valid code.
node = self.tree.get(key, None) node = self.tree.get(key, None)
@ -218,18 +222,28 @@ 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, separator=""):
""" """
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
separator - this string will be put on the line between menu nodes5B.
""" """
self.key = key self.key = key
self.cmdset = None self.cmdset = None
@ -237,23 +251,32 @@ 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
self.separator = separator
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 +287,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 +307,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 = self.separator + "\n" + string.rstrip()
def init(self, menutree): def init(self, menutree):
""" """
@ -292,13 +317,22 @@ class MenuNode(object):
""" """
# Create the relevant cmdset # Create the relevant cmdset
self.cmdset = MenuCmdSet() self.cmdset = MenuCmdSet()
if not self.nodefaultcmds:
# add default menu commands
self.cmdset.add(CmdMenuLook())
self.cmdset.add(CmdMenuHelp())
for i, link in enumerate(self.links): for i, link in enumerate(self.links):
cmd = CmdMenuNode() if self.selectcmds[i]:
cmd.key = str(i + 1) 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)

View file

@ -616,6 +616,29 @@ command must be added to a cmdset as well before it will work.
def func(self): def func(self):
self.caller.msg("Don't just press return like that, talk to me!") 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.html>`_ 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 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. the command, actually doing useful things.
Call ``at_post_command()`` on the command instance. 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).

View file

@ -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 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 ... 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 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. be.
Evennia is in principle a MUD-building system: a bare-bones Python 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 What we *do* however, is to provide a solid foundation for all the
boring database, networking, and behind-the-scenes administration stuff boring database, networking, and behind-the-scenes administration stuff
that all online games need whether they like it or not. Evennia is by that all online games need whether they like it or not. Evennia is
default *fully persistent*, that means things you drop on the ground *fully persistent*, that means things you drop on the ground somewhere
somewhere will still be there a dozen server reboots later. Through will still be there a dozen server reboots later. Through Django we
Django, we support a large variety of different database systems (the support a large variety of different database systems (a database is
default of which is created for you automatically). created for you automatically if you use the defaults).
Using the full power of Python throughout the server offers some Using the full power of Python throughout the server offers some
distinct advantages. All your coding, from object definitions and custom distinct advantages. All your coding, from object definitions and custom
@ -104,11 +104,11 @@ manual <http://code.google.com/p/evennia/wiki/Index>`_ with lots of
examples. But while Python is a relatively easy programming language, it examples. But while Python is a relatively easy programming language, it
still represents a learning curve if you are new to programming. You still represents a learning curve if you are new to programming. You
should probably sit down with a Python beginner's should probably sit down with a Python beginner's
`tutorial <http://docs.python.org/tutorial/tutorial>`_ (there are plenty `tutorial <http://docs.python.org/tutorial/>`_ (there are plenty of them
of them on the web if you look around) so you at least know know what on the web if you look around) so you at least know what you are seeing.
you are seeing. To efficiently code your dream game in Evennia you don't To efficiently code your dream game in Evennia you don't need to be a
need to be a Python guru, but you do need to be able to read example Python guru, but you do need to be able to read example code containing
code containing at least these basic Python features: at least these basic Python features:
- Importing python modules - Importing python modules
- Using variables, `conditional - Using variables, `conditional

View file

@ -44,8 +44,7 @@ Evennia:
**Python** (http://www.python.org) **Python** (http://www.python.org)
- Version 2.5+ strongly recommended, although 2.3 or 2.4 **may** work. - Version 2.5+. Obs- Python3.x is not supported yet.
Obs- Python3.x is not supported yet.
- The default database system SQLite3 only comes as part of Python2.5 - The default database system SQLite3 only comes as part of Python2.5
and later. and later.
- Windows users are recommended to use ActivePython - Windows users are recommended to use ActivePython
@ -97,8 +96,8 @@ Installing pre-requisites
**Linux** package managers should usually handle all this for you. **Linux** package managers should usually handle all this for you.
Python itself is definitely available through all distributions. On Python itself is definitely available through all distributions. On
Debian-derived systems you can do something like this (as root) to get Debian-derived systems (such as Ubuntu) you can do something like this
all you need: (as root) to get all you need:
:: ::
@ -164,6 +163,7 @@ In the future, you just do
:: ::
hg pull hg pull
hg update
from your ``evennia/`` directory to obtain the latest updates. from your ``evennia/`` directory to obtain the latest updates.

View file

@ -60,12 +60,12 @@ form.
:: ::
django-admin.py compilemessages django-admin compilemessages
This will go through all languages and create/update compiled files This will go through all languages and create/update compiled files
(``*.mo``) for them. This needs to be done whenever a ``*.po`` file is (``*.mo``) for them. This needs to be done whenever a ``*.po`` file is
updated. 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 developer list (or push it into your own repository clone) so we can
integrate your translation into Evennia! integrate your translation into Evennia!

View file

@ -99,6 +99,121 @@ be named exactly like this):
parsed. From inside Evennia, ``data_out`` is often called with the parsed. From inside Evennia, ``data_out`` is often called with the
alias ``msg`` instead. 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 <Scripts.html>`_ 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 Assorted notes
-------------- --------------

View file

@ -17,6 +17,7 @@ root directory and type:
:: ::
hg pull hg pull
hg update
Assuming you've got the command line client. If you're using a graphical 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 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. 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 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 changed (check ``hg log`` after you pulled the latest stuff, or read the
run this command: online list), you go to ``game/`` and run this command:
:: ::

View file

@ -355,6 +355,10 @@ def handle_args(options, mode, service):
errmsg = _("The %s does not seem to be running.") errmsg = _("The %s does not seem to be running.")
if mode == 'start': if mode == 'start':
# launch the error checker. Best to catch the errors already here.
error_check_python_modules()
# starting one or many services # starting one or many services
if service == 'server': if service == 'server':
if inter: if inter:
@ -397,6 +401,42 @@ def handle_args(options, mode, service):
kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False) kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False)
return None 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(): def main():
""" """
This handles command line input. This handles command line input.
@ -438,6 +478,7 @@ def main():
Popen(cmdstr) Popen(cmdstr)
if __name__ == '__main__': if __name__ == '__main__':
from src.utils.utils import check_evennia_dependencies from src.utils.utils import check_evennia_dependencies
if check_evennia_dependencies(): if check_evennia_dependencies():
main() main()

View file

@ -24,6 +24,7 @@ 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
#from contrib import chargen, menu_login
class DefaultCmdSet(cmdset_default.DefaultCmdSet): class DefaultCmdSet(cmdset_default.DefaultCmdSet):
""" """
@ -75,7 +76,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
@ -94,7 +94,6 @@ class OOCCmdSet(cmdset_ooc.OOCCmdSet):
# any commands you add below will overload the default ones. # any commands you add below will overload the default ones.
# #
class BaseCmdSet(CmdSet): class BaseCmdSet(CmdSet):
""" """
Implements an empty, example cmdset. Implements an empty, example cmdset.

View file

@ -30,6 +30,7 @@ class CmdNudge(Command):
key = "nudge lid" # two-word command name! key = "nudge lid" # two-word command name!
aliases = ["nudge"] aliases = ["nudge"]
locks = "cmd:all()"
def func(self): def func(self):
""" """
@ -54,6 +55,7 @@ class CmdPush(Command):
""" """
key = "push button" key = "push button"
aliases = ["push", "press button", "press"] aliases = ["push", "press button", "press"]
locks = "cmd:all()"
def func(self): def func(self):
""" """
@ -94,6 +96,7 @@ class CmdSmashGlass(Command):
key = "smash glass" key = "smash glass"
aliases = ["smash lid", "break lid", "smash"] aliases = ["smash lid", "break lid", "smash"]
locks = "cmd:all()"
def func(self): def func(self):
""" """
@ -129,6 +132,7 @@ class CmdOpenLid(Command):
key = "open lid" key = "open lid"
aliases = ["open button", 'open'] aliases = ["open button", 'open']
locks = "cmd:all()"
def func(self): def func(self):
"simply call the right function." "simply call the right function."
@ -159,6 +163,7 @@ class CmdCloseLid(Command):
key = "close lid" key = "close lid"
aliases = ["close"] aliases = ["close"]
locks = "cmd:all()"
def func(self): def func(self):
"Close the lid" "Close the lid"
@ -183,6 +188,8 @@ class CmdBlindLook(Command):
key = "look" key = "look"
aliases = ["l", "get", "examine", "ex", "feel", "listen"] aliases = ["l", "get", "examine", "ex", "feel", "listen"]
locks = "cmd:all()"
def func(self): def func(self):
"This replaces all the senses when blinded." "This replaces all the senses when blinded."
@ -215,6 +222,8 @@ class CmdBlindHelp(Command):
""" """
key = "help" key = "help"
aliases = "h" aliases = "h"
locks = "cmd:all()"
def func(self): def func(self):
"Give a message." "Give a message."
self.caller.msg("You are beyond help ... until you can see again.") self.caller.msg("You are beyond help ... until you can see again.")

View file

@ -99,37 +99,13 @@ class Object(BaseObject):
class Character(BaseCharacter): class Character(BaseCharacter):
""" """
This is the default object created for a new user connecting - the This is the default object created for a new user connecting - the
in-game player character representation. Note that it's important in-game player character representation. The basetype_setup always
that at_object_creation sets up an script that adds the Default assigns the default_cmdset as a fallback to objects of this type.
command set whenever the player logs in - otherwise they won't be The default hooks also hide the character object away (by moving
able to use any commands! it to a Null location whenever the player logs off (otherwise the
character would remain in the world, "headless" so to say).
""" """
pass
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)
class Room(BaseRoom): class Room(BaseRoom):
""" """

View file

@ -7,16 +7,18 @@
# The names of the string variables doesn't matter (except they # The names of the string variables doesn't matter (except they
# shouldn't start with _), but each should hold a string defining a # shouldn't start with _), but each should hold a string defining a
# connection screen - as seen when first connecting to the game # connection screen - as seen when first connecting to the game
# (before having logged in). If there are more than one string # (before having logged in).
# variable defined, a random one is picked.
# #
# After adding new connection screens to this module you must # OBS - If there are more than one string variable viable in this
# either reboot or reload the server to make them available. # 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.commands.connection_screen import DEFAULT_SCREEN
# from src.utils import utils #from src.utils import utils
# #
# CUSTOM_SCREEN = \ # CUSTOM_SCREEN = \
# """{b=============================================================={n # """{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. # Enter {whelp{n for more info. {wlook{n will re-load this screen.
#{b=============================================================={n""" % utils.get_evennia_version() #{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()

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
@ -52,11 +50,17 @@ COMMAND_PARSER = utils.mod_import(*settings.COMMAND_PARSER.rsplit('.', 1))
# allow for custom behaviour when the command handler hits # allow for custom behaviour when the command handler hits
# special situations -- it then calls a normal Command # special situations -- it then calls a normal Command
# that you can customize! # that you can customize!
# Import these variables and use them rather than trying
# to remember the actual string constants.
CMD_NOINPUT = "__noinput_command" CMD_NOINPUT = "__noinput_command"
CMD_NOMATCH = "__nomatch_command" CMD_NOMATCH = "__nomatch_command"
CMD_MULTIMATCH = "__multimatch_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): class NoCmdSets(Exception):
"No cmdsets found. Critical error." "No cmdsets found. Critical error."
@ -122,6 +126,7 @@ def get_and_merge_cmdsets(caller):
cmdsets = [cmdset for cmdset in cmdsets if cmdset] cmdsets = [cmdset for cmdset in cmdsets if cmdset]
# sort cmdsets after reverse priority (highest prio are merged in last) # sort cmdsets after reverse priority (highest prio are merged in last)
cmdsets = sorted(cmdsets, key=lambda x: x.priority) cmdsets = sorted(cmdsets, key=lambda x: x.priority)
if cmdsets: if cmdsets:
# Merge all command sets into one, beginning with the lowest-prio one # Merge all command sets into one, beginning with the lowest-prio one
cmdset = cmdsets.pop(0) cmdset = cmdsets.pop(0)
@ -140,27 +145,21 @@ 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.
raise NoCmdSets raise NoCmdSets
@ -171,12 +170,10 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
syscmd = cmdset.get(CMD_NOINPUT) syscmd = cmdset.get(CMD_NOINPUT)
sysarg = "" sysarg = ""
raise ExecSystemCommand(syscmd, sysarg) raise ExecSystemCommand(syscmd, sysarg)
# Parse the input string and match to available cmdset. # Parse the input string and match to available cmdset.
# This also checks for permissions, so all commands in match # This also checks for permissions, so all commands in match
# are commands the caller is allowed to call. # are commands the caller is allowed to call.
matches = COMMAND_PARSER(raw_string, cmdset, caller) matches = COMMAND_PARSER(raw_string, cmdset, caller)
# Deal with matches # Deal with matches
if not matches: if not matches:
# No commands match our entered command # No commands match our entered command

View file

@ -49,7 +49,6 @@ def cmdparser(raw_string, cmdset, caller, match_index=None):
matches.extend([create_match(cmdname, raw_string, cmd) matches.extend([create_match(cmdname, raw_string, cmd)
for cmdname in [cmd.key] + cmd.aliases for cmdname in [cmd.key] + cmd.aliases
if cmdname and l_raw_string.startswith(cmdname.lower())]) if cmdname and l_raw_string.startswith(cmdname.lower())])
if not matches: if not matches:
# no matches found. # no matches found.
if '-' in raw_string: if '-' in raw_string:

View file

@ -235,10 +235,10 @@ 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'):

View file

@ -121,7 +121,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
logger.log_trace() logger.log_trace()
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"): if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
object.__getattribute__(emit_to_obj, "msg")(errstring) 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 # classes
@ -246,8 +247,8 @@ class CmdSetHandler(object):
def add(self, cmdset, emit_to_obj=None, permanent=False): def add(self, cmdset, emit_to_obj=None, permanent=False):
""" """
Add a cmdset to the handler, on top of the old ones. Add a cmdset to the handler, on top of the old ones.
Default is to not make this permanent (i.e. no script Default is to not make this permanent, i.e. the set
will be added to add the cmdset every server start/login). will not survive a server reset.
cmdset - can be a cmdset object or the python path to cmdset - can be a cmdset object or the python path to
such an object. such an object.

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

@ -616,7 +616,7 @@ class CmdOOCLook(CmdLook):
self.character = None self.character = None
if utils.inherits_from(self.caller, "src.objects.objects.Object"): if utils.inherits_from(self.caller, "src.objects.objects.Object"):
# An object of some type is calling. Convert to player. # 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 self.character = self.caller
if hasattr(self.caller, "player"): if hasattr(self.caller, "player"):
self.caller = self.caller.player self.caller = self.caller.player
@ -685,6 +685,15 @@ class CmdIC(MuxCommand):
if caller.swap_character(new_character): if caller.swap_character(new_character):
new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name) new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name)
caller.db.last_puppet = old_char 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") new_character.execute_cmd("look")
else: else:
caller.msg("{rYou cannot become {C%s{n." % new_character.name) caller.msg("{rYou cannot become {C%s{n." % new_character.name)
@ -720,11 +729,15 @@ class CmdOOC(MuxCommand):
return return
caller.db.last_puppet = caller.character 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 # disconnect
caller.character.player = None caller.character.player = None
caller.character = None caller.character = None
caller.msg("\n{GYou go OOC.{n\n") caller.msg("\n{GYou go OOC.{n\n")
caller.execute_cmd("look") caller.execute_cmd("look")

View file

@ -13,6 +13,7 @@ from src.comms.models import Channel
from src.utils import create, logger, utils, ansi from src.utils import create, logger, utils, ansi
from src.commands.default.muxcommand import MuxCommand from src.commands.default.muxcommand import MuxCommand
from src.commands.cmdhandler import CMD_LOGINSTART
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
@ -170,19 +171,9 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
return return
new_player = new_character.player new_player = new_character.player
# character safety features # This needs to be called so the engine knows this player is logging in for the first time.
new_character.locks.delete("get") # (so it knows to call the right hooks during login later)
new_character.locks.add("get:perm(Wizards)") utils.init_new_player(new_player)
# 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
# join the new player to the public channel # join the new player to the public channel
pchanneldef = settings.CHANNEL_PUBLIC pchanneldef = settings.CHANNEL_PUBLIC
@ -192,9 +183,19 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
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.
@ -221,10 +222,12 @@ class CmdQuit(MuxCommand):
class CmdUnconnectedLook(MuxCommand): class CmdUnconnectedLook(MuxCommand):
""" """
This is an unconnected version of the look command for simplicity. 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" key = CMD_LOGINSTART
aliases = "l" aliases = ["look", "l"]
locks = "cmd:all()" locks = "cmd:all()"
def func(self): def func(self):

View file

@ -68,6 +68,13 @@ class ObjectManager(TypedObjectManager):
# use the id to find the player # use the id to find the player
return self.get_object_with_user(dbref) 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 # attr/property related
@returns_typeclass_list @returns_typeclass_list

View file

@ -316,7 +316,7 @@ class ObjectDB(TypedObject):
string += "%s is not a valid home." string += "%s is not a valid home."
self.msg(string % home) self.msg(string % home)
logger.log_trace(string) logger.log_trace(string)
raise #raise
self.save() self.save()
#@home.deleter #@home.deleter
def home_del(self): def home_del(self):

View file

@ -410,6 +410,34 @@ class Character(Object):
"Default is to look around after a move." "Default is to look around after a move."
self.execute_cmd('look') 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 # Base Room object
# #

View file

@ -34,10 +34,6 @@ SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
# i18n # i18n
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
# Signals
def get_restart_mode(restart_file): def get_restart_mode(restart_file):
""" """
@ -141,6 +137,24 @@ class MsgServer2Portal(amp.Command):
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] 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): class ServerAdmin(amp.Command):
""" """
Portal -> Server Portal -> Server
@ -168,6 +182,8 @@ class PortalAdmin(amp.Command):
errors = [(Exception, 'EXCEPTION')] errors = [(Exception, 'EXCEPTION')]
response = [] 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 # 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. Relays message to server. This method is executed on the Server.
""" """
#print "msg portal -> server (server side):", sessid, msg #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 {} return {}
MsgPortal2Server.responder(amp_msg_portal2server) MsgPortal2Server.responder(amp_msg_portal2server)
@ -232,7 +248,7 @@ class AMPProtocol(amp.AMP):
self.callRemote(MsgPortal2Server, self.callRemote(MsgPortal2Server,
sessid=sessid, sessid=sessid,
msg=msg, msg=msg,
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server") data=dumps(data)).addErrback(self.errback, "MsgPortal2Server")
# Server -> Portal message # Server -> Portal message
@ -241,7 +257,7 @@ class AMPProtocol(amp.AMP):
Relays message to Portal. This method is executed on the Portal. Relays message to Portal. This method is executed on the Portal.
""" """
#print "msg server->portal (portal side):", sessid, msg #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 {} return {}
MsgServer2Portal.responder(amp_msg_server2portal) MsgServer2Portal.responder(amp_msg_server2portal)
@ -253,7 +269,49 @@ class AMPProtocol(amp.AMP):
self.callRemote(MsgServer2Portal, self.callRemote(MsgServer2Portal,
sessid=sessid, sessid=sessid,
msg=utils.to_str(msg), 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 # Server administration from the Portal side
@ -264,7 +322,7 @@ class AMPProtocol(amp.AMP):
operations on the server. This is executed on the Server. 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 #print "serveradmin (server side):", sessid, operation, data
@ -276,7 +334,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)
@ -315,7 +373,7 @@ class AMPProtocol(amp.AMP):
Access method called by the Portal and Executed on the Portal. Access method called by the Portal and Executed on the Portal.
""" """
#print "serveradmin (portal side):", sessid, operation, data #print "serveradmin (portal side):", sessid, operation, data
data = utils.to_str(pickle.dumps(data)) data = dumps(data)
self.callRemote(ServerAdmin, self.callRemote(ServerAdmin,
sessid=sessid, sessid=sessid,
@ -329,7 +387,7 @@ class AMPProtocol(amp.AMP):
This allows the server to perform admin This allows the server to perform admin
operations on the portal. This is executed on the Portal. 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 #print "portaladmin (portal side):", sessid, operation, data
if operation == 'SLOGIN': # 'server_session_login' if operation == 'SLOGIN': # 'server_session_login'
@ -376,7 +434,7 @@ class AMPProtocol(amp.AMP):
Access method called by the server side. Access method called by the server side.
""" """
#print "portaladmin (server side):", sessid, operation, data #print "portaladmin (server side):", sessid, operation, data
data = utils.to_str(pickle.dumps(data)) data = dumps(data)
self.callRemote(PortalAdmin, self.callRemote(PortalAdmin,
sessid=sessid, sessid=sessid,

View file

@ -59,8 +59,7 @@ def create_objects():
character_typeclass=character_typeclass) character_typeclass=character_typeclass)
if not god_character: if not god_character:
print _("#1 could not be created. Check the Player/Character typeclass for bugs.") raise Exception(_("#1 could not be created. Check the Player/Character typeclass for bugs."))
raise Exception
god_character.id = 1 god_character.id = 1
god_character.db.desc = _('This is User #1.') god_character.db.desc = _('This is User #1.')

View file

@ -12,12 +12,16 @@ from datetime import datetime
from django.conf import settings 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, utils
from src.commands import cmdhandler from src.commands import cmdhandler, cmdsethandler
from src.server.session import Session
IDLE_COMMAND = settings.IDLE_COMMAND 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 # i18n
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -37,7 +41,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,7 +51,13 @@ 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
@ -87,11 +96,10 @@ class ServerSession(Session):
player.at_pre_login() player.at_pre_login()
character = player.character character = player.character
#print "at_init() - character"
character.at_init()
if character: if character:
# this player has a character. Check if it's the # this player has a character. Check if it's the
# first time *this character* logs in # first time *this character* logs in
character.at_init()
if character.db.FIRST_LOGIN: if character.db.FIRST_LOGIN:
character.at_first_login() character.at_first_login()
del character.db.FIRST_LOGIN del character.db.FIRST_LOGIN
@ -193,8 +201,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):
@ -203,9 +212,57 @@ class ServerSession(Session):
""" """
self.sessionhandler.data_out(self, msg, data) 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): def __eq__(self, other):
return self.address == other.address return self.address == other.address
def __str__(self): def __str__(self):
""" """
String representation of the user session class. We use String representation of the user session class. We use
@ -239,3 +296,57 @@ class ServerSession(Session):
def msg(self, string='', data=None): def msg(self, string='', data=None):
"alias for at_data_out" "alias for at_data_out"
self.data_out(string, data=data) 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

View file

@ -123,3 +123,20 @@ class Session(object):
""" """
pass 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

View file

@ -19,6 +19,8 @@ from django.contrib.auth.models import User
from src.server.models import ServerConfig from src.server.models import ServerConfig
from src.utils import utils from src.utils import utils
from src.commands.cmdhandler import CMD_LOGINSTART
# i18n # i18n
from django.utils.translation import ugettext as _ from django.utils.translation import ugettext as _
@ -94,7 +96,7 @@ class ServerSessionHandler(SessionHandler):
Creates a new, unlogged-in game session. Creates a new, unlogged-in game session.
""" """
self.sessions[sessid] = session self.sessions[sessid] = session
session.execute_cmd('look') session.execute_cmd(CMD_LOGINSTART)
def portal_disconnect(self, sessid): 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. # to put custom effects on the server due to data input, e.g.
# from a custom client. # 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 # Portal-SessionHandler class
@ -390,5 +406,20 @@ class PortalSessionHandler(SessionHandler):
if session: if session:
session.data_out(string, data=data) 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() SESSIONS = ServerSessionHandler()
PORTAL_SESSIONS = PortalSessionHandler() PORTAL_SESSIONS = PortalSessionHandler()

View file

@ -167,6 +167,12 @@ AT_INITIAL_SETUP_HOOK_MODULE = "game.gamesrc.world.at_initial_setup"
################################################### ###################################################
# Default command sets # 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 # Command set used before player has logged in
CMDSET_UNLOGGEDIN = "game.gamesrc.commands.basecmdset.UnloggedinCmdSet" CMDSET_UNLOGGEDIN = "game.gamesrc.commands.basecmdset.UnloggedinCmdSet"
@ -246,6 +252,8 @@ PERMISSION_PLAYER_DEFAULT = "Players"
# Tuple of modules implementing lock functions. All callable functions # Tuple of modules implementing lock functions. All callable functions
# inside these modules will be available as lock functions. # inside these modules will be available as lock functions.
LOCK_FUNC_MODULES = ("src.locks.lockfuncs",) LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)
# Module holding server-side functions for out-of-band protocols to call.
OOB_FUNC_MODULE = ""
################################################### ###################################################

View file

@ -73,6 +73,7 @@ def create_object(typeclass, key=None, location=None,
# this will either load the typeclass or the default one # this will either load the typeclass or the default one
new_object = new_db_object.typeclass new_object = new_db_object.typeclass
if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True): 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 # this will fail if we gave a typeclass as input and it still gave us a default
SharedMemoryModel.delete(new_db_object) 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. # perform a move_to in order to display eventual messages.
if home: if home:
new_object.home = home new_object.home = home
else:
new_object.home = settings.CHARACTER_DEFAULT_HOME
if location: if location:
new_object.move_to(location, quiet=True) new_object.move_to(location, quiet=True)
else: else:
@ -389,6 +394,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

@ -542,7 +542,7 @@ def has_parent(basepath, obj):
def mod_import(mod_path, propname=None): 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 and imports it. If property is given, return the named
property from this module instead of the module itself. property from this module instead of the module itself.
""" """
@ -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