Made bettermenu into evmenu and moved to utils. Still some issues with MXP.

This commit is contained in:
Griatch 2015-07-08 12:35:38 +02:00
parent 698e226763
commit fe810ec5f7
2 changed files with 95 additions and 59 deletions

View file

@ -3,6 +3,10 @@ Evennia menu system.
Contribution - Griatch 2011
> Note that the evennia/utils/menu.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

View file

@ -1,48 +1,66 @@
"""
(Better) MenuSystem
EvMenu
Evennia contribution - Griatch 2015
This implements a full menu system for Evennia. It is considerably
more flexible than the older contrib/menusystem.py and also uses
menu plugin modules.
To start the menu, just import the EvMenu class from this module,
This implements a better menu system for Evennia. Contrary to the old
contrib menusystem, this is controlled from a simple module with
function definitions, rather than building a set of classes with
arguments.
```python
To start the menu, just import the Menu class from this module,
and call
from evennia.utils.evmenu import EvMenu
from evennia.contrib.bettermenusystem import Menu
Menu(caller, menu_module_path,
startnode="start", allow_quit=True,
cmdset_mergetype="Replace", cmdset_priority=1):
EvMenu(caller, menu_module_path,
startnode="start",
cmdset_mergetype="Replace", cmdset_priority=1,
allow_quit=True, cmd_on_quit="look")
```
Where `caller` is the Object to use the menu on - it will get a new
cmdset while using the Menu. The menu_module_path is the python path
to a python module containing function defintions. By adjusting the
keyword options of the Menu() initialization call you can start the
menu at different places in the menu definition file, adjust if the
menu command should overload the normal commands or not etc.
menu command should overload the normal commands or not, etc.
The menu is defined in a module with function defintions:
The menu is defined in a module (this can be the same module as the
command definition too) with function defintions:
```python
def nodename1(caller):
# code
return text, options
The return values must be given in the above order, but each can be
given as None as well
def nodename2(caller, input_string):
# code
return text, options
```
text (str or tuple): Text shown at this node. If a tuple, the second
Where caller is the object using the menu and input_string is the
command entered by the user on the *previous* node (the command
entered to get to this node). The node function code will only be
executed once per node-visit and the system will accept nodes with
both one or two arguments interchangeably.
The return values must be given in the above order, but each can be
returned as None as well. If the options are returned as None, the
menu is immediately exited and the default "look" command is called.
text (str, tuple or None): Text shown at this node. If a tuple, the second
element in the tuple is a help text to display at this node when
the user enters the menu help command there.
helptext (str): Help text shown at this node.
options (tuple): ( {'key': name, # can also be a list of aliases
'desc': description, # option description
'goto': nodekey, # node to go to when chosen
'exec': nodekey, # node or callback to trigger as callback when chosen
{...}, ...)
options (tuple, dict or None): ( {'key': name, # can also be a list of aliases. A special key is "_default", which
# marks this option as the default fallback when no other
# option matches the user input.
'desc': description, # option description
'goto': nodekey, # node to go to when chosen
'exec': nodekey, # node or callback to trigger as callback when chosen. If a node
# key is given the node will be executed once but its return u
# values are ignored. If a callable is given, it must accept
# one or two args, like any node.
{...}, ...)
If key is not given, the option will automatically be identified by
its number 1..N.
@ -103,7 +121,9 @@ The menu tree is exited either by using the in-menu quit command or by
reaching a node without any options.
For a menu demo, import CmdTestDemo form this
For a menu demo, import CmdTestDemo from this module and add it to
your default cmdset. Run it with this module, like `testdemo
evennia.utils.evdemo`.
"""
@ -113,7 +133,7 @@ from django.conf import settings
from evennia import syscmdkeys
from evennia import Command, CmdSet
from evennia.utils.evtable import EvTable
from evennia.utils.ansi import ANSIString
from evennia.utils.ansi import ANSIString, strip_raw_ansi
from evennia.utils.utils import mod_import, make_iter, pad, m_len
# read from protocol NAWS later?
@ -124,17 +144,18 @@ _CMD_NOINPUT = syscmdkeys.CMD_NOINPUT
# Return messages
_ERR_NOT_IMPLEMENTED = "Menu node '{nodename}' is not implemented. Make another choice."
_ERR_GENERAL = "Error in menu node '{nodename}'."
_ERR_NO_OPTION_DESC = "No description."
_HELP_FULL = "Commands: <menu option>, help, quit"
_HELP_NO_QUIT = "Commands: <menu option>, help"
_HELP_NO_OPTIONS = "Commands: help, quit"
_HELP_NO_OPTIONS_NO_QUIT = "Commands: help"
# i18n
from django.utils.translation import ugettext as _
_ERR_NOT_IMPLEMENTED = _("Menu node '{nodename}' is not implemented. Make another choice.")
_ERR_GENERAL = _("Error in menu node '{nodename}'.")
_ERR_NO_OPTION_DESC = _("No description.")
_HELP_FULL = _("Commands: <menu option>, help, quit")
_HELP_NO_QUIT = _("Commands: <menu option>, help")
_HELP_NO_OPTIONS = _("Commands: help, quit")
_HELP_NO_OPTIONS_NO_QUIT = _("Commands: help")
class MenuError(RuntimeError):
class EvMenuError(RuntimeError):
"""
Error raised by menu when facing internal errors.
@ -147,7 +168,7 @@ class MenuError(RuntimeError):
#
#------------------------------------------------------------
class CmdMenuNode(Command):
class CmdEvMenuNode(Command):
"""
Menu options.
@ -168,13 +189,14 @@ class CmdMenuNode(Command):
if not menu:
err = "Menu object not found as %s.ndb._menutree!" % (caller)
self.caller.msg(err)
raise MenuError(err)
raise EvMenuError(err)
# flags and data
raw_string = self.raw_string
cmd = raw_string.strip().lower()
options = menu.options
allow_quit = menu.allow_quit
cmd_on_quit = menu.cmd_on_quit
default = menu.default
if cmd in options:
@ -191,7 +213,8 @@ class CmdMenuNode(Command):
caller.msg(menu.helptext)
elif allow_quit and cmd in ("quit", "q", "exit"):
menu.close_menu()
caller.execute_cmd("look")
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
elif default:
goto, callback = default
if callback:
@ -204,10 +227,11 @@ class CmdMenuNode(Command):
if not (options or default):
# no options - we are at the end of the menu.
menu.close_menu()
caller.execute_cmd("looK")
if cmd_on_quit is not None:
caller.execute_cmd(cmd_on_quit)
class MenuCmdSet(CmdSet):
class EvMenuCmdSet(CmdSet):
"""
The Menu cmdset replaces the current cmdset.
@ -223,7 +247,7 @@ class MenuCmdSet(CmdSet):
"""
Called when creating the set.
"""
self.add(CmdMenuNode())
self.add(CmdEvMenuNode())
#------------------------------------------------------------
#
@ -231,14 +255,15 @@ class MenuCmdSet(CmdSet):
#
#------------------------------------------------------------
class Menu(object):
class EvMenu(object):
"""
This object represents an operational menu. It is initialized from
a menufile.py instruction.
"""
def __init__(self, caller, menufile, startnode="start",
allow_quit=True, cmdset_mergetype="Replace", cmdset_priority=1):
cmdset_mergetype="Replace", cmdset_priority=1,
allow_quit=True, cmd_on_quit="look"):
"""
Initialize the menu tree and start the caller onto the first node.
@ -246,9 +271,6 @@ class Menu(object):
caller (str): The user of the menu.
menufile (str): The full or relative path to the menufile.
startnode (str, optional): The starting node in the menufile.
allow_quit (bool, optional): Allow user to use quit or
exit to leave the menu at any point. Recommended during
development!
cmdset_mergetype (str, optional): 'Replace' (default) means the menu
commands will be exclusive - no other normal commands will
be usable while the user is in the menu. 'Union' means the
@ -261,8 +283,17 @@ class Menu(object):
cmdset_priority (int, optional): The merge priority for the
menu command set. The default (1) is usually enough for most
types of menus.
allow_quit (bool, optional): Allow user to use quit or
exit to leave the menu at any point. Recommended during
development!
cmd_on_quit (str or None, optional): When exiting the menu
(either by reaching a node with no options or by using the
in-built quit command (activated with `allow_quit`), this
command string will be executed. Set to None to not call
any command.
Raises:
MenuError: If the start/end node is not found in menu tree.
EvMenuError: If the start/end node is not found in menu tree.
"""
self._caller = caller
@ -270,10 +301,11 @@ class Menu(object):
self._menutree = self._parse_menufile(menufile)
if startnode not in self._menutree:
raise MenuError("Start node '%s' not in menu tree!" % startnode)
raise EvMenuError("Start node '%s' not in menu tree!" % startnode)
# variables made available to the command
self.allow_quit = allow_quit
self.cmd_on_quit = cmd_on_quit
self.default = None
self.nodetext = None
self.helptext = None
@ -283,7 +315,7 @@ class Menu(object):
self._caller.ndb._menutree = self
# set up the menu command on the caller
menu_cmdset = MenuCmdSet()
menu_cmdset = EvMenuCmdSet()
menu_cmdset.mergetype = str(cmdset_mergetype).lower().capitalize() or "Replace"
menu_cmdset.priority = int(cmdset_priority)
self._caller.cmdset.add(menu_cmdset)
@ -355,7 +387,7 @@ class Menu(object):
table_width_max = max(table_width_max,
max(m_len(p) for p in key.split("\n")) +
max(m_len(p) for p in desc.split("\n")) + colsep)
table.append(ANSIString(" {lc%s{lt{w%s{n{le: %s" % (key, key, desc)))
table.append(ANSIString(" {lc%s{lt%s{le: %s" % (key, key, desc)))
ncols = (_MAX_TEXT_WIDTH // table_width_max) + 1 # number of ncols
nlastcol = nlist % ncols # number of elements left in last row
@ -411,7 +443,7 @@ class Menu(object):
node = self._menutree[nodename]
except KeyError:
self._caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
raise MenuError
raise EvMenuError
try:
# the node should return data as (text, options)
if len(getargspec(node).args) > 1:
@ -422,7 +454,7 @@ class Menu(object):
nodetext, options = node(self._caller)
except KeyError:
self._caller.msg(_ERR_NOT_IMPLEMENTED.format(nodename=nodename))
raise MenuError
raise EvMenuError
except Exception:
self._caller.msg(_ERR_GENERAL.format(nodename=nodename))
raise
@ -458,7 +490,7 @@ class Menu(object):
try:
# execute the node; we make no use of the return values here.
self._execute_node(nodename, raw_string)
except MenuError:
except EvMenuError:
return
def goto(self, nodename, raw_string):
@ -475,7 +507,7 @@ class Menu(object):
try:
# execute the node, make use of the returns.
nodetext, options = self._execute_node(nodename, raw_string)
except MenuError:
except EvMenuError:
return
# validation of the node return values
@ -511,7 +543,7 @@ class Menu(object):
display_options.append((keys[0], desc))
for key in keys:
if goto or execute:
self.options[key.strip().lower()] = (goto, execute)
self.options[strip_raw_ansi(key).strip().lower()] = (goto, execute)
self.nodetext = self._format_node(nodetext, display_options)
@ -529,7 +561,7 @@ class Menu(object):
"""
Shutdown menu; occurs when reaching the end node.
"""
self._caller.cmdset.remove(MenuCmdSet)
self._caller.cmdset.remove(EvMenuCmdSet)
del self._caller.ndb._menutree
@ -595,6 +627,7 @@ def test_view_node(caller):
text = """
Your name is {g%s{n!
click {lclook{lthere{le to trigger a look command under MXP.
This node's option has no explicit key (nor the "_default" key
set), and so gets assigned a number automatically. You can infact
-always- use numbers (1...N) to refer to listed options also if you
@ -632,7 +665,6 @@ def test_end_node(caller):
return text, None
class CmdTestMenu(Command):
"""
Test menu
@ -648,7 +680,7 @@ class CmdTestMenu(Command):
def func(self):
if not self.args:
self.caller.msg("Usage: testmenu <menumodule>")
self.caller.msg("Usage: testmenu menumodule")
return
# start menu
Menu(self.caller, self.args.strip(), startnode="test_start_node", cmdset_mergetype="Replace")
EvMenu(self.caller, self.args.strip(), startnode="test_start_node", cmdset_mergetype="Replace")