Hatena::ブログ(Diary)

悟茶辞苑ッ このページをアンテナに追加 RSSフィード

2012-02-08 水曜日

正規表現でテキストファイルの語句を置換するスクリプト(コマンド) UTF-8対応

WSH(JScript)で書いてみました。テキストファイル中の語句を正規表現で置換します。バッチ処理に使えます。

ADODB.Stream を使用していて、UTF-8 入出力対応です。BOM なしでの出力もできます。スクリプト後半はテキストファイルの入出力メソッドになっていますので、同じく BOM なしの UTF-8 を出力したい方がいれば流用可能かと思います。

ウェブ上のサンプルを組み合わせつつ作られたものなので、このスクリプトも自由に改変して利用して頂ければと思います。

/** 
 * @fileOverview テキストファイルを置換する JScript コマンドです。
 * コマンド書式の詳細は、引数を付けずに起動することで表示されます。
 */

/**
 * WshArguments を JavaScript 標準の配列に変換する。
 */
function WshArgumentsToArray(wshArguments)
{
    var arguments = [];
    for (var i = 0; i < wshArguments.length; i++)
    {
        var arg = wshArguments(i);

        // WSHの引数にはダブルクオーテーションを利用することができない
        // 代替手段として、「`」が渡された場合「"」として扱う
        // また、「\`」が渡された場合は「`」として扱う
        arg = arg.replace(/^`/g, "\"");
        arg = arg.replace(/([^\\])`/g, "$1\"");
        arg = arg.replace(/\\`/g, "`");

        arguments.push(arg);
    }
    return arguments;
}

/**
 * コマンド メイン処理
 * @param {Array} arguments 引数文字列を格納した配列
 */
function Main(arguments)
{
    var EXIT_SUCCESS = 0;
    var EXIT_FAILURE = 1;
    var ERROR_TEXT_TOO_FEW_PARAMETERS = "パラメータが少なすぎます";

    if (arguments.length == 0)
    {
        // 引数がなければ使い方を表示して終了する
        ShowUsage();
        return EXIT_SUCCESS;
    }

    // 読込元文字コード
    var charsetDst = "utf-8n";
    // 書込先文字コード
    var charsetSrc = "_autodetect";
    // 読込元ファイル名
    var filenameDst = "";
    // 書込先ファイル名
    var filenameSrc = "";
    // 置換元キーワード
    var keywordDst = "";
    // 置換先キーワード
    var keywordSrc = "";

    // オプション文字列を処理
    while (arguments.length > 0 && arguments[0].charAt(0) == "-")
    {
        var option = arguments.shift().substr(1);
        if (option.charAt(0) == "-")
        {
            // 長いオプション --long
            option = option.substr(1);
            if (option == "charset")
            {
                // 読込元/書込先文字コード指定
                if (arguments.length < 1)
                {
                    WScript.Echo(ERROR_TEXT_TOO_FEW_PARAMETERS);
                    return EXIT_FAILURE;
                }
                charsetDst = arguments.shift();
                charsetSrc = charsetDst;
                arguments.shift();
            }
            else if (option == "charset-dst")
            {
                // 読込元文字コード指定
                if (arguments.length < 1)
                {
                    WScript.Echo(ERROR_TEXT_TOO_FEW_PARAMETERS);
                    return EXIT_FAILURE;
                }
                charsetDst = arguments.shift();
            }
            else if (option == "charset-src")
            {
                // 書込先文字コード指定
                if (arguments.length < 1)
                {
                    WScript.Echo(ERROR_TEXT_TOO_FEW_PARAMETERS);
                    return EXIT_FAILURE;
                }
                charsetSrc = arguments.shift();
            }
            else
            {
                WScript.Echo("不明なオプションです: --" + option);
                return EXIT_FAILURE;
            }
        }
        else
        {
            // 1文字連続指定オプション -AbCd
            for (var i = 0; i < option.length; i++)
            {
                var optionChar = option.charAt(i);
                if (optionChar == "")
                {
                    // パラメータ処理があれば追加...
                }
                else
                {
                    WScript.Echo("不明なオプションです: -" + optionChar);
                    return EXIT_FAILURE;
                }
            }
        }
    }

    // 読込元ファイル名
    if (arguments.length < 1)
    {
        WScript.Echo("読込元ファイル名が指定されていません");
        return EXIT_FAILURE;
    }
    filenameSrc = arguments[0];

    // 書込先ファイル名
    if (arguments.length < 2)
    {
        WScript.Echo("書込先ファイル名が指定されていません");
        return EXIT_FAILURE;
    }
    filenameDst = arguments[1];

    // 置換元キーワード
    if (arguments.length >= 3)
    {
        keywordSrc = arguments[2];
    }

    // 置換先キーワード
    if (arguments.length >= 4)
    {
        keywordDst = arguments[3];
    }

    // テキスト置換処理メイン
    var text = LoadTextFile(filenameSrc, charsetSrc);
    var regex = new RegExp(keywordSrc, "gm");
    text = text.replace(regex, keywordDst);
    SaveTextFile(filenameDst, text, charsetDst);
}

/**
 * コマンドの使用方法を出力する。
 */
function ShowUsage()
{
    var lineSeparator = "\n";
    WScript.Echo(
    "コマンド書式:" + lineSeparator +
    "  (cscript) TextReplace.js <引数> 読込元ファイル名 書込先ファイル名 置換元キーワード(正規表現) 置換後キーワード" + lineSeparator +
    "  ※正規表現中のパターンに一致したキーワードは $1,$2... で表現可能です。" + lineSeparator +
    "" + lineSeparator +
    "引数:" + lineSeparator +
    "--charset 読込元/書込先ファイルの文字コード" + lineSeparator +
    "  --charset-dst 書込先ファイルの文字コード (default: utf-8n)" + lineSeparator +
    "  --charset-src 読込元ファイルの文字コード (default: _autodetect)" + lineSeparator +
    "" + lineSeparator +
    "  charset の値の例:" + lineSeparator +
    "  _autodetect, euc-jp, iso-2022-jp, shift_jis, unicode, utf-8,..."
    );
}

//--[ テキスト入出力機能 開始 ]-------------------------------------------------

// StreamTypeEnum Constants
var adTypeBinary = 1;
var adTypeText = 2;

// LineSeparatorEnum Constants
var adLF = 10;
var adCR = 13;
var adCRLF = -1;

// StreamWriteEnum Constants
var adWriteChar = 0;
var adWriteLine = 1;

// SaveOptionsEnum Constants
var adSaveCreateNotExist = 1;
var adSaveCreateOverWrite = 2;

// StreamReadEnum Constants
var adReadAll = -1;
var adReadLine = -2;

/**
 * テキストファイルを読み込む。
 * @param {String} filename 読み込むファイルのパス
 * @param {String} charset 文字コード
 * @return {String} 文字列
 */
function LoadTextFile(filename, charset)
{
    // 文字コードの指定なき場合、自動検出とする。
    // (誤判定が起きることもあるので注意)
    if (charset == undefined)
    {
        charset = "_autodetect";
    }

    var stream, text;
    stream = new ActiveXObject("ADODB.Stream");
    stream.type = adTypeText;
    stream.charset = charset;
    stream.open();
    stream.loadFromFile(filename);
    text = stream.readText(adReadAll);
    stream.close();
    return text;
}

/**
 * テキストファイルを書き出す。
 * @param {String} filename 書き出すファイルのパス
 * @param {String} text 書き出すテキスト
 * @param {String} charset 文字コード
 */
function SaveTextFile(filename, text, charset)
{
    // 文字コードの指定なき場合、UTF-8Nとする。
    if (charset == undefined)
    {
        charset = "utf-8n";
    }

    // 独自にBOM除去を行う文字コードの判定
    var trimUnicodeBOM = false;
    var unicodeBOMLength = 0;
    if (charset.match(/^utf-8n$/i))
    {
        charset = charset.replace(/n$/i, "");
        unicodeBOMLength = 3;
        trimUnicodeBOM = true;
    }
    else if (charset.match(/^utf-16n$/i))
    {
        charset = charset.replace(/n$/i, "");
        unicodeBOMLength = 2;
        trimUnicodeBOM = true;
    }

    var stream;
    stream = new ActiveXObject("ADODB.Stream");
    stream.type = adTypeText;
    stream.charset = charset;
    stream.open();
    stream.writeText(text);
    if (trimUnicodeBOM)
    {
        // Unicode BOMを除去して出力

        // バイナリモードにするためにPositionを一度0に戻す
        // Readするためにはバイナリモードにする必要がある
        stream.Position = 0;
        stream.Type = adTypeBinary;
        // Positionを進めてから読み込むことで
        // BOMをスキップする
        stream.Position = unicodeBOMLength;
        // 変換後の内容をバッファに読み込む
        var textBinary = stream.Read();
        stream.Close();

        // 読み込んだバイナリデータを再出力する
        stream = new ActiveXObject("ADODB.Stream");
        stream.Type = adTypeBinary;
        stream.Open();
        stream.Write(textBinary);
    }
    stream.saveToFile(filename, adSaveCreateOverWrite);
    stream.close();
}

//--[ テキスト入出力機能 終了 ]-------------------------------------------------

WScript.Quit(Main(WshArgumentsToArray(WScript.Arguments)));

コマンド利用例

cscript //nologo TextReplace.js --charset-src utf-8 --charset-dst utf-8 読込元.txt 書込先.txt まぎわらしい まぎらわしい

2011-08-28 日曜日

VisualBoyAdvance でキー入力をAVIに出力するには?

Web拍手経由でご質問いただきました。

VisualboyAdvanceでLuaでの入力表示に関して色々とぐぐってたら、vba-rerecording rr22?とかいうのでjoypad.getdown(int port)という文章を見つけたので、RamWriter.lua(空白区切り版)という奴に file:write(string.format("%d ", joypad.get())) というのを書いて見たんですが、普通にエラーで出てしまいました。これってどういう風に Joypad.get を書けば入力キーを出力できるようになるんでしょうか?

AVIファイルにキー入力を表示させたいという観点でいくつかお答えします。

Lua を使わずにキー入力を AVI に書き出すアプローチ

ただ AVI に Input Display の表示を書き出すだけなら、Lua を使わなくてもできます。

  1. 「,」キーを押して Input Display を表示する。
  2. AVI にも表示されるように、HUD の表示を In Game に変更する(下記画像参照)。
  3. AVI の録画を行う。

f:id:GOCHA:20110828141621p:image

f:id:GOCHA:20110828141620p:image

利点は実現が楽なことです。欠点は表示形式をカスタマイズできないことです。

Lua を使ってキー入力を得るには?

まず最初に、以下の記事からスクリプトを再度ダウンロードしてください。数あるエミュレータの中でもVBAはAVI書き出し時にフレームが複製されて記録される問題があり、本日それに対する対応コードを加筆しました。

file:write(string.format("%d ", joypad.get())) というのを書いて見たんですが普通にエラーで出てしまいました。

joypad.get() の戻り値は table 型です*1が、%d で number 型の出力を行おうとしているからエラーが出ます。

賢い方法はともかく、メモリの値を出力する場面で以下のようなコードを書けば、Aボタンが押されているかいないかを記録できます。

-- RamWriter.lua:
local pad = joypad.get(1)
framedata = framedata .. string.format("%d ", (pad.A and 1 or 0))

対応する読み込み側の一例は次のような感じです。

-- RamReader.lua:
i = file:read("*n"); frame[f].A = ((i ~= 0) and true or false)

*1:print( joypad.get() ) を行ってみるとより詳しく中身がわかります。

2011-07-16 土曜日

プロセスメモリエディタ「MHS」を利用したメモリ監視・操作

MHSはいわゆるプロセスメモリエディタと呼ばれるもので、ゲームの見えないパラメータを表示させたり、値を書き換えたりすることができます。類似のツールはほかにもありますが、TAS界隈ではよくこのMHSが利用されます*1

f:id:GOCHA:20110716002538p:image:w450

日本では同様のツールとして、うさみみハリケーンなどが知られています。噂に聞くところでは検索機能はこちらの方が高度とも聞きますし、可能であれば他のプロセスメモリエディタ、デバッガと組み合わせて利用すると、できることの幅は広がるでしょう。個人的にはMHSの基本のシンプルさがとても好きです。

それでは、導入から簡単な利用までを見てみましょう。

ダウンロードとインストール

MHS Download Page から MHS6.1.rar をダウンロードして展開するだけです。

f:id:GOCHA:20110702114755p:image:w450

起動〜対象プロセス選択

メモリ監視対象のプログラム(ゲーム)を起動した状態で MHS.exe を起動します。初回起動時には下記のダイアログが表示されます。

Would you like to personalize this software?

Doing this can allow you to avoid detection by anti-cheat software.


If you do not want to do this now, you can always do it later from the Tools/Modify Self menu. (はい/いいえ)

不正防止つきソフトウェアへの対策として、MHSを改変しますか?」というような内容です。TAS目的の方にはまず不要ですし、Tools/Modify Self メニューから後で改変を実施することもできますので、ここでは「いいえ」を選択して次へ進みます。

メインウィンドウが表示されたら、メニューから File → Open Process を開きます。

f:id:GOCHA:20110716002543p:image:w450

現在起動中のプロセスを一覧表示したダイアログが開くので、タイトルや実行ファイル名を参考に、メモリ監視対象のプロセスを選択して、OKを押してください。

f:id:GOCHA:20110716002546p:image:w450

これで、MHS でメモリにアクセスする準備ができました。

メモリアドレスを検索する

メモリアドレスを検索するには、Found Addresses ウィンドウにある、虫眼鏡(単一)アイコンのボタンをクリックします(あるいはメニューから Search → Data-Type Search を選択)。

f:id:GOCHA:20110716002735p:image

すると、検索のための Data-Type Search ダイアログが表示されます。

f:id:GOCHA:20110716002732p:image

まずは、Search グループで基本的な検索条件を指定します。選択肢の意味は下記の表のとおりです。

Data Typeバイト数値の型・範囲
Byte10〜255 の整数
Char1-128〜-127 の整数
Short2-32768〜-32767 の整数
Unsigned Short20〜65535 の整数
Long4-2147483648〜-2147483647 の整数
Unsigned Long40〜4294967295 の整数
64-bit Integer8-9223372036854775808〜9223372036854775807 の整数
Unsigned 64-bit Integer80〜18446744073709551615 の整数
Float4実数 (単精度浮動小数点数)
Double8実数 (倍精度浮動小数点数)
Evalutation Type説明
Exact ValueValue to Find に一致する値を検索する。
Not Equal ToValue to Find に一致しない値を検索する。
RangeFrom より大きく、To より小さい値を検索する。
Greater ThanFind Values Greater Than よりも大きい値を検索する。
Lower ThanFind Values Lower Than よりも小さい値を検索する。
Unknown(不明。指定した型に合う全候補を表示?)

その他の主なポイントは以下のとおりです。

  • デフォルトではマップされたメモリが検索対象に含まれない(エミュレータのメモリアドレスが見つけられない可能性がある)ので、Options の General Search Options から設定画面を開いて、General Search タブで MEM_MAPPED を検索対象とするよう、チェックを付けるとよいかもしれません(ただし検索速度は遅くなります)。
  • Search Range は検索対象とするメモリアドレスの範囲です。おおよそ変更する必要はありません。

OKボタンを押して検索を行うと、Found Addresses の一覧に結果が表示されます。

f:id:GOCHA:20110716002735p:image

多くの場合、ここから絞り込み検索を必要とします。Found Addresses ウィンドウにある、虫眼鏡(複数)アイコンのボタンをクリックします(あるいはメニューから Search → Sub Search を選択)。

すると Sub Search ダイアログが表示されます。使い方は先ほどのダイアログと同じなので割愛します。

f:id:GOCHA:20110716002737p:image

探しているメモリアドレスが見つかったら、項目を右クリックして Add Selected を選択します。

f:id:GOCHA:20110716002738p:image

メインウィンドウで特定のメモリアドレスの値を見ることができるようになりました。

f:id:GOCHA:20110716003108p:image:w450

おまけ:ポインタの検索

特定のアドレス領域を指しているポインタを検索する機能があります。メニューから Search → Pointer Search をクリックすると、下図のダイアログが表示されます。

f:id:GOCHA:20110716103910p:image

動的に変化するアドレスを追いたいときには、ポインタの検索が必要になります。これを知らなくても MHS は使えますが、使い方が想像できる人は知っているとお得です。

ウォッチの編集、改造コード機能の利用

f:id:GOCHA:20110716003108p:image:w450

メインウィンドウにあるメモリアドレス一覧の項目をダブルクリックすると、Modify Address ダイアログが開きます。

f:id:GOCHA:20110716003110p:image

  • Description を編集して、説明文を変更できます。
  • Type を変更すると、値のサイズ・型が変更できます。
  • Show as Hex にチェックを入れると、値を16進数で表示できます。

また、Locked のチェックボックスにチェックを付けると、改造コード機能が利用できます。Lock Type の意味は下記のとおりです。

Evalutation Type説明
Exact指定値を強制する。
Range指定した下限値〜上限値の範囲を強制する。
No Lower Than指定値より小さくならないよう強制する。
No Greater Than指定値より大きくならないよう強制する。

OKを押すと変更が反映されます。

おまけ:アドレス計算に式を利用する

動的に確保されるメモリ領域はメモリアドレスが不定になるため、ここまでで紹介した方法では、起動後に毎回再検索しなければなりません。MHS にはポインタの参照先を辿って動的にアドレスを指定する方法が用意されています。

Modify Address ダイアログの Normal Address タブを開きます。

f:id:GOCHA:20110716003111p:image

中断の Complex Address (Overrides Simple) というチェックボックスにチェックを付けると、下部の入力欄にて、メモリアドレスの表現に式が利用できます。

計算式の仕様は下記のとおりです。

  • 式ではC言語の基本的な演算子が利用可能。
  • 間接参照は [40B0F0] のように角括弧で行う(括弧は入れ子にしても問題ない)。
記述例: [40B0F0] + 0x1536 + ([40B0F4] & 0xff)
おまけ:値を計算式で加工して表示する

Expression Evaluator ウィンドウに式を入力して追加すると、値を計算式で加工して表示することができます。

計算式の仕様は、動的アドレス計算に使用するものと同じです(上記参照)。

メモリウォッチ内容の保存

MHS を終了する前に、メモリウォッチの内容を保存しておきましょう。

ファイルの保存は、メニューから File → Save Selected As を選択して行います。

保存したファイルは次回、File → Open Save File から開くことができます。

まとめ

とりとめのない紹介になりましたが、下記をもってまとめとします。

  • MHS を使えば簡単・強力にメモリの検索・監視・書き換えが行える。
  • 解析技術があれば、動的に変化するアドレスにも柔軟に対応できる。よくわからなければ、メモリアドレスによっては、起動ごとに毎回検索する必要があることに注意すべし。
  • MHS には Script など、まだ紹介されていない興味深い機能が多数ある。

Windows 上で動くゲームの TAS 制作を考えている方の参考になればうれしいです!

関連(かもしれない)記事

*1:多くのエミュレータは RAM Watch 機能を内蔵しているのでプロセスメモリエディタを利用する必要はないのですが、Mupen64 のようにそういった機能を持たないエミュレータに対してはプロセスメモリエディタが利用されます。また、最近では Windows でも Hourglass を利用した TAS 記録が行えるようになったため、Windows 向けゲームに対して TAS 目的で利用する事例も増えると考えられます。