🐥

Godot Modding 入門

に公開

はじめに

私のGodot歴は1日です。

説明に間違いがある可能性があります。
間違いを見つけたらやさしく教えてください。

対象者

  • Godotに興味のある方
    • 今回対象にするゲームは構造がきれいなので、ModdingしながらGodotの勉強もできそうです
  • Moddingに興味がある方
    • 興味があれば大体解決できる!
  • ある程度プログラミングができる方
    • 開発に当たり、まずゲームのプログラムを読む必要があります

あるといい知識

  • Godot, Unityに関する理解
    • Unityに似ているのでUnityの知識は流用できそうでした
  • Modding経験
    • UnityのModdingフレームワークBepInExを使ったことがあると理解が早いと思います

対象のゲーム

ゲーム配信プラットフォームのSteamで配信されているデスクトップ小動物牧場 - Tiny Pastureを対象とします。

デスクトップ下に常駐して牧場で動物を育てる基本放置のんびりゲームです。
かわいい、安いので興味があれば遊んでみてください。

Tiny Pasture

ゲームが提供する機能がすくなく、デコンパイルするとゲーム構造やソースコードがとても丁寧に書かれていることがわかります。
難読化もされていないのでコメントがなくても理解しやすいModdingの入門としても非常によいゲームだと思います。

MOD配布の注意

今回対象とするゲームのMOD作成には成功しましたが、Steamワークショップにはアップロードできませんでした。
公式から提供されているSteamワークショップにMODをアップロードするツールがクラッシュして正常動作しないためです。
この問題は解決しました。
詳細はTiny PastureのSteam workshop uploaderがクラッシュする問題の解決にまとめました。
ただし、やはり配布できるかどうかは先に確認しておいた方が無難かと思いますのでこの項目は残します。

もしMODを配布する予定がある場合はアップロード先と通信できるのか事前に確認しておいた方が無難です。
MODのアップロード先はSteamワークショップ以外にもいくつかあるので他のサービスを検討してもよいと思います。

MOD配布プラットフォーム

成果物

この解説を通しての成果物を先に示します。

Tiny Pasture 桁区切りMODを作成しました。
ゲームを進めるとある程度大きな数値が登場しますが、桁区切りがされていないので少し見づらいということで、これを改善するMODです。

シンプルなMODなので初めてのModdingにはよい課題だと思います。

Modding前の注意

Modding End User License Agreement(EULA)が示されていますので、こちらに目を通してから作業を始めてください。

Modding

ゲームの開発環境

  • ゲームエンジン
    • Godot 4.3
  • 言語
    • GDScript

関連フォルダパス

ゲームファイルフォルダ

Moddingにあたりゲームの配置先を参照する場合は以下の方法で参照できます。

  1. Steamクライアントのゲーム一覧を表示
  2. 対象のゲームを右クリック
  3. 管理を表示
  4. ローカルファイルを閲覧を選択

このパスはユーザにより異なります。以下は例になります。

X:\SteamLibrary\steamapps\common\TinyPasture

Steamワークショップフォルダ

ワークショップフォルダはゲームのインストール先と紐づいています。

インストール先が以下の場合

X:\SteamLibrary\steamapps\common\TinyPasture

ワークショップフォルダは以下になります。

X:\SteamLibrary\steamapps\workshop\content\3167550

3167550はゲームのIDです。
ゲームIDが不明な場合はストアページのアドレスから確認できます。

Tiny Pastureの場合のストアアドレスは以下の通りです。
https://store.steampowered.com/app/3167550/_/

ログデータフォルダ

ログデータは以下に保存されます。
AppDataフォルダは隠しフォルダです。
ファイルエクスプローラのアドレス欄にコピー&ペーストでアクセスする方法が簡単です。

C:\Users\%username%\AppData\Roaming\TinyPasture\logs

セーブデータフォルダ

セーブデータは以下に保存されます。
AppDataフォルダは隠しフォルダです。
ファイルエクスプローラのアドレス欄にコピー&ペーストでアクセスする方法が簡単です。

C:\Users\%username%\AppData\Roaming\TinyPasture\save
セーブデータのバックアップ

Moddingの過程などでセーブデータが破損する可能性があります。
事前にバックアップを取ることを強く推奨します。

Steamクラウドによるセーブデータのバックアップ

