← 一覧に戻る

fortics

GitHub ↗ Python 最終push: 2026/10/21 19:54

WIP(現在進行中)

Work In Progress

このプロジェクトで現在進行中の作業と、過去のスナップショットを記録する。

現在の状況

今どこまで進んだか(2026-10-21 19:52 時点):

  • https://analytics.fortrank.com/ で公開中。新規解析はサイト全体で1時間20件、同じアクセス元から10件まで(IPv6 は /64 ごと)。上限は infra/deploy.sh の RATE_LIMIT・PER_IP_LIMIT で決める
  • 試合中の試合も約5分遅れで追いかけられる(ローカル版もクラウド版も)。マップの左上に LIVE の札(「リアルタイム更新中」「約5分遅れ・30秒ごとに更新」)を出す。混んでいて止めたときは灰色の「更新を止めました」(2c7b017。本番に反映済み)
  • クラウドはリプレイを S3 に残さない(f032100。本番に反映済み)。作り直すときは Epic から取り直す
  • 費用(実測からの試算): 新しい解析1件は約0.0005〜0.0013ドルで、今の上限いっぱい(月14,400件)でも月約2.4〜14ドル。解析済みの閲覧は CloudFront だけで、1万人でも無料枠の2%。いちばん大きいのは試合中の表示で、1試合約0.018ドル、天井は月120〜140ドル(3試合を24時間)
  • 利用状況(アクセスログ 10/17〜10/21): 人のアクセスの85%・解析の開始の91%は You 以外(リンクを教えた人たち)。件数の75%は脆弱性スキャン
  • セキュリティ: CSP などのヘッダー、S3 の公開範囲の限定、アクセスログ(90日)、受付 Lambda の同時実行20、AWS Budgets の通知
  • GA4(G-NT6S034ES8。会社の Google アカウント側のプロパティ)で本番だけ計測。マッチIDは送らない
  • Epic の資格情報(Android 版 Fortnite の fortniteAndroidGameClient)はリポジトリ直下の .env(git の管理外)。クラウドは Lambda の環境変数(デプロイ時に .env から渡す)。Epic が値を変えるとトークンの取得が HTTP 400 で失敗するので、.env を書き換えてデプロイし直す
  • 収益化は今はしない(Epic の規約で、ファンサイトは非営利が条件)
  • ローカルで見るときは、ターミナルのタブで python3 -I tools/serve.py 8765 を動かす
  • 最新コミット: 2c7b017

次に何をするか:

  • 本物の試合中の試合で、LIVE の札が出るか確かめる
  • 試合中の表示の費用の天井(月120〜140ドル)を下げるか決める(同時に追いかける試合の数・作り直し用 Lambda の同時実行数・間隔など)
  • クラウドに古いデータのまま残っているリロードの試合2件(d1ce0a53…・ed602a4f…)を解析し直すか決める。試合データを消して開き直すと Epic から取り直す。Epic がサーバーリプレイを消す 10/23〜30 ごろまでに
  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に安地の円やバスの矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す
  • タスクのカードを出してあるもの: ローカルのリプレイで全員の位置が取れるか調べる/英語を見つける Stop hook が最後の返答を見逃す原因を調べる

詰まっていること・未決事項:

  • 最初からの解析が13.2秒で、前の実測(40〜51秒)よりずっと短かった。理由は未確認で、1回しか測れていない。使用メモリは約1GB(割り当て2GB)で、減らせるかは未確認
  • 撃破が1件も無い試合は、時刻のずれを求められず試合データを作れない
  • Lambda のログ: 保存期間が未設定で、中身も読めない(Ichiro に logs:PutRetentionPolicy・FilterLogEvents の権限が無い)。1回ごとの時間とメモリは lambda invoke --log-type Tail の REPORT 行で見る
  • 見送ったレビュー指摘: リプレイ取得の量・時間の上限、deploy.sh で秘密の値がプロセス引数に出る件、関数 URL の IAM 認証(Lambda 用 OAC)
  • シーズンが変わったら、通常マップとリロードのマップの作り直しが要る
  • 規約面: 借りた資格情報で Epic の内部 API を叩く問題は、無料でも残る。収益化するなら、先にローカルのリプレイを読む方式に移り、弁護士に確認する(不正アクセス禁止法・リバースエンジニアリングの禁止に当たらないか)
  • 非公式の表示は、ファンコンテンツのポリシー 1.10 の指定の免責文そのものではなく、柔らかい日本語にした(You の判断)
  • 名前の札は、地図全体の表示では別のチームどうしの重なりで、約4割の札が半分以上隠れる

過去のWIPアーカイブ

(新しい「現在の状況」を書く前に、古いものをここに追記でアーカイブする。新しいものが上)

2026-10-21 19:52 時点のスナップショット

今どこまで進んだか(2026-10-20 03:35 時点):

  • https://analytics.fortrank.com/ で公開中。新規解析はサイト全体で1時間20件、同じアクセス元から5件まで(IPv6 は /64 ごと)。上限は infra/deploy.sh の RATE_LIMIT・PER_IP_LIMIT で決める
  • セキュリティ: CSP などのヘッダー、S3 の公開範囲の限定、アクセスログ(90日)、受付 Lambda の同時実行20、AWS Budgets の通知
  • GA4(G-NT6S034ES8。会社の Google アカウント側のプロパティ)で本番だけ計測。マッチIDは送らない
  • Epic の資格情報(Android 版 Fortnite の fortniteAndroidGameClient)はリポジトリ直下の .env(git の管理外)。クラウドは Lambda の環境変数(デプロイ時に .env から渡す)。Epic が値を変えるとトークンの取得が HTTP 400 で失敗するので、.env を書き換えてデプロイし直す
  • 2026-10-20 に本番へ反映したビューアの変更(3b5b3a0):
    • 画面の下に「非公式のツールで Epic Games とは関係がない」表示
    • 名前の札は同じチームの札とバスの絵だけを避け、別のチームとは重なってよい(順位の良いチームが上)。前の方向を覚える・移るときだけ余白・0.15秒のフェード・出す前に0.12秒待つ
    • 点と×は拡大するほど小さく(5.5px → 最小4px)。拡大の上限は、地図全体の14倍か画像の1pxが画面の6pxの大きいほう
  • 収益化は今はしない(Epic の規約で、ファンサイトは非営利が条件。biz と corporate の expert の見解は expert-brains に記録)
  • 2026-10-21: クラウドはリプレイを S3 に残さなくなった(解析1件の費用が約4割減。DECISIONS 参照)
  • ローカルで見るときは、ターミナルのタブで python3 -I tools/serve.py 8765 を動かす(プレビューのサーバーはアプリに止められることがある)
  • 最新コミット: 3b5b3a0

次に何をするか:

  • クラウドに古いデータのまま残っているリロードの試合2件(d1ce0a53…・ed602a4f…)を解析し直すか決める。S3 のリプレイはもう使わないので、試合データを消して開き直すと Epic から取り直す。Epic がサーバーリプレイを消す 10/23〜30 ごろまでに
  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に安地の円やバスの矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す
  • タスクのカードを出してあるもの: ローカルのリプレイで全員の位置が取れるか調べる/英語を見つける Stop hook が最後の返答を見逃す原因を調べる

詰まっていること・未決事項:

  • 撃破が1件も無い試合は、時刻のずれを求められず試合データを作れない
  • Lambda のログ: 保存期間が未設定で、中身も読めない(Ichiro に logs:PutRetentionPolicy・FilterLogEvents の権限が無い)
  • 見送ったレビュー指摘: リプレイ取得の量・時間の上限、deploy.sh で秘密の値がプロセス引数に出る件、関数 URL の IAM 認証(Lambda 用 OAC)
  • シーズンが変わったら、通常マップとリロードのマップの作り直しが要る
  • 規約面: 借りた資格情報で Epic の内部 API を叩く問題は、無料でも残る。収益化するなら、先にローカルのリプレイを読む方式に移り、弁護士に確認する(不正アクセス禁止法・リバースエンジニアリングの禁止に当たらないか)
  • 非公式の表示は、ファンコンテンツのポリシー 1.10 の指定の免責文そのものではなく、柔らかい日本語にした(You の判断)
  • 名前の札は、地図全体の表示では別のチームどうしの重なりで、約4割の札が半分以上隠れる

2026-10-20 03:35 時点のスナップショット

今どこまで進んだか(2026-10-19 16:20 時点):

  • https://analytics.fortrank.com/ で公開中。新規解析はサイト全体で1時間20件、同じアクセス元から5件まで(IPv6 は /64 ごと)。上限は infra/deploy.sh の RATE_LIMIT・PER_IP_LIMIT で決める
  • セキュリティ: CSP などのヘッダー、S3 の公開範囲の限定、アクセスログ(90日)、受付 Lambda の同時実行20、AWS Budgets の通知
  • GA4(G-NT6S034ES8。会社の Google アカウント側のプロパティ)で本番だけ計測。マッチIDは送らない(URL のクエリを外し、拡張計測のフォーム操作・履歴でのページ変更はオフ)
  • リプレイは S3 の replays/ に7日だけ残す
  • 10/17〜10/19 に別のセッションで、リプレイファイルからマッチIDを読んで開く機能、通常マップの座標合わせの詰め直し(refine_calib.py)、バスのピクトが入った(DEVLOG には記録が無い)
  • Epic の資格情報(Android 版 Fortnite の fortniteAndroidGameClient)は、リポジトリ直下の .env(git の管理外)から読む。yuyutti のリポジトリは読まなくなった。クラウドは今までどおり Lambda の環境変数(デプロイ時に .env から渡す)
    • 新しい Mac では、.env.example を .env に写して値を入れる
    • Epic が値を変えるとトークンの取得が HTTP 400 で失敗する。そのときは .env を書き換えてデプロイし直す
  • 最新コミット: a7cef5a

