Removed the old contrib/menusystem.py. Use EvMenu instead. Renamed evmenu_login contrib to simply menu_login.py for compatibility.

This commit is contained in:
Griatch 2016-04-24 15:36:41 +02:00
parent 7d8dbaf775
commit ea1e88cbe9
3 changed files with 310 additions and 1323 deletions

View file

@ -1,387 +0,0 @@
"""
A login menu using EvMenu.
Contribution - Vincent-lg 2016
This module defines a simple login system, similar to the one
defined in 'menu_login.py". This present menu system, however,
uses EvMenu (hence the name). This module contains the
functions (nodes) of the menu, with the CmdSet and
UnloggedCommand called when a user logs in. In other words,
instead of using the 'connect' or 'create' commands once on the
login screen, players have to navigate through a simple menu
asking them to enter their username (then password), or to type
'new' to create one. You may want to update your login screen
if you use this system.
In order to install, to your settings file, add/edit the line:
CMDSET_UNLOGGEDIN = "contrib.evmenu_login.UnloggedinCmdSet"
When you'll reload the server, new sessions will connect to the
new login system, where they will be able to:
* Enter their username, assuming they have an existing player.
* Enter 'NEW' to create a new player.
The top-level functions in this file are menu nodes (as
described in EvMenu). Each one of these functions is
responsible for prompting the user with a specific information
(username, password and so on). At the bottom of the file are
defined the CmdSet for Unlogging users, which adds a new command
(defined below) that is called just after a new session has been
created, in order to create the menu. See the specific
documentation on functions (nodes) to see what each one should
do.
"""
import re
from textwrap import dedent
from django.conf import settings
from evennia import Command, CmdSet
from evennia import logger
from evennia import managers
from evennia import ObjectDB
from evennia.server.models import ServerConfig
from evennia import syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import random_string_from_module
## Constants
RE_VALID_USERNAME = re.compile(r"^[a-z]{3,}$", re.I)
LEN_PASSWD = 6
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
## Menu notes (top-level functions)
def start(caller):
"""The user should enter his/her username or NEW to create one.
This node is called at the very beginning of the menu, when
a session has been created OR if an error occurs further
down the menu tree. From there, users can either enter a
username (if this username exists) or type NEW (capitalized
or not) to create a new player.
"""
text = random_string_from_module(CONNECTION_SCREEN_MODULE)
text += "\n\nEnter your username or |yNEW|n to create a new account."
options = (
{ "key": "",
"goto": "start",
},
{
"key": "new",
"goto": "create_account",
},
{ "key": "quit",
"goto": "quit"
},
{
"key": "_default",
"goto": "username",
},
)
return text, options
def username(caller, string_input):
"""Check that the username leads to an existing player.
Check that the specified username exists. If the username doesn't
exist, display an error message and ask the user to try again. If
entering an empty string, return to start node. If user exists,
move to the next node (enter password).
"""
string_input = string_input.strip()
player = managers.players.get_player_from_name(string_input)
if player is None:
text = dedent("""
|rThe username '{}' doesn't exist. Have you created it?|n
Try another name or leave empty to go back.
""".strip("\n")).format(string_input)
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "username",
},
)
else:
caller.ndb._menutree.player = player
text = "Enter the password for the {} account.".format(player.name)
# Disables echo for the password
caller.msg("", options={"echo": False})
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "password",
},
)
return text, options
def password(caller, string_input):
"""Ask the user to enter the password to this player.
This is assuming the user exists (see 'create_username' and
'create_password'). This node "loops" if needed: if the
user specifies a wrong password, offers the user to try
again or to go back by entering 'b'.
If the password is correct, then login.
"""
menutree = caller.ndb._menutree
string_input = string_input.strip()
# Check the password and login is correct; also check for bans
player = menutree.player
password_attempts = menutree.password_attempts \
if hasattr(menutree, "password_attempts") else 0
bans = ServerConfig.objects.conf("server_bans")
banned = bans and (any(tup[0] == player.name.lower() for tup in bans) \
or any(tup[2].match(caller.address) for tup in bans if tup[2]))
if not player.check_password(string_input):
# Didn't enter a correct password
password_attempts += 1
if password_attempts > 2:
# Too many tries
caller.sessionhandler.disconnect(
caller, "|rToo many failed attempts. Disconnecting.|n")
text = ""
options = {}
else:
menutree.password_attempts = password_attempts
text = dedent("""
|rIncorrect password.|n
Try again or leave empty to go back.
""".strip("\n"))
# Loops on the same node
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "password",
},
)
elif banned:
# This is a banned IP or name!
string = dedent("""
|rYou have been banned and cannot continue from here.
If you feel this ban is in error, please email an admin.|n
Disconnecting.
""".strip("\n"))
caller.sessionhandler.disconnect(caller, string)
text = ""
options = {}
else:
# We are OK, log us in.
text = ""
options = {}
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, player)
return text, options
def create_account(caller):
"""Create a new account.
This node simply prompts the user to entere a username.
The input is redirected to 'create_username'.
"""
text = "Enter your new account name."
options = (
{
"key": "_default",
"goto": "create_username",
},
)
return text, options
def create_username(caller, string_input):
"""Prompt to enter a valid username (one that doesnt exist).
'string_input' contains the new username. If it exists, prompt
the username to retry or go back to the login screen.
"""
menutree = caller.ndb._menutree
string_input = string_input.strip()
player = managers.players.get_player_from_name(string_input)
# If a player with that name exists, a new one will not be created
if player:
text = dedent("""
|rThe account {} already exists.|n
Enter another username or leave blank to go back.
""".strip("\n")).format(string_input)
# Loops on the same node
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "create_username",
},
)
elif not RE_VALID_USERNAME.search(string_input):
text = dedent("""
|rThis username isn't valid.|n
Only letters are accepted, without special characters.
The username must be at least 3 characters long.
Enter another username or leave blank to go back.
""".strip("\n"))
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "create_username",
},
)
else:
# a valid username - continue getting the password
menutree.playername = string_input
# Disables echo for entering password
caller.msg("", options={"echo": False})
# Redirects to the creation of a password
text = "Enter this account's new password."
options = (
{
"key": "_default",
"goto": "create_password",
},
)
return text, options
def create_password(caller, string_input):
"""Ask the user to create a password.
This node is at the end of the menu for account creation. If
a proper MULTI_SESSION is configured, a character is also
created with the same name (we try to login into it).
"""
menutree = caller.ndb._menutree
text = ""
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "create_password",
},
)
password = string_input.strip()
playername = menutree.playername
if len(password) < LEN_PASSWD:
# The password is too short
text = dedent("""
|rYour password must be at least {} characters long.|n
Enter another password or leave it empty to go back.
""".strip("\n")).format(LEN_PASSWD)
else:
# Everything's OK. Create the new player account and
# possibly the character, depending on the multisession mode
from evennia.commands.default import unloggedin
# We make use of the helper functions from the default set here.
try:
permissions = settings.PERMISSION_PLAYER_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
new_player = unloggedin._create_player(caller, playername,
password, permissions)
if new_player:
if settings.MULTISESSION_MODE < 2:
default_home = ObjectDB.objects.get_id(
settings.DEFAULT_HOME)
unloggedin._create_character(caller, new_player,
typeclass, default_home, permissions)
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.
caller.msg(dedent("""
|rAn error occurred.|n Please e-mail an admin if
the problem persists. Try another password or leave
it empty to go back to the login screen.
""".strip("\n")))
logger.log_trace()
else:
text = ""
caller.msg("|gWelcome, your new account has been created!|n")
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, new_player)
return text, options
def quit(caller):
caller.sessionhandler.disconnect(caller, "Goodbye! Logging off.")
return "", {}
## Other functions
def _formatter(nodetext, optionstext, caller=None):
"""Do not display the options, only the text.
This function is used by EvMenu to format the text of nodes.
Options are not displayed for this menu, where it doesn't often
make much sense to do so. Thus, only the node text is displayed.
"""
return nodetext
## Commands and CmdSets
class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state"
key = "DefaultUnloggedin"
priority = 0
def at_cmdset_creation(self):
"Called when cmdset is first created."
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 = syscmdkeys.CMD_LOGINSTART
locks = "cmd:all()"
arg_regex = r"^$"
def func(self):
"Execute the menu"
EvMenu(self.caller, "evennia.contrib.evmenu_login",
startnode="start", auto_look=False, auto_quit=False,
cmd_on_exit=None, node_formatter=_formatter)

