To set the context: this is my own Boom Beach private server, client version 57.135. Besides the server, there’s a webclient that rebuilds base rendering and editing in the browser. The client ships its own complete set of csv/ — buildings, obstacles, decorations, traps, entries, globals — and even “how much gold it takes to remove this rock, how much wood it drops, what Headquarters level you need” is written in there.
These things used to be copied separately into the engine and into the shell. Over the past ten days I deleted the copied parts one by one and switched to reading the tables directly; the changes are numbered C35 through C68. This post isn’t a replay in commit order — it just picks a few things worth recording: why reading the table is non-negotiable, how to turn “did it actually read” into a test that goes red, and why you can’t invent things the table doesn’t contain.
The cost of hand-copying isn’t “getting it wrong once”
The worst form of hand-copying isn’t a mistake — it’s copying something correctly and then nobody knowing it has gone stale. A cell changes in the client table, the copy in the engine doesn’t move with it, and the symptom usually shows up far from the source: water color off by 1, obstacle clearing not charging anything, the same decoration placeable a hundred times — and the tests all green.
A few of the spots cleaned up this time are pretty typical:
| Location | True value in the table | Behavior when hand-copied |
|---|---|---|
src/terrain.h | globals.csv three water palettes, 12 cells total | a pixel comparison only reports “water in some region is off by 1” |
removeAt | ClearCost of obstacles.csv 600…25000 | free deletion, no gold charged, no loot dropped |
| Decoration placement | MaxCount of decos.csv is only 0/1/2 | duplicate at will, the cap is never shown |
| Attack panel | buildings.csv four columns of status effects | 17 objects, 78 levels carrying values, the panel is completely blank |
So from C44 onward I changed approach: values come only from the table, the engine only does addition and clamping, and the shell doesn’t make up numbers of its own. The only exception is the seed, which is flagged separately in a comment.
First, make “did it read or not” decidable
Reading the table isn’t hard. What’s hard is knowing, next session, that this cell was really read. C44 started from a shipped texts_patch.csv: of 6 rows, 2 cells are non-empty, and one of those TIDs doesn’t exist in texts.csv at all — the client only displays it thanks to a patch. After filling it in, the artifact gained a verifiable section:
window.hbTextMeta.patch = { present: true, rows: 6, cells: 2, sha256: '3e815929…' }TEXTS OK in the acceptance chain prints these two numbers and the patch’s sha256. C45 goes further with tools/globals_check.py, pinning the three hand-copied water palettes in src/terrain.h back cell by cell against csv/globals.csv, checking field order along the way.
This chain now runs to PARITY OK ×96, three browser paths at 413/413/366 ✓, and ENGINE TEXTS OK scans 27 source files and 949 string literals, where non-ASCII must be 0. It can’t catch “read the wrong column”, but it can catch “forgot to read”.
What the table doesn’t have: copy it, don’t invent it
C63 was the hardest one. StatusEffectType has four kinds — Fire, Stun, SpeedBoost, Panic;SpeedBoost — with the four columns split into parallel segments by semicolons, and the value of Barbed Wire_lvl1 is -750 (a negative number means a slowdown). The type word has no exact cell in texts.csv: only 速度加成 hits a row, everything else is zero hits.
So I made a very crude decision: keep the type word’s raw value from the table — no translation, no sentence building. The prompt string likewise follows the client’s single TID_STATBAR_HINT_BURN_DAMAGE (“Set the target on fire”), even though it doesn’t quite fit Stun — that is simply what sits in that spot in the client.
C61 illustrates it even better. Obstacle clearing has a whole flow of copy in the client: TID_POPUP_CLEAR_OBSTACLE_TITLE, TID_AVAILABLE_LOOT, TID_CAN_NOT_CLEAR_OBSTACLE, and even "You need to reach Headquarters levelhb.remove, so none of those four strings was ever used. After switching to table reads, gold deduction, loot crediting, and the level gate all follow the rows; ClearTimeSeconds has nine rows that are all 0, and they’re still read as-is, with no countdown scheduled.
C68’s decoration cap has only three tiers, 0/1/2. No_Decos is 0 and Speaker is 2. 250 items have Unlockable = TRUE (free once unlocked), and the 11 left blank are exactly the paid items carrying BuildCost. The shell now counts the current number, shows TID_BUILDING_CAP_REACHED verbatim once you hit the cap, and actually charges money for placing paid items.
An easter egg: the client left a TID language of its own
While going through csv/locales.csv for C49, I noticed a row in the language list literally named TID, which declares nothing but ShowTID = TRUE, and whose font is empty too. Select it, and every word on screen is the entry’s own name — this “translation table” sits in column 0 of texts.csv: 5,755 data rows, all 5,755 cells equal to the row name, written by the client itself.
The repo had only ever treated TID as “the name of column 0” and never used it. Now ?lang=TID is the 21st shipped language, and it’s in the language selector too. When writing tests I use it as a two-sided check: on the same page ZH-HANS is still “Build”, while on the TID page it should be TID_BUTTON_BUILD.
The cost
For every column you add, you have to change the engine’s catalog, the JSON keys, the shell rendering, the allowlist, and at least four regression tests at the same time, then go to the build machine and run the entire remote_acceptance.sh. The entry allowlist grew from 26 to 86 items, the languages went from 20 to 21, and 422 of the 425 objects can be matched to client TIDs.
But what you get in exchange is a different kind of commit message: no longer “fix the values,” but “these four columns weren’t read before and are now; how should the regressions be re-run?” When the client updates, toss the new csv/ into the corpus and fix whatever goes red—the source of truth isn’t on my side; hand-copying is what needs justification.
(This is part 2 of 2 of the same post.)
