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

@ -48,7 +48,7 @@ class CmdMenuNode(Command):
locks = "cmd:all()"
help_category = "Menu"
menutree = None
menutree = None
code = None
def func(self):
@ -127,8 +127,7 @@ class MenuCmdSet(CmdSet):
mergetype = "Replace"
def at_cmdset_creation(self):
"populate cmdset"
self.add(CmdMenuLook())
self.add(CmdMenuHelp())
pass
#
# Menu Node system
@ -150,15 +149,19 @@ class MenuTree(object):
'START' and 'END' respectively.
"""
def __init__(self, caller, nodes=None, startnode="START", endnode="END"):
def __init__(self, caller, nodes=None, startnode="START", endnode="END", exec_end="look"):
"""
We specify startnode/endnode so that the system knows where to
enter and where to exit the menu tree. If nodes is given, it
shuld be a list of valid node objects to add to the tree.
exec_end - if not None, will execute the given command string
directly after the menu system has been exited.
"""
self.tree = {}
self.startnode = startnode
self.endnode = endnode
self.exec_end = exec_end
self.caller = caller
if nodes and utils.is_iter(nodes):
for node in nodes:
@ -187,7 +190,8 @@ class MenuTree(object):
# if we was given the END node key, we clean up immediately.
self.caller.cmdset.delete("menucmdset")
del self.caller.db._menu_data
self.caller.execute_cmd("look")
if self.exec_end != None:
self.caller.execute_cmd(self.exec_end)
return
# not exiting, look for a valid code.
node = self.tree.get(key, None)
@ -218,18 +222,28 @@ class MenuNode(object):
"""
def __init__(self, key, text="", links=None, linktexts=None,
keywords=None, cols=1, helptext=None, code=""):
keywords=None, cols=1, helptext=None, selectcmds=None, code="", nodefaultcmds=False, separator=""):
"""
key - the unique identifier of this node.
text - is the text that will be displayed at top when viewing this node.
links - a list of keys for unique menunodes this is connected to.
linktexts - a list of texts to describe the links. If defined, need to match links list
keywords - a list of unique keys for choosing links. Must match links list. If not given, index numbers will be used.
links - a list of keys for unique menunodes this is connected to. The actual keys will not be
printed - keywords will be used (or a number)
linktexts - an optional list of texts to describe the links. Must match link list if defined. Entries can be None
to not generate any extra text for a particular link.
keywords - an optional list of unique keys for choosing links. Must match links list. If not given, index numbers
will be used. Also individual list entries can be None and will be replaed by indices.
If CMD_NOMATCH or CMD_NOENTRY, no text will be generated to indicate the option exists.
cols - how many columns to use for displaying options.
helptext - if defined, this is shown when using the help command instead of the normal help index.
selectcmds- a list of custom cmdclasses for handling each option. Must match links list, but some entries
may be set to None to use default menu cmds. The given command's key will be used for the menu
list entry unless it's CMD_NOMATCH or CMD_NOENTRY, in which case no text will be generated. These
commands have access to self.menutree and so can be used to select nodes.
code - functional code. This will be executed just before this node is loaded (i.e.
as soon after it's been selected from another node). self.caller is available
to call from this code block, as well as ObjectDB and PlayerDB.
nodefaultcmds - if true, don't offer the default help and look commands in the node
separator - this string will be put on the line between menu nodes5B.
"""
self.key = key
self.cmdset = None
@ -237,23 +251,32 @@ class MenuNode(object):
self.linktexts = linktexts
self.keywords = keywords
self.cols = cols
self.selectcmds = selectcmds
self.code = code
self.nodefaultcmds = nodefaultcmds
self.separator = separator
Nlinks = len(self.links)
# validate the input
if not self.links:
self.links = []
if not self.linktexts or (self.linktexts and len(self.linktexts) != len(self.links)):
self.linktexts = []
if not self.keywords or (self.keywords and len(self.keywords) != len(self.links)):
self.keywords = []
if not self.linktexts or (len(self.linktexts) != Nlinks):
self.linktexts = [None for i in range(Nlinks)]
if not self.keywords or (len(self.keywords) != Nlinks):
self.keywords = [None for i in range(Nlinks)]
if not selectcmds or (len(self.selectcmds) != Nlinks):
self.selectcmds = [None for i in range(Nlinks)]
# Format default text for the menu-help command
if not helptext:
helptext = "Select one of the valid options"
if self.keywords:
helptext += " (" + ", ".join(self.keywords) + ")"
elif self.links:
helptext += " (" + ", ".join([str(i + 1) for i in range(len(self.links))]) + ")"
helptext = "Select one of the valid options ("
for i in range(Nlinks):
if self.keywords[i]:
if self.keywords[i] not in (CMD_NOMATCH, CMD_NOINPUT):
helptext += "%s, " % self.keywords[i]
else:
helptext += "%s, " % (i + 1)
helptext = helptext.rstrip(", ") + ")"
self.helptext = helptext
# Format text display
@ -264,12 +287,14 @@ class MenuNode(object):
# format the choices into as many collumns as specified
choices = []
for ilink, link in enumerate(self.links):
if self.keywords:
choice = "{g%s{n" % self.keywords[ilink]
choice = ""
if self.keywords[ilink]:
if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT):
choice += "{g%s{n" % self.keywords[ilink]
else:
choice = "{g%i{n" % (ilink + 1)
if self.linktexts:
choice += "-%s" % self.linktexts[ilink]
choice += "{g %i{n" % (ilink + 1)
if self.linktexts[ilink]:
choice += " - %s" % self.linktexts[ilink]
choices.append(choice)
cols = [[] for i in range(min(len(choices), cols))]
while True:
@ -282,9 +307,9 @@ class MenuNode(object):
break
ftable = utils.format_table(cols)
for row in ftable:
string += "\n" + "".join(row)
string +="\n" + "".join(row)
# store text
self.text = 78*"-" + "\n" + string.strip()
self.text = self.separator + "\n" + string.rstrip()
def init(self, menutree):
"""
@ -292,15 +317,24 @@ class MenuNode(object):
"""
# Create the relevant cmdset
self.cmdset = MenuCmdSet()
for i, link in enumerate(self.links):
cmd = CmdMenuNode()
cmd.key = str(i + 1)
if not self.nodefaultcmds:
# add default menu commands
self.cmdset.add(CmdMenuLook())
self.cmdset.add(CmdMenuHelp())
for i, link in enumerate(self.links):
if self.selectcmds[i]:
cmd = self.selectcmds[i]()
else:
cmd = CmdMenuNode()
cmd.key = str(i + 1)
# this is the operable command, it moves us to the next node.
cmd.code = "self.menutree.goto('%s')" % link
# also custom commands get access to the menutree.
cmd.menutree = menutree
# this is the operable command, it moves us to the next node.
cmd.code = "self.menutree.goto('%s')" % link
if self.keywords:
if self.keywords[i] and cmd.key not in (CMD_NOMATCH, CMD_NOINPUT):
cmd.aliases = [self.keywords[i]]
self.cmdset.add(cmd)
self.cmdset.add(cmd)
def __str__(self):
"Returns the string representation."

