Added a new 'contrib' folder for optional code snippets not suitable for the server core. Added contrib/menusystem for implementing a multi-choice menu system. Added contrib/lineeditor - a powerful line editor with commands mimicking VI. Also added an example NPC class using the menu system to allow for a conversation. As part of creating these contributions, lots of bugs were found and fixed. A new and more powerful cmdparser was intruduced as a result - this one is much easier to understand than the old one, while being more efficient and versatile. All testsuites were updated. Also: Resolves issue 165.

This commit is contained in:
Griatch 2011-05-12 21:51:11 +00:00
parent 2c47d6a66b
commit b9c1921a0b
16 changed files with 1426 additions and 354 deletions

34
contrib/README Normal file
View file

@ -0,0 +1,34 @@
'Contrib' folder
----------------
This folder contains 'contributions': extra snippets of code that are
potentially very useful for the game coder but which are considered
too game-specific to be a part of the main Evennia game server. These
modules are not used unless you explicitly import them.
If you want to edit, tweak or expand on this code you should copy the
things you want from here into game/gamesrc and change them there.
* Evennia MenuSystem (Griatch 2011) - A base set of classes and
cmdsets for creating in-game multiple-choice menus in
Evennia. The menu tree can be of any depth. Menu options can be
numbered or given custom keys, and each option can execute
code. Also contains a yes/no question generator function. This
is intended to be used by commands and presents a y/n question
to the user for accepting an action. Includes a simple new
command 'menu' for testing and debugging.
* Evennia Lineeditor (Griatch 2011) - A powerful line-by-line editor
for editing text in-game. Mimics the command names of the famous
VI text editor. Supports undo/redo, search/replace,
regex-searches, buffer formatting, indenting etc. It comes with
its own help system. (Makes minute use of the MenuSystem module
to show a y/n question if quitting without having
saved). Includes a basic command '@edit' for activating the
editor.
* Talking_NPC (Griatch 2011) - An example of a simple NPC object that
you can strike up a menu-driven converstaion with. Uses the
MenuSystem to allow conversation options. The npc object defines
a command 'talk' for starting the (brief) conversation.

0
contrib/__init__.py Normal file
View file

609
contrib/lineeditor.py Normal file
View file