通常Steamクラウドによるバックアップが有効化されています。
この影響で、意図せずセーブデータが巻き戻されたりします。
必要に応じてSteamクラウドの利用設定を変更します。

Steamクラウドの設定
  1. Steamクライアントのゲーム一覧を表示
  2. 対象のゲームを右クリック
  3. プロパティを表示
  4. 一般タブを表示(最初に表示されるタブ)
  5. Steamクラウドに[ゲーム名]のセーブデータを保存のトグルを変更

環境構築

作業環境にはマルチバイト文字が含まれないパスが安全だと思います。
(マルチバイトパスの検証はしていません)

ゲームのデコンパイル

デコンパイルツールの導入

Godotのデコンパイルツールをダウンロードします。
先人の知恵でv0.9.0-beta.4を推奨していますが、v0.9.1でも問題なくデコンパイルできました。

私はv0.9.1を利用しました。

ダウンロードしたzipファイルを任意の位置に展開します。

デコンパイル

gdre_tools.exeを実行します。

ゲームインストール先のTinyPasture.pckGodot RE Toolsにドラッグ&ドロップします。

すべての項目にチェックされていることを確認し、出力先のパスを指定してExtractします。

gdre_tools出力設定

ツールを閉じます。

Godot Steamプラグインの導入

プラグイン入手

GodotSteamをダウンロードします。
ゲームのバージョンに合う物をダウンロードしてください。
Godotのバージョンがわからない場合はgdre_toolsでデコンパイル画面で確認できます。
Tiny Pastureの場合はGodot4.3を利用しているので、Godot 4.1-4.4 - Steamworks 1.61 - GodotSteam GDExtension 4.13godotsteam-4.14-gdextension-plugin-4.1-4.3.zipをダウンロードします。

プラグインの導入

ダウンロードしたzipファイルを任意の位置に展開します。

zipファイル内のaddonフォルダに含まれるgodotsteamフォルダをデコンパイル済みのゲームフォルダaddonにコピーし上書きします。

Godot steam addonの導入

プラグインの設定

デコンパイルしたゲームファイル群のルートフォルダにsteam_data.jsonを作成します。
IDはゲーム合わせて変更します。

このファイルを作成しないとGodotからデバック実行できません。

steam_data.json
{ "app_id": "3167550"}

alt text

Godotによるゲームの読み込み

Godotの導入

Godot4.3を利用して作られているので、Godot公式サイトから4.3-stableを入手します。
私はWindows - Standardを入手しました。

ダウンロードしたzipファイルを任意の位置に展開します。

ゲームの読み込み

Godot_v4.3-stable_win64.exeを実行し、Godotを起動します。

インポートボタンからデコンパイルしたゲームフォルダを選択、インポートして編集から読み込みを開始します。

alt text

読み込み時に警告ポップアップが表示されることがありますが無視します。
Godot起動後にファイルの読み込みが入るのでしばらく待ちます。

ModLoaderの有効化
  1. 上部メニューからプロジェクトメニューを表示
  2. プロジェクト設定を選択
  3. プラグインタブ表示
  4. mod loaderを有効化
  5. 設定画面を閉じる

ModLoaderの有効化

MODの基本構造の作成

新規作成

画面上部のMod Toolを選択し、Create new Modを選択します。
NamespaceMod Nameを記入し作成します。

項目名 必須か 説明 備考
NameSpace 団体名や作者名 半角英数が無難、アンダースコアは使用できる
Mod Name MOD名 半角英数が無難、アンダースコアは使用できる

NamespaceMod NameMODのフォルダ名に使用されます。
後でも変更できますが、ややこしいので最初に決めておく方が良いです。

長すぎる名前をつけるとMODが読み込まれない場合があるのである程度の長さにとどめておくとよいです。
50文字くらいの長さのMODを作ったところ、読み込めませんでした。
※試行錯誤していたので、MODの問題ではなかった可能性もあります。

MODの新規作成

MODの保存先

開発中のMODはデコンパイルしたゲームと同じフォルダに格納されます。
MODのファイルは全てmods-unpackedフォルダにNameSpace-ModNameの書式のフォルダに格納されます。
このフォルダ名の書式はとても重要です。
この書式を守らないとModLoaderに認識されず、正常にMODが読み込まれません。

