diff --git a/game/gamesrc/commands/examples/batch_command_example.ev b/game/gamesrc/commands/examples/batch_command_example.ev new file mode 100644 index 0000000000..a209a2279c --- /dev/null +++ b/game/gamesrc/commands/examples/batch_command_example.ev @@ -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] +# +# 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 \ No newline at end of file diff --git a/src/ansi.py b/src/ansi.py old mode 100755 new mode 100644 diff --git a/src/commands/batchprocess.py b/src/commands/batchprocess.py new file mode 100644 index 0000000000..1fc9c23ca3 --- /dev/null +++ b/src/commands/batchprocess.py @@ -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] + + 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] ") + 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 - 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) diff --git a/src/commands/objmanip.py b/src/commands/objmanip.py index f84e419370..eb7c27cc13 100644 --- a/src/commands/objmanip.py +++ b/src/commands/objmanip.py @@ -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) diff --git a/src/commands/privileged.py b/src/commands/privileged.py index b828ee1eb9..b328fa332b 100644 --- a/src/commands/privileged.py +++ b/src/commands/privileged.py @@ -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. diff --git a/src/config_defaults.py b/src/config_defaults.py index 79e25a77d3..bac5b947f1 100644 --- a/src/config_defaults.py +++ b/src/config_defaults.py @@ -299,6 +299,7 @@ COMMAND_MODULES = ( 'src.commands.search', 'src.commands.imc2', 'src.commands.irc', + 'src.commands.batchprocess' ) """ diff --git a/src/defines_global.py b/src/defines_global.py old mode 100755 new mode 100644