Open18

CS2で大会運営を念頭に置いたマッチ管理プラグインを開発する

FlowingSPDGFlowingSPDG

https://zenn.dev/hiiraginil/scraps/8b6cc5f105ecd7 の続き。
get5のような、web側のシステムと連携して動く試合サーバー管理プラグインを開発したい。

https://zenn.dev/hiiraginil/scraps/9db0056444ae08 こんなスクラップ書いておいてプラグインを開発するのか、という話もあるが...やっぱり正規表現で汚くパースするより、JSONをPOSTするいい感じのAPIの方が開発体験はよっぽど良いし、試合サーバーの安定性も担保しやすい、と思う。

FlowingSPDGFlowingSPDG

まず、コンセプトと方向性に関して考える。

既にコミュニティに存在するマッチ管理プラグインとして、MatchZy、get5、eBotがある。

  • MatchZy
    • CSSharpをベースに開発された新規マッチ管理プラグイン。
    • かなり新しく、不足している機能(チームロック、API連携など)がある
    • get5やeBotを参考に開発されているため、改善箇所が多い。
    • MySQLにデータを書き込めるっぽい?
    • 自分がまだ詳しく触れていないため、わからない部分も多い。
  • pugsetup
    • PUG(紅白やカスタムのような、10人集めてマッチする試合)の管理に焦点を当てて開発されたマッチ管理プラグイン。
    • SourceMod上で動作する。
    • 簡単なマッチには便利だが、試合サーバー外と連携する機能がないため大会運営などには向かない点も多い。
  • get5
    • SourceModをベースに長年開発されているマッチ管理プラグイン。
    • 元々pugsetupと同じ開発者が開発しており、共通点も多い。
      • ちなみに今は別の型がメンテナンスしている。
    • CS2のアップデートに伴いSourceModが使用不能にあり、事実上開発停止となった。
      • 実際には「CS2のプラグイン管理機構が整理されるまで一時停止(suspend)」という扱い
    • 外部のAPIサーバーにHTTPリクエスト/POSTを送信したり、MySQLのDBに書き込み・読み込みが行える。
    • APIの仕様が固まっており、本プラグインと連携するシステムが多く存在する
      • get5-web(開発停止、かつPython2系なので実質使用が難しい)
      • get5-web-go(開発停止、get5-webとの互換を念頭に開発してい)
      • G5API(開発中?)
      • get5loader(開発中、大本命のつもり)
    • コミュニティが大きく、知見や採用例も多い。
  • eBot

事実上、CS2での選択肢はMatchZy, eBotを拡張するか、新規に開発することに限られる。
MatchZy自体かなりイケているものの、大会運営という観点で見ると外部連携機能が甘いことや、コンセプトが異なるように見える(具体的には、pugsetupに近い?)

eBot自体はイケている仕組みではないが、長年の実績があり実際に採用しているイベントも多そう。
ただし、マッチ結果の登録は出来ても管理者不在のまま試合を実施することに難がありそう。

自分の勉強も兼ねて、これまでのマッチプラグインとは違うコンセプトで新規に開発してみようと思う。

FlowingSPDGFlowingSPDG

コンセプトとして、管理者が不在か、最小限のオペレーションのまま自動的に試合を進行したり、技術的な知識が乏しいプレイヤーの制御だけで試合を開始出来るシステムを提供したい。

