Created a state system. See

http://groups.google.com/group/evennia/browse_thread/thread/66a7ff6cce5303b7
for more detailed description.

Created a new folder gamesrc/commands/examples and moved all examples in there.
/Griatch
This commit is contained in:
Griatch 2009-05-01 15:34:43 +00:00
parent cae925c29b
commit 0efe2c3095
9 changed files with 415 additions and 30 deletions

View file

@ -4,16 +4,16 @@ in action.
You'll need to make sure that this or any new modules you create are added to
game/settings.py under CUSTOM_COMMAND_MODULES or CUSTOM_UNLOGGED_COMMAND_MODULES,
which are tuples of module import path strings.
See src/config_defaults.py for more details.
which are tuples of module import path strings. See src/config_defaults.py for more details.
E.g. to add this example command for testing, your entry in game/settings.py would
look like this:
CUSTOM_COMMAND_MODULES = ('game.gamesrc.commands.example',)
CUSTOM_COMMAND_MODULES = ('game.gamesrc.commands.examples.example',)
(note the extra comma at the end to make this into a Python tuple. It's only
needed if you have only one entry.)
needed if you have only one entry.) You need to restart the Evennia server before new
files are recognized.
"""
@ -70,8 +70,6 @@ def cmd_example(command):
# automatically be created for us.
GLOBAL_CMD_TABLE.add_command("example", cmd_example, auto_help=True),
#another simple example
def cmd_emote_smile(command):
@ -92,3 +90,7 @@ def cmd_emote_smile(command):
#add to global command table (no auto_help activated)
GLOBAL_CMD_TABLE.add_command('smile', cmd_emote_smile)

View file

@ -0,0 +1,112 @@
"""
Example of using the state system. The Event system allows a player object to be
'trapped' in a special environment where different commands are available than normal.
This is very useful in order to implement anything from menus to npc-conversational
choices and inline text-editors.
This example uses the State system to create a simple menu.
To test out this example, add this module to the CUSTOM_COMMAND_MODULES tuple in
your game/settings.py as 'game.gamesrc.commands.examples.state_example' (see ./example.py
for another example). You need to restart the Evennia server before new files are
recognized.
Next enter the mud and give the command
> entermenu
Note that the help entries added to the state system with the auto_help flag are NOT
part of the normal help database, they are stored with the state and only accessible
from inside it (unless you also set the 'global_help' flag in the add_command(), in
which case it is also added to the global help system). If you want to describe the
state itself in more detail you should add that to the main help index manually.
"""
#This is the normal command table, accessible by default
from src.cmdtable import GLOBAL_CMD_TABLE
#The statetable contains sets of cmdtables that is made available
#only when we are in a particular state, overriding the GLOBAL_CMD_TABLE
from src.statetable import GLOBAL_STATE_TABLE
#
# Implementing a simple 'menu' state
#
#the name of our state, to make sure it's the same everywhere
STATENAME = 'menu'
#
# 'entry' command
#
def cmd_entermenu(command):
"""
This is the 'entry' command that takes the player from the normal
gameplay mode into the 'menu' state. In order to do this, it
must be added to the GLOBAL_CMD_TABLE like any command.
"""
#get the player object calling the command
source_object = command.source_object
#this is important: we use the set_state() command
#to shift the player into a state named 'menu'.
source_object.set_state(STATENAME)
#display the menu.
print_menu(source_object)
#
# Commands only available while in the 'menu' state. Note that
# these have auto_help, so the __doc__ strings of the functions
# can be read as help entries when in the menu.
#
def menu_cmd_option1(command):
"""option1
This selects the first option.
"""
source_object = command.source_object
print_menu(source_object, 1)
def menu_cmd_option2(command):
"""option2
This selects the second option. Duh.
<<TOPIC:About>>
This is an extra topic to test the auto_help functionality.
"""
source_object = command.source_object
print_menu(source_object, 2)
def menu_cmd_clear(command):
"""clear
Clears the options.
"""
source_object = command.source_object
print_menu(source_object)
#
# helper function
#
def print_menu(source_obj,choice=None):
"""
Utility function to print the menu. More interesting things
would happen here in a real menu.
"""
if choice==1:
ch = "> option1\n option2"
elif choice==2:
ch = " option1\n> option2"
else:
ch = " option1\n option2"
s ="Menu---------\n%s\n help - get help" % ch
source_obj.emit_to(s)
#Add the 'entry' command to the normal command table
GLOBAL_CMD_TABLE.add_command("entermenu", cmd_entermenu)
#Add the menu commands to the state table by tying them to the 'menu' state. It is
#important that the name of the state matches what we set the player-object to in
#the 'entry' command. Since auto_help is on, we will have help entries for all commands
#while in the menu.
GLOBAL_STATE_TABLE.add_command(STATENAME, "option1", menu_cmd_option1,auto_help=True)
GLOBAL_STATE_TABLE.add_command(STATENAME, "option2", menu_cmd_option2,auto_help=True)
GLOBAL_STATE_TABLE.add_command(STATENAME, "clear", menu_cmd_clear,auto_help=True)

View file

@ -8,6 +8,7 @@ from traceback import format_exc
from django.contrib.contenttypes.models import ContentType
import defines_global
import cmdtable
import statetable
import logger
import comsys
import alias_mgr
@ -318,19 +319,40 @@ def handle(command):
# Not logged in, look through the unlogged-in command table.
command_table_lookup(command, cmdtable.GLOBAL_UNCON_CMD_TABLE,
eval_perms=False)
else:
# Match against the 'idle' command.
match_idle(command)
# See if this is an aliased command.
match_alias(command)
# Check if the user is using a channel command.
match_channel(command)
# See if the user is trying to traverse an exit.
match_exits(command)
neighbor_match_found = match_neighbor_ctables(command)
if not neighbor_match_found:
# Retrieve the appropriate (if any) command function.
command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE)
else:
state_name = command.source_object.get_state()
state_cmd_table = statetable.GLOBAL_STATE_TABLE.get_cmd_table(state_name)
if state_name and state_cmd_table:
# we are in a special state.
# check idle command.
match_idle(command)
# check for channel commands
prev_command = command.command_string
match_channel(command)
if prev_command != command.command_string:
# a channel command is handled normally also in the state
command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE)
else:
command_table_lookup(command, state_cmd_table)
else:
#normal operation
# Match against the 'idle' command.
match_idle(command)
# See if this is an aliased command.
match_alias(command)
# Check if the user is using a channel command.
match_channel(command)
# See if the user is trying to traverse an exit.
match_exits(command)
neighbor_match_found = match_neighbor_ctables(command)
if not neighbor_match_found:
# Retrieve the appropriate (if any) command function.
command_table_lookup(command, cmdtable.GLOBAL_CMD_TABLE)
"""
By this point, we assume that the user has entered a command and not

