Added a command batch processor to Evennia. The @batchprocess is a super-user only command that reads normal Evennia-commands

from a special-format batchfile. It is intended for large-scale offline world creation (especially things like room descriptions),
where a real text editor is often easier to use than online alternatives. The @batchprocess also has an /interactive mode which allows
stepping through the batch script, allowing to only execute selected entries; e.g. for editing/updating/debugging etc.  There is
an example batchfile in the gamesrc/commands/examples directory.
/Griatch
This commit is contained in:
Griatch 2009-09-04 08:01:43 +00:00
parent 41365074fd
commit eebfa0d387
7 changed files with 468 additions and 1 deletions

View file

@ -0,0 +1,57 @@
#
# This is an example batch build file for Evennia.
#
# It allows batch processing of normal Evennia commands.
# Test it by loading it with the @batchprocess command
# (superuser only):
#
# @batchprocess[/interactive] </full/path/to/this/file>
#
# A # as the first symbol on a line begins a comment and
# marks the end of a previous command definition (important!).
#
# All supplied commands are given as normal, on their own line
# and accepts arguments in any format up until the first next
# comment line begins. Extra whitespace is removed; an empty
# line in a command definition translates into a newline.
#
# This creates a red button
@create button
# This comment ends input for @create
# Next command:
@set button=desc:
This is a large red button. Now and then
it flashes in an evil, yet strangely tantalizing way.
A big sign sits next to it. It says:
-----------
Press me!
-----------
... It really begs to be pressed, doesn't it? You
know you want to!
# This ends the @set command. Note that line breaks and extra spaces
# in the argument are not considered. A completely empty line
# translates to a \n newline in the command; two empty lines will thus
# create a new paragraph. (note that few commands support it though, you
# mainly want to use it for descriptions)
# Now let's place the button where it belongs (let's say limbo #2 is
# the evil lair in our example)
@teleport #2
#... and drop it (remember, this comment ends input to @teleport, so don't
#forget it!) The very last command in the file needs not be ended with #.
drop button

0
src/ansi.py Executable file → Normal file
View file

View file