これまでの利用実績が大きいget5の仕組みから、参考にすべき機能を挙げてみる。

  • 外部連携
    • 試合状況が更新された際に、外部のAPIにリクエストを送信する
      • 必然に近い。
      • CS2デフォルトのログではなく、認証システムを組み込んだり、JSON形式で送信することが求められる。
    • 外部からマッチを読み込む機能
      • 指定したURLからマッチを読み込むコマンドがある。
      • RCON経由でこれを呼び出して、マッチの設定を試合サーバーに入れ込む。
      • コマンドの実行結果は、RCON経由でJSONが返ってくる。
    • MySQLにデータを書き込む機能
      • 正直これはいらないと思う。試合サーバーから直接MySQLに繋ぎたいと思えないため。
  • 試合管理
    • 試合に登録されていないプレイヤーを弾き出す機能
    • マッチが読み込まれていない際に、プレイヤーが誰も接続できないようにする機能
    • マッチが読み込まれてない際に、現在サーバー上にいるプレイヤーだけで試合を開始出来る機能
      • これはpug/紅白マッチの際にかなり役立つのでマストに近い。
    • ウォームアップ/練習機能
      • これは正直デフォルトでいい気がする。
      • グレネード練習をマッチ管理プラグインに組み込んだとして、煩雑な上にうまく動作する未来が見えない。
    • ポーズ
      • これは必須に近い。
      • tacポーズ(30秒x4回=合計2分間、cvarで調整可能)とtechポーズ(無期限停止)の2種類がある。
      • 公式サーバーの場合Escメニューから投票するが、殆どのコミュニティサーバーではチャットコマンドから実施する。
      • UXを優先するのであれば、公式投票を使用したいところ。
    • バックアップ
      • CS2公式のバックアップ/復元システムがあるので、それに乗っかっている。
      • get5の実装は結構複雑だった。
      • あると嬉しいが、使用頻度は高くないためすぐに必要なモノではない。
    • BANPICK/VETO
      • プレイするマップのBAN/PICK。
      • CS2ではPremierでマップロード前にBANPICKが実施される。
      • get5ではチャットコマンドや、メニュー表示で実施されていた。
      • Premierの仕組みに乗っかることが出来るのであればそれが一番良いが、実現可能かはわからない。
      • 最悪チャットコマンドでも全然良いし、なんなら1map固定にしてもいい。
      • 競技マップが変わった際や、大会ルールで7map以外のBANやBO1/BO3/BO5対応が結構面倒な気がする。
    • BO3/BO5対応
      • 少なくてもPUGや国内大会では滅多に発生しないとはいえ、1マッチの中に複数マップが存在したりするパターンは想定される。
      • その場合、マップを跨いだステートフルな試合管理が求められる。

いや、やること多いな...。

まずは試合サーバー内のゲームフェーズを段階的に示して、それに必要な機能を当てはめてタイムライン化していく。

FlowingSPDGFlowingSPDG

以下、get5+get5-web/get5loader の場合。

  1. ユーザーによって試合サーバーがAPIシステムに登録される。
  2. 外部からゲームサーバーに対して、マッチの読み込み命令が発行される。または、プレイヤー操作によってゲームサーバーからマッチが読み込まれる。
  3. ゲームサーバーがマッチを読み込み、設定が適用される。この時点で、ユーザーはサーバーに入ることが出来る。
  4. プレイヤーが規定人数(通常10名)に達し、コーチ・観戦を含む全員がRDYする。
  5. 対応したMAPのBANPICKが開始される。
  6. 選択されたMAPに切り替える。
  7. 全員が接続したのち、再度RDYチェックを行う。
  8. 全員がRDYしたらウォームアップを終了し、試合を開始する。
  9. 試合中にポーズやバックアップ読み出しが行われる場合、逐次対応する。
  10. マップの勝敗が決定する。
  11. demoのアップロードを実施する。
  12. BO3など複数マッププレイする必要がある場合、次のマップに切り替え、7に戻る。
  13. マッチの勝敗が決定したら、demのアップロードを待機したのち、試合結果を登録しサーバーを空き状態へ戻す。
  14. 全プレイヤーをサーバーからキックする。

書いてて思ったけど、wingmanにも対応させたい気持ちはあるな。

こう見ると、API側のロジックの方が複雑で、試合サーバー側のロジックは薄くシンプルに保った方がメンテナンス性が高いシステムが制作出来そうな気がする。

FlowingSPDGFlowingSPDG

以上を踏まえて、CS2+CSSharpの上で駆動するget5にインスパイアされたマッチ管理システムを、仮称"Project Theseus"として開発を開始する。
キャッチーな名前が欲しいので、この名称は仮のものとする。
private設定でrepositoryを作成した。
C#には慣れていないが、いわゆる「神クラス」を避けれるように、Webhook, GamePhaseManager など、機能ごとにクラスを分けてそれぞれ参照するように実装する。

https://github.com/FlowingSPDG/ProjectTheseus

FlowingSPDGFlowingSPDG

3時間程度で、プレイヤーのRDY状況を管理するクラスと、Webhookのような動作を実現するクラスが実装出来た。

あとは、ゲームフェーズを管理するクラスを実装したり、実際にマップを切り替えたり試合の進捗を握るAPIが必要となってきそう。

FlowingSPDGFlowingSPDG

https://github.com/shobhit-pathak/MatchZy

MatchZyのREADMEをちゃんと読み込んでて初めて気づいたけど、webhookとか外部からマッチを読み込む機能も一応開発予定らしい。良い。
get5に近いプラグインにはなりそうなので、ここにきて再度自信で車輪の再発明を行う必要性を疑い始めている。

むしろget5のシンプルな移植プロジェクトと、次世代のマッチ管理プラグインを分けて開発したほうが良いような気はしないでもない...

FlowingSPDGFlowingSPDG

ちなみに、MatchZyのWebhookに機能に関してリクエスト兼質問を投げてみた。
https://github.com/shobhit-pathak/MatchZy/issues/40

