Largely rewrote and refactored the help system.

The help entry database structure has changed! You have to resync or purge
your database or your will get problems!

New features:
* Help entry access now fully controlled by evennia permissions
* Categories for each help entry
* All entries are created dynamically, with a See also: footer calculated
  after the current state of the database.
* Indexes and topic list calculated on the fly (alphabetically/after category)
* Added auto-help help entries for all default commands.
* Only shows commands _actually implemented_ - MUX help db moved into 'MUX' category
  which is not shown by default.
* More powerful auto-help markup - supports categories and permissions (and inheritance).
* Global on/off switch for auto-help, when entering production
* Auto_help_override switch for selectively activating auto-help when developing
  new commands (like the old system).
* Refactored State help system; no more risk of overwriting global help entries.
* State help now defers to main help db when no match found; makes system more transparent.
* State help entries also support categories/permissions (state categories are not
  used much though).

Other updates:
* Added more commands to the batch processor
* Many bug-fixes.

/Griatch
This commit is contained in:
Griatch 2009-10-14 18:15:15 +00:00
parent 46e2cd3ecb
commit 8074617285
27 changed files with 1995 additions and 1072 deletions

View file

@ -1,32 +1,43 @@
"""
The state system allows the player to enter states/modes 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, much more powerful than using 'rooms')
- inline text editors (entering text into a buffer)
- npc conversation (select replies)
- only allowing certain commands in special situations (e.g. special attack commands
when in 'combat' mode)
- adding special commands to the normal set (e.g. a 'howl' command when in 'werewolf' state)
- deactivating certain commands (e.g. removing the 'say' command in said 'werewolf' state ...)
State table
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
The state system allows the player to enter states/modes 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, much
more powerful than using 'rooms') - inline text editors (entering text
into a buffer) - npc conversation (select replies) - only allowing
certain commands in special situations (e.g. special attack commands
when in 'combat' mode) - adding special commands to the normal set
(e.g. a 'howl' command when in 'werewolf' state) - deactivating
certain commands (e.g. removing the 'say' command in said 'werewolf'
state ...)
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. The main difference is that new states must first be created
using the GLOBAL_STATE_TABLE.add_state() command. See examples in game/gamesrc.
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. The main difference is that new states must first be
created using the GLOBAL_STATE_TABLE.add_state() command. See examples
in game/gamesrc.
"""
from django.conf import settings
from cmdtable import CommandTable, GLOBAL_CMD_TABLE
from logger import log_errmsg
import src.helpsys.management.commands.edit_helpfiles as edit_help
from src import defines_global
from src.helpsys import helpsystem
from src.helpsys.models import HelpEntry
class StateTable(object):
"""
This implements a variant of the command table handling states.
"""
state_table = None
def __init__(self):
@ -35,9 +46,9 @@ class StateTable(object):
self.state_flags = {}
def add_state(self, state_name,
global_cmds=None, global_filter=[],
global_cmds=None, global_filter=None,
allow_exits=False, allow_obj_cmds=False,
exit_command=False):
help_command=True, exit_command=False):
"""
Creates a new game state. Each state has its own unique set of commands and
can change how the game works.
@ -76,51 +87,58 @@ class StateTable(object):
special conditions or clean-up operations before allowing a player
to exit (e.g. combat states and text editors), in which case this
feature should be turned off and handled by custom exit commands.
help_command
"""
state_name = state_name.strip()
#create state
self.state_table[state_name] = CommandTable()
if global_cmds != None:
if global_cmds == 'all':
f = lambda c: True
elif global_cmds == 'include':
f = lambda c: c in global_filter
if not 'help' in global_filter:
global_filter.append('help')
elif global_cmds == 'exclude':
f = lambda c: c not in global_filter
else:
log_errmsg("ERROR: in statetable, state %s: Unknown global_cmds flag '%s'." %
(state_name, global_cmds))
return
for cmd in filter(f,GLOBAL_CMD_TABLE.ctable.keys()):
self.state_table[state_name].ctable[cmd] = \
GLOBAL_CMD_TABLE.get_command_tuple(cmd)
if exit_command:
#if we import global commands, we use the normal help index; thus add
#help for @exit to the global index.
self.state_table[state_name].add_command("@exit",
cmd_state_exit,
auto_help=True)
else:
#when no global cmds are imported, we create a small custom
#state-based help index instead
self.help_index.add_state(state_name)
self.add_command(state_name,'help',cmd_state_help)
if exit_command:
#add the @exit command
self.state_table[state_name].add_command("@exit",
cmd_state_exit)
self.help_index.add_state_help(state_name, "@exit",
cmd_state_exit.__doc__)
#store special state flags
self.state_flags[state_name] = {}
self.state_flags[state_name]['globals'] = global_cmds
self.state_flags[state_name]['exits'] = allow_exits
self.state_flags[state_name]['obj_cmds'] = allow_obj_cmds
if global_cmds == 'all':
# we include all global commands
func = lambda c: True
elif global_cmds == 'include':
# selective global inclusion
func = lambda c: c in global_filter
if not 'help' in global_filter:
global_filter.append('help')
elif global_cmds == 'exclude':
# selective global exclusion
func = lambda c: c not in global_filter
else:
# no global commands
func = lambda c: False
# add copies of the global command defs to the state's command table.
for cmd in filter(func, GLOBAL_CMD_TABLE.ctable.keys()):
self.state_table[state_name].ctable[cmd] = \
GLOBAL_CMD_TABLE.get_command_tuple(cmd)
# create a stand-alone state-based help index
self.help_index.add_state(state_name)
# if the auto-help command is not wanted, just make a custom command
# overwriting this default 'help' command. Keeps 'info' as a way to have
# both a custom help command and state auto-help; replace this too
# to completely hide auto-help functionality in the state.
self.add_command(state_name, 'help', cmd_state_help)
self.add_command(state_name, 'info', cmd_state_help)
if exit_command:
#add the @exit command
self.state_table[state_name].add_command("@exit",
cmd_state_exit)
self.help_index.add_state_help(state_name,
"@exit",
"General",
cmd_state_exit.__doc__)
def del_state(self, state_name):
"""
Permanently deletes a state from the state table. Make sure no users are in
@ -147,12 +165,12 @@ class StateTable(object):
return
try:
del self.state_table[state_name].ctable[command_string]
err = self.help_index.del_state_help(state_name,command_string)
self.help_index.del_state_help(state_name, command_string)
except KeyError:
pass
def add_command(self, state_name, command_string, function, priv_tuple=None,
extra_vals=None, auto_help=False, staff_help=False):
extra_vals=None, help_category="", priv_help_tuple=None):
"""
Transparently add commands to a specific state.
This command is similar to the normal
@ -163,12 +181,12 @@ class StateTable(object):
function: (reference) command function object
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.
Auto-help functionality: (only used if settings.HELP_AUTO_ENABLED=True)
help_category (str): An overall help category where auto-help will place
the help entry. If not given, 'General' is assumed.
priv_help_tuple (tuple) String tuple of permissions required to view this
help entry. If nothing is given, priv_tuple is used.
"""
if state_name not in self.state_table.keys():
@ -177,21 +195,30 @@ class StateTable(object):
return
state_name = state_name.strip()
#handle auto-help for the state commands
if auto_help:
if self.help_index.has_state(state_name):
#add the help text to state help index only, don't add
#it to the global help index
self.help_index.add_state_help(state_name,command_string,
function.__doc__,
staff_help=staff_help)
auto_help = False
#finally add the new command to the state's command table
# handle the creation of an auto-help entry in the
# stand-alone help index
topicstr = command_string
if not help_category:
help_category = state_name
help_category = help_category.capitalize()
self.help_index.add_state_help(state_name,
topicstr,
help_category,
function.__doc__,
priv_help_tuple)
# finally add the new command to the state's command table
self.state_table[state_name].add_command(command_string,
function, priv_tuple,
extra_vals,auto_help=auto_help,
staff_help=staff_help)
function,
priv_tuple,
extra_vals,
help_category,
priv_help_tuple,
auto_help_override=False)
def get_cmd_table(self, state_name):
"""
@ -202,150 +229,157 @@ class StateTable(object):
else:
return None
def get_state_flags(self, state_name):
def get_exec_rights(self, state_name):
"""
Return the state flags for a particular state.
Used by the cmdhandler. Accesses the relevant state flags
concerned with execution access for a particular state.
"""
if self.state_flags.has_key(state_name):
return self.state_flags[state_name]['exits'],\
self.state_flags[state_name]['obj_cmds']
return self.state_flags[state_name]['exits'], \
self.state_flags[state_name]['obj_cmds']
else:
return False, False
return False, False, False
class StateHelpIndex(object):
"""
Handles the dynamic state help system.
Handles the dynamic state help system. This is
a non-db based help system intended for the special
commands associated with a state.
The system gives preference to help matches within
the state, but defers to the normal, global help
system when it fails to find a help entry match.
"""
help_index = None
def __init__(self):
self.help_index = {}
self.identifier = '<<TOPIC:'
def add_state(self,state_name):
def add_state(self, state_name):
"Create a new state"
self.help_index[state_name] = {}
def has_state(self,state_name):
def has_state(self, state_name):
"Checks if we have this state"
return self.help_index.has_key(state_name)
def add_state_help(self, state,command,text,staff_help=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):
text = text.rstrip()
if self.identifier in text:
topic_dict, staff_dict = edit_help.handle_help_markup(command, text, staff_help,
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_help, entry)
else:
self.help_index[state][command] = (staff_help, text)
def del_state_help(self, state, topic):
"""Manually delete a help topic from state help system. Note that this is
only going to last until the next @reload unless you also turn off auto_help
for the relevant topic."""
if self.help_index.has_key(state) and self.help_index[state].has_key(topic):
del self.help_index[state][topic]
return True
else:
return False
def add_state_help(self, state, topicstr, help_category,
help_text, priv_help_tuple=()):
"""
Store help for a command under a certain state.
Supports [[Title, category, (priv_tuple)]] markup
"""
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):
if not self.help_index.has_key(state):
return
# produce nicely formatted help entries, taking markup
# into account.
topics = helpsystem.edithelp.format_help_entry(topicstr,
help_category,
help_text,
priv_help_tuple)
# store in state's dict-based database
for topic in topics:
self.help_index[state][topic[0]] = topic
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):
# this is a state help entry.
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
# check permissions
if help_tuple and help_tuple[2]:
if help_tuple[3] and not caller.has_perm_list(help_tuple[3]):
return None
return help_tuple[2]
else:
return None
def get_state_index(self,caller, state):
def get_state_index(self, 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
tuples = self.help_index[state].items()
items = [tup[0] for tup in tuples]
table = helpsystem.viewhelp.make_table(items, 6)
return table
#default commands available for all special states
# default commands available for all special states. These
# are added to states during the state initialization if
# the proper flags are set.
def cmd_state_exit(command):
"""@exit (when in a state)
"""
@exit - exit from a state
This command only works when inside certain special game 'states' (like a menu or
editor or similar situations).
Usage:
@exit
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."""
This command only works when inside certain special game 'states'
(like a menu or editor or similar situations).
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.
"""
source_object = command.source_object
source_object.clear_state()
source_object.emit_to("... Exited.")
source_object.execute_cmd('look')
## 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.
## Note that at this time we are not displaying categories in the state help system;
## (although they are stored). Instead the state itself is treated as a
## category in itself.
def cmd_state_help(command):
"""
help <topic> (while in a special state)
help - view help database
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.
Usage:
help <topic>
Shows the available help on <topic>. Use without a topic
to get the index.
"""
source_object = command.source_object
state = source_object.get_state()
args = command.command_argument
switches = command.command_switches
help_index = GLOBAL_STATE_TABLE.help_index
state = source_object.get_state()
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\r" % state
for i in index:
s += " %s\n\r" % i
s = s[:-2]
source_object.emit_to(s)
# first show the normal help index
source_object.execute_cmd("help", ignore_state=True)
text = help_index.get_state_index(state)
if text:
# Try to list the state-specific help entries after the main list
string = "\n%s%s%s\n\r\n\r%s" % ("---", " Help topics for %s: " % \
state.capitalize(), "-"*(30-len(state)), text)
source_object.emit_to(string)
return
# try to first find a matching state help topic, then defer to global help
topicstr = args.strip()
helptext = help_index.get_state_help(source_object, state, topicstr)
if helptext:
source_object.emit_to("\n%s" % helptext)
else:
args = args.strip()
source_object.execute_cmd("help %s" % topicstr, ignore_state=True)
if 'del' in switches:
if not source_object.is_staff():
source_object.emit_to("Only staff can delete help topics.")
return
if help_index.del_state_help(state,args):
source_object.emit_to("Topic %s deleted." % args)
else:
source_object.emit_to("Topic %s not found." % args)
return
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)
#import this into modules
#import this instance into your modules
GLOBAL_STATE_TABLE = StateTable()