デコンパイルされたゲームフォルダ
├─mods-unpacked // 開発中のMODが格納されるフォルダ
│  │  
│  ├─rin_jugatla-separate_digits // NameSpace-ModNameの書式
│  │  │  manifest.json // MODの基本情報 先ほどのGUIで設定した項目が保存される
│  │  │  mod_main.gd // MODのエントリーポイント
雛形の修正

MODの新規作成を行うとmod_main.gdが雛形から作成されます。
作成されたmod_main.gdを開き、以下の項目を変更します。
この変更を行うとこでゲーム実行時のログファイルでMODの動作を確認しやすくなります。

状態 NameSpace ModName
雛形 AuthorName ModName
変更例 rin_jugatla separate_digits
- const MOD_DIR: = "AuthorName-ModName"
- const LOG_NAME: = "AuthorName-ModName:Main"
+ const MOD_DIR: = "rin_jugatla-separate_digits"
+ const LOG_NAME: = "rin_jugatla-separate_digits:Main"

既存MODの編集

画面上部のMod Toolを選択し、Connect existing Modから編集対象のMODを選択します。

既存のMODの選択

MODの基本情報を編集できます。ここでは最低限の解説とします。

項目名 必須か 説明 備考
Mod Name MOD名 半角英数が無難、アンダースコアは使用できる
NameSpace 団体名や作者名 半角英数が無難、アンダースコアは使用できる
Compatible Game Version 対応するゲームバージョン 今回は1.0.10
Compatible Mod Loader Version 対応するMod Loaderバージョン 今回は7.0.1

設定を変更した場合は必ず画面右上のSave to manifest.jsonを実行します。
Ctrl + Sでは保存されません。

MOD情報の設定

MODの作成

いよいよ本題のMODの中身の作成に入ります。

今回取り組むのは数字を3桁区切りして表示するという内容です。
具体的には以下のように桁区切りされていない箇所が、,で桁区切りされて表示されれば成功となります。

桁区切り例

変更箇所の洗い出し

ゲームをプレイしながら、どの画面のどの部分に変更すべき箇所があるのか洗い出します。

今回は例として動物の購入画面動物の価格を対象とします。

動物の購入画面

Godotで対象のUIシーンを探す

今回はstoreanimalをキーワードに探すと早そうです。
UIを確認する場合は画面上部の2Dを選択し、.tscnファイルを開いて目当てのファイルか確認します。

今回はres://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.tscnが目当てのファイルになります。

ui_shop_pack_item

関連のUIから対象のUIを探す

上記のようにピンポイントに対象のUIを見つけられれば良いですが、実際には少し難しいです。
そこで関連のUIから探す手段も示します。

例えば、今回はに関する画面なのでstoreはファイル名に入っていそうだな、というところからres://Scene/UI/PopupUI/UIShop/ui_shop.tscnを見つけられたとします。

ui_shop

これは店の画面なので、動物のUIにも関連していそうです。
画面左上のシーンを見るとAnimalsが探しているUIに関連していそうです。

目が閉じているアイコンなのでUIが非表示になっています。
アイコンを切り替えてどのようなUIなのか確認しましょう。

探しているUIであることがわかるので、次にビデオのアイコンを押してみましょう。
ビデオのアイコンを押すと、ノードの元となったシーンに飛びます。
UnityでいうところのPrefabのような物と理解するとわかりやすいと思います。

このように関連のUIをたどっていくことで目的のUIにたどり着ける場合があります。
※動的に作成されるUIの場合はこの方法ではたどれないのでスクリプトも参照して頑張ります。

なおスクロールのアイコンを押すと紐づくスクリプトを開けます。

対象のUIアイテムを探す

対象のUIの中からさらに、どのアイテムを参照すればよいのか探します。
今回は金額部分なので、金額部分の2000ラベルUIをクリックします。
ラベルUIをクリックするとシーンのTXT_Priceアイテムが選択状態に変わります。
これで画面内の2000のラベルがTXT_Priceという名前で定義されていることがわかります。

対象のUIアイテムを探す

UIアイテムの紐づく変数を探す

対象のUIアイテムがわかったので、スクリプトでいつ、どのように変更しているか探すためスクリプトを参照します。
スクロールのアイコンを押してシーンに紐づくスクリプトを開きます。