@ -0,0 +1,609 @@
"""
Evennia Line Editor
Contribution - Griatch 2011
This implements an advanced line editor for editing longer texts
in-game. The editor mimics the command mechanisms of the VI editor as
far as possible.
Features of the editor:
undo/redo
edit/replace on any line of the buffer
search&replace text anywhere in buffer
formatting of buffer, or selection, to certain width + indentations
allow to echo the input or not depending on your client.
"""
import re
from src.commands.command import Command
from src.commands.cmdset import CmdSet
from src.commands.cmdhandler import CMD_NOMATCH, CMD_NOINPUT
from src.utils import utils
from contrib.menusystem import prompt_yesno
RE_GROUP = re.compile(r"\".*?\"|\'.*?\'|\S*")
class CmdEditorBase(Command):
"""
Base parent for editor commands
"""
locks = "cmd:all()"
help_entry = "LineEditor"
code = None
editor = None
def parse(self):
"""
Handles pre-parsing
Editor commands are on the form
:cmd [li] [w] [txt]
Where all arguments are optional.
li - line number (int), starting from 1. This could also be a range given as <l>:<l>
w - word(s) (string), could be encased in quotes.
txt - extra text (string), could be encased in quotes
"""
linebuffer = []
if self.editor:
linebuffer = self.editor.buffer.split("\n")
nlines = len(linebuffer)
# The regular expression will split the line by whitespaces,
# stripping extra whitespaces, except if the text is
# surrounded by single- or double quotes, in which case they
# will be kept together and extra whitespace preserved. You
# can input quotes on the line by alternating single and
# double quotes.
arglist = [part for part in RE_GROUP.findall(self.args) if part]
temp = []
for arg in arglist:
# we want to clean the quotes, but only one type, in case we are nesting.
if arg.startswith('"'):
arg.strip('"')
elif arg.startswith("'"):
arg.strip("'")
temp.append(arg)
arglist = temp
# A dumb split, without grouping quotes
words = self.args.split()
# current line number
cline = nlines - 1
# the first argument could also be a range of line numbers, on the
# form <lstart>:<lend>. Either of the ends could be missing, to
# mean start/end of buffer respectively.
lstart, lend = cline, cline + 1
linerange = False
if arglist and ':' in arglist[0]:
part1, part2 = arglist[0].split(':')
if part1 and part1.isdigit():
lstart = min(max(0, int(part1)) - 1, nlines)
linerange = True
if part2 and part2.isdigit():
lend = min(lstart + 1, int(part2)) + 1
linerange = True
elif arglist and arglist[0].isdigit():
lstart = min(max(0, int(arglist[0]) - 1), nlines)
lend = lstart + 1
linerange = True
if linerange:
arglist = arglist[1:]
# nicer output formatting of the line range.
lstr = ""
if not linerange or lstart + 1 == lend:
lstr = "line %i" % (lstart + 1)
else:
lstr = "lines %i-%i" % (lstart + 1, lend)
# arg1 and arg2 is whatever arguments. Line numbers or -ranges are never included here.
args = " ".join(arglist)
arg1, arg2 = "", ""
if len(arglist) > 1:
arg1, arg2 = arglist[0], " ".join(arglist[1:])
else:
arg1 = " ".join(arglist)
# store for use in func()
self.linebuffer = linebuffer
self.nlines = nlines
self.arglist = arglist
self.cline = cline
self.lstart = lstart
self.lend = lend
self.linerange = linerange
self.lstr = lstr
self.words = words
self.args = args
self.arg1 = arg1
self.arg2 = arg2
def func(self):
"Implements the Editor commands"
pass
class CmdLineInput(CmdEditorBase):
"""
No command match - Inputs line of text into buffer.
"""
key = CMD_NOMATCH
aliases = [CMD_NOINPUT]
def func(self):
"Adds the line without any formatting changes."
# add a line of text
if not self.editor.buffer:
buf = self.args
else:
buf = self.editor.buffer + "\n%s" % self.args
self.editor.update_buffer(buf)
if self.editor.echo_mode:
self.caller.msg("%02i| %s" % (self.cline + 1, self.args))
class CmdEditorGroup(CmdEditorBase):
"""
Commands for the editor
"""
key = ":editor_command_group"
aliases = [":","::", ":::", ":h", ":w", ":wq", ":q", ":q!", ":u", ":uu", ":UU",
":dd", ":dw", ":DD", ":y", ":x", ":p", ":i",
":r", ":I", ":A", ":s", ":S", ":f", ":fi", ":fd", ":echo"]
def func(self):
"""
This command handles all the in-editor :-style commands. Since each command
is small and very limited, this makes for a more efficient presentation.
"""
caller = self.caller
editor = self.editor
linebuffer = self.linebuffer
lstart, lend = self.lstart, self.lend
cmd = self.cmdstring
echo_mode = self.editor.echo_mode
string = ""
if cmd == ":":
# Echo buffer
if self.linerange:
buf = linebuffer[lstart:lend]
string = editor.display_buffer(buf=buf, offset=lstart)
else:
string = editor.display_buffer()
elif cmd == "::":
# Echo buffer without the line numbers and syntax parsing
if self.linerange:
buf = linebuffer[lstart:lend]
string = editor.display_buffer(buf=buf, offset=lstart, linenums=False)
else:
string = editor.display_buffer(linenums=False)
self.caller.msg(string, data={"raw":True})
return
elif cmd == ":::":
# Insert single colon alone on a line
editor.update_buffer(editor.buffer + "\n:")
if echo_mode:
string = "Single ':' added to buffer."
elif cmd == ":h":
# help entry
string = editor.display_help()
elif cmd == ":w":
# save without quitting
string = editor.save_buffer()
elif cmd == ":wq":
# save and quit
string = editor.save_buffer()
string += " " + editor.quit()
elif cmd == ":q":
# quit. If not saved, will ask
if self.editor.unsaved:
prompt_yesno(caller, "Save before quitting?",
yescode = "self.caller.ndb._lineeditor.save_buffer()\nself.caller.ndb._lineeditor.quit()",
nocode = "self.caller.msg(self.caller.ndb._lineeditor.quit())", default="Y")
else:
string = editor.quit()
elif cmd == ":q!":
# force quit, not checking saving
string = editor.quit()
elif cmd == ":u":
# undo
string = editor.update_undo(-1)
elif cmd == ":uu":
# redo
string = editor.update_undo(1)
elif cmd == ":UU":
# reset buffer
editor.update_buffer(editor.pristine_buffer)
string = "Reverted all changes to the buffer back to original state."
elif cmd == ":dd":
# :dd <l> - delete line <l>
buf = linebuffer[:lstart] + linebuffer[lend:]
editor.update_buffer(buf)
string = "Deleted %s." % (self.lstr)
elif cmd == ":dw":
# :dw <w> - delete word in entire buffer
# :dw <l> <w> delete word only on line(s) <l>
if not self.arg1:
string = "You must give a search word to delete."
else:
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Removed %s for lines %i-%i." % (self.arg1, lstart + 1 , lend + 1)
else:
string = "Removed %s for %s." % (self.arg1, self.lstr)
sarea = "\n".join(linebuffer[lstart:lend])
sarea = re.sub(r"%s" % self.arg1.strip("\'").strip('\"'), "", sarea, re.MULTILINE)
buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
editor.update_buffer(buf)
elif cmd == ":DD":
# clear buffer
editor.update_buffer("")
string = "Cleared %i lines from buffer." % self.nlines
elif cmd == ":y":
# :y <l> - yank line(s) to copy buffer
cbuf = linebuffer[lstart:lend]
editor.copy_buffer = cbuf
string = "%s, %s yanked." % (self.lstr.capitalize(), cbuf)
elif cmd == ":x":
# :x <l> - cut line to copy buffer
cbuf = linebuffer[lstart:lend]
editor.copy_buffer = cbuf
buf = linebuffer[:lstart] + linebuffer[lend:]
editor.update_buffer(buf)
string = "%s, %s cut." % (self.lstr.capitalize(), cbuf)
elif cmd == ":p":
# :p <l> paste line(s) from copy buffer
if not editor.copy_buffer:
string = "Copy buffer is empty."
else:
buf = linebuffer[:lstart] + editor.copy_buffer + linebuffer[lstart:]
editor.update_buffer(buf)
string = "Copied buffer %s to %s." % (editor.copy_buffer, self.lstr)
elif cmd == ":i":
# :i <l> <txt> - insert new line
new_lines = self.args.split('\n')
if not new_lines:
string = "You need to enter a new line and where to insert it."
else:
buf = linebuffer[:lstart] + new_lines + linebuffer[lstart:]
editor.update_buffer(buf)
string = "Inserted %i new line(s) at %s." % (len(new_lines), self.lstr)
elif cmd == ":r":
# :r <l> <txt> - replace lines
new_lines = self.args.split('\n')
if not new_lines:
string = "You need to enter a replacement string."
else:
buf = linebuffer[:lstart] + new_lines + linebuffer[lend:]
editor.update_buffer(buf)
string = "Replaced %i line(s) at %s." % (len(new_lines), self.lstr)
elif cmd == ":I":
# :I <l> <txt> - insert text at beginning of line(s) <l>
if not self.args:
string = "You need to enter text to insert."
else:
buf = linebuffer[:lstart] + ["%s%s" % (self.args, line) for line in linebuffer[lstart:lend]] + linebuffer[lend:]
editor.update_buffer(buf)
string = "Inserted text at beginning of %s." % self.lstr
elif cmd == ":A":
# :A <l> <txt> - append text after end of line(s)
if not self.args:
string = "You need to enter text to append."
else:
buf = linebuffer[:lstart] + ["%s%s" % (line, self.args) for line in linebuffer[lstart:lend]] + linebuffer[lend:]
editor.update_buffer(buf)
string = "Appended text to end of %s." % self.lstr
elif cmd == ":s":
# :s <li> <w> <txt> - search and replace words in entire buffer or on certain lines
if not self.arg1 or not self.arg2:
string = "You must give a search word and something to replace it with."
else:
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Search-replaced %s -> %s for lines %i-%i." % (self.arg1, self.arg2, lstart + 1 , lend)
else:
string = "Search-replaced %s -> %s for %s." % (self.arg1, self.arg2, self.lstr)
sarea = "\n".join(linebuffer[lstart:lend])
regex = r"%s|^%s(?=\s)|(?<=\s)%s(?=\s)|^%s$|(?<=\s)%s$"
regarg = self.arg1.strip("\'").strip('\"')
if " " in regarg:
regarg = regarg.replace(" ", " +")
sarea = re.sub(regex % (regarg, regarg, regarg, regarg, regarg), self.arg2.strip("\'").strip('\"'), sarea, re.MULTILINE)
buf = linebuffer[:lstart] + sarea.split("\n") + linebuffer[lend:]
editor.update_buffer(buf)
elif cmd == ":f":
# :f <l> flood-fill buffer or <l> lines of buffer.
width = 78
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Flood filled lines %i-%i." % (lstart + 1 , lend)
else:
string = "Flood filled %s." % self.lstr
fbuf = "\n".join(linebuffer[lstart:lend])
fbuf = utils.fill(fbuf, width=width)
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
editor.update_buffer(buf)
elif cmd == ":fi":
# :fi <l> indent buffer or lines <l> of buffer.
indent = " " * 4
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Indented lines %i-%i." % (lstart + 1 , lend)
else:
string = "Indented %s." % self.lstr
fbuf = [indent + line for line in linebuffer[lstart:lend]]
buf = linebuffer[:lstart] + fbuf + linebuffer[lend:]
editor.update_buffer(buf)
elif cmd == ":fd":
# :fi <l> indent buffer or lines <l> of buffer.
if not self.linerange:
lstart = 0
lend = self.cline + 1
string = "Removed left margin (dedented) lines %i-%i." % (lstart + 1 , lend)
else:
string = "Removed left margin (dedented) %s." % self.lstr
fbuf = "\n".join(linebuffer[lstart:lend])
fbuf = utils.dedent(fbuf)
buf = linebuffer[:lstart] + fbuf.split("\n") + linebuffer[lend:]
editor.update_buffer(buf)
elif cmd == ":echo":
# set echoing on/off
editor.echo_mode = not editor.echo_mode
string = "Echo mode set to %s" % editor.echo_mode
caller.msg(string)
class EditorCmdSet(CmdSet):
"CmdSet for the editor commands"
key = "editorcmdset"
mergetype = "Replace"
class LineEditor(object):
"""
This defines a line editor object. It creates all relevant commands
and tracks the current state of the buffer. It also cleans up after
itself.
"""
def __init__(self, caller, loadcode="", savecode="", key=""):
"""
caller - who is using the editor
loadcode - code to execute in order to load already existing text into the buffer
savecode - code to execute in order to save the result
key = an optional key for naming this session (such as which attribute is being edited)
"""
self.key = key
self.caller = caller
self.caller.ndb._lineeditor = self
self.buffer = ""
self.unsaved = False
if loadcode:
try:
exec(loadcode)
except Exception, e:
caller.msg("%s\n{rBuffer loadcode failed. Could not load initial data.{n" % e)
# Create the commands we need
cmd1 = CmdLineInput()
cmd1.editor = self
cmd1.obj = self
cmd2 = CmdEditorGroup()
cmd2.obj = self
cmd2.editor = self
# Populate cmdset and add it to caller
editor_cmdset = EditorCmdSet()
editor_cmdset.add(cmd1)
editor_cmdset.add(cmd2)
self.caller.cmdset.add(editor_cmdset)
# store the original version
self.pristine_buffer = self.buffer
self.savecode = savecode
self.sep = "-"
# undo operation buffer
self.undo_buffer = [self.buffer]
self.undo_pos = 0
self.undo_max = 20
# copy buffer
self.copy_buffer = []
# echo inserted text back to caller
self.echo_mode = False
# show the buffer ui
self.caller.msg(self.display_buffer())
def update_buffer(self, buf):
"""
This should be called when the buffer has been changed somehow.
It will handle unsaved flag and undo updating.
"""
if utils.is_iter(buf):
buf = "\n".join(buf)
if buf != self.buffer:
self.buffer = buf
self.update_undo()
self.unsaved = True
def quit(self):
"Cleanly exit the editor."
del self.caller.ndb._lineeditor
self.caller.cmdset.delete(EditorCmdSet)
return "Exited editor."
def save_buffer(self):
"Saves the content of the buffer"
if self.unsaved:
try:
exec(self.savecode)
self.unsaved = False
return "Buffer saved."
except Exception, e:
return "%s\n{rSave code gave an error. Buffer not saved." % e
else:
return "No changes need saving."
def update_undo(self, step=None):
"""
This updates the undo position.
"""
if step and step < 0:
if self.undo_pos <= 0:
return "Nothing to undo."
self.undo_pos = max(0, self.undo_pos + step)
self.buffer = self.undo_buffer[self.undo_pos]
return "Undo."
elif step and step > 0:
if self.undo_pos >= len(self.undo_buffer) - 1 or self.undo_pos + 1 >= self.undo_max:
return "Nothing to redo."
self.undo_pos = min(self.undo_pos + step, min(len(self.undo_buffer), self.undo_max) - 1)
self.buffer = self.undo_buffer[self.undo_pos]
return "Redo."
if not self.undo_buffer or (self.undo_buffer and self.buffer != self.undo_buffer[self.undo_pos]):
self.undo_buffer = self.undo_buffer[:self.undo_pos + 1] + [self.buffer]
self.undo_pos = len(self.undo_buffer) - 1
def display_buffer(self, buf=None, offset=0, linenums=True):
"""
This displays the line editor buffer, or selected parts of it.
If buf is set and is not the full buffer, offset should define
the starting line number, to get the linenum display right.
"""
if buf == None:
buf = self.buffer
if utils.is_iter(buf):
buf = "\n".join(buf)
lines = buf.split('\n')
nlines = len(lines)
nwords = len(buf.split())
nchars = len(buf)
sep = self.sep
header = "{n" + sep * 10 + "Line Editor [%s]" % self.key + sep * (78-25-len(self.key))
footer = "{n" + sep * 10 + "[l:%02i w:%03i c:%04i]" % (nlines, nwords, nchars) + sep * 12 + "(:h for help)" + sep * 23
if linenums:
main = "\n".join("{b%02i|{n %s" % (iline + 1 + offset, line) for iline, line in enumerate(lines))
else:
main = "\n".join(lines)
string = "%s\n%s\n%s" % (header, main, footer)
return string
def display_help(self):
"""
Shows the help entry for the editor.
"""
string = self.sep*78 + """
<txt> - any non-command is appended to the end of the buffer.
: <l> - view buffer or only line <l>
:: <l> - view buffer without line numbers or other parsing
::: - print a ':' as the only character on the line...
:h - this help.
:w - saves the buffer (don't quit)
:wq - save buffer and quit
:q - quits (will be asked to save if buffer was changed)
:q! - quit without saving, no questions asked
:u - (undo) step backwards in undo history
:uu - (redo) step forward in undo history
:UU - reset all changes back to initial
:dd <l> - delete line <n>
:dw <l> <w> - delete word or regex <w> in entire buffer or on line <l>
:DD - clear buffer
:y <l> - yank (copy) line <l> to the copy buffer
:x <l> - cut line <l> and store it in the copy buffer
:p <l> - put (paste) previously copied line directly after <l>
:i <l> <txt> - insert new text <txt> at line <l>. Old line will be shifted down
:r <l> <txt> - replace line <l> with text <txt>
:I <l> <txt> - insert text at the beginning of line <l>
:A <l> <txt> - append text after the end of line <l>
:s <l> <w> <txt> - search/replace word or regex <w> in buffer or on line <l>
:f <l> - flood-fill entire buffer or line <l>
:fi <l> - indent entire buffer or line <l>
:fd <l> - de-indent entire buffer or line <l>
:echo - turn echoing of the input on/off (helpful for some clients)
Legend:
<l> - line numbers, or range lstart:lend, e.g. '3:7'.
<w> - one word or several enclosed in quotes.
<txt> - longer string, usually not needed to be enclosed in quotes.
""" + self.sep * 78
return string
#
# Editor access command for editing a given attribute on an object.
#
class CmdEditor(Command):
"""
start editor
Usage:
@editor <obj>/<attr>
This will start Evennia's powerful line editor, which
has a host of commands on its own. Use :h for a list
of commands.
"""
key = "@editor"
aliases = ["@edit"]
locks = "cmd:perm(editor) or perm(Builders)"
help_category = "Building"
def func(self):
"setup and start the editor"
if not self.args or not '/' in self.args:
self.caller.msg("Usage: @editor <obj>/<attrname>")
return
objname, attrname = [part.strip() for part in self.args.split("/")]
obj = self.caller.search(objname)
if not obj:
return
# the load/save codes define what the editor shall do when wanting to
# save the result of the editing. The editor makes self.buffer and
# self.caller available for this code - self.buffer holds the editable text.
loadcode = "obj = self.caller.search('%s')\n" % obj.id
loadcode += "if obj.db.%s: self.buffer = obj.db.%s" % (attrname, attrname)
savecode = "obj = self.caller.search('%s')\n" % obj.id
savecode += "obj.db.%s = self.buffer" % attrname
editor_key = "%s/%s" % (objname, attrname)
# start editor, it will handle things from here.
LineEditor(self.caller, loadcode=loadcode, savecode=savecode, key=editor_key)

