mirror of
https://github.com/evennia/evennia.git
synced 2026-03-28 18:47:16 +01:00
555 lines
18 KiB
Python
555 lines
18 KiB
Python
"""
|
|
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 django.conf import settings
|
|
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"
|
|
|
|
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 is considered to be the name of the batch file
|
|
relative the directory specified in settings.py
|
|
"""
|
|
filename = os.path.abspath("%s/%s" % (settings.BATCH_IMPORT_PATH, 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):
|
|
"""
|
|
@batchprocess - build from batch file
|
|
|
|
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; this is a superuser only command.
|
|
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:
|
|
string = "'%s' not found.\nYou have to supply the real path "
|
|
string += "of the file relative to \nyour batch-file directory (%s)."
|
|
source_object.emit_to(string % (filename, settings.BATCH_IMPORT_PATH))
|
|
return
|
|
switches = command.command_switches
|
|
if switches and switches[0] in ['inter','interactive']:
|
|
# allow more control over how batch file is executed
|
|
|
|
if not source_object.set_state(STATENAME):
|
|
# if we failed it is likely because we have
|
|
# ADMIN_NOSTATE set.
|
|
source_object.unset_flag("ADMIN_NOSTATE")
|
|
if not source_object.set_state(STATENAME):
|
|
source_object.emit_to("Error in entering the interactive state.")
|
|
source_object.set_flag("ADMIN_NOSTATE")
|
|
return
|
|
else:
|
|
string = "OBS: Flag ADMIN_NOSTATE unset in order to "
|
|
string += "run Interactive mode. Don't forget to re-set "
|
|
string += "it (if you need it) after you're done."
|
|
source_object.emit_to(string)
|
|
|
|
# store work data in cache
|
|
source_object.cache.batch_cmdstack = commands
|
|
source_object.cache.batch_stackptr = 0
|
|
source_object.cache.batch_filename = filename
|
|
|
|
source_object.emit_to("\nBatch processor - Interactive mode for %s ..." % filename)
|
|
show_curr(source_object)
|
|
else:
|
|
set_admin_nostate = False
|
|
if not source_object.has_flag("ADMIN_NOSTATE"):
|
|
source_object.set_flag("ADMIN_NOSTATE")
|
|
set_admin_nostate = True
|
|
source_object.emit_to("Running Batch processor - Automatic mode for %s ..." % filename)
|
|
source_object.clear_state()
|
|
batch_process(source_object, commands)
|
|
source_object.emit_to("%s== Batchfile '%s' applied." % (cgreen,filename))
|
|
if set_admin_nostate:
|
|
source_object.unset_flag("ADMIN_NOSTATE")
|
|
|
|
GLOBAL_CMD_TABLE.add_command("@batchprocess", cmd_batchprocess,
|
|
priv_tuple=("genperms.process_control",), help_category="Building")
|
|
|
|
#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."
|
|
ptr = source_object.cache.batch_stackptr
|
|
commands = source_object.cache.batch_cmdstack
|
|
|
|
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)
|
|
source_object.cache.batch_stackptr = 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 "
|
|
ptr = source_object.cache.batch_stackptr
|
|
commands = source_object.cache.batch_cmdstack
|
|
|
|
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
|
|
source_object.cache.batch_stackptr = 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"
|
|
commands = parse_batchbuild_file(source_object.cache.batch_filename)
|
|
if commands:
|
|
ptr = source_object.cache.batch_stackptr
|
|
else:
|
|
source_object.emit_to("Commands in file could not be reloaded. Was it moved?")
|
|
|
|
def move_in_stack(source_object, step=1):
|
|
"store data in stack"
|
|
N = len(source_object.cache.batch_cmdstack)
|
|
currpos = source_object.cache.batch_stackptr
|
|
source_object.cache.batch_stackptr = max(0,min(N-1,currpos+step))
|
|
|
|
def exit_state(source_object):
|
|
"Quit the state"
|
|
source_object.cache.batch_cmdstack = None
|
|
source_object.cache.batch_stackptr = None
|
|
source_object.cache.batch_filename = None
|
|
|
|
# since clear_state() is protected against exiting the interactive mode
|
|
# (to avoid accidental drop-outs by rooms clearing a player's state),
|
|
# we have to clear the state directly here.
|
|
source_object.state = None
|
|
|
|
def cmd_state_ll(command):
|
|
"""
|
|
ll
|
|
|
|
Look at the full source for the current
|
|
command definition.
|
|
"""
|
|
show_curr(command.source_object,showall=True)
|
|
|
|
def cmd_state_pp(command):
|
|
"""
|
|
pp
|
|
|
|
Process the currently shown command definition.
|
|
"""
|
|
process_commands(command.source_object)
|
|
command.source_object.emit_to(printfooter())
|
|
|
|
def cmd_state_rr(command):
|
|
"""
|
|
rr
|
|
|
|
Reload the batch file, keeping the current
|
|
position in it.
|
|
"""
|
|
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_rrr(command):
|
|
"""
|
|
rrr
|
|
|
|
Reload the batch file, starting over
|
|
from the beginning.
|
|
"""
|
|
reload_stack(command.source_object)
|
|
command.source_object.cache.batch_stackptr = 0
|
|
command.source_object.emit_to("\nFile reloaded. Restarting from top.\n")
|
|
show_curr(command.source_object)
|
|
|
|
def cmd_state_nn(command):
|
|
"""
|
|
nn
|
|
|
|
Go to next command. No commands are executed.
|
|
"""
|
|
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_nl(command):
|
|
"""
|
|
nl
|
|
|
|
Go to next command, viewing its full source.
|
|
No commands are executed.
|
|
"""
|
|
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, showall=True)
|
|
|
|
def cmd_state_bb(command):
|
|
"""
|
|
bb
|
|
|
|
Backwards to previous command. No commands
|
|
are executed.
|
|
"""
|
|
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_bl(command):
|
|
"""
|
|
bl
|
|
|
|
Backwards to previous command, viewing its full
|
|
source. No commands are executed.
|
|
"""
|
|
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, showall=True)
|
|
|
|
def cmd_state_ss(command):
|
|
"""
|
|
ss [steps]
|
|
|
|
Process current command, then step to the next
|
|
one. If steps is given,
|
|
process this many commands.
|
|
"""
|
|
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_sl(command):
|
|
"""
|
|
sl [steps]
|
|
|
|
Process current command, then step to the next
|
|
one, viewing its full source. If steps is given,
|
|
process this many commands.
|
|
"""
|
|
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, showall=True)
|
|
|
|
def cmd_state_cc(command):
|
|
"""
|
|
cc
|
|
|
|
Continue to process all remaining
|
|
commands.
|
|
"""
|
|
source_object = command.source_object
|
|
N = len(source_object.cache.batch_cmdstack)
|
|
ptr = source_object.cache.batch_stackptr
|
|
step = N - ptr
|
|
process_commands(source_object,step)
|
|
exit_state(source_object)
|
|
source_object.emit_to("Finished processing batch file.")
|
|
|
|
def cmd_state_jj(command):
|
|
"""
|
|
j <command number>
|
|
|
|
Jump to specific command number
|
|
"""
|
|
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 = source_object.cache.batch_stackptr
|
|
step = no - ptr
|
|
move_in_stack(source_object, step)
|
|
show_curr(source_object)
|
|
|
|
def cmd_state_jl(command):
|
|
"""
|
|
jl <command number>
|
|
|
|
Jump to specific command number and view its full source.
|
|
"""
|
|
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 = source_object.cache.batch_stackptr
|
|
step = no - ptr
|
|
move_in_stack(source_object, step)
|
|
show_curr(source_object, showall=True)
|
|
|
|
def cmd_state_qq(command):
|
|
"""
|
|
qq
|
|
|
|
Quit the batchprocessor.
|
|
"""
|
|
exit_state(command.source_object)
|
|
command.source_object.emit_to("Aborted interactive batch mode.")
|
|
|
|
def cmd_state_hh(command):
|
|
"Help command"
|
|
s = """
|
|
Interactive batch processing commands:
|
|
nn [steps] - next command (no processing)
|
|
nl [steps] - next & look
|
|
bb [steps] - back to previous command (no processing)
|
|
bl [steps] - back & look
|
|
jj <N> - jump to command nr N (no processing)
|
|
jl <N> - jump & look
|
|
pp - process currently shown command (no step)
|
|
ss [steps] - process & step
|
|
sl [steps] - process & step & look
|
|
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, then quit.
|
|
qq - quit (abort all remaining commands)
|
|
"""
|
|
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,exit_command=True)
|
|
#add state commands
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"nn",cmd_state_nn)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"nl",cmd_state_nl)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"bb",cmd_state_bb)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"bl",cmd_state_bl)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"jj",cmd_state_jj)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"jl",cmd_state_jl)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"pp",cmd_state_pp)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"ss",cmd_state_ss)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"sl",cmd_state_sl)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"cc",cmd_state_cc)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"ll",cmd_state_ll)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"rr",cmd_state_rr)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"rrr",cmd_state_rrr)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"hh",cmd_state_hh)
|
|
GLOBAL_STATE_TABLE.add_command(STATENAME,"qq",cmd_state_qq)
|