ここではres://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gdが開きます。
コードを見るとTXT_Priceラベルはtxt_price変数に格納されていることがわかります。

UIアイテムの紐づく変数を探す

変数がいつどのように変更されているのか探す

txt_priceでファイル内を検索するとrefresh関数でpack_data.priceをもとにラベルのテキストを変更している事がわかります。

変数がいつどのように変更されているのか探す

既存機能を拡張するスクリプトを作成

ここまでの作業でres://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gdファイルのrefresh関数に変更を加えれば桁区切りを実現できる事がわかりました。
既存のスクリプトを拡張するスクリプトを作成するため、対象のスクリプトの右クリックメニューからModTool: Create Script Extensionを選択します。

既存機能を拡張するスクリプトを作成

編集中のMODの中にファイル構造を維持してMOD用のファイルが作成されます。
MODの機能は以下のスクリプトに記述します。
これにより、MODにゲーム内のオリジナルのファイルを含まず配布、導入することができます。
res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd

自動生成された拡張スクリプト

拡張スクリプトの読み込み

ModTool: Create Script Extensionから拡張スクリプトを作成した場合はこの作業は不要です。
ModToolの機能で拡張スクリプトをロードする記述が自動追記されます。
知っておいた方が理解が深まるので記載します。

拡張スクリプトui_shop_pack_item.gdなどを作成しただけではMODでは読み込まれません。
MOD起動時に拡張スクリプトを読み込むためmod_main.gdにスクリプトをロードするための記述を行います。

mods-unpacked/rin_jugatla-separate_digits/mod_main.gd
func _init() -> void :
    # MODのスクリプトが読み込まれた時のエントリポイント
    # _readyよりも先に実行される
    install_script_extensions()

func install_script_extensions() -> void :
    # この記述で`ui_shop_pack_item.dg`を拡張したスクリプトをロードする
    # 絶対パスで指定する
    ModLoaderMod.install_script_extension("res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_page_facility_one.gd")
    # こちらでもよい(extensions_dir_path.path_joinを展開すると同じパスの指定になる)
    const MOD_DIR: = "rin_jugatla-separate_digits"
    var mod_dir_path = ModLoaderMod.get_unpacked_dir().path_join(MOD_DIR)
    var extensions_dir_path = mod_dir_path.path_join("extensions")
    ModLoaderMod.install_script_extension(extensions_dir_path.path_join("Scene/UI/PopupUI/UIShop/ui_shop_page_facility_one.gd"))

桁区切り機能の実装

該当シーンの桁区切りを実装するスクリプトは以下の通りです。

mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
extends "res://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd"

## 桁区切りされていない数値を桁区切り
static func format_number_text_with_commas(number_text: String) -> String:
    # ゲームアップデートで桁区切りに対応した場合に二重で桁区切りしないようチェック
    var has_comma = number_text.find(",") > -1
    if has_comma:
        return number_text

    var formated = str(number_text)
    var length = formated.length()
    while length > 3:
        formated = formated.insert(length - 3, ",")
        length -= 3
    return formated

func refresh():
    super()
    
    txt_price.text = format_number_text_with_commas(txt_price.text)
実装の説明

ModLoaderでは既存スクリプトを継承して機能を実装します。
このため、既存スクリプトに記述済みのフィールド変数にアクセスしたり、関数を上書きまたは実行前後に機能を割り込ませることができます。

このため、ファイルの最初にextendsで元のスクリプトを指定します。

mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
extends "res://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd"

さらに、桁区切りを行う関数を実装します。

mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
## 桁区切りされていない数値を桁区切り
static func format_number_text_with_commas(number_text: String) -> String:
    # ゲームアップデートで桁区切りに対応した場合に二重で桁区切りしないようチェック
    var has_comma = number_text.find(",") > -1
    if has_comma:
        return number_text

    var formated = str(number_text)
    var length = formated.length()
    while length > 3:
        formated = formated.insert(length - 3, ",")
        length -= 3
    return formated

最後にrefresh関数にフックします。
既存の機能は書き換えず、価格のラベルだけ変更すればよいので、super()で元の関数を実行させた後に、桁区切りされていない価格の文字列が格納されたtxt_price.textから桁区切りした結果をラベルに反映します。

mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
func refresh():
    super()
    
    txt_price.text = format_number_text_with_commas(txt_price.text)

