mirror of
https://github.com/evennia/evennia.git
synced 2026-03-21 07:16:31 +01:00
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:
parent
41365074fd
commit
eebfa0d387
7 changed files with 468 additions and 1 deletions
57
game/gamesrc/commands/examples/batch_command_example.ev
Normal file
57
game/gamesrc/commands/examples/batch_command_example.ev
Normal 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
0
src/ansi.py
Executable file → Normal file
406
src/commands/batchprocess.py
Normal file
406
src/commands/batchprocess.py
Normal 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)
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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
0
src/defines_global.py
Executable file → Normal file
Loading…
Add table
Add a link
Reference in a new issue