はじめに:BethesdaゲームのMod構造とゲームエンジンの壁
10年以上にわたり、Bethesda Game Studiosが開発したCreation Engineは——『The Elder Scrolls V: Skyrim』や『Fallout 4』の駆動エンジンとして——PCゲーム史上最も活発で巨大なコミュニティModエコシステムを育んできました。数万人に及ぶクリエイターの手により、バニラのゲームは現代的なアクション戦闘、美麗な動的天候シェーダー、洗練されたユーザーインターフェース、高精度な物理シミュレーションを備えた作品へと生まれ変わっています。
しかし、公式エンジン自体は外部からの機能拡張に対して極めて保守的な制限を課しています。公式Mod制作ツール「Creation Kit」が採用するスクリプト言語 Papyrus は、メモリ破壊を防ぐサンドボックス化されたインタープリタ言語です。安全性に優れる反面、Papyrusは厳しい実行速度制限(タイムスライス)を受け、物理メモリ領域への直接アクセスや、マウス・キーボードの低レベル制御、高度なUI連携APIを直接呼び出すことができません。
この制約を突破するためにリバースエンジニアリングコミュニティが生み出したのが、Skyrim Script Extender(SKSE) です。SKSEは実行時にゲームプロセスのメモリ空間へ自身を注入(インジェクション)し、ゲームエンジン内部をフックすることで、Papyrus仮想マシンに数百ものネイティブマシン語命令を追加し、外部C++ DLLプラグインの直接実行を可能にします。
SKSE、Papyrus VM、そして動的メモリアドレス解決の連携メカニズムを理解することは、ゲームの強制終了(CTD)の解決、セーブデータの肥大化防止、そして公式アップデートによるMod破損を乗り切るための必須知識です。
Papyrus仮想マシンの構造:イベントループ、バイトコードとタイムスライス制限
BethesdaはPapyrusを設計する際、実行速度よりも「ゲーム全体の動作安定性」を最優先事項としました:
[Modスクリプトソース (.psc)] ──> Papyrusコンパイラ ──> バイナリバイトコード (.pex)
│
▼
[Creation Engine メインループ] [Papyrus 仮想マシン (VM)]
フレーム1: 描画 -> 物理演算 -> ゲームロジック ───> 割り当て実行時間枠 (~1.2 ms)
フレーム2: 描画 -> 物理演算 -> ゲームロジック ───> バイトコード命令を順次実行
(時間切れ時は中断して次フレームへ)
1. バイトコードとサンドボックス隔離環境
Mod制作者が記述した .psc テキストコードは、コンパイルされてバイナリ形式の .pex バイトコードへと変換されます。エンジンはこのバイトコードをCPUで直接ネイティブ実行するのではなく、内部のPapyrus仮想マシン(VM)上で逐次解釈して実行します:
- オブジェクト指向のイベント駆動型設計:スクリプトはゲーム内の実体(NPC、クエスト、呪文エフェクトなど)にアタッチされ、エンジンから送出されるイベント(
OnHit被弾、OnEquipped装備、OnTriggerEnterトリガー進入など)をフックして動作します。 - メモリ安全性とサンドボックス:PapyrusはOSの物理メモリアドレスや生ポインタを直接操作できません。スクリプト内にバグや無限ループが存在しても、VM内部のエラーとして安全に処理され、Windowsのアクセス違反(Access Violation 0xC0000005)によるクラッシュを未然に防ぎます。
2. 協調型マルチタスクとフレーム単位の実行時間枠
Modスクリプトの暴走によってゲーム画面の描画フレームレートが低下するのを防ぐため、Papyrusは「協調型マルチタスク」方式を採用しています:
- エンジンは各描画フレームごとに、スクリプトVMに対して極めて小さな実行時間枠(通常は1フレームあたり約 1.2〜2.0ミリ秒)しか割り当てません。
- スクリプトの処理がこの制限時間を超えた場合、VMはそのスレッドの実行を一時中断し、残りの処理を次以降の描画フレームへ先送りします。
- Mod導入時の処理遅延(スクリプトラグ):サバイバル系Modや大規模戦闘AIなど、大量のスクリプトが同時に実行キューに溜まると、キューの処理待ちが数秒間発生し、「ボタンを押しても反応が一拍遅れる」といった深刻な遅延の原因になります。
SKSEの深層:プロセス注入、メモリフックとネイティブ関数登録
SKSEは、実行中の SkyrimSE.exe(Windows PE形式バイナリ)のメモリ空間を直接書き換えることで、エンジンの制約を無効化します。
[skse64_loader.exe 外部ローダー]
│
├─ 1. 中断状態(CREATE_SUSPENDED)でゲームプロセスを生成
├─ 2. リモートスレッドで LoadLibraryW を呼び出し skse64.dll をプロセス空間へ注入
├─ 3. メモリ保護を解除(VirtualProtect)し、エントリーポイントや仮想関数テーブル(VTable)を書き換え
└─ 4. メインスレッドを再開(ResumeThread)
│
▼
[SkyrimSE.exe プロセスメモリ空間]
├── オリジナルエンジンコード(DirectX描画、Havok物理、オーディオ)
├── Papyrus スクリプト仮想マシン
│ └── [フックされた仮想関数テーブル]: ネイティブ関数登録テーブル(C++マシン語)
└── SKSE プラグインマネージャー
└── "Data/SKSE/Plugins/" 内の全サードパーティDLLプラグインをロード
1. ローダーによる中断生成とリモートスレッド注入
プレイヤーが skse64_loader.exe を通じてゲームを起動した際の一連の挙動は以下の通りです:
- ローダーはWindows APIの
CreateProcessをCREATE_SUSPENDEDフラグ付きで呼び出し、プロセスの初期化直後、最初のアセンブリ命令が実行される前にメインスレッドを停止させます。 - 対象プロセスのメモリ上に
skse64.dllのパスを書き込み、CreateRemoteThreadを介してLoadLibraryWを実行させ、SKSEのコアモジュールをプロセス内に読み込ませます。 - SKSEは
VirtualProtectを用いてゲームコード領域の書き込み禁止(PAGE_EXECUTE_READ)を解除(PAGE_EXECUTE_READWRITE)し、目的の関数先頭にジャンプ命令(トランポリンフック)を埋め込んだ後、ResumeThreadでゲームの実行を再開させます。
2. ネイティブC++マシン語関数のバインド
フックが完了すると、SKSEはPapyrus VMの関数登録テーブルを拡張します。Mod制作者がPapyrusスクリプト内でSKSE拡張関数(マウス座標の直接取得や未圧縮データ配列の走査など)を呼び出すと、VMはバイトコードの逐次解釈をスキップし、skse64.dll にコンパイルされた超高速なC++マシン語ルーチンを直接実行します。
バイナリ互換性の危機:公式アプデとAddress Libraryによる解決
かつてスカイリムのMod環境において最大の脅威となっていたのが、Bethesda公式によるゲームの小規模パッチ配信でした。わずかなアップデートであっても、世界中のDLLプラグインが一斉にクラッシュして起動不能に陥っていたためです。
メモリオフセットの変動問題
Bethesdaがゲームのソースコードを数行修正して新しい SkyrimSE.exe をビルドすると:
- コンパイラによるコード最適化と再配置により、実行ファイル全体のメモリアドレスが変化します。
- バージョン 1.5.97 でオフセット
0x005A3210に存在した内部関数が、1.6.640 では0x005B48F0へ移動します。 - 旧バージョン向けにコンパイルされたDLLプラグインが古いハードコードされたアドレスにアクセスを試みた瞬間、不正な領域への書き込みが発生し、ゲーム起動時にクラッシュ(Access Violation 0xC0000005)を引き起こします。
[スカイリム 1.5.97] [スカイリム 1.6.640]
オフセット 0x005A3210: GetPlayerCamera() ──再コンパイル──> オフセット 0x005B48F0: GetPlayerCamera()
▲ ▲
│(ハードコードされたポインタが不正アクセスでCTD!) │
[旧式 SKSE プラグイン DLL] ───────────────────────────(アクセス違反 0xC0000005)
Address LibraryとCommonLibSSEによるIDマッピング
この脆さを根本から解決するために生み出されたのが、Address Library for SKSE Plugins です:
- 統一IDデータベース:ゲームの歴代全バージョンにおける数万個の主要関数に対して、バージョンに依存しない恒久的な一意の識別番号(ID)を付与したデータベースを構築。
- 実行時の動的アドレス解決:現代のDLLプラグインはアドレスを直接指定せず、
CommonLibSSEを介して実行時にデータベースへ問い合わせを行います:// プラグイン実行時にカメラ管理クラスの実アドレスを取得 uintptr_t targetAddress = AddressLibrary::GetAddress(52147); - バージョン更新への耐性:Bethesdaが新しいパッチを配布しても、Address Libraryの単一データベースファイルを更新するだけで済み、Mod制作者がコードを書き直して再配布するのを待つ必要がなくなりました。
セーブデータの構造、孤児スクリプトとPapyrusスタックダンプ
スカイリムのセーブデータ形式(.ess)の根本的な特徴として、セーブが行われた瞬間に稼働していたPapyrus VMの全状態が、そのままセーブファイル内へ直接シリアライズ(保存)されます。
[.ess セーブファイルの内部構造]
├── グローバルワールド状態(FormIDレコード、天候、ゲーム内日時)
├── プレイヤーのインベントリおよび進行中のクエストフラグ
└── [Papyrus 仮想マシンの稼働スナップショット]
├── 現在実行中の全スクリプトインスタンス
├── 変数の実データおよびゲームオブジェクト参照
└── タイムスライス待機中の中断スレッドコールスタック
プレイ中のスクリプトMod削除が招く致命的な破損
スクリプトを含むModをセーブデータの途中でアンインストールした場合:
- ドライブから
.pexファイルが削除されても、.essセーブデータの中にはそのスクリプトの稼働インスタンスや待機中のイベントがそのまま残留します。 - 次回ロード時、Papyrus VMはセーブデータからスクリプトを復元しようと試みますが、対応する
.pexが見つからないため、エラーをPapyrus.0.logに大量出力し続けます。 - これらは参照先を失った**孤児スクリプト(Orphaned Scripts)**となり、解放されることなく毎フレームVMの処理枠を消費します。時間の経過とともにセーブファイルの肥大化(Save Bloat)やスクリプトラグを招き、最終的にセーブデータが完全に破損します。
スクリプトキューの飽和とスタックダンプ
Modの競合によってスクリプトが無限ループに陥ったり、削除されたオブジェクトを永遠に待ち続けたりすると、そのスレッドは実行権を手放さなくなります。未消化のスクリプト呼び出しがVMの安全許容量(数万件以上)を超えると、ログに大量の**スタックダンプ(Stack Dump)**が出力され、VMの破綻に伴ってゲームは即座にCTDへと至ります。
トラブルシューティング・マトリクス:スクリプト拡張・DLL競合・CTD診断
| 症状 | 根本的なエンジニアリング原因 | ログでの判別方法 | 推奨される解決策 |
|---|---|---|---|
| 起動ボタンを押した直後に即座にCTDする | DLLプラグインのバージョンがゲームEXEと不一致 | skse64.log 末尾にロード失敗のエラーがないか確認 | 該当DLLを更新するか、EXEバージョンに合致したAddress Libraryを導入 |
| MCMメニューに「SKSE64が実行されていません」と表示 | Steam本体や通常EXEから誤起動しDLLが注入されていない | ゲーム内で半角/全角キーを押し getskseversion と入力 | 必ず Mod Organizer 2 等のランチャーから skse64_loader.exe 経由で起動 |
| 会話の選択肢が出るまで数秒待たされる・動作が重い | Papyrus VMの処理タスクがパンクしスクリプトラグが発生 | Papyrus.0.log に同一スクリプトの大量エラーがないか確認 | 粗悪な常時ポーリングModを特定して排除;FallrimToolsでセーブを清掃 |
| ファストトラベル時やロード扉をくぐった瞬間にCTD | 注入されたネイティブDLL内でヌルポインタ参照が発生 | Crash Logger SSE などのクラッシュログを確認 | スタックトレース最上段のモジュール名を特定し、競合Modを修復 |
| セーブファイルが50MBを超え、ロード時間が異常に長い | アンインストールしたModの孤児スクリプトがセーブ内に蓄積 | FallrimTools ReSaver で該当 .ess ファイルを開いて確認 | 未バインドのスクリプトインスタンスを一括削除;プレイ中のMod削除を禁止 |
ゲームエンジンのフック機構と仮想マシンの協調構造を理解することで、Mod環境のトラブルシューティングは当て推量から確実な技術診断へと昇華します。365Findsの専門的な知見を活用することで、プレイヤーや開発者はメモリフックやバージョン管理の壁を乗り越え、極めて安定した快適なMod環境を構築できます。