MODのデバッグ

実装がうまくいっているかMODをデバッグします。

F5キーまたは画面右上のプロジェクトを実行からゲームを実行します。
必要に応じてブレークポイントを貼ります。

Tiny Pastureでは起動直後にエラーが出るのでF12キーまたは〇->アイコンから処理を続行させます。

デバッグ

MODのテスト

Godotからデバッグできない場合や本番環境でテストする場合はゲームに直接MODを読み込ませます。

MODのパッキング

MODは基本的にzipで配布します。
また、GodotのMODエクスポート機能もzipでパッキングするので特に理由がなければzip形式でテストします。

Tiny Pastureのワークショップフォルダを開きます。
ワークショップフォルダパスはユーザにより異なりますが、関連フォルダパスSteamワークショップフォルダの項からパスを特定します。

例としてワークショップフォルダは以下に配置されていたとします。

X:\SteamLibrary\steamapps\workshop\content\3167550

テスト用にquicktestフォルダを作成します。

X:\SteamLibrary\steamapps\workshop\content\3167550\quicktest

エスクポート先のパスに上記のパスを指定し、Export Modを押します。

MODのパッキング

エクスポートに成功するとファイルエクスプローラが立ち上がり、エクスポート先のフォルダにzipファイルが生成されたことが確認できます。
すでにzipファイルが存在する場合でもExport Modを押すと新しいzipファイルで上書きし、エクスプローラが立ち上がります。
エクスプローラが立ち上がらない場合はエクスポートに失敗している可能性が高いです。
ゲームが起動しているとエクスポートできないので、起動していないか確認しましょう。

エクスポートされたMOD

MODが読み込まれているか確認

MODをエクスポートし、ゲームを起動します。

ゲームの起動時にMODが読み込まれているか確認します。
ログフォルダのgodot.logをメモ帳等で開きます。

C:\Users\%username%\AppData\Roaming\TinyPasture\logs

このエラーはMODを導入していない場合でも表示されるので無関係です。

godot.log
ERROR ModLoader:Path: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
USER ERROR: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
   at: push_error (core/variant/variant_utility.cpp:1092)

見つかったMOD数とMOD名が列挙されます。
ここでMODが見つからない場合はフォルダ構造やファイル名の書式が間違っている可能性があります。

godot.log
DEBUG ModLoader: Found 1 mods at the following paths:

読み込むMODには順序があります。
順序は以下のログで確認できます。

godot.log
INFO ModLoader: mod_load_order -> 1) rin_jugatla-separate_digits

MODの関係でERRORが表示されているとゲームが起動した直後にクラッシュしゲームが終了します。
添付のログではここでエラーが出てゲームをクラッシュさせています。

godot.log
USER SCRIPT ERROR: Parse Error: Identifier "DigitsUtility" not declared in the current scope.
   at: GDScript::reload (res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd:6)
MODの読み込みに成功した場合のログファイル

Godot Engine v4.3.stable.official.77dcf97d8 - https://godotengine.org
OpenGL API 3.3.0 NVIDIA 576.02 - Compatibility - Using Device: NVIDIA - NVIDIA GeForce RTX 3080

