1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Python(Ren'Py)製ゲームのチートエンジンの作り方

1
Posted at

未来の自分向けに備忘録を残します。
MODスクリプト方式での実装方法をメモします。

仕組み

  • Ren'Py 製ゲームは game/ フォルダに .rpy を置くだけ` でスクリプトが追加される
  • ファイル名は cheat.rpy じゃなくてもOK(mod.rpy でも同じ)
  • ゲーム本体(.rpa アーカイブ)の改造は不要
  • やることは スクリプト解析 → 変数・ラベル把握 → 追加 .rpy 作成 の3ステップ

1. 前提知識:Ren'Py とは

Ren'Py は Python ベースのビジュアルノベルエンジン。配布物はだいたい次の構造になる。

GameTitle/
├── GameTitle.exe     # ランチャー
├── renpy/            # エンジン本体(触らなくてよい)
└── game/             # ★ ゲーム定義(MOD はここ)
    ├── script.rpy    # または archive.rpa にパックされている
    ├── images.rpa
    ├── audio.rpa
    └── (ここに追加 .rpy を置く)

重要:読み込みの仕組み

起動時に Ren'Py は game/ をスキャンし、
以下のファイル群を すべて1つのスクリプト空間にマージ する。

  • ディスク上の .rpy / .rpyc
  • .rpa アーカイブ内のスクリプト

renpy/loader.py より:

Files on disk should be checked before archives.

ディスク上のファイルがアーカイブより優先される。
つまり .rpa を展開・再パックしなくても、
game/ に素の .rpy を置けば MOD として動く。

「Python製ゲーム」全般との違い

Ren'Py 一般的な Python ゲーム
スクリプト追加 game/xxx.rpy を置くだけ import / フック改造が必要なことが多い
シーン移動 label + jump でやることが多い 自前設計に依存
変数操作 store.変数名 でグローバルに触れる クラス設計次第
開発者コンソール 標準装備(ON にするだけ) ほぼ無い

「Python 製だから cheat.rpy が効く」わけではなく、
「Ren'Py だから .rpy 追加 MOD が成立する」ということ。

2. 全体ワークフロー

① エンジン判定
   Ren'Py か? → game/ と .rpa の有無を確認

② スクリプト解析
   archive.rpa を展開 → script.rpy から label・変数を洗い出す

③ cheat.rpy(任意の名前)を作成
   - config.developer / config.console でコンソール ON
   - screen でチートメニュー(任意)
   - Jump / SetVariable / Function で既存イベント・変数を操作

④ game/ に配置してゲーム再起動
   自動で .rpyc にコンパイルされ読み込まれる

3. スクリプトの取り出し(.rpa 展開)

多くの Ren'Py 製ゲームは script.rpy を archive.rpa にパックしている。
ディスク上に .rpy が無ければ展開が必要。

展開用 Python スクリプト例

Ren'Py の RPAv3 形式向け(ゲームによってはツールが異なる場合あり):

import zlib
import pickle
import os
import sys


def extract_rpa(rpa_path, out_dir):
    with open(rpa_path, "rb") as f:
        header = f.read(40)
        offset = int(header[8:24], 16)
        key = int(header[25:33], 16)
        f.seek(offset)
        index = pickle.loads(zlib.decompress(f.read()))

        for k, v in index.items():
            if len(v[0]) == 2:
                index[k] = [(o ^ key, d ^ key) for o, d in v]
            else:
                index[k] = [(o ^ key, d ^ key, s) for o, d, s in v]

        for name, chunks in index.items():
            dest = os.path.join(out_dir, name.replace("/", os.sep))
            os.makedirs(os.path.dirname(dest), exist_ok=True)
            parts = []
            for o, d, *_ in chunks:
                f.seek(o)
                parts.append(f.read(d))
            with open(dest, "wb") as out:
                out.write(b"".join(parts))


if __name__ == "__main__":
    extract_rpa(sys.argv[1], sys.argv[2])
python extract_rpa.py game/archive.rpa game/extracted

解析で調べるもの

調べる対象 用途 検索例
label xxx: イベントジャンプ先 ^label event_
default 変数 = チートで書き換える値 ^default
define キャラ 参照用 ^define
if stage == 等 イベント分岐条件 ラベル周辺を読む

展開結果は調査用。
MOD 本体は game/cheat.rpy だけで足りる。

4. 最小構成:開発者コンソールだけ有効化

これだけでも jump や変数操作が可能。
ファイル名は何でもよい。

# game/cheat.rpy

init python:
    config.developer = True
    config.console = True
操作 内容
開き方 Shift + O(Ren'Py デフォルト)
イベントジャンプ jump event_some_label
ラベル確認 renpy.has_label("event_some_label")
変数設定 money = 99999
ヘルプ help

ローカルラベル(サブラベル)のジャンプ

サブラベルは 親.子 形式が必須。. だけの名前は使えない。

jump event_2130_random.event_laundry
jump time_mctime.event_bath

コンソールで使えないこと

  • 単独のローカルラベル名(.event_laundry だけなど)
  • call 前提のイベントを jump したときの戻り先が不自然になる場合がある

5. 実用構成:チートメニュー付き MOD

コンソールは強力だがコマンドを覚える必要がある。
screen(UI)+ overlay でゲーム内メニューを作ると再利用しやすい。

5.1 Python ヘルパー(init python)

ゲームが default で定義した変数は store 経由で触れる。
以下は一例。
変数名はソースコードをgrepしてイイ感じに見つけること。

init python:
    config.developer = True
    config.console = True

    def cheat_fill_vitals():
        store.stamina = store.max_stamina
        store.action_point = store.max_action_point

    def cheat_jump(label, **setup):
        for key, value in setup.items():
            setattr(store, key, value)
        renpy.jump(label)

    def cheat_goto_hour(hour, minute=0):
        store.current_hour = hour
        store.current_minute = minute
        renpy.jump("main_loop")

ポイント:

  • store.変数名 でセーブデータと共有のゲーム変数にアクセス
  • renpy.jump("label") でコンソールの jump と同等のことができる
  • ジャンプ前に setattr(store, ...) で前提条件をセットできる

5.2 キー入力でメニューを開く(overlay)

init -1 python:
    config.overlay_screens.append("cheat_menu_toggle")

screen cheat_menu_toggle():
    zorder 199
    key "shift_K_c" action ToggleScreen("cheat_menu")

overlay_screens に登録した screen はゲーム中ずっと動く。
任意のキー(例: Shift+C)でメニューの開閉ができる。

5.3 メニュー本体

screen cheat_menu():
    modal True
    zorder 200
    default page = "main"

    add Solid("#000000bb")

    frame:
        xalign 0.5
        yalign 0.5
        xsize 900
        ysize 700
        padding (20, 16)

        vbox:
            spacing 8
            text "チートメニュー (Shift+C)" size 28

            # タブ切り替え
            hbox:
                textbutton "ホーム" action SetScreenVariable("page", "main")
                textbutton "ジャンプ" action SetScreenVariable("page", "jump")

            viewport:
                scrollbars "vertical"
                mousewheel True
                ysize 500
                vbox:
                    if page == "main":
                        textbutton "体力満タン" action Function(cheat_fill_vitals)
                        textbutton "メインループへ" action [Hide("cheat_menu"), Jump("main_loop")]
                    elif page == "jump":
                        textbutton "イベントA" action [Hide("cheat_menu"), Jump("event_xxx")]

            textbutton "閉じる" action Hide("cheat_menu")

5.4 ボタンアクションの定番パターン

やりたいこと アクション
変数を変える SetVariable("money", 999999)
Python 関数を呼ぶ Function(cheat_fill_vitals)
ラベルへ飛ぶ Jump("event_xxx")
複合処理 [SetVariable("a_stage", 3), Hide("cheat_menu"), Jump("event_xxx")]
条件分岐 If(条件, true=..., false=Notify("..."))

注意: renpy.jump() を含む Function() の後に別アクションを並べても、jump で制御が飛ぶため後続は実行されない。
セットアップ → ジャンプは 1つの Python 関数にまとめる と安全。

def cheat_goto_night_raid():
    store.stamina = store.max_stamina
    store.a_stage = 3
    renpy.jump("battle_a")

6. イベントジャンプの実践ノウハウ

6.1 ラベル一覧の作り方

展開した script.rpy で検索:

# ripgrep 例
rg "^label " game/extracted/script.rpy

グローバルラベルはそのまま jump できる。
ローカルラベル(label .foo:)は 親ラベル.foo 形式。

6.2 前提変数が必要なイベント

多くのイベントはラベルだけでは正しく動かない。
分岐条件を script から読み取り、事前にセットする。

例(概念):

# stage3 の続行ルートを見たい
a_stage = 3
jump event_stage_3_reveal

# バトルをすぐ開始する
action_point = 5
stamina = 100
a_stage = 3
desire = 80
jump battle_a

6.3 1回限りイベントの再視聴

not xxx_viewed フラグで再発火を防いでいるゲームが多い。
再視聴用にフラグをリセットするボタンを用意すると便利。

def cheat_reset_story_flags():
    store.event_50_viewed = False
    store.event_150_viewed = False
    # ... 他の *_viewed フラグも同様

getattr(store, "flag", False) で参照されているフラグは default が無くても store.flag = False でリセット可能。

6.4 時間帯の移動

時刻変数を変えて main_loop に jump するパターンが通用しやすい。

def cheat_goto_hour(hour, minute=0):
    store.current_hour = hour
    store.current_minute = minute
    renpy.jump("main_loop")

main_loop 側が current_hour で time_1900_freetime 等に分岐しているゲームで効く。

7. 動作確認チェックリスト

  • game/cheat.rpy を置いたあと ゲームを再起動したか
  • 同じ場所に cheat.rpyc が生成されているか(起動時に自動コンパイル)
  • Shift+O でコンソールが開くか
  • renpy.has_label("xxx") が True か
  • ジャンプ先で即終了する場合、前提変数が足りない可能性
  • セーブデータを壊すので 試す前にセーブしているか

8. うまくいかない場合

症状 原因と対処
.rpy を置いても無反応 再起動していない / game/ 以外に置いている
コンソールが開かない config.developer と config.console が両方 True か確認
Label xxx not found typo、またはローカルラベルを 親.子 形式で指定していない
イベントは始まるがすぐ戻る 前提変数・フラグ不足。script.rpy の if 条件を読む
画像・音声が出ない jump でシーン初期化(scene)がスキップされている
.rpa が展開できない RPA バージョン違い。unrpa 等の別ツールを試す

配布版で難しいケース

  • config.developer = False がビルド時に固定されている
  • アーカイブが暗号化・難読化されている
  • Ren'Py 以外のエンジンに偽装している

9. 他ゲーム用ミニテンプレ(コピペ起点)

# game/cheat.rpy
# ファイル名は何でもよい。game/ 直下に置く。

init python:
    config.developer = True
    config.console = True

    def cheat_jump(label):
        renpy.jump(label)

init -1 python:
    config.overlay_screens.append("cheat_toggle")

screen cheat_toggle():
    key "shift_K_c" action ToggleScreen("cheat_menu")

screen cheat_menu():
    modal True
    zorder 200

    frame:
        xalign 0.5
        yalign 0.5
        xsize 600
        ypadding 20
        xpadding 20

        vbox:
            spacing 8
            text "Cheat Menu" size 24 bold True
            text "Shift+O: 開発者コンソール" size 16

            textbutton "メインループ (要ラベル名確認)" action [Hide("cheat_menu"), Jump("main_loop")]
            textbutton "変数リセット例 (要変数名確認)" action SetVariable("money", 99999)

            null height 10
            textbutton "閉じる" action Hide("cheat_menu")

あとは script.rpy の調査結果でボタンを増やしていくだけ。

10. 設計のベストプラクティス

ファイル構成

game/
├── cheat.rpy          # MOD 本体(Git 管理推奨)
└── extracted/         # 調査用(配布・コミット不要)
    └── script.rpy

メニュー設計

タブ分けすると探しやすい。

タブ 内容
リソース 体力・お金・スタミナなど
ステージ stage・フラグリセット
時間帯 hour 変更 + main_loop jump
イベント キャラ別・ED別の label jump
その他 ランダムイベント・ミニゲーム

ドキュメントを残すこと

チートメニューと併せて、ラベル一覧・変数一覧のテキストファイルをゲームルートに置くと、数ヶ月後の自分が助かる。

  • 操作手順(Shift+C / Shift+O)
  • ラベル早見表
  • 変数セット例
  • BAD END / 続行ルートの分岐条件

11. 今回のチート制作で得た知見

やったこと

  1. archive.rpa から script.rpy を展開して変数・ラベルをソースコードから調査
  2. game/cheat.rpy を新規作成して動作することを確認(元の配布物には存在しない)
  3. 開発者コンソール(Shift+O)+ チートメニュー(Shift+C)を実装
  4. ラベル早見表 イベントジャンプ.md を制作

やっていないこと

  • archive.rpa の再パック
  • Game.exe や renpy/ の改造
  • ゲーム本体スクリプトの直接編集

Ren'Py の変数まわりで頻出する単語

  • default var = 0 … ゲームが定義。セーブに含まれる
  • store.var … 実行時にどこからでもアクセス可能
  • define CHAR = Character(...) … キャラ定義。ジャンプには通常不要
  • label / jump / call / return … シーン遷移の基本

参考リンク

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?