413
contrib/menusystem.py Normal file
View file

@ -0,0 +1,413 @@
"""
Evennia menu system.
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
in one or more collumns.
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.
To test, import and add the CmdTestMenu command to the end of the default cmdset in
game.gamesrc.commands.basecmdset. The test command is also a good
example of how to use this module in code.
"""
from src.commands.cmdhandler import CMD_NOMATCH, CMD_NOINPUT
from src.commands.command import Command
from src.commands.cmdset import CmdSet
from src.commands.default.general import CmdLook
from src.commands.default.help import CmdHelp
from src.utils import utils
# imported only to make them available during execution of code blocks
from src.objects.models import ObjectDB
from src.players.models import PlayerDB
#
# 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
code = None
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")
class CmdMenuLook(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(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"
def at_cmdset_creation(self):
"populate cmdset"
self.add(CmdMenuLook())
self.add(CmdMenuHelp())
#
# 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 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
'START' and 'END' respectively.
"""
def __init__(self, caller, nodes=None, startnode="START", endnode="END"):
"""
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.
"""
self.tree = {}
self.startnode = startnode
self.endnode = endnode
self.caller = caller
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)
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.
"""
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
self.caller.execute_cmd("look")
return
# not exiting, look for a valid code.
node = self.tree.get(key, None)
if node:
if node.code:
# Execute eventual code active on this
# node. self.caller is available at this point.
try:
exec(node.code)
except Exception, e:
self.caller.msg("{rCode could not be executed for node %s. Continuing anyway.{n" % key)
# 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, code=""):
"""
key - the unique identifier of this node.
text - is the text that will be displayed at top when viewing this node.
links - a list of keys for unique menunodes this is connected to.
linktexts - a list of texts to describe the links. If defined, need to match links list
keywords - a list of unique keys for choosing links. Must match links list. If not given, index numbers will be used.
cols - how many columns to use for displaying options.
helptext - if defined, this is shown when using the help command instead of the normal help index.
code - functional code. This will be executed just before this node is loaded (i.e.
as soon after it's been selected from another node). self.caller is available
to call from this code block, as well as ObjectDB and PlayerDB.
"""
self.key = key
self.cmdset = None
self.links = links
self.linktexts = linktexts
self.keywords = keywords
self.cols = cols
self.code = code
# validate the input
if not self.links:
self.links = []
if not self.linktexts or (self.linktexts and len(self.linktexts) != len(self.links)):
self.linktexts = []
if not self.keywords or (self.keywords and len(self.keywords) != len(self.links)):
self.keywords = []
# Format default text for the menu-help command
if not helptext:
helptext = "Select one of the valid options"
if self.keywords:
helptext += " (" + ", ".join(self.keywords) + ")"
elif self.links:
helptext += " (" + ", ".join([str(i + 1) for i in range(len(self.links))]) + ")"
self.helptext = helptext
# Format text display
string = ""
if text:
string += "%s\n" % text
# format the choices into as many collumns as specified
choices = []
for ilink, link in enumerate(self.links):
if self.keywords:
choice = "{g%s{n" % self.keywords[ilink]
else:
choice = "{g%i{n" % (ilink + 1)
if self.linktexts:
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 = 78*"-" + "\n" + string.strip()
def init(self, menutree):
"""
Called by menu tree. Initializes the commands needed by the menutree structure.
"""
# Create the relevant cmdset
self.cmdset = MenuCmdSet()
for i, link in enumerate(self.links):
cmd = CmdMenuNode()
cmd.key = str(i + 1)
cmd.menutree = menutree
# this is the operable command, it moves us to the next node.
cmd.code = "self.menutree.goto('%s')" % link
if self.keywords:
cmd.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="", yescode="", nocode="", 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.
"""
# creating and defining commands
cmdyes = CmdMenuNode()
cmdyes.key = "yes"
cmdyes.aliases = ["y"]
# this will be executed in the context of the yes command (so self.caller will be available)
cmdyes.code = yescode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
cmdno = CmdMenuNode()
cmdno.key = "no"
cmdno.aliases = ["n"]
# this will be executed in the context of the no command
cmdno.code = nocode + "\nself.caller.cmdset.delete('menucmdset')\ndel self.caller.db._menu_data"
errorcmd = CmdMenuNode()
errorcmd.key = CMD_NOMATCH
errorcmd.code = "self.caller.msg('Please choose either Yes or No.')"
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)
# 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 = "[Y]/N"
else:
prompt = "Y/[N]"
prompt = "%s %s: " % (question, prompt)
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 not self.args or self.args != "yesno":
# 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,
code="self.caller.db.menutest='Testing!'")
node4 = MenuNode("node4", text="Attribute 'menutest' removed again.",
links=["node2"], linktexts=["Back to second node."], cols=2,
code="del self.caller.db.menutest")
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))")
menu = MenuTree(self.caller, nodes=(node0, node1, node2, node3, node4, node5))
menu.start()
else:
"Testing the yesno question"
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')",
default="N")

123
contrib/talking_npc.py Normal file
View file

@ -0,0 +1,123 @@
"""
Evennia Talkative NPC
Contribution - Griatch 2011
This is a simple NPC object capable of holding a
simple menu-driven conversation. Create it by
creating an object of typeclass contrib.talking_npc.TalkingNPC,
For example using @create:
@create John : contrib.talking_npc.TalkingNCP
Walk up to it and give the talk command
to strike up a conversation. If there are many
talkative npcs in the same room you will get to
choose which one's talk command to call (Evennia
handles this automatically).
Note that this is only a prototype class, showcasing
the uses of the menusystem module. It is NOT a full
mob implementation.
"""
from contrib import menusystem
from game.gamesrc.objects.baseobjects import Object
from game.gamesrc.commands.basecmdset import CmdSet
from game.gamesrc.commands.basecommand import MuxCommand
#
# The talk command
#
class CmdTalk(MuxCommand):
"""
talks to an npc
Usage:
talk
This command is only available if a talkative non-player-character (NPC)
is actually present. It will strike up a conversation with that NPC
and give you options on what to talk about.
"""
key = "talk"
locks = "cmd:all()"
help_category = "General"
def func(self):
"Implements the command."
# self.obj is the NPC this is defined on
obj = self.obj
self.caller.msg("(You walk up and talk to %s.)" % self.obj.key)
# conversation is a dictionary of keys, each pointing to a dictionary defining
# the keyword arguments to the MenuNode constructor.
conversation = obj.db.conversation
if not conversation:
self.caller.msg("%s says: 'Sorry, I don't have time to talk right now.'" % (self.obj.key))
return
# build all nodes by loading them from the conversation tree.
menu = menusystem.MenuTree(self.caller)
for key, kwargs in conversation.items():
menu.add(menusystem.MenuNode(key, **kwargs))
menu.start()
class TalkingCmdSet(CmdSet):
"Stores the talk command."
key = "talkingcmdset"
def at_cmdset_creation(self):
"populates the cmdset"
self.add(CmdTalk())
#
# Discussion tree. See contrib.menusystem.MenuNode for the keywords.
# (This could be in a separate module too)
#
CONV = {"START":{"text": "Hello there, how can I help you?",
"links":["info1", "info2"],
"linktexts":["Hey, do you know what this 'Evennia' thing is all about?",
"What's your name, little NPC?"],
"keywords":None,
"code":None},
"info1":{"text": "Oh, Evennia is where you are right now! Don't you feel the power?",
"links":["info3", "info2", "END"],
"linktexts":["Sure, *I* do, not sure how you do though. You are just an NPC.",
"Sure I do. What's yer name, NPC?",
"Ok, bye for now then."],
"keywords":None,
"code":None},
"info2":{"text":"My name is not really important ... I'm just an NPC after all.",
"links":["info3", "info1"],
"linktexts":["I didn't really want to know it anyhow.",
"Okay then, so what's this 'Evennia' thing about?"],
"keywords":None,
"code":None},
"info3":{"text":"Well ... I'm sort of busy so, have to go. NPC business. Important stuff. You wouldn't understand.",
"links":["END", "info2"],
"linktexts":["Oookay ... I won't keep you. Bye.",
"Wait, why don't you tell me your name first?"],
"keywords":None,
"code":None},
}
class TalkingNPC(Object):
"""
This implements a simple Object using the talk command and using the
conversation defined above. .
"""
def at_object_creation(self):
"This is called when object is first created."
# store the conversation.
self.db.conversation = CONV
self.db.desc = "This is a talkative NPC."
# assign the talk command to npc
self.cmdset.add_default(TalkingCmdSet, permanent=True)