Skip to content

Roll named beaver rows up to their kind (NF1) - #1

Merged
kramsey458 merged 3 commits into
mainfrom
claude/beaver-rollup
Sep 22, 2026
Merged

kramsey458 merged 3 commits into
mainfrom
claude/beaver-rollup

Conversation

@kramsey458

Copy link
Copy Markdown
Contributor

Summary

Entity rows in profile.csv and summary.md are now keyed by the entity's kind, not by the name the tick system recorded.

When the game loads a character from a save, it renames it to <template> <its own name> (NamedEntityGameObjectSynchronizer, at PostInitialize). That happens before TickableEntityLifecycleManager records entity.Name. A character made during play keeps Unity's <template>(Clone). As a result, every loaded beaver got its own row. In the real 0.1.3 session 2026-09-21_23-13-11 there are 316 BeaverAdult <name> / BeaverChild <name> keys, and the entity table shows BeaverAdult(Clone) at 14% of entity time when adult beavers are really about 85%.

tools/perflog.py applies the same rule to older recordings. report now ranks beavers correctly, and compare lines an old recording up with a new one.

Items

NF1: beaver rows are split by name, so beavers are badly under-ranked (fixed)

What changed

  • source/Core/Profile.cs: Profile.IdFor keys ProfileKind.Entity by EntityKindOf(name): the text before the first ' ' or '(', trimmed, or the whole name if that leaves nothing.
    • It runs only on a dictionary miss and stores both raw name -> kind id and kind -> id. Each distinct name costs one lookup-and-register, and every later sampled tick stays the existing allocation-free lookup.
    • A name that already is a kind resolves to the same id.
    • Singletons, load steps, components and watched methods keep their exact names.
  • tools/perflog.py: entity_kind applies the identical rule to entity rows in profile_totals. That covers report, JSON report and compare.
  • A KNOWN_ISSUES entry, fixed in 0.1.4, says the recording's own profile.csv and summary.md are still split. After review, this note is printed only when the recording's entity rows really are split (KNOWN_ISSUE_APPLIES, see Review).
  • Docs:
    • CLAUDE.md gets a not-obvious bullet.
    • In TESTING.md the check counts, the ProfileTests row and five-minute-check step 7 are updated, plus a not-yet-played item 9.
  • CoreTests.WordsAndTails used an entity key only to test CSV cleaning of a name with a space. It now uses a method key, which keeps the test's intent.