INFO ModLoader:Store: Options override feature tag "editor". does not apply, skipping.
INFO ModLoader:Godot: override.cfg setup detected, ModLoader will be the last autoload loaded.
DEBUG ModLoader: Autoload order
[
"ModLoaderStore",
"ModLoader",
"GState",
"FarmDB",
"GSave",
"GSteam",
"GSound"
]
INFO ModLoader: game_install_directory: X:/SteamLibrary/steamapps/common/TinyPasture/
ERROR ModLoader:Path: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
USER ERROR: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
at: push_error (core/variant/variant_utility.cpp:1092)
INFO ModLoader:Path: The directory for mods at path "X:/SteamLibrary/steamapps/common/TinyPasture/mods" does not exist.
INFO ModLoader:ThirdParty:Steam: Checking workshop items, with path: "X:/SteamLibrary/steamapps/workshop/content/3167550"
INFO ModLoader:ThirdParty:Steam: Checking workshop item path: "X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest"
DEBUG ModLoader: Found 1 mods at the following paths:
- X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip
DEBUG ModLoader:File: Loading mod_manifest from -> X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip
SUCCESS ModLoader: X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip loaded.
SUCCESS ModLoader: DONE: Loaded 1 mod files into the virtual filesystem
DEBUG ModLoader:Config: No config for mod id "rin_jugatla-separate_digits"
DEBUG ModLoader:UserProfile: Updated the mod lists of all user profiles
DEBUG ModLoader:UserProfile: Updated the active state of all mods, based on the current user profile "default"
SUCCESS ModLoader: DONE: Loaded all mod configs
DEBUG ModLoader:Dependency: Checking dependencies - mod_id: rin_jugatla-separate_digits optional dependencies: []
DEBUG ModLoader:Dependency: Checking dependencies - mod_id: rin_jugatla-separate_digits required dependencies: []
INFO ModLoader: mod_load_order -> 1) rin_jugatla-separate_digits
INFO ModLoader: Initializing -> rin_jugatla-separate_digits
DEBUG ModLoader: Loading script from -> res://mods-unpacked/rin_jugatla-separate_digits/mod_main.gd
DEBUG ModLoader: Loaded script -> <GDScript#-9223371977463429776>
INFO rin_jugatla-separate_digits:Main: Init
DEBUG ModLoader: Adding mod main instance to ModLoader -> rin_jugatla-separate_digits:<Node#59995325809>
DEBUG ModLoader: mod data
{
"rin_jugatla-separate_digits": "<Resource#-9223371978419731128>"
}
SUCCESS ModLoader: DONE: Completely finished loading mods
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UICollection/hover_tip/wd_collection_tip.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UICollection/hover_tip/wd_collection_tip.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/animal_box/ui_upgrade_tip.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/animal_box/ui_upgrade_tip.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/ui_farm.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/ui_farm.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/ui_farm_animal_detail.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/ui_farm_animal_detail.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_bg.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_bg.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_normal.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_normal.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/ui_shop_page_facility_one.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_page_facility_one.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/baby_creater.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/baby_creater.gd
SUCCESS ModLoader: DONE: Installed all script extensions
SUCCESS ModLoader: DONE: Applied all scene extensions
INFO rin_jugatla-separate_digits:Main: Ready
Did Steam initialize?: { "status": 0, "verbal": "" }
USER ERROR: Parameter "fd" is null.
at: _font_get_ascent (modules/text_server_adv/text_server_adv.cpp:2608)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_descent (modules/text_server_adv/text_server_adv.cpp:2640)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_ascent (modules/text_server_adv/text_server_adv.cpp:2608)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_descent (modules/text_server_adv/text_server_adv.cpp:2640)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_ascent (modules/text_server_adv/text_server_adv.cpp:2608)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_descent (modules/text_server_adv/text_server_adv.cpp:2640)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_ascent (modules/text_server_adv/text_server_adv.cpp:2608)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
USER ERROR: Parameter "fd" is null.
at: _font_get_descent (modules/text_server_adv/text_server_adv.cpp:2640)
USER ERROR: Parameter "fd" is null.
at:_font_get_spacing (modules/text_server_adv/text_server_adv.cpp:2464)
backup saved at: 0019
Game Saved!
ERROR: 1 RID allocations of type 'N5GLES37TextureE' were leaked at exit.
USER ERROR: Texture with GL ID of 14: leaked 3248 bytes.
at: ~Utilities (drivers/gles3/storage/utilities.cpp:79)
USER ERROR: Parameter "RenderingServer::get_singleton()" is null.
at: ~CompressedTexture2D (scene/resources/compressed_texture.cpp:464)
USER WARNING: ObjectDB instances leaked at exit (run with --verbose for details).
at: cleanup (core/object/object.cpp:2284)
USER ERROR: 1 resources still in use at exit (run with --verbose for details).
at: clear (core/io/resource.cpp:604)

MODの読み込みに失敗した場合のログファイル

Godot Engine v4.3.stable.official.77dcf97d8 - https://godotengine.org
OpenGL API 3.3.0 NVIDIA 576.02 - Compatibility - Using Device: NVIDIA - NVIDIA GeForce RTX 3080