View file

@ -616,6 +616,29 @@ command must be added to a cmdset as well before it will work.
def func(self):
self.caller.msg("Don't just press return like that, talk to me!")
Exits
-----
*Note: This is an advanced topic.*
The functionality of `Exit <Objects.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
--------------------------
@ -684,3 +707,14 @@ Call ``func()`` on the command instance. This is the functional body of
the command, actually doing useful things.
Call ``at_post_command()`` on the command instance.
Assorted notes
--------------
The return value of ``Command.func()`` *is* safely passed on should one
have some very specific use case in mind. So one could in principle do
``value = obj.execute_cmd(cmdname)``. Evennia does not use this
functionality at all by default (all default commands simply returns
``None``) and it's probably not relevant to any but the most
advanced/exotic designs (one might use it to create a "nested" command
structure for example).

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

View file

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

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
(``*.mo``) for them. This needs to be done whenever a ``*.po`` file is
updated.
When you are done, send the ``*.po`` and \*.mo file to the Evennia
When you are done, send the ``*.po`` and ``*.mo`` file to the Evennia
developer list (or push it into your own repository clone) so we can
integrate your translation into Evennia!

View file

@ -99,6 +99,121 @@ be named exactly like this):
parsed. From inside Evennia, ``data_out`` is often called with the
alias ``msg`` instead.
Out-of-band communication
-------------------------
Out-of-band communication (OOB) is data being sent to and fro the
player's client and the server on the protocol level, often due to the
request of the player's client software rather than any sort of active
input by the player. There are two main types:
- Data requested by the client which the server responds to
immediately. This could for example be data that should go into a
window that the client just opened up.
- Data the server sends to the client to keep ut up-to-date. A common
example of this is something like a graphical health bar - *whenever*
the character's health status changes the server sends this data to
the client so it can update the bar graphic. This sending could also
be done on a timer, for example updating a weather map regularly.
To communicate to the client, there are a range of protocols available
for MUDs, supported by different clients, such as MSDP and GMCP. They
basically implements custom telnet negotiation sequences and goes into a
custom Evennia Portal protocol so Evennia can understand it.
It then needs to translate each protocol-specific function into an
Evennia function name - specifically a name of a module-level function
you define in the module given by ``settings.OOB_FUNC_MODULE``. These
function will get the session/character as first argument but is
otherwise completely free of form. The portal packs all function names
and eventual arguments they need in a dictionary and sends them off to
the Server by use of the ``sessionhandler.oob_data_in()`` method. On the
Server side, the dictionary is parsed, and the correct functions in
``settings.OOB_FUNC_MODULE`` are called with the given arguments. The
results from this function are again packed in a dictionary (keyed by
function name) and sent back to the portal. It will appear in the Portal
session's ``oob_data_out(data)`` method.
So to summarize: To implement a Portal protocol with OOB communication
support, you need to first let your normal ``getData`` method somehow
parse out the special protocol format format coming in from the client
(MSDP, GMCP etc). It needs to translate what the client wants into
function names matching that in the ``OOB_FUNC_MODULE`` - these
functions need to be created to match too of course. The function name
and arguments are packed in a dictionary and sent off to the server via
``sessionhandler.oob_data_in()``. Finally, the portal session must
implement ``oob_data_out(data)`` to handle the data coming back from
Server. It will be a dictionary of return values keyed by the function
names.
Example of out-of-band calling sequence
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Let's say we want our client to be able to request the character's
current health. In our Portal protocol we somehow parse the incoming
data stream and figure out what the request for health looks like. We
map this to the Evennia ``get_health`` function.
We point ``settings.OOB_FUNC_MODULE`` to someplace in ``game/`` and
create a module there with the following function:
::
# the caller is always added as first argument
# we also assume health is stored as a simple
# attribute on the character here.
def get_health(character):
return character.db.health
Done, this function will do just what we want. Let's finish up the first
part of the portal protocol:
::
# this method could be named differently depending on the
# protocol you are using (this is telnet)
def lineReceived(self, string):
# (does stuff to analyze the incoming string) outdict =
if GET_HEALTH:
# call get_health(char)
outdict["get_health"] = ([], )
elif GET_MANA:
# call get_mana(char)
outdict["get_mana"] = ([], )
elif GET_CONFIG:
# call get_config(char, 2, hidden=True)
outdict["get_config"] = ([2], 'hidden':True) [...] self.sessionhandler.oob_data_out(outdict)
The server will properly accept this and call get\_health and get the
right value for the health. We need to define an ``oob_data_out(data)``
in our portal protocol to catch the return value:
::
def oob_data_out(self, data):
# the indata is a dicationary funcname:retval outstring = ""
for funcname, retval in data.items():
if funcname == 'get_health':
# convert to the right format for sending back to client, store
# in outstring ...
[...]
Above, once the dict is parsed and the return values properly put in a
format the client will understand, send the whole thing off using the
protocol's relevant send method.
Implementing auto-sending
~~~~~~~~~~~~~~~~~~~~~~~~~
To have the Server update the client regularly, simply create a global
`Script <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
--------------