View file

@ -1,6 +1,6 @@
"""
Command Table Module
---------------------
Each command entry consists of a key and a tuple containing a reference to the
command's function, and a tuple of the permissions to match against. The user
only need have one of the permissions in the permissions tuple to gain
@ -19,7 +19,7 @@ from src.helpsys.management.commands.edit_helpfiles import add_help
class CommandTable(object):
"""
Stores command tables and performs lookups.
Stores commands and performs lookups.
"""
ctable = None
@ -28,7 +28,7 @@ class CommandTable(object):
self.ctable = {}
def add_command(self, command_string, function, priv_tuple=None,
extra_vals=None, auto_help=False, staff_only=False):
extra_vals=None, auto_help=False, staff_help=False, state=None):
"""
Adds a command to the command table.
@ -41,7 +41,7 @@ class CommandTable(object):
auto_help (bool): If true, automatically creates/replaces a help topic with the
same name as the command_string, using the functions's __doc__ property
for help text.
staff_help (bool): Only relevant if help_auto is activated; It True, makes the
staff_help (bool): Only relevant if auto_help is activated; If True, makes the
help topic (and all eventual subtopics) only visible to staff.
Note: the auto_help system also supports limited markup. If you divide your __doc__
@ -57,7 +57,7 @@ class CommandTable(object):
#add automatic help text from the command's doc string
topicstr = command_string
entrytext = function.__doc__
add_help(topicstr, entrytext, staff_only=staff_only,
add_help(topicstr, entrytext, staff_only=staff_help,
force_create=True, auto_help=True)
def get_command_tuple(self, func_name):
@ -67,6 +67,7 @@ class CommandTable(object):
"""
return self.ctable.get(func_name, False)
"""
Command tables
"""

View file

@ -867,7 +867,7 @@ def cmd_recover(command):
GLOBAL_CMD_TABLE.add_command("@recover", cmd_recover,
priv_tuple=("genperms.builder"),auto_help=True,staff_only=True)
priv_tuple=("genperms.builder"),auto_help=True,staff_help=True)
def cmd_destroy(command):
"""
@ -943,4 +943,4 @@ def cmd_destroy(command):
source_object.emit_to("You schedule %s for destruction." % target_obj.get_name())
GLOBAL_CMD_TABLE.add_command("@destroy", cmd_destroy,
priv_tuple=("genperms.builder"),auto_help=True,staff_only=True)
priv_tuple=("genperms.builder"),auto_help=True,staff_help=True)

