2011-05-12 21:51:11 +00:00
"""
2012-04-01 22:23:47 +02:00
Evennia menu system .
2011-05-12 21:51:11 +00:00
Contribution - Griatch 2011
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
2012-04-01 22:23:47 +02:00
in one or more collumns .
2011-05-12 21:51:11 +00:00
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 arbitrary code to act on the
system and the calling object when they are selected .
There is also a simple Yes / No function supplied . This will create a
one - off Yes / No question and executes a given code depending on which
choice was made .
2012-04-01 22:23:47 +02:00
To test , make sure to follow the instructions in
game / gamesrc / commands / examples / cmdset . py ( copy the template up one level
and change settings to point to the relevant cmdsets within ) . If you
already have such a module , you can of course use that . Next you
import and add the CmdTestMenu command to the end of the default cmdset in
this custom module .
The test command is also a good example of how to use this module in code .
2011-05-12 21:51:11 +00:00
"""
2012-04-01 22:23:47 +02:00
from ev import syscmdkeys
2011-05-12 21:51:11 +00:00
2012-03-25 18:11:14 +02:00
from ev import Command , CmdSet , utils
from ev import default_cmds
# imported only to make it available during execution of code blocks
import ev
CMD_NOMATCH = syscmdkeys . CMD_NOMATCH
CMD_NOINPUT = syscmdkeys . CMD_NOINPUT
2011-05-12 21:51:11 +00:00
#
2012-04-01 22:23:47 +02:00
# Commands used by the Menu system
#
2011-05-12 21:51:11 +00:00
class CmdMenuNode ( Command ) :
"""
2012-04-01 22:23:47 +02:00
Parent for menu selection commands .
2011-05-12 21:51:11 +00:00
"""
key = " selection "
aliases = [ ]
locks = " cmd:all() "
help_category = " Menu "
2012-04-01 22:23:47 +02:00
menutree = None
code = None
2011-05-12 21:51:11 +00:00
def func ( self ) :
" Execute a selection "
if self . code :
try :
exec ( self . code )
except Exception , 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 " )
2013-11-14 19:31:17 +01:00
2012-03-25 18:11:14 +02:00
class CmdMenuLook ( default_cmds . CmdLook ) :
2011-05-12 21:51:11 +00:00
"""
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 :
2012-04-01 22:23:47 +02:00
# if we have menu data, try to use that.
2011-05-12 21:51:11 +00:00
lookstring = self . caller . db . _menu_data . get ( " look " , None )
if lookstring :
self . caller . msg ( lookstring )
2012-04-01 22:23:47 +02:00
return
# otherwise we use normal look
2011-05-12 21:51:11 +00:00
super ( CmdMenuLook , self ) . func ( )
2013-11-14 19:31:17 +01:00
2012-03-25 18:11:14 +02:00
class CmdMenuHelp ( default_cmds . CmdHelp ) :
2011-05-12 21:51:11 +00:00
"""
2012-04-01 22:23:47 +02:00
help
2011-05-12 21:51:11 +00:00
Usage :
help
Get help specific to the menu , if available . If not ,
2012-04-01 22:23:47 +02:00
works like the normal help command .
2011-05-12 21:51:11 +00:00
"""
key = " help "
aliases = " h "
locks = " cmd:all() "
help_category = " Menu "
2012-04-01 22:23:47 +02:00
2011-05-12 21:51:11 +00:00
def func ( self ) :
" implement the menu help command "
if self . caller . db . _menu_data :
2012-04-01 22:23:47 +02:00
# if we have menu data, try to use that.
2011-05-12 21:51:11 +00:00
lookstring = self . caller . db . _menu_data . get ( " help " , None )
if lookstring :
self . caller . msg ( lookstring )
return
2012-04-01 22:23:47 +02:00
# otherwise we use normal help
2011-05-12 21:51:11 +00:00
super ( CmdMenuHelp , self ) . func ( )
2012-04-01 22:23:47 +02:00
2013-11-14 19:31:17 +01:00
2011-05-12 21:51:11 +00:00
class MenuCmdSet ( CmdSet ) :
"""
Cmdset for the menu . Will replace all other commands .
2012-04-01 22:23:47 +02:00
This always has a few basic commands available .
2011-05-12 21:51:11 +00:00
2012-04-01 22:23:47 +02:00
Note that you must always supply a way to exit the
cmdset manually !
2011-05-12 21:51:11 +00:00
"""
key = " menucmdset "
priority = 1
2012-04-01 22:23:47 +02:00
mergetype = " Replace "
2013-11-14 19:31:17 +01:00
2011-05-12 21:51:11 +00:00
def at_cmdset_creation ( self ) :
" populate cmdset "
2011-11-06 17:38:29 +01:00
pass
2011-05-12 21:51:11 +00:00
2013-11-14 19:31:17 +01:00
2011-05-12 21:51:11 +00:00
#
2012-04-01 22:23:47 +02:00
# Menu Node system
2011-05-12 21:51:11 +00:00
#
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 have 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
2012-04-01 22:23:47 +02:00
' START ' and ' END ' respectively .
2011-05-12 21:51:11 +00:00
"""
2013-11-14 19:31:17 +01:00
def __init__ ( self , caller , nodes = None ,
startnode = " START " , endnode = " END " , exec_end = " look " ) :
2011-05-12 21:51:11 +00:00
"""
We specify startnode / endnode so that the system knows where to
enter and where to exit the menu tree . If nodes is given , it
shuld be a list of valid node objects to add to the tree .
2012-04-01 22:23:47 +02:00
2011-11-06 18:53:10 +01:00
exec_end - if not None , will execute the given command string
directly after the menu system has been exited .
2011-05-12 21:51:11 +00:00
"""
self . tree = { }
self . startnode = startnode
2012-04-01 22:23:47 +02:00
self . endnode = endnode
2011-11-06 18:53:10 +01:00
self . exec_end = exec_end
2012-04-01 22:23:47 +02:00
self . caller = caller
2011-05-12 21:51:11 +00:00
if nodes and utils . is_iter ( nodes ) :
for node in nodes :
self . add ( node )
def start ( self ) :
"""
Initialize the menu
"""
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 .
"""
menunode . init ( self )
2012-04-01 22:23:47 +02:00
self . tree [ menunode . key ] = menunode
2011-05-12 21:51:11 +00:00
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 .
"""
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
2013-11-14 19:31:17 +01:00
if self . exec_end is not None :
2011-11-06 18:53:10 +01:00
self . caller . execute_cmd ( self . exec_end )
2012-04-01 22:23:47 +02:00
return
# not exiting, look for a valid code.
2011-05-12 21:51:11 +00:00
node = self . tree . get ( key , None )
2012-04-01 22:23:47 +02:00
if node :
2011-05-12 21:51:11 +00:00
if node . code :
# Execute eventual code active on this
# node. self.caller is available at this point.
try :
exec ( node . code )
2013-11-14 19:31:17 +01:00
except Exception :
2012-04-01 22:23:47 +02:00
self . caller . msg ( " { rCode could not be executed for node %s . Continuing anyway. { n " % key )
2011-05-12 21:51:11 +00:00
# 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
2013-11-14 19:31:17 +01:00
self . caller . db . _menu_data = { " help " : node . helptext ,
" look " : str ( node . text ) }
2012-04-01 22:23:47 +02:00
# display the node
2011-05-12 21:51:11 +00:00
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 )
"""
2012-04-01 22:23:47 +02:00
def __init__ ( self , key , text = " " , links = None , linktexts = None ,
2013-11-14 19:31:17 +01:00
keywords = None , cols = 1 , helptext = None ,
selectcmds = None , code = " " , nodefaultcmds = False , separator = " " ) :
2011-05-12 21:51:11 +00:00
"""
key - the unique identifier of this node .
2013-11-14 19:31:17 +01:00
text - is the text that will be displayed at top when viewing this
node .
links - a list of keys for unique menunodes this is connected to .
The actual keys will not printed - keywords will be used
( or a number )
linktexts - an optional list of texts to describe the links . Must
match link list if defined . Entries can be None to not
generate any extra text for a particular link .
keywords - an optional list of unique keys for choosing links . Must
match links list . If not given , index numbers will be used .
Also individual list entries can be None and will be replaed
by indices . If CMD_NOMATCH or CMD_NOENTRY , no text will be
generated to indicate the option exists .
2011-05-12 21:51:11 +00:00
cols - how many columns to use for displaying options .
2013-11-14 19:31:17 +01:00
helptext - if defined , this is shown when using the help command
instead of the normal help index .
selectcmds - a list of custom cmdclasses for handling each option .
Must match links list , but some entries may be set to None
to use default menu cmds . The given command ' s key will be
used for the menu list entry unless it ' s CMD_NOMATCH or
CMD_NOENTRY , in which case no text will be generated . These
commands have access to self . menutree and so can be used to
select nodes .
code - functional code . This will be executed just before this
node is loaded ( i . e . as soon after it ' s been selected from
another node ) . self . caller is available to call from this
code block , as well as ev .
nodefaultcmds - if true , don ' t offer the default help and look commands
in the node
2012-04-01 22:23:47 +02:00
separator - this string will be put on the line between menu nodes5B .
2011-05-12 21:51:11 +00:00
"""
self . key = key
self . cmdset = None
self . links = links
self . linktexts = linktexts
self . keywords = keywords
self . cols = cols
2011-11-06 17:38:29 +01:00
self . selectcmds = selectcmds
2011-05-12 21:51:11 +00:00
self . code = code
2011-11-06 17:38:29 +01:00
self . nodefaultcmds = nodefaultcmds
2011-11-06 19:16:34 +01:00
self . separator = separator
2011-11-06 17:38:29 +01:00
Nlinks = len ( self . links )
2012-04-01 22:23:47 +02:00
# validate the input
2011-05-12 21:51:11 +00:00
if not self . links :
self . links = [ ]
2011-11-06 17:38:29 +01:00
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 ) ]
2011-05-12 21:51:11 +00:00
2012-04-01 22:23:47 +02:00
# Format default text for the menu-help command
if not helptext :
2011-11-06 17:38:29 +01:00
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 :
2012-04-01 22:23:47 +02:00
helptext + = " %s , " % ( i + 1 )
2011-11-06 17:38:29 +01:00
helptext = helptext . rstrip ( " , " ) + " ) "
2011-05-12 21:51:11 +00:00
self . helptext = helptext
2012-04-01 22:23:47 +02:00
# Format text display
2011-05-12 21:51:11 +00:00
string = " "
if text :
string + = " %s \n " % text
# format the choices into as many collumns as specified
choices = [ ]
for ilink , link in enumerate ( self . links ) :
2011-11-06 17:38:29 +01:00
choice = " "
if self . keywords [ ilink ] :
if self . keywords [ ilink ] not in ( CMD_NOMATCH , CMD_NOINPUT ) :
choice + = " { g %s { n " % self . keywords [ ilink ]
2011-05-12 21:51:11 +00:00
else :
2012-04-01 22:23:47 +02:00
choice + = " { g %i { n " % ( ilink + 1 )
2011-11-06 17:38:29 +01:00
if self . linktexts [ ilink ] :
choice + = " - %s " % self . linktexts [ ilink ]
2011-05-12 21:51:11 +00:00
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 :
2012-04-01 22:23:47 +02:00
break
2011-05-12 21:51:11 +00:00
ftable = utils . format_table ( cols )
for row in ftable :
2013-11-14 19:31:17 +01:00
string + = " \n " + " " . join ( row )
2012-04-01 22:23:47 +02:00
# store text
2011-11-06 19:16:34 +01:00
self . text = self . separator + " \n " + string . rstrip ( )
2011-05-12 21:51:11 +00:00
def init ( self , menutree ) :
"""
2013-11-14 19:31:17 +01:00
Called by menu tree . Initializes the commands needed by
the menutree structure .
2012-04-01 22:23:47 +02:00
"""
# Create the relevant cmdset
2011-05-12 21:51:11 +00:00
self . cmdset = MenuCmdSet ( )
2011-11-06 17:38:29 +01:00
if not self . nodefaultcmds :
# add default menu commands
self . cmdset . add ( CmdMenuLook ( ) )
2012-04-01 22:23:47 +02:00
self . cmdset . add ( CmdMenuHelp ( ) )
for i , link in enumerate ( self . links ) :
2011-11-06 17:38:29 +01:00
if self . selectcmds [ i ] :
cmd = self . selectcmds [ i ] ( )
else :
cmd = CmdMenuNode ( )
cmd . key = str ( i + 1 )
2012-04-01 22:23:47 +02:00
# this is the operable command, it moves us to the next node.
2011-11-06 17:38:29 +01:00
cmd . code = " self.menutree.goto( ' %s ' ) " % link
2012-04-01 22:23:47 +02:00
# also custom commands get access to the menutree.
2011-05-12 21:51:11 +00:00
cmd . menutree = menutree
2011-11-06 17:38:29 +01:00
if self . keywords [ i ] and cmd . key not in ( CMD_NOMATCH , CMD_NOINPUT ) :
2011-05-12 21:51:11 +00:00
cmd . aliases = [ self . keywords [ i ] ]
2011-11-06 17:38:29 +01:00
self . cmdset . add ( cmd )
2011-05-12 21:51:11 +00:00
def __str__ ( self ) :
" Returns the string representation. "
2012-04-01 22:23:47 +02:00
return self . text
2011-05-12 21:51:11 +00:00
#
# 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
2012-04-01 22:23:47 +02:00
# make use the node system since there is only one level of choice.
#
2011-05-12 21:51:11 +00:00
def prompt_yesno ( caller , question = " " , yescode = " " , nocode = " " , default = " N " ) :
"""
This sets up a simple yes / no questionnaire . Question will
2012-04-01 22:23:47 +02:00
be asked , followed by a Y / [ N ] prompt where the [ x ] signifies
the default selection .
2011-05-12 21:51:11 +00:00
"""
# creating and defining commands
2012-04-01 22:23:47 +02:00
cmdyes = CmdMenuNode ( )
2011-05-12 21:51:11 +00:00
cmdyes . key = " yes "
cmdyes . aliases = [ " y " ]
2013-11-14 19:31:17 +01:00
# this will be executed in the context of the yes command (so
# self.caller will be available)
2011-05-12 21:51:11 +00:00
cmdyes . code = yescode + " \n self.caller.cmdset.delete( ' menucmdset ' ) \n del self.caller.db._menu_data "
2012-04-01 22:23:47 +02:00
cmdno = CmdMenuNode ( )
2011-05-12 21:51:11 +00:00
cmdno . key = " no "
cmdno . aliases = [ " n " ]
# this will be executed in the context of the no command
cmdno . code = nocode + " \n self.caller.cmdset.delete( ' menucmdset ' ) \n del self.caller.db._menu_data "
errorcmd = CmdMenuNode ( )
errorcmd . key = CMD_NOMATCH
2012-04-01 22:23:47 +02:00
errorcmd . code = " self.caller.msg( ' Please choose either Yes or No. ' ) "
2011-05-12 21:51:11 +00:00
defaultcmd = CmdMenuNode ( )
defaultcmd . key = CMD_NOINPUT
defaultcmd . code = " self.caller.execute_cmd( ' %s ' ) " % default
# creating cmdset (this will already have look/help commands)
yesnocmdset = MenuCmdSet ( )
yesnocmdset . add ( cmdyes )
yesnocmdset . add ( cmdno )
yesnocmdset . add ( errorcmd )
yesnocmdset . add ( defaultcmd )
2012-04-01 22:23:47 +02:00
# assinging menu data flags to caller.
2013-11-14 19:31:17 +01:00
caller . db . _menu_data = { " help " : " Please select Yes or No. " ,
" look " : " Please select Yes or No. " }
2012-04-01 22:23:47 +02:00
# assign cmdset and ask question
2011-05-12 21:51:11 +00:00
caller . cmdset . add ( yesnocmdset )
if default == " Y " :
prompt = " [Y]/N "
else :
prompt = " Y/[N] "
prompt = " %s %s : " % ( question , prompt )
caller . msg ( prompt )
2013-11-14 19:31:17 +01:00
2011-05-12 21:51:11 +00:00
#
# Menu command test
#
class CmdMenuTest ( Command ) :
"""
testing menu module
2012-04-01 22:23:47 +02:00
Usage :
menu
menu yesno
2011-05-12 21:51:11 +00:00
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 "
2013-11-14 19:31:17 +01:00
2011-05-12 21:51:11 +00:00
def func ( self ) :
" Testing the menu system "
2012-04-01 22:23:47 +02:00
2011-05-12 21:51:11 +00:00
if not self . args or self . args != " yesno " :
2012-04-01 22:23:47 +02:00
# testing the full menu-tree system
2011-05-12 21:51:11 +00:00
2012-04-01 22:23:47 +02:00
node0 = MenuNode ( " START " , text = " Start node. Select one of the links below. Here the links are ordered in one column. " ,
2011-05-12 21:51:11 +00:00
links = [ " node1 " , " node2 " , " END " ] , linktexts = [ " Goto first node " , " Goto second node " , " Quit " ] )
2012-04-01 22:23:47 +02:00
node1 = MenuNode ( " node1 " , text = " First node. This node shows letters instead of numbers for the choices. " ,
2011-05-12 21:51:11 +00:00
links = [ " END " , " START " ] , linktexts = [ " Quit " , " Back to start " ] , keywords = [ " q " , " b " ] )
2012-04-01 22:23:47 +02:00
node2 = MenuNode ( " node2 " , text = " Second node. This node lists choices in two columns. " ,
2011-05-12 21:51:11 +00:00
links = [ " node3 " , " START " ] , linktexts = [ " Set an attribute " , " Back to start " ] , cols = 2 )
2012-04-01 22:23:47 +02:00
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 ,
2011-05-12 21:51:11 +00:00
code = " self.caller.db.menutest= ' Testing! ' " )
node4 = MenuNode ( " node4 " , text = " Attribute ' menutest ' removed again. " ,
2012-04-01 22:23:47 +02:00
links = [ " node2 " ] , linktexts = [ " Back to second node. " ] , cols = 2 ,
2011-05-12 21:51:11 +00:00
code = " del self.caller.db.menutest " )
2012-04-01 22:23:47 +02:00
node5 = MenuNode ( " node5 " , links = [ " node4 " , " node2 " ] , linktexts = [ " Remove attribute " , " Back to second node. " ] , cols = 2 ,
code = " self.caller.msg( ' %s / %s = %s ' % (self.caller.key, ' menutest ' , self.caller.db.menutest)) " )
2011-05-12 21:51:11 +00:00
2012-04-01 22:23:47 +02:00
menu = MenuTree ( self . caller , nodes = ( node0 , node1 , node2 , node3 , node4 , node5 ) )
2011-05-12 21:51:11 +00:00
menu . start ( )
2012-04-01 22:23:47 +02:00
else :
2011-05-12 21:51:11 +00:00
" Testing the yesno question "
2012-04-01 22:23:47 +02:00
prompt_yesno ( self . caller , question = " Please answer yes or no - Are you the master of this mud or not? " ,
yescode = " self.caller.msg( ' { gGood for you! { n ' ) " ,
nocode = " self.caller.msg( ' { GNow you are just being modest ... { n ' ) " ,
2011-05-12 21:51:11 +00:00
default = " N " )