次に何をするか:

  • クラウドに古いデータのまま残っているリロードの試合2件(d1ce0a53…・ed602a4f…)を解析し直すか決める。S3 の試合データを消して再解析する(リプレイは S3 にあるので Epic から取り直さない。2件分の枠を使う)。リプレイが消える 10/23 ごろまでに
  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に安地の円やバスの矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す

詰まっていること・未決事項:

  • 撃破が1件も無い試合は、時刻のずれを求められず試合データを作れない
  • Lambda のログ: 保存期間が未設定で、中身も読めない(Ichiro に logs:PutRetentionPolicy・FilterLogEvents の権限が無い)
  • 見送ったレビュー指摘: リプレイ取得の量・時間の上限、deploy.sh で秘密の値がプロセス引数に出る件、関数 URL の IAM 認証(Lambda 用 OAC)
  • GA4 のデータストリーム画面に「データ収集が有効になっていません」と出ていた(リアルタイムでは計測できているので、表示の遅れとみられる)
  • シーズンが変わったら、通常マップとリロードのマップの作り直しが要る
  • 一般公開の範囲(非公式 API・Epic の公開クライアント資格情報の利用)は、上限つきで公開した状態のまま。Epic の API を使う限り、借りた資格情報は避けられない(正式な手段は無い)。避けるなら、ローカルのリプレイを丸ごと読むしかない(全員の位置が入っているかは未確認)

2026-10-19 16:20 時点のスナップショット

今どこまで進んだか(2026-10-17 13:43 時点):

  • https://analytics.fortrank.com/ で公開中。新規解析はサイト全体で1時間20件、同じアクセス元から5件まで(IPv6 は /64 ごと)。上限は infra/deploy.sh の RATE_LIMIT・PER_IP_LIMIT で決める
  • セキュリティ: CSP などのヘッダー、S3 の公開範囲の限定、アクセスログ(90日)、受付 Lambda の同時実行20、AWS Budgets の通知
  • GA4(G-NT6S034ES8。会社の Google アカウント側のプロパティ)で本番だけ計測。マッチIDは送らない(URL のクエリを外し、拡張計測のフォーム操作・履歴でのページ変更はオフ)
  • リプレイは S3 の replays/ に7日だけ残す
  • GPT の2回目のレビューの指摘は、見送った3件以外すべて直した
  • 最新コミット: 563997b

次に何をするか:

  • クラウドに古いデータのまま残っているリロードの試合2件(d1ce0a53…・ed602a4f…)を解析し直すか決める。S3 の試合データを消して再解析する(リプレイは S3 にあるので Epic から取り直さない。2件分の枠を使う)。リプレイが消える 10/23 ごろまでに
  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に安地の円やバスの矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す

詰まっていること・未決事項:

  • 撃破が1件も無い試合は、時刻のずれを求められず試合データを作れない
  • Lambda のログ: 保存期間が未設定で、中身も読めない(Ichiro に logs:PutRetentionPolicy・FilterLogEvents の権限が無い)
  • 見送ったレビュー指摘: リプレイ取得の量・時間の上限、deploy.sh で秘密の値がプロセス引数に出る件、関数 URL の IAM 認証(Lambda 用 OAC)
  • GA4 のデータストリーム画面に「データ収集が有効になっていません」と出ていた(リアルタイムでは計測できているので、表示の遅れとみられる)
  • シーズンが変わったら、通常マップとリロードのマップの作り直しが要る
  • 一般公開の範囲(非公式 API・Epic の公開クライアント資格情報の利用)は、上限つきで公開した状態のまま

2026-10-17 13:43 時点のスナップショット