Tests that prove it

  • C#, tests/ProfileTests.cs, check "Profile: a beaver's own name and Unity's '(Clone)' are counted under the entity's kind, and only for entities". It checks:
    • one row per kind;
    • raw name and kind get the same id;
    • other kinds keep their exact names;
    • a table of rule cases shared with Python;
    • 0 bytes allocated over 1000 sampled ticks of names already seen.
  • Python, tools/test_perflog.py EntityRollupTests:
    • the entity_kind case table (the same cases as C#);
    • test_named_entity_rows_are_rolled_up_to_their_kind: totals and the report show BeaverAdult at 78%, no Malak, and the known-issue line;
    • test_an_old_recording_lines_up_with_a_new_one: compare shows no (only in lines;
    • test_a_recording_whose_entity_rows_are_kinds_gets_no_split_note: an already rolled-up 0.1.3 recording and the checked-in fixture get no split note.

Fail before, pass after

  • C# before the fix: FAIL ... one row per kind of entity: expected BeaverAdult,BeaverChild,DistrictCenter.Folktails, got BeaverAdult Malak,BeaverAdult Zengu,BeaverAdult(Clone),BeaverChild Malak,DistrictCenter.Folktails(Clone) (91/92). After: 92/92.
  • Python before the fix:
    • test_named_entity_rows_are_rolled_up_to_their_kind: first differing element ('entity', 'BeaverAdult') vs ('entity', 'BeaverAdult Malak');
    • the compare test: '(only in' unexpectedly found;
    • the case-table test: AttributeError: no entity_kind.
  • Review follow-up: with the KNOWN_ISSUE_APPLIES filter removed, test_a_recording_whose_entity_rows_are_kinds_gets_no_split_note fails (the note is listed for a recording of kinds). With the check forced to false, test_named_entity_rows_are_rolled_up_to_their_kind fails. Both pass with the change.
  • Mutation runs by the reviewers, each failing for its stated reason:
    • dropping raw-name caching: 48000 bytes allocated;
    • applying the rollup to every kind;
    • no trim;
    • no empty-kind fallback;
    • profile_totals not calling entity_kind.
  • Real data (read-only):
    • The analyzer on 2026-09-21_23-13-11 now shows BeaverAdult 85%, DistrictCenter.Folktails 5%, BeaverChild 2%, and still prints the split note.
    • Across all 26 local recordings, 543 distinct entity names map to 128 kinds, and no two distinct (Clone) templates merge.
    • None of the game's 393 blueprint TemplateNames, and none of the installed Workshop and org mods' blueprints, contains ' ' or '('.

Tests

Suite Before (a1c6d1c) After (01cdc14)
C# dotnet run --project tests -c Release ... 91/91 92/92
Python python -W error::ResourceWarning -m unittest discover -s tools -p test_perflog.py 44 OK 48 OK
Mod build dotnet build source/PerformanceLog.csproj -c Release 0 errors 0 warnings, 0 errors

Fixtures are unchanged: no columns changed, and the sample session's entity names have no ' ' or '('. git diff --check is clean.

Co-op impact

None. PerformanceLog only observes. This changes no Harmony patch, no patch priority, no simulation state and no wire format. EntityKindOf is a pure string function, run on the game thread inside the existing try/catch in EndEntity, and idsByName is still cleared at session start. Co-op players do not need to update together (beyond BeaverBuddies' usual mod-list version warning once a release is cut).

Proposed CHANGELOG lines

(Not added to CHANGELOG.md. The KNOWN_ISSUES entry assumes the release carrying this is 0.1.4; if it gets another number, change the string in tools/perflog.py to match.)

  • Entity kinds are one row each again. A beaver or bot loaded from a save was keyed by its own name ("BeaverAdult Malak") and one born during play by "BeaverAdult(Clone)", so beavers ranked far too low: 14% of entity time instead of 85% in a real recording.
  • perflog.py adds up older recordings' entity rows the same way, so report and compare line old and new up. It lists the split as a known issue of 0.1.3 and older, but only for a recording that is actually split.

Needs in-game / two-player testing

Solo is enough; this does not touch simulation. Use a local build of this branch, never the Workshop or the live Mods folder from an automated run.

  • Load an existing save with a colony and play about 3 minutes. In the new session's profile.csv, every entity row should be a kind (BeaverAdult, BeaverChild, DistrictCenter.Folktails), with no BeaverAdult <name> rows and no ...(Clone) rows. summary.md's "Entity kinds (sampled)" table should show BeaverAdult near the top (about 85% of entity time on a big colony).
  • In the same session, let a beaver be born or grow up. It should land on the same BeaverAdult/BeaverChild row. On an Iron Teeth save, bots should appear as Bot.IronTeeth.
  • python tools/perflog.py report <the new folder> should print no "Entity rows are split by name" note, even though the build still says 0.1.3.
  • python tools/perflog.py compare <a 0.1.3 recording, e.g. 2026-09-21_23-13-11> <the new recording>: section 4 should list entity kinds with values in both A and B, with no (only in A)/(only in B) lines for BeaverAdult or building kinds.

Review

Three adversarial reviewers approved: determinism/co-op, save compatibility/cross-mod, and "is the test real". They found no blocker or major issue.

Fixed in 01cdc14:

  • Stale note (minor, flagged by all three). The KNOWN_ISSUES entry was keyed only by "fixed in 0.1.4", and this branch cannot bump the version. So the checked-in fixture, the CI "Report on the sample session" step and any recording from a build of this branch (all saying 0.1.3) printed "Entity rows are split by name" although their rows are already kinds. known_issues() now consults KNOWN_ISSUE_APPLIES, a map from a note to a check on the recording, and the split note is printed only when some entity row's name is not its own kind. KNOWN_ISSUES keeps its (fixed in, note) shape, so other branches adding plain entries are unaffected.
  • Note wording (nit). It said summary.md lists "hundreds of rows", but its entity table is capped at 15. It now says profile.csv has rows named after single beavers and the in-game summary.md ranks beavers too low.
  • Weakened 0.1.1 assertion (nit). The 0.1.1 known-issues test only checked one note. It now asserts that none of the notes fixed by 0.1.1 is printed.
  • Modded template names (nit, hypothetical). A modded template name containing a space would be cut short. No data has one (checked across the game, installed Workshop mods and org mods). The limitation is recorded in the CLAUDE.md bullet; no code change.

Declined (out of scope, pre-existing, not touched by this diff):

  • compare() never prints known_issues() for either recording. This predates this PR, and compare's figures are already rolled up correctly. It is left as a separate follow-up so this PR does not change compare's section 1 while other PerformanceLog branches edit compare.
  • WriterTests "a file someone holds without sharing is retried" failed once in 7 reviewer runs with a sharing violation on frames.csv, a race in the test's own FileShare.None open. Neither the test nor LogWriter is in this diff. It passed in every run here.

Coordination: other PerformanceLog branches (NF2 and others) also add ("0.1.4", ...) KNOWN_ISSUES entries and change the TESTING.md check counts, so expect small conflicts there when they land.

🤖 Generated with Claude Code

kramsey458 and others added 2 commits September 22, 2026 01:42
The game renames every character loaded from a save to "<template> <its
name>" (NamedEntityGameObjectSynchronizer, at PostInitialize) before
TickableEntityLifecycleManager records entity.Name, while a character
made during play keeps Unity's "<template>(Clone)". Profile keyed entity
rows by that raw name, so the real 0.1.3 session 2026-09-21_23-13-11 has
316 "BeaverAdult <name>" / "BeaverChild <name>" keys, and its entity
table shows BeaverAdult(Clone) at 14% of entity time when adult beavers
are 85% of it.

Profile.IdFor now keys ProfileKind.Entity by EntityKindOf(name): the text
before the first ' ' or '(', trimmed, or the whole name if that leaves
nothing. It runs only on a dictionary miss and remembers raw name -> kind
id, so each distinct name costs one lookup-and-register and every later
sampled tick is the existing allocation-free lookup. A name that already
is a kind resolves to the same id. Singletons, load steps, components and
watched methods keep their exact names.

tools/perflog.py applies the identical rule to entity rows in
profile_totals (entity_kind), so reports of older recordings rank beavers
right and compare lines an old recording up with a new one instead of
listing every entity kind as only in A or only in B. A KNOWN_ISSUES entry
(fixed in 0.1.4) says the recording's own profile.csv and summary.md are
still split.

Every entity name in the local recordings is "<template>(Clone)" or
"BeaverAdult|BeaverChild <name>", and none of the game's 393 blueprint
template names contains a space or '(', so the rule merges no distinct
kinds. CoreTests.WordsAndTails used an entity key only to test the CSV
cleaning of a name with a space; it now uses a method key.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Review follow-up. The KNOWN_ISSUES entry for split entity rows was keyed
only by version ("fixed in 0.1.4"), but this change does not bump the
version, so every recording made by a build of it, and the checked-in
fixture the mod's own code wrote, still say 0.1.3 and were told their
entity rows are split when they are already kinds.

known_issues() now also consults KNOWN_ISSUE_APPLIES, a map from a note
to a check on the recording. The entity-split note (ENTITY_SPLIT_NOTE)
is printed only when some entity row's name is not its own kind, so the
real 0.1.3 session 2026-09-21_23-13-11 still gets it and the fixture and
unreleased builds do not. Notes without a check behave as before, and
KNOWN_ISSUES keeps its (fixed in, note) shape.

The note now says profile.csv has rows named after single beavers and
the in-game summary.md ranks beavers too low (its entity table is capped
at 15 rows, so "hundreds of rows" was only true of profile.csv). The
0.1.1 known-issues test asserts that none of the notes fixed by 0.1.1 is
printed, instead of one of them. CLAUDE.md records that a modded
template name containing a space or '(' would be cut short (none does).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts:
#	docs/TESTING.md
@kramsey458
kramsey458 marked this pull request as ready for review September 22, 2026 19:49
@kramsey458
kramsey458 merged commit 3002271 into main Sep 22, 2026
@kramsey458 kramsey458 mentioned this pull request Sep 22, 2026
@kramsey458
kramsey458 deleted the claude/beaver-rollup branch September 22, 2026 19:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant