Water Crown

第6部:C#によるプログラミング

FreeTrainの機能をさらに深く拡張したい場合、XMLの記述だけではなくC#によるコーディングが必要になります。本章では、プラグインから独自の機能や画面を実装するための基礎知識を解説します。

開発環境について FreeTrain EXは .NET Framework 2.0 上で動作しています。 プラグインを開発する際は、必ずコンパイラのターゲットバージョンを .NET 2.0 (v2.0.50727) に設定してください。新しいバージョンの.NETでビルドしたDLLは読み込めません。

ソースコードの入手: 本体やライブラリのソースコードは、現在 Software Heritage archive 等のアーカイブサイトから取得可能です。内部APIの調査にはソースコードの参照が欠かせません。

目次

    6.1 コントリビューションの自作

    XMLで指定したクラスを自作することで、既存の枠組みにとらわれない新しい要素をゲームに追加できます。

    Contributionクラスの実装

    すべての拡張要素は freetrain.framework.plugin.Contribution クラスを継承します。

    using freetrain.framework.plugin;
    
    public class MyNewPlugin : Contribution {
        // XMLから動的にロードされる場合、このシグネチャのコンストラクタが必須です
        public MyNewPlugin(XmlElement e) : base(e) {
            // XMLからの読み込み処理
        }
    }
    

    ファクトリの登録

    新しい type を定義したい場合は、system/plugin.xml に倣って ContributionFactory を実装・登録する必要がありますが、通常は既存の typemenuspecialStructure など)を拡張するのが一般的です。

    6.2 コントローラー(操作画面)の作成

    プレイヤーがマップをクリックして何かを配置したり、情報を表示したりする画面を作るには、AbstractControllerImpl を継承します。

    基本構造

    public class MyController : AbstractControllerImpl {
        // マップがクリックされた時の処理
        public override void onClick(MapViewWindow source, Location loc, Point ab) {
            // loc: クリックされたボクセルの座標
            // ここに建設処理などを記述
        }
    
        // マウス移動時のプレビュー処理など
        public override void onMouseMove(MapViewWindow view, Location loc, Point ab) {
            // ...
        }
    }
    

    メインウィンドウへのアタッチ

    コントローラーを表示し、マップ操作を有効にするには、MainWindow.mainWindow.attachController(this) を呼び出します。これにより、マップ上のマウスイベントがそのコントローラーに送られるようになります。

    6.3 世界(World)へのアクセス

    ゲーム世界の情報は freetrain.world.World.world シングルトンオブジェクトを通じて操作します。

    ボクセルの操作

    • 取得: Voxel v = World.world[x, y, z];
    • 設置: World.world[x, y, z] = new MyVoxel(...);
    • 範囲操作: Cube クラスを使うことで、矩形範囲のボクセルをまとめて扱うことができます。

    地形と地価

    • 地表の高さ: int z = World.world.getGroundLevel(x, y);
    • 地価の取得: int value = World.world.landValue[x, y];

    6.4 グラフィックスプログラミング

    独自の描画を行いたい場合は、freetrain.DirectXWrapper を使用します。

    6.5 注意事項

    6.6 既存プラグインの実装事例分析

    既存のプラグインがどのように機能を実装しているかを見ることは、新しいプラグインを開発する上で非常に参考になります。ここでは5つの事例を分析します。

    事例1:ブルドーザー

    plugins\org.kohsuke.freetrain.land.bulldoze

    このプラグインは、指定された範囲内の地形や構造物を撤去して更地化を行う機能を持ちます。

    • 継承クラス: LandBuilderContribution
      • 土地造成系プラグインの基底クラスです。矩形範囲選択のUIなどが提供されます。
    • 実装のポイント: 段階的な削除処理
      • create メソッド内で、選択された矩形範囲(x1, y1 から x2, y2)を二重ループで走査します。
      • 各座標の Voxel を取得し、その種類に応じて remove() メソッドや World.world.remove() を呼び出します。
      • 山(MountainVoxel)の処理: 一度目はremoveTrees() を呼び出して木を取り除き、既に平坦化済みの場合は World.world.remove(x,y,z) で山自体を削除するという段階的な処理を行っています。
      • コスト計算: 削除処理ごとに AccountGenre.SUBSIDIARIES.spend(...) を呼び出し、撤去費用を計上しています。

    事例2:柵

    plugins\jp.co.tripod.chiname.lib.fence

    ボクセルの境界線上に柵を設置するプラグインです。

    • 継承クラス: LandBuilderContribution (インターフェースとして Fence も実装)
    • 実装のポイント: DummyVoxelと独自コントローラー
      • DummyVoxel: 柵は通常、ボクセル(土地や建物)の「属性」として付与されます。しかし、何もない空中や地面の上に柵だけを設置したい場合、属性を付与する対象がありません。そこで、透明で当たり判定以外の実体を持たない DummyVoxel を生成して配置し、そのボクセルに対して setFence メソッドで柵情報を付与する設計になっています。
      • 独自コントローラー: 通常の土地造成は矩形範囲を使いますが、柵は「線」で引く操作が適しています。そのため、createBuilder メソッドをオーバーライドし、BorderLineSelectorController を継承した独自のコントローラー (LogicL) を返すことで、ドラッグによる線描画操作を実現しています。

    事例3:柵の撤去

    plugins\jp.co.tripod.chiname.lib.fence

    • 継承クラス: FenceBuilder
    • 実装のポイント: 継承による差分実装
      • 建設用クラス FenceBuilder を継承し、setFence メソッドのみをオーバーライドしています。
      • 建設時は World.world[loc].setFence(d, this) として柵オブジェクトを登録していた部分を、撤去時は setFence(d, null) とすることで削除を行っています。
      • このように、建設クラスを継承して「逆操作」だけを上書きすることで、コードの重複を抑えています。

    事例4:鉄橋

    core\world\rail\BridgeRailContributionImpl.cs

    列車が中をくぐることができる鉄橋の実装です。

    • 継承クラス: SpecialRailContribution
      • 通常の線路 (RailRoadContribution) とは異なり、描画や建設処理をC#で細かく制御するためのクラスです。
    • 実装のポイント: 描画順序の制御と自動生成
      • 描画順序 (drawBefore/drawAfter): トラス橋は、列車の「手前」と「奥」に鉄骨が存在します。
        • drawBefore: 列車の描画「前」に呼び出され、奥側の鉄骨を描画します。
        • (ここで列車が描画される)
        • drawAfter: 列車の描画「後」に呼び出され、手前側の鉄骨を描画します。これにより、列車が鉄骨の間を走行しているように見せています。
      • 橋脚の自動生成: build メソッド内で、線路の高さから地面の高さ (getGroundLevel) を検索し、その差分だけ BridgePierVoxel (橋脚) を積み上げる処理が実装されています。

    事例5:サッカースタジアム

    plugins\org.kohsuke.freetrain.soccerStadium

    単なる建物ではなく、試合が行われ、チームの強さや人気が変動するシミュレーション要素を持つ建物です。

    • 継承クラス: PThreeDimStructure
      • 複数のボクセルを占有する巨大構造物の基底クラスです。
    • 実装のポイント: 時間イベントと内部状態
      • ClockHandler: コンストラクタで World.world.clock.registerRepeated を呼び出し、定期実行されるメソッド (onClock) を登録しています。これにより、定期的にチームの人気や強さを変動させたり、試合スケジュール(futureGames)を管理したりしています。
      • 内部状態管理: _strength(チームの強さ)や _popularity(人気)といった独自のフィールドを持ちます。これらの値はプロパティダイアログで確認できるほか、試合の勝敗判定や収入計算に利用されます。
      • remove時の後始末: remove メソッドで ClockHandler の登録解除 (unregister) を行い、建物が削除された後にイベントが走り続けないようにしています。

    6.7 チュートリアル:動的な乗降需要の実装(ショッピングモール)

    本節では、「テナントを入れることで魅力度が変わり、駅の利用客数が増減するショッピングモール」を実際に作成する手順を解説します。

    概要

    作成するプラグインの主な機能は以下の通りです。

    1. テナント管理: 設定画面からテナント(ファストフード、映画館など)を追加・削除できる。
    2. 動的な需要: テナントの構成によって「魅力度」が変化し、乗降客数が変動する。
    3. 収支計算: 売上とコストを計算し、利益(または赤字)を計上する。

    Step 1: 開発環境の準備

    pluginsフォルダ内に以下のディレクトリ構造を作成します。

    plugins\freetrain.structure.shopping_mall\
      ├── bin\                  (DLL出力先)
      ├── src\                  (ソースコード)
      ├── plugin.xml            (定義ファイル)
      ├── build.bat             (ビルドスクリプト)
      └── stadium.bmp           (画像ファイル:今回はサッカースタジアムを流用)
    
    plugin.xml

    建設メニュー登録用の specialStructure と、建物実体用の anonymous を分けて登録します。

    <plug-in>
        <title>ショッピングモール</title>
        <!-- メニュー登録用 -->
        <contribution type="specialStructure" id="{...MALL}">
            <class name="freetrain.world.shoppingmall.MallContributionImpl" codebase="bin/FreeTrain.ShoppingMall.dll" />
        </contribution>
        
        <!-- 建物実体用 -->
        <contribution type="anonymous" id="{...MALL_S}">
            <class name="freetrain.world.shoppingmall.StructureContributionImpl" codebase="bin/FreeTrain.ShoppingMall.dll" />
            <name>ショッピングモール</name>
            <price>500000000</price>
            <size>10,6</size>
            <height>2</height>
            <sprite origin="0,0" offset="78"><picture src="stadium.bmp"/></sprite>
            <!-- 独自の人口計算クラスを指定 -->
            <population>
                <class name="freetrain.world.shoppingmall.MallPopulation" codebase="bin/FreeTrain.ShoppingMall.dll"/>
            </population>
        </contribution>
    </plug-in>
    

    Step 2: データ構造の定義

    テナント情報を管理するクラスです。カテゴリや売上情報も持ちます。

    [Serializable]
    public class Tenant {
        public string Name;
        public TenantCategory Category; // enum定義が必要
        public double AttractivenessBonus;
        public long BaseSales;
        public long BaseCost;
        
        // コンストラクタ等は省略
    }
    

    Step 3: 動的な人口計算

    Population クラスを継承し、魅力度に応じた乗降客数を計算します。

    重要: プラグイン読み込み時にシステムがこのクラスをインスタンス化するため、XmlElement を引数に取るコンストラクタが必須です。

    [Serializable]
    public class MallPopulation : Population {
        private MallStructure owner;
        
        public MallPopulation(MallStructure owner) { this.owner = owner; }
        
        // システム読み込み用コンストラクタ(必須!)
        public MallPopulation(XmlElement e) {}
    
        public override int calcPopulation(Time t) {
            if (owner == null) return 0;
            // 時間帯ごとの基本人数 × 建物の魅力度
            int baseP = (t.hour >= 10 && t.hour <= 20) ? 50 : 0;
            return (int)(baseP * owner.Attractiveness);
        }
    }
    

    Step 4: 構造体の実装

    PThreeDimStructure を継承します。

    [Serializable]
    public class MallStructure : PThreeDimStructure {
        private MallPopulation _population;
        private SpecialStationListenerImpl _listener; // 駅との接続役
        public ArrayList Tenants = new ArrayList();
        public double Attractiveness = 1.0;
    
        public MallStructure(StructureContributionImpl type, WorldLocator wloc) : base(type, wloc) {
            _population = new MallPopulation(this);
            // 駅からの集客を受け付けるリスナーを作成
            _listener = new SpecialStationListenerImpl(_population, wloc.location);
            
            // 定期イベント
            World.world.clock.registerRepeated(new ClockHandler(OnClock), TimeLength.fromDays(30));
        }
    
        // 駅からの問い合わせに対し、リスナーを返す(最重要)
        public override object queryInterface(Type aspect) {
            if (aspect == typeof(SpecialStationListener))
                return _listener;
            return base.queryInterface(aspect);
        }
    
        // クリック時に設定画面を開く
        public override bool onClick() {
            using(TenantConfigForm form = new TenantConfigForm(this)) {
                form.ShowDialog(MainWindow.mainWindow);
            }
            return true;
        }
        
        public void OnClock() {
            // テナントの売上計算や競合ペナルティ計算を行い、収支を計上する
            long profit = UpdateStatus();
            AccountManager.theInstance.spend(-profit, AccountGenre.SUBSIDIARIES);
        }
    }
    

    Step 5: UIの実装

    Windows Forms を使用して、テナントの設定画面を作成します。System.Windows.Forms.Form を継承したクラス (TenantConfigForm) を作成し、リストボックスやボタンを配置して MallStructureTenants リストを操作するようにします。

    Step 6: コントリビューションとビルド

    src/MallContributionImpl.cs

    メニューから選択された時に、建物を配置するコントローラーを起動します。

    public override void showDialog() {
        // MallController は FixedSizeStructController を継承したクラス
        MallController.create();
    }
    
    build.bat

    FreeTrainは .NET Framework 2.0 上で動作しているため、コンパイラもバージョンを合わせる必要があります。

    @echo off
    set CSC="C:\Windows\Microsoft.NET\Framework\v2.0.50727\csc.exe"
    set OUT="..\bin\FreeTrain.ShoppingMall.dll"
    set SRC="src\*.cs"
    set REFS="/r:System.dll"
    set REFS=%REFS% "/r:System.Windows.Forms.dll"
    set REFS=%REFS% "/r:System.Drawing.dll"
    set REFS=%REFS% "/r:..\..\..\FreeTrain.Core.dll"
    set REFS=%REFS% "/r:..\..\..\FreeTrain.Controls.dll"
    
    %CSC% /target:library /out:%OUT% %REFS% %SRC%
    

    ※パスは環境に合わせて調整してください。

    以上の手順により、シミュレーション要素を持つ独自の建物を実装できます。

    6.8 コーディングエージェントを活用した開発

    大規模言語モデル(LLM)やコーディングエージェント(AI)を活用してFreeTrainのプラグインを開発する手法(いわゆるVibe Coding)は、APIドキュメントが少ないFreeTrain開発において非常に強力な武器となります。ここでは、AIと協働してスムーズに開発を進めるための手順とノウハウをまとめます。

    1. 準備:AIにコンテキストを与える

    AIはFreeTrainのAPIを知りません。開発を始める前に、AIがコードベースを理解できる環境を整える必要があります。

    • Coreソースコードの提示: FreeTrain本体のソースコード(core フォルダや trunk フォルダ)のパスをAIに教えます。AIは grep やファイル検索機能を使って、必要なAPI(クラス名、プロパティ名、メソッドシグネチャ)を自力で調査できるようになります。
      • : 「FreeTrainのソースコードは C:\Projects\FreeTrain\trunk にあります。APIが不明な場合はここを検索してください。」
    • Gitの利用: AIは大胆なコード修正を行うことがあります。いつでも元に戻せるよう、Gitでリポジトリ管理を行い、作業前にブランチを作成することを指示しましょう。
    • AGENTS.md の作成: プロジェクトのルートディレクトリに AGENTS.md というファイルを作成し、プロジェクト固有の制約(.NET 2.0、WinForms限定、特定の命名規則など)を記載しておくと、エージェントが最初からその規約を理解した状態で開発を始めてくれます。

    注意: 以下の記述例はあくまで一例です。実際のフォルダ構成やプロジェクト独自のルールに合わせて、内容は適宜書き換えて使用してください。

    AGENTS.md の記述例
    # FreeTrain プラグイン開発エージェントへの指示
    
    このプロジェクトは非常に古い技術スタック(2000年代中盤)で構成されています。
    エージェントは以下の制約を厳守してください。
    
    ## 開発環境と制約
    - **Gitの利用**: 大胆なコード修正を行う際、いつでも元に戻せるようGitでリポジトリ管理を行ってください。大きな変更の前にはブランチを作成することを推奨します。
    - **ランタイム**: .NET Framework 2.0
    - **言語**: C# 2.0
        - `var`(型推論)、LINQ、ラムダ式(`=>`)は使用不可。
        - 匿名メソッド(`delegate`)を使用すること。
    - **UIフレームワーク**: Windows Forms (System.Windows.Forms) 限定。
        - WPFやWinUIなどのモダンなフレームワークは動作しません。
    
    ## 実装の作法
    - **リソースアクセス**: 画像や独自の設定ファイルを読み込む際は、直接パスを指定せず、`freetrain.framework.plugin.Plugin` クラスが提供するパス解決メソッドを介すること。
    - **シリアル化**: セーブデータに保存されるクラスには `[Serializable]` 属性が必須。
    - **調査**: 
        - APIが不明な場合は、必ず `trunk/core` 以下のソースコードを確認すること。
        - 実装パターンに迷った場合は、`plugins` フォルダ内の既存プラグイン(C#ソースが含まれるもの)を参考にすること。
    
    ## 禁止事項
    - 本体ソース(`core/` や `trunk/`)への書き込みは厳禁です。調査にのみ使用してください。
    - 修正が必要な場合は、必ずプラグイン側のソースコードを編集してください。
    

    2. 開発フロー

    AIとの対話による開発は、以下のサイクルで行います。

    1. 指示 (Prompt): 「何を」「どこに」「どうしたいか」を伝えます。
      • 良い例: 「時計塔プラグイン ClockTower.cs を作成し、毎正時にステータスバーにメッセージを表示するようにしてください。core フォルダを検索して、定期実行イベントの適用方法を調べてください。」
    2. 実装 (Implementation): AIがコードを書き換え、ビルドコマンドを実行します。
    3. エラー解決 (Fix): FreeTrain開発ではビルドエラーが頻発します。AIは最新のC#文法を使いがちですが、FreeTrainは古い環境のためエラーになります。エラーログをそのままAIに貼り付けて修正を促します。
    4. 動作確認 (Verify): ゲームを起動して動作を確認し、結果(「メッセージが表示されない」「ログに例外が出ている」など)をAIにフィードバックします。

    3. よくある落とし穴と回避策 (Tips)

    AI開発をスムーズに進めるための重要なポイントです。

    • APIの幻覚 (Hallucination):
      • AIは「あるはずだ」と思って存在しないプロパティを使うことがあります。
      • 対策: 「コンパイルエラーになりました。ソースコードを検索して正しいアクセス方法を探してください」と指示すると、AIは実際のソースから代替手段を見つけ出してくれます。
    • C#のバージョン制限:
      • FreeTrainは .NET Framework 2.0 (C# 2.0) です。最新の文法は使えません。
      • 対策: プロンプトの最初に「開発環境は .NET 2.0 / C# 2.0 です。LINQやラムダ式、varなどは使えません」と明記しておくと手戻りが減ります。
    • コンテキストの維持:
      • 長い会話になるとAIは前の指示を忘れることがあります。
      • 対策: 重要なファイル(plugin.xml や修正中の .cs ファイル)のパスを定期的に提示したり、直近の修正意図を再認識させたりすることで精度を維持できます。