@ -0,0 +1,406 @@
"""
Batch processor
The batch processor accepts 'batchcommand files' e.g 'batch.ev', containing a
sequence of valid evennia commands in a simple format. The engine
runs each command in sequence, as if they had been run at the terminal prompt.
This way entire game worlds can be created and planned offline; it is
especially useful in order to create long room descriptions where a
real offline text editor is often much better than any online text editor
or prompt.
Example of batch.ev file:
----------------------------
# batch file
# all lines starting with # are comments; they also indicate
# that a command definition is over.
@create box
# this comment ends the @create command.
@set box=desc: A large box.
Inside are some scattered piles of clothing.
It seems the bottom of the box is a bit loose.
# Again, this comment indicates the @set command is over. Note how
# the description could be freely added. Excess whitespace on a line
# is ignored. An empty line in the command definition is parsed as a \n
# (so two empty lines becomes a new paragraph).
@teleport #221
# (Assuming #221 is a warehouse or something.)
# (remember, this comment ends the @teleport command! Don'f forget it)
@drop box
# Done, the box is in the warehouse! (this last comment is not necessary to
# close the @drop command since it's the end of the file)
-------------------------
An example batch file is found in game/gamesrc/commands/examples.
"""
import os
import re
from src import logger
from src import defines_global
from src.cmdtable import GLOBAL_CMD_TABLE
from src.statetable import GLOBAL_STATE_TABLE
#global defines for storage
STATENAME="_interactive_batch_processor"
CMDSTACKS={} # user:cmdstack pairs (for interactive)
STACKPTRS={} # user:stackpointer pairs (for interactive)
FILENAMES={} # user:filename pairs (for interactive/reload)
cwhite = r"%cn%ch%cw"
cred = r"%cn%ch%cw"
cgreen = r"%cn%ci%cg"
cyellow = r"%cn%ch%cy"
cnorm = r"%cn"
def read_batchbuild_file(filename):
"""
This reads the contents of batchfile.
"""
filename = os.path.abspath(filename)
try:
f = open(filename)
except IOError:
logger.log_errmsg("file %s not found." % filename)
return None
lines = f.readlines()
f.close()
return lines
def parse_batchbuild_file(filename):
"""
This parses the lines of a batchfile according to the following
rules:
1) # at the beginning of a line marks the end of the command before it.
It is also a comment and any number of # can exist on subsequent
lines (but not inside comments).
2) Commands are placed alone at the beginning of a line and their
arguments are considered to be everything following (on any
number of lines) until the next comment line beginning with #.
3) Newlines are ignored in command definitions
4) A completely empty line in a command line definition is condered
a newline (so two empty lines is a paragraph).
5) Excess spaces and indents inside arguments are stripped.
"""
#read the indata, if possible.
lines = read_batchbuild_file(filename)
if not lines:
logger.log_errmsg("File %s not found." % filename)
return
#helper function
def identify_line(line):
"""
Identifies the line type (comment, commanddef or empty)
"""
try:
if line.strip()[0] == '#':
return "comment"
else:
return "commanddef"
except IndexError:
return "empty"
commands = []
curr_cmd = ""
#purge all superfluous whitespace and newlines from lines
reg1 = re.compile(r"\s+")
lines = [reg1.sub(" ",l) for l in lines]
#parse all command definitions into a list.
for line in lines:
typ = identify_line(line)
if typ == "commanddef":
curr_cmd += line
elif typ == "empty" and curr_cmd:
curr_cmd += "\r\n"
else: #comment
if curr_cmd:
commands.append(curr_cmd.strip())
curr_cmd = ""
if curr_cmd: commands.append(curr_cmd.strip())
#second round to clean up now merged line edges etc.
reg2 = re.compile(r"[ \t\f\v]+")
commands = [reg2.sub(" ",c) for c in commands]
#remove eventual newline at the end of commands
commands = [c.strip('\r\n') for c in commands]
return commands
def batch_process(source_object, commands):
"""
Process a file straight off.
"""
for i, command in enumerate(commands):
cmdname = command[:command.find(" ")]
source_object.emit_to("%s== %s%02i/%02i: %s %s%s" % (cgreen,cwhite,i+1,
len(commands),
cmdname,
cgreen,"="*(50-len(cmdname))))
source_object.execute_cmd(command)
#main access function @batchprocess
def cmd_batchprocess(command):
"""
Usage:
@batchprocess[/interactive] <filename with full path>
Runs batches of commands from a batchfile. This is a
superuser command, intended for large-scale offline world
development.
Interactive mode allows the user more control over the
processing of the file.
"""
global CMDSTACKS,STACKPTRS,FILENAMES
source_object = command.source_object
#check permissions
if not source_object.is_superuser():
source_object.emit_to(defines_global.NOPERMS_MSG)
return
args = command.command_argument
if not args:
source_object.emit_to("Usage: @batchprocess[/interactive] <filename with full path>")
return
filename = args.strip()
#parse indata file
commands = parse_batchbuild_file(filename)
if not commands:
return
switches = command.command_switches
if switches and switches[0] in ['inter','interactive']:
#allow more control over how batch file is executed
source_object.set_state(STATENAME)
CMDSTACKS[source_object] = commands
STACKPTRS[source_object] = 0
FILENAMES[source_object] = filename
source_object.emit_to("Interactive mode (h for help).")
show_curr(source_object)
else:
source_object.clear_state()
batch_process(source_object, commands)
source_object.emit_to("%s== Batchfile '%s' applied." % (cgreen,filename))
GLOBAL_CMD_TABLE.add_command("@batchprocess", cmd_batchprocess,
auto_help=True, staff_help=True,
priv_tuple=("genperms.process_control"))
#interactive state commands
def printfooter():
"prints a nice footer"
#s = "%s\n== nn/bb/jj == pp/ss == ll == rr/rrr == cc/qq == (hh for help) ==" % cgreen
s = ""
return s
def show_curr(source_object,showall=False):
"Show the current command."
global CMDSTACKS,STACKPTRS
ptr = STACKPTRS[source_object]
commands = CMDSTACKS[source_object]
if ptr >= len(commands):
s = "\n You have reached the end of the batch file."
s += "\n Use qq to exit or bb to go back."
source_object.emit_to(s)
STACKPTRS[source_object] = len(commands)-1
show_curr(source_object)
return
command = commands[ptr]
cmdname = command[:command.find(" ")]
s = "%s== %s%02i/%02i: %s %s===== %s %s%s" % (cgreen,cwhite,
ptr+1,len(commands),
cmdname,cgreen,
"(hh for help)",
"="*(35-len(cmdname)),
cnorm)
if showall:
s += "\n%s" % command
s += printfooter()
source_object.emit_to(s)
def process_commands(source_object, steps=0):
"process one or more commands "
global CMDSTACKS,STACKPTRS
ptr = STACKPTRS[source_object]
commands = CMDSTACKS[source_object]
if steps:
try:
cmds = commands[ptr:ptr+steps]
except IndexError:
cmds = commands[ptr:]
for cmd in cmds:
#this so it is kept in case of traceback
STACKPTRS[source_object] = ptr + 1
show_curr(source_object)
source_object.execute_cmd(cmd)
else:
show_curr(source_object)
source_object.execute_cmd(commands[ptr])
def reload_stack(source_object):
"reload the stack"
global CMDSTACKS,FILENAMES
commands = parse_batchbuild_file(FILENAMES[source_object])
if commands:
CMDSTACKS[source_object] = commands
else:
source_object.emit_to("Commands in file could not be reloaded. Was it moved?")
def move_in_stack(source_object, step=1):
global CMDSTACKS, STACKPTRS
N = len(CMDSTACKS[source_object])
currpos = STACKPTRS[source_object]
STACKPTRS[source_object] = max(0,min(N-1,currpos+step))
def exit_state(source_object):
global CMDSTACKS,STACKPTRS,FILENAMES
del CMDSTACKS[source_object]
del STACKPTRS[source_object]
del FILENAMES[source_object]
source_object.clear_state()
def cmd_state_l(command):
"l-ook at current command definition"
show_curr(command.source_object,showall=True)
def cmd_state_p(command):
"p-rocess current command definition"
process_commands(command.source_object)
command.source_object.emit_to(printfooter())
def cmd_state_r(command):
"r-eload file, keep current stack position"
reload_stack(command.source_object)
command.source_object.emit_to("\nFile reloaded. Staying on same command.\n")
show_curr(command.source_object)
def cmd_state_rr(command):
"r-eload file, start over"
global STACKPTRS
reload_stack(command.source_object)
STACKPTRS[command.source_object] = 0
command.source_object.emit_to("\nFile reloaded. Restarting from top.\n")
show_curr(command.source_object)
def cmd_state_n(command):
"n-ext command (no exec)"
source_object = command.source_object
arg = command.command_argument
if arg and arg.isdigit():
step = int(command.command_argument)
else:
step = 1
move_in_stack(source_object, step)
show_curr(source_object)
def cmd_state_b(command):
"b-ackwards to previous command (no exec)"
source_object = command.source_object
arg = command.command_argument
if arg and arg.isdigit():
step = -int(command.command_argument)
else:
step = -1
move_in_stack(source_object, step)
show_curr(source_object)
def cmd_state_s(command):
"s-tep to next command (exec)"
source_object = command.source_object
arg = command.command_argument
if arg and arg.isdigit():
step = int(command.command_argument)
else:
step = 1
process_commands(source_object,step)
show_curr(source_object)
def cmd_state_c(command):
"c-ontinue to process remaining"
global CMDSTACKS,STACKPTRS
source_object = command.source_object
N = len(CMDSTACKS[source_object])
ptr = STACKPTRS[source_object]
step = N - ptr
process_commands(source_object,step)
exit_state(source_object)
source_object.emit_to("Finished processing batch file.")
def cmd_state_j(command):
"j-ump to specific command index"
global STACKPTRS
source_object = command.source_object
arg = command.command_argument
if arg and arg.isdigit():
no = int(command.command_argument)-1
else:
source_object.emit_to("You must give a number index.")
return
ptr = STACKPTRS[source_object]
step = no - ptr
move_in_stack(source_object, step)
show_curr(source_object)
def cmd_state_q(command):
"q-uit state."
exit_state(command.source_object)
command.source_object.emit_to("Aborted interactive batch mode.")
def cmd_state_h(command):
"Help command"
s = """
Interactive batch processing commands:
nn [steps] - next command (no processing)
bb [steps] - back to previous command (no processing)
jj <N> - jump to command no N (no processing)
pp - process currently shown command (no step)
ss [steps] - process & step
ll - look at full definition of current command
rr - reload batch file (stay on current)
rrr - reload batch file (start from first)
hh - this help list
cc - continue processing to end and quit.
qq - quit (abort all remaining)
"""
command.source_object.emit_to(s)
#create the state; we want it as open as possible so we can do everything
# in our batch processing.
GLOBAL_STATE_TABLE.add_state(STATENAME,global_cmds='all',
allow_exits=True,allow_obj_cmds=True)
#add state commands
GLOBAL_STATE_TABLE.add_command(STATENAME,"nn",cmd_state_n)
GLOBAL_STATE_TABLE.add_command(STATENAME,"bb",cmd_state_b)
GLOBAL_STATE_TABLE.add_command(STATENAME,"jj",cmd_state_j)
GLOBAL_STATE_TABLE.add_command(STATENAME,"pp",cmd_state_p)
GLOBAL_STATE_TABLE.add_command(STATENAME,"ss",cmd_state_s)
GLOBAL_STATE_TABLE.add_command(STATENAME,"cc",cmd_state_c)
GLOBAL_STATE_TABLE.add_command(STATENAME,"ll",cmd_state_l)
GLOBAL_STATE_TABLE.add_command(STATENAME,"rr",cmd_state_r)
GLOBAL_STATE_TABLE.add_command(STATENAME,"rrr",cmd_state_rr)
GLOBAL_STATE_TABLE.add_command(STATENAME,"hh",cmd_state_h)
GLOBAL_STATE_TABLE.add_command(STATENAME,"qq",cmd_state_q)

View file

@ -210,7 +210,7 @@ def cmd_set(command):
if attrib_value:
# An attribute value was specified, create or set the attribute.
target.set_attribute(attrib_name, attrib_value)
s = "Attribute %s=%s set to %s." % (target_name, attrib_name, attrib_value)
s = "Attribute %s=%s set to '%s'" % (target_name, attrib_name, attrib_value)
else:
# No value was given, this means we delete the attribute.
ok = target.clear_attribute(attrib_name)

View file

@ -12,6 +12,9 @@ from src.scripthandler import rebuild_cache
from src.util import functions_general
from src.cmdtable import GLOBAL_CMD_TABLE
def cmd_reload(command):
"""
Reloads all modules.

View file

@ -299,6 +299,7 @@ COMMAND_MODULES = (
'src.commands.search',
'src.commands.imc2',
'src.commands.irc',
'src.commands.batchprocess'
)
"""

0
src/defines_global.py Executable file → Normal file
View file