Open4

漢字でGO!雑記

素人の戯言素人の戯言

目標

  • システムで問題を解析して回答を推測

前提

  • RPGツクール開発経験:なし
  • JavaScript開発経験:趣味程度
  • HTML5開発経験:趣味程度
  • 画像解析経験:趣味程度

アプローチ

  • 全問題の画像と回答を紐づけ
  • 問題画面を画像解析し、問題文字列領域を抽出して全問題の画像と類似度を比較
  • 類似度が高い順に回答候補を提示

補足

  • 全問題の画像解析理由
    使用フォントであるセイビタカナワBが有料により、フォントからのレンダリングは不採用としたため。 またHTML5で書かれた(?)ゲーム画面内のDOM要素をどうすれば取れるか知らないため。
素人の戯言素人の戯言

ソースコード

以下から最新版を取得。

画像はrpgmvp形式。暗号化された画像で、System.jsonencryptionKeyを用いて復号する。
本システムではwww/data/System.jsonに配置されている。
問題画像はwww/img/pictures以下にあり、Lv**_****.rpgmvpというファイル名で格納されている。
なお、****0000の場合はダミー画像となっていた。
RPGツクールでは以下で復号化している模様。

rpg_core.js
・・・
Decrypter._headerlength = 16;
Decrypter._xhrOk = 400;
Decrypter._encryptionKey = "";
・・・
Decrypter.decryptArrayBuffer = function(arrayBuffer) {
・・・
    var view = new DataView(arrayBuffer);
    this.readEncryptionkey();
    if (arrayBuffer) {
        var byteArray = new Uint8Array(arrayBuffer);
        for (i = 0; i < this._headerlength; i++) {
            byteArray[i] = byteArray[i] ^ parseInt(Decrypter._encryptionKey[i], 16);
            view.setUint8(i, byteArray[i]);
        }
    }
・・・

以下処理を参考にPythonで一括復号化を試みる。

素人の戯言素人の戯言

rpgmv形式の一括デコードスクリプト

既定値は本システムに特化

rpgmvdecoder.py
import binascii
import json
import re
import traceback
from argparse import ArgumentParser, Namespace
from pathlib import Path
from re import Match, Pattern
from typing import Any

ARGS_FILES: str = "files_root_dir"
ARGS_SYSTEM_JSON: str = "system_json_file"
ARGS_OUTPUT: str = "output_dir"
DEFAULT_FILES_ROOT_DIR: str = "www/img/pictures"
DEFAULT_SYSTEM_JSON_PATH: str = "www/data/System.json"
DEFAULT_FILES_GLOB_PATTERN: str = "**/Lv*.rpgmv*"
DEFAULT_FILE_NAME_PATTERN: Pattern = r"(Lv\d+)_\d+"
OUTPUT_SUBDIR: str = "decoded"


def decrypt_rpgmv(file: Path, key: bytearray) -> bytearray:
    encrypted_data: bytes = file.read_bytes()
    data_body: bytes = encrypted_data[16:]
    decoded_header: bytearray = bytearray(
        a ^ b for a, b in zip(*map(bytearray, [data_body[:16], key]))
    )

    return decoded_header + data_body[16:]


def output_name(file: Path) -> Path:
    return file.with_suffix(
        ".png"
        if file.suffix == ".rpgmvp"
        else (
            ".m4a"
            if file.suffix == ".rpgmvm"
            else ".ogg" if file.suffix == ".rpgmvo" else f"{file.suffix}.bin"
        )
    )


def arg_to_path(args: Namespace, arg_name: str) -> Path:
    target: str = getattr(args, arg_name)
    return (
        (Path(args.root_dir) / target)
        if not args.root_dir.startswith("/")
        and not args.root_dir.startswith("\\")
        and ":" not in args.root_dir
        else Path(target)
    )


def main(args: Namespace) -> None:
    try:
        print("decode start")
        files_root_dir: Path = arg_to_path(args, ARGS_FILES)
        output_root_dir: Path = arg_to_path(args, ARGS_OUTPUT)
        system_json: dict[str, str | Any] = json.loads(
            arg_to_path(args, ARGS_SYSTEM_JSON).read_text(encoding="utf-8")
        )
        encrypted_key: bytearray = bytearray(
            binascii.unhexlify(system_json["encryptionKey"].encode())
        )
        file_name_pattern: Pattern = re.compile(args.name_pattern)
        for source in files_root_dir.glob(args.glob_pattern):
            name_matched: Match = file_name_pattern.match(source.name)
            if not isinstance(name_matched, Match):
                continue
            subdir: str = name_matched.group(1)
            output: Path = output_name(
                output_root_dir
                / f"{OUTPUT_SUBDIR}{'' if not isinstance(subdir, str) else ('/' + subdir)}/{source.name}"
            )
            already_exist: bool = output.exists()
            try:
                output.parent.mkdir(parents=True, exist_ok=True)
                output.write_bytes(decrypt_rpgmv(source, encrypted_key))
                print("decoded", source, "->", output)
            except Exception:
                traceback.print_exc()
                if not already_exist and output.is_file():
                    output.unlink()
        print("decode end")
    except Exception:
        traceback.print_exc()


if __name__ == "__main__":
    arg_parser: ArgumentParser = ArgumentParser(description="rpgmv file decoder")
    arg_parser.add_argument("-d", "--root_dir", default=".", help="source root dir")
    arg_parser.add_argument(
        "-f", f"--{ARGS_FILES}", default=DEFAULT_FILES_ROOT_DIR, help="files root dir"
    )
    arg_parser.add_argument(
        "-s",
        f"--{ARGS_SYSTEM_JSON}",
        default=DEFAULT_SYSTEM_JSON_PATH,
        help="System.json path",
    )
    arg_parser.add_argument(
        "-g",
        "--glob_pattern",
        default=DEFAULT_FILES_GLOB_PATTERN,
        help="files glob pattern",
    )
    arg_parser.add_argument(
        "-n",
        "--name_pattern",
        default=DEFAULT_FILE_NAME_PATTERN,
        help="file name pattern",
    )
    arg_parser.add_argument(
        "-o", f"--{ARGS_OUTPUT}", default=DEFAULT_FILES_ROOT_DIR, help="output root dir"
    )
    main(arg_parser.parse_args())
素人の戯言素人の戯言

回答データはwww/excelData以下にLv**.txtで保存されている。
形式は独自フォーマットのため自前パーサーを組む必要あり。