開発者・内部の処理を深く知りたい方向け
Apex Custom Manager
コードで見る仕組み
「どう使う?」の、その先へ。画面の裏側で行われる判断や計算を、実際のコードと具体例から読み解きます。
このページは、内部のコードに興味がある方のための追加資料です。通常の利用にコードの知識は必要ありません。操作方法を探している方は、一般ユーザー向けの基本ガイドはこちら。
このページの目次を開く
- このページの読み方
- 100ポイントを3人で分けたら?
- 試合の出来事を、どう振り分ける?
- 「最新の状態」をどう持っている?
- どの大会に試合が記録される?
- 倒した賞金首が復活したら?
- 予約時刻に少し遅れたら?
- OBSの数字は、どう更新される?
- 起動・画面・保存先の全体像
- Apexとの接続・新着待ち・情報の鮮度
- 別ウィンドウ・定期更新・重複取得の抑止
- ロビー作成・参加者取得・参加コード
- チーム割当・個別キック・終了補助
- モード・マップ・ルール・選択肢のカタログ
- レジェンド禁止の取得・選択・適用
- 即時チャット・予約チャット・事前設定
- 全ポイント要素・倍率・上限・丸め方
- 大会管理・順位・同点・再計算・テンプレート
- アイテム分類・武器名・使用ポイントの候補
- 履歴・個人統計・所持品・移動と動画
- CSV・JSON出力と、試合・大会の移行
- マップ画像・座標変換・キャリブレーション
- 試合ライフサイクル・中断復旧・保存失敗
- 設定ファイル・通常ログ・イベントログ
- Discord投稿・送信先・オートスタート
- 自動カメラ・手動切替後の休止・確認待ち
- AI接続・モデル・プロンプト・分析ジョブ
- AIとの会話・大会下書き・作成確定
- 外部カスタム・VC・タイムラインの反映
- 音声ファイル・テンプレート・予約・終了トリガー
- 公開結果・現在ルールでの再計算・再送
- 外部接続設定と、手動・自動マッチレポート
- ライセンス・秘密情報・外部接続の境界
- 更新・インストーラー・再起動
- 開発時の構成・ビルド・テスト
1 このページの読み方
ソースコードを全部読まなくても、ひとつの関数から仕組みを知ることができます。まずは「何をしたい処理か」を読み、気になる行だけ追ってみてください。
実コードの抜粋
アプリ内の処理を、そのまま短く切り出したものです。出典のファイル名と行番号を付けています。型や前後の処理を省いているため、通常は単独では動きません。
説明用サンプル
動きを試しやすくするために用意した学習用の例です。実アプリ全体を再現するものではありません。実コードとの違いを明記します。
if は条件による分岐、for は繰り返し、return は結果を返して処理を終える行です。Goの nil は「値がない」ことを表します。
操作の準備は基本ガイド、設定の意味や使い分けはもっと深く知るガイドをご覧ください。
イベントが届いた後は、役割ごとに分かれる
変換したイベントを、用途に応じて処理
読みやすく簡略化した流れです。実際にはログ記録や機能ごとの処理もあります。「メモリの最新値」と「保存された試合履歴」は同じものではありません。OBSなどの表示は、用途に合うデータをアプリから取得します。
気になる問いから読む
機能から探す
準備・試合運営・集計・記録・配信・AI・保守をひとつの索引にまとめました。「解説」は設定の意味、「仕組み・コード」は内部の判断や処理へ進みます。通常の画面操作に加え、接続設定が必要な機能と、現在の動作上の制限も扱います。
起動・接続・画面
- 動作環境・起動・運営の流れ解説:動作環境・準備するもの / 起動・終了 / 試合を運営する流れ
仕組み・コード:起動・画面・保存先の全体像 / 開発時の構成・ビルド・テスト - Apexとの接続・情報の鮮度解説:Apexとの接続設定(LiveAPI)
仕組み・コード:Apexとの接続・新着待ち・情報の鮮度 - 画面の状態・大会の記録先解説:画面の見方と大会の状態
仕組み・コード:「最新の状態」をどう持っている? / どの大会に試合が記録される? - 別ウィンドウ・更新の重複防止解説:別ウィンドウで状況を確認する
仕組み・コード:別ウィンドウ・定期更新・重複取得の抑止
試合を運営する
- ロビー・参加コード解説:ロビーを作成する
仕組み・コード:ロビー作成・参加者取得・参加コード - チーム割当・キック・終了補助解説:参加者の管理・試合終了の補助
仕組み・コード:チーム割当・個別キック・終了補助 - モード・マップ・ルール解説:マップ・マッチのルールを設定する
仕組み・コード:モード・マップ・ルール・選択肢のカタログ - レジェンド禁止解説:レジェンドの使用を制限する
仕組み・コード:レジェンド禁止の取得・選択・適用 - 即時・予約チャット解説:ロビーへチャットを送る
仕組み・コード:即時チャット・予約チャット・事前設定
大会・ポイント・判定
- 大会管理・順位・同点・テンプレート解説:大会を作成・管理する
仕組み・コード:大会管理・順位・同点・再計算・テンプレート / どの大会に試合が記録される? - 全配点・倍率・上限・順位点の分配解説:ポイントの設定と計算ルール
仕組み・コード:全ポイント要素・倍率・上限・丸め方 / 100ポイントを3人で分けたら? - 武器・アイテム分類と加点解説:ポイントの設定と計算ルール
仕組み・コード:アイテム分類・武器名・使用ポイントの候補 - 賞金首・リスポーン後の判定解説:賞金首を設定する
仕組み・コード:倒した賞金首が復活したら?
保存・履歴・地図
- 試合イベント・個人統計・所持品解説:マッチ履歴・移動履歴を確認する
仕組み・コード:試合の出来事を、どう振り分ける? / 履歴・個人統計・所持品・移動と動画 - 移動履歴・地図補正・動画保存解説:マッチ履歴・移動履歴を確認する / 地図の表示・位置を調整する
仕組み・コード:履歴・個人統計・所持品・移動と動画 / マップ画像・座標変換・キャリブレーション - CSV・完全JSON・大会の移行解説:マッチ・大会のデータを移す
仕組み・コード:CSV・JSON出力と、試合・大会の移行
予約・カスタム・音声
- 設定の事前適用・開始予約解説:試合の開始・チャットを予約する
仕組み・コード:予約時刻に少し遅れたら? / 即時チャット・予約チャット・事前設定 - 外部カスタム・VC・タイムライン解説:カスタム情報・音声を連携する
仕組み・コード:外部カスタム・VC・タイムラインの反映 - 音声・テンプレート・終了トリガー解説:音声の設定とテスト
仕組み・コード:音声ファイル・テンプレート・予約・終了トリガー
配信・外部送信
- Discord・用途別の送信先解説:Discordへコード・結果を送る
仕組み・コード:Discord投稿・送信先・オートスタート - OBSの4種類の表示・更新元解説:OBSに試合情報を表示する
仕組み・コード:OBSの数字は、どう更新される? - 自動カメラ・観戦先の切替解説:自動で観戦対象を切り替える
仕組み・コード:自動カメラ・手動切替後の休止・確認待ち - 公開結果・再計算・再送解説:大会の結果を公開ページへ送る
仕組み・コード:公開結果・現在ルールでの再計算・再送 - 手動・自動マッチレポート解説:マッチレポートを送信する
仕組み・コード:外部接続設定と、手動・自動マッチレポート
AIを使う
- AI設定・分析入力・ジョブ管理解説:AIの設定・マッチ分析
仕組み・コード:AI接続・モデル・プロンプト・分析ジョブ - AIとの大会作成・下書き・確認解説:AIと相談して大会を作る
仕組み・コード:AIとの会話・大会下書き・作成確定
保存・保守・開発
- 中断復旧・バックアップ・ログ解説:バックアップ・中断時の確認
仕組み・コード:試合ライフサイクル・中断復旧・保存失敗 / 設定ファイル・通常ログ・イベントログ - 外部接続・秘密情報解説:外部サービスの接続設定
仕組み・コード:ライセンス・秘密情報・外部接続の境界 / 外部接続設定と、手動・自動マッチレポート - ライセンス・利用できる条件解説:ライセンスの認証・有効期限
仕組み・コード:ライセンス・秘密情報・外部接続の境界 - 更新・安全な終了・ビルド解説:アプリを更新する
仕組み・コード:更新・インストーラー・再起動 / 開発時の構成・ビルド・テスト - 問題の切り分け・検証の範囲解説:困ったとき・よくある質問
仕組み・コード:Apexとの接続・新着待ち・情報の鮮度 / 試合ライフサイクル・中断復旧・保存失敗 / 開発時の構成・ビルド・テスト
掲載対象は確認した2.0.4の機能です。内部コードは理解に役立つ短い部分を選び、接続情報や個別の試合データは掲載しません。連携先サービスの管理機能すべてや、ゲーム側の仕様全体を説明するものではありません。
2.0.2の期限表示:通常画面は、アプリが画面へ渡したローカル保護トークンを付けてヘルス情報を取得します。認証済みのローカル画面には有効期限を含む詳細状態を返し、操作トークンのないヘルス確認には期限や端末情報を含めません。期限が空のときだけ無期限として扱い、2040年以降を含む日付は通常の日付として保持・表示します。関連箇所は internal/httpapi/security.go 40–64行、internal/httpapi/server.go 165–203行です。
2 100ポイントを3人で分けたら?
端数があっても、全員分を足すと元のチーム点になるように分配します。
出典:internal/points/logic.go 608–628行
func splitPlacementPoints(points int64, memberCount int) []int64 {
if memberCount <= 0 {
return nil
}
count := int64(memberCount)
base, remainder := points/count, points%count
// Go truncates division toward zero. Convert it to floor division so a
// negative team value keeps the exact configured sum, matching Python.
if remainder < 0 {
base--
remainder += count
}
result := make([]int64, memberCount)
for index := range result {
result[index] = base
if int64(index) < remainder {
result[index]++
}
}
return result
}
ここを読むと、動きがわかる
points/countで基本点、points%countで余りを求めます。- 余りが負なら補正します。負の設定点でも合計が変わらないようにするためです。
- 全員に基本点を配り、余りの数だけ先頭から1ポイントずつ追加します。
| チーム点 | 人数 | 分配結果 | 合計 |
|---|---|---|---|
| 100 | 3 | [34 33 33] | 100 |
| −5 | 3 | [-1 -2 -2] | −5 |
| 0 | 3 | [0 0 0] | 0 |
nil を返します。自分のPCで試せる、全体のサンプルを開く
上の関数を変更せず、結果を表示する main と標準ライブラリの読み込みを付けた説明用サンプルです。アプリ固有のパッケージは使いません。Apexやアプリの起動、通信、試合データの読み書きも不要です。
package main
import "fmt"
func splitPlacementPoints(points int64, memberCount int) []int64 {
if memberCount <= 0 {
return nil
}
count := int64(memberCount)
base, remainder := points/count, points%count
// Go truncates division toward zero. Convert it to floor division so a
// negative team value keeps the exact configured sum, matching Python.
if remainder < 0 {
base--
remainder += count
}
result := make([]int64, memberCount)
for index := range result {
result[index] = base
if int64(index) < remainder {
result[index]++
}
}
return result
}
func main() {
examples := []struct {
points int64
members int
}{
{100, 3},
{-5, 3},
{0, 3},
}
for _, example := range examples {
shares := splitPlacementPoints(example.points, example.members)
var total int64
for _, share := range shares {
total += share
}
fmt.Printf("%d / %d -> %v (sum=%d)\n",
example.points, example.members, shares, total)
}
}
Goが使えるPCで、新しい作業フォルダの main.go に上のコード全体を保存し、そのフォルダで次を実行します。アプリのフォルダに置く必要はありません。
go run main.go
出力例:
100 / 3 -> [34 33 33] (sum=100)
-5 / 3 -> [-1 -2 -2] (sum=-5)
0 / 3 -> [0 0 0] (sum=0)
試すなら:{100, 3} を {101, 3} や {100, 4} に変え、分配先の合計と元の点が一致するかを見てみましょう。例の数値は説明用で、実際の大会設定や参加者のデータではありません。
計算全体も読む:全ポイント要素・倍率・上限・端数 / 大会順位と保存済みポイント / アイテム分類
3 試合の出来事を、どう振り分ける?
同じ入口に届くイベントでも、開始・終了と、試合中の更新では処理を分けています。
出典:internal/matchstate/session.go 122–139行
func (s *Session) Handle(ctx context.Context, event liveapi.Event) error {
if event.Type == "rtech.liveapi.MatchSetup" {
return s.onSetup(ctx, event.Data)
}
if event.Type == "rtech.liveapi.MatchStateEnd" {
return s.onEnd(ctx, event.Data)
}
s.mu.Lock()
defer s.mu.Unlock()
if s.current == nil || (s.state != Active && s.state != Ending) {
return nil
}
timestamp, _ := intValue(event.Data["timestamp"])
s.extractPositions(s.current, event.Data, timestamp)
s.applyEvent(s.current, event.Type, event.Data, timestamp)
return s.checkpointLocked(false)
}
MatchSetupなら開始処理へ、MatchStateEndなら終了処理へ渡します。- それ以外はロックを取り、対象の試合があるか、状態が
ActiveまたはEndingかを確認します。 - 位置と出来事を現在の試合に反映し、
checkpointLocked(false)へ進みます。
前提:Session、liveapi.Event、状態の定義、補助関数などが必要です。ロックは更新が途中で入り交じるのを防ぐためのものです。この抜粋だけでは実行できません。
記録の先へ:終了待機・保存失敗・中断復旧 / 個人統計・位置・所持品 / イベントログ
4 「最新の状態」をどう持っている?
受信するたびに種類ごとの最新値を置き換え、更新を待っている処理へ知らせます。
出典:internal/store/store.go 27–44行
func (s *Store) Update(resultType string, value any) uint64 {
s.mu.Lock()
defer s.mu.Unlock()
s.counter++
now := time.Now()
current := s.data[resultType]
if current == nil {
current = &entry{wait: make(chan struct{})}
s.data[resultType] = current
}
close(current.wait)
current.wait = make(chan struct{})
current.value = value
current.counter = s.counter
current.updatedAt = now
s.updatedAt = now
return s.counter
}
- ロックを取り、全体の更新番号
s.counterを1つ増やします。 - その種類のデータが初めてなら、保存枠と通知用のチャネルを作ります。
close(current.wait)で待機側に通知し、次回用のチャネルを用意します。- 最新値、更新番号、更新時刻を入れて、今回の更新番号を返します。
| 項目 | ここで保持するもの | 読み違えやすい点 |
|---|---|---|
current.value | その種類の最新値 | 過去の値をすべて追加し続けるリストではありません。 |
current.counter | その値が更新された番号 | その種類だけの累計件数ではなく、全体の更新番号です。 |
current.wait | 次の更新を知らせるチャネル | そのままOBSへ送る通信ではなく、アプリ内部の待機処理への通知です。 |
前提:Store と entry の定義、初期化、更新を待つ側の処理は省略しています。単独実行用ではありません。
取得する側の仕組み:新しい情報を待つ・鮮度を調べる / 別ウィンドウの更新を重ねない
5 どの大会に試合が記録される?
大会・選手別の武器統計は、取得時に確認済みの武器別名を正規化してから合算します。内部名と日本語名の重複行をまとめ、使用者数は正規化後の武器ごとにプレイヤー識別値のDISTINCTで計算します。集計済みの使用者数を足す方式ではありません。元の保存データは変更せず、未確認の別名は統合しません。
試合の開始処理では、画面で眺めている大会ではなく「アクティブな大会」を参照します。
出典:internal/matchstate/session.go 196–217行
err := s.db.WithTx(ctx, func(tx *sql.Tx) error {
var tournamentID sql.NullInt64
err := tx.QueryRowContext(ctx, "SELECT id FROM tournaments WHERE is_active=1 ORDER BY id LIMIT 1").Scan(&tournamentID)
if err != nil && !errors.Is(err, sql.ErrNoRows) {
return err
}
if tournamentID.Valid {
snapshot.TournamentID = tournamentID.Int64
}
result, err := tx.ExecContext(ctx, `INSERT INTO matches
(match_uuid, server_id, map, playlist_name, playlist_desc, datacenter,
aim_assist_on, anonymous_mode, start_timestamp, tournament_id)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
snapshot.MatchUUID, snapshot.ServerID, snapshot.Map, snapshot.PlaylistName,
snapshot.PlaylistDesc, snapshot.Datacenter, snapshot.AimAssistOn,
snapshot.AnonymousMode, snapshot.StartTimestamp, nullableTournamentID(tournamentID))
if err != nil {
return err
}
snapshot.MatchID, err = result.LastInsertId()
return err
})
is_active=1の大会を探し、見つかればsnapshot.TournamentIDに入れます。- その大会IDを含めて、
matchesに新しい試合の記録を追加します。 WithTxの中をまとめて処理します。途中でエラーを返せば取り消し、成功すれば確定する仕組みです。
大会がないと、エラーになる?
大会が見つからない sql.ErrNoRows は、ここでは許容しています。その場合は大会未所属の試合として保存します。一方、データベースの別のエラーはそのまま返します。
SQLの「?」には何が入る?
VALUES (?, ...) の位置には、後ろに渡した試合情報が順番に入ります。掲載しているのは変数名で、実際の試合ID、参加者情報や接続先の値ではありません。
出典は開始処理 onSetup の途中です。データベース、テーブル、snapshot、nullableTournamentID などの周辺処理が必要で、単独では実行できません。
大会機能の全体:大会管理・同点・再計算・テンプレート / AIとの大会作成 / 大会の移行
6 倒した賞金首が復活したら?
最後のリスポーンより後に成立する、最後の有効な撃破を探します。
出典:internal/httpapi/tournaments.go 731–750行
lastRespawnTime, lastRespawnID := int64(-1), int64(-1)
if len(respawns) > 0 {
last := respawns[len(respawns)-1]
lastRespawnTime, _ = mapInt64(last, "timestamp")
lastRespawnID, _ = mapInt64(last, "id")
}
var valid map[string]any
for _, kill := range kills {
killer := textOrEmpty(kill["killer_hash"])
if killer == "" || killer == target {
continue
}
killTime, _ := mapInt64(kill, "timestamp")
killID, _ := mapInt64(kill, "id")
if killTime < lastRespawnTime || (killTime == lastRespawnTime && killID <= lastRespawnID) {
continue
}
valid = kill
}
return valid, nil
- 最後のリスポーンの時刻と記録IDを取り出します。
- 撃破者が不明な記録や、自分自身を撃破した記録を除外します。
- 最後のリスポーン以前の撃破を除外します。同じ時刻なら記録IDで前後を比較します。
- 残った撃破を順に
validへ入れるので、最後の有効なものが残ります。なければnilです。
| 出来事の順序 | この判定の結果 |
|---|---|
| Aが撃破 → 対象がリスポーン | 有効な撃破なし |
| Aが撃破 → 対象がリスポーン → Bが撃破 | Bの撃破 |
| 撃破とリスポーンの時刻が同じ | 時刻だけでなく記録IDで前後を区別 |
timestamp, id の昇順で取得します。並び順があるからこそ、最後に代入した記録が最後の有効撃破になります。撃破者の決定も前半で行います。大会側では、終了時刻と勝者の記録がある大会所属試合を対象にします。この抜粋は賞金首の表示マーカー用の判定で、賞金の支払いや追加ポイントを自動処理するコードではありません。
出典は validBountyKill の後半です。対象者 target、並べた履歴、型変換などの補助関数が必要です。この部分だけをコピーしても動きません。
候補の作り方・大会からの継承
大会の賞金首候補は、勝者が確定した所属試合の参加者と、現在のロビーで競技チームに割り当てられた参加者を合わせます。ロビー情報を取得できれば初戦前にも指定できます。未割当と観戦者は現在ロビーの候補から除きます。
大会所属の試合は大会の賞金首設定を引き継ぎ、無所属の単独試合は別に指定できます。機能をOFFにしても対象指定は残りますが、表示やAI分析への入力から外れます。撃破者は付与先の記録を優先し、なければ攻撃者から解決してから、上記の有効撃破を判定します。
設定・候補・判定:internal/httpapi/tournaments.go 428–751行
試合側の継承:internal/httpapi/matchdata_more.go 1238–1355行
7 予約時刻に少し遅れたら?
時刻だけでなく、同じ予約の同じ操作を処理済みかどうかも確認します。
出典:internal/httpapi/background.go 213–225行
func (s *Server) scheduleDue(ctx context.Context, scheduleID int64, actionType, actionRef string, target, now time.Time) bool {
delta := now.Sub(target)
if delta < 0 || delta > scheduleGrace {
return false
}
var marker int
err := s.database.QueryRow(ctx, `SELECT 1 FROM schedule_action_log WHERE schedule_id=? AND action_type=? AND action_ref=? AND scheduled_for=?`, scheduleID, actionType, actionRef, target.Format("2006-01-02 15:04:05")).Scan(&marker)
return errors.Is(err, sql.ErrNoRows)
}
func (s *Server) markScheduleComplete(ctx context.Context, scheduleID int64, actionType, actionRef string, target time.Time) {
_, _ = s.database.Exec(ctx, `INSERT OR IGNORE INTO schedule_action_log(schedule_id,action_type,action_ref,scheduled_for) VALUES(?,?,?,?)`, scheduleID, actionType, actionRef, target.Format("2006-01-02 15:04:05"))
}
now.Sub(target)で予定時刻からの経過時間を求めます。- まだ予定時刻前、または許容時間を過ぎていれば対象外にします。
- 同じ予約・操作・予定時刻の履歴を照会し、「該当する行がない」ときだけ実行候補にします。
- 別の処理で操作を送信し、成功扱いになった後に
markScheduleCompleteで履歴を残します。
scheduleGrace は、同じファイルの16行目で 120 * time.Second と定義されています。
| 予定からの経過時間 | この関数の判断 |
|---|---|
| 予定時刻より前 | 対象外 |
| 0秒〜120秒 | 処理済み履歴がなければ候補 |
| 120秒を超える | 対象外 |
| 履歴の照会自体でエラー | 「未処理」と決めつけず、候補にしない |
前提:Server、データベース、時刻やエラーを扱う型、実際に操作する呼び出し側などが必要です。単独では実行できません。公開ページから予約を登録・実行する機能はありません。
実行される順序と、重複を防げる範囲
予定の処理は、設定の事前適用、該当するチャット、開始時の設定再適用、マッチメイキング開始へ進みます。ひとつの予定でも操作ごとに時刻と処理済み記録を確認するため、一部成功・一部失敗があり得ます。
時刻は実行PCのタイムゾーンで解釈します。同じ予定ID・操作・予定時刻の再処理は抑えますが、別々の予約として登録した同一内容までまとめて抑止する仕組みではありません。予約チャットと音声の時刻予約でも、実行記録や猶予は異なります。
実行側:internal/httpapi/background.go 156–245行
8 OBSの数字は、どう更新される?
表示用データをアプリから取得し、画面の種類に合った描画処理へ渡します。
出典:webassets/static/js/overlay_stats.js 86–108行
async function loadData() {
const endpoint = overlayType === 'latest' ? '/api/overlay/latest-data' : '/api/overlay/live-data';
try {
const response = await fetch(endpoint, { cache: 'no-store' });
const data = await response.json();
if (!data.success) {
renderEmpty(data.error || 'データ取得エラー');
scheduleNext(data.settings);
return;
}
if (overlayType === 'latest') {
renderLatest(data);
} else if (overlayType === 'observer') {
renderObserver(data);
} else {
renderLive(data);
}
scheduleNext(data.settings);
} catch (error) {
renderEmpty(`接続エラー: ${error.message}`);
scheduleNext();
}
}
- 直近の試合を表示する場合と、それ以外で取得先を切り替えます。
fetchで取得した内容をJSONとして読み、data.successを確認します。- 直近の試合・観戦者視点・試合中の状況に合わせて描画関数を選びます。
- 成功時もエラー時も、
scheduleNextで次の取得を予約します。
/api/overlay/... はアプリが提供する画面で使う相対パスです。このマニュアルサイトへ貼り付けても表示用データは取得できません。アプリ側の接続準備や周辺処理は省略しています。何秒ごとに取り直す?
続く scheduleNext(同ファイル110–116行)では、設定された更新間隔を使い、最短2秒に調整しています。値が未指定などの場合の既定値は5秒です。ここで示しているのはこの取得処理の流れで、観戦者表示に関わる更新経路をすべて列挙したものではありません。
前提:overlayType、各描画関数、タイマー処理などが別に定義されています。これはOBSを操作したり映像を取得したりするコードではなく、ブラウザソース用の表示データを扱う部分です。単独実行用ではありません。
コードと設定を、行き来して読む
条件式の意味がわかったら、実際に触る設定や利用上の注意も確認してみてください。使い方は基本ガイド、結果が変わる理由や設定の関係はもっと深く知るガイドにまとめています。
このページは選んだコードを読むための資料です。内部の関数名や相対パスを、外部連携向けに固定・保証された仕様として案内しているものではありません。
4種類の表示は、同じ記録を使うわけではない
| 表示 | 取得する記録 | 更新 |
|---|---|---|
| イベント通知 | 受信したキル・リスポーン・全滅など | WebSocketの通知 |
| 進行中の成績 | 現在の試合の状態と集計 | 設定間隔の取り直し |
| 最新マッチ | 終了時刻のある最新の保存済み試合 | 設定間隔の取り直し。勝者あり条件は使わない。 |
| 観戦者の情報 | 指定観戦者の最新視点と対象選手 | 定期取得と観戦先変更の通知 |
表示件数は1〜30、更新間隔は2〜60秒(既定5秒)です。最新マッチの武器・アイテム使用率は使用人数を観戦者以外の参加者数で割り、武器ダメージ率は全武器ダメージに占める割合です。使用回数の割合とは異なります。
観戦者表示はロビーで取得した対象のハッシュを選択して保存します。保存済みの対象も再利用できます。未指定・未保存なら設定を案内し、指定した対象の視点がない場合は待機します。別の観戦者の最新視点へはフォールバックしません。所持品は全アイテムの汎用一覧ではなく、回復、投げ物、バックパック、ノックダウンシールドなどを決まった配置で描きます。OBS側で観戦者を選ぶことと、自動カメラを有効にすることは別です。
表示データと通知:internal/httpapi/overlay.go 425–607行
観戦更新・所持品・テスト:webassets/static/js/overlay_stats.js 137–157行、279–386行、487–633行
2.0.3の操作画面は、目的別カテゴリをTAB_GROUPSに定義し、既存の画面IDへ対応付けます。lastTabByGroupでカテゴリごとの直前の画面を記憶し、利用できない機能へ移動しようとした場合はホームへ戻します。高度な設定だけをサイドバー下部へ分け、ホームの4ステップは画面案内に限定します。大会選択を起動時に強制するダイアログは開かず、記録先の状態を確認します。機能情報は直接のオブジェクトとfeaturesに包まれた形式の両方に対応し、使用できない連携は表示しません。
9 起動・画面・保存先の全体像
ひとつのアプリの中で、ローカル画面、Apexとの通信、試合の保存、任意の連携が役割を分担します。
起動時に設定と保存先を決め、データベースと各機能を初期化します。管理画面とLiveAPIの待ち受けは同じPCのループバックに限定され、ブラウザの画面がローカルのHTTP処理を呼び出します。公開しているこのガイドが、管理画面のサーバーを兼ねているわけではありません。
HTML・JavaScript・CSS・既定設定はアプリに埋め込まれています。標準のマップ画像は外部ファイルです。画面ファイルを配布用EXEの横で編集すれば、そのまま埋め込み画面が変わるという構成ではありません。
実行時のデータはインストール先と分けて保存します。通常は %APPDATA% 配下の ApexCustomManager に設定、試合DB、ログ、復旧用スナップショットを持ちます。メモリ上の最新値、DBの試合履歴、機能別設定ファイルは別の役割です。
画面は通常Edgeのアプリウィンドウで開き、利用できない場合は既定ブラウザへ切り替えます。起動時の認証確認を待たずに画面を準備しますが、有効なライセンスなしで全機能を使えるという意味ではありません。
通常のEdgeの独立ウィンドウを閉じると、アプリの終了処理へ進みます。既定ブラウザへ切り替わった場合はタブを閉じてもアプリが残ることがあるため、保存完了とアプリ本体の終了を別に確認します。通常の終了は独立ウィンドウの終了、OSの終了シグナル、更新処理からの停止要求で管理されます。
関連する処理:
起動と機能の組み立て:cmd/apexcustom/main.go 111–214行
実行時設定と保存先:internal/appconfig/config.go 29–110行
管理画面の起動:internal/desktop/open_windows.go 32–63行
埋め込みと外部画像:webassets/embed.go 5–12行
設定・使い分け:起動・終了 / 画面の見方と大会の状態 / バックアップ・中断時の確認
機能の索引へ戻る ↑
10 Apexとの接続・新着待ち・情報の鮮度
「つながっている」「新しいデータを受け取った」「ゲームで操作が完了した」は別の状態です。
LiveAPIから届くバイナリはスキーマに沿ってイベントへ変換され、最新値の保管と試合記録へ渡されます。初回の接続案内は現在のLiveAPIポートから起動オプションを作り、確認済みの状態を設定に残します。
参加者一覧や設定を読み込むときは、送信前の更新番号を覚え、それより新しい対象データが届くのを待ちます。参加者は5秒、設定や禁止状態は2秒の待機を使い、間に合わなければ空の結果になる場合があります。
送信はロックで直列化して同時書き込みを避けますが、保存して後から再送するキューではありません。送信処理の成功はWebSocketへの書き込み成功で、ゲーム内の反映完了や試合開始・終了を確認した結果ではありません。
接続数と受信データの新しさは別に評価します。ヘルス表示では30秒を基準に鮮度を見ており、つながっていても表示が古い場合は、受信・ゲーム側の状態も確認します。
実コードを見る:Apexとの接続・新着待ち・情報の鮮度
出典:internal/httpapi/legacy.go 145–159行
送信後、指定種類の更新番号が進むまで待つ部分です。タイムアウトとデータなしを処理し、取得した値を返します。parent・timeout・before・resultTypeなどは前半で用意するため、この抜粋だけでは動きません。
waitContext, cancelWait := context.WithTimeout(parent, timeout)
updated := s.store.WaitForUpdate(waitContext, resultType, before)
cancelWait()
if !updated {
return map[string]any{}, false
}
value, ok := s.store.Get(resultType)
if !ok {
return map[string]any{}, false
}
result, ok := value.(map[string]any)
if !ok {
return map[string]any{}, false
}
return result, false
関連する処理:
受信と送信の直列化:internal/liveapi/server.go 153–195行
新着の取得待ち:internal/httpapi/legacy.go 121–160行
接続と鮮度の判定:internal/httpapi/server.go 165–211行
初回ガイド状態:internal/httpapi/setup.go 21–42行
設定・使い分け:Apexとの接続設定(LiveAPI) / 画面の見方と大会の状態
機能の索引へ戻る ↑
11 別ウィンドウ・定期更新・重複取得の抑止
表示対象と更新周期を窓ごとに持ち、同じ取得を重ねないようにしています。
プレイヤー・マッチ・ポイント・総合順位は、表示専用の別ウィンドウとして開けます。別モニターへの配置はできますが、ブラウザ側でポップアップが禁止されていると開けません。
更新の目安は、プレイヤー約2秒、マッチ約1秒、アクティブ大会のポイント約3秒、指定大会の総合順位約5秒です。取得が終わる前に次の周期が来た場合は、その回を見送ります。通信が遅いと表示間隔も伸びます。
ポイント窓は現在アクティブな大会、総合順位窓は開くときに指定した大会を対象にします。進行中1試合のリアルタイムポイント表示は別で、同じ数字を別デザインにしているだけではありません。
既に開いている総合順位窓はフォーカスを戻す経路があるため、対象大会を変えるときは窓を閉じて開き直します。表示対象を指定する時点と、自動更新する時点の違いが理由です。
実コードを見る:別ウィンドウ・定期更新・重複取得の抑止
出典:webassets/static/js/dashboard.js 141–149行
keyが実行中の集合にあれば次の取得を見送り、成功・失敗のどちらでもfinallyで集合から外します。refreshFlightsというSetを外側で定義します。待機キューを作ったり自動再送したりする関数ではありません。
async function runRefreshSingleFlight(key, refreshFunction) {
if (refreshFlights.has(key)) return;
refreshFlights.add(key);
try {
await refreshFunction();
} finally {
refreshFlights.delete(key);
}
}
関連する処理:
同じ取得の重複防止:webassets/static/js/dashboard.js 141–149行
別ウィンドウを開く:webassets/static/js/dashboard.js 2157–2219行
ポイント更新と大会指定:webassets/static/js/dashboard.js 12526–12567行
設定・使い分け:別ウィンドウで状況を確認する / ポイントの設定と計算ルール / 大会を作成・管理する
機能の索引へ戻る ↑
12 ロビー作成・参加者取得・参加コード
作成要求、参加者の再取得、参加コードの共有はそれぞれ別の操作です。
管理画面の「ロビーを作成」は、ローカルのHTTP処理を通じてApexのロビー作成操作へ変換されます。画面は返った応答のエラーを確認し、成功扱いのときは約1秒後に参加者情報を取り直します。
参加者の取得では新しい応答を待ち、一覧表示、保持しているロビー情報、チーム割当の候補を更新します。作成操作の送信成功だけで、参加者の取得や参加コードの確定まで完了したとは限りません。
参加コードを取得した際、Discordのオートスタート用Botと送信先が設定されている場合には、Bot向けコマンドを自動投稿する経路もあります。「カスタムマッチコード」のプレビュー作成でも同じ取得処理を使うため、確認後の通常投稿とは別に、プレビュー取得時点でBot向け投稿が起きる場合があります。
関連する処理:
ロビー作成と再取得:webassets/static/js/dashboard.js 2075–2149行
参加者表示の更新:webassets/static/js/dashboard.js 1334–1363行
参加者・参加コード取得と連携:internal/httpapi/legacy.go 121–178行
コードのプレビューと取得処理:webassets/static/js/dashboard.js 11612–11633行
設定・使い分け:ロビーを作成する / Discordへコード・結果を送る
機能の索引へ戻る ↑
13 チーム割当・個別キック・終了補助
見えているチーム番号と内部のチーム番号を変換し、対象者を識別して操作します。
ロビー情報の内部番号は未割当が0、観戦者が1、競技チームが2以降です。画面では競技チームから1を引いて表示し、割当では表示番号に対応する内部値を送ります。試合結果のモード別チーム統合とは別の変換です。
チーム割当や個別キックは、名前の表示だけでなくハードウェア名やNucleus Hashなどの識別情報を使って対象を指定します。候補を更新し、同名プレイヤーを取り違えないよう確認します。
終了補助で受け付ける方法は「マッチメイキング停止」「ロビー退出」「停止してから退出」の3種類です。停止と退出を順に送っても、進行中の試合が必ず終了するわけではありません。
終了補助のフォームは「高度な設定」→「診断・終了補助」の折りたたみにあります。画面は受け付ける3種類だけを選択肢にし、確認後にmethodを送ります。未対応の一括キック、送信間隔、対象範囲の入力は置いていません。
関連する処理:
ロビーのチーム表示:webassets/static/js/dashboard.js 1374–1396行
対象を指定した割当・キック:internal/httpapi/legacy.go 329–366行
終了補助で受け付ける3方式:internal/httpapi/legacy.go 599–653行
終了補助の画面項目:webassets/templates/index.html 282–304行
設定・使い分け:参加者の管理・試合終了の補助 / 画面の見方と大会の状態
機能の索引へ戻る ↑
14 モード・マップ・ルール・選択肢のカタログ
選択肢への登録、Apexへの適用、履歴の地図表示は別々の仕組みです。
画面で選んだモードとマップは、Apexが受け付けるPlaylist名へ変換して送ります。管理者チャット、チーム名変更、自己割当、エイムアシスト、匿名モードの状態も同時に組み立てます。
受け取り側では許可したフィールドと値の型を確認します。知らない設定名やbooleanが必要な項目への別の型は受け付けないため、画面で送る形式とサーバー側の検証を合わせて読むと理解できます。
モード・マップ追加は、表示名とPlaylist名をカタログへ保存する機能です。同じモード・マップの組があればPlaylist名を更新します。この保存だけでゲーム側へルールは適用されません。
正しいPlaylist名が必要で、名前を追加すれば存在しないゲームモードが動くわけではありません。また履歴で使う画像ファイルや位置合わせは、このカタログではなくマップ設定で扱います。
実コードを見る:モード・マップ・ルール・選択肢のカタログ
出典:webassets/static/js/dashboard.js 1140–1159行
Playlistの選択を確認し、チェックボックスから設定オブジェクトを作る部分です。settingsオブジェクト、画面要素、safeGetValue・safeGetElement・showNotificationなどの定義が必要です。この部分は設定を返すだけで、単独でApexへ適用するものではありません。
const playlist = safeGetValue('mapSelect').trim();
if (!playlist) {
showNotification('モードとマップを選択してください', 'error');
return null;
}
settings.playlistName = playlist;
const adminChatEl = safeGetElement('adminChat');
const teamRenameEl = safeGetElement('teamRename');
const selfAssignEl = safeGetElement('selfAssign');
const aimAssistEl = safeGetElement('aimAssist');
const anonModeEl = safeGetElement('anonMode');
if (adminChatEl) settings.adminChat = adminChatEl.checked;
if (teamRenameEl) settings.teamRename = teamRenameEl.checked;
if (selfAssignEl) settings.selfAssign = selfAssignEl.checked;
if (aimAssistEl) settings.aimAssist = aimAssistEl.checked;
if (anonModeEl) settings.anonMode = anonModeEl.checked;
return settings;
関連する処理:
送信する設定値の組み立て:webassets/static/js/dashboard.js 1137–1160行
許可項目と型の検証:internal/httpapi/legacy.go 368–403行
モード・マップカタログの保存:internal/httpapi/legacy.go 665–738行
設定・使い分け:マップ・マッチのルールを設定する / 地図の表示・位置を調整する
機能の索引へ戻る ↑
15 レジェンド禁止の取得・選択・適用
画面でのチェック変更と、ゲームへの禁止一覧の適用を分けています。
Apexから現在の禁止状態とレジェンドの識別名を取得し、画面の一覧を組み立てます。表示名と送信に使う識別名は同じものとは限りません。
チェック変更や一括選択は、まず画面の状態だけを変えます。「禁止を適用」の送信時に、チェックされた識別名を集めて禁止一覧として渡します。1人ずつ別々の禁止APIを順番に呼ぶ構成ではありません。
解除も一覧の送り直しで行います。チェックを外すだけではゲーム側へ反映されず、取得できずに空になった一覧を禁止なしと誤認しないことが大切です。
実コードを見る:レジェンド禁止の取得・選択・適用
出典:webassets/static/js/dashboard.js 372–383行
選ばれたチェックボックスのvalueを配列へ集めて送信する、フォーム処理の抜粋です。DOM要素やsubmitFormの定義が必要で、このガイド内では実行しません。
// Legend ban form
const setLegendBanForm = document.getElementById("setLegendBanForm");
if (setLegendBanForm) {
setLegendBanForm.addEventListener('submit', function(e) {
e.preventDefault();
const checkedLegends = Array.from(document.querySelectorAll('#legendSelectionGrid input[type="checkbox"]:checked'))
.map(checkbox => checkbox.value);
const formData = { legendRefs: checkedLegends };
submitForm('/set_legend_ban', formData, e.submitter);
});
}
関連する処理:
選んだレジェンド一覧の送信:webassets/static/js/dashboard.js 372–383行
取得と表示、一括選択:webassets/static/js/dashboard.js 1467–1620行
状態取得と禁止一覧の適用:internal/httpapi/legacy.go 485–544行
設定・使い分け:レジェンドの使用を制限する
機能の索引へ戻る ↑
16 即時チャット・予約チャット・事前設定
即時送信と、試合の予定に紐づく送信では入力と実行のタイミングが異なります。
即時チャットは入力した文章を現在のApexロビーへ送ります。Discordへメッセージを投稿する処理ではありません。
予約チャットは試合の開始予定に紐づき、「開始の何分前」または「予定日の指定時刻」から送信時刻を決めます。複数行は空行を除いて別々のメッセージとして保存します。
予約処理では設定の事前適用、チャット送信、開始時の設定再適用、マッチメイキング開始を扱います。チャットだけが成功し、その後の開始に失敗することもあるため、予定全体をひとつの成功と決めつけません。
判定は実行PCの時刻を使い、予定時刻から120秒以内の未処理項目が対象です。アプリ停止や未接続が続いて時間を過ぎた予約が、再開後に必ず実行されるわけではありません。
実コードを見る:即時チャット・予約チャット・事前設定
出典:internal/httpapi/background.go 181–198行
相対分指定から時刻を作り、指定時刻がある場合はそちらを使う部分です。対象時刻と実行履歴を確認し、送信成功後に記録します。予約データ、DB、LiveAPIと周辺関数が必要で、単独実行用ではありません。
for _, chat := range chats {
chatID := fmt.Sprint(chat["id"])
var chatAt time.Time
if absolute := strings.TrimSpace(textOrEmpty(chat["send_time"])); absolute != "" {
chatAt, err = parseScheduleDateTime(textOrEmpty(row["scheduled_date"]), absolute, now.Location())
} else {
chatAt = target.Add(-time.Duration(int64Value(chat["minutes_before"])) * time.Minute)
}
if err != nil {
continue
}
if s.scheduleDue(ctx, scheduleID, "chat", chatID, chatAt, now) && s.sendScheduledLiveAction(ctx, "customMatch_SendChat", map[string]any{"text": textOrEmpty(chat["message"])}) {
s.markScheduleComplete(ctx, scheduleID, "chat", chatID, chatAt)
if !s.waitForBackground(500 * time.Millisecond) {
return context.Canceled
}
}
}
関連する処理:
即時チャット:internal/httpapi/legacy.go 405–417行
複数行と時刻指定:webassets/static/js/dashboard.js 15514–15568行
予約の入力検証と保存:internal/httpapi/matchdata_more.go 1409–1535行
設定反映・チャット・開始の処理:internal/httpapi/background.go 156–245行
設定・使い分け:ロビーへチャットを送る / 試合の開始・チャットを予約する
機能の索引へ戻る ↑
17 全ポイント要素・倍率・上限・丸め方
同じ数字でも、何を数え、どの段階で倍率や上限を適用したかによって意味が変わります。
個人の総合点は基本点、武器ボーナス、アイテム点、分配した順位点、各ペナルティの合計です。戦闘イベントが残っている種類はイベントから数え、古い記録などでイベントがない場合は一部の保存済み統計を使います。
リング倍率はキル・アシスト・ノックダウン・ダメージに適用します。全得点を最後に一律で掛ける倍率ではありません。リング情報のない旧記録のダメージへ、リング0の倍率を勝手に適用することもしません。
自分や味方へのダメージは通常ダメージの加点から除き、対象イベントの回数で減点します。キルの付与先が記録されていれば攻撃者より優先し、自部隊が全滅した時刻より後のキルは通常点や武器キルボーナスから除きます。同時刻は除外しません。
上限は統計のキル数・ダメージ自体を切り捨てるのではなく、対象点を比率で縮めます。個人キルとチームキルの上限は厳しい比率を一度だけ使い、武器ボーナスは別枠です。端数には偶数丸めとチームの予算補正があり、単純に人数分を丸めて足す計算とは異なります。
ダメージ点はリング別に重み付けしたダメージを合算し、必要量で割って倍率を掛けてから、最後に小数を0に近い整数へ切り捨てます。最後の最低総合点の補正により、総合点が内訳合計と一致しない場合があります。全体OFFは点を0にする設定で、試合統計を削除するものではありません。
| 要素 | 初期値・状態 | 対象と注意 |
|---|---|---|
| キル / アシスト / ノック | 100 / 50 / 25 | 有効な対象イベント。リング倍率を適用できる。 |
| ダメージ | 100ダメージで1点、倍率1 | 対象ダメージを合算後に計算。必要量0ではダメージ点が無効。 |
| 蘇生 / リスポーン | 30 / 20 | 味方を蘇生した・リスポーンさせた回数。リング倍率は対象外。 |
| 武器ボーナス | 初期OFF | 武器別にキルとノックを分ける。単一数値の設定はキルのみ。 |
| アイテム | 初期OFF | 対象分類のuseの数量。拾う・捨てる・投擲イベント自体とは区別。 |
| 順位 | 初期OFF | チームに一度配点し、選手へ整数分配。 |
| 死亡 / 被ノック | −50 / −10 | 該当するイベントの判定に基づく。自殺は別項目。 |
| 早期退出 | −100 | 試合終了前・自部隊全滅前などの退出。全滅後や勝敗決着後の例外あり。 |
| 自傷 / 味方ダメージ | −5 / −20 | ダメージ量ではなく、対象イベントの回数で計算。 |
| 自殺 | −75 | 付与先、次に攻撃者から解決した撃破者が空、または被害者自身の場合を判定。 |
| チーム / 個人キル上限 | 5 / 5、初期OFF | 通常キル点へ厳しい方の比率を1回だけ適用。 |
| アシスト / ダメージ上限 | 10 / 3000、初期OFF | 各得点へ独立して適用。上限0を有効にすると対象点は0。 |
| 総合点の下限 | 初期未設定 | 加減点の合算後に個人総合点を補正。 |
実コードを見る:全ポイント要素・倍率・上限・丸め方
出典:internal/points/logic.go 296–315行
重み付きダメージをまとめて点へ変換します。250÷100×2なら5点、10+20+70を合算して100で割れば1点です。引数の型、math・math/big、倍率の補助関数などが必要で、設定入力の検証は別の処理です。
func calculateDamagePoints(stages map[int64]int64, basic BasicSettings, rings RingSettings) int64 {
if basic.DamagePerPoint <= 0 || math.IsNaN(basic.DamagePerPoint) || math.IsInf(basic.DamagePerPoint, 0) ||
math.IsNaN(basic.DamageMultiplier) || math.IsInf(basic.DamageMultiplier, 0) {
return 0
}
// Settings are decimal values. Exact decimal arithmetic keeps an integer
// boundary (such as 10+20+70 damage / 100) independent of map iteration order.
total := new(big.Rat)
for stage, damage := range stages {
weighted := new(big.Rat).Mul(new(big.Rat).SetInt64(damage), decimalRatio(ringMultiplier(rings, stage)))
total.Add(total, weighted)
}
total.Quo(total, decimalRatio(basic.DamagePerPoint))
total.Mul(total, decimalRatio(basic.DamageMultiplier))
integer := new(big.Int).Quo(total.Num(), total.Denom())
if !integer.IsInt64() {
return 0
}
return integer.Int64()
}
関連する処理:
初期設定:internal/points/settings.go 79–94行
加減点と上限・倍率:internal/points/logic.go 118–480行
キル付与先と名前の正規化:internal/points/logic.go 496–554行
設定・使い分け:ポイントの設定と計算ルール
機能の索引へ戻る ↑
18 大会管理・順位・同点・再計算・テンプレート
表示中の大会、試合の所属先、計算に使う設定、保存済みの点を切り分けます。
大会は作成・編集・アクティブ化・解除・完了・削除を扱います。試合を自動で受け取るアクティブ大会は同時に1つで、表示中の大会を切り替える操作とは別です。大会の削除は試合そのものの削除ではなく、所属を解除します。
大会順位は、終了時刻と勝者の記録がある所属試合を、現在の大会ルールで都度計算します。履歴に保存された点や、選択試合の保存済みポイントを合算する表示とは、設定変更後に違う結果になる場合があります。
個人成績はNucleus Hashで通算します。表示名や所属は最新試合に合わせますが、チーム点は各試合で実際に所属していたチームへ残します。途中でチームを移った選手の点がすべて新チームへ移るわけではありません。
大会の同点は、個人では内部識別値の文字列順、チームでは内部チーム番号順に並べ、1からの連番を付けます。最多キルなどの競技規定に基づく決着や同順位表示ではありません。この規則をすべての履歴・集計画面へ一般化しないでください。
履歴の点も更新するときは再計算で保存済みの点を置き換えます。このとき全滅後キルに由来する選手・武器・リングの残存キル集計も補正しますが、raw戦闘イベント自体を削除する処理ではありません。
ポイントテンプレートには作成・一覧・編集・削除があり、作成元は共通の保存済みポイント設定です。大会専用設定を編集中なら、その内容が自動でテンプレートへ入ると考えず、保存元と適用先を確認します。
| 表示・機能 | 使う点数 | 設定変更後の注意 |
|---|---|---|
| 大会順位・公開結果 | 現在の大会設定で対象試合を計算 | 勝者ありの終了済み大会所属試合が対象。 |
| 履歴のポイント | 保存済みの計算結果 | 必要な試合で再計算を実行。 |
| ポイントの複数試集合計 | 保存済みポイントの合算 | 大会順位の再計算と同じ操作ではない。未指定は最新10試合、明示指定は最大100試合。 |
実コードを見る:大会管理・順位・同点・再計算・テンプレート
出典:internal/httpapi/tournaments.go 856–866行
大会の個人順位を総合点の降順で並べ、同点なら内部識別値を比較して順位番号を付ける部分です。事前の集計結果と変換用の補助関数が必要です。表示名順やキル数順ではありません。
sort.SliceStable(list, func(i, j int) bool {
a, _ := mapInt64(list[i], "total_points")
b, _ := mapInt64(list[j], "total_points")
if a != b {
return a > b
}
return fmt.Sprint(list[i]["nucleus_hash"]) < fmt.Sprint(list[j]["nucleus_hash"])
})
for i, player := range list {
player["rank"] = i + 1
}
関連する処理:
大会状態と試合所属:internal/httpapi/tournaments.go 173–408行
順位・選手/チーム/武器/アイテム集計:internal/httpapi/tournaments.go 753–1041行
テンプレート:internal/httpapi/tournaments.go 1214–1327行
保存点の集計・履歴・再計算:internal/httpapi/points.go 469–653行
保存点の置換とキル集計補正:internal/points/database.go 53–179行
設定・使い分け:大会を作成・管理する / ポイントの設定と計算ルール
機能の索引へ戻る ↑
19 アイテム分類・武器名・使用ポイントの候補
ゲーム内で使用を禁止する設定ではなく、記録をどの得点分類として扱うかの設定です。
アイテムや武器の名前から末尾のLevel表記と空白を整理し、設定と記録を照合します。正規化した結果が重複するポイント設定名は、曖昧なまま保存しないよう検証します。
分類は利用者が指定したものを優先し、指定がなければ既定の名前パターンで決めます。候補一覧は所持品・武器ログ・ポイント設定・分類設定から集めます。
初期状態でアイテム点の候補になるのは回復・投げ物・サバイバル・その他です。武器・弾薬・アタッチメント・装備・アビリティ・環境ダメージは候補外ですが、分類の手動指定で扱いを変更できます。
加点には対象のuse数量を使います。投げ物を候補にしたからといって投擲イベントを自動で使用回数へ置き換えたり、拾っただけで使用点を付けたりするわけではありません。
実コードを見る:アイテム分類・武器名・使用ポイントの候補
出典:internal/points/logic.go 245–254行
アイテム点が有効なら使用データを走査し、分類が候補かを確認して数量×配点を加えます。選手別の使用データと設定、itemCandidateなどが必要な部分抜粋です。
if settings.ItemPoints.Enabled {
lookup := normalizedLookup(settings.ItemPoints.Items)
for _, row := range items {
entry := byID[row.MatchPlayerID]
config, ok := lookup[normalizeName(row.Item)]
if entry != nil && ok && itemCandidate(row.Item) {
entry.result.Breakdown.Item += row.TotalUsed * configInt(config, "points", true)
}
}
}
関連する処理:
分類と初期の対象候補:internal/itemfilter/manager.go 18–23行
手動分類・既定分類・候補収集:internal/itemfilter/manager.go 119–176行
使用数量からの加点:internal/points/logic.go 245–254行
名前の正規化と重複検証:internal/points/settings.go 548–591行
設定・使い分け:ポイントの設定と計算ルール / マッチ履歴・移動履歴を確認する
機能の索引へ戻る ↑
20 履歴・個人統計・所持品・移動と動画
3D移動履歴は同梱のmap3d.jsとmap3d-controls.jsを必要時に読み込み、WebGL 2で地図画像を平面に配置します。scenePositionで画像座標を描画座標へ変換し、保存済みpos_zと試合全体の最低記録高度を使って高さを表します。高度不足は基準面に置き、リスポーン・記録欠落をまたぐ線を描きません。2D切替、描画失敗時の2Dへの復帰、後始末を備えます。これは履歴表示であり、LiveAPIによる自由カメラ移動ではありません。
保存した元の記録と、見やすく加工した表示を分けて読むと、空白や数字の違いを理解できます。
履歴では試合概要、個人・チーム成績、戦闘・キル、武器、使用アイテム、リング、アビリティ、位置、観戦者と観戦先の切替、最後に把握した所持品を扱います。選手の識別値から過去の試合を辿り、選択試合を集計する経路もあります。
位置は受信イベントと記録間隔に応じてサンプリングするため、毎フレームの完全な軌跡ではありません。表示する移動履歴は、拾う・使う・アビリティ・攻撃など、最初の活動以降を対象にします。
死亡や全滅で活動区間を切り、蘇生やリスポーン直後に混ざる共有座標を避けて次の座標サンプルから再開します。最初の活動がない選手の座標が空でも、保存処理がすべて失敗したとは限りません。
所持品は最後に把握した状態です。全時点のインベントリを自由に巻き戻せる記録とは異なります。使用アイテムの統計も、所持数や拾った数ではなくuseの情報から読みます。
動画保存はブラウザで描いた地図を録画します。ゲーム映像や音声の録画ではなく、形式はブラウザの対応によりMP4を優先しWebMへ切り替えます。CanvasやMediaRecorderが利用できない場合は保存できません。
実コードを見る:履歴・個人統計・所持品・移動と動画
出典:internal/httpapi/matchdata_more.go 270–284行
ある時刻が表示対象の活動区間に入るかを判定する部分です。movementActiveIntervalの型、開始・終了境界を作る前段処理が必要です。座標の受信や動画録画そのものを行う関数ではありません。
func movementIntervalContains(interval movementActiveInterval, timestamp int64) bool {
if timestamp < interval.StartTimestamp {
return false
}
if timestamp == interval.StartTimestamp && !interval.StartInclusive {
return false
}
if interval.EndTimestamp == nil {
return true
}
if timestamp < *interval.EndTimestamp {
return true
}
return interval.EndInclusive && timestamp == *interval.EndTimestamp
}
関連する処理:
試合と詳細記録の取得:internal/httpapi/matchdata.go 13–98行
活動開始・活動区間と座標表示:internal/httpapi/matchdata_more.go 104–303行
観戦・所持品・過去試合・削除:internal/httpapi/matchdata_more.go 395–453行
地図動画の形式と記録開始:webassets/static/js/dashboard.js 6365–6514行
設定・使い分け:マッチ履歴・移動履歴を確認する
機能の索引へ戻る ↑
21 CSV・JSON出力と、試合・大会の移行
結果を共有する出力と、別環境へ関連データを持っていく完全JSONでは保存範囲が違います。
結果のCSV/JSONは順位などを共有する出力です。試合の完全JSONは選手、戦闘、武器、使用品、位置、リング、アビリティ、観戦、所持品、ポイント、賞金首、AI分析などの関連データをまとめます。大会の完全JSONは大会情報・ルール・賞金首設定も加えます。
単体試合の出力では元の大会所属を取り除きます。大会ごと移すときは大会出力を選びます。ただし完全JSONでも開始予約、予約チャット、アプリの全設定、ライセンス、データフォルダー全体までは含みません。
取り込みはUUIDで同じ試合かを判断し、既存試合を上書きしません。新しい内部IDへ付け直し、関連する選手IDなども結び直します。大会の取り込みでは既存の同じ試合が無所属なら関連付け、別大会所属ならスキップします。
取り込んだ大会を自動でアクティブにはしません。元が完了済みなら完了済み、それ以外は下書きになります。保存はトランザクション単位で、途中の失敗を成功として扱いません。
| 対象 | 検証する範囲 |
|---|---|
| 形式 | apex_custom_matches / apex_custom_tournament、version 1 |
| 試合数 | 1回250試合まで。大会は試合0件でも取り扱える。 |
| 選手数 | 1試合500選手まで。 |
| 関連テーブルの行数 | 1試合内の各子テーブル10万行まで。 |
| 関連データの総量 | 取り込む全試合を合わせた子データ25万行まで。 |
実コードを見る:CSV・JSON出力と、試合・大会の移行
出典:internal/migration/matches.go 305–318行
試合UUIDが既にあるかを調べ、存在すれば新規挿入せずスキップ数を増やす部分です。トランザクション、entries、result、存在確認関数などは周辺処理で用意します。
result := ImportResult{Skipped: []map[string]any{}}
err := db.WithTx(ctx, func(tx *sql.Tx) error {
cache := map[string][]string{}
for _, entry := range entries {
uuid := strings.TrimSpace(entry.Match["match_uuid"].(string))
var existing int64
err := tx.QueryRowContext(ctx, "SELECT id FROM matches WHERE match_uuid=?", uuid).Scan(&existing)
if err == nil {
result.Skipped = append(result.Skipped, map[string]any{"match_uuid": uuid, "map": entry.Match["map"], "reason": "既に存在するマッチです"})
continue
}
if !errors.Is(err, sql.ErrNoRows) {
return err
}
関連する処理:
形式・上限・対象データ:internal/migration/matches.go 19–39行
出力の対象と所属除去:internal/migration/matches.go 91–166行
検証・重複判定・ID再割当:internal/migration/matches.go 168–407行
大会の完全JSONと取り込み:internal/httpapi/tournaments.go 1043–1211行
設定・使い分け:マッチ・大会のデータを移す / マッチ履歴・移動履歴を確認する / 大会を作成・管理する
機能の索引へ戻る ↑
22 マップ画像・座標変換・キャリブレーション
試合のマップ選択ではなく、保存された座標を地図上のどこに描くかを決めます。
内部のマップ名へ表示名と画像ファイルを結び付けます。試合用のPlaylistカタログとは別で、標準画像や独自画像を、履歴などの描画に使用します。
補正ではゲーム座標と画像上の基準点から6係数を求め、画像X=a×ゲームX+b×ゲームY+c、画像Y=d×ゲームX+e×ゲームY+fとして変換します。拡大縮小・回転・平行移動を表現できます。
保存する基準点は3点以上です。複数点から係数を計算し、プレビューしてから保存します。直線上などに偏る点を避け、離れた位置で一致を確認すると、変換が不安定になりにくくなります。
補正がない場合は標準の座標範囲を使い、画像の下方向に合わせてY軸を反転します。サーバーは有限の係数や基準点を検証し、未登録のマッピングや不正な値を拒否します。表示用の変換であり、過去に受信した座標を書き換える機能ではありません。
実コードを見る:マップ画像・座標変換・キャリブレーション
出典:webassets/static/js/dashboard.js 5535–5548行
補正係数があれば式を使い、なければ標準範囲から画像座標へ変換します。画像寸法・境界・補正を受け取る関数で、補助関数の定義も必要です。
function gameToMapCoords(x, y, mapName, imageWidth, imageHeight, affineOverride = null) {
const affine = getMapAffine(mapName, affineOverride);
if (affine) {
return {
x: affine.a * x + affine.b * y + affine.c,
y: affine.d * x + affine.e * y + affine.f
};
}
// Fallback to legacy mapBounds
const bounds = getMapBounds(mapName);
const mapX = ((x - bounds.minX) / (bounds.maxX - bounds.minX)) * imageWidth;
const mapY = ((bounds.maxY - y) / (bounds.maxY - bounds.minY)) * imageHeight; // Y is inverted
return { x: mapX, y: mapY };
}
関連する処理:
画像・マッピング・補正保存の検証:internal/httpapi/matchdata_more.go 653–823行
描画座標への変換:webassets/static/js/dashboard.js 5535–5548行
係数の計算と正規化:webassets/static/js/dashboard.js 5689–5771行
設定・使い分け:地図の表示・位置を調整する / マッチ履歴・移動履歴を確認する
機能の索引へ戻る ↑
23 試合ライフサイクル・中断復旧・保存失敗
終了の受信と保存完了の間に、遅れて届く情報と失敗時の保護を扱います。
試合は開始、取り込み中、終了待機、保存中、保存失敗などの状態で管理します。終了イベントの後は約1秒待ち、遅れて届いた情報も取り込んでから選手・戦闘・位置・所持品・ポイントなどを確定保存します。
通常の受信時は復旧用の途中保存を5秒より短い間隔で繰り返さないようにし、開始・終了の境目では直ちに保存を試みます。受信に応じた間隔制限であり、独立した5秒ごとの全体バックアップではありません。
同じ開始時刻・サーバー・マップの開始通知は重複扱いです。前試合の終了より次試合の開始が先に来ても、前の記録を保存してから新しい試合へ進みます。
正常終了や次回起動時には未終了のスナップショットをInterruptedとして保存します。保存失敗では失敗状態と復旧用記録を残し、破損・過大な復旧ファイルは隔離します。復旧ファイルの上限は64MiBです。
DBの保存では試合UUIDとIDを照合し、関連データをまとめて置き換えます。SQLiteのWAL方式を使うため、起動中のDB本体だけをコピーして完全なバックアップと考えないでください。記録の保存完了と正常終了を確認して、データフォルダー全体を保管します。
| 状態 | 意味 |
|---|---|
| inactive | 処理中の試合がない。 |
| active | 試合の出来事を取り込み中。 |
| ending | 終了後に遅れて届く情報を待っている。 |
| finalizing | 関連データと点数を確定保存中。 |
| finalize_failed | 保存に失敗した。終了済み成功とは扱わない。 |
実コードを見る:試合ライフサイクル・中断復旧・保存失敗
出典:internal/matchstate/session.go 346–360行
残ったスナップショットを読み、未終了の場合だけInterruptedへ変えて保存する部分です。Session、復旧ストア、persist・restoreFailedなどの周辺処理が必要です。停止中のイベントを取り直す処理ではありません。
func (s *Session) Recover(ctx context.Context) error {
snapshot, err := s.recovery.Load()
if err != nil || snapshot == nil {
return err
}
if snapshot.EndTimestamp == 0 {
snapshot.EndTimestamp = time.Now().Unix()
snapshot.EndState = "Interrupted"
}
if err := s.persist(ctx, snapshot); err != nil {
s.restoreFailed(snapshot)
return err
}
return s.recovery.Clear(snapshot.MatchUUID)
}
関連する処理:
開始・終了・保存失敗・中断復旧:internal/matchstate/session.go 141–360行
途中保存の間隔:internal/matchstate/session.go 395–405行
試合データの確定保存:internal/matchstate/session.go 511–598行
復旧ファイルの保存・読み込み:internal/matchstate/recovery.go 28–92行
保存方式とスキーマ初期化:internal/database/database.go 50–89行
設定・使い分け:バックアップ・中断時の確認 / マッチ履歴・移動履歴を確認する / アプリを更新する
機能の索引へ戻る ↑
24 設定ファイル・通常ログ・イベントログ
何を保存するファイルなのかで、保持やバックアップの意味が変わります。
同梱の既定設定は、必要なファイルがないときに用意するものです。既存の設定を起動のたびに初期値へ戻す処理ではありません。設定は機能別ファイルやDBに分かれ、一部は同じフォルダーの一時ファイルへ書いてから置き換えます。
通常ログはアプリ全体の動作やエラーを記録します。試合イベントログはゲームから受けた出来事をJSONLの1行ずつで記録し、試合開始前にも最大200イベントをためておきます。
イベントログには30日超や新しい順200ファイルより後を整理する処理があります。ただし作成直後の件数まで厳密に200以下とする仕組みではありません。これはDBの試合履歴を30日で削除する規則でもありません。
復旧スナップショット、DB、設定、ログは用途が異なります。バックアップは正常終了後にデータフォルダーをまとめて保管し、個人情報や秘密情報を含み得るログや設定をそのまま公開しないでください。
実コードを見る:設定ファイル・通常ログ・イベントログ
出典:internal/rawlog/logger.go 133–146行
イベントをJSONに変換し、改行を加えてバッファをFlushする部分です。書込み先やLoggerの初期化、情報の伏せ字処理は前後にあります。エラーチェックの流れを読むための抜粋で、ログを公開してよいという意味ではありません。
func (l *Logger) writeLocked(entry map[string]any) error {
data, err := json.Marshal(entry)
if err != nil {
return err
}
if _, err := l.writer.Write(append(data, '\n')); err != nil {
return err
}
if err := l.writer.Flush(); err != nil {
return err
}
l.count++
return nil
}
関連する処理:
初期設定の用意と保存先:internal/appconfig/config.go 120–174行
JSON設定の一時ファイル書込み:internal/httpapi/legacy.go 740–771行
イベント受信と開始前バッファ:internal/rawlog/logger.go 51–86行
JSONL書込みと保持整理:internal/rawlog/logger.go 133–191行
設定・使い分け:バックアップ・中断時の確認 / 困ったとき・よくある質問
機能の索引へ戻る ↑
25 Discord投稿・送信先・オートスタート
同じDiscord連携でも、通常投稿、AI分析、外部Bot連携は別の送信経路です。
通常のDiscord送信は参加コード、任意文、大会総合順位、マッチ結果を画面で組み立て、送信先とプレビューを確認してから投稿します。大会は選択中を優先し、なければアクティブ大会を使います。
チャンネル候補はBotから取得できるテキスト・アナウンスチャンネルです。ID・メンション・重複しない名前から送信先を解決し、同名で曖昧な場合は自動で選びません。大会の公開カスタムに設定した通知先が優先される経路もあります。
AI分析の送信は保存された分析を長文分割し、AI分析用チャンネルへ送る別の経路です。通常結果の送信先を変えただけで、すべての自動投稿先が変わるとは限りません。
オートスタート用Botの設定がある場合には、参加コードを取得したときにもBot向けコマンドを自動投稿します。この外部Bot連携は、アプリ内の日時指定による開始予約とは別です。参加コードの表示成功だけでは、外部Botの受信成功も確認できたことにはなりません。
参加コード取得は、通常のコード送信プレビューからも呼ばれます。オートスタートBot向けの設定がある場合は、通常投稿の確認前でも別のBot向け投稿が起こり得ます。コードが表示されたことだけでは、そのBot向け投稿の成功まで確認できません。
実コードを見る:Discord投稿・送信先・オートスタート
出典:internal/httpapi/integrations.go 630–643行
名前で一致する候補を探し、複数あれば曖昧なまま選ばず空結果を返します。前段で読み込んだチャンネル一覧と入力値、補助関数が必要です。
wanted := strings.TrimPrefix(strings.ToLower(value), "#")
found := ""
cache, _ := settings["channels_cache"].([]any)
for _, raw := range cache {
item, _ := raw.(map[string]any)
if strings.ToLower(textOrEmpty(item["name"])) == wanted {
if found != "" && found != textOrEmpty(item["id"]) {
return ""
}
found = textOrEmpty(item["id"])
}
}
return found
}
関連する処理:
チャンネル取得・解決・投稿:internal/httpapi/integrations.go 564–676行
参加コード取得と外部Bot連携:internal/httpapi/legacy.go 162–218行
分析結果の送信:internal/httpapi/matchanalysis.go 654–685行
大会と通知先の選択:webassets/static/js/dashboard.js 11377–11425行
設定・使い分け:Discordへコード・結果を送る / ロビーを作成する / AIの設定・マッチ分析
機能の索引へ戻る ↑
26 自動カメラ・手動切替後の休止・確認待ち
チーム単位の交戦維持、直近の与ダメージ、鮮度付き座標、切替確認を組み合わせます。
最初の候補は攻撃側または被攻撃側です。combatEventLockedは、正の敵チーム間ダメージを確認したら、確認待ち・休止・最小ダメージの判定より先にrecordCombatLockedで両チームの活動時刻を更新します。観戦チームの最後の与ダメージ・被ダメージから4秒で待機表示、15秒未満は無関係な交戦への切替を抑止します。15秒後もタイマーだけでは切り替えず、新しいダメージイベントで判断します。
手動で視点を切り替えた後は、設定した間隔だけ通常の自動切替を休止します。ただし観戦中の選手がノックされた場合は、同じチームの生存・接続済み・非ノックの代替選手を探す別の処理が働き、休止時間を待たず切り替える場合があります。
切替要求の送信成功だけでは確定とせず、観戦者切替イベントを受けて結果を確認します。確認待ちの間は複数要求を重ねず、3秒以内に確認できない場合はエラー状態になります。
初期状態はOFF、別チームへの最短間隔8秒、最小ダメージ1です。設定範囲は間隔1〜60秒、ダメージ1〜500。同チームの通常切替は直近の確認から2秒以上、現在の選手の最後の攻撃から2秒以上を必要とし、直近2秒以内に基準以上のダメージを出した味方から選びます。候補は攻撃中かを優先し、5秒以内の座標がある候補を優先したうえで交戦相手への3次元距離を比較します。座標不足時は名前・識別子の順、ダウン時の候補は部隊内番号・名前・識別子の順で安定させます。
OBSの観戦者指定はどの視点の情報を表示するかの指定です。ゲームの観戦対象を変える自動カメラとは別で、OBS表示元を選んだだけで自動切替が有効になるわけではありません。
実コードを見る:自動カメラ・手動切替後の休止・確認待ち
出典:internal/httpapi/autocamera.go 293–308行
同じチームの代替候補を絞り込む前半です。現在の対象や生存・切断・ノック状態を確認します。後段の並べ替えと実際の切替送信は含みません。
func (a *autoCameraManager) replacementPlayerLocked(victimHash, victimName string, combatTeam int64) (autoCameraPlayer, bool) {
candidates := make([]autoCameraPlayer, 0)
for _, player := range a.roster() {
hash := strings.TrimSpace(player.Hash)
name := strings.TrimSpace(player.Name)
if !player.Alive || player.Disconnected || player.TeamID <= 1 || hash == "" && name == "" ||
a.combatTeamID(player.TeamID) != combatTeam || sameAutoCameraPlayer(hash, name, victimHash, victimName) ||
a.isPlayerDownedLocked(hash, name) {
continue
}
player.Hash, player.Name = hash, name
candidates = append(candidates, player)
}
if len(candidates) == 0 {
return autoCameraPlayer{}, false
}
関連する処理:
既定値と設定検証:internal/httpapi/autocamera.go 82–188行
イベントと代替候補:internal/httpapi/autocamera.go 229–324行
確認待ち・戦闘判定・要求送信:internal/httpapi/autocamera.go 393–565行
実コードを見る:4秒・15秒の状態判定
状態を返す部分の抜粋です。設定済みの観戦チームと活動時刻、固定時間の定義が必要で、単独では実行できません。別チームへの送信判断はcombatEventLockedで別に行います。
出典:internal/httpapi/autocamera_stability.go 44–60行
func (a *autoCameraManager) engagementStatusLocked(now time.Time) (string, float64) {
if !a.active || !booleanValue(a.settings["enabled"]) || a.observedTeamID <= 0 {
return "searching", 0
}
if now.Before(a.manualHoldUntil) {
return "manual_hold", a.manualHoldUntil.Sub(now).Seconds()
}
elapsed := now.Sub(a.lastTeamActivityLocked())
if elapsed >= engagementReleaseAfter {
return "ready", 0
}
remaining := (engagementReleaseAfter - elapsed).Seconds()
if elapsed >= engagementIdleAfter {
return "waiting", remaining
}
return "combat", remaining
}設定・使い分け:自動で観戦対象を切り替える / OBSに試合情報を表示する
機能の索引へ戻る ↑
27 AI接続・モデル・プロンプト・分析ジョブ
記録の整形、生成、保存、共有は段階を分けて実行します。
AI分析は選んだ接続方法・モデル・推論設定で、保存された試合記録を分析します。Codexを使う場合は別途CLIとログイン状態が必要で、OpenAI API・Anthropic APIを使う方法もあります。使えるモデルは接続先から確認し、固定の一覧が常に利用できるとは案内しません。
「プロンプト確認」は指示と入力データの表示です。実行するとアプリ内のバックグラウンドジョブを開始し、同じ試合ですでに分析中なら処理を重ねません。分析画面を閉じてもアプリが動いていれば継続します。
入力には選手・チーム成績、戦闘、武器、リング、使用アイテム、アビリティなどを含みますが、件数に上限があります。戦闘は時刻順の先頭から20〜500件、既定180件で、全ログや試合映像を無制限に分析するわけではありません。
生成には15分のタイムアウトがあり、失敗・空の回答・保存失敗は失敗状態になります。成功時は分析文、モデル、生成日時などを保存してから、任意のDiscord自動送信へ進みます。共有の失敗と、分析の生成・保存の失敗を混同しないでください。
表示に残る「バックグラウンドモード」の値だけで、Go側の分析が別の処理経路になるとは限りません。いずれもアプリ内では非同期に進める処理として扱い、状態と保存された結果を確認します。
モデル一覧の取得はCodex側の接続で使う機能です。APIを使う接続先では、利用可能なモデル名を設定します。接続方式ごとに利用条件が異なり、一覧取得・文章生成・Discord送信を同じ接続テストで保証するものではありません。
| 記録 | 上限 |
|---|---|
| 戦闘 | 設定で20〜500件、既定180件 |
| 武器 / 使用アイテム | それぞれ上位20件 |
| リング別アイテム / 選手別アイテム | 80件 / 120件 |
| アビリティ | 100件 |
| 賞金首 | 機能が有効な場合だけ入力へ追加 |
実コードを見る:AI接続・モデル・プロンプト・分析ジョブ
出典:internal/httpapi/matchanalysis.go 271–285行
同じ試合の分析中状態を確認し、新しいジョブを登録して非同期処理へ渡す関数です。analysisManagerとrun本体などが必要です。公開ページからAIに接続するものではありません。
func (m *analysisManager) begin(server *Server, matchID int64, payload map[string]any) map[string]any {
m.mu.Lock()
if existing := m.jobs[matchID]; existing != nil && (existing.Status == "starting" || existing.Status == "queued" || existing.Status == "in_progress") {
result := map[string]any{"background": true, "job": publicAnalysisJob(existing)}
m.mu.Unlock()
return result
}
now := time.Now().Format("2006-01-02T15:04:05")
job := &analysisJob{MatchID: matchID, Status: "starting", StartedAt: now, UpdatedAt: now}
m.jobs[matchID] = job
settings := cloneMap(m.settings)
m.mu.Unlock()
go m.run(server, job, payload, settings)
return map[string]any{"background": true, "job": publicAnalysisJob(job)}
}
関連する処理:
ジョブの重複抑止と保存順:internal/httpapi/matchanalysis.go 253–358行
プロンプト・生成・共有:internal/httpapi/matchanalysis.go 575–685行
入力範囲と指示:internal/httpapi/matchanalysis.go 687–850行
設定・使い分け:AIの設定・マッチ分析
機能の索引へ戻る ↑
28 AIとの会話・大会下書き・作成確定
AIへの相談と、実際の大会・予約を保存する操作を分離します。
大会作成には試合分析と共通のAI接続設定を使います。大会作成だけ別の接続先が自動で用意されるわけではないため、先にAI設定を確認します。
AIへ渡すのは会話履歴だけでなく、現在の下書きと登録済みモード・マップのカタログです。フォームで直した内容を次の会話にも反映し、AI出力だけを無条件に正解として扱いません。
返ってきた大会名、設定、試合予定、案内文を正規化し、不足や不整合を警告して編集用の下書きにします。通信や回答形式に問題があれば、現在の下書きを返して編集を続けられるようにします。
「この内容で作成」のときに厳しく検証し、大会・開始予定・予約チャットをまとめて保存します。会話しただけでは登録されず、フォームと詳細JSONに同じ項目がある場合は収集時にフォーム側を優先します。
最大30試合、各試合20チャットで、大会名120文字、説明2000文字、チャット500文字までです。日時の重複、未登録Playlist、時刻条件などを確認します。事前適用は0〜120分、相対チャットは0〜1440分前を扱います。
作成時にアクティブ化を選ぶと既存のアクティブ大会を解除して切り替えます。試合複製の30分加算は日付を自動で翌日へ進めないため、深夜の予定は日付も確認します。
実コードを見る:AIとの会話・大会下書き・作成確定
出典:internal/httpapi/tournament_ai_builder.go 203–218行
AIとの通信や回答の読み取りに失敗したとき、現在の下書きと警告を返す部分です。ctx・instructions・prompt・currentDraft・currentWarningsや周辺処理が必要です。失敗を無視して大会を確定するものではありません。
output, provider, err := s.analysis.generateAITournamentBuilderText(ctx, instructions, prompt)
if err != nil {
writeJSON(w, http.StatusBadGateway, map[string]any{
"success": false, "error": "AIとの通信に失敗しました: " + err.Error(),
"draft": currentDraft, "warnings": currentWarnings,
})
return
}
root, err := decodeAIJSONObject(output)
if err != nil {
writeJSON(w, http.StatusBadGateway, map[string]any{
"success": false, "error": "AIの回答を構成データとして読み取れませんでした: " + err.Error(),
"draft": currentDraft, "warnings": currentWarnings,
})
return
}
関連する処理:
下書き生成と作成確定:internal/httpapi/tournament_ai_builder.go 155–329行
内容の正規化と検証:internal/httpapi/tournament_ai_builder.go 331–530行
フォーム収集・複製・確定:webassets/static/js/dashboard.js 10329–10670行
設定・使い分け:AIと相談して大会を作る / 大会を作成・管理する / 試合の開始・チャットを予約する
機能の索引へ戻る ↑
29 外部カスタム・VC・タイムラインの反映
外部サービスの状態変更と、アプリ内の大会・試合開始を区別します。
カスタム連携は外部サービスから情報を取り込み、ローカルの大会や開始予定と関連付けます。カスタムの開始・停止・終了済み変更は、連携先への操作と、必要なVC割当・復元の依頼であり、Apexの試合開始や強制終了そのものではありません。
タイムラインは音声、マップ、マッチ開始のカードを持ちます。カード順でマップと後続の試合を対応付け、実際の実行は設定日時に従います。マップカードは後続試合の事前適用へ変換され、単独のマップ予約になるわけではありません。
試合開始カードを含めて再反映すると、同じカスタムに由来する既存の開始予定と関連する予約チャットを置き換えます。別カスタムまで一括削除するものではありません。音声だけの反映では既存の開始予定を消さないため、最後の試合予約まで削除したい場合は「自動進行」→「開始時刻・予約チャット」の一覧で削除結果を確認します。
マップカードの直後の試合では時刻差を事前適用分へ変換し、同じマップを引き継ぐ次試合は既定2分前を使います。時刻差の計算と日付の扱いが同じではないため、日付をまたぐ予定やカード順と時刻の矛盾には注意します。
実コードを見る:外部カスタム・VC・タイムラインの反映
出典:internal/httpapi/custom_schedule.go 440–448行
対象カスタムに由来する設定だけを除き、別カスタムなどの設定を残す関数です。カスタム名の変数と、設定配列の形式を前提とします。予定の削除を実行するサンプルではありません。
func filterCustomOwned(items []map[string]any, customName string) []map[string]any {
result := make([]map[string]any, 0, len(items))
for _, item := range items {
if textOrEmpty(item["source_custom"]) != customName {
result = append(result, item)
}
}
return result
}
関連する処理:
外部カスタムの状態操作:internal/httpapi/integrations.go 330–363行
予定と音声設定への反映:internal/httpapi/custom_schedule.go 20–297行
同じカスタムに由来する設定の区分:internal/httpapi/custom_schedule.go 440–448行
タイムラインの対応付け:webassets/static/js/dashboard.js 17218–17324行
設定・使い分け:カスタム情報・音声を連携する / 外部サービスの接続設定 / 試合の開始・チャットを予約する
機能の索引へ戻る ↑
30 音声ファイル・テンプレート・予約・終了トリガー
音声の生成機能ではなく、外部サービスに登録した音声の再生を依頼します。
外部サービスの音声ファイルやテンプレートを選び、再生、停止、イントロなどを依頼します。再生先はサービス側で決まり、アプリのPC内のファイル選択やローカルスピーカー再生を保証する機能ではありません。
時刻予約は日付を持たず、動作中に毎秒確認して該当時刻を検出します。不要な予定を残すと翌日も対象になり、開始予約の120秒猶予とは違う仕組みです。
同じ起動中はIDと日付で実行済みを管理しますが、実行済みマーカーは再生依頼の前に付くため、失敗すれば自動で何度も再生を試すとは限りません。管理はメモリ上なので、再起動をまたぐ厳密な一度だけの再生保証もありません。
試合終了時、勝者なしならリマッチ用の音声だけを扱い、勝者ありなら試合カウンターを進め、共通終了・試合番号別・最終試合の音声を判定します。最終試合番号がない古い設定では再同期が必要です。
自動トリガーと時刻予約ではループを無効にします。テスト再生は実際に外部サービスで音が出る操作で、ループの扱いも別です。開始トリガーの設定キーが残っていても現在のイベント入口では処理しないため、開始前の案内は予約で確認します。
実コードを見る:音声ファイル・テンプレート・予約・終了トリガー
出典:internal/httpapi/integrations_runtime.go 118–134行
勝者なしの終了と、勝者ありの終了を分ける前半です。勝者なしでは試合カウンターを進めません。設定、マネージャー、外部再生処理などが必要な抜粋です。
func (m *integrationManager) audioMatchEnd(hasWinners bool) {
settings := m.audio.snapshot(false)
if !booleanValue(settings["enabled"]) {
return
}
if !hasWinners {
trigger, _ := settings["rematch_trigger"].(map[string]any)
if trigger != nil && booleanValue(trigger["enabled"]) {
copy := cloneMap(trigger)
m.spawn(func() { _, _ = m.executeAudioTrigger(copy, audioDelay(copy, 0), 0) })
}
return
}
m.mu.Lock()
m.audioCounter++
matchNumber := m.audioCounter
m.mu.Unlock()
関連する処理:
一覧・テスト再生・停止・予約保存:internal/httpapi/integrations.go 419–511行
時刻検出・終了時の条件・再生依頼:internal/httpapi/integrations_runtime.go 68–208行
設定・使い分け:カスタム情報・音声を連携する
機能の索引へ戻る ↑
31 公開結果・現在ルールでの再計算・再送
保存した大会の結果を組み立て直し、外部の公開ページへ送る機能です。
終了時刻と勝者がある大会所属試合を集め、現在の大会ポイント設定で再計算します。新しく終わった1試合の差分だけでなく、対象大会の試合別・累計の結果を再構成して送ります。
自動処理は試合保存後、その時点のアクティブ大会を対象とします。大会の公開カスタム指定を優先して送信先を決めるため、試合保存時の所属先と、送信処理で選ぶ大会の状態をあわせて確認します。
送るのは選手・チームの成績、得点内訳、使用アイテム、大会累計などです。試合映像やアプリ設定一式ではありません。手動の対象指定がなければアクティブ大会を優先し、なければ保存された未公開対象を参照します。
送信は各回15秒で打ち切り、失敗時は5秒間隔で最大2回再試行します。未公開の対象と最後のエラーを保持し、原因を解消して手動で再送できます。再送時にも対象大会と公開先を確認します。
実コードを見る:公開結果・現在ルールでの再計算・再送
出典:internal/httpapi/public_results.go 74–87行
未公開対象を記録し、成功したら未公開状態を解消して成功日時を残す2つの関数です。公開状態のマネージャーや設定保存処理が必要で、実際の公開送信そのものは省いています。
func (m *integrationManager) markPublicPending(customName string, tournamentID int64) {
m.updatePublicStatus(map[string]any{
"pending": true, "pending_tournament_id": tournamentID,
"pending_custom_name": customName, "last_error": "",
})
}
func (m *integrationManager) markPublicSuccess(customName string) {
m.updatePublicStatus(map[string]any{
"pending": false, "pending_tournament_id": nil, "pending_custom_name": "",
"last_error": "", "last_published_at": time.Now().Format("2006-01-02T15:04:05"),
"last_custom_name": customName, "last_via": "api",
})
}
関連する処理:
試合保存後の連携:internal/httpapi/background.go 18–33行
状態保存・手動と自動の対象選択:internal/httpapi/public_results.go 33–192行
公開先・再試行・送信内容の構成:internal/httpapi/public_results.go 219–402行
設定・使い分け:大会の結果を公開ページへ送る / 大会を作成・管理する
機能の索引へ戻る ↑
32 外部接続設定と、手動・自動マッチレポート
送信方式によって、適用する設定や履歴の残し方が違います。
「高度な設定」→「外部サービス・AI」のカスタム接続を保存すると、音声・レポート向けの接続設定にも反映されます。「保存して取得テスト」は設定保存の後に実際の情報取得を行い、条件を満たす連携の画面が表示されます。
マッチレポートは試合概要・チーム・選手・保存済みポイントなどを対応サービスへ送ります。AIの分析文でも、現在の大会ルールで作る公開順位でもありません。プレビューは送信内容を見る操作で、送信確定とは別です。
自動送信は初期OFFで、既定では保存後3秒待ち、失敗時は2回・5秒間隔で再試行します。ポイントを含めるか、リーグID、待機・再試行設定をこの経路で使い、HTTP 4xxでは再試行を止めます。
手動の単体・大会送信では、現在の処理は自動送信のinclude_points・league_id・再試行設定を同様には適用せず、保存済みポイントを含む内容を送ります。大会送信は所属全試合をまとめるため、公開結果の勝者あり条件を流用しません。
自動送信と手動単体送信は履歴を残しますが、手動の大会一括送信はローカル送信履歴に記録しません。大会一括は送信直後に返る結果と連携先で確認します。自動処理は直近100履歴へ整理し、履歴画面は50件を表示します。
| 方式 | 設定と点数 | 確認場所 |
|---|---|---|
| 手動の単体送信 | 保存済みポイントを含む。自動送信オプションは同様には適用しない。 | プレビュー、送信応答、履歴 |
| 手動の大会一括送信 | 所属試合をまとめて送る。公開結果の対象条件とは別。 | プレビュー、送信応答、連携先。ローカル履歴には残さない。 |
| 自動送信 | 待機、ポイント含有、リーグID、再試行を設定から使用。 | 送信履歴と連携先 |
実コードを見る:外部接続設定と、手動・自動マッチレポート
出典:internal/httpapi/background.go 72–84行
自動送信で成功したら終わり、4xxなら再試行を止め、その他の失敗では次の試行前に待つ部分です。事前の送信結果、設定、履歴記録などに依存します。
for attempt := 0; attempt <= retries; attempt++ {
lastResult, lastStatus, lastErr = s.externalCallWithKeyPolicy(ctx, settings, http.MethodPost, "/match-report", payload, false)
if lastErr == nil {
s.recordReportHistory(matchID, true, lastStatus, lastResult)
return
}
if lastStatus >= 400 && lastStatus < 500 {
break
}
if attempt < retries && !s.waitForBackground(retryDelay) {
return
}
}
関連する処理:
接続設定の反映:internal/httpapi/integrations.go 253–270行
保存点からのレポート・手動送信・履歴:internal/httpapi/integrations.go 693–826行
自動送信オプションと再試行:internal/httpapi/background.go 35–98行
設定・使い分け:マッチレポートを送信する / 外部サービスの接続設定
機能の索引へ戻る ↑
33 ライセンス・秘密情報・外部接続の境界
利用状態の確認と、運営データや接続情報を安全に扱うための仕組みです。
認証状態と有効期限は起動時だけでなく利用中も確認します。無効・期限切れなどの状態は、管理機能やApexとの接続に影響します。画面を表示できることと、機能の利用が許可されていることは別です。
通信障害時の一時的な猶予には、同じ起動中に有効な認証が確認できていることなどの条件があります。既定の6時間という値を、オフライン起動や必ず6時間利用できる保証として扱わないでください。
秘密の設定値は通常の表示では伏せられ、Windows側の保護機構を利用して保存します。別PCや別Windowsユーザーへ設定ファイルをコピーしても、そのまま使えるとは限りません。移行後に接続情報を入力し直す場合があります。
外部サービスのURLを変更した際は、新しい接続先のキーを明示して保存します。古い接続先のキーが勝手に引き継がれる前提にしないでください。試合JSONにも選手の識別値や成績が含まれるため、秘密設定がなくても公開範囲の確認は必要です。
関連する処理:
公開状態・有効性の確認:internal/license/manager.go 98–165行
設定保護の担当箇所:internal/securestore/settings.go 9–55行
外部接続設定の扱い:internal/externalapi/security.go 13–90行
設定・使い分け:ライセンスの認証・有効期限 / 外部サービスの接続設定 / バックアップ・中断時の確認
機能の索引へ戻る ↑
34 更新・インストーラー・再起動
更新の取得、ファイル検証、記録を守る終了、インストーラー起動は別々の段階です。
更新情報のアプリ種別・配布チャネル・数値形式のバージョンを確認し、現在より新しい更新だけを受け付けます。文字列の大小ではなく、ドットで区切った各数値を順に比較します。
ダウンロードでは許可された同一オリジンのHTTPS、リダイレクトの禁止、サイズ、SHA-256などを確認します。配布ビルドに署名検証用の公開鍵が設定されている場合は、電子署名も確認します。すべてのビルドで同じ署名設定とは限りません。
検証後すぐに現在の処理を切り捨てるのではなく、LiveAPI・HTTP・試合保存・バックグラウンド処理を順に終了させます。終了処理でエラーが残る場合は、後続の再起動やインストーラー起動へ進みません。
通常の更新はセットアップEXEを使い、アプリ本体と外部マップ画像を更新します。設定・試合履歴は別のデータ領域に保持されます。ただし更新前のバックアップは必要で、単体のアプリEXEを配信する手順と混同しないでください。
実コードを見る:更新・インストーラー・再起動
出典:internal/updater/updater.go 258–279行
バージョンの各桁を最大4つの整数にして比較する部分です。たとえば2.0.10は2.0.9より新しく、2.0.1と2.0.1.0はこの比較では同じになります。呼び出し前の形式検査や公開情報の検証は省略しており、strings・strconvの読み込みも必要です。
func compareVersions(left, right string) int {
parse := func(value string) [4]int {
var out [4]int
for i, part := range strings.Split(value, ".") {
if i >= 4 {
break
}
out[i], _ = strconv.Atoi(part)
}
return out
}
a, b := parse(left), parse(right)
for i := range a {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
return 0
}
関連する処理:
更新の検証と終了前の保護:internal/httpapi/application.go 52–125行
更新情報と取得ファイルの検証:internal/updater/updater.go 43–219行
安全な終了の順序:cmd/apexcustom/main.go 246–287行
本体・地図画像の配布:installer.iss 40–59行
設定・使い分け:アプリを更新する / バックアップ・中断時の確認
機能の索引へ戻る ↑
35 開発時の構成・ビルド・テスト
公開しているサンプルと、アプリ本体を開発・配布する作業を区別します。
起動処理は cmd/apexcustom、通信と各機能は internal 以下、画面は webassets のテンプレート・JavaScript・CSSに分かれています。HTTPの処理が状態管理や集計を呼び、画面がその結果を表示する構造です。
確認したソースの go.mod はGo 1.27.0を要求します。配布用の build.ps1 は必要なGoの確認、全Goテスト、本体のビルド、埋め込みスキーマの確認、Inno Setup 6によるセットアップ作成を順に行います。ここで最新のツールチェーン一覧を案内しているわけではありません。
ApexのLiveAPIスキーマは実行ファイルへ埋め込みます。build.ps1 は見つかったApexの定義から記述子を更新・検証し、Apexを見つけられない環境では同梱の定義を使います。配布アプリが起動のたびに定義を自動更新する仕組みではありません。
回帰テストにはGoの各パッケージのテストと、Node.jsで動かす大会画面のテストがあります。公開サンプルの単独動作確認と、Apex・OBS・各連携先を使った結合確認は別です。実装上の入出力だけで、実際の試合や外部送信の成功を証明したとはいえません。
ローカルHTTPの内部処理には、試合の一時停止、準備状態、チーム名、スポーン位置、最終リングなどの補助操作もあります。ただし通常の管理画面からつながっていない処理があり、定義があることだけで「一般ユーザーが画面から使える機能」や「外部向けの安定したAPI」とは扱いません。Apex側の対応や権限も必要になるため、ここでは操作を実行する例は掲載しません。
関連する処理:
依存関係と必要バージョン:go.mod 1–21行
配布用ビルドの流れ:build.ps1 201–317行
開発者向けの構成説明:README_GO.md 23–116行
画面と区別する補助操作:internal/httpapi/legacy.go 287–313行
チーム・配置などの補助操作:internal/httpapi/legacy.go 434–483行
設定・使い分け:動作環境・準備するもの / アプリを更新する / 困ったとき・よくある質問
機能の索引へ戻る ↑
共同運営:専用権限と試合開始スナップショット
2.0.5の共同運営は Go の collaboration*.go と Custom Management の custom_collaboration.py で実装しています。従来章のコード抜粋は2.0.4確認時点です。以下は2.0.5の設計説明であり、そのまま実行するコードではありません。
専用キーは X-Collaboration-Key で送信し、サーバーではSHA-256ハッシュで照合します。カスタム単位の境界と editor / recorder / viewer を検査し、全体管理 API のキーと混用しません。更新は expected_revision による比較で競合を409として拒否します。スキーマ版2は許可グループと入れ子の型・範囲・容量を制限します。
共有グループは point_settings、item_candidates、lobby_settings、camera_settings、roster、schedule。OBS認証やローカル接続情報は含めません。カスタム専用の actions は editor のみが開始/停止/完了/公開を既存処理へ渡せます。キー発行と端末移管は管理API側に限定します。
Session.WithIdle で MatchSetup と設定反映を直列化し、マッチ作成のトランザクションで tournament_shared_context から match_recording_context を生成します。後者は UPDATE 禁止トリガーで固定し、採点と再集計では RecordingPointSettings / RecordingItemFilter を使います。マッチ詳細には運営者・端末・担当・設定版を返しますが、一般公開結果へ端末識別情報は追加しません。
プロフィールごとに接続キーをsecurestoreで保持し、約10秒間隔の自動取得は古い接続先からの応答を適用しないよう識別子を確認します。DBとローカル設定ファイルを跨ぐ原子性は保証しないため、保存エラーは再反映と開始前チェックで扱います。旧形式マッチは後から記録担当を推定しません。相互比較と正式結果の採用は将来段階です。