diff --git a/docs/cli.rst b/docs/cli.rst index f974468..6cc8fb1 100644 --- a/docs/cli.rst +++ b/docs/cli.rst @@ -274,21 +274,40 @@ lines $ lmlsb dump --help Usage: lmlsb dump [OPTIONS] INPUT_FILE... - Dump the contents of the specified LSB file(s) to stdout in a human- - readable format. + Dump the contents of the specified LSB file(s) to stdout in a human-readable + format. - For text mode, the full LSB will be output as human-readable text. + MODE: + text + The full LSB will be output as human-readable text. - For xml mode, the full LSB file will be output as an XML document. + xml + The full LSB file will be output as an XML document. - For lines mode, only text lines will be output. + lines + only text lines will be output. + + json + The output will be a JSON-formatted LSB command (JSON will always be in UTF-8 format). + You can edit the JSON and import it back using the lmlsb edit command. + Use "jsonfull" option if you want to output all line. + Note: - Don't forget to set the "modified" flag to true for each line you edit. + - This mode will only output the editable lines. + + jsonfull + Will output the complete line instead of only editable lines + + + Example: + lmlsb.exe dump 00000001.lsb -m json -o 00000001.json Options: - -m, --mode [text|xml|lines] Output mode (defaults to text) - -e, --encoding [cp932|utf-8] Output text encoding (defaults to utf-8). - -o, --output-file FILE Output file. If unspecified, output will be - dumped to stdout. - --help Show this message and exit. + -m, --mode [text|xml|lines|json|jsonfull] + Output mode (defaults to text) + -e, --encoding [cp932|utf-8] Output text encoding (defaults to utf-8). + -o, --output-file FILE Output file. If unspecified, output will be + dumped to stdout. + --help Show this message and exit. edit ^^^^ @@ -311,7 +330,7 @@ For more specific usage/implementation details refer to the thread in `issue #9 :: $ lmlsb edit --help - Usage: lmlsb edit [OPTIONS] LSB_FILE LINE_NUMBER + Usage: lmlsb edit [OPTIONS] LSB_FILE [LINE_NUMBER] Edit the specified command within an LSB file. @@ -324,11 +343,38 @@ For more specific usage/implementation details refer to the thread in `issue #9 behavior (or a complete crash) in the LiveMaker engine during runtime. Note: Setting empty fields to improper data types may cause undefined - behavior in the LiveMaker engine. When editing a field, the data type of - the new value is assumed to be the same as the original data type. + behavior in the LiveMaker engine. When editing a field, the data type of the + new value is assumed to be the same as the original data type. + + Batch mode: + You can batch edit several line and paramaters with JSON file. + The format of JSON file is as follow: + { + "36" : { + "modified": true, + "params": { + "PR_LEFT": "20", + "PR_TOP": "12" + } + } + } + + You can generate the JSON via "lmlsb.exe dump" command with JSON mode. + + Example: + - To edit line 33 with input prompt: + lmlsb.exe edit 00000001.lsb 33 + + - To set the value of PR_LEFT parameter on line 33 to 20: + lmlsb.exe edit 00000001.lsb 33 -p '{\"PR_LEFT\": 20}' + + - To import back the value from lmlsb dump: + lmlsb.exe edit 00000001.lsb -b 00000001.json Options: - --help Show this message and exit. + -p, --param TEXT Parameter in JSON format. + -b, --batch TEXT Edit with parameter with JSON formatted file. + --help Show this message and exit. extract ^^^^^^^ diff --git a/src/livemaker/cli/lmlsb.py b/src/livemaker/cli/lmlsb.py index 8ea9561..6eb3a43 100644 --- a/src/livemaker/cli/lmlsb.py +++ b/src/livemaker/cli/lmlsb.py @@ -19,6 +19,7 @@ import csv import hashlib +import json import re import shutil import sys @@ -150,9 +151,57 @@ def validate(input_file): print(f" script mismatch, {len(orig_bytes)} {len(new_bytes)}") +def _dump_json(lsb, pylm, jsonmode): + json_data = {} + for cmd in lsb.commands: + this_data = {} + this_data["text"] = str(cmd) + this_data["type"] = cmd.type.name + this_data["mute"] = cmd.Mute + this_data["modified"] = False + this_data["indent"] = cmd.Indent + + comp_keys = None + + try: + comp_keys = cmd._component_keys + this_data["editable"] = True + except AttributeError: + this_data["editable"] = False + if jsonmode != "jsonfull": + continue + + if comp_keys is not None: + params = {} + for key in cmd._component_keys: + params[key] = str(cmd[key]) + this_data["params"] = params + + if cmd.type == CommandType.TextIns: + dec = LNSDecompiler() + this_data["script"] = str(dec.decompile(cmd.get("Text"))) + + ref = cmd.get("Page") + if ref and isinstance(ref, LabelReference): + if ref.Page.endswith("lsb") and pylm: + # resolve lsb refs + line_no, name = pylm.resolve_label(ref) + if line_no is not None: + this_data["target"] = {} + this_data["target"]["line"] = str(line_no) + this_data["target"]["label"] = str(name) + + json_data[cmd.LineNo] = this_data + return json_data + + @lmlsb.command() @click.option( - "-m", "--mode", type=click.Choice(["text", "xml", "lines"]), default="text", help="Output mode (defaults to text)" + "-m", + "--mode", + type=click.Choice(["text", "xml", "lines", "json", "jsonfull"]), + default="text", + help="Output mode (defaults to text)", ) @click.option( "-e", @@ -171,11 +220,33 @@ def validate(input_file): def dump(mode, encoding, output_file, input_file): """Dump the contents of the specified LSB file(s) to stdout in a human-readable format. - For text mode, the full LSB will be output as human-readable text. - - For xml mode, the full LSB file will be output as an XML document. - - For lines mode, only text lines will be output. + \b + MODE: + text + The full LSB will be output as human-readable text. + + \b + xml + The full LSB file will be output as an XML document. + + \b + lines + only text lines will be output. + + \b + json + The output will be a JSON-formatted LSB command (JSON will always be in UTF-8 format). + You can edit the JSON and import it back using the lmlsb edit command. + Use "jsonfull" option if you want to output all line. + Note: - Don't forget to set the "modified" flag to true for each line you edit. + - This mode will only output the editable lines. + \b + jsonfull + Will output the complete line instead of only editable lines + + \b + Example: + lmlsb.exe dump 00000001.lsb -m json -o 00000001.json """ if output_file: outf = open(output_file, mode="w", encoding=encoding) @@ -210,6 +281,10 @@ def dump(mode, encoding, output_file, input_file): etree.tostring(root, encoding=encoding, pretty_print=True, xml_declaration=True).decode(encoding), file=outf, ) + elif mode == "json" or mode == "jsonfull": + jsonData = _dump_json(lsb, pylm, mode) + print(json.dumps(jsonData, ensure_ascii=False, indent=4), file=outf) + elif mode == "lines": lsb_path = Path(path) for line, name, scenario in lsb.text_scenarios(): @@ -766,10 +841,75 @@ def _edit_calc(cmd): _edit_parser(parser) +def _edit_component_auto(cmd, setting): + """Edit a BaseComponent (or subclass) command with predetermined settings.""" + print() + print("Apply setting :", setting) + print() + + edited = False + + for key in setting: + if key not in cmd._component_keys: + print(f"Warning: Cannot find {key} in command") + continue + + parser = cmd[key] + + if key not in setting: + continue + else: + print(f"{key} : {parser} -> {setting[key]}") + + # TODO: editing complex fields and adding values for empty fields will + # require full LiveParser expression parsing, for now we can only edit + # simple scalar values. + if ( + len(parser.entries) > 1 + or (len(parser.entries) == 1 and parser.entries[0].type != OpeDataType.To) + or (len(parser.entries) == 0 and key not in EDITABLE_PROPERTY_TYPES) + ): + print(f"{key} [{parser}]: ") + continue + + value = setting[key] + if parser.entries: + e = parser.entries[0] + op = e.operands[-1] + if op: + if value != op.value: + if op.type == ParamType.Int or op.type == ParamType.Flag: + op.value = int(value) + elif op.type == ParamType.Float: + op.value = numpy.longdouble(value) + else: + op.value = value + edited = True + else: + if value: + param_type = EDITABLE_PROPERTY_TYPES[key] + try: + if param_type == ParamType.Int or param_type == ParamType.Flag: + value = int(value) + elif param_type == ParamType.Float: + value = numpy.longdouble(value) + op = Param(value, param_type) + e = OpeData(type=OpeDataType.To, name="____arg", operands=[op]) + parser.entries.append(e) + edited = True + + except ValueError: + print(f"Invalid datatype for {key}, skipping.") + continue + return edited + + def _edit_component(cmd): """Edit a BaseComponent (or subclass) command.""" print() print("Enter new value for each field (or keep existing value)") + + edited = False for key in cmd._component_keys: parser = cmd[key] # TODO: editing complex fields and adding values for empty fields will @@ -780,7 +920,7 @@ def _edit_component(cmd): or (len(parser.entries) == 1 and parser.entries[0].type != OpeDataType.To) or (len(parser.entries) == 0 and key not in EDITABLE_PROPERTY_TYPES) ): - print(f"{key} [{parser}]: ") + print("{} [{}]: ".format(key, parser)) continue if parser.entries: e = parser.entries[0] @@ -794,6 +934,7 @@ def _edit_component(cmd): op.value = numpy.longdouble(value) else: op.value = value + edited = True else: value = click.prompt(key, default="") if value: @@ -806,8 +947,12 @@ def _edit_component(cmd): op = Param(value, param_type) e = OpeData(type=OpeDataType.To, name="____arg", operands=[op]) parser.entries.append(e) + edited = True except ValueError: - print(f"Invalid datatype for {key}, skipping.") + print("Invalid datatype for {}, skipping.".format(key)) + continue + + return edited def _edit_jump(cmd): @@ -830,10 +975,31 @@ def _edit_jump(cmd): _edit_parser(parser) +def _main_edit(cmd, setting, line_number): + if line_number is not None: + print("{}: {}".format(line_number, str(cmd).replace("\r", "\\r").replace("\n", "\\n"))) + + if isinstance(cmd, BaseComponentCommand): + if setting is not None: + return _edit_component_auto(cmd, setting) + else: + return _edit_component(cmd) + elif isinstance(cmd, Calc): + _edit_calc(cmd) + elif isinstance(cmd, Jump): + _edit_jump(cmd) + else: + print(f"Cannot edit {cmd.type.name} commands.") + return False + return True + + @lmlsb.command() @click.argument("lsb_file", required=True, type=click.Path(exists=True, dir_okay=False)) -@click.argument("line_number", required=True, type=int) -def edit(lsb_file, line_number): +@click.argument("line_number", required=False, type=int) +@click.option("-p", "--param", type=str, help="Parameter in JSON format.") +@click.option("-b", "--batch", type=str, help="Edit with parameter with JSON formatted file.") +def edit(lsb_file, line_number, param, batch): """Edit the specified command within an LSB file. Only specific command types and specific fields can be edited. @@ -849,30 +1015,82 @@ def edit(lsb_file, line_number): the data type of the new value is assumed to be the same as the original data type. + \b + Batch mode: + You can batch edit several line and paramaters with JSON file. + The format of JSON file is as follow: + { + "36" : { + "modified": true, + "params": { + "PR_LEFT": "20", + "PR_TOP": "12" + } + } + } + + You can generate the JSON via "lmlsb.exe dump" command with JSON mode. + + \b + Example: + - To edit line 33 with prompt: + lmlsb.exe edit 00000001.lsb 33 + + \b + - To set the value of PR_LEFT parameter on line 33 to 20: + lmlsb.exe edit 00000001.lsb 33 -p '{\\"PR_LEFT\\": 20}' + + \b + - To import back the value from lmlsb dump: + lmlsb.exe edit 00000001.lsb -b 00000001.json """ + batchData = None + if batch is not None: + with open(batch, "rb") as f: + try: + batchData = json.load(f) + except LiveMakerException as e: + sys.exit(f"Could not read JSON file : {batch}\nWith error {e}") + + if batch is None and line_number is None: + sys.exit("One of the parameter line number or -b must exist") + with open(lsb_file, "rb") as f: try: lsb = LMScript.from_file(f) except LiveMakerException as e: sys.exit(f"Could not open LSB file: {e}") - cmd = None - for c in lsb.commands: - if c.LineNo == line_number: - cmd = c - break - else: - sys.exit(f"Command {line_number} does not exist in the specified LSB") + # handling setting + setting = None + if param is not None: + print("Parsing param") + try: + setting = json.loads(param) + except LiveMakerException as e: + sys.exit(f"Cannot JSON parse parameter : {param}\nWith error {e}") - print("{}: {}".format(line_number, str(cmd).replace("\r", "\\r").replace("\n", "\\n"))) - if isinstance(cmd, BaseComponentCommand): - _edit_component(cmd) - elif isinstance(cmd, Calc): - _edit_calc(cmd) - elif isinstance(cmd, Jump): - _edit_jump(cmd) - else: - sys.exit(f"Cannot edit {cmd.type.name} commands.") + writeData = False + if line_number is not None: + for c in lsb.commands: + if c.LineNo == line_number: + writeData = _main_edit(c, setting, line_number) + break + else: + sys.exit("Command {line_number} does not exist in the specified LSB") + elif batchData is not None: + if setting is not None: + print("Found both batchData and parameter. Parameter will be ignored.") + for c in lsb.commands: + key = str(c.LineNo) + if key in batchData: + if batchData[key]["modified"]: + print("Found modified line no", c.LineNo) + writeData = _main_edit(c, batchData[key]["params"], c.LineNo) or writeData + print("Batch edit completed") + + if not writeData: + sys.exit("Nothing to write") print("Backing up original LSB.") shutil.copyfile(str(lsb_file), f"{str(lsb_file)}.bak")