今どこまで進んだか(2026-10-16 20:29 時点):

  • https://analytics.fortrank.com/ で公開中(鍵なし、新規解析はサイト全体で1時間10件まで)。マッチIDを入れると Lambda で取得・解析し、マップ上で全員の動きを再生する
  • 対応マップ: 通常マップ(br)、リロードの Surf City(piperboot)・Nitemare Island(sourspawn)・Slurp Rush(dashberry)。どれも地図画像と座標合わせあり(画像は data/maps/ と S3 にあり、git の管理外)
  • ローカル版も同じ作り(python3 -I tools/serve.py → http://localhost:8765/)
  • 最新コミット: e35fd8a(リロード3マップ対応)

次に何をするか:

  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する(これまでの Surf City・Slurp Rush の試合IDはユーザーが1人で入った短い試合で、試合データにならなかった)
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に安地の円やバスの矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す(今は2〜3文字かすれている)

詰まっていること・未決事項:

  • 撃破が1件も無い試合は、時刻のずれを求められず試合データを作れない(安地からも求める方法は未実装)
  • Lambda のログ保存期間が未設定(ユーザー Ichiro に logs:PutRetentionPolicy の権限が無い)。ログの中身も読めない
  • シーズンが変わったら通常マップ(python3 -P -E tools/fetch_map.py)と、リロードのマップ(スクショから)の作り直しが要る
  • 一般公開の範囲(非公式 API・Epic の公開クライアント資格情報の利用)は、上限つきで公開した状態のまま。本格的に広めるなら規約面を考える
  • fort-rank 側: 配布アプリに AWS キーが埋め込まれている件(IAM の権限を v/ への書き込みだけに絞れているか確認が要る)

2026-10-16 20:29 時点のスナップショット

(初回のまとめのため、以前の「現在の状況」は雛形のままだった)

ROADMAP(計画)

ロードマップ

今週

  • マッチIDから全員の動きを再生する Web ビューアを作る(ローカル)
  • Codex(GPT)レビューの指摘を直す
  • analytics.fortrank.com にクラウド版(Lambda)を公開する
  • リロードの3マップ(Surf City・Nitemare Island・Slurp Rush)の地図に対応する
  • セキュリティを強化し、GA4 を入れる
  • Codex(GPT)の2回目のレビューの指摘を直す
  • クラウドの古いリロードの試合2件(d1ce0a53…・ed602a4f…)を解析し直す(Epic から取り直す。Epic がリプレイを消す 10/23〜30 ごろまでに)
  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認する
  • ゼロビルドの本番の試合で、タイトルに「ゼロビルド」が出るか確認する
  • 地名に円や矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す(文字のかすれをなくす)
  • 名前の札のチカチカを減らす(同じチームの札だけ避ける・方向を覚える・フェード)
  • 画面の下に非公式の表示を入れる
  • 点を拡大するほど小さくし、拡大の上限を広げる
  • 新しい解析の費用を実測から見直し、クラウドでリプレイを S3 に残すのをやめる
  • 試合中はマップの左上に LIVE の札を出す
  • 本物の試合中の試合で、LIVE の札が出るか確かめる

今月

  • 撃破が1件も無い試合でも試合データを作れるようにする(安地から時刻のずれを求める)
  • bot がらみの撃破をキルフィードに出す
  • FortRank.SessionNotifier の Discord 投稿に、fortics のビューアのリンクを付ける
  • Lambda のログ保存期間を設定する(Ichiro に logs:PutRetentionPolicy の権限が要る)
  • fort-rank: 配布アプリに埋め込まれた AWS キーの権限を確認する(確認して、今のままにすると決めた)
  • リプレイ取得の量・時間に上限を付ける(GPT の2回目のレビューで見送った指摘)
  • ローカルのリプレイで全員の位置(CharacterSample)が取れるか調べる(取れれば、借りた資格情報を使わない版が作れる)
  • 試合中の表示の費用の天井(3試合を24時間で月120〜140ドル)を下げるか決める
  • 解析の時間とメモリを何回か測り、解析用 Lambda のメモリ(2GB、使用は約1GB)を減らせるか確かめる

今四半期

  • シーズン切り替え時のマップ更新を手順化する(通常マップは fetch_map.py、リロードはスクショから)
  • 一般公開するかを決める(非公式 API・Epic の公開クライアント資格情報の扱い、規約面。2026-10-20 に、今は収益化しないと決めた)

いつか

  • gunType・DeathCause の番号の意味を調べて表示に使う
  • データチャンクから細かい移動を読み、10秒おきより細かく再生する
  • 長く位置が途切れている区間を「位置不明」と区別して出す
  • 起動時、通常マップの画像を読み終える前から ?match= の試合を読み始める
  • deploy.sh で秘密の値をプロセス引数に出さない(パラメーターファイルから渡す)
  • 収益化を考え直す(ローカルのリプレイを読む方式に移れて、弁護士に確認してから。本命はスクリム主催者向けの有料、つなぎは寄付)
  • 「リプレイから」で、前に開いたフォルダを覚える・保存場所の案内を出す(2026-10-20 は今のままにした)

DECISIONS(意思決定)

意思決定記録

このプロジェクトで下した重要な意思決定を記録する。 最新が上に来る。


2026-10-21: 試合中の表示は、マップの左上に2行の LIVE の札を出す

背景: 試合中の試合を開いていることが、上のバーのタイトル(「試合中(約5分遅れで更新)」)でしか分からなかった。You から、よくある LIVE 中の見た目で「リアルタイム更新中」と出したいと要望があった。

決定: マップの左上に、点滅する赤い点・赤い「LIVE」・「リアルタイム更新中」と、2行目に「約5分遅れ・30秒ごとに更新」を出す(4案のうち D 案。You の判断)。混んでいて更新を止めたときは、灰色にして点滅をやめ、「更新を止めました」と理由を出す。遅れと止めた理由は札に出すので、タイトルは「…・試合中」だけにする。

理由: 中身は約5分遅れなので、「リアルタイム」とだけ書くと、今この瞬間が映っていると受け取られる。遅れの説明を同じ札に添えた。ほかの案は、赤い LIVE+文字の1行(配信サイト風)、点滅する点+文字(控えめ)、赤い丸札に広がる白い点(目立つが地図の上ではうるさい)。

2026-10-21: クラウドではリプレイを S3 に残さない

背景: 新しい解析の費用を見直したところ、1件約0.0024ドルのうち約4割が、組み立てたリプレイ(平均152MB)を S3 の replays/ に7日残す分だった。リプレイは解析ツールを直したときに作り直すための保険としていた。ところが試合データ(matches/)ができたあとは、受付用 Lambda が解析済みと返して解析用 Lambda を呼ばない。そのため S3 のリプレイが読まれるのは試合データを手で消したときだけで、まとめて作り直す道具も無く、一度も使っていなかった。

決定: 解析用 Lambda は、リプレイ・Epic のメタデータ・リーダーの出力を S3 に置かず、読みにもいかない。作り直すときは Epic から取り直す。replays/ のライフサイクル規則(7日)は、残っている分が消えるまで残す。手元の data/replays/ はこれまでどおり残す(開発で使う)。2026-10-17 の「リプレイの保存は7日」を置き換える。

理由: 7日のあいだは Epic 側にもサーバーリプレイが残っているので、S3 に残しても作り直せる期間は延びない。残る利点は、Epic から取り直さずに済むことだけ。試合データを作る build_match.py は位置や撃破を .replay から直接読むので、軽い reader.json(約0.9MB)だけ残しても作り直せない。やめると解析1件は約0.0015ドルになり、今の上限いっぱい(月14,400件)で月約29ドルが約16ドルになる。

2026-10-20: 名前の札は同じチームの札だけを避け、別のチームとは重なってもよくする

背景: 名前の札が、人の動きに合わせて置き場所を変えたり、出たり消えたりしてチカチカし、見にくかった(You の指摘)。全部の札を避ける置き方では、人が集まった所で札が飛んだり消えたりする。

決定: 札は、同じチームの札とバスの絵だけを避ける。別のチームの札とは重なってよく、重なったところは順位の良いチームの札が上にくる。前の方向を覚える・移るときと出し直すときだけ 3px の余白・0.15秒のフェード・出す前に0.12秒待つ、は続けて使う。

理由: 同じ条件で数えると、デュオ58人・×5・地図全体で、出し消しが1分に198回(全部避ける版)から13回になり、出ている札も23枚から45枚に増えた。味方どうしの重なりは0%。全部の重なりを許すと、地図全体で札の約半分が半分以上隠れて読めなかった。味方の札を「A / B」の1枚にまとめる案は、同じパーティーを1枚にするのは良くないので却下(You の判断)。

2026-10-20: 点は拡大するほど小さくし、拡大の上限は地図の画像を基準にする

背景: 点は拡大しても同じ大きさ(半径 5.5px)で、地図に対して大きく見えた。拡大の上限が「地図全体の14倍」だけだと、小さい画面では画像の1pxが画面の約2pxまでしか拡大できなかった。

決定: 点と×は、拡大率の 0.15 乗分の1 で小さくし、半径 4px で止める(地図全体で 5.5px)。撃破の輪・名前の札の文字・バスの絵は変えない。拡大の上限は「地図全体の14倍」か「地図の画像の1pxが画面の6px」の大きいほう。

理由: 最初は拡大するほど大きくする案を作ったが、You の意図は逆だった。0.3 乗・最小 3px は小さくなりすぎたので緩めた(You の判断)。拡大の上限を画像基準にすると、どの画面でも同じくらいまで拡大できる(それ以上は画像がぼやけるだけ)。

2026-10-20: 非公式の表示は、柔らかい日本語の一文にする(指定の免責文そのままにはしない)

背景: Epic のファンコンテンツのポリシー 1.10 は、ファン作品のページに決まった免責文を出すよう求める。日本語版の文面(「Epicは無断転用を禁じます」など)は硬く、You が怖いと感じた。

決定: 画面の下に「fortics はファンが作った非公式のツールです。Epic Games とは関係がありません。マップ画像などの権利は Epic Games, Inc. にあります。」を常に出す。指定の免責文(日本語・英語の原文)は載せない。

理由: 意味はほぼ同じで、読む人に柔らかく伝わる。1.10 の条件を文字どおりには満たさないが、問題になる可能性は低いと判断した(You の判断)。英語の原文を併記する案もあった。

2026-10-20: 今は収益化しない

背景: You から「マネタイズするなら広告か」と聞かれ、biz と corporate の expert に相談した。

決定: 今は広告・寄付・有料のどれも入れない。

理由: Epic のファンコンテンツのポリシーは「個人的で非営利」が条件で、サイトの広告・寄付・有料を認める例外は無い。利用規約も、別契約がない限り商業利用を禁じる。広告収入の見込みも月数千円と小さい(biz の推測)。収益化を考え直すなら、先にローカルのリプレイを読む方式(借りた資格情報を使わない)に移れるかを確かめ、弁護士に確認する。

2026-10-19: Epic の資格情報は yuyutti のリポジトリから読まず、git の管理外の .env に置く

背景: トークンに使う資格情報を、手元の取得(make_match.py・fetch_meta.sh)とデプロイ(deploy.sh)のたびに、yuyutti/Fortnite_ServerReplay_Downloader(★3)の constants.js から gh で読んでいた。調査中に「借りた値を自分のところに置くかは未決」として仮にそうしていたもの。リポジトリが消えるか書き換わると、手元の取得とデプロイが止まる。

決定: 値をリポジトリ直下の .env(git の管理外、権限 600)に一度だけ写し、3か所ともそこから読む。書式は .env.example(値は空)に残す。クラウドは今までどおり CloudFormation のパラメータ → Lambda の環境変数。macOS のキーチェーンは使わない。

理由: 値は yuyutti のものではなく、Android 版 Fortnite アプリに入っている Epic のクライアント(fortniteAndroidGameClient)のもので、LeleDerGrasshalmi/FortniteEndpointsDocumentation などの公開資料にも載っている。守る意味が薄いので、infra/.env.deploy と同じ持ち方にそろえた。認証なしだとメタデータの時点で 404(folder_not_found_or_not_allowed)で断られ、Epic の正式な手段(Epic Online Services)でも Fortnite の内部サービスには届かないので、資格情報を使うこと自体は変えられない。代わりに、Epic が値を変えたときは自分で書き換える。

2026-10-17: GA4 は本番だけで計測し、マッチIDは送らない。利用者向けの告知は出さない

背景: どれくらい使われているかを知りたい。ただ、マッチIDやプレイヤー名は Google に送りたくない。

決定: analytics.fortrank.com で開いたときだけ gtag を読み込み、page_location から URL のクエリを外す。Google シグナルと広告のパーソナライズはオフ。拡張計測の「フォームの操作」と「ブラウザの履歴イベントに基づくページの変更」もオフにする。「Google アナリティクスを使っています」という表示は出さない(ユーザーの判断)。

理由: 拡張計測の2項目は、コード側でクエリを外しても ?match= 付きの URL を送っていた(本番の通信で確認)。送るイベントの値は、数や決まった語だけにしている。

2026-10-17: 新規解析の上限は全体1時間20件・1人5件、リプレイの保存は7日

背景: 鍵なしで公開しているので、誰でも解析を始められる。最初は全体10件・1人3件にしていたが、少ない。

決定: サイト全体で1時間20件、同じアクセス元から5件(IPv6 は /64 ごと)にする。S3 のリプレイの保存は30日から7日に縮める。

理由: 解析1件は Lambda で約0.0012ドル(2GB で最大46秒)で、毎月約4,000件までは無料枠に収まる。上限まで24時間使われ続けても、計算が月約12ドル、リプレイの保存が月約13ドル。費用でいちばん大きかったのはリプレイの保存(1件50〜180MB)。リプレイは解析ツールを直したときに作り直すための保険で、普段は使わない。Epic 側に止められるおそれもあるので、上限そのものは残す。

2026-10-17: 確認用 API は同時実行数の上限だけ付け、WAF や関数 URL の IAM 認証は見送る

背景: GPT のレビューで、GET /api/matches/<id> に回数制限が無いと指摘された(重要度は高とされた)。

決定: 受付 Lambda の予約同時実行数を20にするだけにした。

理由: 100万回たたかれても約2〜3ドルで、AWS Budgets の通知もある。WAF は月額がかかる。関数 URL の IAM 認証(Lambda 用 OAC)は、POST にペイロードのハッシュが要るなど構成の変更が大きい。

2026-10-17: fort-rank の配布アプリに埋め込まれたキーの権限は今のままにする

背景: 配布アプリに入っているキー(fortrank-uploader)の権限は s3:PutObject on fortrank/* で、配布物を差し替えられる。

決定: 今のままにする(ユーザーの判断)。

理由: fortics とは別のバケットで、fortics には影響しない。fort-rank 側の作業として、今は手を付けない。

2026-10-16: 優勝チームが勝ったあとに退出したのは撃破に数えない

背景: 優勝者が試合の最後に「自滅・ストーム等」として撃破扱いになり、残り人数が0人になっていた(Codex のレビューで指摘)。実際はキルフィードの DeathTags に DeathCause.LoggedOut が付いた退出だった。

決定: LoggedOut の撃破には印を付けてキルフィードに「退出」と出し、優勝チーム(順位1)の退出は撃破に数えない。それ以外の途中退出は撃破として扱う。

理由: 勝って抜けた人を倒された扱いにすると、最後の場面の表示(残り人数・×印)が事実と違ってしまうため。

2026-10-16: バトルバスの航路は位置データから推定する

背景: FortniteReplayReader の BattleBusFlightPaths は空で、航路が取れない。

決定: 各自の最初の点の高さでいちばん多いもの(バスの高さ)にいる点へ、x(t)・y(t) の一次式を最小二乗で当てはめ、島の範囲に入っている区間を航路とする。速さが 50〜100m/s でなければ出さない。

理由: バスは一定の速さでまっすぐ飛ぶので直線で十分。5試合で速さ約75m/s・ずれ中央値約20m と安定していた。バスの高さはマップで違うので自動で求める。

2026-10-16: リロードのマップ画像はゲーム内のスクショから作り、物の位置で座標を合わせる

背景: fortnite-api.com は通常マップしか配っておらず、リロードのマップ画像と座標の手がかりが無い。

決定: ユーザーのゲーム内全体マップのスクショから、安地の円・バスの航路の矢印・ピン・画面表示を消して切り抜く。座標は、リプレイに入る ActorsPosition(宝箱などの位置、マップごとに固定)の塊と地名ラベルを対応づけて向き・倍率・位置を決め、陸地に乗る割合で詰める。スクショがその試合の最中なら、安地の円やバスの位置とも突き合わせる。

理由: 撃破が無い短い試合でも使える材料で、地名ラベル・建物の位置と独立に照合できる。実際に3マップとも、物が建物に乗る・地名やバスの位置が一致することで確かめられた。

2026-10-16: マップはマップの種類ごとに画像と座標合わせを持ち、リロードはプレイリスト名で見分ける

背景: リロードの試合は通常マップとは別の小さいマップで、マップは入れ替わる。リプレイの見出しにはマップ名が入らない。

決定: data/maps/<種類>/ に画像と calib.json(向きが違うマップは matrix)を置く。リロードはプレイリスト名の _RE_ の後ろのコード名(PiperBoot・SourSpawn・DashBerry)で、それ以外はキルフィードの場所のタグの種類で見分ける。画像が無いマップはマス目と地名で描く。

理由: プレイリスト名は撃破が無い試合でも必ずあり、ビルドあり・ゼロビルドで同じコード名になる。マップごとに向きが違うため、座標合わせは一般の一次変換で持つ。

2026-10-16: 解析中の画面は中身を見せず「解析中」と目安の進捗バーだけにする

背景: 取得中の段階(Epic への問い合わせ、ダウンロード件数、リーダー名など)がそのまま画面に出ていた。

決定: 途中の段階は出さず、回る輪と「解析中」、目安の進捗バー(ダウンロードに 5〜80% を割り当て)だけを出す。終わったら自動で再生する。

理由: 利用者には何をしているかより、待てば開くことが伝わればよい(ユーザーの希望)。

2026-10-16: 解析の鍵をなくし、新しい解析はサイト全体で1時間10件までにする

背景: 最初は解析の開始に鍵を付けたが、鍵を持つ人しか解析できないのは不便だった。鍵を外すと、いたずらの大量依頼で費用や Epic への負荷が増える恐れがある。

決定: 鍵をなくし、新しい解析をサイト全体で直近1時間に10件までにする(S3 の rate/starts.json に ETag 条件つきで記録して数える)。超えたら「時間をおいてから、もう一度アクセスしてください」とだけ出す。解析中の試合への再依頼と解析済みの試合は数えない。

理由: 追加の基盤(DynamoDB や WAF)なしで上限をかけられ、同時実行5本の制限と合わせて費用と Epic への負荷に天井ができる。

2026-10-16: 処理は AWS Lambda、公開は analytics.fortrank.com、バケットは fortrank と分ける

背景: 多目的サーバー(Lightsail Nano 512MB)では、リーダーで 130〜170MB のリプレイを読むメモリ(約400MB)が足りない。fortrank.com は S3+CloudFront の静的配信だけで計算するサーバーが無い。

決定: 個人の AWS アカウントに CloudFront(analytics.fortrank.com)・S3(新しいバケット fortrank-analytics)・受付用と解析用の Lambda を CloudFormation で作る。解析用は 2GB・同時5本まで、試合データは1回作ったら配るだけ。

理由: 費用は解析する試合の数にだけ比例し(見る人の数には比例しない)、自分用の範囲ならほぼ無料枠。大会後の集中も並列で捌ける。既存の fortrank バケットは配布アプリに埋め込まれたキーで書き込める可能性があるので、ビューアを書き換えられないよう分けた。

2026-10-16: Web 版のリプレイビューアとして作り、最初は自分用にする

背景: 引き継ぎ.md の調査で、マッチIDだけで全員の位置(約10秒おき)・撃破・安地が取れると分かった。

決定: マッチIDを入れると、下のシークバーでマップ上の全員の動きを再生する Web 版を作る。最初は自分用(Mac のローカル)から始める。

理由: ユーザーの想定が Web 版で、位置が約10秒おきでも移動の流れ(降下・漁り・ローテーション)は十分に追える。非公式 API と Epic の公開クライアント資格情報を使うので、公開範囲は小さく始める。

DEVLOG(作業ログ)

開発日誌

このプロジェクトでの作業を時系列で記録する。 最新のエントリが上に来る。


2026-10-21

19:52 - 利用状況と費用の調査、リプレイを S3 に残すのをやめる、試合中の LIVE の札

やったこと:

  • 閲覧の負荷を見積もった。解析済みの試合は CloudFront から JSON を配るだけで、Lambda は動かない。1人が最初に開くと約2MB(9割がマップの画像)で、1万人でも約20GB(CloudFront の常時無料枠 月1TB の2%)
  • CloudFront の利用履歴を CloudWatch と Cost Explorer で見た。3つのディストリビューションとも、作ってからずっと0ドル
    • fortrank.com は定額プランの Free で動いていて、Cost Explorer では「CloudFront Flat-Rate Plans」という別のサービス名で記録される
    • analytics.fortrank.com は従量課金で、常時無料枠の中
  • analytics.fortrank.com のアクセスログ(10/17 12:06〜10/21 17:56、8,637件・62MB)を集計した
    • 件数の75%は脆弱性スキャン(/.git/config・/.env・/.aws/config など)。すべて 404・403・301 で、何も漏れていない
    • 転送量の72%はマップの画像(24回で44.6MB)
    • 人のアクセスのうち85%、解析の開始の91%(68回中62回)は You 以外だった。内訳は、同じ回線の Windows の人(46回)、一緒にプレイした人(IPv6 の Chrome)、10/17 22:15 ごろに貼ったリンクから来た人(Edge の5つ)
  • 新しい解析の費用を、実測(Lambda の実行時間・S3 の保存量・実際の請求)から試算した
    • 前の仕組み: 1件約0.0024ドル(計算51%・リプレイの保存39%・リクエスト10%)。今の上限いっぱい(月14,400件)で月約29ドル
    • 試合中の表示: 作り直し1回約0.00045ドル、1試合を約20分追いかけると約0.018ドル(通常の解析の約7倍)。天井は、3試合を24時間ずっと追いかけた場合で月120〜140ドル
  • S3 にリプレイを残すのをやめた(f032100)。bash infra/deploy.sh で本番に反映した
    • worker.py は、リプレイ・メタデータ・リーダーの出力を replays/ に上げず、読みにもいかない。解析用 Lambda の replays/ の権限も外した。replays/ の7日の規則は、残っている分が消えるまで残す
    • 本番で lambda invoke を2回動かして確かめた。試合データは前と完全に同じで、replays/ には書き込まれていない
    • 最初から解析すると(274ファイル・145MB)13.2秒、使用メモリは1,020MB(割り当ては2GB)
    • 試算をやり直した: 1件の Lambda は約0.0004ドル(15秒)〜0.0012ドル(45秒)で、無料枠に収まるのは月約4,400〜13,000件。今の上限いっぱいでも月約2.4〜14ドル
  • 試合中の表示の見た目を4案作り、You が D 案(2行の LIVE の札)を選んだ。マップの左上に出すようにして(2c7b017)、bash infra/deploy.sh viewer で本番に反映した
    • 試合中は、赤い点が点滅して「LIVE リアルタイム更新中」、2行目に「約5分遅れ・30秒ごとに更新」と出す
    • 混んでいて更新を止めたときは灰色の「更新を止めました」と理由。試合が終わったら消える
    • タイトルは「…・試合中」だけにした(遅れの説明と止めた理由は札に出す)
    • ローカルで、ブラウザの中だけ通信を差し替えて、試合中・停止・終了の3つの状態を確かめた。本番のファイルに入ったことも確かめた

決めたこと: DECISIONS.md に2件(クラウドではリプレイを S3 に残さない、試合中の表示はマップ左上の LIVE の札にする)

詰まったこと / 気づき:

  • アクセス元を IP だけで分けると間違える。同じ回線(1つの IPv4)に、You の Mac・iPhone・Claude アプリ・curl と、別の人の Windows が同居していた。UA と合わせて分ける必要がある
  • S3 のリプレイは、実際にはほぼ読まれていなかった。試合データができたあとは受付用 Lambda が解析済みと返すので、読まれるのは試合データを手で消したときだけ
  • 最初からの解析が13.2秒で、前の実測(40〜51秒)よりずっと短かった。理由は未確認(Epic からのダウンロードの速さが時間帯で違う?)。1回しか測れていない
  • デプロイ用のユーザー Ichiro は CloudWatch Logs を読めない(FilterLogEvents の権限が無い)。1回ごとの時間とメモリは、lambda invoke --log-type Tail で返るログの末尾(REPORT 行)で見られる
  • 試合中に追いかけた試合は live/ にファイルが1日残り、試合後の解析はそれを使う。最初からの解析の時間を測るときは、live/ に何も無い試合を選ぶ
  • Cost Explorer の API は1回0.01ドルかかる(今回は約5回)
  • 試合中の表示の費用の天井(月120〜140ドル)が、今いちばん大きな費用のリスク。通常の解析の天井(月約2.4〜14ドル)よりずっと大きい

次回やること:

  • 本物の試合中の試合で、LIVE の札が出るか確かめる
  • 試合中の表示の費用の天井を下げるか決める
  • リロードの試合2件の再解析を決める(Epic から取り直す。Epic がリプレイを消す 10/23〜30 ごろまでに)

2026-10-20

03:35 - 収益化の相談、非公式の表示、名前の札のチカチカ対策、点の大きさと拡大の上限、英語の返答への対策

やったこと:

  • 収益化を biz と corporate の expert に相談した
    • biz: 広告は第一の選択肢ではない(見込みは月数千円)。今は収益化せず、利用状況を測るのが先。将来の本命はスクリム主催者向けの有料、つなぎは寄付
    • corporate(Epic の公式ページの英語原文で確認): ファンコンテンツのポリシーは「個人的で非営利」が条件で、サイトの広告・寄付・有料の例外は無い。利用規約は、別契約がない限り商業利用(広告・収益化)を禁じる。規約上の安全な順は、無料>寄付>広告>有料。借りた資格情報で内部 API を叩く問題は、無料でも残る
  • 画面のいちばん下に「fortics はファンが作った非公式のツールです。Epic Games とは関係がありません。マップ画像などの権利は Epic Games, Inc. にあります。」を常に出す(index.html・style.css)
  • 名前の札のチカチカを減らした(GPT との三者議論。記録は .devnotes/discussions/2026-10-19-name-label-flicker.md)
    • 毎フレーム前のことを覚えずに置き直していたのが原因。札ごとに前の方向を覚え、移るときと出し直すときだけ 3px の余白を求める
    • 出し消しを 0.15秒のフェードにし、消えていた札は同じ場所が 0.12秒続けて空いてから出す
    • カーソルを合わせた人の札は、ほかの札を組み替えずに一番上に描く。シークと試合を開いたときは置き直す
    • 重なりを許す・味方の札をまとめる案も試し、最後に「同じチームの札とバスの絵だけを避ける(別のチームとは重なってよい。順位の良いチームが上)」を普通の表示にした
    • 測り方: 札を置くコードを取り出して Node で同じ条件(同じ試合・画面・速さ・60fps)で動かし、方向の変化・出し消し・重なりを数えた。デュオ58人・×5・地図全体で、出し消しは1分に 350 → 13 回、味方どうしの重なりは 0%
  • 点と×は拡大するほど小さくした(地図全体で半径 5.5px、拡大率の 0.15 乗分の1、最小 4px)。撃破の輪・札の文字・バスの絵は変えない
  • 拡大の上限を「地図全体の14倍」か「地図の画像の 1px が画面の 6px」の大きいほうにした(小さい画面でも拡大できるように)
  • 3b5b3a0 でまとめてコミットし、bash infra/deploy.sh viewer で本番に反映した。本番の app.js・index.html・style.css が手元と同じことをハッシュで確かめた
  • Opus 5.5 が日本語の指定を無視して英語で返す件を調べた(X の検索は xAI のログインが切れていたので、普通のウェブ検索で)。anthropics/claude-code に同じ報告が多数あり、公式の修正は見当たらない。英語の返答を見つけて日本語で書き直させる Stop hook を入れた(claude-config の a0c2ccf、~/.claude/settings.json の Stop)
  • 「リプレイから」で Windows のリプレイの保存場所を直接開けるか相談された。ブラウザからは AppData の中を指定して開けない。代わりの案(前に開いたフォルダを覚える・パスの案内・フォルダを一度選んでもらう)を出したが、今のままにする(You の判断)

決めたこと: DECISIONS.md に4件(今は収益化しない、非公式の表示の文面、名前の札は同じチームだけ避ける、点の大きさと拡大の上限)

詰まったこと / 気づき:

  • 重なりを全部許すと、チカチカはほぼ消えるが、地図全体では札の約半分が半分以上隠れて読めない。重なりの多くは、味方どうしより別のチームどうし
  • 味方の札を「A / B」の1枚にまとめると札は減るが、You の判断で却下(同じパーティーを1枚にするのは良くない)
  • 点を拡大に合わせて大きくする案を先に作ってしまったが、You の意図は逆(拡大するほど小さく)だった
  • プレビューのサーバーは、アプリがブラウザ画面を閉じたときなどに止める。ローカルで見てもらうときは、ターミナルのタブで python3 -I tools/serve.py 8765 を動かす
  • 試合を開くと URL が ?match=… に書き換わるので、ほかの URL の指定は読み込み直すと消える(実験用の指定で気づいた。今は実験用の指定は無い)
  • 英語を見つける hook は、ターンの途中の英語には反応したが、最後の報告の英語を見逃したことがある(記録への書き込みが間に合っていない可能性)

次回やること:

  • 英語を見つける hook が最後の返答を見逃す原因を調べる(タスクのカードを出した)
  • ローカルのリプレイで全員の位置が取れるか調べる(収益化を考え直す前提。タスクのカードを出した)

2026-10-19

16:20 - リプレイ取得の仕組みの確認と、Epic の資格情報を .env に移す

やったこと:

  • マッチIDからリプレイを取る流れを確認した。トークン → メタデータ → ダウンロードリンク → 全チャンク → .replay に組み立て → probe → build_match.py の順。クラウドでは api.py が worker の Lambda を非同期で呼び、worker が make_match.py を動かす
  • 借りている資格情報を調べた
    • Android 版 Fortnite アプリに入っている Epic のクライアント(fortniteAndroidGameClient)のもの。yuyutti のものではない
    • トークンには internal_client: true、client_service: prod-fn が付き、有効期限は4時間。アカウントには結びつかない
    • yuyutti の constants.js(★3)の値は 2026-01 から変わっていない。LeleDerGrasshalmi/FortniteEndpointsDocumentation(★484)と MixV2/EpicResearch(★426)にも同じ値が載っている
    • 認証なし、またはでたらめなトークンだと、メタデータの段階で 404(folder_not_found_or_not_allowed)になる。正しいトークンなら 200
  • 資格情報を yuyutti のリポジトリから読むのをやめ、リポジトリ直下の .env(git の管理外、権限 600)から読むようにした(a7cef5a)
    • 対象は make_match.py・scripts/fetch_meta.sh・infra/deploy.sh。値は一度だけ写した。書式の見本として .env.example(値は空)を追加した
    • 資格情報が断られたとき(HTTP 400・401)は、値が変わったかもしれないとエラーに添える
    • README のセットアップ手順から gh を外し、.env の作り方を書いた
  • 確かめたこと
    • .env の値で、トークン・メタデータ・ダウンロードリンクがどれも取れた
    • .env が無いときと空のときは、何をすればいいかが分かるエラーで止まる
    • でたらめな値だと HTTP 400 になる
    • .env の値が Lambda の環境変数の値と同じこと(ハッシュで比較)
    • デプロイは流していない

決めたこと: DECISIONS.md に1件(Epic の資格情報は .env に置く。a7cef5a で記録済み)

詰まったこと / 気づき:

  • 動いている Lambda は、yuyutti のリポジトリも .env も読まない(値は Lambda の環境変数にある)。変更前も、yuyutti のリポジトリが消えて止まるのは次のデプロイからだった。今回の変更は、デプロイとローカルでの取得をその依存から外すもの
  • Epic の正式な手段(Epic Online Services)の資格情報は Fortnite の内部サービスに届かない。試合やリプレイを返す公式 API も無い。資格情報を使わずに済む道は、ローカルのリプレイを丸ごと読むことだけ(全員の位置が入っているかは未確認)
  • 10/17 18:25〜10/19 01:13 のコミット6件(リプレイファイルからマッチIDを読む、通常マップの座標合わせの詰め直し、バスのピクトなど)は、別のセッションのもので DEVLOG に記録が無い

2026-10-17

13:43 - セキュリティ強化と GA4 導入、GPT の2回目のレビュー対応、解析の上限の見直し

やったこと:

  • セキュリティを強化した(7925f73)
    • CloudFront でセキュリティ用のヘッダー(CSP など7種類)を付け、ビューアからインラインのスタイル指定をなくした
    • S3 のうち CloudFront から読めるのを、ビューア一式と maps/・matches/ だけに絞った(jobs/・replays/・rate/ は読めない)
    • 同じアクセス元(IP)ごとの上限を追加した(CloudFront-Viewer-Address の IP を、秘密の値を鍵にしたハッシュにして数える)
    • CloudFront のアクセスログを別バケットに残す(90日)。関数 URL は CloudFront 経由の呼び出しだけを許可。エラーの詳細は画面に出さずログにだけ残す
    • AWS Budgets で予算とメール通知を設定した(Chrome で操作)
    • fort-rank の配布アプリに埋め込まれたキー(fortrank-uploader)の権限を確認した → s3:PutObject on fortrank/*。今のままにすることにした
  • GA4(G-NT6S034ES8)を入れた(viewer/analytics.js)。本番だけで読み込み、URL のクエリを外して送る。試合の読み込み結果・再生・速度変更などをイベントで送る
  • Codex(GPT)に2回目のレビューを依頼し、指摘を直した(563997b)
    • 本番の通信を確かめると、GA4 の拡張計測(フォームの操作・履歴でのページ変更)が ?match= 付きの URL を送っていた → フォームの送信先を / に固定し、GA4 の管理画面で2項目をオフにした。本番で送られないことを確認
    • 同じ試合への同時依頼で解析の枠を余分に使う → 先に枠を取り、ジョブを書けなかった依頼は枠を返す
    • worker の標準エラー(traceback など)が公開の進捗に混ざる → 標準エラーはログにだけ流し、公開する理由は「エラー:」の行だけにした
    • worker の起動・準備に失敗すると20分「解析中」のまま残る → エラーにして枠を返す
    • 通常マップを読めないとビューアが止まる・試合を素早く切り替えると前のマップが重なる → マップの読み込みと表示への反映を分けた
    • IPv6 は /64 ごとに数え、X-Forwarded-For は使わない
    • 受付 Lambda の同時実行数を20までにした
    • AWS を使わない模擬テスト(21項目)・ローカル・本番で確認した
  • 上限を見直した: 全体は1時間10件から20件に、1人は3件から5件に。リプレイの保存は30日から7日に

決めたこと: DECISIONS.md に4件(GA4 の計測範囲、上限とリプレイの保存期間、確認用 API の守り方、fort-rank のキーは今のまま)

詰まったこと / 気づき:

  • aws cloudformation deploy は、渡さなかったパラメーターを前回の値のまま使う。テンプレートの既定値を変えても反映されないので、上限は deploy.sh から明示的に渡すようにした
  • 解析の所要時間は8〜46秒(取得済みのリプレイを使うと短い)。リプレイは1件50〜180MBあり、費用でいちばん大きいのはその保存だった
  • GA4 の gtag("set", {page_location}) では、拡張計測の履歴でのページビュー(dl・dr)とフォーム操作(form_destination)の URL は変えられない
  • クラウドにあるリロードの試合2件(d1ce0a53…・ed602a4f…)は、リロード対応の前に作ったデータのまま(マップの種類が無い、または ts)
  • 起動時は、通常マップの画像を読み終えてから ?match= の試合を読み始める(大きい画像のデコードで数秒かかる)
  • ユーザー Ichiro には logs:FilterLogEvents の権限も無く、Lambda の実際の課金時間を確かめられなかった
  • GA4 のプロパティは、個人の Gmail ではなく会社の Google アカウント(ramanu.co.jp)側にある

次回やること:

  • クラウドの古いリロードの試合2件を解析し直すか決める(S3 のリプレイが消える 10/23 ごろまでに)
  • Surf City・Slurp Rush の本番の試合、ゼロビルドのタイトル、スクショの作り直し(前回からの持ち越し)

2026-10-16

20:29 - マッチIDから全員の動きを再生する Web ビューアを作り、analytics.fortrank.com で公開、リロード対応まで

やったこと:

  • 引き継ぎ.md(サーバーリプレイの調査結果)を読み、Web 版のリプレイビューアとして作り始めた
  • ビューア(viewer/。ビルド不要の HTML/JS/CSS、Canvas 描画)
    • マップ上に全員の位置(約10秒おきの点を直線補間)、軌跡、撃破・ダウン位置、安地(縮小中の円・次の安地)、キルフィード、チームの生存状況を、下のシークバーに合わせて再生
    • 名前の札(重なりを避けて間引く)、安地の段階表示(例: 4 / 12 と残り時間)、キルフィードの撃破・ダウンのマーク、バトルバスの航路(流れる点線とバス)、アイコン、タイトル(日時・リロード/バトルロイヤル・ゼロビルド・モード・人数)
    • 解析中は「解析中」と目安の進捗バーだけを出し、終わったら ×5 で自動再生
  • 取得〜試合データ作成(tools/)
    • make_match.py: マッチID → トークン → メタデータ → 全チャンク取得 → .replay 組み立て → probe(Full)→ build_match.py。ロック・一時ファイル経由の書き込み・制限時間・進捗出力つき
    • build_match.py: 撃破・位置・安地・蘇生・退出・バスの航路・マップの種類と地名の位置を試合データ JSON にまとめる。世界時間とリプレイ時間の差は撃破イベントとキルフィードの突き合わせで求める
    • fetch_map.py: fortnite-api.com から通常マップの画像を取り、地名ラベルの位置で座標合わせ(誤差 7.5px)
    • serve.py: ローカル配信と取得の受け付け(URL の形はクラウドと同じ)
  • Codex(GPT)にコードレビューを依頼し、指摘12件(配信範囲の抜け道、書きかけファイル、退出の扱い、リブート後の軌跡、読み込みの競合など)と、異常終了・制限時間の改善を直した
  • クラウド版を AWS に構築(infra/stack.yaml、infra/deploy.sh、cloud/)
    • analytics.fortrank.com(CloudFront)→ S3 バケット fortrank-analytics、/api/ は受付用 Lambda(関数 URL)、解析用 Lambda は非同期・同時5本まで
    • ACM 証明書(us-east-1)を作るため、ユーザー Ichiro に証明書だけを扱う権限を追加(ユーザーの許可を得て)
    • 当初は解析の鍵を付けたが、鍵なしにして新規解析をサイト全体で1時間10件までにした
  • リロード対応
    • マップの種類を判別(通常マップは br、リロードはプレイリスト名のコード名: Surf City=PiperBoot、Nitemare Island=SourSpawn、Slurp Rush=DashBerry)。画像が無いマップはマス目と地名で描く
    • 3マップの画像を、ユーザーのゲーム内スクショから加工(安地の円・バスの航路の矢印・ピン・画面表示を消して切り抜き)し、座標を合わせた(tools/maps/)
  • 取得した試合: 通常マップ(デュオのカスタム、ソロの公式大会など)とリロード(Nitemare の本番の試合)。いずれも表示を確認

決めたこと: DECISIONS.md に8件(Web・自分用から開始、Lambda と分離バケット、鍵なし+1時間10件、解析中は中身を見せない、マップごとの画像と座標合わせ、スクショ加工と ActorsPosition での座標合わせ、バスの航路は推定、勝利後の退出は撃破に数えない)

詰まったこと / 気づき:

  • 世界時間とリプレイ時間の差は試合ごとに違う(44〜996秒)。撃破が1件も無い試合は差を求められず、試合データを作れない
  • サーバーリプレイは1か月前の試合でも取れた(保存期間は1〜2週間より長い可能性)
  • リーダーはバスの航路を返さない。バスの高さ(通常マップ 79,850cm、リロード 41,002cm)の点に直線を当てはめると、速さ約75m/s・ずれ中央値20m で求まる
  • 試合の始めにリプレイに入る ActorsPosition(宝箱などの位置、マップごとに固定)が、マップ画像の座標合わせに使える
  • マップごとに画像の向きが違う(通常マップと Nitemare は右が +X、Surf City は右が -Y、Slurp Rush は右が +Y)
  • window.prompt はアプリ内ブラウザ(Electron)で出ない → 入力欄はページ内に作る
  • ユーザー Ichiro には logs:PutRetentionPolicy・FilterLogEvents の権限が無く、Lambda のログの保存期間は未設定・中身も読めない
  • fort-rank の配布アプリ(S3Config.cs)に AWS のアクセスキーが埋め込まれている(fortics とは別件。fortics はバケットを分けて影響を避けた)

次回やること:

  • Surf City・Slurp Rush の本番の試合(人数あり)で表示を確認
  • ゼロビルドの本番の試合でタイトル表示を確認
  • 地名に円や矢印がかからないスクショで、Surf City・Nitemare の画像を作り直す(文字のかすれ)

最近のコミット

README

fortics

概要

Fortnite のサーバーリプレイ(マッチIDだけで Epic から取れるリプレイ)を取得・解析する。調査の経緯と分かったことは 引き継ぎ.md を参照。

セットアップ

  • jq、curl、python3
  • .env(Epic の資格情報。git の管理外): .env.example を .env に写し、FORTICS_EPIC_CLIENT_ID・FORTICS_EPIC_CLIENT_SECRET に Android 版 Fortnite の公開クライアント(fortniteAndroidGameClient)の値を入れて、権限を 600 にする。値は LeleDerGrasshalmi/FortniteEndpointsDocumentation の EpicGames/AccountService/Authentication/Clients.md などに載っている。make_match.py・scripts/fetch_meta.sh・infra/deploy.sh がここから読む。Epic が値を変えると、トークンの取得が HTTP 400 か 401 で失敗するので、新しい値に書き換える
  • dotnet 8 SDK(probe/ で FortniteReplayReader を使う場合)
  • Pillow(tools/fetch_map.py でマップの座標合わせをする場合)

ビューア

マッチIDを入れると、マップ上で全プレイヤーの動きをシークバーで再生できる Web ページ。クラウド版(https://analytics.fortrank.com/ 。下の「クラウド版」)と、Mac のローカルで動かす版がある。どちらも URL の形は同じ(/ がビューア、/maps/・/matches/ がデータ、/api/ が API)。

  • マッチIDの代わりに、ローカルのリプレイファイル(%LOCALAPPDATA%\FortniteGame\Saved\Demos の .replay)をページにドロップするか「リプレイから」で選んでも開ける(viewer/replay.js)。ファイルはサーバーに送らず、ブラウザで先頭の最初のデータ(数百KB)だけを読む。マッチIDは試合本体のデータ(AES-256 で暗号化し、Oodle で圧縮したもの)にあるが、暗号だけを解けば圧縮を解かなくてもCK-xxxx|<32桁> の文字列がビット単位でずれて残っているので、0〜7ビットずらして | のあとの32桁を探す。暗号の鍵はファイルの見出しに入っている。正式な読み方ではないので、Fortnite の更新で見つからなくなることはありうる(そのときは手入力に戻す)。手元のカスタムマッチ10本で、リーダーの GameSessionId と一致することを確かめた(公式大会のリプレイは未確認)。いくつか入れると録画日時の一覧から選ぶ。ローカルのリプレイの録画日時は PC の時計の時刻が UTC として入っているので、画面の時間帯の時刻として読み直す
  • 位置は約10秒おきの点を直線でつないでいる。点・軌跡(直近45秒)・安地(縮小中の円と次の安地)・撃破/ダウンの位置・キルフィード・チームごとの生存状況が、再生位置に合わせて変わる
  • 点(倒された人の×も)は、拡大するほど小さくなる。地図全体を表示したときがいちばん大きく半径 5.5px で、拡大率の 0.15 乗分の1 にし、半径 4px で止める。撃破の輪・名前の札の文字・バスの絵は大きさを変えない
  • 名前の札は、同じチームの札と重ならない場所(右・左・上・下の順)に置く。別のチームの札とは重なってもよく、重なったところは順位の良いチームの札が上にくる(全部の札を避けると、人が集まった所で札が消えたり飛んだりしてチカチカするため)。チカチカしないよう、人ごとに前に置いた方向を覚えておき、重ならなければ動かさない。別の方向へ移るとき・また出すときだけ、まわりに 3px の余白を求める。出すとき・消すときは 0.15 秒かけて濃く・薄くし、消えていた札は同じ場所が 0.12 秒続けて空いてから出す。シークしたときと試合を開いたときは、一から置き直す。カーソルを合わせた人の札は、ほかの札の配置を変えずに一番上に重ねて出す
  • バトルバスの航路と、その上を進むバスを出す。航路は、通った所を薄い実線、これから通る所を暗い下地の上の淡い青白の破線と、まばらな「›」で描く(線は流さない。白い点線は次の安地と紛らわしいので使わない)。バスは水色の風船の下に紺の車体を描いたピクト(いつも正立)で、進む向きは横の小さな三角で出す。大きさは拡大しても変えず、名前の札はバスの絵を避けて置く。航路はリーダーが返さないので、バスの高さにいる位置に時刻の一次式を当てはめて求めている(速さは約75m/s で一定、ずれは中央値で約20m)。バスの高さはマップで違う(通常マップ 79,850cm、リロード 41,002cm など)ので、全員の最初の位置の高さでいちばん多いものを使う。バスが島を出てから60秒で消える
  • GA4(測定 ID G-NT6S034ES8、viewer/analytics.js): analytics.fortrank.com で開いたときだけ読み込み、ローカルでは送らない。送る URL からクエリ(?match=)を外し、マッチIDや名前は送らない。Google シグナルと広告のパーソナライズはオフ。画面にお知らせは出さない(GA の利用規約上、プライバシーポリシーなどで GA の利用を書いておく必要がある)
  • GA4 のイベント: match_load(result = cached・analyzed・limited・not_found・no_elims・error・invalid_id など、成功時は map・game・mode・zero_build・players・wait_sec)、playback(action = play・pause。自動再生は送らない)、speed_change、team_focus、killfeed_click、toggle(target = names・trails)、replay_open(result = found・not_found・not_replay・unsupported・broken・error、files・found は数)
  • マップはマップの種類ごとに分けている。リロードはプレイリスト名の _RE_ の後ろのコード名で見分ける(Surf City は piperboot、Nitemare Island は sourspawn。例: Playlist_Showdown_RE_PiperBootNoBuildSolo_CTS)。それ以外はキルフィードの場所のタグ Athena.Location.POI.<種類>.<地名> の種類で見分ける(通常マップは Generic → br)。地名の位置は、そのタグが付いた撃破位置の中央値で見積もって試合データの map.pois に入れる。プレイリスト名かルールに NoBuild があればゼロビルド(zeroBuild)として、タイトルに出す
  • マップ画像は data/maps/<種類>/ に calib.json と画像を置く。calib.json は image、width・height(正方形なら size)、scale(px/cm)と、向きが通常マップと同じなら originX・originY、違えば matrix(px = aX + bY + c、py = dX + eY + f)
  • 今ある画像: 通常マップ(br。fortnite-api.com から取得)と、リロードの3マップ(Surf City piperboot、Nitemare Island sourspawn、Slurp Rush dashberry。コード名はプレイリスト名から。Slurp Rush は Playlist_Showdown_RE_CTS_DashBerry_Duos の形)
  • リロードの画像は、ゲーム内の全体マップのスクショから作った。tools/maps/clean_screenshot.py がバスの航路の矢印・位置のピンを消して切り抜く(Surf City は安地の円などを消す clean_surfcity.py)。消した所はまわりの色で埋めるので、矢印や円が地名にかかっていた所は文字が少しかすれる
  • 向きはマップごとに違う。通常マップと Nitemare Island は「画像の右がゲーム内の +X、下が +Y」、Surf City は「右が -Y、下が +X」、Slurp Rush は「右が +Y、下が -X」
  • 座標合わせ: 試合の始めにリプレイに入る物の位置(ActorsPosition。宝箱などおよそ400〜500件。マップごとに固定)の塊と、画像の地名ラベルを対応づけて向きと初期値を決め(tools/maps/fit_labels.py。Surf City は fit_actors.py)、物(と選手)の位置が陸地に乗る割合が最大になるよう詰めた。Surf City はスクショに写った安地の円を、その試合の最初の安地に合わせて決めた。どれも物の位置が建物に乗ることを目で確かめ、Nitemare Island は本番の試合(39人)の撃破位置から見積もった地名がラベルと重なることも確かめた
  • 通常マップの座標合わせ: fetch_map.py が地名ラベルで当てはめたあと、tools/maps/refine_calib.py で物の位置が建物・道・陸地に乗る割合が最大になるよう倍率と原点を詰め直す。ラベルは地点の真上に描かれているとは限らず、ラベルだけだと倍率で約3%小さく、原点が約12px右にずれていた(2026-10-18。着地した建物の右上に点が出ていた)。詰め直すと、海の上に乗る物の位置が約半分になり、選手が着地した建物の上に点が乗ることを確かめた。詰め直した結果は calib.json の refinedBy に残る
  • スクショは、マッチIDを送る試合の最中に撮ると、その試合のバスの航路や安地とも突き合わせられる。画像の無いマップは、地図の代わりに 100m ごとのマス目と地名を描き、位置関係だけ正しく出す
  • 試合中の試合も開ける(ローカル版もクラウド版も。クラウド版の上限は下の「クラウド版」)。Epic のサーバーには試合中もリプレイが少しずつ上がるが、出てくるのは約5分遅れ(メタデータの DesiredDelayInSeconds が300。試合が始まって5分ほどはメタデータ自体が無く、その後も遅れは5分5秒〜5分22秒で一定だった)。試合が終わると、残りの分もすぐに出る。ビューアは30秒ごとにサーバーに作り直しを頼み、再生位置・再生中か・強調しているチームを保ったまま差し替える。同じ試合を何人が見ていても、サーバーは作り直しを1つずつしか動かさず、前回の完了から20秒たつまでは新しく始めずに今ある試合データを返す(serve.py の LIVE_MIN_GAP_SEC。Epic のデータは約20秒ごとにしか増えないため)。試合中は最後まで再生しても止めず、次のデータが届いたら続きを再生する。マップの左上に LIVE の札(赤い点の点滅・「リアルタイム更新中」・「約5分遅れ・30秒ごとに更新」)を出し、タイトルにも「試合中」と付ける。順位の無いチーム(まだ残っている)は順位を「-」にしてチームの欄の上に出す。試合が終わると完成版に差し替わり、更新をやめる。前に作った試合中の試合データ(開いた人がページを閉じて、そのまま試合が終わったものなど)を開いたときは、古いまま出さずに、作り直してから出す(試合が終わっていれば完成版になる)。リプレイの保存期間(1〜2週間)を過ぎるまで誰も開かなかった試合は、作り直せず試合中のまま残る
  • 開いたままのタブで別の試合を開くと(入力欄・リプレイファイル)、ビューア(app.js)がデプロイで変わっていないかを確かめ(本番は ETag、ローカルは更新日時。起動したときの値と比べる)、変わっていればページを読み込み直してからその試合を開く。2026-10-20 に、前の日から開いていたタブで試合中の試合を開き、古いビューアのまま30秒ごとの更新が動かなかったため
  • チーム名かマップ上の点をクリックすると、そのチームだけを強調する。キルフィードをクリックすると、その3秒前に飛ぶ
  • 操作: スペースで再生/一時停止、←→で5秒(Shift つきで30秒)移動、ホイールで拡大、ドラッグで移動、ダブルクリックで全体表示
  • 拡大は、地図全体の14倍か、地図の画像の 1px が画面の 6px になるまでの大きいほうまで(小さい画面でも同じくらいまで拡大できる)。画像の 1px は通常マップで約1.46m なので、大きく拡大すると地図はぼやける
# 1. 通常マップの画像と座標合わせ(初回と、シーズンが変わったとき)→ data/maps/br/
python3 -P -E tools/fetch_map.py
#    続けて、そのシーズンの通常マップの試合のリプレイ(make_match.py が組み立てたもの。何本でもよい)で詰め直す
python3 -P -E tools/maps/refine_calib.py data/maps/br data/replays/<id>.replay --write

# 2. サーバーを起動して http://localhost:8765/ を開き、マッチIDを入れる
python3 -I tools/serve.py

まだ無い試合は、サーバーが tools/make_match.py を動かして取得から試合データ作成まで行う(1試合 約130MB・40秒前後)。画面には途中の段階は出さず、「解析中」と目安の進捗バーだけを出す(make_match.py --progress が出す 0〜100 の数値を使う。時間のほとんどはダウンロードなので、そこに 5〜80% を割り当てている)。解析が終わると自動で再生が始まる。コマンドだけで作ることもできる:

python3 -I tools/make_match.py <id>

make_match.py の流れ:

  1. トークン → メタデータ → 全チャンクのリンク
  2. 全チャンクのダウンロード → .replay に組み立てる
  3. probe(Full モード)で解析
  4. build_match.py で試合データを作る

できるもの(すべて data/ 配下で git の管理外):

  • data/replays/<id>.replay: 組み立てたリプレイ。あれば再取得しないので、パーサーを直したら data/matches/<id>.json を消して開き直せば作り直せる
  • data/replays/<id>.meta.json: Epic のメタデータ
  • data/replays/<id>.reader.json: probe の出力
  • data/matches/<id>.json: ビューア用の試合データ
  • data/replays/<id>.lock: 同じ試合を同時に作らないためのロック(中身は空。消してもよい)

--live をつけると(serve.py は付ける)、まだ終わっていない試合でも、その時点で出ている分から試合データを作る(試合データに "live": true が付く)。途中までのリプレイは data/replays/ に置かず(置くと、試合後に開いても続きを取り直さなくなる)、data/live/<id>/ に置く:

  • data/live/<id>/chunks/: 取ったファイル。2回目からは増えた分だけ取る(header.bin だけは毎回取る)。試合後の取得でもここにある分は使い、完成版ができたら消す
  • data/live/<id>/<id>.replay・.reader.json・.meta.json: その時点までのリプレイと解析結果(完成版ができたら消す)

ファイルは一時ファイルに書いてから置き換えるので、途中で止まっても書きかけのものは残らない。同じ試合を別の処理が作っている間は、後から始めたほうがエラーで止まる。各段階には制限時間がある(解析 600秒、試合データ作成 120秒。サーバーから動かしたときは全体で15分)。

serve.py が配るのは viewer/ と data/maps/・data/matches/ の中だけ(URL をデコードして ../ を解決したあとの実際のパスで判定する。フォルダの一覧は出さない)で、127.0.0.1 でだけ待ち受ける。

fetch_map.py について:

  • 画像は map-<中身のハッシュ>.png という名前で置き、最後に calib.json を置き換えて切り替える。途中で失敗しても、画像と座標合わせの組み合わせは崩れない
  • 地点を対応づけられないか、ずれが 15px を超えるときは入れ替えない。map_en.png を目で見て問題なければ --force をつけて実行し直す
  • ラベルだけの座標合わせは数%ずれるので、そのあと必ず refine_calib.py で詰め直す(--write を付けなければ結果を出すだけ)
  • Pillow がユーザーの site-packages にあるため -I ではなく -P -E で動かす(スクリプトのフォルダやカレントディレクトリからは読み込まない点は同じ)

試合データの判定:

  • 時刻のずれ(リーダーが返す世界時間とリプレイ時間の差。安地と蘇生の時刻に使う)は、撃破イベントとキルフィードの突き合わせで求める。撃破が無いとき(人間が自分だけのカスタムなど)は、次の安地が出た時刻と前の縮小が終わった時刻の差で求める(終わった10試合でどれも ±0.3秒以内、撃破から求めた値とは0.2秒以内で一致)。どちらも無いと、完成版は作らない。試合中は、安地と蘇生を出さずに作る(最初の縮小が終わるまで)
  • 試合中は、順位の無い人(まだ残っている人)も入れる。チームの順位は、順位の無い人が1人でもいれば無し。チームの並びは、更新しても色が変わらないようにチームの番号順にする。再生の長さは、位置の点がある所まで(位置は約40秒ごとにまとめて届くので、データの最後より少し前で止まっている)
  • 退出(キルフィードの DeathCause.LoggedOut)は「退出」と表示する。優勝チームが勝ったあとに退出したものは撃破に数えない
  • リブートで復帰した人は、倒されてから次の位置が出るまで×で表示し、復帰後の動きはそのまま残す

クラウド版(analytics.fortrank.com)

AWS の個人アカウント(プロファイル ichirokisanuki、東京リージョン)に置いている。構成は infra/stack.yaml、デプロイは infra/deploy.sh。

ブラウザ ── analytics.fortrank.com(CloudFront)
              ├─ /  /maps/  /matches/  ── S3 バケット fortrank-analytics(CloudFront からだけ読める)
              └─ /api/  ── 受付用 Lambda fortics-api(関数 URL)
                               └─ 非同期で呼ぶ ── 解析用 Lambda fortics-worker(2GB、同時5本まで)
                               └─ 非同期で呼ぶ ── 試合中の作り直し用 Lambda fortics-live-worker(中身は同じ。同時2本まで)
  • 試合データが無いと、ビューアが /api/ に解析を頼む(誰でも頼める)。受付用 Lambda は jobs/<id>.json を「無いときだけ書く」で作るので、同じ試合に同時に依頼が来ても解析は1回だけ
  • 新しい解析はサイト全体で直近1時間に20件まで、同じアクセス元(IP)からは10件まで(infra/deploy.sh の RATE_LIMIT・PER_IP_LIMIT)。始めた時刻と IP のハッシュ(秘密の値を鍵にした先頭16桁。IP そのものは残さない)を rate/starts.json に記録して数え、超えたら「時間をおいてから、もう一度アクセスしてください」と返す。解析中の試合をもう一度頼んだときや、解析済みの試合を見るときは数えない。IP は CloudFront が付ける CloudFront-Viewer-Address から取る(/api/ へはこのヘッダーだけを渡す)
  • 解析用 Lambda は tools/make_match.py を動かし(解析ツールは linux-arm64 の自己完結版を同梱)、進み具合を jobs/<id>.json に書く。終わったら matches/<id>.json を置く。組み立てたリプレイ(1試合150MB前後)は S3 に残さないので、作り直すときは Epic から取り直す(Epic がサーバーリプレイを残している1〜2週間のうちだけ作り直せる)
  • 同時に動く解析は5本まで(予約同時実行数)。あふれた依頼は Lambda の待ち行列で順番を待つ。失敗しても自動ではやり直さない
  • 試合中の試合: 最初の解析は普通の解析と同じく上限に数える。できた試合データはキャッシュさせず(no-cache)、S3 のメタデータに live=1 と試合開始の時刻(started-at)を付ける。ビューアが30秒ごとに頼む作り直しは、上限には数えないかわりに次の3つで絞り、試合中の作り直し用 Lambda(同時2本まで。LiveWorkerConcurrency)で動かす(cloud/api.py の LIVE_*)
    • 同じ試合は、前の作り直しから30秒あける(何人が見ていても1試合1つにまとまる)
    • 同時に追いかける試合は、サイト全体で3試合まで(rate/live.json に試合ごとに最後に頼まれた時刻を書き、2分以内のものを数える)。超えたら 429 と「今は試合中の表示が混んでいます。試合が終わってから開き直してください」を返し、ビューアは更新をやめ、LIVE の札を灰色の「更新を止めました」にして理由を出す
    • 試合開始から40分を過ぎたら、作り直しも新しい解析として数える(終わらない試合をいつまでも作り直さない。試合は長くても25分前後)
    • Epic から取ったファイルは live/<id>/chunks/ に足していき(一覧は live/<id>/chunks.json)、次の作り直しと試合後の解析ではそこにある分を取り直さない
    • 費用の目安(概算): 作り直し1回は約0.06円、1試合を最後まで追いかけて約2〜3円。天井は、試合中の作り直し用 Lambda が2本とも動き続けた場合で1日約700円
  • ビューアのファイルを足すときは、infra/deploy.sh の一覧と infra/stack.yaml のバケットポリシー(CloudFront から読めるファイル)の両方に足す。バケットポリシーが変わるので、viewer だけでなく通常のデプロイが要る
  • 受付用 Lambda は、CloudFront だけが付ける秘密のヘッダー(infra/.env.deploy の ORIGIN_SECRET。git の管理外)が無いリクエストを断る
  • Epic の資格情報は、デプロイ時に .env から読んで解析用 Lambda の環境変数に入れる
  • バケットの中身: ビューア(index.html など)、maps/<種類>/、matches/(ずっと残す)、live/(試合中に取ったファイル。1日で消える)、jobs/(30日で消える)、rate/(上限を数える記録)、_deploy/(Lambda の zip。7日で消える)。CloudFront から読めるのはビューアと maps/・matches/ だけ
  • ログは CloudWatch Logs の /aws/lambda/fortics-api・/aws/lambda/fortics-worker・/aws/lambda/fortics-live-worker(保存期間は未設定。デプロイするユーザー Ichiro に logs:PutRetentionPolicy の権限が無いため)
  • セキュリティ用の応答ヘッダー(CloudFront の fortics-security-headers。ローカルの serve.py も同じ内容): HSTS、CSP(読み込んでよい先は自分と GA4 だけ。インラインのスクリプト・スタイルとほかのサイトへの埋め込みは禁止)、nosniff、X-Frame-Options DENY、Referrer-Policy、Permissions-Policy、COOP。ビューアで HTML の style 属性やインラインのスクリプトは使わない(色はスクリプトから入れる)
  • エラーの文言: 利用者向けに書いた理由(「エラー: …」)だけを画面に出す。想定外の例外やリーダーなどの出力はログ(標準エラー・CloudWatch Logs)にだけ出す
  • アクセスログ: CloudFront の標準ログを非公開のバケット fortrank-analytics-logs(cloudfront/)に残し、90日で消える。スタックを消してもバケットは残る
  • 今の権限ではできないこと: AWS WAF、費用の上限アラート(AWS Budgets)、Lambda のログの保存期間。費用アラートは AWS コンソールで設定する
# デプロイ(初回も2回目以降も同じ)
bash infra/deploy.sh

# ビューアやマップだけ直したとき(Lambda と CloudFormation は触らない)
bash infra/deploy.sh viewer

# 手元で作った試合データをクラウドにも置く
aws --profile ichirokisanuki s3 sync data/matches/ s3://fortrank-analytics/matches/ --content-type application/json

調査時の手順(scripts/)

上の make_match.py ができる前の、調査時のスクリプト。1つずつ中間ファイルを残しながら動かす。<id> はマッチID、<dir> はデータの置き場所(例: data/a870840c)。data/ と out/ は git の管理外。

# 1. トークン → メタデータ → 全チャンクのリンク(meta.json / links.json)。リンクには期限があるので、落とす直前に実行する
bash scripts/fetch_meta.sh <id> <dir>

# 2. チャンクのダウンロード(header / events / checkpoints / data)
for k in header events checkpoints data; do python3 -I scripts/dl_chunks.py <dir> $k; done

# 3. .replay に組み立てる
python3 -I scripts/build_replay.py <dir> out/<id>.replay

# 4. FortniteReplayReader で読む(プレイヤー・チーム・順位・キル)。Minimal は FortRank と同じ設定
dotnet run --project probe -- out/<id>.replay Minimal out/<id>_minimal.json

# 5. イベントの自前解析
python3 -I scripts/parse_events.py <dir>                      # 撃破ログ・ストーム → <dir>/elims.json, zones.json
python3 -I scripts/parse_samples.py <dir> out/samples.json    # 約10秒おきの位置
python3 -I scripts/kills.py <dir>/elims.json out/<id>_minimal.json out/killlog.json   # 誰が誰を倒したか・キルの数え方

検証用: check.py / check2.py(順位・キルの整合性)、check_samples.py(位置と撃破位置の突き合わせ)。

試合中の遅れの記録: python3 -I scripts/live_probe.py <id> [--interval 秒] [--build]。一定の間隔でメタデータを取り、録画開始からの経過時間と、出ているデータの最後の時刻の差(遅れ)を1行ずつ出す。メタデータは data/live/<id>/meta-<時刻>.json に残す。--build をつけると、最初の撃破が出たところで途中までのリプレイから試合データを作ってみる(data/live/<id>/build-<時刻>/ に作る)。