View file

@ -17,6 +17,7 @@ root directory and type:
::
hg pull
hg update
Assuming you've got the command line client. If you're using a graphical
client, you will probably want to navigate to the ``evennia`` directory
@ -112,8 +113,8 @@ used (you have to give the ``mange.py migrate`` command as well as
Once you have a database ready and using South, you work as normal.
Whenever a new Evennia update tells you that the database schema has
changed (check ``hg log`` or the online list), you go to ``game/`` and
run this command:
changed (check ``hg log`` after you pulled the latest stuff, or read the
online list), you go to ``game/`` and run this command:
::

View file

@ -355,6 +355,10 @@ def handle_args(options, mode, service):
errmsg = _("The %s does not seem to be running.")
if mode == 'start':
# launch the error checker. Best to catch the errors already here.
error_check_python_modules()
# starting one or many services
if service == 'server':
if inter:
@ -397,6 +401,42 @@ def handle_args(options, mode, service):
kill(SERVER_PIDFILE, SIG, _("Server stopped."), errmsg % 'Server', restart=False)
return None
def error_check_python_modules():
"""
Import settings modules in settings. This will raise exceptions on
pure python-syntax issues which are hard to catch gracefully
with exceptions in the engine (since they are formatting errors in
the python source files themselves). Best they fail already here
before we get any further.
"""
def imp(path, split=True):
mod, fromlist = path, "None"
if split:
mod, fromlist = path.rsplit('.', 1)
__import__(mod, fromlist=[fromlist])
# core modules
imp(settings.COMMAND_PARSER)
imp(settings.SEARCH_AT_RESULT)
imp(settings.SEARCH_AT_MULTIMATCH_INPUT)
imp(settings.CONNECTION_SCREEN_MODULE, split=False)
#imp(settings.AT_INITIAL_SETUP_HOOK_MODULE, split=False)
for path in settings.LOCK_FUNC_MODULES:
imp(path, split=False)
# cmdsets
from src.commands import cmdsethandler
cmdsethandler.import_cmdset(settings.CMDSET_UNLOGGEDIN, None)
cmdsethandler.import_cmdset(settings.CMDSET_DEFAULT, None)
cmdsethandler.import_cmdset(settings.CMDSET_OOC, None)
# typeclasses
imp(settings.BASE_PLAYER_TYPECLASS)
imp(settings.BASE_OBJECT_TYPECLASS)
imp(settings.BASE_CHARACTER_TYPECLASS)
imp(settings.BASE_ROOM_TYPECLASS)
imp(settings.BASE_EXIT_TYPECLASS)
imp(settings.BASE_SCRIPT_TYPECLASS)
def main():
"""
This handles command line input.
@ -438,6 +478,7 @@ def main():
Popen(cmdstr)
if __name__ == '__main__':
from src.utils.utils import check_evennia_dependencies
if check_evennia_dependencies():
main()

View file

@ -24,6 +24,7 @@ from game.gamesrc.commands.basecommand import Command
#from contrib import menusystem, lineeditor
#from contrib import misc_commands
#from contrib import chargen, menu_login
class DefaultCmdSet(cmdset_default.DefaultCmdSet):
"""
@ -49,7 +50,7 @@ class DefaultCmdSet(cmdset_default.DefaultCmdSet):
#
#self.add(menusystem.CmdMenuTest())
#self.add(lineeditor.CmdEditor())
#self.add(misc_commands.CmdQuell())
#self.add(misc_commands.CmdQuell())
class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
"""
@ -75,7 +76,6 @@ class UnloggedinCmdSet(cmdset_unloggedin.UnloggedinCmdSet):
# any commands you add below will overload the default ones.
#
class OOCCmdSet(cmdset_ooc.OOCCmdSet):
"""
This is set is available to the player when they have no
@ -92,9 +92,8 @@ class OOCCmdSet(cmdset_ooc.OOCCmdSet):
#
# any commands you add below will overload the default ones.
#
#
class BaseCmdSet(CmdSet):
"""
Implements an empty, example cmdset.

View file

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

View file

@ -99,37 +99,13 @@ class Object(BaseObject):
class Character(BaseCharacter):
"""
This is the default object created for a new user connecting - the
in-game player character representation. Note that it's important
that at_object_creation sets up an script that adds the Default
command set whenever the player logs in - otherwise they won't be
able to use any commands!
in-game player character representation. The basetype_setup always
assigns the default_cmdset as a fallback to objects of this type.
The default hooks also hide the character object away (by moving
it to a Null location whenever the player logs off (otherwise the
character would remain in the world, "headless" so to say).
"""
def at_disconnect(self):
"""
We stove away the character when logging off, otherwise the character object will
remain in the room also after the player logged off ("headless", so to say).
"""
if self.location: # have to check, in case of multiple connections closing
self.location.msg_contents("%s has left the game." % self.name)
self.db.prelogout_location = self.location
self.location = None
def at_post_login(self):
"""
This recovers the character again after having been "stoved away" at disconnect.
"""
if self.db.prelogout_location:
# try to recover
self.location = self.db.prelogout_location
if self.location == None:
# make sure location is never None (home should always exist)
self.location = self.home
# save location again to be sure
self.db.prelogout_location = self.location
self.location.msg_contents("%s has entered the game." % self.name)
self.location.at_object_receive(self, self.location)
pass
class Room(BaseRoom):
"""

View file

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

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
3) If no command string was supplied, we search the merged cmdset for system command CMD_NOINPUT
and branches to execute that. --> Finished
4) Depending on the login/not state, it collects cmdsets from different sources:
not logged in - uses the single cmdset defined as settings.CMDSET_UNLOGGEDIN
normal - gathers command sets from many different sources (shown in dropping priority):
4) Cmdsets are gathered from different sources (in order of dropping priority):
channels - all available channel names are auto-created into a cmdset, to allow
for giving the channel name and have the following immediately
sent to the channel. The sending is performed by the CMD_CHANNEL
@ -52,11 +50,17 @@ COMMAND_PARSER = utils.mod_import(*settings.COMMAND_PARSER.rsplit('.', 1))
# allow for custom behaviour when the command handler hits
# special situations -- it then calls a normal Command
# that you can customize!
# Import these variables and use them rather than trying
# to remember the actual string constants.
CMD_NOINPUT = "__noinput_command"
CMD_NOMATCH = "__nomatch_command"
CMD_MULTIMATCH = "__multimatch_command"
CMD_CHANNEL = "__send_to_channel"
CMD_CHANNEL = "__send_to_channel_command"
# this is the name of the command the engine calls when the player
# connects. It is expected to show the login screen.
CMD_LOGINSTART = "__unloggedin_look_command"
class NoCmdSets(Exception):
"No cmdsets found. Critical error."
@ -115,13 +119,14 @@ def get_and_merge_cmdsets(caller):
try:
player_cmdset = caller.player.cmdset.current
except AttributeError:
player_cmdset = None
player_cmdset = None
cmdsets = [caller_cmdset] + [player_cmdset] + [channel_cmdset] + local_objects_cmdsets
# weed out all non-found sets
cmdsets = [cmdset for cmdset in cmdsets if cmdset]
# sort cmdsets after reverse priority (highest prio are merged in last)
cmdsets = sorted(cmdsets, key=lambda x: x.priority)
if cmdsets:
# Merge all command sets into one, beginning with the lowest-prio one
cmdset = cmdsets.pop(0)
@ -131,7 +136,7 @@ def get_and_merge_cmdsets(caller):
cmdset = merging_cmdset + cmdset
else:
cmdset = None
for cset in (cset for cset in local_objects_cmdsets if cset):
cset.duplicates = cset.old_duplicates
@ -140,27 +145,21 @@ def get_and_merge_cmdsets(caller):
# Main command-handler function
def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
def cmdhandler(caller, raw_string, testing=False):
"""
This is the main function to handle any string sent to the engine.
caller - calling object
raw_string - the command string given on the command line
unloggedin - if caller is an authenticated user or not
testing - if we should actually execute the command or not.
if True, the command instance will be returned instead.
"""
try: # catch bugs in cmdhandler itself
try: # catch special-type commands
if unloggedin:
# not logged in, so it's just one cmdset we are interested in
cmdset = import_cmdset(settings.CMDSET_UNLOGGEDIN, caller)
else:
# We are logged in, collect all relevant cmdsets and merge
cmdset = get_and_merge_cmdsets(caller)
cmdset = get_and_merge_cmdsets(caller)
#print cmdset
# print cmdset
if not cmdset:
# this is bad and shouldn't happen.
raise NoCmdSets
@ -171,12 +170,10 @@ def cmdhandler(caller, raw_string, unloggedin=False, testing=False):
syscmd = cmdset.get(CMD_NOINPUT)
sysarg = ""
raise ExecSystemCommand(syscmd, sysarg)
# Parse the input string and match to available cmdset.
# This also checks for permissions, so all commands in match
# are commands the caller is allowed to call.
matches = COMMAND_PARSER(raw_string, cmdset, caller)
# Deal with matches
if not matches:
# No commands match our entered command

