🎮

RGSS (RPGツクールXP/VX) のスクリプトデータを平文で得る

2021/07/30に公開
3

2004年発売のRPGツクールXPに搭載されたRGSS (Ruby Game Scripting System) のことを久しぶりに思い出して、懐かしんでいました。ゲームエンジンをRubyによってかなり自由度高く制御できます。これと出会わなければ私はこのIT業界にはいなかったでしょう。
https://tkool.jp/products/rpgxp/index/index.html

このスクリプトデータは .rxdata (VXだと .rvdata) という独自バイナリ形式で固められています。久しぶりに中を見たくなりましたが、我が家のツクールはどっかに行ってしまったので、Rubyの力で解読します。

ソースコード (Scripts.rxdata) 以外にも.rxdataはありますが、今回は省略します。


環境

Ruby 1.8.1 以上ならなんでも (初代RGSSの内蔵Rubyバージョンが1.8.1)

形式

Scripts.rxdataファイルはバイナリファイルで、Marshal.dump で固められています。フォーマットはこんな感じです。

[[ID1, セクション名1, ソースコードのzlib圧縮バイナリ1], 
 [ID2, セクション名2, ソースコードのzlib圧縮バイナリ2], ...]

デコードする実装

デコード結果をセクションごとにテキストファイルに出してみます。ソースコード文字列については、そのままだと x\x9C\xDDX_S\x13W\x14\x7Fw\xC6\xEFp+>$... のような値になっており、 zlibの展開処理が必要です。

require 'zlib'

rxdata = IO.binread("Scripts.rxdata")
data = Marshal.load(rxdata)

data.each do |id, name, content|
    content_inf = Zlib::inflate(content)
        .force_encoding("UTF-8")
        .gsub("\r\n", "\n")    
    File.write("#{name}_#{id}.rb", content_inf)
end

以下は出力される Main セクションの例です。死ぬほど懐かしい。

#==============================================================================
# ■ Main
#------------------------------------------------------------------------------
#  各クラスの定義が終わった後、ここから実際の処理が始まります。
#==============================================================================

begin
  # トランジション準備
  Graphics.freeze
  # シーンオブジェクト (タイトル画面) を作成
  $scene = Scene_Title.new
  # $scene が有効な限り main メソッドを呼び出す
  while $scene != nil
    $scene.main
  end
  # フェードアウト
  Graphics.transition(20)
rescue Errno::ENOENT
  # 例外 Errno::ENOENT を補足
  # ファイルがオープンできなかった場合、メッセージを表示して終了する
  filename = $!.message.sub("No such file or directory - ", "")
  print("ファイル #{filename} が見つかりません。")
end

またこちらはGame_Tempセクションの例です。

#==============================================================================
# ■ Game_Temp
#------------------------------------------------------------------------------
#  セーブデータに含まれない、一時的なデータを扱うクラスです。このクラスのイン
# スタンスは $game_temp で参照されます。
#==============================================================================

class Game_Temp
  #--------------------------------------------------------------------------
  # ● 公開インスタンス変数
  #--------------------------------------------------------------------------
  attr_accessor :map_bgm                  # マップ画面 BGM (バトル時記憶用)
  attr_accessor :message_text             # メッセージ 文章
  attr_accessor :message_proc             # メッセージ コールバック (Proc)
  attr_accessor :choice_start             # 選択肢 開始行
  attr_accessor :choice_max               # 選択肢 項目数
  attr_accessor :choice_cancel_type       # 選択肢 キャンセルの場合
  attr_accessor :choice_proc              # 選択肢 コールバック (Proc)
  attr_accessor :num_input_start          # 数値入力 開始行
  attr_accessor :num_input_variable_id    # 数値入力 変数 ID
  # (以下略)

おわりに

Marshal.load, Zlib::inflate といった処理の組み合わせで読み込むことができます。最新のRuby 3.0.0でも読み込むことができ、Marshalの仕様は変わりないようですね。

これと逆の変換をかければ .rxdata 形式を作れることも意味します。他のエディタ・IDEでコーディングしたのちに.rxdataとして固めるという使い方が可能です。

というのは今更で、16,7年前にやるべきでしたが。RPGツクールXPのスクリプトエディタは、タブ表示もできず補完も一切なしで、2004年当時としても使いづらいものでした。

参考文献

8割がた私の記憶だけ頼りに書いており、Zlib::Inflateだけ忘れていました。
https://blog.aotak.me/post/69162974987/git-for-rpgtkool

RPGツクールVX, VX Aceは私は持っていなかったので、ほとんど似ていると聞いていますがもしかすると違うかもしれません。

Discussion

名無し名無し

すみません。こちらの記事を読んで以前製作したゲームのScripts.rxdataを平文化することができましたが、rubyに詳しくないので他のrxdataファイルをmarshal.loadすることができませんでした。よろしければほかのファイルをmarshal.loadするプログラムについて教えていただけないでしょうか。

shimatshimat

記事最後に示した参考サイトを当たると良いと思います。特に参考になるのはその記事中の というわけでためしにActors.rvdata2をMarshal.loadしてみよう。 以下のところですね。示されているGistを使わせて頂けば、大概はうまくいくような気がします。この記事はRPGツクールVX Ace向けに書かれているのでもしかすると非互換性があり、そこは要注意です。

たとえば Actors.rxdata なら以下コードのようにすれば読めるということです。省略している箇所はGistまたはRPGツクールのヘルプファイルを参照してコピペしてきてください。

事前のこうしたクラス定義をせずに Marshal.load をすると、uninitialized constant RPG::BaseItem (NameError) のように欠けているクラス定義がエラーメッセージに出るので、それを見て都度クラスを手前に足していく流れになります。

class Table
  # 略
end

module RPG
end
class RPG::BaseItem
  # 略
end
class RPG::Actor < RPG::BaseItem
  # 略
end

rxdata = IO.binread("Actors.rxdata")
data = Marshal.load(rxdata)
p data
# 出力例
[nil, #<RPG::Actor:0x0000022a2408f868 @armor4_id=0, @initial_level=1, @name="\xE3\x82\xA2\xE3\x83\xAB\xE3\x82\xB7\xE3\x82\xA7\xE3\x82\xB9", @character_name="001-Fighter01", @weapon_id=1, @armor2_fix=false, @parameters=#<Table:...