まず背景から。これは私自身の『Boom Beach』プライベートサーバーで、クライアントのバージョンは 57.135。サーバー側に加えて、ブラウザ上で基地のレンダリングと編集を再現する webclient もある。クライアントはパッケージ内に一整套の csv/ を同梱している——建物、障害物、装飾、トラップ、词条、globals、さらには「この岩を除去するのに金貨がいくら必要で、木材がいくつ落ちて、司令部が何レベル必要か」まで全部そこに書かれている。
以前はこれらのものをエンジンとシェルの両方にそれぞれ写していた。ここ十日で写した部分を一つずつ消し、直接テーブルを読む形に変えて、番号は C35 から C68 まで並んだ。この記事はコミット順に復唱するつもりはなく、記録に値するいくつかの事だけを拾う:なぜテーブルを読まなければならないのか、「読んだかどうか」をどうやって赤くなるテストにするのか、そしてテーブルにないものをなぜ勝手に作ってはいけないのか。
手書きの代償は「一度写し間違える」ことではない
手書きの最悪の形は写し間違いではなく、正しく写した後にそれがすでに古くなっていることに誰も気づかないことだ。クライアントのテーブルの一マスが変わっても、エンジン側のコピーはそれに追随しない。その結果の現れ方は往々にして源から遠く離れている:水面の色が 1 違う、障害物の除去で金貨が引かれない、同じ装飾を百個置ける、しかもテストは全部緑。
今回洗い出した箇所はどれも典型的だった:
| 位置 | テーブル上の真値 | 手書き時の挙動 |
|---|---|---|
src/terrain.h | globals.csv 三枚の水面パレット、計 12 マス | ピクセル比較は「ある水面の色が 1 違う」としか報告しない |
removeAt | obstacles.csv の ClearCost 600…25000 | 削除がタダ、金貨は引かれず戦利品も落ちない |
| 装飾の設置 | decos.csv の MaxCount は 0/1/2 のみ | いくらでも複製でき、上限の通知が一切出ない |
| 攻撃パネル | buildings.csv 四列の状態効果 | 17 個のオブジェクト、78 レベル分の値があるのに、パネルは真っ白 |
そこで C44 以降はやり方を変えた:数値はテーブルからのみ来て、エンジンは足し算と切り捨てだけを行い、シェルは自分で数を作らない。唯一の例外はシードで、コメントに別途明記した。
まず「読んだかどうか」を判定可能にする
テーブルを読むこと自体は難しくない。難しいのは、次のセッションでこのマスを本当に読んだのかをどう知るかだ。C44 はパッケージ同梱の一枚の texts_patch.csv から始まった:6 行のうち 2 マスが非空で、そのうち一つの TID は texts.csv にそもそも存在せず、クライアントが表示できているのはひとえに patch のおかげだった。補完した後、生成物には照合できるものが一段加わった:
window.hbTextMeta.patch = { present: true, rows: 6, cells: 2, sha256: '3e815929…' }受け入れチェーンの TEXTS OK はこの二つの数と patch の sha256 を出力するようになった。C45 はさらに進み、tools/globals_check.py を書き、src/terrain.h に手書きしていた三枚の水面パレットを一マスずつ csv/globals.csv へと固定し、フィールドの順序も一緒に検証するようにした。
このチェーンはいま PARITY OK ×96、三本のブラウザ経路 413/413/366 ✓、ENGINE TEXTS OK は 27 本のソースと 949 個の文字列リテラルを走査し、非 ASCII は 0 でなければならない。これは「列を読み間違える」ことは防げないが、「読み忘れる」ことは防げる。
テーブルにないものは、そのまま写す、自分で作らない
C63 は最も難しいマスだった。StatusEffectType には Fire、Stun、SpeedBoost、Panic;SpeedBoost の四種類があり、四列はセミコロンで並列に区切られ、Barbed Wire_lvl1 の数値は -750(負数は減速を表す)。タイプ語は texts.csv の中に正確なマスがなく、速度加成 が一つだけ命中し、残りはゼロ命中だった。
ここではとても泥臭い決断をした:タイプ語はテーブル内の元の値を保持する、翻訳もせず、文も作らない。ヒント文もクライアントのあの一行 TID_STATBAR_HINT_BURN_DAMAGE(「目標に着火」)をそのまま写す。たとえそれが Stun にぴったりでなくても——クライアントのその位置はまさにこの文だからだ。
C61 はさらに問題をよく示している。障害物の除去にはクライアント側に一整套のフロー文言がある:TID_POPUP_CLEAR_OBSTACLE_TITLE、TID_AVAILABLE_LOOT、TID_CAN_NOT_CLEAR_OBSTACLE、さらには「hb.remove を呼ぶだけで、この四つの文は一つも使われていなかった。テーブルを読むように変えた後は、金貨の控除、戦利品への加算、要件のゲートはすべて行単位で行い、ClearTimeSeconds の九行が全部 0 でも、同じく読み、カウントダウンも並べない。
C68 の装飾上限は 0/1/2 の三档しかなく、No_Decos は 0、Speaker は 2。250 件の Unlockable は TRUE(解除すれば無料)、空欄の 11 件はちょうど BuildCost 付きの有料アイテムだ。シェルはいま現在の件数を数え、上限に達したら TID_BUILDING_CAP_REACHED をそのまま表示し、有料アイテムの設置では本当に金貨を引く。
一つのイースターエッグ:クライアント自身が TID 言語を残していた
C49 で csv/locales.csv を漁っていた時、言語リストのある行の名前がそのまま TID で、ShowTID = TRUE 以外は何も宣言しておらず、フォントも空だと気づいた。それを選ぶと、画面上のあらゆる単語は词条自身の名前になる——この「翻訳表」は texts.csv の第 0 列にあり、5,755 のデータ行、5,755 マスがすべて行名と等しく、クライアント自身が書いたものだ。
リポジトリはこれまで TID を「第 0 列の列名」としか扱っておらず、使ったことがなかった。いま ?lang=TID は 21 番目の同梱言語であり、言語選択にもそれが並んでいる。テストを書く時はこれを両側の対照として使う:同じページの ZH-HANS は依然として「建造」で、TID ページなら TID_BUTTON_BUILD であるべきだ。
代償
列をひとつ増やすたびに、エンジンの catalog、JSON キー、シェルのレンダリング、ホワイトリスト、そして少なくとも4本のリグレッションテストを同時に直し、そのうえでビルドマシンで一連の remote_acceptance.sh を最後まで走らせる必要がある。エントリのホワイトリストは26件から86件に増え、言語は20種類から21種類になり、425個のオブジェクトのうち422個がクライアントの TID と一致する。
しかし、その代わりに得られるのは別種のコミットメッセージだ。もはや「数値を正しく直す」ではなく、「この4列はこれまで読んでいなかったが、今回読むようにした。リグレッションテストをどう再実行するか」である。クライアントが更新されたら、新しい csv/ をコーパスに放り込み、赤くなったら直す——事実の源はこちら側にはない。手で書き写すからこそ理由が要る。