簡単にまとめると、「イベントに応じて外部にデータを投げるWebhookのような機能は作りたい」かつ、「外部の指定したURLからマッチを読み込む機能(get5_loadmatch_url のようなコマンド) と連携するwebpanelに設計に着手出来ると思う」とのことだった。
道のりは長そうだが、幸い開発が活発なので自分もコントリビュートしつつ開発を見守ろうと思う。

もちろん、自作のマッチ管理システムは並行して進める。良い部分があれば取り込めるかもしれない。

FlowingSPDGFlowingSPDG

CSGOでちょっと使っていたNative Votesプラグインの移植に取り組んでいる。

https://github.com/powerlord/sourcemod-nativevotes

かなり低レイヤーな機能であるEventの制御・人口的な発火と、各クライアントへのUserMessage送信などの処理が必要で結構複雑。
やりきればCSのサーバーシステムが大体理解出来そうまである。

あと、CounterStrikeSharpがUserMessage送信に対応しているのかちょっとわからない感じがする。

FlowingSPDGFlowingSPDG

まだ詳しい順番や正確なシーケンスは把握していないが、

  • vote_controller というentityのフィールドを変更
  • vote_options イベントの発火
  • VoteStart UserMessageの送信

を行う必要がありそう。結構複雑なのと、もしかするとC#がUserMessage周りにまだ対応していないかも。

https://github.com/roflmuffin/CounterStrikeSharp/issues/145

FlowingSPDGFlowingSPDG

そんな中、Apfelwurm氏からDiscordで連絡があった。
https://github.com/Apfelwurm
参考までに、get5の開発用Dockerイメージのメンテナンスを行っていた人である。
https://github.com/Apfelwurm/csgo-get5-docker-dev

曰く、「既にAPI機能があり、かつget5との互換を目指している」というPugSharpなるプラグインを教えてもらった。
https://github.com/Lan2Play/PugSharp

これから実装を追ってみようと思う。というか、CSコミュニティは本当にアクションが早い。

FlowingSPDGFlowingSPDG

さて、ここらで状況整理。
現在のCS2のサーバープラグイン周りについて。
大本がMetaModというのは変わらずらしい。
その上で、MetaModの上に乗るプラグイン管理プラグイン、の存在が複数存在している可能性がありそう。

ツリーで表すと、

うおー、開発合戦になってきた。

FlowingSPDGFlowingSPDG

player_death イベントにフックして、ヘッドショットの場合だけキャンセルするようにしたいがうまくいかない。

[GameEventHandler(Mode = HookMode.Pre)]
    public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
    {
        Logger.LogInformation("Event {0} fired, Handle.", @event.EventName);
        if (@event.Headshot) {
            return HookResult.Handled;
        }
        return HookResult.Continue;
    }

うーん、HookResultの扱いがまだちょっと甘いのか、イベントによっては制御できないのだろうか?

FlowingSPDGFlowingSPDG
 [GameEventHandler(Mode = HookMode.Pre)]
    public HookResult OnPlayerDeath(EventPlayerDeath @event, GameEventInfo info)
    {
        Logger.LogInformation("Event {0} fired, Handle.", @event.EventName);
        var victim = @event.Userid;
        var killer = @event.Attacker;
        // swap!
        @event.Userid = killer;
        @event.Attacker = victim;
        return HookResult.Continue;
    }

このコードでBOTを倒してみたところ、キルログだけが入れ替わっており、実際のゲーム状況は書き変わらなかった。
イベントフック、複雑だ。

FlowingSPDGFlowingSPDG

というか、処理も1フレーム挟んだりしないと行けなかったり謎の癖がある。

RegisterListener<Listeners.OnEntitySpawned>(entity =>
        {
            // スモークグレネード以外の場合return
            if (entity.DesignerName != "smokegrenade_projectile") return;

            // 取得したentityのHandleからスモークグレネードのエンティティを取得
            var projectile = new CSmokeGrenadeProjectile(entity.Handle);

            // ここでNextFrameを挟まないと、処理が反映されない 理由は不明
            Server.NextFrame(() =>
            {
                projectile.SmokeColor.X = Random.Shared.NextSingle() * 255.0f;
                projectile.SmokeColor.Y = Random.Shared.NextSingle() * 255.0f;
                projectile.SmokeColor.Z = Random.Shared.NextSingle() * 255.0f;
                Logger.LogInformation("Smoke grenade spawned with color {SmokeColor}", projectile.SmokeColor);
            });
    });

NextFrameを使わず、直接projectile.SmokeColor を弄っても反応しなかった。