INFO ModLoader:Store: Options override feature tag "editor". does not apply, skipping.
INFO ModLoader:Godot: override.cfg setup detected, ModLoader will be the last autoload loaded.
DEBUG ModLoader: Autoload order
[
"ModLoaderStore",
"ModLoader",
"GState",
"FarmDB",
"GSave",
"GSteam",
"GSound"
]
INFO ModLoader: game_install_directory: X:/SteamLibrary/steamapps/common/TinyPasture/
INFO ModLoader:UserProfile: No mod_ids inside "mod_list" for user profile "default"
ERROR ModLoader:Path: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
USER ERROR: Encountered an error (Invalid parameter) when attempting to open a directory, with the path: res://mods-unpacked/
at: push_error (core/variant/variant_utility.cpp:1092)
INFO ModLoader:Path: The directory for mods at path "X:/SteamLibrary/steamapps/common/TinyPasture/mods" does not exist.
INFO ModLoader:ThirdParty:Steam: Checking workshop items, with path: "X:/SteamLibrary/steamapps/workshop/content/3167550"
INFO ModLoader:ThirdParty:Steam: Checking workshop item path: "X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest"
DEBUG ModLoader: Found 1 mods at the following paths:
- X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip
DEBUG ModLoader:File: Loading mod_manifest from -> X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip
SUCCESS ModLoader: X:/SteamLibrary/steamapps/workshop/content/3167550/quicktest/rin_jugatla-separate_digits.zip loaded.
SUCCESS ModLoader: DONE: Loaded 1 mod files into the virtual filesystem
DEBUG ModLoader:Config: No config for mod id "rin_jugatla-separate_digits"
DEBUG ModLoader:UserProfile: Updated the mod lists of all user profiles
DEBUG ModLoader:UserProfile: Updated the active state of all mods, based on the current user profile "default"
SUCCESS ModLoader: DONE: Loaded all mod configs
DEBUG ModLoader:Dependency: Checking dependencies - mod_id: rin_jugatla-separate_digits optional dependencies: []
DEBUG ModLoader:Dependency: Checking dependencies - mod_id: rin_jugatla-separate_digits required dependencies: []
INFO ModLoader: mod_load_order -> 1) rin_jugatla-separate_digits
INFO ModLoader: Initializing -> rin_jugatla-separate_digits
DEBUG ModLoader: Loading script from -> res://mods-unpacked/rin_jugatla-separate_digits/mod_main.gd
DEBUG ModLoader: Loaded script -> <GDScript#-9223371977429875343>
INFO rin_jugatla-separate_digits:Main: Init
DEBUG ModLoader: Adding mod main instance to ModLoader -> rin_jugatla-separate_digits:<Node#60028880242>
DEBUG ModLoader: mod data
{
"rin_jugatla-separate_digits": "<Resource#-9223371978386176689>"
}
SUCCESS ModLoader: DONE: Completely finished loading mods
USER SCRIPT ERROR: Parse Error: Identifier "DigitsUtility" not declared in the current scope.
at: GDScript::reload (res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd:6)
ERROR: Failed to load script "res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd" with error "Parse error".
at: load (modules/gdscript/gdscript.cpp:2936)
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UICollection/hover_tip/wd_collection_tip.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UICollection/hover_tip/wd_collection_tip.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/animal_box/ui_upgrade_tip.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/animal_box/ui_upgrade_tip.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/ui_farm.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/ui_farm.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIFarm/ui_farm_animal_detail.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIFarm/ui_farm_animal_detail.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_bg.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_bg.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_normal.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/decorations/ui_shop_deco_normal.gd
INFO ModLoader:ScriptExtension: Installing script extension: res://Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd <- res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_pack_item.gd
USER SCRIPT ERROR: Parse Error: Identifier "DigitsUtility" not declared in the current scope.
at: GDScript::reload (res://mods-unpacked/rin_jugatla-separate_digits/extensions/Scene/UI/PopupUI/UIShop/ui_shop_opened_pack.gd:6)

MODの動作を確認

変更した部分が正常に動作しているか確認します。
無事動物の購入金額が3桁区切りされたことが確認できます。

MODの動作を確認

MODの公開

ここではSteamワークショップへの公開方法を解説します。
自分で使うだけ、友達に使ってもらうだけならパッキングしたzipで事足りるかと思います。

Steam Workshop Uploaderの入手

Tiny PastureのSteam Workshop Uploaderはゲームエディタという名前で提供されています。
ゲームエディタは提供される機能から乖離があるのでタイトルはSteam workshop uploaderとしました。

このアプリはSteamのブランチ機能でbetaブランチとして提供されることが多いです。

  1. Steamクライアントのゲーム一覧からゲームのプロパティを開く
  2. ベータタブを表示
  3. ベータへの参加からmod uploaderブランチに切り替える

ブランチを切り替えると、自動的にブランチの内容がダウンロードされます。
ダウンロードされない場合はファイルの整合性確認やSteamクライアント、PCの再起動を試します。

ベータブランチの切り替え

Steam Workshop Uploaderの起動

ゲームのプレイから、ゲームエディターを選択して起動

Uoloaderの起動

MODのアップロード

  1. Select mod fileにMODのzipファイルを指定
  2. Select preview imageに画像を指定
    これはSteamワークショップのMOD一覧のサムネイルに使用されます
    新規の場合は新しい画像を指定
    既存MODの更新の場合は未記入または更新するサムネイルの画像を指定
  3. workshop IDにIDを記入
    新規MODの場合は空欄(自動採番されます)
    既存MODの更新の場合は公開済みのWorkshopIDを調べて記入
    Workshop URLが以下の場合は3484179926がIDになります
    https://steamcommunity.com/sharedfiles/filedetails/?id=3484179926
  4. Uploadを押す

alt text

  • Select preview imageに指定したサムネイルの表示例

alt text

SteamのワークショップページでMOD情報を記入

新規にアップロードしたMODは非公開の状態でSteamに保存されます。

アップロードしたMODは以下のように探します。

  1. Steamクライアントのコミュニティからワークショップを押す
  2. あなたのファイルを押す

alt text

MODの情報を記入して、公開状態を非公開から公開に変更します。

うまく公開できているか不安な場合はシークレットブラウザなどでワークショップページを確認します。
ページが参照できてサブスクライブが表示されていればMODが公開できています。

言語別の表示を確認したい場合は画面右上の言語を切り替えます。

MODの公開状態

さいごに

基礎の基礎はおさえられたと思います。

たくさん面白いMODを作ってみてください!

困りごと

MODを作ってみて困ったことです。

解決方法を知っていればコメントなどで教えてください。

TinyPastureのSteam Workshop Uploaderが起動できない

解決しました。

詳細はTiny PastureのSteam workshop uploaderがクラッシュする問題の解決にまとめました。

register_global_classes_from_arrayが機能しない

Godot Mod Loaderにはクラスをグローバルに使用するための機能があります。
ユーティリティクラスなどは毎回定義するのが手間なのでグローバルに使えるようにできると便利です。

公式のドキュメントを見るとregister_global_classes_from_arrayで実現できそうですが、以下のコードはエラーで起動できませんでした。
なお、GitHubのIssueでこの機能が利用できないとの記述があります。

mods-unpacked/rin_jugatla-separate_digits/mod_main.gd
func _init() -> void :
    install_global_script()

func install_global_script() -> void :
    var classes = [
      { 
         "base": "Object", 
         "class": "DigitsUtility", 
         "language": "GDScript", 
         "path": "res://mods-unpacked/rin_jugatla-separate_digits/digits_utility.gd" 
      }
    ]
    
    ModLoaderMod.register_global_classes_from_array(classes)
mods-unpacked/rin_jugatla-separate_digits/digits_utility.gd
class_name DigitsUtility
extends Object

## 桁区切りされていない数値を桁区切り
static func format_number_text_with_commas(number_text: String) -> String:
    # ゲームアップデートで桁区切りに対応した場合に二重で桁区切りしないようチェック
    var has_comma = number_text.find(",") > -1
    if has_comma:
        return number_text

    var formated = str(number_text)
    var length = formated.length()
    while length > 3:
        formated = formated.insert(length - 3, ",")
        length -= 3
    return formated

Godotの便利な機能

定義に飛ぶ

Ctrlを押したときにアンダーバーが出た項目について、マウス左クリックで定義に飛べます。
Godot4.4からはF12で定義に飛べます。

定義に飛ぶ

開いているファイルのツリー位置に飛ぶ

開いているファイルがファイルツリーのどこにあるのかわからなくなったときは、右クリックメニューからファイルシステム上で表示を選択します。

開いているファイルのツリー位置に飛ぶ

参考資料

関連ツール

GitHubで編集を提案

Discussion