View file

@ -67,7 +67,7 @@ def _create_help(topicstr, entrytext, staff_only=False, force_create=False,
new_entry.save()
return [new_entry]
def _handle_help_markup(topicstr, entrytext, staff_only, identifier="<<TOPIC:"):
def handle_help_markup(topicstr, entrytext, staff_only, identifier="<<TOPIC:"):
"""
Handle help markup in order to split help into subsections.
Handles markup of the form <<TOPIC:STAFF:TopicTitle>> and
@ -103,7 +103,7 @@ def _handle_help_markup(topicstr, entrytext, staff_only, identifier="<<TOPIC:"):
staff_dict[topic] = staff_only
return topic_dict, staff_dict
def _format_footer(top, text, topic_dict, staff_dict):
def format_footer(top, text, topic_dict, staff_dict):
"""
Formats the subtopic with a 'Related Topics:' footer. If mixed
staff-only flags are set, those help entries without the staff-only flag
@ -147,13 +147,13 @@ def add_help(topicstr, entrytext, staff_only=False, force_create=False,
identifier = '<<TOPIC:'
if identifier in entrytext:
#There is markup in the entry, so we should split the doc into separate subtopics
topic_dict, staff_dict = _handle_help_markup(topicstr, entrytext,
topic_dict, staff_dict = handle_help_markup(topicstr, entrytext,
staff_only, identifier)
topics = []
for topic, text in topic_dict.items():
#format with nice footer
entry = _format_footer(topic, text, topic_dict, staff_dict)
entry = format_footer(topic, text, topic_dict, staff_dict)
if entry:
#create the subtopic

View file

@ -117,6 +117,10 @@ class Object(models.Model):
objects = ObjectManager()
#state system can set a particular command table to be used.
state = None
def __cmp__(self, other):
"""
Used to figure out if one object is the same as another.
@ -981,5 +985,21 @@ class Object(models.Model):
otype = int(self.type)
return defines_global.OBJECT_TYPES[otype][1][0]
def get_state(self):
return self.state
def set_state(self, cmd_table=None):
"""
Set the state by defining which cmd_table is currently used.
"""
if self.is_player():
self.state = cmd_table
def clear_state(self):
self.state = None
# Deferred imports are poopy. This will require some thought to fix.
from src import cmdhandler

228
src/statetable.py Normal file
View file

@ -0,0 +1,228 @@
"""
The state system allows the player to enter a state where only a special set of commands
are available. This can be used for all sorts of useful things:
- in-game menus (picking an option from a list)
- inline text editors (entering text into a buffer)
- npc conversation (select replies)
- commands only available while in 'combat mode' etc
This allows for more power than using rooms to build 'menus'.
Basically the GLOBAL_STATE_TABLE contains a dict with command tables keyed after the
name of the state. To use, a function must set the 'state' variable on a player object
using the obj.set_state() function. The GLOBAL_STATE_TABLE will then be searched by the
command handler and if the state is defined its command table is used instead
of the normal global command table.
The state system is pluggable, in the same way that commands are added to the global command
table, commands are added to the GLOBAL_STATE_TABLE using add_command supplying in
addition the name of the state.
"""
from cmdtable import CommandTable
from logger import log_errmsg
import src.helpsys.management.commands.edit_helpfiles as edit_help
class StateTable(object):
state_table = None
def __init__(self):
self.state_table = {}
self.help_index = StateHelpIndex()
def add_command(self, state_name, command_string, function, priv_tuple=None,
extra_vals=None, auto_help=False, staff_help=False, help_global=False,
exit_command=True):
"""
Access function; transparently add commands to a specific command table to
reprsent a particular state. This command is similar to the normal
command_table.add_command() function. See example in gamesrc/commands/examples.
command_string: (str) command name to run, like look, @list etc
function: (reference) command function object
state_name: (str) identifier of the state we tie this to.
priv_tuple: (tuple) String tuple of permissions required for command.
extra_vals: (dict) Dictionary to add to the Command object.
auto_help: (bool) Activate the auto_help system. By default this stores the
help inside the statetable only (not in the main help database), and so
the help entries are only available when the player is actually inside
the state. Note that the auto_help system of state-commands do not
support <<TOPIC:mytitle>> markup.
staff_help: (bool) Help entry is only available for staff players.
help_global: (bool) Also auto_add the help entry to the main help database. Be
careful with overwriting same-named help topics if you define special
versions of commands inside your state.
exit_command: (bool) Sets if the default @exit command is added to the state. Only
one command needs to include this statement in order to add @exit. This is
usually a good idea to make sure the player is not stuck, but you might want
to turn this off if you know what you're doing and want to avoid players
'escaping' the state (like in a combat state or similar), or when
you need to do some special final cleanup or save operations before
exiting (like in a text editor).
"""
if not state_name:
log_errmsg("Command %s was not given a state. Not added." % command_string)
return
state_name = state_name.strip()
if not self.state_table.has_key(state_name):
#create the state
self.state_table[state_name] = CommandTable()
#always add a help index even though it might not be used.
self.help_index.add_state(state_name)
if exit_command:
#add the @exit command
self.state_table[state_name].add_command("@exit",
cmd_state_exit,
auto_help=True)
if auto_help:
#add help for @exit command
self.help_index.add_state_help(state_name, "@exit",
cmd_state_exit.__doc__)
#handle auto-help for the state
if auto_help:
#make sure the state's help command is in place
self.state_table[state_name].add_command('help',cmd_state_help)
#add the help text
helptext = function.__doc__
self.help_index.add_state_help(state_name,command_string,
helptext,staff_only=staff_help)
if not help_global:
#if we don't want global help access, we need to
#turn off auto_help before adding the command.
auto_help = False
#finally add the new command to the state
self.state_table[state_name].add_command(command_string,
function, priv_tuple,
extra_vals,auto_help,
staff_help)
def get_cmd_table(self, state_name):
"""
Return the command table for a particular state.
"""
if self.state_table.has_key(state_name):
return self.state_table[state_name]
else:
return None
class StateHelpIndex(object):
"""
Handles the dynamic state help system.
"""
help_index = None
def __init__(self):
self.help_index = {}
self.identifier = '<<TOPIC:'
def add_state(self,state_name):
"Create a new state"
self.help_index[state_name] = {}
def add_state_help(self, state,command,text,staff_only=False):
"""Store help for a command under a certain state.
Supports <<TOPIC:MyTopic>> and <<TOPIC:STAFF:MyTopic>> markup."""
if self.help_index.has_key(state):
if self.identifier in text:
topic_dict, staff_dict = edit_help.handle_help_markup(command, text, staff_only,
identifier=self.identifier)
for topic, text in topic_dict.items():
entry = edit_help.format_footer(topic,text,topic_dict,staff_dict)
if entry:
self.help_index[state][topic] = (staff_only, entry)
else:
self.help_index[state][command] = (staff_only, text)
def get_state_help(self,caller, state, command):
"get help for a particular command within a state"
if self.help_index.has_key(state) and self.help_index[state].has_key(command):
help_tuple = self.help_index[state][command]
if caller.is_staff() or not help_tuple[0]:
return help_tuple[1]
return None
else:
return None
def get_state_index(self,caller, state):
"list all help topics for a state"
if self.help_index.has_key(state):
if caller.is_staff():
index = self.help_index[state].keys()
else:
index = []
for key, tup in self.help_index[state].items():
if not tup[0]:
index.append(key)
return sorted(index)
else:
return None
#default commands available for all special states
def cmd_state_exit(command):
"""@exit (when in a state)
This command only works when inside a special game 'state' (like a menu or
editor or similar place where you have fewer commands available than normal).
It aborts what you were doing and force-exits back to the normal mode of
gameplay. Some states might deactivate the @exit command for various reasons.
In those cases, read the help when in the state to learn more."""
source_object = command.source_object
source_object.clear_state()
source_object.emit_to("... Exited.")
source_object.execute_cmd('look')
def cmd_state_help(command):
"""
help <topic> (while in a state)
In-state help system. This is NOT tied to the normal help
system and is not stored in the database. It is intended as a quick
reference for users when in the state; if you want a detailed description
of the state itself, you should probably add it to the main help system
so the user can read it at any time.
If you don't want to use the auto-system, turn off auto_help
for all commands in the state. You could then for example make a custom help command
that displays just a short help summary page instead.
"""
source_object = command.source_object
state = source_object.get_state()
args = command.command_argument
help_index = GLOBAL_STATE_TABLE.help_index
if not args:
index = help_index.get_state_index(source_object, state)
if not index:
source_object.emit_to("There is no help available here.")
return
s = "Help topics for %s:\n" % state
for i in index:
s += " %s\n" % i
s = s[:-1]
source_object.emit_to(s)
return
else:
args = args.strip()
help = help_index.get_state_help(source_object, state, args)
if help:
source_object.emit_to("%s" % help)
else:
source_object.emit_to("No help available on %s." % args)
GLOBAL_STATE_TABLE = StateTable()