View file

@ -1,325 +1,364 @@
"""
Menu-driven login system
A login menu using EvMenu.
Contribution - Griatch 2011
Contribution - Vincent-lg 2016
This module defines a simple login system, similar to the one
defined in 'menu_login.py". This present menu system, however,
uses EvMenu (hence the name). This module contains the
functions (nodes) of the menu, with the CmdSet and
UnloggedCommand called when a user logs in. In other words,
instead of using the 'connect' or 'create' commands once on the
login screen, players have to navigate through a simple menu
asking them to enter their username (then password), or to type
'new' to create one. You may want to update your login screen
if you use this system.
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).
In order to install, to your settings file, add/edit the line:
CMDSET_UNLOGGEDIN = "contrib.evmenu_login.UnloggedinCmdSet"
Install is simple:
When you'll reload the server, new sessions will connect to the
new login system, where they will be able to:
To your settings file, add/edit the line:
* Enter their username, assuming they have an existing player.
* Enter 'NEW' to create a new player.
CMDSET_UNLOGGEDIN = "contrib.menu_login.UnloggedinCmdSet"
That's it. Reload the server and try to log in to see it.
You will want to change the login "graphic", which defaults to give
information about commands which are not used in this version of the
login. You can change the screen used by editing
`$GAME_DIR/server/conf/connection_screens.py`.
The top-level functions in this file are menu nodes (as
described in EvMenu). Each one of these functions is
responsible for prompting the user with a specific information
(username, password and so on). At the bottom of the file are
defined the CmdSet for Unlogging users, which adds a new command
(defined below) that is called just after a new session has been
created, in order to create the menu. See the specific
documentation on functions (nodes) to see what each one should
do.
"""
import re
from textwrap import dedent
from django.conf import settings
from evennia import managers
from evennia import utils, logger, create_player
from evennia import ObjectDB
from evennia import Command, CmdSet
from evennia import syscmdkeys
from evennia import logger
from evennia import managers
from evennia import ObjectDB
from evennia.server.models import ServerConfig
from evennia import syscmdkeys
from evennia.utils.evmenu import EvMenu
from evennia.utils.utils import random_string_from_module
from evennia.contrib.menusystem import MenuNode, MenuTree
CMD_LOGINSTART = syscmdkeys.CMD_LOGINSTART
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
MULTISESSION_MODE = settings.MULTISESSION_MODE
## Constants
RE_VALID_USERNAME = re.compile(r"^[a-z]{3,}$", re.I)
LEN_PASSWD = 6
CONNECTION_SCREEN_MODULE = settings.CONNECTION_SCREEN_MODULE
## Menu notes (top-level functions)
# 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.
#
def start(caller):
"""The user should enter his/her username or NEW to create one.
# Menu entry 1a - Entering a Username
This node is called at the very beginning of the menu, when
a session has been created OR if an error occurs further
down the menu tree. From there, users can either enter a
username (if this username exists) or type NEW (capitalized
or not) to create a new player.
class CmdBackToStart(Command):
"""
Step back to node0
text = random_string_from_module(CONNECTION_SCREEN_MODULE)
text += "\n\nEnter your username or |yNEW|n to create a new account."
options = (
{ "key": "",
"goto": "start",
},
{
"key": "new",
"goto": "create_account",
},
{ "key": "quit",
"goto": "quit"
},
{
"key": "_default",
"goto": "username",
},
)
return text, options
def username(caller, string_input):
"""Check that the username leads to an existing player.
Check that the specified username exists. If the username doesn't
exist, display an error message and ask the user to try again. If
entering an empty string, return to start node. If user exists,
move to the next node (enter password).
"""
key = CMD_NOINPUT
locks = "cmd:all()"
string_input = string_input.strip()
player = managers.players.get_player_from_name(string_input)
if player is None:
text = dedent("""
|rThe username '{}' doesn't exist. Have you created it?|n
Try another name or leave empty to go back.
""".strip("\n")).format(string_input)
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "username",
},
)
else:
caller.ndb._menutree.player = player
text = "Enter the password for the {} account.".format(player.name)
# Disables echo for the password
caller.msg("", options={"echo": False})
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "password",
},
)
def func(self):
"Execute the command"
self.menutree.goto("START")
return text, options
def password(caller, string_input):
"""Ask the user to enter the password to this player.
This is assuming the user exists (see 'create_username' and
'create_password'). This node "loops" if needed: if the
user specifies a wrong password, offers the user to try
again or to go back by entering 'b'.
If the password is correct, then login.
class CmdUsernameSelect(Command):
"""
Handles the entering of a username and
checks if it exists.
"""
key = CMD_NOMATCH
locks = "cmd:all()"
menutree = caller.ndb._menutree
string_input = string_input.strip()
def func(self):
"Execute the command"
player = managers.players.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")
# Check the password and login is correct; also check for bans
player = menutree.player
password_attempts = menutree.password_attempts \
if hasattr(menutree, "password_attempts") else 0
bans = ServerConfig.objects.conf("server_bans")
banned = bans and (any(tup[0] == player.name.lower() for tup in bans) \
or any(tup[2].match(caller.address) for tup in bans if tup[2]))
if not player.check_password(string_input):
# Didn't enter a correct password
password_attempts += 1
if password_attempts > 2:
# Too many tries
caller.sessionhandler.disconnect(
caller, "|rToo many failed attempts. Disconnecting.|n")
text = ""
options = {}
else:
# store the player so next step can find it
self.menutree.player = player
self.caller.msg(echo=False)
self.menutree.goto("node1b")
menutree.password_attempts = password_attempts
text = dedent("""
|rIncorrect password.|n
Try again or leave empty to go back.
""".strip("\n"))
# Loops on the same node
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "password",
},
)
elif banned:
# This is a banned IP or name!
string = dedent("""
|rYou have been banned and cannot continue from here.
If you feel this ban is in error, please email an admin.|n
Disconnecting.
""".strip("\n"))
caller.sessionhandler.disconnect(caller, string)
text = ""
options = {}
else:
# We are OK, log us in.
text = ""
options = {}
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, player)
return text, options
# Menu entry 1b - Entering a Password
def create_account(caller):
"""Create a new account.
This node simply prompts the user to entere a username.
The input is redirected to 'create_username'.
class CmdPasswordSelectBack(Command):
"""
Steps back from the Password selection
text = "Enter your new account name."
options = (
{
"key": "_default",
"goto": "create_username",
},
)
return text, options
def create_username(caller, string_input):
"""Prompt to enter a valid username (one that doesnt exist).
'string_input' contains the new username. If it exists, prompt
the username to retry or go back to the login screen.
"""
key = CMD_NOINPUT
locks = "cmd:all()"
menutree = caller.ndb._menutree
string_input = string_input.strip()
player = managers.players.get_player_from_name(string_input)
def func(self):
"Execute the command"
self.menutree.goto("node1a")
self.caller.msg(echo=True)
# If a player with that name exists, a new one will not be created
if player:
text = dedent("""
|rThe account {} already exists.|n
Enter another username or leave blank to go back.
""".strip("\n")).format(string_input)
# Loops on the same node
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "create_username",
},
)
elif not RE_VALID_USERNAME.search(string_input):
text = dedent("""
|rThis username isn't valid.|n
Only letters are accepted, without special characters.
The username must be at least 3 characters long.
Enter another username or leave blank to go back.
""".strip("\n"))
options = (
{
"key": "",
"goto": "start",
},
{
"key": "_default",
"goto": "create_username",
},
)
else:
# a valid username - continue getting the password
menutree.playername = string_input
# Disables echo for entering password
caller.msg("", options={"echo": False})
# Redirects to the creation of a password
text = "Enter this account's new password."
options = (
{
"key": "_default",
"goto": "create_password",
},
)
return text, options
def create_password(caller, string_input):
"""Ask the user to create a password.
This node is at the end of the menu for account creation. If
a proper MULTI_SESSION is configured, a character is also
created with the same name (we try to login into it).
class CmdPasswordSelect(Command):
"""
Handles the entering of a password and logs into the game.
"""
key = CMD_NOMATCH
locks = "cmd:all()"
menutree = caller.ndb._menutree
text = ""
options = (
{
"key": "",
"exec": lambda caller: caller.msg("", options={"echo": True}),
"goto": "start",
},
{
"key": "_default",
"goto": "create_password",
},
)
def func(self):
"Execute the command"
self.caller.msg(echo=True)
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.check_password(self.args):
self.caller.msg("{rIncorrect password.{n")
self.menutree.goto("node1b")
return
# before going on, check eventual bans
bans = ServerConfig.objects.conf("server_bans")
if bans and (any(tup[0]==player.name.lower() for tup in bans)
or
any(tup[2].match(self.caller.address) for tup in bans if tup[2])):
# this is a banned IP or name!
string = "{rYou have been banned and cannot continue from here."
string += "\nIf you feel this ban is in error, please email an admin.{x"
self.caller.msg(string)
self.caller.sessionhandler.disconnect(self.caller, "Good bye! Disconnecting...")
return
# we are ok, log us in.
self.caller.msg("{gWelcome %s! Logging in ...{n" % player.key)
#self.caller.session_login(player)
self.caller.sessionhandler.login(self.caller, player)
# abort menu, do cleanup.
self.menutree.goto("END")
# 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 managers.players.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.caller.msg(echo=False)
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.caller.msg(echo=True)
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
self.caller.msg(echo=False)
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 "
string += "long, avoid making it a real word and mix numbers "
string += "into it.{n"
self.caller.msg(string)
self.menutree.goto("node2b")
return
# everything's ok. Create the new player account and possibly the character
# depending on the multisession mode
password = string_input.strip()
playername = menutree.playername
if len(password) < LEN_PASSWD:
# The password is too short
text = dedent("""
|rYour password must be at least {} characters long.|n
Enter another password or leave it empty to go back.
""".strip("\n")).format(LEN_PASSWD)
else:
# Everything's OK. Create the new player account and
# possibly the character, depending on the multisession mode
from evennia.commands.default import unloggedin
# we make use of the helper functions from the default set here.
# We make use of the helper functions from the default set here.
try:
permissions = settings.PERMISSION_PLAYER_DEFAULT
typeclass = settings.BASE_CHARACTER_TYPECLASS
new_player = unloggedin._create_player(self.caller, playername,
password, permissions)
new_player = unloggedin._create_player(caller, playername,
password, permissions)
if new_player:
if MULTISESSION_MODE < 2:
default_home = ObjectDB.objects.get_id(settings.DEFAULT_HOME)
unloggedin._create_character(self.caller, new_player, typeclass,
default_home, permissions)
# 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")
if settings.MULTISESSION_MODE < 2:
default_home = ObjectDB.objects.get_id(
settings.DEFAULT_HOME)
unloggedin._create_character(caller, new_player,
typeclass, default_home, permissions)
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.
self.caller.msg("An error occurred. Please e-mail an admin if the problem persists.")
caller.msg(dedent("""
|rAn error occurred.|n Please e-mail an admin if
the problem persists. Try another password or leave
it empty to go back to the login screen.
""".strip("\n")))
logger.log_trace()
else:
text = ""
caller.msg("|gWelcome, your new account has been created!|n")
caller.msg("", options={"echo": True})
caller.sessionhandler.login(caller, new_player)
return text, options
# Menu entry 3 - help screen
def quit(caller):
caller.sessionhandler.disconnect(caller, "Goodbye! Logging off.")
return "", {}
## Other functions
def _formatter(nodetext, optionstext, caller=None):
"""Do not display the options, only the text.
This function is used by EvMenu to format the text of nodes.
Options are not displayed for this menu, where it doesn't often
make much sense to do so. Thus, only the node text is displayed.
LOGIN_SCREEN_HELP = \
"""
Welcome to %s!
return nodetext
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.sessionhandler.disconnect(self.caller, "Good bye! Disconnecting...")
# The login menu tree, using the commands above
START = MenuNode("START", text=utils.random_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])
node1b = MenuNode("node1b", text="Please enter your password (empty to go back).",
links=["node1a", "END"],
keywords=[CMD_NOINPUT, CMD_NOMATCH],
selectcmds=[CmdPasswordSelectBack, CmdPasswordSelect])
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])
node2b = MenuNode("node2b", text="Please enter your password (empty to go back).",
links=["node2a", "START"],
helptext="Try to pick a long and hard-to-guess password.",
keywords=[CMD_NOINPUT, CMD_NOMATCH],
selectcmds=[CmdPasswordCreateBack, CmdPasswordCreate])
node3 = MenuNode("node3", text=LOGIN_SCREEN_HELP,
links=["START"],
helptext="",
keywords=[CMD_NOINPUT],
selectcmds=[CmdBackToStart])
# access commands
## Commands and CmdSets
class UnloggedinCmdSet(CmdSet):
"Cmdset for the unloggedin state"
@ -337,14 +376,12 @@ class CmdUnloggedinLook(Command):
when the player first connects. It sets up the menu before handing off
to the menu's own look command.
"""
key = CMD_LOGINSTART
# obs, this should NOT have aliases for look or l, this will clash with the menu version!
key = syscmdkeys.CMD_LOGINSTART
locks = "cmd:all()"
arg_regex = r"^$"
def func(self):
"Execute the menu"
menu = MenuTree(self.caller, nodes=(START, node1a, node1b,
node2a, node2b, node3),
exec_end=None)
menu.start()
EvMenu(self.caller, "evennia.contrib.evmenu_login",
startnode="start", auto_look=False, auto_quit=False,
cmd_on_exit=None, node_formatter=_formatter)

View file

@ -1,663 +0,0 @@
"""
Evennia menu system.
Contribution - Griatch 2011
> Note that the evennia/utils/evmenu.py module is probably a better and
more flexible implementation of a menu system than this. Try that
first.
This module offers the ability for admins to let their game be fully
or partly menu-driven. Menu choices can be numbered or use arbitrary
keys. There are also some formatting options, such a putting options
in one or more columns.
The menu system consists of a MenuTree object populated by MenuNode
objects. Nodes are linked together with automatically created commands
so the player may select and traverse the menu. Each node can display
text and show options, but also execute a callback to act on the
system and the calling object when they are selected.
There is also a simple Yes/No function as well as a one-level choice
function supplied. This will create a one-off Yes/No question or a
one-level choice. These helpers will execute a given callback
depending on which choice was made.
To start a menu, define the nodes of the menu and then add the
following to a command `func` you can call:
```python
menu = MenuTree(self.caller, nodes=(...))
menu.start()
```
This will switch you into menu-mode. See `contrib/menu_login.py` for an
example of usage.
For a simple demonstration, add `CmdMenuTest` from this module to the default cmdset.
"""
from builtins import object, range
from types import MethodType
from evennia import syscmdkeys
from evennia import Command, CmdSet, utils
from evennia import default_cmds, logger
CMD_NOMATCH = syscmdkeys.CMD_NOMATCH
CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
#
# Commands used by the Menu system
#
class CmdMenuNode(Command):
"""
Parent for menu selection commands.
"""
key = "selection"
aliases = []
locks = "cmd:all()"
help_category = "Menu"
menutree = None
callback = None
def func(self):
"Execute a selection"
if self.callback:
try:
self.callback()
except Exception as e:
self.caller.msg("%s\n{rThere was an error with this selection.{n" % e)
else:
self.caller.msg("{rThis option is not available.{n")
class CmdMenuLook(default_cmds.CmdLook):
"""
ooc look
Usage:
look
This is a Menu version of the look command. It will normally show
the options available, otherwise works like the normal look
command.
"""
key = "look"
aliases = ["l", "ls"]
locks = "cmd:all()"
help_cateogory = "General"
def func(self):
"implement the menu look command"
if self.caller.db._menu_data:
# if we have menu data, try to use that.
lookstring = self.caller.db._menu_data.get("look", None)
if lookstring:
self.caller.msg(lookstring)
return
# otherwise we use normal look
super(CmdMenuLook, self).func()
class CmdMenuHelp(default_cmds.CmdHelp):
"""
help
Usage:
help
Get help specific to the menu, if available. If not,
works like the normal help command.
"""
key = "help"
aliases = "h"
locks = "cmd:all()"
help_category = "Menu"
def func(self):
"implement the menu help command"
if self.caller.db._menu_data:
# if we have menu data, try to use that.
lookstring = self.caller.db._menu_data.get("help", None)
if lookstring:
self.caller.msg(lookstring)
return
# otherwise we use normal help
super(CmdMenuHelp, self).func()
class MenuCmdSet(CmdSet):
"""
Cmdset for the menu. Will replace all other commands.
This always has a few basic commands available.
Note that you must always supply a way to exit the
cmdset manually!
"""
key = "menucmdset"
priority = 1
mergetype = "Replace"
# secure the menu against local cmdsets (but leave channels)
no_objs = True
no_exits = True
no_channels = False
def at_cmdset_creation(self):
"populate cmdset"
pass
#
# Menu Node system
#
class MenuTree(object):
"""
The menu tree object holds the full menu structure consisting of
MenuNodes. Each node is identified by a unique key. The tree
allows for traversal of nodes as well as entering and exiting the
tree as needed. For safety, being in a menu will not survive a
server reboot.
A menutree has two special node keys given by 'startnode' and
'endnode' arguments. The startnode is where the user will start
upon first entering the menu. The endnode need not actually
exist, the moment it is linked to and that link is used, the menu
will be exited and cleanups run. The default keys for these are
'START' and 'END' respectively.
"""
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
should be a list of valid node objects to add to the tree.
caller (Object): The caller triggering the menu
nodes (tuple, optional): A tuple of `MenuNode` objects. This need
not be in any particular order.
startnode (str, optional): The key of the first `MenuNode` to jump
to when starting the menu. Defaults to "START".
endnode (str, optional): The key of the end node. When
instructed to go to this node (by any means), the menu
will be gracefully exited. Defaults to "END".
exec_end (str, optional): If not `None`, this command name will be executed
directly after the menu system has been exited. It is
normally useful for making sure the user realizes their UI
mode has changed.
"""
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:
self.add(node)
def start(self):
"""
Initialize the menu and go to the starting node.
"""
self.goto(self.startnode)
def add(self, menunode):
"""
Add a menu node object to the tree. Each node itself keeps
track of which nodes it is connected to.
Args:
menunode (MenuNode): The node to add.
"""
self.tree[menunode.key] = menunode
def goto(self, key):
"""
Go to a key in the tree. This sets up the cmdsets on the
caller so that they match the choices in that node.
Args:
key (str): The node-key to go to.
"""
if key == self.endnode:
# if we was given the END node key, we clean up immediately.
self.caller.cmdset.delete("menucmdset")
del self.caller.db._menu_data
if self.exec_end is not None:
self.caller.execute_cmd(self.exec_end)
return
# not exiting, look for a valid node
node = self.tree.get(key, None)
# make caller available on node
node.caller = self.caller
if node:
# call on-node callback
if node.callback:
try:
node.callback()
except Exception:
logger.log_trace()
self.caller.msg("{rNode callback could not be executed for node %s. Continuing anyway.{n" % key)
# initialize - this creates new cmdset
node.init(self)
# clean old menu cmdset and replace with the new one
self.caller.cmdset.delete("menucmdset")
self.caller.cmdset.add(node.cmdset)
# set the menu flag data for the default commands
self.caller.db._menu_data = {"help": node.helptext,
"look": str(node.text)}
# display the node
self.caller.msg(node.text)
else:
self.caller.msg("{rMenu node '%s' does not exist - maybe it's not created yet..{n" % key)
class MenuNode(object):
"""
This represents a node in a menu tree. The node will display its
textual content and offer menu links to other nodes (the relevant
commands are created automatically)
"""
def __init__(self, key, text="", links=None, linktexts=None,
keywords=None, cols=1, helptext=None,
selectcmds=None, callback=None, nodefaultcmds=False, separator=""):
"""
Initialize the node.
Args:
key (str): The unique identifier of this node.
text (str, optional): The text that will be displayed at
top when viewing this node.
Kwargs:
links (list): A list of keys for unique menunodes this is connected to.
The actual keys will not printed - keywords will be
used (or a number)
linktexts (list)- A list of texts to describe the links. Must
match order of `links` list if defined. Entries can be
None to not generate any extra text for a particular
link.
keywords (list): A 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 (int): How many columns to use for displaying options.
helptext (str): If defined, this is shown when using the help command
instead of the normal help index.
selectcmds (list): 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.
callback (function): Function callback. This will be
called as callback(currentnode) just before this node is
loaded (i.e. as soon as possible as it's been selected
from another node). currentnode.caller is available.
nodefaultcmds (bool): If `True`, don't offer the default
help and look commands in the node
separator (str): This string will be put on the line
between menu nodes.
"""
self.key = key
self.cmdset = None
self.links = links
self.linktexts = linktexts
self.keywords = keywords
self.cols = cols
self.selectcmds = selectcmds
self.callback = MethodType(callback, self, MenuNode) if callback else None
self.nodefaultcmds = nodefaultcmds
self.separator = separator
Nlinks = len(self.links)
# validate the input
if not self.links:
self.links = []
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 ("
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
string = ""
if text:
string += "%s\n" % text
# format the choices into as many columns as specified
choices = []
for ilink, link in enumerate(self.links):
choice = ""
if self.keywords[ilink]:
if self.keywords[ilink] not in (CMD_NOMATCH, CMD_NOINPUT):
choice += "{g{lc%s{lt%s{le{n" % (self.keywords[ilink], self.keywords[ilink])
else:
choice += "{g {lc%i{lt%i{le{n" % ((ilink + 1), (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:
for i in range(len(cols)):
if not choices:
cols[i].append("")
else:
cols[i].append(choices.pop(0))
if not choices:
break
ftable = utils.format_table(cols)
for row in ftable:
string += "\n" + "".join(row)
# store text
self.text = self.separator + "\n" + string.rstrip()
def init(self, menutree):
"""
Called by menu tree. Initializes the commands needed by
the menutree structure.
"""
# Create the relevant cmdset
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):
if self.selectcmds[i]:
cmd = self.selectcmds[i]()
else:
# this is the operable command, it moves us to the next node.
cmd = CmdMenuNode()
cmd.set_key(str(i + 1))
cmd.link = link
def _callback(self):
self.menutree.goto(self.link)
cmd.callback = MethodType(_callback, cmd, CmdMenuNode)
# also custom commands get access to the menutree.
cmd.menutree = menutree
if self.keywords[i] and cmd.key not in (CMD_NOMATCH, CMD_NOINPUT):
cmd.set_aliases(self.keywords[i])
self.cmdset.add(cmd)
def __str__(self):
"Returns the string representation."
return self.text
#
# A simple yes/no question. Call this from a command to give object
# a cmdset where they may say yes or no to a question. Does not
# make use the node system since there is only one level of choice.
#
def prompt_yesno(caller, question="", yesfunc=None, nofunc=None, default="N"):
"""
This sets up a simple yes/no questionnaire. Question will be
asked, followed by a Y/[N] prompt where the [x] signifies the
default selection. Note that this isn't actually making use of the
menu node system, but does use the MenuCmdSet.
Args:
caller (Object): The object triggering the prompt.
question (str, optional): The Yes/No question asked.
yesfunc (function, optional): Callback for a Yes answer.
nofunc (functionm optional): Callback for a No answer.
default (str, optional): Default used if caller just hits
return. Either `"Y"` or `"N"`
"""
# creating and defining commands
cmdyes = CmdMenuNode(key="yes", aliases=["y"])
if yesfunc:
cmdyes.yesfunc = yesfunc
def _yesfunc(self):
self.caller.cmdset.delete('menucmdset')
del self.caller.db._menu_data
self.yesfunc(self)
cmdyes.callback = MethodType(_yesfunc, cmdyes, CmdMenuNode)
cmdno = CmdMenuNode(key="no", aliases=["n"])
if nofunc:
cmdno.nofunc = nofunc
def _nofunc(self):
self.caller.cmdset.delete('menucmdset')
del self.caller.db._menu_data
self.nofunc(self) if self.nofunc else None
cmdno.callback = MethodType(_nofunc, cmdno, CmdMenuNode)
errorcmd = CmdMenuNode(key=CMD_NOMATCH)
def _errorcmd(self):
self.caller.msg("Please choose either Yes or No.")
errorcmd.callback = MethodType(_errorcmd, errorcmd, CmdMenuNode)
defaultcmd = CmdMenuNode(key=CMD_NOINPUT)
def _defaultcmd(self):
self.caller.execute_cmd('%s' % default)
defaultcmd.callback = MethodType(_defaultcmd, defaultcmd, CmdMenuNode)
# creating cmdset (this will already have look/help commands)
yesnocmdset = MenuCmdSet()
yesnocmdset.add(cmdyes)
yesnocmdset.add(cmdno)
yesnocmdset.add(errorcmd)
yesnocmdset.add(defaultcmd)
yesnocmdset.add(CmdMenuLook())
yesnocmdset.add(CmdMenuHelp())
# assinging menu data flags to caller.
caller.db._menu_data = {"help": "Please select Yes or No.",
"look": "Please select Yes or No."}
# assign cmdset and ask question
caller.cmdset.add(yesnocmdset)
if default == "Y":
prompt = "{lcY{lt[Y]{le/{lcN{ltN{le"
else:
prompt = "{lcY{ltY{le/{lcN{lt[N]{le"
prompt = "%s %s: " % (question, prompt)
caller.msg(prompt)
#
# A simple choice question. Call this from a command to give object
# a cmdset where they need to make a choice. Does not
# make use the node system since there is only one level of choice.
#
def prompt_choice(caller, question="", prompts=None, choicefunc=None, force_choose=False):
"""
This sets up a simple choice questionnaire. Question will be
asked, followed by a series of prompts. Note that this isn't
making use of the menu node system but uses the MenuCmdSet.
Args:
caller (object): The object calling and being offered the choice
question (str, optional): Text describing the offered choice
prompts (list, optional): List of strings defining the available choises.
choicefunc (function, optional): A function called as
`choicefunc(self)` when a choice is made. Inside this function,
`self.caller` is available and `self.prompt_index` is the index
(starting with 0) matching the chosen prompt in the `prompts` list.
force_choose - force user to make a choice
Examples:
```python
def mychoice(self):
self.caller.msg("Index of choice is %s." % self.prompt_index)
prompt_choice(caller, "Make a choice:", prompts=["A","B","C"], choicefunc=mychoice)
```
When triggering the above from a command or @py prompt you get the following options:
>>> Make a choice:
[1] A
[2] B
[3] C
<<< 2
>>> Index of choice is 1.
"""
# creating and defining commands
count = 0
choices = ""
commands = []
for choice in utils.make_iter(prompts):
# create the available choice-commands
count += 1
choices += "\n{lc%d{lt[%d]{le %s" % (count, count, choice)
cmdfunc = CmdMenuNode(key="%d" % count)
cmdfunc.prompt_index = count-1
if choicefunc:
cmdfunc.choicefunc = choicefunc
def _choicefunc(self):
self.caller.cmdset.delete('menucmdset')
del self.caller.db._menu_data
self.choicefunc(self)
# set a new method "callback" on cmdfunc
cmdfunc.callback = MethodType(_choicefunc, cmdfunc, CmdMenuNode)
commands.append(cmdfunc)
if not force_choose:
choices += "\n{lc{lt[No choice]{le"
prompt = question + choices + "\nPlease choose one."
# create the error-reporting command
errorcmd = CmdMenuNode(key=CMD_NOMATCH)
if force_choose:
def _errorcmd(self):
self.caller.msg("You can only choose given choices.")
else:
if choicefunc:
errorcmd.choicefunc = choicefunc
def _errorcmd(self):
self.caller.msg("No choice.")
self.caller.cmdset.delete('menucmdset')
del self.caller.db._menu_data
self.choicefunc(self)
errorcmd.callback = MethodType(_errorcmd, errorcmd, CmdMenuNode)
# create the fallback command
defaultcmd = CmdMenuNode(key=CMD_NOINPUT)
if force_choose:
def _defaultcmd(self):
caller.msg(prompt)
else:
if choicefunc:
defaultcmd.choicefunc = choicefunc
def _defaultcmd(self):
self.caller.msg("No choice.")
self.caller.cmdset.delete('menucmdset')
del self.caller.db._menu_data
self.choicefunc(self)
defaultcmd.callback = MethodType(_defaultcmd, defaultcmd, CmdMenuNode)
# creating cmdset (this will already have look/help commands)
choicecmdset = MenuCmdSet()
for cmdfunc in commands:
choicecmdset.add(cmdfunc)
choicecmdset.add(errorcmd)
choicecmdset.add(defaultcmd)
choicecmdset.add(CmdMenuLook())
choicecmdset.add(CmdMenuHelp())
# assigning menu data flags to caller.
caller.db._menu_data = {"help": "Please select.",
"look": prompt}
# assign cmdset and ask question
caller.cmdset.add(choicecmdset)
caller.msg(prompt)
#
# Menu command test
#
class CmdMenuTest(Command):
"""
testing menu module
Usage:
menu
menu yesno
This will test the menu system. The normal operation will produce
a small menu tree you can move around in. The 'yesno' option will
instead show a one-time yes/no question.
"""
key = "menu"
locks = "cmd:all()"
help_category = "Menu"
def func(self):
"Testing the menu system"
if self.args.strip() == "yesno":
"Testing the yesno question"
prompt_yesno(self.caller, question="Please answer yes or no - Are you the master of this mud or not?",
yesfunc=lambda self: self.caller.msg('{gGood for you!{n'),
nofunc=lambda self: self.caller.msg('{GNow you are just being modest ...{n'),
default="N")
else:
# testing the full menu-tree system
node0 = MenuNode("START", text="Start node. Select one of the links below. Here the links are ordered in one column.",
links=["node1", "node2", "END"], linktexts=["Goto first node", "Goto second node", "Quit"])
node1 = MenuNode("node1", text="First node. This node shows letters instead of numbers for the choices.",
links=["END", "START"], linktexts=["Quit", "Back to start"], keywords=["q","b"])
node2 = MenuNode("node2", text="Second node. This node lists choices in two columns.",
links=["node3", "START"], linktexts=["Set an attribute", "Back to start"], cols=2)
node3 = MenuNode("node3", text="Attribute 'menutest' set on you. You can examine it (only works if you are allowed to use the examine command) or remove it. You can also quit and examine it manually.",
links=["node4", "node5", "node2", "END"], linktexts=["Remove attribute", "Examine attribute",
"Back to second node", "Quit menu"], cols=2,
callback=lambda self: self.caller.attributes.add("menutest",'Testing!'))
node4 = MenuNode("node4", text="Attribute 'menutest' removed again.",
links=["node2"], linktexts=["Back to second node."], cols=2,
callback=lambda self: self.caller.attributes.remove("menutest"))
node5 = MenuNode("node5", links=["node4", "node2"], linktexts=["Remove attribute", "Back to second node."], cols=2,
callback=lambda self: self.caller.msg('%s/%s = %s' % (self.caller.key, 'menutest', self.caller.db.menutest)))
menu = MenuTree(self.caller, nodes=(node0, node1, node2, node3, node4, node5))
menu.start()