Devlog — Ricky — Week 38, 2026¶
- [2026-09-15 16:48] Data Layer policy + runtime spike - DONE, submitted from CL 115139 (approved by review; PIE by Ricky).
POLICY (wiki: Rephrased Wiki/wiki/Systems/DataLayers.md, new page + mkdocs nav)
- Six shared Editor Data Layers in Content/Rephrased/DataLayers: DL_Ed_Art, DL_Ed_Gameplay, DL_Ed_Logic, DL_Ed_AudioVFX, DL_Ed_Blockout, DL_Ed_Debug, each with its own outliner colour.
- Runtime layers: DL_RT_
RUNTIME SPIKE - PASSED (PIE, 2026-09-13, Lvl_DataLayerExample)
- Runtime layer toggle swaps actors both in the act map and inside the WP room. Visuals and collision swap together. Repeated A->B->A toggles logged no WP/LevelInstance errors.
- Console: wp.Runtime.ToggleDataLayerActivation
REFERENCE RULE - the load-bearing finding - Hard reference from an always-loaded actor into a runtime layer: MapCheck error at PIE start ("references an actor in a different set of runtime data layers") AND the target stays loaded when its layer unloads, so the swap silently fails. Observed: layer reported Unloaded while the room cube stayed visible. - Soft reference: no error, unloads and reloads correctly. But a soft ref still resolves to the old actor until GC (~5s, confirmed with obj gc), so gameplay must read the Data Layer runtime state, not reference validity. - Tested with a throwaway BP_RefProbe (hard + soft var, 1s print). Probe deleted; not submitted.
SAVE/LOAD (future work, recorded per ticket) - Must persist each runtime layer state (asset + Unloaded/Loaded/Activated) and the gameplay state of mutable actors inside runtime layers - Unloaded destroys actors, so runtime changes are lost. Initial states are design data in the map, not save data.
PERFORCE COST (measured) - Layer visibility toggle: no checkout. Actor assignment: only that actor's ExternalActors file. Adding a layer or changing Initial Runtime State: the map's WorldDataLayers actor file - one shared lock per act map, so layer-list changes must be planned.
CONTENT MOVES IN THIS CL - Example map moved to personal scratch: Content/Developers/Engineer/Ricky/Lvl_DataLayerExample + Rooms/LI_Test03 + DataLayers/DL_RT_Test_StateA/B. - Content/Rephrased/Levels/Act3/Lvl_Act3 recreated as an empty Empty Open World (WP, streaming ON) with the six editor layers registered. Done as an edit of the same depot files (p4 revert -k + p4 edit after redirector cleanup), so Lvl_Act3 keeps its 003.2 history and it stayed a single changelist. - 003.3 cleanup: LI_Test01, LI_Test02 and their placement actors, old Rooms/LI_Test03 path, DL_Test probe layer all deleted.
TRAPS WORTH REMEMBERING
- Renaming/moving a WP map only moves the actors currently LOADED. 8 cube actors were left orphaned in the old ExternalActors folder with their owner already repointed; they had to be recreated. Load the whole map before moving or renaming a WP map.
- A move leaves redirectors at the old path, and a redirector blocks saving a new asset there. 5.8 calls the fix "Update Redirector References".
- Editor revision-control status can go stale after p4-side changes (showed "marked for delete" while p4 said edit). Right-click > Revision Control > Refresh; do not revert from that indicator.
- content_check reports 19 false ERRORs for WP external actor files under Content/Developers/
STILL OPEN - Cooked/packaged build behaviour untested; everything above is PIE. - HLOD interaction and the stacked Cascade Town sheets untested - revisit during the HLOD validation pass. - DataLayers.md is status: draft pending Gabriel's review.
- [2026-09-17 16:31] SAVE-001 Save/Load Schema - C++ complete in CL 115886, NOT yet compiled or PIE tested (human step).
SCHEMA (new, Source/Rephrased/Save/)
- URephrasedSaveGame: SchemaVersion (CurrentSchemaVersion=1) + MigrateToCurrent, CheckpointId, LevelPath, PlayerTransform, TArray
ARCHITECTURE - the load-bearing decision - Receivers PULL, they are not pushed to. Runtime data layer unload destroys actors (W37 finding) and rooms stream in minutes later, so there is no single load-time moment to push state. URuneableObjectComponent::BeginPlay asks URephrasedSaveSubsystem for its own record; already-live receivers are pushed to in FinishRestore, which covers reload-in-place. - Subsystem is GameInstance-scoped, not World: restore can span an OpenLevel. - Restore order per SaveLoad.md: read version -> migrate -> open level if different -> set data layer states -> poll UWorldPartitionSubsystem::IsAllStreamingCompleted -> apply live receivers -> player transform. - ApplySavedRecord assigns Status through FilterToAllowed rather than SetStatus on the BeginPlay path, so presentation is not rebuilt from inside Super::BeginPlay; the owner's existing RefreshPresentation does it.
IDENTITY - URuneSaveIdComponent: authored FName SaveId, seeded once in OnComponentCreated from a short guid + actor label, never re-seeded (re-seeding orphans every existing save). MarkPackageDirty so the CallInEditor fix-up button actually persists. - Seeded on creation, NOT on load, deliberately: auto-seeding at load would dirty WP ExternalActors files and trigger checkouts under exclusive checkout. - Empty id -> IsDataValid error. Duplicate id -> logged as an Error at capture and skipped, since duplicates are only detectable at map scope. - Player character deliberately has no SaveId: it is runtime-spawned from a PlayerStart and is saved as a single named record.
VERIFIED AGAINST 5.8 SOURCE (no compiler in the agent loop, so APIs were read not assumed) - AActor::ActorGuid is WITH_EDITORONLY_DATA and GetActorGuid() is WITH_EDITOR. A guid-only identity would not have compiled in a packaged build - this is why the authored-id contract in SaveLoad.md is right. - UGameplayStatics never sets ArIsSaveGame, and FProperty::ShouldSerializeValue only honours CPF_SaveGame when Ar.IsSaveGame(). So UPROPERTY(SaveGame) filters NOTHING: every non-Transient UPROPERTY on a USaveGame persists. Use Transient to actually exclude. Recorded in SaveLoad.md. - UObject::IsDataValid is WITH_EDITOR-guarded; EDataValidationResult lives in UObjectGlobals.h, not DataValidation.h. - UDataLayerManager::GetDataLayerManager / GetDataLayerInstances / SetDataLayerRuntimeState / GetDataLayerInstanceRuntimeState, UDataLayerInstance::GetAsset+IsRuntime, EDataLayerRuntimeState {Unloaded=0,Loaded,Activated}, UWorld::RemovePIEPrefix all confirmed present. No new module deps: World Partition and data layers are in Engine.
TRIGGERS - ACheckpointVolume: invisible box, authored CheckpointId, bSaveOnce. Defers the write while a command orb is in flight or a puzzle is sliding / playing its repair cinematic, retrying on a timer - keeps mid-puzzle saves out of scope. - Debug keys F5/F9 via DebugExecBindings in Config/DefaultInput.ini -> Rephrased.Save.Checkpoint / .Load, the same entry points the volume uses. DebugExecBindings is stripped from shipping, so this cannot become a player-facing manual save. - Feature is inert until a volume is placed. No volume went into a shared map.
WIPE-ON-PLAY - The BeginPlay wipe is KEPT and on by default (fresh state is easier for everyday dev), but now routes through WipeAllSaves so it follows the slot instead of the hardcoded RuneKnowledgeSave string, and also clears the legacy slot and orphaned clip slots. - Rephrased.Save.WipeOnPlay 0 preserves saves for testing; CVars persist across PIE runs in one editor session.
TWO BUGS CAUGHT IN SELF-REVIEW (no compile loop, so this mattered) - Making the save resident at Initialize meant receivers would pull saved status on a NORMAL play start while data layers and player transform stayed authored - a half-restored world. Fixed with bRestoreActive: residency alone never hands out receiver records, only an explicit restore does. - The clip sweep's root set moved from a synchronous disk read to the resident copy, so an empty root set became ambiguous between "wiped" and "not loaded yet" - the latter would delete clips a valid save still references. DeleteUnreferencedRecordings now bails unless HasAttemptedLoad, and the subsystem reads the slot into residency at Initialize.
TESTS (Rephrased.Save.SAVE001.*, 5 specs, round-tripped through the real save archive) - version written and read; older migrates, newer refused; receiver record round trip + unknown SaveId is a no-op; data layer state round trips as a byte; knowledge dictionary round trip.
convention_check: 0 errors, 0 warnings (twice).
NOT DONE - human steps - Compile. Nothing here has been through a compiler. - Content: add an ARunePuzzleActor inside Rooms/LI_Test03 on DL_RT_Test_StateA and an ACheckpointVolume to Lvl_DataLayerExample. Needs the editor; unreal-mcp was ConnectionRefused this session. - PIE verification per SaveLoad.md, including the streaming round trip (walk away until the room unloads and back) - that is the case a push model would fail. - UPlayerRuneKnowledge is NOT deleted. All C++ call sites are migrated off it and Source/Archived is not compiled, so only Content/Rephrased/Gameplay/Core/BP_PlayerKnowledge and Developers/Engineer/Ryo/BP_RecordingDebug still reference it. Check those in-editor, then delete the class in the same changelist as repointing them. - RephraseCharacter.cpp is also open by rsafadi; text file so it merges, but resolve before submit. - Design: one-line confirm that "active sentences" means persisted noun+status results, not sentences. The 2026-09-02 lock (instant effects, 2-token sentences, cleared on evaluation) leaves nothing durable to serialize, and the TDD says not to serialize a command orb or half transaction.
-
[2026-09-17 16:59] SAVE-001 follow-up: CL 115886 now COMPILES CLEAN (RephrasedEditor Win64 Development) and all 5 automation tests PASS. Supersedes the "NOT compiled or PIE tested" line in the entry above.
-
UHT accepted the reflected TMap
and TSet , which was the main structural unknown - there is no UPROPERTY precedent for soft-path container keys in the engine source. - Rephrased.Save.SAVE001.* 5/5 Success, ~140ms, clean TestExit. Verified: version written and read; older save migrates and newer is refused; receiver record round trip with unknown SaveId as a no-op; data layer runtime state round trips as uint8; knowledge dictionary round trip.
- The one Warning in the log ("save is version 2, this build reads 1; discarding") is the intentional reject-newer path proving it executed.
- Headless run: UnrealEditor-Cmd -ExecCmds="Automation RunTests Rephrased.Save" -unattended -nopause -nullrhi -testexit="Automation Test Queue Empty". Results land in Saved/Logs/Rephrased.log under LogAutomationController, not on stdout.
STILL OPEN (unchanged): PIE verification incl. the streaming round trip; content placement in Lvl_DataLayerExample; the BP_PlayerKnowledge / BP_RecordingDebug check before deleting UPlayerRuneKnowledge; rsafadi's concurrent RephraseCharacter.cpp edit; design confirm on the "active sentences" reading; and request_review is still blocked by the toolkit bug that cannot read edited .cpp/.h.
-
[2026-09-17 17:06] SAVE-001 bug fix found by review question: CaptureReceivers was calling Record.Receivers.Reset() before writing, which silently erased progress in every unloaded area.
-
Receivers register at BeginPlay and unregister at EndPlay. A runtime Data Layer unload or a room streaming out DESTROYS its actors, so they leave the Receivers array. Reset() then wiped their records and only the currently-loaded receivers were written back.
- Failure sequence: solve a puzzle in room A -> walk to room B (A streams out) -> cross a checkpoint -> A's record is gone -> walk back to A -> receiver pulls, finds nothing, falls back to authored status -> solved puzzle is unsolved. Exactly the failure the pull architecture exists to prevent, introduced on the capture side instead.
- Severity scales with how much of the world streams, so it would look mild on a small test map and bad on Cascade Town.
- Fix: records are merged, never reset. TMap::Add overwrites the live receiver's own entry; entries for receivers that are not currently loaded are preserved, because once the actor is destroyed the save is the only place that state exists.
- Duplicate-SaveId detection had to move with it: it previously checked Record.Receivers.Contains, which after merging would flag every receiver that had a prior record. Now checked against a TSet of ids seen in THIS capture pass only.
- Checkpoint log now prints "%d receivers, %d captured live" so the merge is visible in PIE: total should exceed live once the player has been through more than one streamed area.
- Side effect worth noting: a receiver deleted from the level keeps a stale record forever. Harmless (an unmatched SaveId is a no-op on restore) and strictly preferable to losing state, but a content-validation prune could clear them later.
- Not unit tested: capture needs a live world plus registered components. This is what PIE step 7 (walk away until the room unloads, then back) actually verifies, so that step is now load-bearing rather than nice-to-have.
Also confirmed while checking: the three-word sentence does not exist in the live build. FSentence (Subject/Verb/Object) is declared in RuneSystemStructs.h and referenced nowhere in Source/Rephrased - it is a leftover from the archived SentenceValidator. The live path is FRuneTransaction, strictly one VERB + one NOUN per the 2026-09-02 lock, and CommandNoun in ReceiveCommand is a target SELECTOR (RuneCoordinatorComponent.cpp:682 skips receivers whose NounRune != CommandNoun), not a transformation target. Nothing in the live code assigns NounRune except the save restore, so FRuneReceiverRecord::NounRune currently persists a field no gameplay mutates - open question whether to cut it for now on the same "nothing speculative" grounds that cut the recent-use clip ids.
-
[2026-09-17 17:13] SAVE-001 schema change: receiver noun REMOVED from the save. Engineer confirmed 2026-09-17 that the game is two-word sentences only, permanently.
-
With three-word sentences permanently cut, nothing at runtime ever mutates URuneableObjectComponent::NounRune. Confirmed by grep: the only assignment anywhere in Source/Rephrased was the save restore itself. CommandNoun in ReceiveCommand is a target selector, not a transformation target.
- That makes NounRune authored design data living in the .umap / external actor file, exactly like StatusPresentations or AllowedStatuses. DataLayers.md already states the rule for this case: "Initial runtime states set in the editor are design data stored in the map, not save data." So this is not a taste call, it follows an existing project rule.
- Dropped FSoftObjectPath NounRune from FRuneReceiverRecord; CaptureReceivers no longer reads it; ApplySavedRecord no longer assigns it.
- The record is now pure runtime state: Status + optional Transform. New reviewable invariant: if only a designer can change a field, it does not belong in the save.
- This is strictly better, not just smaller. Restored status is now filtered against the AUTHORED noun, so a designer can swap an object's noun or retune its AllowedStatuses and existing saves keep working - newly illegal tags get dropped. Restoring a saved noun would instead have forced stale content back over a content edit.
- No schema version bump. Tagged property serialization skips a property the struct no longer declares, and the version field is for when surviving fields change MEANING, not for adding or removing fields. v1 has also not shipped.
- The 5 automation tests needed no change: the receiver round-trip test never set a noun.
- convention_check still 0 errors / 0 warnings. Needs a rebuild + retest (the earlier clean compile and 5/5 pass predate this and the CaptureReceivers merge fix).
-
Follow-up spun off: FSentence (Subject/Verb/Object, Is2Word/Is3Word) in RuneSystem/Core/RuneSystemStructs.h is now unambiguously dead - referenced nowhere in the live module, leftover from the archived SentenceValidator. It is USTRUCT(BlueprintType) so it needs a Blueprint reference check before deletion. Separate chore ticket, not SAVE-001.
-
[2026-09-18 13:36] SAVE-001: resolved RephraseCharacter.cpp against rsafadi's CORE-005 (submitted as CL 116061, head is now #7).
-
Not a textual conflict, a semantic one. Ryo had independently gated the same BeginPlay save wipe, with a bClearsSaveOnBeginPlay UPROPERTY on the character. I had gated it with the Rephrased.Save.WipeOnPlay CVar routed through WipeAllSaves.
- Both are needed and they answer different questions. bClearsSaveOnBeginPlay = WHICH pawns may wipe: CORE-005 streams the Mind Palace level alongside the main one, so a second ARephraseCharacter begins play and would wipe progress mid-session. Rephrased.Save.WipeOnPlay = WHETHER this session wipes at all. Merged as: if (bClearsSaveOnBeginPlay && ShouldWipeOnPlay()).
- An automatic merge would likely have kept one gate and dropped the other, and losing Ryo's would reintroduce the mid-session wipe his flag exists to prevent.
- Near miss worth recording: my working file was based on #6, so p4 diff against #7 showed it DELETING his ToggleMindPalace function, the ToggleMindPalaceAction input binding, and the whole Move() rewrite for bMoveRelativeToOwnCamera. Submitting without resolving would have reverted his character integration while keeping his header.
- Method: backed the working file up to scratch, p4 resolve -at (take his #7 whole), then re-applied my three edits on top - the Save subsystem include, the merged BeginPlay gate, and ResetGame routing through WipeAllSaves. Final p4 diff against #7 is exactly those three hunks and nothing else.
- ResetGame keeps ignoring the CVar; it is an explicit player action.
- The shelf on 115886 predated the resolve and still held the clobbering version, so it was refreshed. Anyone unshelving it before now would have got the pre-merge file.
- No other files in the CL needed resolving; Source/ and Config/ are up to date, and his new MindPalaceSubsystem / MindPalaceOverlayWidget files are synced locally.
-
convention_check 0 errors / 0 warnings. Needs a rebuild: this is the first build that includes both CORE-005 and SAVE-001.
-
[2026-09-19 12:19] SAVE-001 test content placed via unreal-mcp (editor was live this session, unlike earlier).
PLACED - ARunePuzzleActor "SavePuzzle_Door" INSIDE Rooms/LI_Test03 (entered edit_level_instance, placed, committed). NounRune = DA_Door, Status = Status.Broken, assigned to DL_RT_Test_StateA. Added a StaticMeshComponent with SM_Door because a bare ARunePuzzleActor has no mesh component at all - its constructor makes only SceneRoot, CommandCollision and RuneableObject, and ApplyStatusPresentation looks for an existing StaticMeshComponent rather than creating one. Without it there is nothing to see change. - BP_CheckpointVolume instance "CP_RoomEntrance" in the act map at (-700,-670,380). PlayerStart is at (-1010,-670,380) and the box extent is 200, so the player spawns ~110 units clear of the edge and crosses it walking +X. CheckpointId left empty on purpose so ResolveCheckpointId falls back to the actor label - exercises that path and gives a readable log line. - Chose DA_Door + DA_Repair rather than copying an actor out of LV_2ndPlayable: the verb wiring lives on the data assets, not the actor, so a fresh actor pointed at DA_Door is equivalent and does not touch a shared map. DA_Door is a NOUN with AllowedStatuses {Status.Broken, Status.Open}; DA_Repair is a VERB with VerbEffectClass = RepairEffectComponent, which requires and removes Status.Broken. Starting Broken means the puzzle is hidden until repaired, so the state change is visible without authoring StatusPresentations.
SEEDING CONFIRMED LIVE, WITH A COSMETIC FLAW - OnComponentCreated fired on placement and auto-assigned SaveId = "RunePuzzleActor_UAID_047C16067AAA300303_2064782447_5B1ADCD1". Unique and correct, so the mechanism works. - But it used the actor's INTERNAL name, not a readable label: the component is created during spawn, before the editor applies the label, so GetActorNameOrLabel() returns the UAID object name. The readability half of the "
PERFORCE
- Three new binary adds, all landed in the DEFAULT changelist as usual and were p4 reopen'd into 115886: the two ExternalActors files (checkpoint volume in Lvl_DataLayerExample, puzzle actor in LI_Test03) and BP_CheckpointVolume.uasset.
- content_check: 2 errors, both the known false positive - external actors under Content/Developers/
STILL HUMAN-ONLY: PIE verification. Everything is placed and configured, nothing has been played.
-
[2026-09-19 21:11] SAVE-001 test content gap found and fixed: the puzzle I placed was unsolvable.
-
Placed SavePuzzle_Door but no runes, and find_actors for AWorldRune returned ZERO in Lvl_DataLayerExample. A sentence needs two activations, and with no world runes and drawing not unlocked there was no way to form one. The puzzle would have sat Broken forever and the PIE status-change test could not have run.
- Added BP_RuneWord_Door (RuneData = DA_Door) at (-350,-100,330) and BP_RuneWord_Repair (RuneData = DA_Repair) at (-350,-350,330), both in the act map rather than inside the room, so they stay loaded when DL_RT_Test_StateA unloads and the player can always re-solve.
GEOMETRY CHECKED RATHER THAN GUESSED - Orb radius comes from the NOUN rune's CommandRange sphere (RuneCoordinatorComponent.cpp:493-495), falling back to DrawnCommandRange only for drawn nouns. BP_RuneWord_Door's CommandRange is 600; the puzzle is 250 away, so the orb reaches it. - DeactivationRange is 800 and clears a pending rune when the player leaves it. The two runes are 250 apart, so the first stays pending while walking to the second. - Layout runs spawn (-1010,-670) -> checkpoint CP_RoomEntrance (-700,-670) -> runes (-350,...), so crossing the checkpoint happens naturally on the way to the puzzle.
SOLUTION SEQUENCE: walk to either rune, activate, walk to the other, activate. Order does not matter (FRuneTransaction is order-independent). REPAIR requires and removes Status.Broken, so the door goes from hidden to visible.
PERFORCE: 5 content files now in 115886 (checkpoint volume, puzzle actor, BP_CheckpointVolume, and the two rune actors). Both new rune adds landed in default again and were reopened.
- [2026-09-19 21:15] SAVE-001 PIE report: puzzle repairs correctly, but "door still broken after restarting PIE" is expected, not a bug.
DIAGNOSIS FROM THE LOG (not guessed) - Rephrased.Save.WipeOnPlay 0 was set at 04:08:40, so the save was not being wiped. - Three checkpoint writes and two reads are present. The reads sit between "Created PIE world by copying editor world" and "WorldPartition initialize started", i.e. they are URephrasedSaveSubsystem::Initialize -> LoadResidentSaveFromDisk at PIE start, NOT F9 presses. - No "Cmd: Rephrased.Save.Load" anywhere in the log, and no "restore complete, reapplied N live receiver(s)" line. LoadCheckpoint was never called.
WHY THIS IS CORRECT - Residency does not restore. bRestoreActive is only set by an explicit LoadCheckpoint, which is the gate added after the half-restore bug: without it a plain Play would repair the door while Data Layers and player transform stayed authored. - A fresh Play therefore shows the AUTHORED state (Broken), which is what it should show. Restoring needs F9 / Rephrased.Save.Load. - My fault for burying the step: the last instructions covered solving the puzzle and mentioned F9 only as a way to re-arm it.
REAL GAP WORTH RAISING - In a shipped build NOTHING calls LoadCheckpoint. The console commands and the F5/F9 DebugExecBindings are all under #if !UE_BUILD_SHIPPING, and ACheckpointVolume only writes. The save system is complete and unreachable by a player. - Correct for SAVE-001 scope (main menu "Continue" is UI Menus, explicitly out of scope) but it needs an owner or the feature ships invisible. Raise with whoever holds UI Menus.
ALSO CONFIRMED WORKING FROM THE LOG - Checkpoint volume fires on crossing with no player action: "checkpoint 'CP_RoomEntrance' written (1 receivers, 1 captured live, 2 data layers, version 1)". CheckpointId fallback to the actor label works. - 2 data layers captured, version 1 written and read back. - Auto-seeded SaveIds appeared for the pre-existing BP_Puzzle_* actors in another map too (BP_Puzzle_Door_B5715B43 etc.), so OnComponentCreated seeding works on Blueprint puzzle actors, not just the C++ one.
- [2026-09-19 21:20] SAVE-001: "F9 restores but the door is still missing" traced to my test layout, not the code.
EVIDENCE - strings on Saved/SaveGames/RephrasedCheckpoint.sav showed: CP_RoomEntrance, Door_LI_Test03, Status, Status.Broken. The save contained Status.Broken, so the checkpoint captured the door BEFORE it was repaired, and the restore faithfully reapplied Broken, which hides the puzzle. The save system behaved correctly end to end; the test was mis-staged. - Cause: CP_RoomEntrance was at (-700,-670), between PlayerStart (-1010,-670) and the runes (-350,...). The player crosses it on the way TO the puzzle, so the write always captured the pre-repair state. bSaveOnce=true meant re-crossing after repairing did nothing.
FIX (content only, no code change) - Moved the volume to (250,-100,330), past the door at (-100,-100), so the order is spawn -> runes -> repair -> cross -> save captures Repaired. This also matches SaveLoad.md's "prefer checkpoint saves between puzzles". - Set bSaveOnce=false on this instance so re-crossing re-saves, which makes iteration much less fiddly for a test volume. - Relabelled CP_RoomEntrance -> CP_AfterDoor, since CheckpointId is empty and the label is what ResolveCheckpointId falls back to and what appears in the log. - Deleted the stale pre-repair Saved/SaveGames/RephrasedCheckpoint.sav so the next run starts clean rather than restoring the old Broken record.
WHAT THIS ACTUALLY PROVED - Checkpoint write on crossing with zero player action: works. - Version written and read back: works. - LoadCheckpoint -> data layers -> streaming settle -> FinishRestore -> push to live receiver: works, "restore complete, reapplied 1 live receiver(s)". - Status round trip through the real save archive: works - it restored exactly the tag that was captured. The apparent failure was that the captured tag was the wrong one to capture.
Lesson for the checkpoint-placement guidance: a checkpoint placed before the thing it is meant to preserve looks identical to a broken save on reload. Worth a line in SaveLoad.md when design picks granularity.
- [2026-09-19 21:31] SAVE-001 REAL BUG found in PIE: bRestoreActive over-corrected and broke the pull path it exists to serve. Fixed.
SYMPTOM: toggle DL_RT_Test_StateA off then back on in one play session, the door returns BROKEN, and a following F5 + F9 does not bring it back.
LOG TRACE - 04:27:32 layer Activated -> Unloaded. - 04:27:57 checkpoint written (1 receivers, 0 captured live) <- the CaptureReceivers merge fix WORKS: the record survived a capture that could not see the destroyed actor. Before that fix this would have read 0 receivers and the progress would be gone. - 04:28:10 layer Unloaded -> Activated, fresh actor spawns. - 04:28:52 checkpoint written (1 receivers, 1 captured live) <- it captured the respawned actor. - 04:28:54 / 04:29:00 restore complete, reapplied 1 live receiver(s), still broken. - No "restore complete" anywhere before 04:28:10, so no LoadCheckpoint had run when the actor respawned.
ROOT CAUSE - FindReceiverRecord was gated on bRestoreActive, which ONLY an explicit LoadCheckpoint set. So the respawned actor pulled nothing and fell back to authored Broken. - Worse than a failed restore: the next checkpoint then captured that Broken state over the good repaired record, so the F5 actively destroyed saved progress. F9 afterwards faithfully restored Broken. - bRestoreActive was added to stop a half-restored world on a normal play start. Correct problem, wrong gate: it also blocked the mid-session streaming case, which is the entire reason the architecture pulls instead of pushes.
FIX - Renamed to bRecordIsAuthoritative, meaning "the resident record describes THIS session and may be handed to receivers". - Set by LoadCheckpoint as before, AND now by the first RequestCheckpointSave. Once this session has written a checkpoint, the record is this session's own state, so an actor destroyed by a layer unload can recover it on respawn. - The first checkpoint of a session that did NOT continue from disk resets Record.Receivers first: those entries describe a session the player chose not to resume and must not leak in via the merge. Subsequent writes merge normally. - Still no half-restore: with no load and no checkpoint written, nothing is authoritative, so receivers keep authored state while data layers and player transform are also authored. Consistent.
convention_check 0/0. Needs a rebuild before retesting.
- [2026-09-19 21:39] SAVE-001: Blueprint check done in-editor. UPlayerRuneKnowledge STAYS - do not delete it.
BP_PlayerKnowledge (Content/Rephrased/Gameplay/Core/) - Confirmed a Blueprint subclass of UPlayerRuneKnowledge via search_subclasses. - Empty: list_variables and list_functions both return zero. ~5.8KB, EventGraph only. - Zero referencers anywhere in the project. - Kept anyway. Unused is not the same as unwanted, and an empty subclass of the knowledge class is exactly the shape of a placeholder for planned work.
BP_RecordingDebug (Content/Developers/Engineer/Ryo/) - A LIVE consumer. Its graph calls the static UPlayerRuneKnowledge::LoadKnowledge() and then GetRecordingForRune(). Deleting the class breaks Ryo's debug tool outright.
VERDICT: two independent blockers, so the class is not deleted in SAVE-001. It now has no C++ callers - MemoryOrb and GameplayRecorderSubsystem were migrated to URephrasedSaveSubsystem - and is retained purely for Blueprint compatibility. That is intentional, not an oversight.
REGRESSION TO FLAG TO RYO (found while checking) - BP_RecordingDebug now reads a store nothing writes. Previously MemoryOrb and the recorder drove UPlayerRuneKnowledge and MarkRuneUsed populated RuneRecordingMap. First-use clip ids now land in URephrasedSaveGame::Knowledge.Entries instead, so LoadKnowledge()->GetRecordingForRune() will return None forever. - WipeAllSaves also deletes the legacy RuneKnowledgeSave slot, so no stale copy survives either. - Repointing is small because the subsystem API was deliberately mirrored: URephrasedSaveSubsystem::GetRecordingForRune(URuneDataAsset*) has the same name and signature as the old one. The BP needs LoadKnowledge swapped for the subsystem getter and the call retargeted. - Not doing it unilaterally: the asset is in Ryo's personal scratch and CORE-005 is his active work. Raise with him.
- [2026-09-19 21:50] SAVE-001: "ran wipe but F9 still picked up state" - the wipe never ran, plus a second defect found in the same log.
DEFECT 1: console commands no-op silently outside PIE
- No "wiped checkpoint, legacy knowledge, and N clip(s)" line anywhere in the log, and Saved/SaveGames/RephrasedCheckpoint.sav was still on disk. WipeAllSaves was never called.
- Cause: all three FAutoConsoleCommandWithWorld handlers do if (URephrasedSaveSubsystem* S = Get(World)). The subsystem is GameInstance-scoped, so in the editor world (no game instance) Get returns null and the command does nothing AND logs nothing. Typing Rephrased.Save.Wipe outside PIE looks like it worked.
- Fix: FindSubsystemForCommand helper shared by Checkpoint/Load/Wipe, warning "did nothing, there is no running game. Start Play In Editor first." A no-op is fine; a silent no-op is not.
DEFECT 2: one crossing wrote the save twice - Log shows paired writes in the same millisecond throughout, e.g. 04:47:48:563 and :564, and again at 5382/5383 and 5641/5642. - Cause: ACheckpointVolume binds OnComponentBeginOverlap and a pawn overlaps with several primitives (capsule, mesh, InteractionTrigger), so each raises its own begin-overlap and each ran a full save. Masked while bSaveOnce was true; visible once I set it false on the test volume. - Fix: LastSaveFrame guard in TrySave, compared against GFrameCounter. One crossing, one write. The retry timer still works because it fires on a later frame.
WHY IT LOOKED LIKE THE WIPE FAILED: with bSaveOnce off, lingering anywhere near CP_AfterDoor re-writes the save from the live world, so even a successful wipe would be undone within a frame of moving. Both defects pointed the same way.
convention_check 0/0. Needs a rebuild before retesting.
Note for the wipe test once rebuilt: wiping does NOT revert actors that are already alive, it only deletes the slot and drops authority. The door stays visually repaired until something reloads it. Correct behaviour, but easy to misread as the wipe failing.
- [2026-09-19 21:59] SAVE-001: rebound the debug save keys F5/F9 -> F6/F7, and recovered a lost config edit.
THE CONFLICT IS UNREAL ITSELF, NOT A TEAMMATE - Engine/Config/BaseInput.ini already binds +DebugExecBindings=(Key=F5,Command="viewmode shadercomplexity") and (Key=F9,Command="shot showui"). - So SAVE-001's F5/F9 were fighting shader-complexity viewmode and the screenshot key. My mistake: I picked the keys by eye instead of checking BaseInput.ini. - Engine claims F1-F5, F9 and F11; F8 ejects from PIE. F6, F7 and F10 are the only free function keys, and F10 opens the Windows menu bar in some setups. Took F6 = Rephrased.Save.Checkpoint, F7 = Rephrased.Save.Load. - Checked first: no teammate binds F5/F9. UI-003 (CL 116215, Lucas) uses C for the controls overlay and only edits DefaultEngine.ini. Nothing in Content/Rephrased/Input binds them either. - Rephrased.Save.Wipe deliberately stays console-only. A stray keypress should not be able to delete every save.
SEPARATE PROBLEM FOUND WHILE CHECKING: the config edit had vanished - Config/DefaultInput.ini was NOT in CL 115886 and the DebugExecBindings lines were gone from disk. p4 filelog shows depot head is still #2 (Gabriel, 115364), so my edit was reverted locally at some point and never submitted. - Consequence if unnoticed: SaveLoad.md and the earlier devlog entries documented hotkeys that did not exist in the change. The review that passed did not include this file either. - Re-opened the file into 115886 and re-added the bindings with the new keys. 27 files in the changelist now. Worth a p4 opened check before submit rather than trusting that earlier edits survived.
Wiki updated: SaveLoad.md now says F6/F7 and carries the rule - pick debug keys against Engine/Config/BaseInput.ini rather than by eye, and the list of what the engine already claims.
Note: the review passed BEFORE this and before the two console/overlap fixes, so the reviewed diff is now behind the changelist. Re-run request_review after Gabriel's playtest so what he approves matches what gets submitted.
- [2026-09-19 22:17] SAVE-001: deleted AMemoryOrb, rebuilt, re-reviewed. CL 115886 now 29 files.
DELETED: Source/Rephrased/MemoryOrb.h/.cpp and Content/Rephrased/Gameplay/SCRAPPED/BP_MemoryOrb.uasset - AMemoryOrb was the orb-based Mind Palace entry: walk up, camera fade, teleport to the palace level, matching orb inside to return. bHasUsedWorldOrb gated the return orb. - Superseded. The 2026-09-02 sync replaced it ("old orb version replaced, press a key, thought bubble, jump into your own mind") and Ryo's CORE-005 shipped the replacement as UMindPalaceSubsystem. Its only Blueprint was already sitting in a folder named SCRAPPED, and nothing in C++ referenced it. - It was only in this changelist because it called UPlayerRuneKnowledge::LoadKnowledge in four places for bHasUsedWorldOrb and had to keep compiling after the knowledge store moved. Maintaining a scrapped feature to satisfy the linker. - Sequence note: MemoryOrb.cpp was open for edit, so it needed p4 revert before p4 delete would take.
THIS ALSO FIXED THE REVIEW PAYLOAD - The previous re-review was rejected at 201805 chars against a 200000 limit. The check is on len(diff) in request_review.py, which is the canonical diff PLUS the full text of every edited .cpp/.h - design_context is not counted, so trimming the write-up would not have helped. - Edited files are included whole regardless of edit size: GameplayRecorderSubsystem.cpp 16.7KB for ~10 lines, RephraseCharacter.cpp 13KB for ~15, RunePuzzleActor.cpp 12.5KB for 2. Deleting MemoryOrb removed ~11KB (an edit counts twice, diff plus full text; a delete carries no full text at all) and put it back under. - Worth knowing for future large changes: splitting the changelist, which the tool suggests, does not work when the edits are interdependent - any split produces changelists that do not compile. Removing dead code is the better lever.
VERIFIED THIS PASS: build Succeeded (167s, UnrealEditor-Rephrased.dll relinked). Rephrased.Save.SAVE001.* 5/5 Success headless. convention_check 0/0. Review passed, no issues, replied on the existing Discord thread rather than opening a new one.
STILL GATING SUBMIT: Gabriel's playtest (Risk HIGH). Then p4 shelve -d -c 115886 before submit, since a shelved changelist cannot be submitted.
- [2026-09-20 16:52] SAVE-001 follow-up findings after approval. No change to CL 115886.
RUNE DISCOVERY IS NEVER RECORDED - and was not before SAVE-001 either - The belief that the knowledge store "saves the runes the player has encountered" describes the design intent and the legacy code, not the current build. - Schema side works: FRuneKnowledgeEntry::bDiscovered, URephrasedSaveSubsystem::DiscoverRune(), and the DictionaryRoundTrips automation test all prove it serializes. - But nothing calls DiscoverRune. grep across Source/Rephrased finds only the two definitions (old class and new subsystem); no Blueprint calls it. - Only callers anywhere are archived and uncompiled: Source/Archived/LegacyRune/RuneCarryComponent.cpp:68 and UInteractModeComponent.cpp:51, the old pick-up-and-carry system. When PROT-001 moved to AWorldRune + URuneCoordinatorComponent, discovery was never rewired. So UPlayerRuneKnowledge::DiscoveredRunes was ALSO always empty in the live build before this change. Not a SAVE-001 regression. - Net effect: the save faithfully persists a set no player can add to.
SIBLING GAP: drawing unlock is not persisted - URuneCoordinatorComponent::UnlockDrawing() only sets a plain bDrawingUnlocked member; the save never reads it, so a checkpoint reload re-locks the drawing tablet. UnlockedDrawingPatterns is declared in the knowledge struct and never written. - SaveLoad.md's contract says a checkpoint stores "learned runes and learned drawing patterns" - both halves are empty in practice today.
WHERE THE FIX GOES, AND THE QUESTION BLOCKING IT - ActivateCandidate (world) and ActivateDrawnRune (drawn) both funnel into ActivateToken at RuneCoordinatorComponent.cpp:466, so one DiscoverRune(Token.Rune) there covers both origins. - But "encountered" is ambiguous. MemoryPalace.md's spawn rule says a rune "SEEN within the last 30 seconds", which points at proximity (the SetCandidate path) rather than activation. Seen = a pedestal for every rune walked past; activated = only runes used. Needs a design call before anyone codes it. - Kept OUT of SAVE-001: it is approved and playtest-gated, and this is gameplay wiring rather than save schema. The ticket's criterion was that knowledge serializes under the dictionary model, which it does. Raised as a separate follow-up task.
BP_RecordingDebug: Ryo is fine with it staying broken. Leaving it - fixing it would change an approved changelist and force another review for a two-node Blueprint edit nobody is blocked on, and the pedestal work will rewrite that path anyway.
- [2026-09-20 16:58] SAVE-001 Save/Load Schema - SUBMITTED. Approved by review twice and by Gabriel (playtest via video recording). Submitted from pending CL 115886; the submit renumbers it, so cite the post-submit number, not 115886.
WHAT SHIPPED - URephrasedSaveGame: versioned root save (SchemaVersion + MigrateToCurrent). Carries checkpoint and level identity, player transform, runtime Data Layer records (runtime state stored as uint8), receiver records keyed by an authored SaveId, and the knowledge dictionary. - URephrasedSaveSubsystem (GameInstance-scoped, because a restore can span OpenLevel): one resident save, capture and restore, knowledge accessors. - URuneSaveIdComponent: authored SaveId seeded once on creation, re-seeded on duplicate/paste (PostEditImport), IsDataValid on empty. Duplicates detected per capture pass. - ACheckpointVolume: invisible autosave trigger, CheckpointId falls back to the actor label, defers while a puzzle is resolving, debounced by frame. - FRuneKnowledgeSaveData: the dictionary model, one entry per rune keyed by asset path. Replaces UPlayerRuneKnowledge's four parallel containers; RuneToSlotMap dropped. - Deleted AMemoryOrb (superseded orb-based palace entry; only Blueprint was in SCRAPPED). - Debug: F6 checkpoint / F7 load via DebugExecBindings, stripped from shipping. Rephrased.Save.WipeOnPlay (default on) keeps everyday dev starting clean. Wipe is console-only.
THE DECISIONS THAT MATTER FOR ANYONE EXTENDING THIS - Receivers PULL their own record at BeginPlay; they are not pushed to at load. Runtime Data Layer unloads destroy actors and rooms stream in long after a restore, so there is no moment when all receivers exist. Already-live receivers are pushed to in FinishRestore. - bRecordIsAuthoritative gates the pull. Set by an explicit LoadCheckpoint OR the first checkpoint write. Gating on load alone reverted streamed-in actors and then captured the reverted state over the good record - found in PIE. - Capture MERGES receiver records, never resets, or unloaded areas lose their progress. - Receiver noun is not saved: grammar is two words permanently, so NounRune is authored map data. Status is filtered against the authored noun, so designers can retune AllowedStatuses without breaking saves. - UPROPERTY(SaveGame) filters nothing on this path (UGameplayStatics never sets ArIsSaveGame). Use Transient to exclude.
VERIFIED - Compiles clean; Rephrased.Save.SAVE001.* 5/5 headless; convention_check 0/0; review passed twice; Gabriel approved. - PIE: checkpoint write on crossing with zero player action; version read on load; status round trip; full restore chain; mid-session pull after a Data Layer unload/reload; capture merge ("1 receivers, 0 captured live" with the actor destroyed); restart plus load restores.
NOT VERIFIED - known gaps - Transform restore (bSaveTransform false on the test actor). - Restoring a Data Layer to a state different from its current one. - True spatial streaming (Data Layer toggle only - same destroy/respawn path). - Knowledge dictionary in PIE (unit tested only).
KNOWN FOLLOW-UPS, NOT IN THIS CHANGE - Rune discovery is never recorded and drawing unlock is not persisted. Both pre-date SAVE-001 (discovery was only wired in the archived pick-up system). Needs a design call on "seen" vs "activated" first. Raised as a separate task. - No player-facing path calls LoadCheckpoint in a shipping build - needs a Continue entry point from UI Menus. - BP_RecordingDebug (Ryo's scratch) reads a store nothing writes. Ryo is fine leaving it broken; repoint at URephrasedSaveSubsystem::GetRecordingForRune when pedestals land. - UPlayerRuneKnowledge retained for Blueprint compatibility only (BP_PlayerKnowledge, BP_RecordingDebug); no C++ callers. - FSentence is dead code (two-word grammar is permanent). Separate chore.
PROCESS NOTES WORTH KEEPING - The editor's DebugExecBindings are baked in at startup: config key changes need an editor restart, console commands do not. - Pick debug keys against Engine/Config/BaseInput.ini, not by eye - the engine claims F1-F5, F9, F11. - A checkpoint placed before the thing it should preserve is indistinguishable from a broken save on reload. - When diffing a workspace file against a shelf on Windows use --strip-trailing-cr; p4 print emits CRLF.
- [2026-09-20 17:32] SAVE-001.1 (CL 116349, review passed, awaiting compile + PIE):
- Checkpoint writes moved from blocking SaveGameToSlot to AsyncSaveGameToSlot. Serialization stays inline on the game thread, only the file write goes to a background task, so the snapshot is still atomic. Teardown save deliberately stays synchronous -- a background task and its game-thread callback do not outlive the world.
- Request functions now return "dispatched", not "on disk". Outcome arrives on OnCheckpointSaveCompleted. Gotcha: a serialization failure fires that delegate inline, before the request call returns.
- Writes in flight are counted, not flagged, so two volumes firing close together raise one started/one completed instead of flickering the indicator.
- New USaveIndicatorWidget base; subsystem creates and drives it, Blueprint subclass owns all visuals. Class lives on ARephraseCharacter next to HUDWidgetClass (same reasoning FMindPalaceOpenParams gives for OverlayWidgetClass). First proposed a Config FSoftClassPath; Ricky pushed back and was right -- zero UPROPERTY(Config) exist in this project and all five widget-class refs are TSubclassOf.
- Answered the standing question: player transform was captured at write time, not at boundary touch. Normally the same frame, but TrySave's unbounded 0.25s settle retry meant a save during an orb flight or puzzle slide recorded wherever the player walked to. Volume now latches the transform at overlap.
- Quit inside a volume rewrites the checkpoint with the pawn's current transform; outside a volume nothing is written. Containment via IsOverlappingActor, not overlap-event tracking -- a pawn raises those once per primitive and they do not ref-count cleanly.
- Teardown hook early-returns while bRestoreWaitingForMap, or LoadCheckpoint's own OpenLevel would write a half-restored world over the record being restored from.
-
Still to verify by hand: PIE stop and standalone quit tear down differently, so the quit save needs both.
-
[2026-09-20 17:36] SAVE-001.1 compile fix: RephraseCharacter.cpp needed #include "Save/SaveIndicatorWidget.h". A forward declare is enough to declare a TSubclassOf
member but not to use it -- operator UClass* instantiates T::StaticClass(), which needs the complete type. The subsystem .cpp already had the include, so only the character failed. RephrasedEditor Win64 Development now builds clean (16/16, both DLLs linked). PIE still pending. -
[2026-09-20 18:04] SAVE-001.1 restore-path fixes from review (all three verified against the code before changing anything):
- LoadCheckpoint travelled with GetAssetName() while comparing the full path. Map names are not unique here -- confirmed 4x Showcase and 4x LV_test under Developers/ -- so the short name could open a different map than the comparison just rejected. Now travels by GetLongPackageName().
- HandlePostLoadMap ignored its LoadedWorld parameter, so any map finishing satisfied a pending restore. Now compares MakeLevelPath(LoadedWorld) against Save->LevelPath and abandons on mismatch.
- OpenLevel only logs on an unresolvable name, leaving bRestoreWaitingForMap set forever. Now checks FPackageName::DoesPackageExist first and returns false instead of arming a restore that cannot happen. This one also broke SAVE-001.1: HandleWorldBeginTearDown early-returns on that flag, so a stuck restore permanently disabled the quit save.
- Beyond the reviewer's snippet: both abandon paths also clear bRecordIsAuthoritative, or a record for a level never entered gets handed to receivers in whatever level actually loaded, where authored ids can collide. Also guarded HandlePostLoadMap against a null Save, reachable via WipeAllSaves during a pending travel.
- NOT yet compile-verified: editor was open with Live Coding active and the build refused. Previous batch built clean; this one still needs a build.
-
Open question raised for Ricky: FinishRestore only teleports the pawn after streaming completes, so in a partitioned world streaming is driven from PlayerStart rather than the saved location -- IsAllStreamingCompleted can pass having loaded the wrong region.
-
[2026-09-20 18:11] SAVE-001.1 corrections to the entry above:
- Compile now verified: RephrasedEditor Win64 Development builds clean (16/16, both DLLs linked) with the restore-path fixes in.
-
Retracting the streaming "open question" from the previous entry. What is confirmed is only the ordering: FinishRestore teleports the pawn after the streaming poll, not before. The follow-on claim that World Partition therefore streams the wrong region was my inference about streaming sources, not checked against UWorldPartitionSubsystem, and should not be treated as a finding. Ricky's call: leave it alone until someone actually observes it in PIE.
-
[2026-09-20 20:40] SAVE-001.1 indicator Blueprint:
- Built Content/Rephrased/UI/WBP_SaveIndicator (parent USaveIndicatorWidget) via unreal-mcp; compiles clean with warnings-as-errors; saved and added to CL 116349.
- Tree: RootCanvas > IndicatorRoot (HorizontalBox, bottom-right anchor, 48px inset, starts Collapsed) > SavingThrobber (CircularThrobber) + StatusText (18pt).
- OnSaveStarted: bSaveInProgress=true, text "Saving", throbber and root visible. OnSaveCompleted: flag false, throbber collapsed, text "Checkpoint saved" / "Save failed", RetriggerableDelay 1.5s, then hide root only if no new save started during the hold.
- BLOCKED: could not set SaveIndicatorWidgetClass on BP_RephraseCharacter1 -- exclusively checked out by jsun3563 (Lucas). Retried once, still locked. Until that is set the indicator never appears.