mirror of
https://github.com/evennia/evennia.git
synced 2026-03-23 08:16:30 +01:00
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:
parent
7d8dbaf775
commit
ea1e88cbe9
3 changed files with 310 additions and 1323 deletions
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
Loading…
Add table
Add a link
Reference in a new issue