SotA

Chris “Atos” Spears · Work Log

Shroud of the Avatar development at Catnip Games, day by day

Roadmap → Proposals Patch Notes Resources SQUIRREL! RSS

Week of June 22, 2026. This is an archive page; back to the current week.

2026-06-28 · 100 items

Headlines

  • Finished game builds now come with a download link: the build server packages each player and posts it right on the build job page, instead of leaving it on disk to fetch by hand.
  • Split the one big Unity build into separate jobs per platform and player type, each routed to run only where it's allowed (any machine can build a Mono player; an IL2CPP player builds on its own platform), plus a one-action deploy job that ships a build through the patch system and updates the game server. Now live on the build server, verified by building a Linux player through the new job in under a minute, and the Windows IL2CPP shipping client now builds green through CI too.
  • Unlocked IL2CPP on macOS and Linux (it had been Windows-only) and moved the build definition into version control so the committed pipeline is what actually runs — then got the Mac build agent itself producing finished players, building both a Mac Mono and a Mac IL2CPP player green, after fixing why Mac jobs never started (a missing agent label), a too-low macOS file limit that crashed the build backend, and a build checkout on an external drive that broke Unity's asset importer.
  • Filled in German across the whole game: every NPC conversation line and all menu, item, and skill text, taking German from 59% translated to complete apart from in-game book contents (~49,000 new lines), translated by the in-house LLM pipeline and gated against the in-game markup rules.
  • Audited the new German for meaning, not just markup: a model review of all ~49,000 machine-translated lines, every suspect line re-checked by a second model before any change, applied 3,474 fixes — mistranslated names, wrong-sense words, grammar, and formal/informal address decided by context — with 2,154 lower-confidence calls left for a native-speaker pass.
  • Pinned down why pressing Play in the editor is slow: every play pays a steady ~11-13s of engine overhead (the C# domain reload plus loading the startup scene), and the first play of a session adds a large one-time asset-load cost that caches away, so the game's own startup drops from ~50s cold to ~3s warm.
  • Gave the whole game a voice: generated and shipped about 98,000 AI voice clips so every NPC across 134 areas now speaks its dialogue in a cast voice, gender-matched by character description, with relayed lines in the right speaker's voice, area names spoken correctly, and player-name lines voiced instead of going silent. Live on the CDN.

Build & CI

  • Reworked the Jenkins Unity build into one parameterized pipeline that fans out across three OS-specific agents and builds both the Mono and IL2CPP player for each platform.
  • Made finished builds package and publish themselves: each player is zipped and archived so it downloads straight from the build job page. Before, builds were left on the build box with no link.
  • Switched the job to run its pipeline from the committed file in the repo instead of a script pasted into the web UI, so the two can't drift.
  • Removed the old restriction that only allowed IL2CPP on Windows; macOS and Linux IL2CPP build now too, each using that platform's C++ toolchain.
  • Extended the post-build check to confirm the player actually landed on every platform, not just Windows, so a Mac or Linux build that fails partway can't still report success.
  • Folded the separate Windows debug-build job into a flag on the main job, and documented per-agent setup (toolchains, licensing, seeded checkouts) in the build docs.
  • Stood up the macOS build agent: installed a JDK, connected it to the build server over SSH with its workspace on the external drive, gave it the Unity and repo paths, and confirmed it runs the build job.
  • Fixed a portability bug in the build's plugin-strip step. It used a GNU-sed form the macOS agent's BSD sed skipped, which left an editor-only plugin in the player and broke the compile; replaced it with a portable approach that works on both.
  • Copied the gitignored Curvy spline plugin onto the Mac build checkout so the project's scripts compile there, the same asset-store seeding the Linux builder needed.
  • Brought the Windows build agent online on the dev workstation, with its build workspace on a separate drive and the C++ toolchain in place, so it can produce both Windows Mono and IL2CPP players.
  • Added a second build executor on the Linux build box using a copy-on-write clone of the checkout, giving it a second independent Unity workspace so it can build two players at once, and allowed the build job to run more than one build concurrently. Confirmed with two Linux builds running side by side.
  • Made the build's plugin-exclusion step non-destructive: it temporarily drops the vendored editor-only Unity-MCP plugin so it can't compile into the player, then restores the plugin and the manifest right after the build, so a build never leaves the plugin deleted in a checkout.
  • Fixed the new Windows IL2CPP build job, which failed every run even though the player built fine. The build temporarily removes an editor-only plugin and used to restore it with a git checkout; a stale git lock on the build checkout (left by an out-of-band refresh or an open editor) made that checkout fail, which failed the whole job and discarded the finished build. Switched the restore to a plain file move that never touches git, so a stray lock can't wedge it, and made a genuinely failed restore fail loudly instead of leaving the plugin deleted.
  • Verifying that surfaced two more Windows-only failures, both from the build agent being a dev workstation with the editor often open. Unity's exit code wasn't read reliably (it's a GUI app, so the usual launch leaves the code empty or hangs on Unity's background import workers), which faked a "build failed" on a build that actually succeeded; and a build that died on a Unity licensing-startup race left a stale project lock that blocked the next run. Now Unity is launched through the .NET process API and waited on by just its main process for the real exit code, and a stale build-tree lock is cleared before each attempt. The Windows IL2CPP client now builds green end to end and uploads its zip; the same fixes went to all six client jobs.
  • Fixed client-windows-mono, which failed in under a second with an X display error. A full build renders the asset bundles and needs a graphics context, so the Linux build machine points Unity at a headless GPU display; that was only wired up for Linux-target builds, but the Windows Mono player is cross-built on the same Linux machine, so it got no display and died on startup. Pointed every non-Mac full build at the display. Verified: it now lands on the Linux machine and runs the asset-bundle pass under the GPU display instead of failing instantly, the same path the Linux builds already pass.
  • Recovered source files the vendored Unity-MCP plugin needs (its log-collector and log-storage types): the repo's generic Logs/ ignore rule had excluded the plugin's own Logs folder when it was vendored, so it was never in version control and a clean checkout couldn't compile the plugin. Restored the folder from the package registry and added a gitignore exception so it stays tracked.
  • Reworked the single parameterized build job into separate jobs, one per platform and player type (six client jobs), so each can be triggered and read on its own instead of one job with a row of checkboxes.
  • Encoded where each job is allowed to run: any agent can build a Mono player, but an IL2CPP player must build on that platform's own machine because it needs the native C++ toolchain. Routing is by capability labels on the agents, and keeping one build slot per machine stops two jobs from sharing a checkout and corrupting each other's import cache.
  • Added a job that builds the C# server and archives the result, so a release no longer depends on a server build done by hand on a developer's machine.
  • Added a manual deploy job that takes a built client and server and runs the existing release flow: publish the client through the patch system and update the game server, the same path the in-editor Release button uses. It defaults to the test channel and pauses for confirmation before touching QA or live.
  • Made the jobs definition-as-code: one seed job generates every entry from a single list, with the shared build and deploy logic kept in one file, so adding a platform or changing a build step is one edit plus a re-run. Left the old combined job in place, marked deprecated, until the new jobs are verified on the farm.
  • Applied the new setup to the build server: installed the three Jenkins plugins it needs, labeled the four build agents by what each can actually produce, and generated all nine jobs from the seed (no controller restart needed). Confirmed it works end to end by building a Linux player through the new job, which produced and archived a ~98 MB player in under a minute.
  • Brought the Windows build agent back online. The controller had the Windows node flagged offline from earlier troubleshooting, and an agent reconnecting doesn't clear that flag, so the node stayed offline even though the agent process was connected and healthy the whole time. Cleared the flag; the node takes jobs again.
  • Made the Windows agent survive a reboot. It had been a bare foreground process with nothing to restart it, so a logoff or reboot would drop the node. Added a logon scheduled task that relaunches it in the user session, kept in the session rather than run as a system service so the per-user Unity license still resolves, with a committed installer and a secret-free launcher template.
  • Ran down why the agent had been crash-looping earlier: its Java runtime was older than the controller's, so it couldn't load the controller's classes and the connection flapped. The launcher was already moved onto a matching runtime; wrote the requirement into the build docs so it doesn't recur.
  • Hardened the Windows build against a Unity licensing startup race. A stale licensing helper left from an earlier editor session can reject the current editor's newer handshake and fail the build before it starts. The build now retries once past that error, and the docs record the permanent fix.
  • Documented the failure modes in the build-machine ops notes: the offline-but-connected flag, the agent runtime requirement, the licensing race, and a config issue where the Windows node builds the live developer checkout instead of a dedicated build tree.
  • Fixed that last one: pointed the Windows build node at its own dedicated checkout (a separate full clone with its own warm import cache) instead of the live developer working copy, so a build no longer collides with an open editor or briefly modifies the dev tree. Updated the docs to match.
  • Found why a full build (player plus asset bundles) had never actually run on the headless build server: building the bundles loads the game's terrains, which need a graphics device to render their splat materials, and the server runs with graphics disabled, so it crashed. Gave the full-build path a software graphics context (an off-screen X server plus a software GL driver) so the bundle pipeline runs without a card. It works but is slow without real hardware; a GPU is going into the box to make it fast.
  • Closed a false-success hole in the headless build entry point: when a build step failed, it logged the failure but the editor still exited zero, so the job went green and archived an incomplete player. It now exits non-zero on any step failure, so a broken build fails the job instead of shipping. Compile-checked against the editor assembly.
  • Committed the Unity 6.5 project housekeeping the editor had left uncommitted: the QualitySettings file reserialized to the new format, a refreshed package lock, and an orphaned settings .meta removed.
  • Tracked down why selecting a Mac build did nothing and the job hung on "no nodes with the label": the Mac build agent was online, but carried only its OS label, not the per-player capability labels the Mac jobs route to, so the jobs queued forever with nowhere to run. Added the labels; Mac builds now schedule and run on the Mac.
  • Raised the Mac agent's open-files limit, which macOS defaults far too low. The low cap starved Unity's native build backend mid-build, which Unity then reported as a misleading "scripts have compiler errors" with no actual errors. Set the limit high on the agent launch so the build backend has the descriptors it needs.
  • Moved the Mac agent's build checkout off an external exFAT drive onto internal storage. exFAT can't hold the file locks and metadata Unity's build backend and asset importer need, so a full build failed importing a scene-data asset it wrongly reported as corrupt; the identical code builds clean from internal storage.
  • Refreshed the Mac build checkout to current code (it was hundreds of commits behind) with a LAN file copy of just the changed files from an up-to-date checkout, since a fresh clone of this large asset repo over the network stalls. Being behind had also kept Mac IL2CPP blocked, because the older build code still rejected it.
  • Confirmed both Mac players build green on the agent and package their own download: a Mono player and an IL2CPP player. The Mac IL2CPP build compiled with the installed command-line developer tools, so no full IDE install was needed. Wrote the Mac agent setup and the limit, exFAT, and tree-refresh gotchas into the build-machine ops notes.
  • Sped up a stuck client patch upload that was crawling at about 1/100th of its normal rate. It was routing through WSL's copy of rsync, which reads the multi-gigabyte build tree over the Windows-to-Linux file bridge and pushes it through a slow virtual network adapter. Installed native rsync on the Windows build agent and the same push finished at full speed in seconds.
  • Stopped the publish script from silently choosing that slow path again: its automatic transport now uses native rsync only and fails fast with the fix to run when rsync is missing, instead of quietly dropping back to the WSL path. WSL stays available, but only when asked for by name.
  • Fixed a latent bug in the native-rsync path that would have failed the first real use: it ran the transfer over the operating system's built-in SSH, which corrupts native rsync's binary stream and drops the connection at once. It now uses the matching SSH that ships next to rsync.
  • Caught a permissions regression in the same path during verification: native rsync had uploaded the build without world-read access, so the web server couldn't read it and the download returned not-found. Pinned every upload to web-servable permissions, restored access on the affected build, and recorded the native-rsync requirement in the build-agent docs.

Localization

  • Extended the German audit to the medium-severity flags: a second model re-judged all 2,534 with a context-aware register rule (the first pass had treated every informal "du" as wrong, but the game uses informal address in some contexts, like children or intimate speech), confirmed 2,029 and rejected 498, and applied the confirmed ones that still passed the markup checker. That brings the audit to 3,474 fixes across 106 files (grammar, register, mistranslations, duplications), with 2,154 lower-severity lines left for a native-speaker review. The du/Ihr cleanup is not complete; the quest-task convention still needs a native speaker to settle before any live push.
  • Made the audit tool idempotent so re-runs don't rewrite already-fixed lines, and confirmed the keyword glossary self-corrects: regenerated from the fixed text, the wrong "family = magic" entry flips back and the context-dependent keywords stop being treated as fixed terms.
  • Ran a full meaning-and-grammar audit of all 48,942 machine-translated German lines, separate from the markup check (which only catches structural breakage). A first model flagged suspect lines with a severity and a suggested fix; a second model independently re-checked every high-severity flag (1,415 confirmed, 174 thrown out as false positives); only high-severity, double-confirmed fixes that still passed the markup checker were written: 1,426 across 94 files. It caught over-translated proper names (a character surname "Price" rendered as the word "Preis"), wrong-sense words ("Page" of an item translated as a pageboy haircut instead of a book page), informal "du" where the game uses the formal "Ihr", duplicated words, and grammar. The 4,205 lower-severity flags were queued for a native-speaker review pass. Test shard only.
  • Hit a glossary bug while auditing: the keyword-term list mined from the original fill had a few wrong "canonical" German words (it listed the keyword "family" as "magic"), and the translation step would have silently reimposed them, undoing the reviewer's fixes. The audit now writes the reviewer's wording as-is and skips that substitution.
  • Re-audited the German fill line by line against the English source and fixed the output errors it surfaced: eight journal entries carried a literal "\n" where a paragraph break belonged, and one Oracle line had dropped a player-name token and repeated a word ("Ihr Eurem Eurem ..."). Corrected both, re-ran the markup checker on the changes, and regenerated the two affected in-game text files. The other ~49,000 machine-translated lines came back clean on the structural checks (markup, encoding, placeholders, record counts).
  • Translated all ~48,900 missing German lines. The pipeline split the work into batches that one model translated, reusing the same context human translators were given (where each line appears, what its parameters mean) plus a glossary mined from the existing human translations so place names, character names, and the clickable in-dialogue keywords stay consistent; the existing markup checker rejected any line that dropped or broke an in-game tag, and a stronger model re-translated the rejects under stricter rules. NPC dialogue went from 62% to 100% and the menu/item/skill/journal text to 100%; only in-game book contents remain, which ship on a separate track.
  • Did it without the retired web translation tool and without an outside translation service: the models run in-house as part of the dev tooling. Thirteen edge cases where the English source itself had broken formatting were balanced by hand. Every machine translation is tagged so it can be told apart from human work, re-reviewed, or rolled back. This targets the test shard only.
  • Reviewed the result and shipped it to the test shard: cheap checks surfaced ~840 lines that looked off (English left in, odd length, garbled characters), a review model judged each, and the 48 genuinely wrong ones (mostly English place names like "Beautiful Meadow") were corrected and re-checked. Then regenerated the in-game German text files so the new translations load, including the 32 conversation scenes that had no German at all before.
  • Updated the localization docs to match: the coverage snapshot now shows German at the top (59% to fully translated), and the workflow writeup describes the as-built pipeline so it can be pointed at the next language.
  • Wrote a per-language runbook and saved the reusable translate and audit scripts so the same fill can be run end to end for the other five languages, with the gotchas we hit written down.

Performance

  • Trimmed an unneeded step from every zone load: after a zone finished loading, the client searched the whole scene for dungeon-building pieces and asked each one to return any misplaced decorations to the player's inventory, but that only does anything on a plot you own. Guarded the search so it is skipped entirely when you do not own the plot you are on, which is the usual case when visiting a town. Behavior is unchanged in the common case; verified it compiles.
  • Profiled how long Ardoris takes to load after login, in the editor and against an actual client build. The editor number was misleading: about 88s, most of it editor-only asset-tracking overhead. The built client loads Ardoris in about 48s the first time and about 12s after. Roughly 36s of that first load is one-time, reading the zone's asset bundles off cold disk, which then caches. The recurring per-zone cost is about 12s, spent building the scene, rebuilding the pathfinding graph, and the network scene-join.
  • Captured it with no code changes and no manual clicking: drove a remembered-login zone load from the editor, read the game's own per-phase load-time log line for the breakdown, and attached the editor profiler to a running development build for the real per-frame hotspots, which put the cold freeze on synchronous asset-bundle reads on the main thread. Wrote the method and the Ardoris numbers into the ops docs.
  • Added two opt-in editor timers to measure why pressing Play takes so long: one times the compile / domain-reload / scene-reload phases of entering Play, the other times the runtime bootstrap up to the login screen. Both default off; toggle under Tools › Performance.
  • Then added per-step marks through the startup sequence and timed cold versus warm plays. Two cost groups fell out. Every play pays a steady ~11-13s of engine overhead: the C# domain reload (~5-7s) plus deserializing the startup scene (~6s), with another ~11s of script compile only after a code edit. On top of that, the game's own startup is asset-load-bound and caches hard: ~3s on a warm play, but up to ~50s the first time in a session, almost all of it loading the rune data, the startup data assets, and the localization text for the first time.
  • The useful surprise: for repeated iteration the slow part is the engine overhead, not our code, and instantiating the UI manager (an early suspect) turned out to be cheap. Logged the full breakdown in the performance-pass plan with the next levers ranked: turn off the play-mode domain reload (needs the static game bootstrap made re-entry-safe first), use a lighter editor bootstrap scene, then warm the first-load asset path. Measure-only so far; no game behavior changed.
  • Scoped the top lever: tried Unity's "skip the C# reload when entering Play" option and measured the entry time drop from ~13.6s to about a third of a second. But the game then silently didn't start, because our whole startup runs from a one-time static setup that doesn't re-run without the reload. Wrote up what it would take to make that startup safe to re-run (a wide sweep of one-time state and event hookups across ~100 files) and a phased plan, and reverted the setting.
  • Started on the other half of the slow editor loop: the ~11s recompile after a code edit. Almost the whole C# client builds as a single assembly, including ~110,000 lines of server-mirrored shared code that rarely changes, so every script edit recompiles all of it. As the first step toward moving that mirrored code into its own assembly (so a normal edit stops rebuilding it), untangled the one combat damage type that reached back into client classes: moved its entity-lookup accessors into a small client-side helper and stored its aggro tag as a plain byte. Behaviour is identical, the client and editor assemblies both compile clean, and the server's mirror copies were updated to match. The assembly split itself is blocked, though: the mirrored code turned out to be tied into the client's data and storage layer, so cleanly separating it needs a larger restructure, which we wrote up and filed for later.
  • Cut a recurring per-frame memory allocation in NPC movement, found with the profiler: the navmesh wall-avoidance step allocated two fresh scratch buffers each time it ran (for every nearby NPC, on a timer), making steady garbage for the collector. It now reuses one set of buffers per NPC. A model review of the change flagged a possible aliasing bug; checked the one caller and confirmed it's safe, then tightened the code and documented why. Verified it compiles.
  • Removed most of the per-frame garbage from the client's cooperative-task scheduler, the largest live allocation the profiler showed at the time. It resumes many short background routines each frame, and each resume was capturing and restoring async thread-context the scheduler never relies on; the custom awaiter now uses the lower-overhead completion path that skips that capture. No behavior change, since the scheduler runs every routine on the main thread.
  • Stopped a file-browser dialog from scanning the disk every frame, found in the profiler. The save/load/import file list re-read its directory and hashed every filename on every frame to notice new files, so it allocated steadily even while the dialog was closed. It now checks twice a second, which is imperceptible for a file list, and the list still fills immediately when opened. Verified it compiles.
  • Cut a per-frame allocation in the Lua scripting manager: it pushed about a dozen player values (position, health, time) into the script environment every frame whenever Lua was loaded, even with no script enabled to read them, boxing each value into garbage. It now does that only when at least one script is actually enabled. Running scripts behave the same. Verified it compiles.
  • Cut a per-frame allocation in the mouse cursor, found in the profiler under the targeting update. Player targeting refreshes the cursor every frame, and the cursor manager re-applied the cursor to the operating system each time even when its image hadn't changed, which allocated steadily. It now only re-applies on an actual change, which also stops a repeated texture allocation in the double-size-cursor mode; the reticle got a matching change-guard. Verified it compiles.
  • Stopped a VFX texture-scroller from instancing materials, found in the profiler's LateUpdate. The script that scrolls flowing textures (water, lava, banners) asked Unity for a private per-object copy of the material, which gave each of the 72 scrollers in Ardoris its own material and stopped them being drawn together, adding to the frame's draw-call cost. It now scrolls the shared material through a per-renderer property block and only runs while the object is on screen. Same look. Verified it compiles.
  • Took another run at the per-frame garbage in the cooperative-task scheduler, the largest steady allocation the profiler showed. A deep capture traced it to the async machinery re-capturing the thread's execution-context on every routine resume, allocating a runner, a delegate, and a call-context copy each frame; an earlier attempt to skip that via the lower-overhead completion path didn't help, because this runtime's task builder captures regardless of the await path. Suppressing the capture only around the resume loop also missed: the builder resumes each routine inside a call that restores the routine's own previously-captured context, so the next await captured a fresh one and the cost came straight back. The fix is to suppress execution-context flow once when the scheduler is created, before any routine starts, so each routine's first capture comes back empty and the builder reuses a single cached continuation with nothing to allocate after that. The scheduler runs every routine on the main thread and never relies on that context flowing across awaits. Verified it compiles.
  • Cut a per-frame allocation in the Lua manager's on-screen-GUI hook, found in the profiler under GUI repaint. Because the manager defines an on-GUI method, the engine ran its automatic immediate-mode layout pass every frame and allocated a layout group and backing lists each time, even with no script drawing anything (about 368 bytes a frame). The Lua drawing API only uses fixed-rectangle calls, not the auto-layout ones, so the manager now switches the layout pass off. Scripts that draw still work. Verified it compiles.
  • Cut another per-frame allocation under player targeting: the cursor and reticle hit-tests were handed their target-setter methods directly, and because those are instance methods that becomes a fresh delegate allocated every frame. Cached the two delegates once and reused them. Verified it compiles.
  • Cut a small per-frame allocation in NPC path-following: the line-of-sight check between two path nodes called the navmesh linecast without telling it which node to start from, so the navmesh searched for the nearest node and allocated a throwaway constraint object on every call. It now passes the node the NPC is already on as the start hint, which skips both the search and the allocation and is identical otherwise. Verified it compiles.
  • Centralized the UI kit's per-widget update tick, which the profiler showed as a large pile of tiny per-frame engine calls. Every UI element (label, sprite, panel, and so on) had the engine call its update and late-update on its own each frame, a separate native-to-managed crossing per element per method, and a busy screen has thousands of them. A single manager now holds the live elements in one list and ticks them all from one update and one late-update, calling each element's work directly. The work each element does is unchanged, anchor ordering is preserved, and it sits behind a compile flag so it can be switched back. Verified it compiles.
  • Set the shipping standalone build's compiled-code optimization to its highest setting (Master), trading a longer build for faster runtime in the player.

Voice-over

  • Generalized the Ardoris voice-over tooling to cover the whole game. One shared module now does the text normalization and clip-naming for the generator, the cost estimate, and the casting; a regression reproduces all 17,016 existing Ardoris clips exactly, so the offline tool and the in-game client stay byte-for-byte aligned.
  • Read every NPC's voice key, display name, role, gender, and home area from the conversation prefabs, covering 1,729 characters with gender resolved for all of them.
  • Wrote the casting step that assigns each character one of 140 voices using the voices' own descriptions for age, accent, and tone, keeps male and female characters on gender-appropriate voices, and spreads voices so two characters in the same area rarely share one. The hand-picked Ardoris lead voices are kept as-is.
  • Measured the whole job with no API spend: about 99,000 unique clips and 7.8 million characters of dialogue, roughly seven times the Ardoris pilot.
  • Moved off the voice model the provider just discontinued to its current higher-quality one, and made the generator refuse the retired model. The Ardoris pilot will be regenerated on the new model.
  • Surfaced a few characters whose stored gender disagrees with their hand-picked voice (such as a female-named merchant marked male in the data) for a person to settle, instead of silently flipping either.
  • Made relayed lines play in the right voice: when one NPC speaks another's words (333 lines across 35 areas), both the generator and the in-game client now resolve the original speaker and use that character's voice, sharing one name-to-voice table so they can't disagree. It falls back to the host NPC's voice if the speaker can't be matched, so nothing regresses.
  • Fixed how lines that name the current area are voiced: about 600 lines that say the place name now use each area's real display name (such as "Highvale Outskirts" or "Brittany Wharfs"), read from the scene data for 105 areas, instead of the internal folder id, so those clips match what the player hears in that zone.
  • Wrote up the whole-game voice pipeline end to end in the tooling readme so it runs as one ordered sequence: pull NPC data, build the voice attributes and casting, resolve area names, estimate cost, run the parity checks, generate, publish.
  • Generated voice for the whole game on the new model: about 98,000 clips across 134 areas staged locally (the Soltown pilot plus every remaining scene), in their cast voices. Relayed lines, area-name lines, player-name lines, and non-speakable machine text are all handled. Not published to the CDN yet.
  • Player-name lines now speak instead of going silent: a line like "Welcome back, <name>" is voiced with the generic address "outlander" (the word the game already uses when it doesn't know your name). The generator bakes that word in, and the client maps whatever the line actually substituted, your character name or an NPC's own "friend"/"traveller" fallback, back to the same word so the clip matches.
  • Published the whole-game voice set to the content delivery network: 97,951 clips uploaded with no failures, and after refreshing the cached lookup file, every area's voices are live (the lookup grew from the Ardoris-only 169 entries to 1,726). Also fixed the publish tool's cache-refresh step, which was failing on a header-encoding quirk, to use a more reliable call.
  • Wired up automatic cache-refresh for the publish tool: with the right account credential stored locally and a throttle added for the provider's rate limit, the publish step now invalidates the changed index files on its own (143 refreshed, none failed), so a future voice update goes live without a manual dashboard step.
  • Ran the first whole-game voice generation as a one-town pilot (Soltown): 3,476 lines voiced on the new model in about five minutes, with each voice fitting its character (an authoritative town guard, a child, a villainous skeleton). One non-speakable line, a run of dots left after removing the player's name, returns no audio from the model; those lines are now skipped up front instead of erroring, so they're simply silent. Clips are staged locally, not published yet.

Unity 6 upgrade

  • Cleared the build warnings the Unity 6.5 upgrade surfaced: obsolete-API uses (CS0618), the new serialization-rules analyzer, and one member-hiding warning.
  • Fixed the ones worth fixing in code: the volumetric-lighting octree now marks its parent and child back-references non-serialized, which also stops a repeated runtime "serialization depth limit exceeded" message; deleted image-effect calls to a render-texture method that no longer does anything; moved the Gaia editor onto the current scripting-define API; and annotated about 22 of our own scripts whose runtime caches, dictionaries, and 2D arrays were never meant to serialize.
  • Suppressed the unavoidable third-party noise with a single path-scoped editorconfig rather than editing vendored plugin code: plugins that still rely on the deprecated Built-In Render Pipeline components (projectors, flare layers) keep compiling without warnings, while the analyzers stay fully active on our own code. Verified with a headless build that the listed warnings are gone, and wrote up the mechanism and its gotchas in the Unity 6 upgrade notes for the next pass.
  • Added a scene tool to clear the runtime "BoxCollider does not support negative scale or size" warnings (Ardoris alone logged 24 on load) by removing what triggers them. It sets negative collider sizes positive, and for a collider flipped by a negative transform scale it bakes the mirror into a generated mesh and resets the scale to positive, so the object looks the same. Edits are made in the scene and recorded as prefab-instance overrides where the object comes from a prefab, so shared prefabs and their other instances are left alone. Scoped to negative-scaled objects that actually carry a Box/Sphere/Capsule collider, it covers the prefab-instance and parent-group cases the existing mirror-bake tool skipped. Menu: Maintenance › Negative Scale › Fix Collider Warnings (current scene or all scenes, dry run or FIX). First targets are the Ardoris Orn_el5 props, Guardpost wall cubes, and Ruins curbs.
  • Followed up on that warning cleanup after the warnings kept showing in the editor: the path-scoped editorconfig only silenced the obsolete-API (CS0618) and serialization-analyzer warnings for the IDE and the headless dotnet build. Unity's own compiler ignored it, so the Editor Console and the Unity and CI builds still listed every one of them. Unity skips a project-root editorconfig, and even an Assets-level copy, because the file name starts with a dot and the asset database never imports it.
  • Suppressed those warnings where Unity's compiler does read them: a file-scoped pragma on each of the 26 affected scripts, the Built-In Render Pipeline projector, flare-layer, and deferred-path uses plus the vendored serialization flags, kept narrow so the analyzers stay active on the rest of our code. The server-mirrored SceneAdvertisement got the same pragma in both copies to stay byte-identical. Verified with a clean editor rebuild that read the console directly: no compile errors, and none of the 26 files warn anymore. Corrected the upgrade notes, which had said the editorconfig covered the editor too.
  • Cleared a further batch of 6.5 build warnings in vendored plugins with file-scoped pragmas: the leftover Built-In Render Pipeline deprecations (Projector, FlareLayer, the removed DeferredLighting render path, the dropped OpenGLES2 check) in the DCG water shader, Ceto, ImposterSystem, HxVolumetricLighting, the PostProcessing stack, and a Standard Assets image effect, plus one unused MoonSharp field. Scoped per file so the same deprecation warnings still surface on our own code.
  • Did the same for the new serialization-rules analyzer where it only flags third-party code: Gaia, Heatmaps, and FinalIK go in version control. Curvy and DevTools live in the gitignored Packages folder, so those pragmas clear the editor console here but can't be committed; a project-wide compiler-response flag is the fallback if that set ever needs silencing on the build farm too.
  • Pragma'd two more vendored Gaia files the upgrade flagged: the deprecated Built-In Render Pipeline flare layer in the camera-effects tool, and the deprecated NavigationStatic flag in the spawner (its replacement needs a NavMesh-builder rewrite). Both files are in version control. The matching Curvy fixes (an int-to-EntityId cast and a debug serialization field) sit in the gitignored Packages folder, so those clear the local console but don't commit.
  • Root-caused a recurring client-only error from the vendored Ceto ocean plugin, "Destroying object … is not allowed at this time," that three earlier attempts hadn't stopped. The earlier guards keyed on Application.isPlaying, which is always true in a shipped client, so their corrective path only ever ran in the editor while the build kept making the deferred destroy call Unity forbids while a scene unloads or the app quits. It only reproduces in a player, so a headless compile never caught it.
  • Reworked it to key on actual teardown state and destroy the render textures synchronously then, keeping the deferred destroy for normal mid-game buffer recreation, and routed every Ceto teardown destroy through one helper so the same error can't resurface from a sibling path (reflection/depth/mask cameras, projected-grid meshes, FFT and spectrum textures). Compiles clean against the Unity 6.5 assemblies; still to be confirmed in a client build. Wrote the root cause and fix into the Unity 6 upgrade notes.

Oracle

  • Added a Lord British entry to the in-game Oracle's player-facing knowledge base, its first lore page in a base that until now held only gameplay help. A player had asked the Oracle who Lord British is, the knowledge base had no answer, so the Oracle filed Gitea #105. Wrote the entry from the game's own dialogue (the king of New Britannia, his queen Arabella, their children Kinga and Ronin, his castles in Brittany and Mistrendur), rebuilt the search index, and restarted the service; the Oracle now answers the question.
  • Added a dragons entry to the Oracle knowledge base after a player asked about dragons and got no answer (Gitea #98). Wrote it from the game's own journal, books, and ability data: where dragons appear (Ulfheim's high chief white dragon, the dragons over Jade Valley, the Wyrmsands), the kinds (fire, baby, Elder and Ancient, clockwork), their real attacks (Dragon Fire breath, Dragon Bite, melee Dragon Breath, the wyvern-style tail strike), aerial-versus-ground tactics, and what you can harvest (dragon teeth for the Ulfheim collector, dragon bones for ritual crafting). Drop rates and engine internals left out per the public-content rules. Rebuilt the search index on the box and restarted the service; the Oracle now answers dragon questions (the entry is the top knowledge-base hit for dragon queries).

Stability / log triage

  • Stopped a client log flood from creature and pet attacks (Gitea #102). Combat telemetry reads a rune's display name on nearly every nearby cast, and a rune with no associated skill logged an error each time, about 2,500 errors a day on the test shard, mostly creature, NPC, and tamed-pet attacks. Those abilities aren't player-learnable skills, so the missing skill is expected, not a fault: the display-name lookup now warns once per distinct rune and falls back to the rune name without logging, leaving the real missing-localization check untouched. Clears on the shard after the next republish.

Balance dashboard

  • Stopped the live Balance dashboard flashing on every refresh. The 3.5s auto-update was reloading each chart from scratch, which flashed the chart's loading overlay, replayed the draw-in animation, and rebuilt the summary cards. The background refresh now merges new data into the existing charts and writes the new numbers into the cards in place, so it keeps updating at the same rate with no flash and the chart's current zoom is kept. Explicit actions like changing a metric, range, or filter still show the loading spinner and redraw.

Docs & research

  • Investigated a tracker task to adopt the "TMP Advanced Text Generator" and found the premise was wrong. The 10-40% text-CPU win is Unity's new Advanced Text Generator, which in 6.5 is the default backend for UI Toolkit text only; TextMeshPro and the legacy uGUI text keep their own unchanged generator, and bringing the new generator to those is a later opt-in. The win is also gated on an NGUI-to-uGUI UI migration the project hasn't started. Corrected the feature-adoption doc and closed the task as not-actionable, noting that the UI migration still has no roadmap epic despite being the top upgrade risk.
2026-06-27 · 39 items

Headlines

  • Gave Ardoris a voice: 17,016 AI-generated NPC lines now play in their own cast voices as you talk to townsfolk, streamed from the CDN and confirmed live in the editor.
  • The build farm went fully autonomous: one Jenkins run now turns a cold checkout into finished Windows and Linux clients, license and all.
  • Brought every terrain and every scene onto Unity 6's current format in one sweep, 475 terrains and all 448 scenes, clearing deprecated-format warnings game-wide.
  • Killed the login server's phantom "max player capacity" wall that was turning players away while the shard sat empty.
  • Rebuilt the test shard's entire database index set from the server's own definitions after a reload had stripped them.
  • Taught the /balance combat recorder the whole pet story, damage, deaths, abilities, survivability, with their own live dashboards.
  • Documented the entire text-localization pipeline end to end and measured how far each of the seven languages is translated, then started replacing the retired translation tool with an LLM pipeline that translates and self-reviews to close the gaps, German first.

Client / UI

  • Fixed rename dialogs (item/house) dropping the first character you type. The text field threw away keyboard input on the same frame it auto-focused; that was harmless before but started eating the first keystroke after the engine upgrade shifted when the first character arrives. It now keeps that character.
  • Removed the leftover CanvasRenderer the engine upgrade left on world-space 3D text objects (the guild/name/title/health nameplate labels and a couple of others). The 3D text component renders through a mesh, not a canvas, so the extra component did nothing but make each object log a warning on load. Cleared 14 across four nameplate prefabs and one scene, and added a reusable editor sweep (SotA › Maintenance › TMP CanvasRenderer) to catch any that turn up later.

NPC voice-over

  • Stood up a working NPC voice-over proof of concept in Ardoris. Every NPC there is cast to a distinct AI-generated (Inworld) voice: 79 main characters get a unique voice that is never reused, while rank-and-file guards, townsfolk, and children draw from small fixed voice pools. Generated all 17,016 spoken-line clips (about 482 MB) from the existing localized dialogue and staged them to the CDN rather than the repo.
  • Built the client runtime that plays them. For each line an NPC speaks, a new voice manager picks the right clip from the NPC's voice and the line's text, pulls it from a local cache or the CDN, and plays it from a 3D audio source that fades out with distance. Confirmed in the editor by walking around Ardoris and talking to NPCs; lines that include the player's own name stay silent for now.
  • Added the generation and publish tooling: a script that walks the conversation corpus, normalizes and de-duplicates each line, sends it to the text-to-speech service, and converts the result to OGG, plus a publisher that uploads the clips to the CDN and clears its mutable index files on each release.

Build machine / CI

  • Synced the full game project onto the new build machine. A normal clone of our repository stalls because the server struggles to pack a tree this large, so the box does a shallow clone (just the latest revision) and then pulls the roughly 150 GB of large binary assets directly. About 440,000 files now live on the machine; a few thousand stragglers that the asset tooling skipped after a shallow clone were filled in by copying them from the local cache.
  • Got a build running end to end on it. The Jenkins job checks out the project and launches the Linux and Windows (Mono) builds, and it now reaches the actual Unity build step, stopping there only because the editor still needs a license activated. That license is the one remaining piece before it produces finished builds.
  • Licensed it and got it compiling. Activated a free Unity Personal license on the machine without a screen attached, which took some doing since Unity removed the old offline activation method; found the right licensing-client command to assign the seat headlessly. The build now gets past licensing and compiles the whole project.
  • Cleared the two things that were breaking the player build: removed an editor-only development plugin that shouldn't ship in a player build (on the Linux builder only; the Windows build still uses it), and supplied a third-party spline asset the repository doesn't keep under version control. After that the build moves on to importing the full asset library.
  • Stood up a local asset-import cache on the build machine's scratch drive so clean rebuilds, and other machines, can skip re-importing assets from scratch. It runs entirely on the box with its connection to the vendor's cloud blocked, and the in-progress import now feeds it as it goes.
  • Installed Claude Code on the build machine and wired it up for use from VS Code over a remote SSH connection, so the project can be worked on there with AI assistance.
  • The build machine now produces finished players. Once the first asset import completed, a single Jenkins run built both the Linux and Windows (Mono) standalone players end to end and both were confirmed on disk. The first import takes hours, but after that the cached import makes repeat builds fast.
  • Documented the legacy TeamCity build server end to end so we can retire it. It still drives the whole build, deploy, test, reporting, and website-publish pipeline off the old version-control system, and little of it was written down. Captured a complete read-only export of its configuration (7 projects, 167 build jobs, every step and script, the job-to-job dependency chains, the version-control roots, and the build agents) plus a per-project walkthrough.
  • Wrote a rebuild-and-retirement guide alongside it: each pipeline (server build, client and patcher builds, asset baking, deploys, functional tests, database backups, reports and metrics, localization, website publish) mapped to how it would be reproduced on the current build machine, with a power-off checklist and the outside systems that still depend on the box.

Player tools / combat analytics

  • Taught the /balance combat recorder about pets. A pet's hits used to be dropped because the recorder only watched the player's own attacks; it now also records the player's pets' damage, and that damage counts toward the player's own DPS and damage totals.
  • Added pet survivability and abilities: the recorder now captures damage taken by a pet and pet deaths (including what killed it), plus the abilities a pet casts. Each event is tagged by whether it came from you or your pet, so the two can be split apart or combined. This also makes the previously empty "damage taken" charts work for the player too.
  • The session dashboard now shows pet charts on its own when a recording includes a pet: pet damage over time, your damage vs your pet's share, a breakdown by pet type and creature, pet ability use, and pet damage taken. There's also a button to add them by hand.

Dev tooling / log triage

  • Ran the recurring log-triage pass over the test shard and filed the new distinct problems as tickets: a large batch of missing-localization errors on creature and rune abilities, an audio clip trying to attach to a bone the plant prefab lacks, and a short cluster of database-selection timeouts during a restart window. Recurring ones were added as comments on their existing tickets instead of re-filed.
  • The log-triage tool now records the time window each run covered and can resume from where the last run stopped, via a new -SinceLast option, so repeated passes tile the timeline instead of overlapping or leaving gaps.
  • Fixed a bug in that resume path: the saved cursor timestamp was read back in a format the log search could not parse, which would have returned zero results with no error. It now round-trips correctly, and a malformed query raises an error instead of reporting an empty window.

Repo conventions

  • Pointed the "work directly on" default and the work-blog requirement at the unity-6.5 branch, where active work lives during the Unity 6 upgrade (they previously named main).
  • Added a Headlines callout to the top of every day on this work blog: 3-7 one-line highlights of the day's biggest items, above the detailed sections. Backfilled today's NPC voice-over work, which had only been logged as a plan, and made keeping the headlines current a standing rule in the contributor guide.

Login / server

  • Fixed the login server turning players away with "the server has reached max player capacity" when nobody was online. The capacity check was counting stale login records left behind by earlier disconnects, not just active sessions, so the count could sit at the limit with the shard empty. It now counts only live sessions, and a background pass clears leftover records on a timer so they can't accumulate. A prior fix had addressed a different cause of the same message (no game servers registered), which is why this kept happening.
  • Put the login-queue count on the internal health panel, with a warning as it nears the limit, so this failure is visible instead of hiding behind an otherwise all-green status.

Test shard / data

  • Found the test shard's database had come back from its last bulk data reload with none of its application indexes, only the default per-row key on each table. Without them, time-expiring records were never cleaned up on their own (a background safety net had been removing them by hand and logging a warning), and uniqueness rules weren't enforced. Rebuilt every index straight from the server's own definitions.
  • Changed the data-reload runbook so it rebuilds those indexes from the server code after every restore, instead of trusting the data dump to carry them across. The next time we copy fresh live data onto the test shard, it comes up fully indexed.

Unity 6 migration

  • Stopped the water plugin from logging an error every time a scene unloads. It destroyed its render textures during teardown in a way the new engine no longer allows; it now uses the immediate-destroy call outside play mode, matching the plugin's own cleanup code elsewhere.
  • Removed a leftover render component from the floating chat text inside the nameplate prefabs. The 3D text it sits on doesn't use that component, so the editor logged a warning for every nameplate on screen.
  • Removed duplicate right-click "Help" menu entries in the NGUI editor tools that the new engine warns about. The same menu was registered on two methods; the kept entries still open the correct help page.
  • Reserialized every out-of-date terrain in the project (475 of 518) to the current format, clearing the deprecated-save-format warnings game-wide. Terrains are referenced by ID, so no scenes changed; the update stayed isolated to the terrain data files.
  • Did the same conversion for the scenes themselves (the terrains are referenced by ID; scenes carry the serialization format directly). Converted all 448 scenes to the current format through the live editor and committed them in batches of ten. Older scenes get rewritten in full the first time the upgraded engine saves one, so converting them deliberately up front means a later edit to a scene shows only the real change instead of a whole-file format diff.

Asset pipeline / Git LFS

  • Tracked down a batch of baked scene assets (combined meshes and occlusion data, 28 files) that the editor reported as corrupt. They were stored in large-file storage, but a text rule in the repo's attributes was keeping them out of it on checkout, so the working copy held a small pointer stub instead of the real file. Added targeted rules so they're stored and restored correctly, and pulled the real files down.
  • Scanned every large-file-tracked asset in the project (about 95,000) for the same pointer-stub problem and confirmed none remain. Added a script that re-runs the check on demand and reports, per bad file, whether the attributes route it correctly, and wrote it up in the LFS guide.

Documentation

  • Documented the text-localization system end to end in a new docs/systems/localization/ suite: the in-game runtime (the language manager's string-table databases, the localized-string types, the older NGUI text layer, and how the active language is chosen and stored per install), the out-of-game translation data plus the roughly 43 scripts that pull English source out of the project, send it for translation, bring it back, and bake it into the client. Seven languages ship today: English plus German, French, Spanish, Italian, Portuguese, and Russian.
  • Wrote up the web translation tool the team used (a self-hosted Zanata, plus an earlier hosted service) and the import/export scripts that move strings to and from it. Flagged the whole pipeline as legacy: the scripts are Python 2 and tied to Perforce, and the translation server is long gone. Added a section on what a modern replacement has to preserve before any new language can be added.
  • Added a forward plan and a roadmap entry for the longer-term voice work: first fill in the missing English voice-over beyond the Ardoris prototype, then generate native-language voices for the languages we already translate, then add Mandarin (new text, fonts, and voice). Left the choice of speech engine open.
  • Measured how much of the game's text is actually translated into each language, read straight from the translation database. Of 119,442 English lines, German, Russian, and French are 53–59% done and Spanish, Italian, and Portuguese 27–29%; the gap is almost entirely NPC dialog, since the UI/item/book strings sit near half-translated across all six. Added a small read-only script that prints the per-language and per-scene breakdown so it can be re-checked as translations land, and recorded a dated snapshot in the localization docs.

Localization

  • Built the replacement for the retired web translation tool: a pipeline that fills the missing translations with an LLM. One model translates each missing line, with the same context human translators were given, a second model reviews it for accuracy and fluency, and the existing markup validator gates both. A glossary mined from the translations humans already did keeps terminology consistent (place and character names, the clickable keyword links in dialogue). German is the first target; the tool handles the other languages too.
  • Fixed a Python 3 bug in the localization file writer (it wrote bytes to a text-mode file) and confirmed that loading and rewriting a file leaves all 113 German files, about 71,000 records, byte-for-byte identical, so filling gaps can't disturb existing human translations. Machine translations are tagged so they can be filtered, re-reviewed, or rolled back separately. Also ported the step that bakes translations into the client off the old version-control system to plain Git. The actual translation run targets the test shard only.
2026-06-26 · 52 items

Headlines

  • Launched the Server Health panel: one live red/green view of the whole stack, with the game server now reporting its own status every 30 seconds.
  • Stood up a brand-new Linux build machine from bare metal, RAID0 build volume, headless Jenkins, and the full Unity 6.5 toolchain.
  • Built a community-sentiment dashboard tracking how players feel about Shroud of the Avatar and Richard Garriott across Reddit, in-game chat, and the forums.
  • Rebuilt the companion app as standalone desktop and browser chat windows that talk only to your running client.
  • Hardened the deploy pipeline so a broken shard can no longer pass itself off as a healthy deploy.
  • Built tooling to load a sanitized copy of the live game database onto the test shard, stripping every payment and personal record first.
  • Audited negative-scaled props across 60+ shipping scenes and built a mesh-rebake fix for them.

Build machine / CI

  • Set up a new Linux build machine for continuous builds. Striped its two main NVMe drives into a RAID 0 array as the fast build volume (Jenkins working directory and build artifacts), installed Jenkins headless, and switched the box to boot to a console with no desktop to free up resources.
  • Before reusing a third NVMe drive as scratch space, found it still held about 700 GB of old files. Copied all of it onto the RAID array and verified the copy file-for-file (every file accounted for) before reformatting the drive.
  • Installed the Unity 6.5 Linux editor with every build-support module usable on Linux: Linux, dedicated server, and WebGL, plus Windows (Mono) and Android support extracted by hand from their installers since Unity doesn't ship those as Linux packages. Confirmed the editor runs headless and that the RAID, scratch drive, and Jenkins all come back correctly after a reboot.
  • Wrote a runbook and a reproducible setup script for the box, and recorded the main limitation up front: Unity's Linux editor can't cross-compile the Windows IL2CPP client, so producing that build will still need a Windows build agent.
  • Finished the Jenkins setup and added a build job for the Linux and Windows (Mono) standalone players, wired to the project's headless build entry point. Confirmed the pipeline runs end-to-end on the box; it now waits on getting the project synced to the machine and an editor license in place before a build can finish.

Player tools / combat analytics

  • Fixed the in-game /balance connection bug: the client was posting to the wrong URL path, so every request to the analytics service came back "not found". Found it in the service's request log, corrected the client path, and switched the service to accept the game's data using the per-recording token it already hands out — so no shared key needs to ship in the client, and each recording is still protected from anyone posting under someone else's code. Lands with the next client build.

Test shard / logging

  • Tracked down why the test shard's logs had gone dark for ~2 days: the game server had stopped forwarding its logs to our central log store. The store itself was healthy; the server's log-forwarding config had been reset to an old, unreachable destination. Repointed it, restarted, and live logs are flowing again.
  • Fixed the underlying cause: the test-shard server-deploy tool was overwriting that environment-specific forwarding config with the built-in default on every deploy. It now preserves that config the same way it already preserves the server's login config, so a redeploy no longer kills logging.

Test shard / data

  • Added tooling to load a sanitized copy of the live game database onto the test shard for load/system testing. It drops the sensitive collections before anything leaves the build machine (payment, cached-credential, and anti-cheat/hardware-tracking records) and clears in-row personal data after the restore (last login IP, hardware-fingerprint history, and the private notes players leave about each other), keeping the bulk world/item/economy data so the server runs against realistic volumes.
  • Wrote the runbook for it: snapshot the VM for rollback, build the sanitized archive, send it through the existing test-shard jump host, and drop-and-replace the test database. Documented why this is for load/system testing and not for logging in as live players: the test login server issues different account ids than the live data, so a restored character isn't reachable by its original owner, while the existing test account keeps working.

Server

  • Added a server switch (off by default) to silence the team's internal chat notifications, so the test shard can run without pinging the team's channels. With the switch off the notification path does nothing, instead of needing per-message edits, and live behavior is unchanged unless it's turned on.

Observability / Server Health

  • Started a Server Health panel for the dev portal: a live red/green view of the game shard and our web services, prompted by the dropped server connection and logging outage that both slipped by recently. First piece is the web-infrastructure half. It watches our internal services (source control, wiki, the analytics and AI helpers, the databases, the web server), TLS certificate expiry, disk and memory, and whether the game is reachable from the internet, then rolls it into one status with history. The game-shard half (player counts, server-to-server connections, database and response-time metrics) is next.
  • Built the game-shard half: a small read-only collector that runs next to the game server's logs and watches error/warning rates, a dropped server-to-server connection detector (the failure that got past us), a "logs went quiet" alarm that fires if the server stops reporting, players online, the host machine's load/memory/disk/storage-pool/bandwidth, and the game VM's CPU and memory. It's exposed to the portal over a locked-down internal channel reachable only from the portal box, so the panel's "Game shard" pane has data. Live numbers from the game server itself come next.
  • Finished it: the game server now reports its own numbers. Every 30 seconds it sends whether it's ready for players, its database connection state, and how many of its internal sub-servers are connected, over the existing signed, private server-to-web channel, shown on the panel. Verified live end-to-end: the panel shows the server ready, database connected, and all sub-servers linked. (The server-side piece was hot-swapped in with an automatic rollback if it failed to come back up; it folds into the next full server build.)
  • Documented and registered it: wrote up the whole Server Health system (how it's built, what it watches, how to deploy it), added it to the team's living roadmap, and registered its internal channel in our security baseline so the automated pre-deploy security check confirms it stays locked to the portal box.
  • Tested it end-to-end — including deliberately taking a service down to confirm the panel turns red and then recovers — which caught two false alarms in the dashboard's own checklist (it was looking for the wiki's database under the wrong name). Corrected the checks so the board reads true green when everything is healthy.
  • Tracked down why the panel read no heartbeat after a server update: the build that landed on the test shard predated the 30-second health-push feature added earlier the same day, so it served players fine but never sent the data the panel reads — and restarting can't fix what a build doesn't contain. Rebuilt the server from current source and redeployed; verified the panel heartbeat, the database connection, and a test login all came back.
  • Hardened the server-deploy tool against two traps that surfaced. It now warns when the build being shipped lacks the health-push code, and it preserves the shard's own database-connection config instead of replacing it with the committed default on every deploy — that default points at an address the test shard can't reach, which had knocked out its database connection mid-fix until it was restored. Wrote up the deploy and heartbeat flow, and that the test shard is a self-contained environment, in the docs.
  • Ran down a report that the client couldn't connect to the test shard, showing "max player capacity." That message isn't a real player cap: it's what the client shows when the master server has no game sub-servers registered to hand a login off to. The shard's master had been down in two windows earlier that day (during the deploy churn we were already fixing) and had recovered by the time we looked — confirmed it was serving again with a live test login, and confirmed the editor was pointed at the test shard, not a local server.
  • Made the server deploy refuse to report success until the shard is actually serving. After it restarts the server it now polls the shard's own health report until the master is ready with its game, scene, and group sub-servers all registered, and fails the deploy otherwise, instead of stopping at "the process is running" — the gap that let a broken shard pass as a good deploy.
  • Added an alert-only watchdog on the game host that reads the same health report every minute or so and notifies us when the shard stops serving (after a few consecutive misses, so a normal restart doesn't page anyone). It never restarts anything on its own; it just catches an outage that happens between deploys instead of waiting for a player to report it.
  • Fixed a client-side trap behind the same symptom: the networking default still pointed at a local address as a fallback, so a connection path that skipped the locked server host would quietly connect to nothing and look exactly like "no server." Pointed the default at the real shard and noted the requirement so it can't drift back. Wrote a runbook for the whole "max capacity" / shard-down case.

Deploy tooling

  • Added a Cancel button to the test-shard deploy window. It actually stops an in-flight upload now — the old stop only closed the launcher and left the transfer running underneath — and offers to remove the half-uploaded build from the patch host afterward, which is safe because a build only goes live at the very end once the upload completes. The cleanup refuses to delete whatever build is currently live.
  • Reworked the client patch upload to use a native sync tool instead of routing it through the Windows Subsystem for Linux, whose virtual network had been throttling the upload to a crawl. Needs a one-time install of that tool on the build machine before it takes effect.

Community sentiment dashboard

  • Built a community-sentiment page on the dev control panel (behind the portal login). It pulls the r/shroudoftheavatar and r/ultima subreddits, the live server's in-game chat, and the game's forums, scores each message, and tracks two subjects: how people feel about Shroud of the Avatar, and separately about Richard Garriott. For each subject it charts a smoothed sentiment line per source plus a combined line and a 7-day average, a per-day bar of how much the subject was talked about, and a few highlighted positive and negative comments per source. It runs from an internal machine because the live chat logs are only reachable on the internal network.
  • Sampled the live chat from only the public channels (global, trade, local, scene, guild, emote), leaving private and party messages out. A message counts toward a subject if its venue is inherently about it or its text mentions it, so a general-Ultima post only counts toward Shroud when it actually names it. Backfilled 60 days so the trend lines start with history instead of a single point. Wrote the runbook, and packaged it as a Claude skill that pulls the data and curates the highlighted comments, to run nightly.

Crown Store authoring tools (internal)

  • Fixed two long-standing bugs in the in-editor tools we use to add Crown Store items. The "set thumbnail" image picker discarded your choice as soon as you closed it (and on an item with no picture yet it could error or wipe the existing one). It now keeps the picked image, never clears one by accident, and the item form's own thumbnail field is editable again.
  • Fixed item edits and bulk tag/image "paint" actions not saving reliably: the store's lookup data was updated in memory but only written to disk on a full manual project save, so changes could revert and the related data files could fall out of sync with the item. Edits and bulk paints now save immediately, and again when you switch to another item.
  • Wrote up a short proposal for further Crown Store authoring improvements: easier linking of the pictures we take to the right items, a less finicky item/tag entry form, one-click creation of a new subcategory with bulk move of existing items into it, and clearer in-game messaging when Crown Store purchases are paused until you update your client.

Engine upgrade / editor console cleanup

  • Started a triage-and-burndown pass over the Unity Editor's warning/error console as part of the engine upgrade, filing the new items as tracked issues and skipping the ones already covered. First fix: an internal Crown Store tool could throw a confusing low-level error when asked to build a store item's display data from an unsaved, in-memory item; it now stops early with a clear message naming the item, and the normal saved-item path is unchanged.
  • Cleared a batch of compile warnings the engine's new serialization checker started raising: two internal UI helper base classes that hold saved data were missing the marker telling the engine to save them, which produced a dozen warnings on every compile. Added the marker (no behavior change) and confirmed the game code still builds cleanly.
  • Repo hygiene: stopped version-controlling a local Unity client tempfile (a per-run process/status scratch file) that kept showing up as a spurious repo change — removed it from tracking and added it to the ignore list.
  • Cleaned up a batch of build-time "missing tree" errors traced to non-shipping terrains (four old vendor demo scenes, a stray test terrain left at the project root, and two scratch scenes). Their tree placements all pointed at art that no longer exists, so they rendered nothing yet logged an error on every build. Cleared those dead placements (no visual change; it also shrank the files) and left the real content terrains for an art decision.
  • Triaged the editor console and cleared another round of deprecated-API warnings from the engine upgrade, all in third-party/vendored plugins (pathfinding, popup UI, occlusion culling, a screen-resolution helper, and the networking layer). Swapped the handful with direct modern replacements (the 2D physics-body kinematic flag and the display refresh-rate field) and removed now-unneeded member-hiding keywords; no behavior change, and the game still builds clean. Left the larger group of render-pipeline deprecations (projectors, lens flares, the old deferred path) for the render-pipeline migration, since those have no drop-in replacement yet.

Scene tools / negative-scale props

  • Added editor tooling for props that designers flipped by typing a negative Scale, which the engine upgrade surfaces. An audit lists and categorizes every mirrored object across the shipping scenes, and a bake step removes the negative scale by rebuilding the flipped mesh (reversed faces, recomputed normals) and resetting the transform to positive scale, so the prop looks identical. The generated meshes are shared and de-duplicated, and the risky cases (skinned characters, parented groups, prefab instances) are flagged rather than touched. Compiles clean; the project-wide bake is a later branch step that also needs a lighting re-bake.
  • Measured what negative scale actually costs before writing any fix. On a live test the engine still reports a correct bounding box for a flipped object and the physics overlap check ignores the flip, so the in-game systems that read bounds are unaffected; the real penalties are exclusion from baked lighting and batching, plus inverted surface normals. That is why the fix bakes the mirror into the mesh rather than patching gameplay code. A repo scan found flipped props in 60+ scenes, including major production maps.
  • Validated the mirror fixer end to end (the rebuilt geometry matches the original) and ran it across the shipping scenes, correcting 339 standalone props. Saving an old-format scene in the upgraded engine rewrites the entire scene file, so the result was a format-conversion diff rather than a clean fix, and it was reverted. Scoped the real prerequisite instead: a one-time, project-wide scene/asset format conversion as a planned migration step, gated behind the version-control cutover (the content team still authors in the old format); after that conversion the prop fix re-runs with a change-only diff. The remaining flipped props live inside reusable prefabs or parented groups and need their own passes.

Security / pre-deploy hardening

  • Ran our pre-deploy security check across the test shard and the portal sites and fixed what it flagged. Tightened the shard's public port-forwarding: an internal server-to-server admin/logging port was being forwarded from the internet to the game machine even though only in-house ops tooling ever uses it and our own docs say keep it private. Removed it from the live forward rules and the re-apply script; players are unaffected, since it's not a game-client port. Also taught the security baseline about the extra game-server instances we run for capacity, so the check stops flagging those known-good game ports.
  • Hardened the portal web sites: added the standard browser security-response headers (transport-security, content-type-options, frame-options, referrer-policy, plus a content policy on the static download host) to every portal site, and stopped the web server from advertising its version. Left the richer content policy to the apps that already set their own, and skipped it where a strict one would break an embedded dashboard. The pre-deploy preflight now passes: no sensitive ports exposed, certificates healthy, login/auth gates holding, the public blog leaking nothing, and 0 failures.

Companion app

  • Restarted the companion app as a separate chat view that runs alongside the game and talks only to the running client, never to the game servers. Built the client-side bridge: a local-only connection the client opens for itself, reachable only from the same machine and gated by a per-run token, that mirrors incoming chat out and sends typed messages back through the game's normal chat path, so channels and slash commands keep working. Added a browser companion page that reads and sends chat, plus a /companion toggle to start it (off by default). Compiles with no errors; not yet runtime-tested.
  • Wrote up the design and as-built notes (message format, file map, how to run it), added the app to the living roadmap, and recorded the next steps: confirming the local connection survives the optimized build, a live read-and-send test, a standalone desktop companion app, and hiding the in-game chat panel while it's popped out.
  • Built the desktop half: a standalone chat window (on the same UI toolkit as our game launcher) that connects to the running client the same local-only way the browser page does, shows chat in the in-game colors, and sends messages back. It can stay on top, remembers its size and position, and closes itself when the game exits. Launched with /companion desktop. Builds with no warnings.
  • Made /companion desktop find the desktop app when running inside the editor: it now also looks for the locally built copy in the repo, so it can be launched and tested in play mode without first pointing it at the file by hand. Verified the lookup resolves in the live editor.
  • Made the companion mirror the client's chat tabs. The client now sends the list of tabs and each tab's channel filter, and both the browser page and the desktop window show a matching tab bar that splits the stream per tab (any messages that always show, like system notices, appear in every tab). Picking a tab in the companion also switches the in-game send channel, so a reply goes where you'd expect.
  • Made the companion ship with the game build. The browser page already rode along inside the build; added a post-build step that compiles the desktop window and drops it next to the game, so a patched client finds and launches it on its own. Players need nothing extra installed (the desktop window is self-contained); the build machine needs the dev toolchain, and if it's missing the step is skipped with a warning and only the browser page ships.

In-game Oracle (AI assistant)

  • Fixed the new Oracle window not handling resize: dragging it bigger or smaller left the conversation area stuck at its old size and the type-here box floating in place instead of staying at the bottom. The input box was being skipped by the UI's "only reposition things that are visible" optimization while it sat empty, so it never followed the window frame. The transcript and input now scale and stay pinned to the window edges at any size. Lands with the next client build.
  • Fixed the Oracle window's text box cutting you off at about 16 characters: it was built from the chat-tab rename field, which is capped short on purpose. Raised the limit and made a longer question scroll within the box instead of shrinking the text to fit. Lands with the next client build.
  • Fixed a leftover from when the Oracle briefly lived in a chat tab before moving to its own window: characters who ran that build had a saved chat tab pointing at a channel that no longer exists, which spammed errors on login and left the chat "send to" label stuck. The client now cleans up that stale tab on load and the tab keeps working as a normal one. Lands with the next client build.
  • Tightened what the Oracle files as a report. It used to turn almost anything a player typed into a tracker item, including test strings and off-topic or joke messages, which buried the real ones. Gave it a rubric: file genuine bugs and ideas after one confirmation, but decline gibberish, abuse, and favor or account/billing requests, and explain rather than file when a reported "bug" is actually working as designed. Added a server-side backstop that catches the obviously empty or test reports the model still let through, plus a log of what it declines so we can tune the line; declined messages no longer pollute the list of unanswered questions we use to grow its help articles.
  • Expanded the Oracle's player knowledge base from three articles to about thirty so it can answer gameplay questions instead of guessing. Added guides for keys and controls, the interface, combat basics, skills and advancement, and the crafting loop, plus detailed per-school writeups of all nine magic schools and every weapon, armor, and support skill line, with the in-game numbers and how abilities scale. Built it from our existing design docs, left out unreleased skills and anything internal, and verified the whole set parses and leaks no engine internals.
  • Added an enchantments article to the Oracle's knowledge base, from a player asking about enchantments that the Oracle couldn't fully answer and filed as a tracker item. It covers every enchantment players can apply to gear: the universal attribute boosts that go on any item, and the nine gem schools, each with its attunement, the opposing-element resistance it grants, and its per-ability buffs, plus which enchants only roll on weapons or only on heavy armor. Built from the crafting catalog with the in-game stat names put into plain terms and engine internals left out. Goes live on the next assistant update.

Test shard / login backend

  • Tracked down a flood of errors against the test shard's login backend, surfaced by a log triage: two internal endpoints (the server's keepalive heartbeat and the player reward-offer fetch) were returning HTML error fragments instead of JSON, so the game server logged every call as a failed web request. The login site's debug mode was forcing the language runtime to print its harmless deprecation notices into the response body, which broke the JSON parsing. Turned off showing errors in responses (kept the error log), and confirmed both endpoints now return clean JSON.
  • Made it stick and documented it: the login-server setup script now sets that flag when it writes the site config, so a re-provision can't bring the flood back, and corrected the operations doc that had wrongly assumed those notices never reached responses.

Docs

  • Wrote a worked walkthrough of the quest system: one real questline traced end to end through every place its data lives, the NPC conversation scripts, the journal and task entries, the per-stage quest markers, and the player's saved progress flags, with two diagrams (a map of where each piece of quest data lives from authoring through the live server, and a step-by-step chart of the questline). Cross-linked it from the existing quest-system reference and the docs index. Documentation only, no code change.
  • Documented the whole Episode 1 main story (the Path of the Oracle) as a single reference: the three-act arc from the Isle of Storms intro and virtue-path choice, through the Love, Truth, and Courage paths, to the Dire Prophecy endgame (gather the prophecy pages and the three lenses, assemble them at the Confluence, receive the Shroud). Traced act by act against the actual quest assets, journal entries, and conversation scripts, with the progress-flag spine, per-stage tables, and flowcharts. Pinned down a few things that are easy to get wrong: all three virtue paths are required to reach the finale, the starting-path choice is set in two steps (and not symmetrically across the three paths), and the per-account virtue values are numeric scores read only at the finale to pick the ending. Documentation only.
  • Surveyed the gameplay features of the big Ultima Online community servers (Outlands, Forever, Renaissance, Evolution, and others) and mapped them against what our game already has, tagging each feature as have/partial/gap. The takeaway: we already have the UO sandbox core, often in more depth; the real gaps are seasonal live events, recurring weekly play loops, structured/ranked PvP, and new-player retention. Wrote it up as a research doc, added four candidate epics to the roadmap (marked as ideas to evaluate, not committed work), and logged the rest as ideas on the tracker so they aren't lost.
2026-06-25 · 8 items

Headlines

  • Shipped a full player combat-analytics tool: /balance streams your fight data to a hosted dashboard you share with a 6-digit code, anonymized, with CSV export.
  • Took the analytics service live over HTTPS with an auto-renewing certificate and a Balance card on the dev portal.
  • Retired the single-sign-on experiment and put the developer portal back on one clean login.

Player tools / combat analytics

  • Built a combat-analytics tool for players: an in-game /balance command toggles streaming your combat data to a web app we host and gives you a shareable 6-digit code. Anyone with that code can open an interactive dashboard, build charts for any metric (damage per second, focus used, skills per second, and more), scrub a timeline of every skill use, and download a CSV of any slice they select. Charts are anonymized: your name never shows publicly. You appear only by your code, other players as pseudonyms, and monsters by name.
  • It captures detail per skill use (your level in that skill, the result, focus cost, your current stats, passives, and gear bonuses) plus any skill used near you or aimed at you, so the team gets balancing data while players keep a private, shareable view. Built and browser-tested end to end; the game-side code compiles clean.
  • Took the analytics service live (HTTPS with an auto-renewing certificate) and added a Balance card to the developer portal. Verified the pipeline on the live host: data streaming in, anonymized charts coming out, the admin "see the real names" view locked behind the portal login, and one-click delete of a session. The in-game /balance command rides the next client build.

Infrastructure / accounts & auth

  • Reverted the developer portal to a single login and retired the single-sign-on (SSO) experiment: it had ended up making every internal tool demand its own separate login with no central place to manage accounts, the opposite of the goal. One login again covers the portal; the design tool is parked offline until it gets its own login.
  • Cleaned up the repo afterward: removed the now-dead SSO setup scripts and runbook, updated the infra docs and the security-posture baseline to match, and fixed a web-server config clash the rollback surfaced (a duplicate setting that had been blocking config reloads).
  • Refreshed the login passwords on a couple of internal game test accounts.

Stability & bug fixes (log triage)

  • Fixed a null-reference crash in the skill-advancement UI: toggling an elixir skill's learn/maintain/unlearn mode could error for a character who had learned only one of the two paired elixir skills, because the code assumed the companion skill was always present. The companion lookup is now guarded, so the toggle is safe either way.
  • Hardened the game server's calls to our web backend: if the web side returns a non-JSON error page (e.g. an HTML maintenance/error page) under an otherwise-OK status, the server now treats it as a temporary web failure and backs off, instead of throwing an unhandled error that dropped the player's store-offer/reward lookup. The unexpected response is logged (truncated) so the web-side fault is diagnosable.
2026-06-24 · 76 items

Headlines

  • Three multi-year epics crossed the finish line on the same day: the Unity 6 engine upgrade, the Perforce-to-Git migration, and Patcher v2.
  • Launched the in-game Oracle, an AI assistant in its own window that answers gameplay questions and files high-context bug reports straight to the tracker.
  • Fixed players falling straight through the world, built clients were shipping scenes with no terrain, and closed the entire binary-corruption class behind it.
  • Shipped a parallel-download launcher that multiplies patch speed for players far from the server.
  • Cut a no-change full client build from ~25 minutes to seconds with a content-signature skip.
  • Opened the client-performance campaign the new engine unlocks: object pooling, cached cameras, and off-thread warm-up to kill frame hitches.
  • Made every build regenerate the networked-action table from live code, so gameplay actions can never silently fall off again.

Milestones

  • Epic complete: Unity engine upgrade. The client is fully migrated onto Unity 6, off the long-retired 2018 line. The multi-year engine jump is done.
  • Epic complete: source-control migration. The team is fully off the legacy Perforce server and onto Git.
  • Epic complete: Patcher v2. The new rsync-style game patcher and static patch host are live and delivering updates without manual steps.

In-game Oracle (AI assistant)

  • Started an in-game Oracle, an AI helper that lives in its own chat tab. Players can ask it gameplay questions and get answers drawn from a curated, growing game-help knowledge base, or report a bug or request a feature in plain language. For reports it asks clarifying questions, checks the team's issue tracker for an existing match first, and files a ticket only when the report is new and the player confirms, with the player's account, character, location, and game build already attached, so the team gets a high-context report instead of "it's broken". The game never holds the AI key: the client talks to a small private server-side helper, and only that helper calls the AI model.
  • The Oracle can only read the separate, curated player knowledge base (never any internal or operational material), every auto-filed report lands in a triage queue for a human to review before it hits the active board, and there are per-player rate limits and a daily cap on new reports. Client and server are both built and compile-verified; switching it on for the test shard is a follow-up step.
  • Stood up the Oracle's backend: it now runs behind HTTPS on our server, with the AI key kept entirely server-side. Verified end-to-end: a gameplay question comes back with a cited answer drawn from the knowledge base, and a two-step bug report (describe it, then confirm) files a single, fully-tagged ticket with the player's account, character, and location attached.
  • Tightened the report flow after live testing caught it filing a duplicate when the player confirmed: it now confirms before filing and files each report at most once per conversation, and we set the AI to use its full context window rather than a clipped one. A follow-up balanced it the other way: it had become too cautious and kept asking for more detail instead of filing, so it now summarizes once and files as soon as the player confirms (partial repro is fine). The remaining step to reach players is publishing the in-game tab to the test shard.
  • Fixed the Oracle tab not appearing for existing characters: their saved chat-window layout predated the tab, so it's now auto-added on login. Also added an "Oracle" option to the chat-tab filter checkboxes (so it can be shown in any tab, and so editing an Oracle tab's filters no longer wipes it), and grew the filter popup so the new checkbox no longer overlaps a neighbor.
  • Rethought the Oracle's front-end after testing: a chat channel is the wrong fit for a back-and-forth assistant (typing to it from the wrong tab felt like talking into the void). Moving it to its own dedicated window — a conversation transcript + an input box, opened with /oracle or /ask — which is how the game's other panels work. Reworked the client toward that (the window, plus rewiring the request client to feed the window instead of the chat log) and removed the old chat-channel plumbing; assembled the window itself (transcript + input box, built from the existing Help-window panel) and wired it to open on /oracle — ready to test in-game.

Process / docs

  • Work-log convention fix: the blog day is now picked by US Central time (the team's timezone), not UTC, so a commit made after midnight Central lands under the new day instead of getting tacked onto "yesterday".
  • Researched Episode 2 — promised vs. delivered (a pass on what was originally planned against what's actually been built) and used it to turn the Episode 2 completion plan from a placeholder into a scoped plan. The Episode 2 continent, its zones, and most of its systems are already built and now run on the new engine, so what's left is mostly finishing the story rather than building the world. Follow-up: tightened the write-up's sourcing with verbatim quotes of the original promises (and corrected which boat goal was which).
  • Roadmap: added a Major client performance pass epic, a profiler-driven push on frame rate, hitching/GC stalls, load times, and memory now that the engine upgrade reset the performance baseline. Seeded a stub plan naming the likely cost centers (legacy UI, the manager singletons, the huge world scenes) to scope it next.
  • Researched which new Unity engine features are worth adopting now the client is off the 2018 line and on the current Unity 6 generation: a prioritized list mapped to our actual code: object pooling and async object instantiation to reduce spawn hitches, a modern async type to gradually retire coroutines, the new input system (rebindable controls, controller support), and modern build profiles, each flagged for whether it works on our current render pipeline today. Also captured the engine's upcoming deprecation deadlines that now bound the render-pipeline and scripting-runtime upgrades. Feeds the new client-performance-pass epic.
  • Surveyed the whole codebase for oversized source files (over 5,000 lines) and triaged the results, separating real refactoring targets from third-party libraries, generated code, and mirrored copies that shouldn't be hand-edited. Ranked the dozen "god class" offenders (the player controller, the editor build menu, the conversation engine, the AI brain, and more), wrote a low-risk, behavior-preserving split plan for each as a reference doc, and filed them as tracked tasks so the team can pay the debt down over time.
  • Documented an easy-to-miss contributor gotcha: a chunk of the client's "shared" code is actually a mirrored copy of the server's source, kept identical by a branching script, so editing one copy without the other gets overwritten on the next sync. Added the rule (edit one, propagate to the other) to the project's contributor guide.
  • Assessed "could the game run in a browser?" (WebGL) and wrote it up as a feasibility doc plus a roadmap entry. Not viable today; it's a very large, multi-stage effort. The main wall is content delivery and memory: the game world is hundreds of gigabytes streamed from local disk, which a browser tab (with a ~2 GB memory ceiling) can't download or hold, and that clashes with our already-settled "keep content packaged locally, not over a CDN" decision. Other hard blockers: our networking talks over raw sockets that browsers forbid (they only allow WebSockets), and several built-in features rely on desktop-only native plugins. One thing is in our favor: the build setting browsers require is the one we're on. Parked as a low-priority idea so nobody has to re-investigate it.
  • Assessed "could the game run on Android & iOS?" (mobile) and wrote it up as a feasibility doc plus roadmap entry, the phone/tablet sibling to the browser study. A very large, multi-stage effort, gated behind the render-pipeline and networking upgrades. Unlike a browser, a phone is a real native platform, so the things that block the browser version (networking, loading content from disk, native plugins) mostly just work here. The hard parts are different: the whole game is built for mouse-and-keyboard, so it would need a touch-controls and small-screen redesign; the app-store rules take a 30% cut and forbid outside payments, which clashes with our own store and player marketplace; and the game world (hundreds of gigabytes) is far too big for a phone's install size and memory. We flagged two cheaper ways to get value: a companion app for out-of-game stuff (inventory, chat, mail, marketplace), and cloud streaming, running the normal desktop game on a server and streaming it to the phone, which sidesteps the port entirely. A few mobile settings are already in place; the rest is recorded so nobody has to re-derive it.
  • Researched how player login/authentication and Steam account-linking work today, and wrote it up as an architecture reference plus a go-forward decision for the rebooted server structure. Key points: the real account backend is the website itself (the game server delegates each login to it over a signed request); sign-in comes in two forms, a normal account password and a Steam sign-in; and Steam sign-in is fully built but intentionally switched off on the test server (its client isn't delivered through Steam, so there's nothing to validate against), with a step-by-step runbook to turn it on later if we ever re-ship through Steam. Also corrected an earlier note that mislabeled the Steam path as a "stub".

Tooling

  • Refreshed the internal multi-model code-review tooling: the AI reviewer models behind our code-review and plan-review helpers were all bumped to their current generations, and one hosted reviewer was consolidated onto a single provider to simplify auth and config.
  • Added a one-shot helper + a slash-command skill for filing issues on the team's tracker — it resolves label names to IDs at run time, reads its token automatically, and sends a Unicode-safe body, so creating a task or bug is a single command instead of hand-built API calls. Used it to file the deferred "purge old binary assets from git history" maintenance task.
  • New /log-errors skill + script: pulls recent ERROR/WARN entries from our centralized log store over a chosen window (default the last 24 hours) and de-duplicates them into a ranked list of distinct problems with counts and first/last-seen — turning a wall of repeated log lines into a short, actionable triage list, with an optional hand-off to the issue-filing skill.
  • New Build Progress window for client builds: instead of one opaque progress bar, it lists each major build step, checks it off as the build runs, and shows the time each step took plus a running total, so you can see where a long build is spending its time, and which step failed if one does.
  • Refactored the build into one ordered list of named steps so the interactive window and the automated/headless build run the exact same path (no drift), with the cleanup steps always finishing even if an earlier step fails; the same steps also show in the editor's built-in progress strip.
  • Build Progress window upgrades: each step now shows the average of its last 5 runs next to the current time, the header shows a live ETA-remaining that ticks down as steps finish, and the player-build row expands to reveal the engine's own internal phases with their durations — so you can see exactly where a long build spends its time.
  • Split the build's giant "post-process" stage into separate, individually-timed steps (scene bundles, resource/character bundles, copy), and made a bundle/copy failure fail the build instead of passing silently, so a broken bundle can't ship unnoticed.
  • Profiled a full client build end-to-end: the post-process alone is ~30 min. Logged the per-phase breakdown and a ranked list of speed-ups (verify incremental rebuilds, re-save old-format assets, trim two redundant import post-processors costing ~100s/build, cache the repeated dependency walks) as a tracked task.
  • Made the ~18-min "resource & character bundles" build step expandable: it now reveals its internal phases with timings (re-serialize, asset groups, store-metadata, per-folder resources, character/conversation, compile) — surfacing the real time-sinks (store-metadata ~3 min, the Items folder ~3.5 min) right in the build window.
  • Added a remembered Safe build toggle to the build window: the pre-build sanity checks (terrain validation) are heavy and aren't needed every time, so you can now skip them for faster builds when you know assets are clean — on by default, with the skipped check shown clearly in the progress list and a visible "sanity checks off" warning.
  • Shaved ~100s off every full build by skipping two legacy asset post-processors while a build runs — one redundant nested-prefab cleanup pass (the project has moved to the engine's built-in nested prefabs) and a terrain-texture preprocessor that was needlessly re-scanning on every import. Both already had a "skip during automated builds" switch; we just extended it to cover the in-editor build too.
  • Diagnosed why a full client build still took ~25 min for the asset bundles even with zero asset changes: the engine was correctly rebuilding nothing, but our pipeline re-ran all the prep plus a full project hash every time anyway. Added a content-signature check that detects when no bundle input has changed and skips the whole bundle rebuild, reusing the existing bundles. A no-asset-change full build drops from ~25 min to seconds; a "force rebuild" toggle is there for safety. (First build seeds the signature; subsequent unchanged builds skip.)
  • Fixed and hardened the test-shard release tool. A client publish was failing because it hard-required a sync utility that wasn't installed; rewrote the upload to work without it (archive → copy → expand on the host, same pattern the server deploy already uses), and made it fail fast with a clear message if a transport's tools are missing. Also added a downgrade guard (it now refuses to flip the update channel to an older version — the original footgun), pre-fills the version to "live + 1", and added a deploy-scope switch so you can re-push just the client (or just the server) instead of always doing both.
  • Made client publishes practical again. The no-sync-utility fallback worked but re-uploaded the entire ~16 GB client tree every time (it bundles all the game assets), which was very slow. Restored proper delta sync by routing through the copy that is installed (in the Linux subsystem), so an unchanged tree now transfers tens of KB instead of 16 GB and only changed files go over the wire. Verified with a dry run. The slow full-push path stays as a clearly-labeled last resort.

Unity 6 build hygiene

  • Reverted yesterday's enabling of Dynamic Batching for desktop builds: it's deprecated in the current Unity 6 generation (slated for removal), so relying on it now would just bank migration debt — GPU instancing is the forward path for draw-call batching on our render pipeline. Filed the broader modernization follow-ups (object pooling, async object instantiation, the input-system move, modern build profiles, and more) as tracked tasks.
  • Started clearing the Unity 6 deprecated-API (obsolete-call) warnings out of the build — swept 29 editor build/tooling scripts so real warnings stop hiding in the noise.
  • Migrated ~40 object-finding calls (the old FindObjectsOfType / FindObjectOfType) to Unity 6's faster sorted/any replacements.
  • Updated the player-settings calls for scripting-define symbols and application icons to the new named-build-target API.
  • Replaced texture-compression formats Unity 6 removed (PVRTC → ASTC), swapped the old shader-property inspection calls and a 2D-physics flag for their current equivalents, and quieted a serialization analyzer warning.
  • For the handful of vendored spots with no clean modern equivalent, suppressed the warning with an inline explanation rather than making a blind behavioral change.
  • Confirmed every change compiles and that the old calls are gone; logged the rest on the tracker — a larger batch in runtime code, plus a set tied to the future render-pipeline upgrade that can't be mechanically swapped.
  • Finished the object-finding migration codebase-wide: swept the remaining ~120 deprecated FindObjectOfType/FindObjectsOfType calls across our own editor tooling and the vendored plugins (pathfinding, UI, AI behaviour graphs, water/fog, terrain tools, and more), so the old object-finding API is now gone from the entire client (86 files).
  • One trap caught while finishing it: the "sorted find" replacement that the migration guides (and our own earlier pass) recommend is itself already deprecated in the current Unity 6 generation, so using it would have re-introduced the very warnings we were clearing. Switched to the current sort-free form everywhere, including normalizing the earlier editor pass, and verified a clean compile with no object-finding warnings left. Wrote the gotcha into our engine-adoption notes so nobody re-adds the deprecated form.
  • Added a pre-deploy guard that catches the earlier networked-action (RPC) drift before it can ship: it checks that every build still rebuilds the complete action table from the live code and that the saved table hasn't fallen behind — so a missing or renamed action can't slip out in a build again.

Client performance

  • Started on the client-performance wins the new engine unlocks: in the combat code, the short-lived helper lists built every time an ability resolves (including its area-of-effect and chain targeting) are now borrowed from a reusable pool instead of being allocated and thrown away on every cast, cutting the memory churn that causes frame hitches during busy fights. Documented the reuse pattern so the team can apply it consistently.
  • Fixed a wasteful per-frame scan while aiming: the fishing-targeting code was searching the entire scene for special "override" objects (and allocating a fresh array) every single frame the line was aimed — it now scans once per cast and reuses the result, and the aiming loops reuse a cached main-camera reference instead of re-finding it each frame.
  • Extended that cached-camera win across the client: the engine's built-in "find the main camera" call searches the whole scene every time it's used, so we swapped it for a single cached reference in ~16 per-frame hot spots (weapon and spell trails, terrain tree fade, the off-screen target arrow, several spell effects, the dev console, Lua, and player targeting) and lifted the lookup out of tight loops so it runs once instead of once per item. Behavior is unchanged; it just trims repeated per-frame work.
  • De-hitched spawn warm-up: the shared object pools that recycle frequently-spawned things (combat and weapon effects, floating nameplates, projectiles, instrument audio, and more) now build their initial batch off the main thread using the new engine's async object-instantiation, spread over a few frames, instead of creating them all at once during a scene load — removing a noticeable load/first-spawn stutter. The pool stays usable immediately (it just makes one on demand if something's needed before the batch finishes), and the heavier networked-creature spawns are left for a follow-up. Added a small reusable helper so other bursty spawns can adopt the same trick.
  • Another low-risk per-frame cleanup across the gameplay code: replaced the old "fetch a component, then check whether it exists" idiom with the engine's combined check-and-fetch call, which skips a small wasteful memory allocation when the component isn't present. Applied to ~60 spots in combat, effects, decoration placement, the player, NPCs, and UI scripting, including a handful that were fetching the same component twice. Behavior is unchanged. (First-party code done; the same sweep across bundled third-party plugins is next.)
  • Carried that same check-and-fetch conversion into the bundled third-party plugins that ship in the game — the UI framework (which runs every frame), pathfinding, water, the rope/fracture/mesh tools, and more — another ~90 spots, runtime code only. Left the editor-only tooling alone (it doesn't affect the running game). Both the game and editor code still compile clean.
  • Swept the every-frame networking/sync code for throwaway memory: three hot update loops (the object-sync lock manager, interactive-object state batching, and view subscriptions) were building short-lived lists on every frame — they now borrow those from the shared pool and hand them back, cutting steady-state memory churn during normal play. Also turned two trap "remove dead entries" passes into in-place cleanups so they stop allocating a fresh list each time. The bigger takeaway, written up for the team: the rest of the client is already careful here (it reuses its lists), so the remaining cleanup targets were far fewer than the original estimate suggested.
  • Picked up the "pool the inventory/bank/vendor list rows" performance task and found the main work was already done: the shared list-UI base class only ever builds rows for what's on screen and recycles them as you scroll, so even a 200-item inventory never spawns-and-destroys hundreds of rows on refresh. Redirected the effort to the offender that approach would have missed, the quest journal, which tore down and rebuilt every task/journal/entry row from scratch on each update. It now recycles those rows from a shared pool instead, cutting the memory churn (and the little frame hitch) when the journal refreshes. Compiles clean; needs an in-game check before it's fully trusted.

Dev portal & infra

  • Deployed the self-hosted Penpot design tool (a Figma alternative) to the developer portal — live behind the Design tile, with login over single sign-on (no separate Penpot password). Memory footprint landed right around the estimate (~a couple of GB), well within the box's headroom.
  • Stood up a self-hosted single sign-on service for the portal: one login now covers the portal and Penpot, using our own per-user accounts the admin manages — so teammates who don't have a source-control (Gitea) account can still get in. This replaces the portal's old shared password.
  • Cut the live portal over to the new SSO and pointed Penpot's login at it, keeping a one-command rollback ready. The public work blog and the self-authenticating account tool stay reachable as before.
  • Verified the whole thing end-to-end in a real browser as a brand-new non-Gitea user: the portal loads after one login, then Penpot opens and auto-creates their workspace with no second prompt. Fixed several wiring issues the test caught along the way (a render service missing a shared key, the login callback path, the token-exchange method, and first-login account provisioning) before calling it done.
  • Updated the pre-deploy security sweep for the new SSO service + login model (new certificate, login-only gate, self-registration off) — it passes with zero failures. Onboarding the rest of the team as SSO users is the remaining manual step.
  • Hardened the SSO so adding a teammate's login hot-reloads instead of restarting the IdP — removes a brief blip on the portal during onboarding.
  • Marked the login-gated portal page as non-cacheable (no-store) so browsers can't replay a stale page captured around the cutover; first-time visitors and incognito were always fine, and existing users just need one hard refresh.
  • Turned on programmatic access to Penpot — a personal access token plus a Penpot automation server (70+ tools: create/modify shapes & text, upload images to a page, manage pages/components) — so design changes can be driven from our tooling, not just by hand in the UI. Verified the token authenticates as a real user end-to-end.
  • Fixed that automation server failing to start on Windows — it was being launched via a script shim the OS can't execute directly; pointed it at the real program instead. Confirmed it starts and lists its tools.
  • Wrote a backup design plan and filed it as a top-priority task: right now we have no off-box backups of our irreplaceable data (the source-control repo and its large binary store, the wiki, and the test game server), so a single disk loss would be unrecoverable. The plan turns the coming high-capacity build/backup box into an off-box target: fast incremental snapshots every few hours plus nightly whole-server backups, using filesystem-level snapshots for the snapshot-capable server and a deduplicating, encrypted, append-only tool for the other, plus a regular restore drill that actually boots a copy of the backed-up server to prove the backup works.

Stability / log triage

  • Burned down the batch of distinct errors/warnings that log triage surfaced from the test shard — root-caused and fixed each, with the fix verified to compile before commit.
  • Made log triage build-aware: each logged error is now tagged with the build it came from, so we can tell "still broken in a build that already has the fix" from "fix committed but not published yet." Documented the convention so we don't re-chase issues that are simply waiting on the next publish.
  • Build: the networked-action (RPC) table is now regenerated from the live game code on every build, instead of relying on a manual editor button. That table had drifted, so a set of gameplay actions (jump, teleport, resurrect, PvP-kill recording, emotes, and more) were being rejected at runtime as "not a recognized" action between builds.
  • Client: fixed a null-reference crash on level load when a scene's terrain had no terrain data assigned.
  • Fixed missing terrain in built clients: players were spawning into some scenes (e.g. the Solace Bridge starter area) with no ground and falling straight through. Root cause: a source-control setting was line-normalizing certain binary terrain-data files as if they were text, corrupting them just enough that the new engine could no longer load them, so the build dropped the terrain from those scenes and shipped a hole. It looked fine in the editor (which reads the live files), and only broke in a real build. Recovered the affected terrains from the authoritative source and verified they load again.
  • Closed off the whole class of corruption: the rule that routes binary assets to large-file storage only covered files over 1 MB, so smaller binary terrains slipped through and got corrupted. We now route all binary data assets there regardless of size, and recovered every affected terrain from source. A sweep separated the actually-corrupted files from three dozen others that merely differ from the old source because they were legitimately upgraded to the new engine (left untouched). Added a build-time check that fails the build if any terrain won't load, instead of shipping an invisible hole, plus a runtime log that names the exact scene and terrain when one is missing, and fixed a bug in the asset-tracking tool that made re-runs error out.
  • Client (dev console): the add-experience command now validates its arguments instead of throwing when handed a bad value; resolved an "ambiguous match" warning when a console command name has overloads; and corrected a long-standing typo in a warning string.
  • Client: an asset "slow load" warning now reports the actual measured load time instead of a misleading rounded figure, so real slow-loaders are easy to spot.
  • Server: a harmless "state already set" guard no longer spams warnings (it's an expected no-op, now logged quietly).
  • Server: the in-game store price check no longer runs on shards that aren't configured for it, so test shards stop logging a recurring connection error every few minutes.
  • Client (rendering): killed a graphics warning that spammed the player log every frame on the new engine — the depth-of-field blur was requesting a single-channel image in a format the graphics card can't produce, so the engine fell back and complained 60+ times a second, burying real warnings. It now asks for the supported format (also the correct one for that buffer), so the spam is gone and a small wasted per-frame allocation is trimmed.

Launcher / patcher

  • Added a Show debug panel to the game patcher: tick a box and the window widens to reveal a live, copyable trace of the download — per-file timing, the skip/delta/full-download decision, and per-request network detail (status, time-to-first-byte, throughput, key response headers) — so we can finally see what's happening for the handful of players hitting slow downloads with long pauses between files. The trace is also written to a per-session log file they can send us.
  • Fixed the most likely cause of those between-file pauses: the patcher was re-reading and re-hashing every already-present file on each update. It now trusts a file that's unchanged since we last verified it (size + modified-time + expected hash) and skips the full re-hash, while the Repair button still forces a complete re-verify. Shipped the updated patcher so installed copies self-update on next launch.
  • Fixed a "patches fine, but Play does nothing" bug: the publisher was telling the launcher to start the game by an old executable name the current build no longer produces, so the Play button tried to launch a file that wasn't there and silently failed (running the game directly always worked — only the launcher's Play action was broken). Corrected the publish default to the real executable name.
  • Added a pre-publish guard so this can't ship again: a build is now refused if its declared launch target isn't actually one of the packaged files, turning a silent dead Play button into a build-time failure.
  • While shipping that fix, caught and fixed a publish-side bug that defeated the delta upload: the "reuse unchanged files from the previous build" pointer was aimed one folder too deep, so a brand-new build matched nothing and re-uploaded the entire ~18 GB client every release (the earlier delta test only looked fast because it re-pushed the same build number). With the pointer corrected, the game data now reuses the prior build and a release moves tens of MB instead of 18 GB. Re-published the corrected client so the launcher fix is live.
  • Tracked the remaining "crazy slow patch" cases to the real bottleneck: the patcher downloaded files one at a time over a single connection, so players far from the server were capped by a single stream's throughput — over a high-latency link one connection simply can't fill a fast pipe (the server was never throttling anyone). It now downloads several files in parallel (default 4), which multiplies throughput on exactly those distant/high-latency connections and overlaps the per-file work between downloads. Added a Download connections setting in the launcher so a player on a slow or far-away connection can dial it up (or down on a weak machine), kept peak memory bounded so large files can't all load at once, and left the progress bar and resume/repair behaviour unchanged — all covered by new automated tests, including a latency simulation that proves parallel beats serial, and documented in the patcher component doc.
  • Shipped the parallel-download launcher to the test shard. Built a fresh, code-signed launcher, signed its self-update pointer, and published it to the test shard's update channel, verified live and serving, so installed launchers pull it and swap themselves on next start (no manual reinstall) and the speedup reaches players. Staged to the test shard first to confirm the real-world gain before promoting to the live channel.
  • Fixed the new Download connections control: the up/down spinner we'd added wouldn't respond to clicks, so the parallel-download count couldn't actually be changed. Replaced it with plain −/+ buttons (the same style the rest of the launcher uses) that reliably raise/lower the value between 1 and 16, grey out at the limits, and save the choice for the next update — covered by a new automated test.
2026-06-23 · 67 items

Headlines

  • Stood up a complete self-hosted log search-and-dashboards stack on the test box, portal-gated, TLS-fronted, and dressed in the Shroud look.
  • Built the Build Chaos command center: one window builds all six client flavors and ships them with a single Release to Chaos click.
  • Deployed the self-hosted Penpot design suite to the developer portal.
  • Caught an IL2CPP client that crashed on launch while the build reported success, then hardened the build to never lie about a pass again.
  • Hunted down the launcher's patch failures, a URL-encoding bug and a block-by-block download storm, and shipped self-updating fixes.
  • Built a one-command pre-deploy security preflight that can gate a release.
  • Routed binary Unity assets to Git LFS, ending the silent corruption and gigabytes of repo bloat.

Unity MCP

  • Fixed the Unity MCP plugin failing to connect to Claude Code — the client config pointed at a dead local port; matched it to the port the editor actually hosts, and removed a stale duplicate config (with a leftover auth header) that was shadowing the shared one.
  • Documented the root cause — the plugin derives its local port from a hash of the Unity working-directory path, so the value is per-machine — and added a "can't connect" troubleshooting section (find the live port, match the client, restart) to the ops doc.

Unity client

  • Started clearing the Unity 6 upgrade's Editor-console errors: read the live console, deduped thousands of repeats down to a dozen root causes, and filed each as a tracked task.
  • Fixed a Lua scripting bug that threw a NullReferenceException every frame once the boot script was unloaded — added a null-guard so the per-frame global-variable sync skips cleanly when no script is loaded.
  • Fixed Unity 6 dropping the built-in Arial font: pointed the Lua HUD and the Reader text-asset editor at the replacement legacy font so they no longer throw at runtime.
  • Cleaned up the SpeedTree tree shaders for Unity 6 — confirmed they compile again, fixed an uninitialized-variable warning, swapped a missing custom material-dropdown for Unity's built-in one, and normalized the file's line endings.
  • Pinned the test client to a single backend (the Chaos test server) across all build configs, and hard-locked it so a previously-saved server override can no longer redirect the connection — done as clearly-marked, easy-to-revert code blocks.
  • Disabled (but did not remove) the login-window server picker so testers can't point the client at other environments; the dialog and its handlers stay in place behind the hidden control.
  • Added a circuit-breaker to the client's log-server connection: when the logging server is unreachable it now backs off and goes quiet after a few tries instead of spamming the console all session, and reconnects automatically if the server comes back.
  • Silenced a recurring NGUI font-plugin warning by marking an obsolete 32-bit binary as disabled — the 64-bit build is the one Unity 6 actually loads.
  • Re-saved a terrain asset that was still in a pre-2019 save format, clearing its deprecation warning. Net result: the editor console is now clear of errors, with two low-priority cosmetic warnings triaged for follow-up.
  • Started clearing the Unity 6 deprecated-API warnings from our own C#: swept ~90 call sites across 50 scripts off the old object-lookup calls onto the current replacement API, and fixed two other deprecated calls (a web-request error check and a reflection-probe assignment). Editor recompiles clean.
  • Continued the warning sweep: finished moving the screen-resolution / refresh-rate code onto the current refresh-rate API, and fixed an editor-menu bug where "Create Mount archetype" was filed under the "Consumable" menu path — so it collided with the real Consumable entry, was unreachable, and warned on every editor load.
  • Imported the TextMesh Pro essential resources the project was missing (settings, default fonts/materials, shaders, and examples), clearing the "TMP Essential Resources are missing" startup error.
  • Committed 8 MicroSplat terrain-layer assets (with their .meta companions) for the Ardoris scene, tracked as text alongside the rest of the project's terrain layers.

Logging / infra

  • Planned and scaffolded a self-hosted log search-and-dashboards stack on the Chaos test box to replace the old log viewer the box can't reach — a search engine, dashboards, and a lightweight syslog collector, packaged as a one-command, re-runnable installer with retention that auto-expires old logs to bound disk.
  • Repointed the server's log shipping at the new on-box collector and added a dedicated "chaos" deploy profile so the cutover is reproducible — both the destinations and ports are rewritten at deploy time.
  • The test client already sends its logs to this box, so it needed no change: the redirect to the new backend is entirely server-side.
  • Published the dashboards behind the developer portal — login-gated and TLS-fronted, firewalled so only the portal can reach it — and wrote the ops doc plus host provisioning scripts; the admin secret stays out of the repo.
  • Deployed the stack to the Chaos box and verified it end to end: installed the container runtime, brought the three services up healthy, and confirmed test log lines flow through the collector into the search store with the right fields broken out. The dashboards are reachable through the portal with a two-step login (portal sign-in, then the log tool's own admin login).
  • Added a Logs card to the developer portal so the log dashboards are one click from the portal home.
  • Turned the log dashboards into true one-click access from the portal: a read-only viewer account is signed in automatically behind the portal login (no second prompt), the per-user view separation was switched off so everyone sees the same shared dashboards, and the viewer can browse logs but can't modify or delete anything. The full read-write admin account stays available over a private tunnel; no shared secret is committed to the repo.
  • Fixed the logs link still asking for a second sign-in: moved the dashboards from their own subdomain onto a path on the portal itself (same web address), so the single portal login now carries through. The old subdomain link redirects to the new one.
  • Wrote up the full logging runbook for the team — architecture, deploy steps, access, retention, and the gotchas we hit along the way — so the setup is reproducible from the docs alone.
  • Gave the log dashboards the Shroud of the Avatar look to match the rest of the site — dark charcoal, gold trim, the Shroud wordmark and crest, and a "Chaos Shard — Logs" tab title — reusing the existing web branding. Visuals only; no functional change.
  • Did it with no custom build: dark mode and the tab title come from the dashboards' own settings, while the gold trim, the Shroud wordmark + crest, and the favicon are served and swapped in at the existing reverse proxy (the dashboards' own server-side logo check can't see past the portal login, so the browser-side swap is the reliable path). Deployed it to the Chaos box and verified the whole look in a browser; runbook updated.
  • Finished initializing the new log box's data model: pulled the field and data-type definitions straight off the old log system and reproduced them, so money, counts, true/false flags and coordinates are stored as real typed values (numbers, booleans) instead of plain text — the foundation everything else builds on.
  • Taught the collector to break each log line's key=value pairs out into their own searchable fields (like the old system did): it splits on the first = so values that contain one survive, converts the game's capitalized true/false to real booleans, and tidies item identifiers. Economy, gameplay and item events are now queryable field-by-field, correctly typed.
  • Wired up the third log stream end to end — the item-audit trail (item create/destroy and gold-change events) — into its own typed store, including a small deploy-time fix so those records are delimited the way the collector expects.
  • Rebuilt the core dashboards from the old system and committed them so the installer recreates them automatically: an Economy board (gold flow, top items and players, vendor sales, scene activity), a Server/Debug board (log levels, errors, concurrency, busiest scenes), and an Item Audit board.
  • Verified the whole pipeline on the box with synthetic events — every field lands with the right type and all three dashboards populate and chart real numbers — then cleared the test data so real traffic starts clean. Captured the old system's panels and schema in the repo as reference and updated the runbook.
  • Fixed the logs link landing on an "Application Not Found" page — pointed the default route straight at the dashboards list so opening Logs now drops you right on the boards (Economy / Server Health / Item Audit) instead of a generic home page that linked to a view this build doesn't have.

Dev portal & infra

  • Planned and scaffolded a self-hosted Penpot (open-source UI/UX design & prototyping tool, a Figma alternative) for the developer portal — one-command, re-runnable installer scripts that stand it up as an isolated container stack with its own database, plus a full ops runbook and a backup job.
  • Wired it for single sign-on: Penpot signs you in with your existing developer-account identity, and the portal itself moves from a shared password to that same account login — so one sign-in reaches the portal and every tool behind it. The portal login change ships with a safe, reversible rollback.
  • Sized the memory up front (the design tool wants ~a few GB; the box has ample headroom), updated the security baseline for the new service and login model, and linked the new docs from the index. Box-side rollout is staged pending go-ahead.
  • Added the Design (Penpot) tile to the developer-portal home so it's one click from the landing page; it opens Penpot once the box-side rollout lands.

Build tooling

  • Added a new ChaosShard editor menu with a Build Chaos window — one dashboard to build all six client flavors (Windows / Mac / Linux, each in dev or release; quick or full build), each with its own Build button and a live status badge: up to date with the latest commit, how many commits behind, when it was last built, and its size.
  • Factored the build logic so CI can run the exact same builds unattended later; outputs go to a project-relative, gitignored folder. The window also surfaces the server build state and what's currently deployed — the live client channel plus a local deploy record, with an optional live read for those who have access.
  • Added an IL2CPP option for the Windows builds alongside Mono — the window now lists both backends for Windows, sets the chosen backend just for that build and restores it afterward, and writes IL2CPP output to its own folder so it never overwrites the Mono build.
  • Tracked down a hard editor crash during a full build and fixed it: the engine's asset-bundle writer crashes in native code (so it can't be caught) on any terrain whose grass-detail layer lost its texture in the engine upgrade. Added a validator that auto-repairs those terrains right before the bundle step, exposed as a menu item and a command-line entry for one-off sweeps.
  • Added a pre-flight log of the scene build order before that single big native bundle call, so if any one asset ever crashes it again, the last line written names the exact scene — which is how the bad terrain was found.
  • Audited the full Unity 6 build log (a ~30 MB, 300k-line editor log) by scoring it with deduplicating searches instead of reading it end to end — collapsing thousands of repeated lines into a handful of root causes, all upgrade residue (the build itself still succeeds with zero compile errors).
  • Filed each category as a tracked task with the exact file/line, counts, root cause, proposed fix, and a verification check: a legacy nested-prefab plugin throwing hundreds of build-time errors and dominating import time (Unity 6 does nested prefabs natively now), ~500 terrain/lightmap assets still in the old save format, a handful of terrains that fail to load, ~140 deprecated-API warnings, build-size waste (oversized sky textures + demo art force-shipped via Resources folders), shader warnings, and degenerate flat-quad colliders.
  • Reopened the earlier "re-save old terrains" task once the log showed the problem was project-wide (~500 files across many scenes), not the single file it was originally scoped to.
  • Wrote a build-health ops doc: where the log lives, a reusable "grep scoreboard" to re-score any build before/after, and the audit findings — each linked to its tracked task.
  • Added a one-click Release to Chaos button to the Build Chaos window (with a confirmation dialog): it deploys the freshly built server to the test shard, publishes the matching client patch, and runs a login smoke-test, collapsing a multi-step runbook into one reviewed click. Runs in the background with live progress; on success it records what was deployed so the window's "currently deployed" panel updates.
  • The work is plain PowerShell under tools/deploy/ (so CI can run it headlessly too): it reuses the existing security-posture table for connection details — no addresses or secrets in the scripts — reaches the test VM the documented way, and deliberately preserves the VM's environment-specific config and signing key so a fresh build can't break test logins. Supports a dry-run that previews every step without touching anything, with distinct exit codes per phase so a failure says exactly what broke. Verified the editor compiles clean and the dry-run/​error paths behave.
  • Gave it its own panel plus a dedicated Release console window: it polls what's currently live on the test shard (server and client versions) and shows it side-by-side with the freshly built local version, with a one-click "release to make them match" button (same confirmation + live progress). Refactored both entry points to share one gate/confirm/launch path so they behave identically.
  • Taught the build window's optional live-server read to reach the test VM the documented way, auto-filling the connection defaults from the tracked security-posture file, so there's nothing to type and every field stays overridable.
  • Added a client flavor picker to the release flow so you can choose which built Windows client to push — dev or release, on either scripting backend — all to the same channel. Fixed the release button greying out for locally-built clients: those don't carry a build number (only CI stamps one), so added a patch-version field to set the channel version by hand, with the publish step refusing a zero/blank version.
  • Pushed the dev client to the test patch channel and took it live: staged + signed the build, streamed all ~1,000 files (~16 GB) up to the patch host over SSH (the box was missing the usual sync tool, so used a tar-over-SSH stream instead), flipped the channel pointer, and verified end-to-end: signature valid, every file reachable, and partial-range downloads working (so the patcher can do deltas). Also fixed a wrong channel URL baked into the editor tool that omitted the platform sub-path and was returning not-found.
  • Then players' patches all failed at the same spot (~halfway, ~7.5 GB). Tracked it to a launcher bug: some asset-bundle filenames legitimately contain # or spaces, and the launcher wasn't URL-encoding download paths; a bare # truncates the URL (everything after it is treated as a fragment), so those files came back not-found. Fixed the patcher to percent-encode each path segment, added a regression test, and proved it by re-downloading the exact failing files through the real patcher code. Shipped a new self-updating launcher build so installed launchers auto-update and then patch cleanly, with no manual reinstall.
  • Next, patches crawled for several minutes on the last few files. Cause: for a file that already exists locally but changed almost entirely (e.g. the engine runtime DLL across a version bump), the patcher was fetching it one 16 KB block at a time — thousands of tiny requests, latency-bound, minutes per file. Fixed it to fall back to a single streamed download when little can be reused, and to batch contiguous changed blocks into one request. Verified on the real 84 MB runtime DLL: ~5,400 requests/minutes → 1 request/~2 seconds, exact hash. Added a regression test that fails if the per-block storm ever returns, and shipped another self-updating launcher build.
  • Rounded out the launcher from the patch debugging: the "Downloaded" figure now shows how much of the patch you actually have (transferred + reused), so it reaches 100% at full size instead of stalling at a partial number; added a cross-platform guard (Windows/Mac/Linux) that detects the game already running and offers a one-click "Close game & update" before patching (a running game locks files — the patch fails on Windows / risks a mixed install elsewhere); and the launcher now shows its OWN build number under the game version. Bundled all the patcher fixes into one self-updating launcher build.
  • A worse trap: an IL2CPP client build crashed on launch yet the build window reported it as a success. Two bugs: the build wrapper was ignoring the engine's build result (a leftover "skip errors for now" hack), so a failed native compile got stamped as a clean build; and the real failure was a third-party terrain asset whose legacy UI code can't be compiled ahead-of-time on the new engine. Made the window verify a build truly succeeded (engine result + the player exe + the IL2CPP native lib all present) so a broken build can't pass as a clean one, and ripped out the ignore-errors hack so failures surface. Offending asset removed; IL2CPP now produces a runnable client.

Repo / Git LFS

  • Found that binary Unity assets (terrains, lightmaps, navmesh, texture arrays, baked meshes) were being committed as raw git blobs and line-ending-normalized — which silently corrupts binary data and had bloated the repo by gigabytes, because a catch-all rule treated every .asset as text.
  • Routed just the binary ones to Git LFS (the text/YAML assets stay mergeable) via an exact, generated per-file rule set, and converted the existing large binary assets to LFS pointers — forward-only, with no history rewrite, so the version-control bridge stays intact.
  • Added a helper script that regenerates the rule set by content-detecting binary assets, and documented the policy plus a deferred history-purge runbook (to reclaim the old bloat later, after the version-control migration).

Security

  • Built a one-command pre-deploy security check (a reusable /security-preflight skill) for the Chaos test server and the companion web portals — it verifies the documented security posture still holds before a deploy and exits with a failure code if anything is wrong, so it can gate a deploy. Non-destructive: it only connects, reads, and reports — no fuzzing or brute-force.
  • From outside (black-box): confirms only the intended public ports answer and that sensitive internal services refuse from the internet. Plus TLS/certificate health (validity, expiry, name match), security headers, and that the login-gated portals actually require auth with self-registration turned off, and the patch host rejects directory listing and path traversal.
  • From on the boxes (over SSH): checks the firewall rules, that internal services bind to loopback only, and that the host's port-forwarding exposes only the intended public ports, including into the Windows VM.
  • Added a guard that fails the run if an IP, internal hostname, or secret ever slips into this public devlog — enforcing our own redaction rule automatically.
  • Kept it maintainable: a single "expected posture" data file is the source of truth (edit it when the infra changes, not the checks), with a full ops doc. Validated live end to end across both boxes and the VM — the run is green, and it already surfaced a couple of forwarded ports worth a human review.

Planning

  • Researched modernizing the Unity asset-bundle system (Addressables) — effort, building, in-game loading, patch deltas, and whether content can run hands-off — and wrote a decision doc. Key finding: the client already has a mature custom asset-bundle system behind a clean facade, and the new delta patcher already ships only the changed bytes of the whole build tree, so a "modern" system is mostly a tooling/maintenance upgrade, not a delivery one.
  • Decision: keep the custom bundles through the Unity 6 upgrade (delivery is already effectively hands-off); adopt Addressables only later, packed locally behind the existing facade, never as remote/CDN content. Filed a deferred backlog epic tracking the decision and pointing at the doc.

Documentation

  • Started a living feature roadmap doc that collects the big future epics in one place — render-pipeline modernization, asset bundles, finishing Episode 2, a full combat-skill balance pass, and a website rebuild — each with priority/effort/status and a link to its deeper plan; wired it into the docs index and the repo tour.
  • Scaffolded ready-to-fill planning stubs for the new epics, and added a standing convention (to the project guide) to keep the roadmap current whenever an epic is added, advanced, or completed.
2026-06-22 · 28 items

Headlines

  • Gave the launcher a full Shroud makeover with live patch progress, self-update, and a one-file self-installer.
  • Proved the new block-delta patcher for real: a ~100-byte edit moved ~17 KB over the wire on a 430 MB asset.
  • Backed everything up and opened the dedicated branch for the Unity 6 engine upgrade.
  • Wired the Unity editor to Claude Code over MCP so the live editor can be driven directly.
  • Drafted the plan to give every NPC a spoken voice with archetype-cast TTS.

Patcher / Launcher

  • Gave the launcher the Shroud of the Avatar look — the gold "Forsaken Virtues" wordmark, the heraldic shield window icon, and a gold-on-charcoal theme — reusing the legacy launcher's own art.
  • Surfaced rich live progress while patching: current file, network speed, the higher "effective" delta speed and bytes saved, total size, and version.
  • Added launcher self-update — a signed pointer, download plus SHA-256 verify, then a running-exe swap that keeps the Patcher.exe alias in sync; a failed self-update is a safe no-op.
  • Hardened long downloads with retry/backoff so a ~16 GB first install survives transient network blips.
  • Published the first real game build to the patch host and proved a real 430 MB asset delta — a ~100-byte change moved only ~17 KB over the wire.
  • Added a single-exe self-installer: ship one signed file; on first run it asks where to install, creates the folder, copies itself in (launcher + Patcher.exe alias + config), and launches the installed copy.
  • Wired Authenticode code-signing and stamped the launcher as Catnip Games (self-signed for now; a real certificate to follow).
  • Packaged a self-contained, single-file launcher so players need no separate runtime installed.
  • Fixed the first-run installer exiting instantly (a single-instance lock race); it now defaults to Program Files, elevates to install/self-patch there, and writes a diagnostic log.
  • Embedded the SotA shield as the launcher's exe and taskbar icon.
  • Rebranded the launcher to "Chaos Shard" (window, installer, install folder, shortcuts, and file metadata).
  • Reworked the installer to elevate just once at install (so it can live in Program Files), then run and self-patch with no further prompts.
  • Made the installer create Start-menu and desktop shortcuts.
  • Roughly halved the launcher download (about 100 MB down to ~48 MB) via single-file compression and dropping unused globalization data — no trimming, no behaviour change.
  • Added a one-command release script that ships a launcher self-update end to end: pin the embedded build, publish and sign the single-file launcher, write the signed channel pointer, then push to the patch host (exe first, signed pointer last) so installed launchers update on next start.

Unity client

  • Removed the unused embedded-browser plugin and its orphaned in-game web-map: the feature was never reachable in play, so the live native map is unaffected and a large pile of bundled native binaries no longer ships.
  • Tracked down why a couple hundred terrain-config assets kept showing as modified — an editor import step re-saves them whenever the project is reimported — so the spurious churn stops getting swept into unrelated commits.
  • Backed up everything ahead of the Unity 6 engine upgrade: froze the multi-GB asset import cache to a side copy, recorded the current project state in version control as a restore point, and opened a dedicated upgrade branch so the shippable line stays intact.
  • Swept the Unity 6 engine's renamed APIs across ~30 third-party plugin scripts (NGUI, NodeCanvas, Gaia, RootMotion, imposters, water/fog, and more) so they compile on the new engine — object-finding, rigidbody velocity, animator update mode, and the instance-ID → entity-ID rename.

Dev portal & infra

  • Planned fronting the patch host with a CDN so big-download bandwidth scales beyond a single box; filed it for tracking.
  • Converted the legacy datacenter configuration spreadsheet into a searchable docs reference so the infrastructure inventory is greppable and lands in the wiki; addresses and shared passwords redacted.

Tooling & bridge

  • Standardized issue filing on the Gitea REST API, with a copy-paste recipe and the current label IDs documented.
  • Made the work-blog update a mandatory, commit-triggered step.
  • Reworked the wiki publisher to group docs into two shelves — a general team shelf and an access-restricted Internal Only shelf — preserving each shelf's permissions and any wiki-authored books on republish; the sync skill now keeps internal/restricted pages out of git by default.
  • Fixed a list-mode crash in the import-cache version-swap helper (the tool that lets us flip between engine versions by renaming caches instead of reimporting for hours); it tripped whenever a live cache folder was present.
  • Wired the Unity-MCP editor plugin (Ivan Murzak's open-source com.ivanmurzak.unity.mcp) into the Unity 6 upgrade branch via OpenUPM, so Claude Code can drive the live editor — read the console, run editor commands, inspect scenes — over a local localhost MCP endpoint; install/activate/troubleshoot documented.
  • Vendored that Unity-MCP plugin and its runtime dependencies straight into the repo (upgraded from the registry build) so the editor resolves it from disk with no download; refreshed the setup doc and stopped tracking the auto-generated IDE solution file.

Planning

  • Drafted a plan to give every NPC spoken dialogue: archetype-cast inworld.ai TTS voices, audio pre-generated to a CDN with player-name fragments generated on demand, piloting on one starter town first — including the cross-process cache-key design that keeps the offline generator, game client, and server byte-for-byte in lockstep.