View file

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

View file

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

View file

@ -121,7 +121,8 @@ def import_cmdset(python_path, cmdsetobj, emit_to_obj=None, no_logging=False):
logger.log_trace()
if emit_to_obj and not ServerConfig.objects.conf("server_starting_mode"):
object.__getattribute__(emit_to_obj, "msg")(errstring)
#raise # have to raise, or we will not see any errors in some situations!
logger.log_errmsg("Error: %s" % errstring)
raise # have to raise, or we will not see any errors in some situations!
# classes
@ -246,8 +247,8 @@ class CmdSetHandler(object):
def add(self, cmdset, emit_to_obj=None, permanent=False):
"""
Add a cmdset to the handler, on top of the old ones.
Default is to not make this permanent (i.e. no script
will be added to add the cmdset every server start/login).
Default is to not make this permanent, i.e. the set
will not survive a server reset.
cmdset - can be a cmdset object or the python path to
such an object.

View file

@ -129,7 +129,7 @@ class Command(object):
previously extracted from the raw string by the system.
cmdname is always lowercase when reaching this point.
"""
return (cmdname == self.key) or (cmdname in self.aliases)
return cmdname and ((cmdname == self.key) or (cmdname in self.aliases))
def access(self, srcobj, access_type="cmd", default=False):
"""

View file

@ -616,7 +616,7 @@ class CmdOOCLook(CmdLook):
self.character = None
if utils.inherits_from(self.caller, "src.objects.objects.Object"):
# An object of some type is calling. Convert to player.
print self.caller, self.caller.__class__
#print self.caller, self.caller.__class__
self.character = self.caller
if hasattr(self.caller, "player"):
self.caller = self.caller.player
@ -685,6 +685,15 @@ class CmdIC(MuxCommand):
if caller.swap_character(new_character):
new_character.msg("\n{gYou become {c%s{n.\n" % new_character.name)
caller.db.last_puppet = old_char
if not new_character.location:
# this might be due to being hidden away at logout; check
loc = new_character.db.prelogout_location
if not loc: # still no location; use home
loc = new_character.home
new_character.location = loc
if new_character.location:
new_character.location.msg_contents("%s has entered the game." % new_character.key, exclude=[new_character])
new_character.location.at_object_receive(new_character, new_character.location)
new_character.execute_cmd("look")
else:
caller.msg("{rYou cannot become {C%s{n." % new_character.name)
@ -720,11 +729,15 @@ class CmdOOC(MuxCommand):
return
caller.db.last_puppet = caller.character
# save location as if we were disconnecting from the game entirely.
if caller.character.location:
caller.character.location.msg_contents("%s has left the game." % caller.character.key, exclude=[caller.character])
caller.character.db.prelogout_location = caller.character.location
caller.character.location = None
# disconnect
caller.character.player = None
caller.character = None
caller.msg("\n{GYou go OOC.{n\n")
caller.execute_cmd("look")

View file

@ -13,6 +13,7 @@ from src.comms.models import Channel
from src.utils import create, logger, utils, ansi
from src.commands.default.muxcommand import MuxCommand
from src.commands.cmdhandler import CMD_LOGINSTART
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
@ -169,20 +170,10 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
session.msg("There was an error creating the default Character/Player. This error was logged. Contact an admin.")
return
new_player = new_character.player
# character safety features
new_character.locks.delete("get")
new_character.locks.add("get:perm(Wizards)")
# allow the character itself and the player to puppet this character.
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
(new_character.id, new_player.id))
# set a default description
new_character.db.desc = "This is a Player."
new_character.db.FIRST_LOGIN = True
new_player = new_character.player
new_player.db.FIRST_LOGIN = True
# This needs to be called so the engine knows this player is logging in for the first time.
# (so it knows to call the right hooks during login later)
utils.init_new_player(new_player)
# join the new player to the public channel
pchanneldef = settings.CHANNEL_PUBLIC
@ -191,10 +182,20 @@ its and @/./+/-/_ only.") # this echoes the restrictions made by django's auth m
if not pchannel.connect_to(new_player):
string = "New player '%s' could not connect to public channel!" % new_player.key
logger.log_errmsg(string)
# allow only the character itself and the player to puppet this character (and Immortals).
new_character.locks.add("puppet:id(%i) or pid(%i) or perm(Immortals) or pperm(Immortals)" %
(new_character.id, new_player.id))
# set a default description
new_character.db.desc = "This is a Player."
# tell the caller everything went well.
string = "A new account '%s' was created with the email address %s. Welcome!"
string += "\n\nYou can now log with the command 'connect %s <your password>'."
session.msg(string % (playername, email, email))
except Exception:
# We are in the middle between logged in and -not, so we have to handle tracebacks
# ourselves at this point. If we don't, we won't see any errors at all.
@ -221,10 +222,12 @@ class CmdQuit(MuxCommand):
class CmdUnconnectedLook(MuxCommand):
"""
This is an unconnected version of the look command for simplicity.
All it does is re-show the connect screen.
This is called by the server and kicks everything in gear.
All it does is display the connect screen.
"""
key = "look"
aliases = "l"
key = CMD_LOGINSTART
aliases = ["look", "l"]
locks = "cmd:all()"
def func(self):

View file

@ -68,6 +68,13 @@ class ObjectManager(TypedObjectManager):
# use the id to find the player
return self.get_object_with_user(dbref)
@returns_typeclass_list
def get_objs_with_key_and_typeclass(self, oname, otypeclass_path):
"""
Returns objects based on simultaneous key and typeclass match.
"""
return self.filter(db_key__iexact=oname).filter(db_typeclass_path__exact=otypeclass_path)
# attr/property related
@returns_typeclass_list

View file

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

View file

@ -409,6 +409,34 @@ class Character(Object):
def at_after_move(self, source_location):
"Default is to look around after a move."
self.execute_cmd('look')
def at_disconnect(self):
"""
We stove away the character when logging off, otherwise the character object will
remain in the room also after the player logged off ("headless", so to say).
"""
if self.location: # have to check, in case of multiple connections closing
self.location.msg_contents("%s has left the game." % self.name, exclude=[self])
self.db.prelogout_location = self.location
self.location = None
def at_post_login(self):
"""
This recovers the character again after having been "stoved away" at disconnect.
"""
if self.db.prelogout_location:
# try to recover
self.location = self.db.prelogout_location
if self.location == None:
# make sure location is never None (home should always exist)
self.location = self.home
# save location again to be sure
self.db.prelogout_location = self.location
self.location.msg_contents("%s has entered the game." % self.name, exclude=[self])
self.location.at_object_receive(self, self.location)
#
# Base Room object

View file

@ -34,10 +34,6 @@ SERVER_RESTART = os.path.join(settings.GAME_DIR, "server.restart")
# i18n
from django.utils.translation import ugettext as _
# Signals
def get_restart_mode(restart_file):
"""
@ -141,6 +137,24 @@ class MsgServer2Portal(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
class OOBPortal2Server(amp.Command):
"""
OOB data portal -> server
"""
arguments = [('sessid', amp.Integer()),
('data', amp.String())]
errors = [(Exception, "EXCEPTION")]
response = []
class OOBServer2Portal(amp.Command):
"""
OOB data server -> portal
"""
arguments = [('sessid', amp.Integer()),
('data', amp.String())]
errors = [(Exception, "EXCEPTION")]
response = []
class ServerAdmin(amp.Command):
"""
Portal -> Server
@ -168,6 +182,8 @@ class PortalAdmin(amp.Command):
errors = [(Exception, 'EXCEPTION')]
response = []
dumps = lambda data: utils.to_str(pickle.dumps(data, pickle.HIGHEST_PROTOCOL))
loads = lambda data: pickle.loads(utils.to_str(data))
#------------------------------------------------------------
# Core AMP protocol for communication Server <-> Portal
@ -220,7 +236,7 @@ class AMPProtocol(amp.AMP):
Relays message to server. This method is executed on the Server.
"""
#print "msg portal -> server (server side):", sessid, msg
self.factory.server.sessions.data_in(sessid, msg, pickle.loads(utils.to_str(data)))
self.factory.server.sessions.data_in(sessid, msg, loads(data))
return {}
MsgPortal2Server.responder(amp_msg_portal2server)
@ -232,7 +248,7 @@ class AMPProtocol(amp.AMP):
self.callRemote(MsgPortal2Server,
sessid=sessid,
msg=msg,
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgPortal2Server")
data=dumps(data)).addErrback(self.errback, "MsgPortal2Server")
# Server -> Portal message
@ -241,7 +257,7 @@ class AMPProtocol(amp.AMP):
Relays message to Portal. This method is executed on the Portal.
"""
#print "msg server->portal (portal side):", sessid, msg
self.factory.portal.sessions.data_out(sessid, msg, pickle.loads(utils.to_str(data)))
self.factory.portal.sessions.data_out(sessid, msg, loads(data))
return {}
MsgServer2Portal.responder(amp_msg_server2portal)
@ -253,8 +269,50 @@ class AMPProtocol(amp.AMP):
self.callRemote(MsgServer2Portal,
sessid=sessid,
msg=utils.to_str(msg),
data=utils.to_str(pickle.dumps(data))).addErrback(self.errback, "MsgServer2Portal")
data=dumps(data)).addErrback(self.errback, "OOBServer2Portal")
# OOB Portal -> Server
# Portal -> Server Msg
def amp_oob_portal2server(self, sessid, data):
"""
Relays out-of-band data to server. This method is executed on the Server.
"""
#print "oob portal -> server (server side):", sessid, loads(data)
self.factory.server.sessions.oob_data_in(sessid, loads(data))
return {}
OOBPortal2Server.responder(amp_oob_portal2server)
def call_remote_OOBPortal2Server(self, sessid, data=""):
"""
Access method called by the Portal and executed on the Portal.
"""
#print "oob portal->server (portal side):", sessid, data
self.callRemote(OOBPortal2Server,
sessid=sessid,
data=dumps(data)).addErrback(self.errback, "OOBPortal2Server")
# Server -> Portal message
def amp_oob_server2portal(self, sessid, data):
"""
Relays out-of-band data to Portal. This method is executed on the Portal.
"""
#print "oob server->portal (portal side):", sessid, data
self.factory.portal.sessions.oob_data_out(sessid, loads(data))
return {}
OOBServer2Portal.responder(amp_oob_server2portal)
def call_remote_OOBServer2Portal(self, sessid, data=""):
"""
Access method called by the Server and executed on the Server.
"""
#print "oob server->portal (server side):", sessid, data
self.callRemote(OOBServer2Portal,
sessid=sessid,
data=dumps(data)).addErrback(self.errback, "OOBServer2Portal")
# Server administration from the Portal side
@ -264,7 +322,7 @@ class AMPProtocol(amp.AMP):
operations on the server. This is executed on the Server.
"""
data = pickle.loads(utils.to_str(data))
data = loads(data)
#print "serveradmin (server side):", sessid, operation, data
@ -276,7 +334,7 @@ class AMPProtocol(amp.AMP):
if sess.logged_in and sess.uid:
# this can happen in the case of auto-authenticating protocols like SSH
sess.player = PlayerDB.objects.get_player_from_uid(sess.uid)
sess.at_sync() # this runs initialization without acr
sess.at_sync() # this runs initialization without acr
self.factory.server.sessions.portal_connect(sessid, sess)
@ -315,7 +373,7 @@ class AMPProtocol(amp.AMP):
Access method called by the Portal and Executed on the Portal.
"""
#print "serveradmin (portal side):", sessid, operation, data
data = utils.to_str(pickle.dumps(data))
data = dumps(data)
self.callRemote(ServerAdmin,
sessid=sessid,
@ -329,7 +387,7 @@ class AMPProtocol(amp.AMP):
This allows the server to perform admin
operations on the portal. This is executed on the Portal.
"""
data = pickle.loads(utils.to_str(data))
data = loads(data)
#print "portaladmin (portal side):", sessid, operation, data
if operation == 'SLOGIN': # 'server_session_login'
@ -376,7 +434,7 @@ class AMPProtocol(amp.AMP):
Access method called by the server side.
"""
#print "portaladmin (server side):", sessid, operation, data
data = utils.to_str(pickle.dumps(data))
data = dumps(data)
self.callRemote(PortalAdmin,
sessid=sessid,

View file

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

View file

@ -12,12 +12,16 @@ from datetime import datetime
from django.conf import settings
from src.scripts.models import ScriptDB
from src.comms.models import Channel
from src.utils import logger
from src.commands import cmdhandler
from src.utils import logger, utils
from src.commands import cmdhandler, cmdsethandler
from src.server.session import Session
IDLE_COMMAND = settings.IDLE_COMMAND
from src.server.session import Session
# load optional out-of-band function module
OOB_FUNC_MODULE = settings.OOB_FUNC_MODULE
if OOB_FUNC_MODULE:
OOB_FUNC_MODULE = utils.mod_import(settings.OOB_FUNC_MODULE)
# i18n
from django.utils.translation import ugettext as _
@ -37,7 +41,6 @@ class ServerSession(Session):
through their session.
"""
def at_sync(self):
"""
This is called whenever a session has been resynced with the portal.
@ -48,12 +51,18 @@ class ServerSession(Session):
the session as it was.
"""
if not self.logged_in:
# assign the unloggedin-command set.
self.cmdset = cmdsethandler.CmdSetHandler(self)
self.cmdset_storage = [settings.CMDSET_UNLOGGEDIN]
self.cmdset.update(init_mode=True)
self.cmdset.update(init_mode=True)
return
character = self.get_character()
if character:
# start (persistent) scripts on this object
ScriptDB.objects.validate(obj=character)
def session_login(self, player):
"""
Startup mechanisms that need to run at login. This is called
@ -87,11 +96,10 @@ class ServerSession(Session):
player.at_pre_login()
character = player.character
#print "at_init() - character"
character.at_init()
if character:
# this player has a character. Check if it's the
# first time *this character* logs in
character.at_init()
if character.db.FIRST_LOGIN:
character.at_first_login()
del character.db.FIRST_LOGIN
@ -181,7 +189,7 @@ class ServerSession(Session):
if str(command_string).strip() == IDLE_COMMAND:
self.update_session_counters(idle=True)
return
# all other inputs, including empty inputs
character = self.get_character()
@ -193,8 +201,9 @@ class ServerSession(Session):
# there is no character, but we are logged in. Use player instead.
self.get_player().execute_cmd(command_string)
else:
# we are not logged in. Use special unlogged-in call.
cmdhandler.cmdhandler(self, command_string, unloggedin=True)
# we are not logged in. Use the session directly
# (it uses the settings.UNLOGGEDIN cmdset)
cmdhandler.cmdhandler(self, command_string)
self.update_session_counters()
def data_out(self, msg, data=None):
@ -203,9 +212,57 @@ class ServerSession(Session):
"""
self.sessionhandler.data_out(self, msg, data)
def oob_data_in(self, data):
"""
This receives out-of-band data from the Portal.
This method parses the data input (a dict) and uses
it to launch correct methods from those plugged into
the system.
data = {funcname: ( [args], {kwargs]),
funcname: ( [args], {kwargs}), ...}
example:
data = {"get_hp": ([], {}),
"update_counter", (["counter1"], {"now":True}) }
"""
print "server: "
outdata = {}
entity = self.get_character()
if not entity:
entity = self.get_player()
if not entity:
entity = self
for funcname, argtuple in data.items():
# loop through the data, calling available functions.
func = OOB_FUNC_MODULE.__dict__.get(funcname, None)
if func:
try:
outdata[funcname] = func(entity, *argtuple[0], **argtuple[1])
except Exception:
logger.log_trace()
else:
logger.log_errmsg("oob_data_in error: funcname '%s' not found in OOB_FUNC_MODULE." % funcname)
if outdata:
self.oob_data_out(outdata)
def oob_data_out(self, data):
"""
This sends data from Server to the Portal across the AMP connection.
"""
self.sessionhandler.oob_data_out(self, data)
def __eq__(self, other):
return self.address == other.address
def __str__(self):
"""
String representation of the user session class. We use
@ -239,3 +296,57 @@ class ServerSession(Session):
def msg(self, string='', data=None):
"alias for at_data_out"
self.data_out(string, data=data)
# Dummy API hooks for use a non-loggedin operation
def at_cmdset_get(self):
"dummy hook all objects with cmdsets need to have"
pass
# Mock db/ndb properties for allowing easy storage on the session
# (note that no databse is involved at all here. session.db.attr =
# value just saves a normal property in memory, just like ndb).
#@property
def ndb_get(self):
"""
A non-persistent store (ndb: NonDataBase). Everything stored
to this is guaranteed to be cleared when a server is shutdown.
Syntax is same as for the _get_db_holder() method and
property, e.g. obj.ndb.attr = value etc.
"""
try:
return self._ndb_holder
except AttributeError:
class NdbHolder(object):
"Holder for storing non-persistent attributes."
def all(self):
return [val for val in self.__dict__.keys()
if not val.startswith['_']]
def __getattribute__(self, key):
# return None if no matching attribute was found.
try:
return object.__getattribute__(self, key)
except AttributeError:
return None
self._ndb_holder = NdbHolder()
return self._ndb_holder
#@ndb.setter
def ndb_set(self, value):
"Stop accidentally replacing the db object"
string = "Cannot assign directly to ndb object! "
string = "Use ndb.attr=value instead."
raise Exception(string)
#@ndb.deleter
def ndb_del(self):
"Stop accidental deletion."
raise Exception("Cannot delete the ndb object!")
ndb = property(ndb_get, ndb_set, ndb_del)
db = property(ndb_get, ndb_set, ndb_del)
# Mock access method for the session (there is no lock info
# at this stage, so we just present a uniform API)
def access(self, *args, **kwargs):
"Dummy method."
return True

View file

@ -123,3 +123,20 @@ class Session(object):
"""
pass
def oob_data_out(self, data):
"""
for Portal, this receives out-of-band data from Server across the AMP.
for Server, this sends out-of-band data to Portal.
data is a dictionary
"""
pass
def oob_data_in(self, data):
"""
for Portal, this sends out-of-band requests to Server over the AMP.
for Server, this receives data from Portal.
data is a dictionary
"""
pass

View file

@ -19,6 +19,8 @@ from django.contrib.auth.models import User
from src.server.models import ServerConfig
from src.utils import utils
from src.commands.cmdhandler import CMD_LOGINSTART
# i18n
from django.utils.translation import ugettext as _
@ -94,7 +96,7 @@ class ServerSessionHandler(SessionHandler):
Creates a new, unlogged-in game session.
"""
self.sessions[sessid] = session
session.execute_cmd('look')
session.execute_cmd(CMD_LOGINSTART)
def portal_disconnect(self, sessid):
"""
@ -293,6 +295,20 @@ class ServerSessionHandler(SessionHandler):
# to put custom effects on the server due to data input, e.g.
# from a custom client.
def oob_data_in(self, sessid, data):
"""
OOB (Out-of-band) Data Portal -> Server
"""
session = self.sessions.get(sessid, None)
if session:
session.oob_data_in(data)
def oob_data_out(self, session, data):
"""
OOB (Out-of-band) Data Server -> Portal
"""
self.server.amp_protocol.call_remote_OOBServer2Portal(session.sessid,
data=data)
#------------------------------------------------------------
# Portal-SessionHandler class
@ -390,5 +406,20 @@ class PortalSessionHandler(SessionHandler):
if session:
session.data_out(string, data=data)
def oob_data_in(self, session, data):
"""
OOB (Out-of-band) data Portal -> Server
"""
self.portal.amp_protocol.call_remote_OOBPortal2Server(session.sessid,
data=data)
def oob_data_out(self, sessid, data):
"""
OOB (Out-of-band) data Server -> Portal
"""
session = self.sessions.get(sessid, None)
if session:
session.oob_data_out(data)
SESSIONS = ServerSessionHandler()
PORTAL_SESSIONS = PortalSessionHandler()

View file

@ -167,6 +167,12 @@ AT_INITIAL_SETUP_HOOK_MODULE = "game.gamesrc.world.at_initial_setup"
###################################################
# Default command sets
###################################################
# Note that with the exception of the unloggedin set (which is not
# stored anywhere), changing these paths will only affect NEW created
# characters, not those already in play. So if you plan to change
# this, it's recommended you do it on a pristine setup only. To
# dynamically add new commands to a running server, extend/overload
# these existing sets instead.
# Command set used before player has logged in
CMDSET_UNLOGGEDIN = "game.gamesrc.commands.basecmdset.UnloggedinCmdSet"
@ -246,6 +252,8 @@ PERMISSION_PLAYER_DEFAULT = "Players"
# Tuple of modules implementing lock functions. All callable functions
# inside these modules will be available as lock functions.
LOCK_FUNC_MODULES = ("src.locks.lockfuncs",)
# Module holding server-side functions for out-of-band protocols to call.
OOB_FUNC_MODULE = ""
###################################################

View file

@ -73,6 +73,7 @@ def create_object(typeclass, key=None, location=None,
# this will either load the typeclass or the default one
new_object = new_db_object.typeclass
if not object.__getattribute__(new_db_object, "is_typeclass")(typeclass, exact=True):
# this will fail if we gave a typeclass as input and it still gave us a default
SharedMemoryModel.delete(new_db_object)
@ -105,6 +106,10 @@ def create_object(typeclass, key=None, location=None,
# perform a move_to in order to display eventual messages.
if home:
new_object.home = home
else:
new_object.home = settings.CHARACTER_DEFAULT_HOME
if location:
new_object.move_to(location, quiet=True)
else:
@ -389,6 +394,8 @@ def create_player(name, email, password,
from src.players.models import PlayerDB
from src.players.player import Player
if not email:
email = "dummy@dummy.com"
if user:
new_user = user
else:

View file

@ -542,7 +542,7 @@ def has_parent(basepath, obj):
def mod_import(mod_path, propname=None):
"""
Takes filename of a module, converts it to a python path
Takes filename of a module (a python path or a full pathname)
and imports it. If property is given, return the named
property from this module instead of the module itself.
"""
@ -624,3 +624,15 @@ def string_from_module(modpath, variable=None):
if not mvars:
return None
return mvars[random.randint(0, len(mvars)-1)]
def init_new_player(player):
"""
Helper method to call all hooks, set flags etc on a newly created
player (and potentially their character, if it exists already)
"""
# the FIRST_LOGIN flags are necessary for the system to call
# the relevant first-login hooks.
if player.character:
player.character.db.FIRST_LOGIN = True
player.db.FIRST_LOGIN = True