Docs · Engineering reference
Life Framework — Implemented Features & Contracts
docs/features.mdEdit on GitHub
Ground truth of what the codebase currently contains and what each system is supposed to do. First written from a read-only sweep of
addons/LifeFramework/Scripts/Game/(2026-08-29); rows marked verified 2026-08-30 were re-audited against current code during the P0 ship-blocker pass. Every claim carries a file path. This is the contract test cases must prove.Keep this file current. It is the doc every agent reads to know what a system does. Update the affected feature's rows in the SAME change that alters the system — a stale row reads as "broken" and wastes the next agent's whole session (the 2026-08-29 sweep claimed several already-fixed exploits were live; that gaslight is what this note prevents).
tools\cli validaterunsvalidate-docs-sync.ps1, which warns when feature code changes without this file changing.Status legend per feature: ✅ implemented (what "working" means), ⚠️ known-fragile/untested contract, ❌ known bug or broken path.
Core
EL_GameModeRoleplay — Scripts/Game/Core/EL_GameModeRoleplay.c
- ✅ Lazy-creates
EL_CharacterCreationManager,EL_ATMManager,EL_JobManageron first player connect and drives character creation. - ✅ World-end static reset (2026-11-08): the game-mode
destructor calls
LF_StaticReset.ResetAll, which dropsLF_Breach,LF_Jail,LF_JailEscort,LF_Contraband,LF_PoliceRanks, the jail cell/arrestable tick owners, the garage and spawn-point registries, and the quantity transfer intents. It is not called at game start: placed components register inOnPostInit, before the game mode starts. - ✅ Disconnect cleanup (2026-11-08): server
OnPlayerDisconnectedreleases escort holds where the player is officer or prisoner (LF_JailEscort.ReleasePlayer) and fails a running convoy, dropping its vehicle watch (LF_Contraband.ReleasePlayer). Jail sentences stay (they resume on reconnect). An officer who leaves on duty stays on duty with the police inventory carried; the civilian inventory waits at the desk (M1-11). - ⚠️ Managers are only created on first connect; a server-side call before any player connects gets null.
EL_SpawnLogic — Scripts/Game/Feature/Character/Spawning/EL_SpawnLogic.c
- ✅ Account-aware respawn: loads
EL_PlayerAccount, spawns the active character's prefab at a spawn point for the account's role (CIVILIAN, or POLICE for an officer who reconnects on duty; falls back to any point), and applies the role's kit (GiveRoleKit, recursive, purpose-filtered storage fill). The POLICE kit is the starter police kit the duty desk issues (M1-11). - ✅
ResolveSpawnPoint(static) picksSCR_SpawnPointby account role key; shared with the character-creation flow so both spawn paths land in the same area. Verified 2026-08-30. - ✅ No-spawn-point fail-safe (verified 2026-08-30):
GetCreationPositionreturnsboolandCreateCharacteraborts the spawn with anEL_Debug.Errorinstead of dropping the player at world origin. - ✅ Registration retry cap (2026-11-08): a UID or account that
does not resolve retries about once a second up to
MAX_REGISTER_RETRIES(60), then logsEL_Debug.Errorand stops. A success clears the counter. - ⚠️ Item is silently dropped if no matching storage; loadout recursion has no depth guard.
EL_CharacterCreationManager — Feature/CharacterCreation/EL_CharacterCreationManager.c
- ✅ Flow (M1-11, 2026-11-08): no account → create → registration form R-89
(
LF_RegistrationForm) → civilian spawn with an ID card. There is no faction screen and no spawn-location picker; police clock in at the duty desk. A returning account skips the menus and the form.OnCharacterCreatedpersists a character, inits ATM + survival components, spawns. - ✅ Server-owned on a dedicated server (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual): the flow runs on the server
only. The form opens on the joining client through an owner RPC on its
SCR_PlayerController(LF_ShowRegistrationForm,EL_CharacterCreationController.c); the answer comes back asRpcAsk_EL_CreateCharacterand lands inServerCreateCharacter. The player is the controller's id, never a client value.EL_CharacterCreationRulesre-checks it: one character per account, names 1..32 ASCII letters with single inner spaces, hyphens, or apostrophes (LF_RegistrationRules), age 18..80. Names are not unique. A refusal toasts the reason key (#EL-CC_Error_*) and re-opens the menu. A listen host's joiners now get their own menus instead of the host. - ✅ Real character prefab (verified 2026-08-30):
{9B5BB216CC7FF18E}Prefabs/Characters/Core/Character_Roleplay.et. - ✅ (2026-11-08, M1-08) No more "Default Citizen": an account with no
character opens the registration form over
LF_ShowRegistrationForm(owner RPC). A server refusal sends the form back with the reason written on it. The form never picks a faction.
LF_PlayerInteraction — Feature/Interaction/ (M1-12, 2026-11-08)
- ✅ One list on another player's body (
Character_Roleplay.et, contextEL_ID): Introduce yourself, Show ID, Give cash, Give what you hold; on-duty police also Search and Arrest.LF_PlayerRoleActionslotsfineandorg_invitestay hidden untilLF_PlayerInteraction.RegisterRole(slot, handler)is called on every machine. Reach: 2 m between origins, both alive, the target a player, not yourself; the server re-checks every action. - ✅ Give cash: the server opens
LF_GiveCashSheeton the giver (GiveCashAmount,GiveCashButton), thenRpcAsk_LF_GiveCashmoves MoneyStack cash throughLF_CashHandover.Move: more than carried is refused, what the receiver cannot take goes back (no cash created or lost). GesturePoint+SOUND_PICK_UP. - ✅ Give item: the giver's machine names the item in hand (gadget, right-hand item, or current weapon); the server moves it only when its storage root is the giver and the receiver has room.
- ✅ Introduce: the target learns the user's character name (
LF_Introductions, keyed by player uid, one way). Persisted as theintroductionspayload (save version 8); names re-sync to clients on every spawn. GestureHalt. - ✅
LF_KnownNames(client): a known name shows under the crosshair only while looking at that player (15 degree cone) within 4 m. Strangers show nothing. - ⏳ Gesture visibility, the name label, and the restart path on a real dedicated server are manual (PR #98 steps).
EL_ShowIDAction — Feature/CharacterCreation/EL_ShowIDAction.c
- ✅ (2026-11-08) Shows the ID card the user carries (their own first, else
the first printed one) to the player they face: the server reads the item
and opens
LF_IdCardViewon the viewer's machine. No card: not performable, reason#LF-ID_NoCard. Offered by the player interaction reach rule (LF_PlayerInteraction.CanOffer, 2 m, both alive, a player); the server re-checks it. The shower points (Gesture_Point) and the viewer hearsSOUND_PICK_UP(M1-12). - ⏳ Proof on a dedicated server with two clients is manual (PR #93 steps).
EL_Utils / EL_FormatUtils / EL_Component / EL_ComponentData — Core/
- ✅
MaxInt/MinInt,AbbreviateNumber(999→raw, 1000→1.0K, 1e6→M, 1e9→B), typed component finders. - ⚠️
EL_ComponentData.Getstrips the last 5 chars of the class name (XxxClass→Xxx); breaks for any data class not ending inClass.
EL_InventoryUtils (Core) / EL_BuildingManagerComponent / EL_ChatManagerComponent
- ✅ (2026-11-08)
EL_InventoryUtils.AddItemspawns throughEL_Utils.SpawnEntityPrefab(unresolved prefab logs and returns false);IsStorageHierachyRoot,GetStorageOwner, andDropItemtolerate an entity withoutInventoryItemComponent/ a character without an inventory manager. - ✅ The building preview is a local (non-replicated) entity on the owning
client and is deleted in
OnDeleteand the destructor, so a life no longer leaves an orphan preview on every client. A null camera manager retries next frame; a preview that fails to spawn logs once and stops the frame loop. - ✅
EL_ChatManagerComponentregisters onlyEntityEvent.INIT; it has no frame handler. - ✅ Vehicle garage and spawn-point registries remove themselves in
OnDelete.
LF_RegistrationForm — Feature/CharacterCreation/LF_RegistrationForm.c
- ✅ (2026-11-08) Civil registry form R-89 on
LF_UIScreen: given name, surname, age, Sign. Client checks only write the error line; the server decides. MenuBack brings the form back unsigned ("sign before you go into town"). Harness:type RegFirstName Ada...click SignButton.
LF_IdCardComponent / LF_IdCard — Feature/CharacterCreation/LF_IdCardComponent.c
- ✅ (2026-11-08)
IDCard.etcarries holder character id, first name, last name, age (RplProps, server-issued once). Max stack 1, so cards never merge. - ✅ (M1-18, 2026-11-08) The town clerk hands the card over at arrival step 1
(a quest with
m_bRewardIdCard). From then onEL_SpawnLogicprints one from the account's character record on every spawn of that player (LF_QuestManager.HasEarnedIdCard, from the persisted turn-ins), so the card survives death and restart.m_bGrantIdAtSpawn(now default off) gives every spawn a card for a mode with no clerk. A character already carrying their own card is refused a second; a blank carried card is printed instead of adding one. The DebugWorld civilian starter kit no longer carries a blank card.
EL_NpcBaseEntity station NPCs — Prefabs/Characters/Core/Npc_Base.et
- ✅ Dressed on every machine (2026-11-08, #150):
BaseLoadoutManagerComponentspawns the slot clothing (jacket, pants, boots) on the server, and that clothing is replicated.Npc_Base.etcarries anRplComponent("Rpl State Override" Runtime,"Parent Node From Parent Entity" 0) and aHierarchy, as vanillaCharacter_Base.etdoes: the server streams each placed NPC with its worn clothes as children. Before this fix, a dedicated client saw the NPCs in underwear. Test:npc/clothes-replicate(WORLD). - ⚠️ A client builds a placed NPC from its prefab, not from the layer. A
layer may override only
CharacterIdentityComponenton an NPC. An added component kicks every client (RplCreationError: Number of items mismatch), and other overrides never reach clients.validate-npc-layer-overrides.mjsenforces this. Names set in the layer do not reach clients either (FindEntityByNameworks only on the server). - ✅ (2026-11-08) An NPC's talk context (clerk, quest giver, police duty) sits on
its chest:
PivotID "Spine5",Radius 0.6, as vanillaCharacter_Basedoes for "Chest". It wasOffset 0 0.5 0,Radius 2.0, at the hips. The town trader's counter crate (TraderChainCounter) stands on a table in front of the trader instead of on the ground at his feet. Test:npc/interaction-height(WORLD).
Character & Survival
EL_SurvivalStats — Feature/Survival/EL_SurvivalStats.c
- ✅ Hunger and thirst clamped
[0,100].Eat/Drinkare additive through clamped setters.Create(id)starts both at 100. - ✅ (2026-11-08, M1-28)
UpdateStats(dt, config)drains needs at theLF_SurvivalConfigrates. Flagship: hunger0.01/s(about 2.8 h from full), thirst0.0125/s(about 2.2 h).dt <= 0is a no-op. - ✅ (2026-11-08) The separate survival health stat is gone. Needs act on the
character's real health (
LF_SurvivalHealth). The save record keeps itsm_fHealthslot, written as 100 and ignored on load, so the save format does not change.
LF_SurvivalHealth — Feature/Survival/LF_SurvivalHealth.c
- ✅ (2026-11-08) Needs never kill. On the server tick, with hunger or
thirst below 20, the damage manager's default
Healthhit zone falls at0.0005of max per second (about 8 min) to0.75of max, then stops. An injury already below the cap is not touched. Survival tracks what it took; once needs are met it gives back only that, at0.001/s. Vanilla healing above the cap is never pulled down while fed. A dead body is skipped. Tests:survival/light(math),survival/real-health(WORLD, spawned character). - ✅ The consumable
m_fHealthRestoreheals real health (percent of max).
LF_SurvivalConfig — Feature/Survival/LF_SurvivalConfig.c
- ✅ (2026-11-08) Framework knobs: hunger and thirst drain, the low-needs
threshold, the real-health cap, drain and give-back (fractions of max), the
stamina drain multiplier, the tick interval and the max tick delta. A
missing
.conflogs[ELDebug:Survival]once and falls back to the same built-in defaults.
EL_CharacterSurvivalComponent — Feature/Survival/EL_CharacterSurvivalComponent.c
- ✅ (2026-11-08) Server-authoritative: stats tick on the server only, on a
1 s call-queue timer. The delta is clamped to 5 s so a hitch does not drop a
chunk. The owner gets whole-point hunger and thirst and the low-needs flag as
owner-only RplProps;
BumpMefires only when one of them changes (about once per 100 s at flagship rates), and right after eating or drinking. The low-needs flip logs[ELDebug:Survival] <id> needs low=.... Tests:survival/sync. - ✅ The old per-frame
EOnFramedriver is gone. It had no event mask and treated world-time milliseconds as seconds.
LF_SurvivalStaminaPenalty — Feature/Survival/LF_SurvivalStaminaPenalty.c
- ✅ (2026-11-08) Modded
SCR_CharacterStaminaComponent.OnStaminaDrain: with low needs, exertion drains stamina1.5x(config), so a hungry player sprints for less time and breathes hard sooner. It lifts on the next sync after eating and drinking. Same reentrancy pattern as the base hydration penalty. - ⚠️ Not yet felt in game: set survival to 10 in the DebugMenu and sprint.
EL_BetaHud survival gauges — HUD/EL_BetaHud.c
- ✅ (2026-11-08) Hunger and thirst gauges read the replicated whole points. The HUD stays second to the body cue. The health gauge already reads the damage manager, so the needs cap now shows there too.
EL_ConsumableEffectSurvival — Feature/Survival/EL_ConsumableEffectSurvival.c
- ✅ Applies configured hunger/thirst/health restore on item use; only applies
values
> 0.
EL_SurvivalStatsSaveData — Feature/Survival/Persistence/
- ✅ Save/apply round-trip re-clamps through setters (corrupt save sanitized).
- ⚠️
Equalsuses exact float==(no epsilon).
Death & Respawn — Feature/Character/Spawning/
- ✅ Death is defined (verified 2026-08-30): the dead body stays in the world with everything it carried — it is never deleted by the spawn flow and is only removed by a server restart or world cleanup.
- ✅ Respawn via the death screen:
EL_SpawnLogic.OnPlayerKilled_Sno longer auto-respawns; it callsNotifyReadyForSpawn_S, the client opensEL_DeathScreen(workspace widget), and its Respawn button runsSCR_RespawnComponent.RpcAsk_EL_Respawn→EL_SpawnLogic.RespawnPlayer_S(account-aware, re-applies the faction loadout). - ⚠️ Body persistence until restart is default-engine behavior (no explicit corpse-cleanup control in this mod yet) — verify on a dedicated server.
- ✅ You wake up in hospital (M1-26, #121; tests
respawn/bill-once,respawn/bill-debt,respawn/bed-pick,respawn/ward). A death marks the player's uid with a pending wake (LF_HospitalRespawn.MarkDied). The next spawn of that player (death-screen Respawn, or a reconnect while dead) takes it once: the body spawns at a freeLF_HospitalBedComponentbed (rotating, occupied beds skipped), the bill is charged to the bank, a printed paperHospitalBill.et(amount, balance after, in-game date) goes in the pockets, and a toast names the amount. The owner plays the base game's bed wake loiter (the tutorial intro's, type 7) with the spawn sound, gets up after 4 s and steps down to the bed's stand point on the floor, then reads the bill: the note "You don't remember how you got here." and the New Life Rule window (a printed server rule, nothing enforces it). A bill lying in the world has Read. Server-decided; the client never sends an amount or a bed. Every charge and debt logs[ELDebug:Respawn]. - ✅ Debt:
EL_BankAccount.Chargemay go below zero when debt is allowed; a negative balance is kept through save and load, deposits pay it off first, withdrawals are refused while in debt. - ✅ Config
Configs/Spawning/LF_Respawn.conf(LF_RespawnConfig): wake in hospital (off, or no bed placed: the spawn plaza), bill amount (flagship 150), debt allowed, New Life Rule minutes (15), wake animation. The beds are the respawn point list: putLF_HospitalBedComponenton any bed (wake offset and yaw are attributes). DebugWorld: threeBedMedical_01in the front west room of the hospital (Street_Hospital.layer); the hospital building is now the unfurnishedHouse_Town_E_2I03_Base. - ⚠️ The pending wake is session state: a restart between death and respawn forgets it (plaza, no bill). A carried bill comes back blank after a restart and hides Read.
Carried inventory — Feature/Character/Persistence/LF_StorageSnapshot.c
- ✅ Carried cash and items survive a restart (2026-11-08, M1-04, tests
persist/carried-roundtrip,persist/carried-death-no-dupe, ande2e/pass2-verify-loadacross a real two-boot restart): the whole storage tree (prefab, stack quantity, storage index, slot id) is thecarriedpayload ofEL_PersistenceComponentSerializer, appended at version 6. Keyed by character persistence id plus a slot name, so one owner can hold several named snapshots (carriednow; police duty / civilian stash later, M1-11). - ✅ Captured at every save point (autosave, manual, shutdown) for each living player and on disconnect before the character is deleted. On spawn a snapshot replaces the starter kit; without one the starter kit is granted.
- ✅ Apply reconciles instead of adding: a matching occupant is kept, an occupant
no record claims is deleted, a missing record is spawned. A second apply
spawns nothing. A prefab that no longer loads logs
EL_Debug.Errorand is skipped with its contents. - ✅ Death drops what you carry:
OnPlayerKilled_Sdrops the snapshot and a dead body is never captured, so the body keeps the items and a restart grants the starter kit. - ✅ No ground copy:
LifeFramework.confsaves a stack with its parent and no longer self-spawns the player character, so a pocketMoneyStackdoes not come back loose on the ground next to the restored one (the two-boot e2e counts it). - ✅ Placed storage (2026-11-08, M1-15):
LF_PersistentStorageComponent(Scripts/Game/Persistence/) saves a placed container's tree in the same store under itsm_sStorageKeyand slotplaced, so it rides thecarriedpayload with no new save version. Captured on every item added or removed and at every save point; restored a frame after the entity initializes and a frame after a load. Restore reconciles, so a second restore spawns nothing. The prefab carries no nativePersistencecomponent, so the engine does not save the contents a second time (the two-boot e2e finds the 151 cash in the stash and no loose copy in the world). - ⚠️ Ammo count and weapon state are not in the snapshot. Attachments and magazines are kept as items.
Progression — Feature/Account/LF_Progression.c
- ✅ Progression survives a restart (2026-11-08, M1-10, tests
persist/progression-roundtrip,persist/progression-old-save, ande2e/pass2-verify-loadacross a real two-boot restart). Theprogressionpayload, appended at save version 7, holds oneLF_ProgressionRecordper player uid: the civilian delivery career (level, XP, paid total), police rank, on-duty seconds and unpaid duty seconds, seconds since the last benefit collection, the player level (level, XP, skill points), licences by enum name, and turned-in quest ids. A courier at level 2 still opens fuel delivery and a senior officer stays senior. - ✅ Level, licences, and turn-ins live on the character, so their owners write
through to the store and
EL_SpawnLogicapplies the record to every new body. They now survive death as well; only carried items drop. - ✅ Tolerant load: an unknown licence name is skipped with a warning, an unknown quest id is kept with a warning, a rank above the ladder clamps to senior. A version 6 save leaves live rank and duty totals alone (the account apply carries them over). A running duty clock is settled into the save.
- ✅ The delivery sign-up survives a restart (2026-11-08, test
persist/job-signup-roundtrip). Thejobsignupspayload, appended at save version 11, holds oneLF_JobSignUpRecord(player uid, job by enum name) per signed-up player. It loads afterprogression, so the career level that gates the job is back first.EL_SpawnLogicputs the job on every new body after the hand-over, so the sign-up also survives death. An unknown name, a job that is not delivery work, or one above the player's level stays unemployed with a[ELDebug:Jobs]warning. - ⚠️ An active run and the open duty clock are session state; after a restart the
courier is still signed up and starts a new run.
EL_LicenseManagerComponentis not onCharacter_Roleplay.et, so licences persist only once a prefab carries it.
Account & Persistence
EL_PlayerAccount — Feature/Account/EL_PlayerAccount.c
- ✅ Persistent per-player state: faction, on-duty flag, wanted level
(clamped
[0,5]), character roster.AddCharacter/RemoveCharacter,GetActiveCharacter,SetActiveCharacter,IncreaseWantedLevel. - ⚠️
RemoveCharacterdoes not repairm_iActiveCharacterIdx; a stale index is an out-of-bounds read.SetActiveCharacterwith a non-member sets idx to -1 with no guard. Fields are effectively public (workaround for BIS ticket T174113).
EL_PlayerCharacter — Feature/Account/EL_PlayerAccount.c
- ✅ Immutable record (id, prefab, name, age),
GetFullName,Creategenerates a persistence id.
EL_PlayerAccountManager — Feature/Account/EL_PlayerAccountManager.c
- ✅ Async load + cache +
SaveAndReleaseAccount. Verified 2026-08-30:SaveAndReleaseAccountnow keeps the account resident, so consecutive wanted bumps and police-menu reads do not lose the account mid-session (the old evict-on-write churn is gone).
EL_PlayerAccountRecord — Feature/Account/Persistence/ (verified 2026-11-07)
- ✅ Explicit persisted form of an account and its characters (faction,
duty, wanted, roster, active index).
EL_PlayerAccountManager.ExportAll/ApplyAllconvert it;EL_PersistenceComponentSerializerwrites it after itsversionfield.account/save-roundtripcovers the export → reset → apply path. EPF is gone; there is noApplyTo.
EL_PersistenceManagerComponent — Persistence/ (verified 2026-08-30)
- ✅ Server-side save orchestration on the game-mode entity: autosave timer
(one AUTO slot per playthrough via
EL_SaveSelection), manual saves, save-cache scan, load/re-apply paths, wipe. Every path passesPassesSaveGates()(manager present, saving allowed, persistence system active) and logsEL_DebugunderPersistence. - ✅ Shutdown save is engine-owned: the engine requests the SHUTDOWN save point
on graceful exit; the manager observes the result via
OnAfterSaveand logs anEL_Debug.Errorif a session ends with no shutdown save. The old manualRequestSavePoint(SHUTDOWN)fromOnGameEndalways lost the race against the persistence teardown on the Workbench (fixed 2026-08-30). - ✅ Persistence system resolution is stable across reloads: the systems config
is now
Configs/Systems/LifeFrameworkSystems.conf(unique GUID, referenced explicitly from both missions), replacing the GUID-collidingChimeraSystemsConfig.confoverride. Resolution retries at game start and logs anEL_Debug.Errorwhen the entry is truly missing. - ✅ Continuing a saved world needs
-loadSessionSave(verified 2026-11-07 on the dedicated test server: fresh,-loadSessionSave, and kept-saves-without-flag boots each logged their line): the engine loads the latest save point at launch only with that flag (vanilla behavior). Script does not auto-load:LoadLatestSave()is a game-state transition that boots the world twice and drops connected players. At game start on a dedicated server that was not launched from a save, the manager scans the mission's saves and logsEL_Debug.Error("started a NEW session while N save point(s) exist") when any exist; with none it logs a fresh-session info line. Seedocs/persistence.md. - ✅ Playtest host (
toolscli host start|stop|status|backup,tools/host/host.mjs; verified 2026-11-08 on the dedicated server):startrunsLF_PlaytestTown.conffrom a profile outside every worktree (%LOCALAPPDATA%LifeFrameworkhost,LF_HOST_DIR), passes-loadSessionSavewhen a save exists, and backs the saves up first.stopwrites$profile:lf-host-stop;LF_HostStopcloses the game normally so the engine writes the SHUTDOWN save, then the CLI backs the saves up. The server owns the machine-wideserver-2001lock while it runs. The no-shutdown-save error now fires at teardown, not on the SHUTDOWN state change that comes before the save. Selftest:tools/validation/host-selftest.mjs. Joining: the host runs-noBackend, so every player has the NULL platform identity. Until 2026-11-08 that madeGetPlayerUIDempty and the faction pick failed with#EL-CC_Error_NoAccount(nobody could join).EL_Utils.ChooseIdentityFallbacknow keys a player by Steam name on a-noBackenddedicated server: stable across restarts, one character per name (spawn/identity-fallback; join, restart, rejoin seen live). - ✅ One-command playtest (
tools\cli playtest [stop|status|shortcut],Play Life Framework.cmd,tools/host/playtest.mjs): fast-forwards the detached checkout..\Life-Framework-playtesttoorigin/main(only while no host runs), runs itshost start --wait, then opensArmaReforgerSteam.exevisibly with the persistent profile%LOCALAPPDATA%\LifeFramework\player(LF_PLAYER_DIR), waits for the host'sjoin allowedline, and prints friend join lines. A running host is reused and joined with its own addon.stopcloses the game, thenhost stop.
EL_AccountReplication — Feature/Account/EL_AccountReplication.c
- ✅ Client copy of the account (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual):
EL_PlayerAccountManageris server-only, so client-side offers read a replicated copy. Faction, duty, wanted, and police rank pack into one int (EL_AccountStateBits) carried by an[RplProp]on the player'sSCR_ChimeraCharacter; the on-duty police count is an[RplProp]onEL_GameModeRoleplay. The server writes both from the account setters (SetFaction/SetOnDuty/SetWantedLevel/SetPoliceRank), onOnPlayerSpawned, and on a 10 s self-healing pass;BumpMeonly on change. Late joiners get the values with the snapshot.GetState(entity)answers from the account on the server and from the replica on a client. Consumers:EL_RobAction,EL_DutyAction,EL_ConfiscateAction,EL_OpenPoliceMenuAction,LF_ArmoryDrawAction,LF_RankPromoteAction,LF_BreachRepairAction, and (since 2026-11-08) the jail actions: arrest, escort, escort-arrest, put in and take out of a vehicle. Before that their offers read the server-only account, so on a dedicated client an on-duty officer was never offered an arrest. Every server path still re-checks the real account; the jail cell and escort holds are checked on the server only.tools/validation/validate-offer-checks.mjsfails aCanBeShownScript/CanBePerformedScriptthat readsEL_PoliceUtils.GetAccountForEntityorEL_PlayerAccountManager. - ✅ Armory actions hidden from non-police (2026-11-08, #157): Draw and
Promote at
Roadmap_Police_Armoryare shown only to on-duty police (LF_ArmoryDrawAction.ShownTo, replicated state); a civilian no longer sees them greyed. The server still refuses inPerformAction. Test:playtest/offers. - ⚠️
EL_RobWeaponAction,EL_RobVehicleAction, andLF_Bankstill count police and read accounts directly in their offers; move them toEL_AccountReplicationnext.
Money & Economy
EL_MoneyUtils — Feature/Money/EL_MoneyUtils.c
- ✅ Cash is the
MoneyStackinventory item.GetCash(‑1 on fault),AddCash/RemoveCash(actual amount, 0 on fault). - ⚠️ Inconsistent sentinels (
-1vs0); doc comments wrong on Remove/Take.
EL_BankAccount + EL_ATMManager — Feature/ATM/
- ✅ The canonical bank (verified 2026-08-30): every payout in the game pays
cash;
EL_ATMManager+EL_BankAccount(per-player, keyed by UID, persisted) is the only bank, and the ATM is the only cash↔account boundary.Depositonly if>0,Withdrawonly if sufficient; balance never negative through these paths. Manager is a session map registry + async loader. - ✅
SetBalanceclamps corrupt negative persistence values, andCreateAccountis idempotent and rejects empty IDs (verified 2026-08-30). - ✅ No bank wipe on the first connect after a restart (2026-11-07, test
restart/bank-survives-connect, verified 2026-11-07): the game mode used tonew EL_ATMManager()on the first connect, and the constructor stole the singleton the save load had filled.EL_ATMManager,EL_JobManager, andEL_CharacterCreationManagernow share one pattern:static ref s_Instance, created only byGetInstance(), the constructor never registers itself, and the destructor only clears the singleton when it is that instance. The game mode keeps no manager fields;EL_GameModeRoleplay.ResolveConnectManagers()only resolves them.
ATM terminal — Feature/ATM/LF_ATMTerminal.c (was EL_ATMMenu)
- ✅ (2026-11-08, M1-13) Terminal screen on
LF_UIScreenwith theLF_UISkin.Terminal()skin: green phosphor (vanillarobotomonoface) in a bezel, balance and carried cash from the server, anAMOUNT >entry line, a status line, a 3x4 keypad (CLR, 0, DEL), DEPOSIT, WITHDRAW, EXIT. Keys: mouse, pad focus that moves like a grid (MenuUp/Down keep the column, MenuLeft/Right walk a row), keyboard digits and numpad (LF_Keypad0..9), Backspace or pad X (LF_KeypadBack), Delete or pad Y (LF_KeypadClear). The entry cannot passMAX_TRANSACTION_AMOUNT. Closes on EXIT, MenuBack, death, or 3.5 m from the machine.EL_ATMActionruns PerformAction only on the acting machine (HasLocalEffectOnlyScript) and opens only when the user is this machine's character (LF_CounterAccess.IsLocalActor). - ✅ (2026-11-08) Refusals say why, on the screen: the server replies
LF_EATMRefusal(amount, cash short, balance short, pockets full, not at an ATM, no account) with the new balance and carried cash; no toast (the old away-from-ATM toast plus the menu toast was the duplicate). No reply in 5 s printsNO REPLY FROM THE BANK. Sounds: key click, accept and refuse are UI events set on theEL_ATMActionattributes; an accepted deposit or withdrawal playsSOUND_PICK_UPon the character for everyone nearby. Tests:atm/keypad,atm/refusal-reasons,counter/refusal-text,counter/local-only,counter/screen-layout. - ✅ Cash moves (verified 2026-08-30): the menu forwards through
EL_CharacterATMComponent.RequestDeposit/RequestWithdraw, and the server RPC handlersRpcAsk_Deposit/RpcAsk_Withdrawremove cash on deposit (partial removals rolled back) and pay cash out on withdraw (all-or-nothing with balance restore), guarded byEL_ATMManager.IsValidAmount. No money from thin air. - ✅ The money-moving bodies are the static seams
EL_CharacterATMComponent.ServeDeposit/ServeWithdraw(extracted 2026-11-07, no behavior change; the RPC handlers keep the range check and reply);atm/request-cash-boundary(WORLD) drives them with a real character (red-proof observed 2026-11-08). The withdraw rollback case pays into an entity with no inventory: a bareCharacter_Roleplaystill holds MoneyStack. - ✅ At the ATM only (2026-11-07): the server refuses a
deposit or withdraw unless an entity carrying
EL_ATMActionstands withinEL_CharacterATMComponent.ATM_RANGE(5 m) of the character (EL_ActionSite, server-side search; the client names no site). A refusal logsdeposit refused: not at an ATM/withdraw refused: ...and the terminal printsREFUSED: STAND AT THE MACHINE(2026-11-08; was a toast). On a listen host the requests and the replies call their handlers directly, like the shop bridge. Test:security/service-range.
EL_BankAccountComponent — Feature/Banking/ (deleted 2026-08-30)
- ❌ The second, unintegrated entity bank is gone (file deleted, no references remain). The $20k grant, the persistence race, and the duplicate money path died with it.
EL_ShopComponent — Feature/Shop/EL_ShopComponent.c
- ✅ Catalog-driven buy:
BuyItem(item, qty, buyer)guards null/qty, server- only, price × quantity,EL_MoneyUtils.RemoveAmount, refund on inventory-full. A sell or buy of a stack pays or gives one unit per quantity throughEL_ShopRules.StackTradeTotal(verified 2026-10-11). The menu sells at most 99 of a held stack per click, the server capEL_CharacterShopComponent.MAX_TRANSACTION_QUANTITY; a request outside 1..99 is refused and logged. (2026-11-07) - ✅ Money correctness (verified 2026-08-30): the partial-funds exploit is
closed —
BuyItemcompares the actual removed amount against the total and refunds only what was removed before failing (a $50 buyer cannot buy a $100 item), and a partial delivery refunds only the undelivered quantity.SellItempays cash only after the goods are confirmed removable and claws back overpayments.IsItemSoldHeregates buy and sell. - ⚠️ No shop stock (infinite supply).
- ✅ The general store sells a short list (2026-11-08, M1 RC QA):
Shop_GeneralStore_Debugcarries a curatedm_aShopItems(apple, tomato, cabbage, water, apple juice, field dressing, ALICE backpack). Without it the price list showed the whole 1312-line base-game catalog, starting with an SVD magazine pouch and two "!BASE KIT!" rows. Prices still come fromShopPrices.conf. The UI kit status row gives its action button more width (label 1.5, status 1, button 1.5), so "Order" is no longer cut to "Orde".
Shop price list — Feature/Shop/LF_ShopPriceList.c (was EL_ShopMenu)
- ✅ (2026-11-08, M1-13) Typed price list on
LF_UIScreenwith theLF_UISkin.PriceList()skin (paper, mono face, ink buttons): carried cash, a "Look for" field that narrows the list as you type, up to 40 lines of item and buy price with an Order button, an order slip (quantity with One less / One more, 1..99), Pay cash, Sell these, Leave. Buy and sell are the existingEL_CharacterShopComponentRPCs; every request now gets a reply withLF_EShopRefusal(not sold here, unpriced, cash short, pockets full, nothing to sell, quantity, too far, no shop), the money moved and the carried cash, printed on the slip. The shop no longer raises toasts. An accepted trade playsSOUND_PICK_UPon the character; UI sounds areEL_ShopActionattributes.EL_ShopActionopens only on the acting machine for its own character. Closes on Leave, MenuBack, death, or 3.5 m from the counter. Tests:shop/refusal-reasons,counter/local-only,counter/screen-layout.
EL_TraderManagerComponent + EL_InventoryStorageManagerComponent — Components/InventorySystem/
- ✅ Sell-to-trader pays cash (verified 2026-10-11): inserting into a
trader-owned storage pays one cash unit per stack quantity
(
EL_ShopRules.StackTradeTotal) viaEL_MoneyUtils.AddCash(pay-before-delete, clawed back if the item cannot be removed); black-market rejects non-civilian factions. The old "payout to ATM bank" and thePrint("testtesttest")debug line are gone. - ✅ The value lookup and the black-market decision are the static seams
EL_TraderManagerComponent.GetUnitValue/CanAccessBlackMarket(extracted 2026-11-07; the lookup now skips a null row instead of crashing), pinned bytrader/sell-rules(LOGIC; red-proof observed 2026-11-08). - ⚠️ Unguarded
pStorageFrom.GetOwner()(null crash); nullm_aTradableItemsforeach crash; delete-then-pay not atomic;RETCODE_ITEM_TOO_BIGused for "not tradable"; black-market check fails closed but is only cache-keyed.
Gathering / Processing / Resources
EL_GatherAction — Feature/Gathering/EL_GatherAction.c
- ✅ Interact action granting
m_GatherAmountofm_GatherItemPrefabinto the user inventory; tool requirement (right hand / left gadget / anywhere in inventory); per-resource gather counter + cooldown restock. - ✅ Server re-validates tool, crop stage, cooldown, and malformed item/amount
configuration in
PerformAction(verified 2026-08-30). Remaining: timeout attribute says "ms" but is compared as seconds-compatible — fragile. - ✅ The server applies a gather when no peer owns the prop, and a grant that adds nothing is an error log (verified 2026-10-10). A sandbag the inventory rejects, or a nugget with no usable slot, used to look like a successful use.
- ✅ A node id copies tool, yield, and cooldown from
ResourceChains.conf. - ✅ Crop growth runs on the server only (2026-11-08, #157):
EL_BaseCropstarts its growth clock only on the server (Replication.IsServer()andRplSession.Mode()is notClient), not onRplComponent.IsOwner(), which is still true on a client while the world loads. Every stage read goes throughEL_BaseCrop.StageAt, which returns null for an index outside the list. Before this each game client logged aNULL pointer to instanceVME inEL_BaseCrop.EOnFramefor every crop. Each crop logscrop <name> init: grows here <bool> (rpl mode <mode>)once. A missing row or a prefab that does not resolve logsEL_Debug.Errorand grants nothing (verified 2026-10-11). The action stays on the node.
EL_ProcessAction — Feature/Processing/EL_ProcessAction.c
- ✅ Recipe: consume all
m_aProcessingInputs, produce allm_aProcessingOutputs(into inventory or force-dropped). - ✅ Free-output exploit closed (verified 2026-08-30): malformed recipes,
empty input/output lists, missing inputs, and non-positive amounts are
rejected before inventory mutation. Regression coverage is in
EL_Test_ActionGuards. - ✅ A machine id copies inputs and outputs from
ResourceChains.conf. An empty recipe or a prefab that does not resolve logsEL_Debug.Errorand returns before inventory changes (verified 2026-10-11). - ✅ Cocaine, jam, and beer are one-input, one-output recipe prefabs under
Prefabs/Processing/Chains/, each parented toItemProcessor_Base(verified 2026-10-11). Cocaine consumes coca leaves and produces a cocaine pouch. Jam consumes rowan berries and produces berry jam. Beer consumes an empty beer bottle and produces a beer bottle.EL_Test_RecipeChainschecks that each input prefab and output prefab is set and distinct.
EL_ResourceChainCatalog — Feature/Resources/EL_ResourceChainCatalog.c
- ✅
Configs/Resources/ResourceChains.confnames each gather node's tool, yield, and cooldown, and each machine's inputs and outputs (verified 2026-10-11). Yields are item prefabs. Gather and process do not mint cash. The playable iron, copper, and gold rows name the pickaxe. The log row does not: the axe stays onCuttableTree_Base, andLogPile_Debuggrants a log with no held tool so the sawmill still runs before the axe pickup. - ✅ MainWorld's St Philippe farm (
Area_StPhillipe_AppleFarm.layer) places the apple trees, the juicer, and a juice trader (15 cash, matching the DebugWorld juice crate), plus iron ore, a pickaxe, the furnace, and an iron-bar trader (45 cash, the ShopPrices iron-bar sell price). That iron node requires the pickaxe in hand. DebugWorld keeps Apple, Plum, Tomato, Logging, Mining, Cement, Gravel, and Sand.IronOre_Debugusesiron-debug(4 nuggets per piece) so one piece still feeds both furnaces.GoldOre_DebugandCopperOre_Debuguse the debug rows with the prefab yields. Every ore rock still needs the pickaxe (the hit zone's tool list).
EL_DestructibleResourceComponent + EL_DestructibleResourceHitZone — Feature/Resources/
- ✅ Damage gated to configured tools; tool → per-hit damage; FX on hit. The
tool match compares prefab GUIDs, so path casing no longer matters. A melee
hit from a tool the resource does not list logs
[ELDebug:Resources]and deals 0 (2026-11-08).
LF_OreNodeComponent + LF_OreYield — Feature/Mining/ (2026-11-08)
- ✅ You mine ore by swinging the pickaxe at the rock; there is no gather
prompt. The hit goes through
EL_DestructibleResourceHitZone(pickaxe 20 damage, the axe 0). Every 60 damage (three swings) a piece breaks off and the node row's yield drops as real nugget items beside the rock, toward the miner. After the row'sm_iMaxGatherspieces the rock crumbles (hidden, no collision) and grows back after the row'sm_fCooldownMs. Server- authoritative; clients get the hit dust, a shake and the vanilla rock-break sound by broadcast, and the spent state by RplProp (JIP-safe). The spent state is not saved. Ported from thebonbonEL_ToolHitZoneandEL_RandomPrefabSpawnablewith permission. Tests: LOGICmining/ore-yield, WORLDmining/pickaxe-hits. - ✅ Nuggets and bars render textured:
EL_QuantityComponent.UpdateStackModelno longer re-sets the base mesh, which dropped the prefab's material overrides and rendered the items white. Cash, pickaxe, axe, ore and bar inventory previews use the Front preset with a narrow FOV. - ⚠️ Inventory previews draw the mesh's baked material and ignore the prefab's
material override, so nuggets preview as grey stone and bars as red brick
(re-applying the remap to
ResolvePreviewEntityForPrefabdid not change the render, 2026-11-08). The fix is a mesh that bakes the ore material (Workbench FBX import). The pickaxe uses the pistol rig and is gripped at the handle end;HandAdditiveTranslationhas no effect on a weapon, so moving the grip also needs a re-exported mesh with the pivot up the handle.
LF_MiningSpawn + LF_MiningForge — Feature/Mining/
- ✅ Weighted ore spawn. A roll in
[0, total)maps onto the first bin, including a roll of 0. The spawn count is the shortfall to the configured amount, and an over-full area spawns nothing. An empty list logsEL_Debug.Error. - ✅ Timed forge. One iron nugget
(
{6853E2E2AE878611}Prefabs/Resources/Mining/IronNugget.et) becomes one iron bar ({BCD1E8CD7F975074}Prefabs/Resources/Mining/IronBar.et) after 5 seconds (5000 ms). The nugget stays until the delay elapses. A missing nugget, a second start, or a nugget removed before the end logsEL_Debug.Errorand yields no bar. A zero delay finishes inside the start.
Jobs / Level / Licenses / Whitelist
EL_PlayerJobComponent — Feature/Jobs/EL_PlayerJobComponent.c
- ✅ Per-player job state, per-job level/XP maps, license gates (POLICE→POLICE_ACCESS,
MEDIC→MEDIC_ACCESS), client→server
RpcAsk_SetJobre-validates licenses. - ✅ Fruit-catcher mint closed (verified 2026-08-30): the server clamps any
claimed score through
EL_GetFruitCatcherRewardCounttoEL_FRUIT_CATCHER_MAX_SCORE(100); pin testsecurity/fruitcatcher-reward-clamp. - ✅ Fruit-catcher claim loop closed (verified 2026-11-07): a claim
pays only a server-opened round. The minigame calls
AskStartFruitCatcherRoundwhen a round starts; the server records the round id and start time (LF_FruitCatcherRound) andAskClaimFruitCatcherRewardsends that id back. One claim per round, same fruit, at leastLF_FruitCatcherRound.MIN_ROUND_SECONDS(30 s) after the start, and the caller must hold FARMER. The minigame screen is not in this repo; when it lands it must call the start ask. Pin testssecurity/fruitcatcher-rounds,security/job-workplace-and-seed. - ✅ Job switch and kit gates (verified 2026-11-07): a
client-requested switch (
RequestJobFromClient, behindRpcAsk_SetJob) needs anLF_JobSignUpActionfor that job withinLF_JobGuards.WORKPLACE_RADIUS(5 m) andSWITCH_COOLDOWN_SECONDS(30 s) since the last switch; going UNEMPLOYED needs no workplace. Only delivery jobs have a placed workplace today, so a client cannot switch itself to MINER/LUMBERJACK/FARMER/MEDIC/POLICE by RPC.GiveJobEquipmenthands each job's kit once per account per server session (LF_JobGuards.ClaimEquipmentGrant). Pin testsecurity/job-switch-guards. - ✅ Debug-license backdoor removed (verified 2026-08-30):
RpcAsk_DebugGrantLicenseno longer exists anywhere inScripts/Game. - ✅ No passive paycheck (verified 2026-11-08, M1-07): the timer paycheck
(700-1000 cash every 300 s to every job, unemployed included) is deleted; the
component has no frame event.
EL_JobConfighas no salary fields. Money comes from completed runs, trader sales, police duty pay (LF_DutyPay), and the unemployment benefit (LF_UnemploymentBenefit). Pin testjobs/no-passive-paycheck. - ✅ Civilian run XP (verified 2026-10-11):
AddExperienceforwards toLF_CivilianJob. A completed courier, fuel, or taxi run addsm_iJobXpPerRunXP andPullCareercopies the level onto this component, soOnLevelUpfires. Level 1 may take courier work, level 2 opens fuel delivery, level 3 opens taxi. Under-level signup refuses and is logged. - ✅ Job pacing is config and hidden (2026-11-08, #156):
LF_Jobs.confsetsm_iJobXpPerRun(100),m_iJobXpFirstLevel(500), andm_fJobXpLevelGrowth(2): 5 runs open fuel delivery, 15 open the taxi (LF_CivilianJob.RunsToReach). No level text reaches the player: the level-up hint ("grade N, +N%") and the grade in the hire hint are gone (#LF-Job_HiredHidden). When a run opens new work the drop-off shows#LF-Job_NewWorktitled with the job name (LF_CivilianJob.NewlyOpened). Pay per run is unchanged (40/60/80): 5 parcels then 5 fuel runs is about 500 cash, so a new player can buy the S105 (1000, from the bank) after about 10 runs. Test:jobs/pacing. - ✅ Job tool grant (2026-11-08): miner gets
{23341E2840F5ACB4}Prefabs/Tools/Pickaxe.et(the oldPrefabs/Tools/Axe/Pico.etnever existed), lumberjack gets{9CAC68BA551417A4}Prefabs/Tools/Axe/Axe.et(the old GUID was wrong). Spawn goes throughEL_Utils.SpawnEntityPrefab; a tool that does not fit is logged and left at the player's feet. - ⚠️ Whitelist branch in
SetJobis dead (only POLICE is restricted and it is special-cased). Hard-coded Spanish strings (not localized). Per-job level/XP persistence is convention-only, no SaveData pair.
LF_CivilianJob — Feature/Jobs/LF_CivilianJob.c
-
✅ Workplace signup, one active run, cash once, abandon pays nothing (verified 2026-10-11). The bank balance stays 0. Parcel cargo is
{9CA41609F6C806AE}Prefabs/Items/Roleplay/LF_Parcel.et, a cardboard box carried in both hands (#155: the bareHandCarryItem_Base.ethad no mesh, so the hands were empty on a client). Test:items/carry-meshes. Fuel cargo is{12D5AD21E383B768}Prefabs/Items/Fuel/Jerrycan_01/Jerrycan_01_item.etand the drop consumes it. The taxi fare uses the rank vehicle{54C3CC22DEBD57BE}Prefabs/Vehicles/Wheeled/S105/S105_beige.etand does not need a second client. A missing cargo prefab or a full inventory logsEL_Debug.Errorand that run does not pay. DebugWorld placesRoadmap_Job_Depot(post counter{28F9F4DC40089B51}Prefabs/Props/Services/Post/CounterPostOffice_01.et),Roadmap_Job_Drop,Roadmap_Job_TaxiPad, andRoadmap_Job_FuelCanonStreet_Jobs.layer: the counter on the post office porch, the drop on a house porch at the east end, the taxi pad at the bus stop by the square, the fuel can at the fuel station. -
✅ Sign-up offers on a dedicated client (2026-11-08, #157): the greyed reason reads
#LF-Job_MoreRunsFirst("Do more parcel runs first"), not "Your work level is too low". A client reads the level replicated onEL_PlayerJobComponent(the career map is server-only), so fuel and taxi open on the client once the server levels the career. -
✅ The toasts say what to do next (2026-11-08, M1 RC QA): signing up shows the job as the title and the next step as the body (
LF_CivilianJob.SignedUpKey: "Pick up a parcel at this counter"), not the job name twice. Picking up a parcel toasts where the drop is (RunStartedKey,#LF-Job_RunStarted_Courier, 8 s). -
✅ Pick up, Complete, and Abandon work on a dedicated client (2026-11-08, LOGIC
jobs/client-offers): the offers read the server-only sign-up and run maps, so on a hosted server "Pick up parcel" stayed greyed ("Sign up at the workplace first") after a good sign-up. A client now reads the job replicated onEL_PlayerJobComponent(set at sign-up and when a restored sign-up is put on a spawned body) and, for Complete and Abandon, the cargo in its own inventory (LF_JobStartRunAction.ClientRefusal,LF_CivilianJob.RunOfferOpen). The server re-checks everything when the action runs.
EL_JobManager — Feature/Jobs/EL_JobManager.c
- ✅
GetGatherRewardandGetProcessRewardstay 0 unless a job scope callsSetScopeRewardfor that prefab (verified 2026-10-11). Gather and process mint nothing on their own. The player sells the product to a trader. A scope payout, when one exists, is cash viaEL_MoneyUtils.GiveCash. The ATM stays the only cash-to-account boundary. - ✅
#EL-Job_EarnedisEarned $%1 for work!andGiveRewardpasses the paid amount throughWidgetManager.Translate(verified 2026-10-11).
EL_PlayerLevelComponent — Feature/Level/EL_PlayerLevelComponent.c
- ✅ Level/XP/skill-point state;
GetExperienceForNextLevel = level * 100; passive bonuses (gatheringSpeed 1+(level-1)*0.1,gatheringAmountsame,saleBonus 1+(level-1)*0.05);CheckLevelUpdrains XP cascades, +1 SP and +1 total SP per level. - ⚠️
SetLevel/SetExperience/SetSkillPoints/SetTotalSkillPointsEarnedhave no server guard, no validation, no recompute (inconsistent state possible). Saves on every XP gain (DB-write hazard).GetBonusesTextreturns a Spanish literal.
EL_LicenseManagerComponent — Feature/License/EL_LicenseManagerComponent.c
- ✅ License catalog (26 entries),
CanUnlockLicense(owned/config/whitelist/ level),CanAffordLicense(SP cost),PurchaseLicense(spends SP + unlock),HasLicense, persistence restore. - ✅ Whitelist mapping and purchase ordering fixed (verified 2026-08-30):
GetLicenseWhitelistJobmaps MEDIC access to MEDIC and police licenses to POLICE;PurchaseLicensevalidates requirements beforeUnlockLicensespends skill points.SetUnlockedLicensesis the persistence restore seam, not a player purchase path. Remaining: not localized;m_bIsInitialLicenseis used for initial grants. - ✅ Replicates as a bitmask (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual): the old
[RplProp] ref arraybroke the simple-types rule. The server list stays local;m_iUnlockedBits(an intRplProp,EL_LicenseBits) is written on every change. On a clientHasLicenseandGetUnlockedLicensesdecode the mask. Bit order isEL_LicenseBits.Order(): append new types, never reorder.
EL_WhitelistManager — Feature/Whitelist/EL_WhitelistManager.c
- ✅ The playtest host is police from the first join (2026-11-08, LOGIC
whitelist/first-player-police): withLF_MissionHeader.m_bLF_FirstPlayerPolice(on inLF_PlaytestTown.conf, off by default), the first player to join while the police roster is empty is added to it ($profile:LifeFramework/whitelist.txt), so the maintainer can clock in on his own playtest without writing the file. That file is the one roster: the duty desk and the join gate read it. The unusedWhitelists/Police.txtandWhitelists/Medic.txtlists onEL_WhitelistRespawnHandlerComponent(nothing read their flags) are gone fromGameMode_Roleplay.et; the component keeps the CONNECT join gate. - ✅ Pure static in-memory whitelists;
IsFactionRestricted(POLICE, MILITARY, MAFIA),IsJobRestricted(POLICE only), add/remove/get. - ✅ Compiles (verified 2026-08-30):
enum EL_EFactionTypeis now defined in this file (CIVILIAN/POLICE/MILITARY/MAFIA). - ✅ Restart save and police gate (verified 2026-10-11): the manager
writes
$profile:LifeFramework/whitelist.txtitself, version first, andLoadreplaces the cache. A second load does not duplicate entries. A missing file logsEL_Debug.Errorand leaves the police faction closed. Civilian stays open.MayJoinFactionrefuses a uid that is not on the police list and logs that refusal. The server calls it inLF_DutyLocker.RefuseClockInwhen a player clocks in at the duty desk (M1-11; the faction screen is gone). A client has no whitelist; the desk offer reads the roster bit ofEL_AccountReplication. A uid that was not in the file is still refused after load. This file is notEL_PersistenceComponentSerializer. - ✅ An unreadable file is never overwritten (2026-11-07, test
restart/whitelist-unreadable-file-kept, verified 2026-11-07): a file that exists but cannot be opened, has no or a wrong version, or fails to parse setsIsLoadFailed().Save()then refuses withEL_Debug.Errorand returns false, so the next admin add cannot rewrite the file with only the new entry. A missing file is a first run and saving stays allowed. A later goodLoadclears the flag. - ⚠️ Getter returns the live internal array (callers can mutate without logging).
AdminChatChannel — Feature/Chat/AdminChatChannel.c
- ✅ Server re-checks the admin role (verified 2026-11-07):
ProcessMessageon the authority refuses any sender withoutEPlayerRole.ADMINISTRATOR(IsAdminSender) before parsing!wadd, so a modded client cannot whitelist itself for POLICE. Pin testsecurity/admin-chat-role.
EL_BuildingManagerComponent — Components/BuildingSystem/EL_BuildingManagerComponent.c
- ✅ Placement gated and paid (verified 2026-11-07): the server
(
PlaceHeldBuilding, behindRpcAsk_SpawnEntityPrefab) requires the held performer for that prefab, a position withinMAX_PLACEMENT_DISTANCE(5 m) of the character, and consumes the held item (ConsumeBuildingItem: one off a quantity stack, else the item is deleted) before spawning. The client preview stops offering placements past that range. Pin testssecurity/building-placement-range,security/job-workplace-and-seed.
LF_UnemploymentBenefit — Feature/Jobs/LF_UnemploymentBenefit.c
- ✅ "Collect benefit" (
LF_CollectBenefitAction) atRoadmap_Job_Depot(verified 2026-11-08: LOGIC testjobs/unemployment-benefit; in-world collection is manual). The server reads the bank balance (EL_ATMManager) and paysm_iBenefitAmountcash when the balance is strictly belowm_iBenefitBalanceThreshold, the player is not on duty as police and has no active delivery run, andm_fBenefitCooldownSecondspassed since that account's last collection. Values inConfigs/Jobs/LF_Jobs.conf(flagship $100, below $500, 1200 s). Pick-up sound, then a#EL-Benefit_Paidtoast; a refusal toasts its reason. A full inventory pays nothing and does not start the cooldown. - ✅ The cooldown survives a restart (M1-10): the save holds seconds since the last collection, because world time starts over.
Crime & Police
LF_Breach — Feature/Crime/LF_Breach.c
- ✅ Configured charge opens an
EL_LockComponentafter the fuse and leaves it open until repair (verified 2026-10-11).Arm(targetId, playerUid)is the call later robberies use. Fuse 0 opens in that call. Wanted and the police notify fire once, at arm time, not when the offer refreshes. A second charge on an open target refuses. Police can always repair; the owning organization can repair its own door.Export/Applyput the open lock back in memory but are not wired into the save serializer, so a restart loses it. The vanilla charge is not detonated. DebugWorld placesRoadmap_Breach_DoorandRoadmap_Breach_ChargeonStreet_Breach.layer. The charge prefab is{33CBDE73AB48172A}Prefabs/Weapons/Explosives/DemoBlock_M112/DemoBlock_M112.et. Charge consumed (verified 2026-11-07):Arm(targetId, playerUid, chargeCarrier)takes one charge fromchargeCarrierbefore the wanted bump and refuses (arm refused: ... carries no breach charge) when there is none. The door, bank vault, and hospital cabinet actions pass the acting player; the door action shows#EL-Breach_NeedCharge. A null carrier is the server-scripted arm used by tests. Pin testsecurity/breach-needs-charge.EL_Door_Baseinherits{776E0766BBDB17A3}Prefabs/Structures/BuildingParts/Doors/Door_HouseWooden_E_A_LEFT_EXT_COV_B.et. - ✅ Client offers (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual): the target table is server-only, so
LF_BreachTargetComponentmirrors open/armed asRplProps (SetReplicatedState, written fromOpen/Repair/Applyand the fuse schedule).CanArmForOffer/CanRepairForOfferaskLF_Breachon the server and the mirror on a client. A client offers repair to police and, on an organization door, to anyone; the server decides membership. - ✅ Charge lifetime (2026-11-08): a charge whose fuse cannot
fire is dropped with
EL_Debug.Error(CancelCharge) instead of leaving the target armed forever: no world, a placed door withoutLF_BreachTargetComponent, a timer that could not be scheduled, or the door deleted before the fuse fired (LF_BreachTargetComponent.OnDeletealso removes the callqueue entry). The cancel also clears the replicated armed mirror. An unplaced, script-driven target keeps its charge and opens throughAdvance.Configureclears the armed flag and elapsed time; the open flag is kept. Testrobustness/lifetime.
LF_Bank — Feature/Crime/LF_Bank.c
- ✅ A vault calls
LF_Breach.Arm, then spills a config number of bags of MoneyStack cash (verified 2026-10-11). The haul never deposits or withdrawsEL_BankAccount. Civilian-only, cooldown, and minimum police on duty use the same comparisons asEL_RobAction. A second breach inside the cooldown spawns nothing. Over-capacity requests are cut to the configured bag count. Carry leaves the cash on the bag; dropping it leaves that cash in the world.Export/Applyrestore the ground bags and do not write a bank balance; they are not wired into the save serializer, so a restart loses the bags. DebugWorld placesRoadmap_Bank_Vaultat 194.1 0 265 (on the bank's east wall) andRoadmap_Bank_Bagat 195.6 0 263 onStreet_Bank.layer. The bag is{65DE48DCC1D4DD01}Prefabs/Items/Roleplay/LF_MoneyBag.et(a vanilla suitcase mesh, #155). Cash is{5439738849229352}Prefabs/Items/Currencies/MoneyStack.et. The shell is{D1859197E1AB4E8A}Prefabs/Structures/Services/EL_Bank.et, which inherits{363A616E6C64BB7F}Prefabs/Structures/Services/Government/MunicipalOffice_E_01/MunicipalOffice_E_01.et. That building is capture-only and is not placed in the debug row.
LF_Hospital — Feature/Crime/LF_Hospital.c
- ✅ Hospital till pays
{5439738849229352}Prefabs/Items/Currencies/MoneyStack.etthrough the civilian, cooldown, and minimum-police gates and does not deposit to the bank (verified 2026-10-11). The placed action isEL_RobActionon the till's singleActionsManagerComponent. Wanted rises by 1. One police alert per successful rob; an offer refresh does not alert. DebugWorld placesRoadmap_Hospital_Tillat257 0 256.4(hospital porch) onStreet_Hospital.layer. - ✅ The cabinet is a forced-entry container.
BreachCabinetcallsLF_Breach.Arm. When that lock opens it spawns the configured medicine prefabs: field dressing, morphine, saline, and tourniquet. A missing prefab logs and that line is skipped. A second breach while the cabinet is open spawns nothing and does not alert again.Roadmap_Hospital_Cabinetis at198 0 120. Items stay ordinary; confiscation of a contraband flag belongs to the contraband scope.
LF_Murder / LF_SafeZone — Feature/Crime/ (M1-27, #125)
- ✅ Murder makes you wanted (test
crime/murder). Every character death reachesEL_GameModeRoleplay.OnControllableDestroyedExon the server. A player who kills another player or an NPC gets wanted +m_iMurderWanted(LF_Police.conf, default 3), oneLF_WantedReportsreport with the#LF-Offence_Murderoffence and their description (no name), a radio dispatch to every on-duty officer (LF_Dispatch.Report, no site alarm), and a toast. Not murder: a death with no player behind it, a self-kill, a destroyed vehicle, and (configm_bOfficerKillOfWantedExempt, default on) an on-duty officer killing a wanted suspect. Each decision logs[ELDebug:Murder]. No automatic ban. - ✅ Spawn safe zone (test
safezone/blocks).LF_SafeZoneComponentonLF_SafeZone_Square(Gamemode.layer, fountain 115 0 250, radius 30 m as the component attribute) registers a flat circle. The moddedSCR_CharacterDamageManagerComponent.HijackDamageHandlingswallows a hit when a player hurts a different player and either stands inside: no health, bleeding, or ragdoll. NPC, world, and self damage pass. Weapons still raise and fire. No police exemption: arrest is an interact action. Each refusal logs[ELDebug:SafeZone] refused ...; the shooter gets a toast at most every 8 s. Twosafe_zonenotice boards (Sign_SafeZone_Spawn,Sign_SafeZone_East) say so in the world. - ✅ Both civilian spawn points stand on the square (the second moved from
170 0 199, by the quarry, to 121 0 253);
safezone/blocksasserts every civilian spawn point is inside a zone. The hospital wake (M1-26) is not a spawn and stays outside.
EL_RobAction / EL_RobWeaponAction / EL_RobVehicleAction — Feature/Crime/
- ✅ Cash/weapon/vehicle robbery; cooldown + civilian-only + min-police-on-duty gates; raises wanted level (+1/+2/+3); notifies police. DebugWorld's three points set the minimum to 0 so one civilian can run them (verified 2026-10-10). The action classes still default to 2 / 1 / 3.
- ✅ Cash payout + reliable wanted bump (verified 2026-08-30): the haul is
EL_MoneyUtils.AddCash(never the bank — pin testcrime/rob-rewardasserts the bank balance stays 0), and the account cache churn is fixed so every robbery escalates wanted. - ✅ Fuel-station till (verified 2026-10-11):
Roadmap_Fuel_TillonStreet_Fuel.layerat259 1.04 241.2(on a counter at the fuel station) is{4E4E1620C6B02E1F}Prefabs/Props/Commercial/EL_FuelTill.et, which inherits{51D31AB07AE3C1C7}Prefabs/Props/Commercial/CashierShop_01.et, withEL_RobAction. Payout is a config range (m_iMinPayout..m_iMaxPayout); the server rolls it, and min equal to max pays that fixed amount. The debug till pays 150 MoneyStack cash. The bank balance stays 0. Wanted rises bym_iWantedIncrease(default 1) and still clamps at 5. A second attempt inside the cooldown is refused and logged withEL_Debug.Error. The police alert fires once, insideCommitRobbery, not while counting on-duty police and not when the offer refreshes. The fuel station building{9269BD1B6BF3A441}Prefabs/Structures/Commercial/FuelStations/FuelStation_E_01/FuelStation_E_01_V1.etis the capture prefab and is not placed in the row. Player strings:#EL-Fuel_Till_Rob,#EL-Rob_Cooldown,#EL-Rob_CiviliansOnly,#EL-Rob_NeedPolice. - ✅ Tests call the real actions (2026-11-07; red-proofs observed
2026-11-08).
crime/rob-reward(WORLD) runsEL_RobAction.CommitRobberywith a real character: MoneyStack cash, bank 0, wanted +1, cooldown, civilian gate. The weapon and vehicle grant/spawn, wanted, and alert moved intoEL_RobWeaponAction.CommitWeaponRobberyandEL_RobVehicleAction.CommitVehicleRobbery(wantedWANTED_INCREASE2 / 3;PerformActionkeeps the server-only guard), and their civilian check intoEL_RobAction.IsFactionAllowedToRob;crime/rob-weapon-vehicle(WORLD) drives them. The earliercrime/rob-rewardandcrime/rob-wanted-clampnever called an action and are gone; the clamp is pinned byroadmap/fuel. - ✅ No account, no robbery (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual):
CommitRobberyrefuses and logsrobbery refused: no accountinstead of paying without the civilian gate and wanted bump.PerformActionruns on the server only; offers readEL_AccountReplication, so the till is offered on a dedicated client and the min-police gate uses the replicated count. The success toast goes to the robber only (EL_Utils.NotifyEntity), the alert to on-duty police only. - ✅ Weapon/vehicle cooldown in seconds (2026-11-08, test
robustness/lifetime): both compare throughLF_RobCooldown.IsActive, which converts the seconds attribute to world-time milliseconds (before, 300 s was 0.3 s). A never-robbed action has no cooldown.PerformActiongrants/spawns, raises wanted, and alerts on the server only.EL_RobVehicleActionno longer notifies from its on-duty count (that ran every offer refresh) and refuses an empty vehicle prefab withEL_Debug.Error; the spawn goes throughEL_Utils.SpawnEntityPrefab. - ⚠️ Near-duplicated guard logic across all three.
Police alerts go through
LF_Dispatch.Report(M1-20).
LF_Jail — Feature/Police/LF_Jail.c
- ✅ Placed cell and timed sentence (verified 2026-10-11).
GetJailPosition()reads the cell origin. Sentence length is wanted level times the config step, clamped to the config max. Arrest of a clean player refuses. Expiry reduces wanted by the configured amount. Early release refuses. A locked jail door returns a prisoner who leaves the volume; an open door (the sharedLF_Breach.ArmonRoadmap_Jail_Door) does not, and the remaining time stays.Export/Applyput the remaining seconds back, andExportAll/ApplyAllcarry every serving sentence through the save. DebugWorld placesRoadmap_Jail_Door,Roadmap_Jail_Cell, andRoadmap_Police_DeskonStreet_Jail.layer(the desk onStreet_Police.layer). The placed door is{787617D5B683FE41}Prefabs/Structures/BuildingParts/Doors/EL_JailDoor.et, which inherits{B597177F09247CE9}Prefabs/Structures/BuildingParts/Doors/Door_PrisonCell_E_A_Left_INT_ST_B.etand addsEL_LockComponent, so the prison mesh and the shared breach actions stay together. That vanilla door is the prefab the world test loads. The officer actionLF_JailArrestActionis on the suspect.LF_JailEscortis the vehicle path: an on-duty officer loads a wanted player into the nearest free non-driver seat of a base-game vehicle. The sentence starts when they arrive at the cell, not when they enter the car. A player who is not wanted is refused and logged. Early release stays refused. A reconnect or a restart keeps the remaining sentence. DebugWorld placesRoadmap_Escort_Carat247.5 0 238(beside the police station) onStreet_Jail.layer, the police M151 withLF_JailPutInVehicleAction. The action is listed at the car. Performing it still requires an on-duty officer and a prisoner on foot. (verified 2026-10-11) - ✅ Sentences survive a restart (2026-11-07, tests
restart/jail-serializer-roundtripande2e/pass2-verify-loadacross a real restart, verified 2026-11-07):LF_Jail.ExportAll/ApplyAllare thejailpayload ofEL_PersistenceComponentSerializer, appended at version 5. Only serving sentences with time left are saved;ApplyAllreplaces the live set, so a second apply converges. A version 1-4 save stops before the payload and leaves live sentences alone. - ✅ One jail tick (2026-11-08, test
robustness/single-jail-tick): the first placedLF_JailCellComponentowns the jail config and the repeatingLF_Jail.Tick; a second cell logsEL_Debug.Errorand neither reconfigures nor ticks (two cells used to run sentences at double speed).OnDeleteremoves the tick.LF_JailArrestableComponenthostsLF_JailEscort.Tickon one live instance and hands it to the next registered one when that host is deleted. - ⚠️ Moving a connected prisoner to the cell, or pulling them back into it,
goes through
SCR_Global.TeleportPlayer(the vanilla server-side player teleport) instead of a server-onlySetOriginthat the owning client can overwrite; a stand-in with no player is still set directly (2026-11-07). A prisoner seated in a vehicle is dismounted straight to the target first (GetOutVehicle_NoDoor; a client-owned player through the vanilla Game MasterSCR_EditableCharacterComponent.SetTransform), so the vehicle stays put (M1-21, testjail/vehicle-arrest). Not yet checked on a dedicated server with a remote client. - ✅ Online time (M1-21, 2026-11-08, tests
jail/online-time,e2e/pass2-verify-load): the sentence counts down only for connected players (LF_Jail.TickreadsPlayerManager.GetPlayers;TickOnlineis the testable seam). An offline prisoner keeps the time they had at logoff and logssentence paused/sentence resumed. A prisoner who spawns with time left (rejoin, restart) goes back to the cell with their slip (EL_SpawnLogic). - ✅ Sentence slip (M1-21, test
jail/slip): sentencing hands the prisonerPrefabs/Items/Roleplay/SentenceSlip.et(vanilla personal-papers mesh).LF_JailSlipComponentprints name, offence (wanted level), and online minutes left; the minutes replicate only when the whole minute changes. Hovering it in the inventory reads it (LF_JailSlipUIInfo). A respawn or a second arrest reprints the carried slip instead of adding one. No new save data. - ✅ Visible release (M1-21, tests
jail/slip,jail/online-time): release opens the cell door (DoorComponent.SetControlValue, the vanilla door motion and sound; it closes after 5 s), places the prisoner at the cell release offset (m_vReleaseOffset; DebugWorld0.15 0 10, outside the door), stamps the slip, then shows the toast 1.5 s later. Release runs once per sentence. The DebugWorld cell now sentences 60 s per wanted level, max 300 s (it was zero-length).
EL_PoliceMenu — Feature/Police/EL_PoliceMenu.c
- ✅
ArrestPlayeris the client-side entry point. The wanted list left the menu in M1-11: wanted people are on the station wanted board. The remote fine (RpcAsk_EL_PoliceFine,FinePlayer, the Fine button) is gone (M1-19): a fine is a paper ticket written face to face (LF_FineTicket*). - ✅ Client→server bridge works (verified 2026-08-30): the menu routes
through
SCR_ChimeraCharacter.EL_AskPoliceArrest, and the server handlerRpcAsk_EL_PoliceArrestvalidates on-duty officer and wanted target before acting. Arrest also requires the configured arrest rank (patrol on the flagship, #165).EL_PoliceUtils(pure) holds the sentence-length math and the reach check for tests;MAX_FINEis the senior ticket cap.EL_AskPoliceSelfPromotealways refuses. - ✅ Arrest starts a sentence (verified 2026-10-11): the arrest RPC calls
LF_Jail.Arrestafter the rank gate and no longer clears wanted or teleports to"127 1 137". - ✅ Hands-on only (2026-11-07): arrest refuses a
target that is not connected or stands beyond
EL_PoliceUtils.POLICE_REACH(5 m, the in-world arrest range) and shows#EL-Police_TooFar. Neither the framework nor vanilla has a restraint state, so reach is the requirement. Test:security/police-abuse.
LF_FineTickets — Feature/Police/LF_FineTicket*.c
- ✅ A fine is a paper ticket paid at the police desk (M1-19, #104;
decision 2026-11-08). An on-duty officer facing a player picks Write ticket
(the
fineslot of the player interaction menu,LF_FineTicketRole, no prefab edit). The server opens the ticket pad (LF_FineTicketPad, paperLF_UIScreen) on the officer's machine: one "Write: , " button per offence fromLF_Police.confm_aTicketOffencesat its fixed amount; offences above the officer's rank cap (LF_PoliceRanks.FineCap: patrol 1000, senior 5000) print as a plain "senior officers only" line. The server re-checks duty, reach (2 m), the cap, and at most 3 unpaid tickets per holder, then hands overPrefabs/Items/Roleplay/FineTicket.et: number, date, holder, offence, amount, deadline, what it rises to, where to pay, officer rank and name. Cues: the officer's point gesture, the paper sound from the ticket, the holder's pick-up gesture, and the ticket card on the holder's screen. Inventory hover and Read (on the ground) show the same text. - ✅ Pay ticket on the punch clock of
LF_PoliceDutyDesk.et: carried cash only (LF_EntityCashWallet), never the bank, all or nothing, oldest unpaid first. The clock's punch sound stamps it for everyone nearby; the carried paper turns into a receipt ("PAID "). - ✅ Deadlines in online time (like the jail sentence): unpaid after
m_iTicketFirstDeadline(1800 s) the amount is multiplied once bym_fTicketLateMultiplier(x2) and the paper updates; after a furtherm_iTicketSecondDeadline(1800 s) the holder getsm_iTicketLateWanted(+1) wanted and a named "Unpaid fine" poster on the wanted board. Still payable after; paying does not lift the warrant. Logs:[ELDebug:Ticket]. - ✅ Persisted (save version 12, payload
tickets). A restart's blank copy is reprinted on spawn; a respawn hands an unpaid ticket back. Up to 3 paid receipts per holder are kept for reprinting. Tests:police/fine-ticket-rules,police/fine-ticket,police/fine-ticket-serializer.
EL_DutyAction — Feature/Police/EL_DutyAction.c
- ✅ The police kit carries the station car key (2026-11-08, WORLD
police/car-key): the starter police kit addsPrefabs/Items/Roleplay/LF_PoliceCarKey.et, a vehicle key bound to the shared identifierPOLICE;Roadmap_Escort_Car's lock is bound to the same, so every officer's key opens every station car. The key is part of the duty inventory (clock-out stores it with the uniform). The car is a world entity, not an owned record: no garage, no impound, back at the station after a restart.EL_VehicleKeyComponentnow asks for its INIT event, so an editor-bound key actually takes its identifier (before, it stayed unbound). - ✅ Clock in / Clock out at the duty desk (M1-11, 2026-11-08; WORLD
police/duty-swap,police/death-on-duty,police/station; restart viawt e2e).Roadmap_Police_DeskisPrefabs/Police/LF_PoliceDutyDesk.et(desk + vanillaClockPunch_01). The offer reads the roster bit ofEL_AccountReplication; the server re-checksEL_WhitelistManagerandLF_DutyLockerswaps the whole carried inventory, clothes included: clock-in stores the civilian tree ascivilian_stashand restorespolice_duty(or the starter police kit, the POLICE entry ofEL_SpawnLogic.m_aFactionCharacterItems); clock-out does the reverse and pays duty pay. The faction is the duty role: POLICE only while clocked in. Feedback: the vanilla pick-up gesture on the acting client, the range control panel button click (SOUND_RANGECP_STARTBUTTON) broadcast from the desk, then a toast. Death on duty ends the shift: the body keeps the police gear, the civilian stash becomes the carried snapshot the respawn restores, and the next shift starts from the starter kit. Going on duty starts the rank clock; going off duty settles it.PoliceStation_DebugandPoliceDutyNPCno longer offer duty. The debug menu's police button still sets the role without a swap (dev only). - ✅ Duty pay at the desk (verified 2026-11-08: LOGIC test
jobs/duty-pay; the 2-client cash check is manual). Going off duty pays the shift's on-duty seconds timesm_fPoliceDutyPayPerSecond(Configs/Jobs/LF_Jobs.conf, flagship 0.5) as cash throughLF_DutyPay.PayAtClockOut, with the pick-up sound and a#EL-Duty_Paidtoast. Seconds settled by an armory draw mid-shift are kept for the clock-out. A second clock-out, a clock-out with no clock-in, and a non-police account pay nothing. Unpaid seconds are session state: a restart during a shift drops that shift's pay, it never pays twice. - ✅ Toggling keeps the account resident:
SaveAndReleaseAccountre-caches the account instead of evicting it, so the officer can reopen the police menu and re-toggle at once. - ✅ Dedicated client (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual): the toggle
runs on the server only; the offer and the name (
#LF-Duty_ClockIn/#LF-Duty_ClockOut) readEL_AccountReplication.
LF_WantedReports / LF_WantedBoard — Feature/Police/
- ✅ Crime reports (M1-11, LOGIC
police/wanted-board): each crime that raises wanted (till, vehicle and weapon robbery, breach, hospital till) callsLF_Dispatch.Report(M1-20), which callsLF_WantedReports.Report. It records the offence and a description of the suspect at that moment: clothing display names, the vehicle they sit in, and the nearest sign title withinm_fPlaceRadius(else a 100 m grid square). An on-duty officer withinm_fCatchInActRadius(Configs/Police/LF_Police.conf) catches it in the act and the report is identified at once. Showing your own ID card to an on-duty officer (EL_ShowIDAction) identifies every open report. Reports come down when wanted returns to 0, and persist as thewantedpayload (save version 10). - ✅ Wanted board
Roadmap_Police_WantedBoard(Prefabs/Police/LF_WantedBoard.et, a corkLF_Sign_Boardprinting WANTED): "Read the wanted posters" asks the server per read (RpcAsk_LF_WantedBoard, the server finds the board near the reader). On-duty police get offence, stars, clothing, vehicle, and place; everyone else gets the public poster without the place. A name appears only on identified reports. The card is anLF_UIScreenpaper sheet that closes 4 m from the board; reading plays the map pick-up sound and gesture. On-duty police also see the dispatch log under the posters (M1-20).
LF_Dispatch / LF_SiteAlarmComponent — Feature/Police/ (M1-20, #105)
- ✅ One crime entry point (LOGIC
dispatch/once):LF_Dispatch.Reportis what every crime calls (till, vehicle and weapon robbery, breach and bank vault, hospital cabinet, murder with no site alarm). It records the crime report, sounds the site alarm, radios on-duty officers, and appends a line to the dispatch log. A refused crime reports nothing; no crime refuses for lack of police (the per-crime minimum stays a framework attribute, 0 on the flagship).EL_Utils.AlertPoliceis gone. - ✅ Site alarm (WORLD
dispatch/site-alarm): a replicatedPrefabs/Police/LF_SiteAlarm.etspawns at the site, so only players the site streams to get it. It loops the vanilla siren (Structures_Siren.acp,SOUND_SIREN_LP) and flashes an orange light form_fAlarmSeconds(30). With nobody on duty when the crime starts it isLF_SiteAlarm_Loud.et: the vanilla loud siren (Structures_Siren_Loud.acp, 600 m), a brighter, faster flash,m_fLoudAlarmSeconds(60). A newer alarm at the same site replaces the old one. Durations, prefabs, and the radio sound are inConfigs/Police/LF_Police.conf; the sound, hear range, and light are attributes on the prefabs. - ✅ Radio dispatch: each on-duty officer's own client (Owner RPC on the
controller) hears a vanilla radio chirp from their body
(
Props_Radios_Signals.acp,SOUND_RADIO_SIGNAL_2), then a toast "All units: at . Suspect: ; on foot/driving ...". The line is built from the report's description snapshot; it has no name field. - ✅ Dispatch log on the wanted board: the newest
m_iDispatchLogLines(5) calls, "HH:MM at : N on duty, radioed" or "nobody on duty, loud alarm". Session-local (the posters are the persisted record). - Harness:
stateaddsdispatch=<n> lastdispatch=<offence@place> alarm=none|normal|loud;node agent-box/session.mjs dispatch-proofdrives the two-client run. - Manual: listening check (the siren loop and the radio chirp) and the light at night; payphones, pagers, and AI police are later.
LF_PoliceRanks — Feature/Police/LF_PoliceRanks.c
- ✅ Two ranks (verified 2026-10-11). Patrol officer draws
{1353C6EAD1DCFE43}Prefabs/Weapons/Handguns/M9/Handgun_M9.et, fines up to 1000 (LF_PoliceConfig.PatrolFineCap). Senior officer draws{3E413771E1834D2F}Prefabs/Weapons/Rifles/M16/Rifle_M16A2.etand fines up to the existing 5000 cap. The resolved kit is the intersection of that rank's list and the station catalog; a prefab that does not load is logged and skipped, and the rank stays usable. An on-duty senior promotes a lower officer. 3600 seconds on duty promotes patrol.RequestSelfPromotionis the self-promotion RPC body and always refuses. Rank does not change faction and does not write the police whitelist. Lower ranks still open the wanted list. DebugWorld placesRoadmap_Police_Armoryat241.2 0 244.2(police station forecourt) onStreet_Police.layer. - ✅ One armory draw per duty shift (2026-11-07): a draw
that hands out any kit line marks the account; a second draw logs
draw refused: <uid> already drew a kit this shiftand shows#EL-Rank_AlreadyDrawn.OnDutyChangedclears the mark. Test:security/police-abuse. - ✅ Rank, on-duty seconds, and unpaid duty seconds survive a restart (M1-10,
progressionpayload, save version 7). - ⚠️ The shift draw mark is session state, so a restart allows one more draw.
- ✅ Arrest and escort are config, open to every officer (2026-11-08, #165):
LF_PoliceConfig({916BDC092C01B7D1}Configs/Police/LF_Police.conf) sets the lowest rank that may arrest (m_iArrestMinRank) and escort (m_iEscortMinRank), and the patrol fine cap. Flagship: 0, 0, 1000, so a patrol officer arrests on a fresh server.LF_Jail.ArrestandLF_JailEscortre-check the rank on the server for every path (action, menu, escort to the cell); the offers read the replicated rank and show#EL-Rank_ArrestDenied/#LF-Rank_EscortDeniedwhen a server raises the minimum. A file that does not resolve logs once and uses the same defaults. Test:police/rank-permissions.
LF_Contraband — Feature/Crime/LF_Contraband.c (verified 2026-11-08, M1-14)
- ✅ One convoy. A member of the organization that owns the hideout performs
Start convoyatRoadmap_Convoy_Start, a pallet of wooden crates by the barn out of town (466 0 331), and is handed a crate of spirits:{968892A9535FE6A4}Prefabs/Items/Contraband/LF_ContrabandCrate.et, a hand-carried item onHandCarryItem_Base.etwith the vanillaBoxWooden_01.xobmesh (carry poseplayer_carry,SOUND_PICK_UP). A member already holding one keeps it; full hands logpickup refused: hands fulland toast#LF-Contraband_HandsFull.Deliver contrabandatRoadmap_Convoy_Drop, an empty crate cage behind the pub on the square (131.5 0 284, off the farm lane), takes the crate and pays 200 MoneyStack cash in hand once (SOUND_PICK_UP). A second delivery on that run pays nothing. A non-member is refused and the refusal is logged. The hideout machine also outputs the crate. Dressing onStreet_Contraband.layer: crates and a kerosene lamp on the pallet, a parked vanillaS1203_cargo_brownvan (Convoy_Barn_Van) at the barn; beer kegs, empty crates, bottles, and a lamp at the pub's back door. SignsSign_ConvoyPickup(barn front wall) andSign_ConvoyDrop(pub east wall) onSigns.layerexplain both ends. Both action contexts sit 0.6 m up, on the cage. Player strings:#EL-Contraband_Start,#EL-Contraband_Deliver,#EL-Contraband_NotMember,#EL-Contraband_Paid,#EL-Contraband_Failed,#LF-Contraband_Loaded,#LF-Contraband_HandsFull,#LF-Contraband_Crate_Name. - ✅ The vehicle the member is in at
Start convoy, or the first one they get into afterwards (SCR_CompartmentAccessComponent.GetOnCompartmentEntered), is watched. Getting out of it withinLF_Contraband.DROP_RADIUS_M(25 m) of the drop ends the drive and keeps the run for the delivery (convoy reached the drop); getting out anywhere else, or dying, fails the run and logs it; a failed run pays nothing. WORLDe2e/roadmap-contrabandcovers the crate (mesh, hand-carry, flagged) and both outcomes of getting out. - ⚠️ Not driven end to end by automation: whether a player can board with the crate in hand or must stow it in the vehicle's cargo is a manual check.
- ✅ Vehicle watch lifetime (2026-11-08): payment, failure, and
Resetunregister theOnCompartmentLefthandler; a restarted run drops the old watch and watches the vehicle the player is in now. A disconnect fails the running convoy. - ✅ The refusal reaches the player (2026-11-08, #157): a start refused for
membership shows a toast with the reason,
#LF-Contraband_JoinOrg(no organization: join or found one at the organization desk) or#LF-Contraband_NotHideoutOwner(your organization does not hold the hideout).LF_Contraband.StartRefusalKeypicks it. Before this the server loggedconvoy refused: not a memberand the player saw nothing. - ⚠️ The crate still offers "Start convoy" to everyone: a client cannot see organization membership, so the reason arrives after the press, not on the offer.
EL_ConfiscateAction — Feature/Police/EL_ConfiscateAction.c
- ✅ (2026-11-08, M1-12) Search in the player interaction menu: shown to on-duty police within the 2 m reach; deletes the weapons of the wanted player in front of them (the action owner), not the nearest wanted player. Weapons are collected before deletion. Earlier contract: on-duty police delete weapons from a wanted player. The offer reads replicated officer and target state; the deletion runs on the server only and re-checks the officer's account (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual).
- ⚠️ No compensation or wanted reduction.
Ownership
EL_OrganizationManager — Feature/Organizations/ (verified 2026-10-11; ledger and invites 2026-11-08)
- ✅ A character founds one organization. The id comes from
PersistenceIdUtils, never an entity id. The founder invites and kicks. A member can leave. The founder cannot leave or be kicked. A second membership is refused. - ✅ The shared fund is a balance keyed by organization id, the same shape as
EL_BankAccount. Deposit credits it. Withdraw requires the withdraw rank and takes the whole amount or nothing. Buying an upgrade (extra storage, processing slot, parked vehicle) debits the configured price in one step or leaves the fund and the flag untouched. The price and the new flag are logged. - ✅ Cash moves only as MoneyStack through
EL_OrganizationPlayer. A short removal is returned to the character. A short payout puts the fund back. The personal ATM balance is not read or written. The desk isRoadmap_Org_DeskonStreet_Org.layerat288.9 0 255.7(org registry porch), using{6B7B7CCC74AACA67}Prefabs/Props/Furniture/Desk_01_short.et. - ✅ (2026-11-08, M1-17) The desk opens the registry ledger
LF_OrgLedger, a 1989 ledger page on the UI kit (the oldOrganizationMenu.layoutis gone). No organization: name field and "Sign and pay $250"; the fee (LF_OrgRules.FOUNDING_FEE) leaves carried MoneyStack and comes back if the registration is refused. Names: 3 to 32 letters, digits, spaces,- ' & ., unique ignoring case and outer spaces (EL_OrganizationManager.IsNameTaken). A member's page: name, rank, fund with deposit (and withdraw for the rank that may), the processing upgrade, the alley yard lease (LF_OrgHideoutLease, free, founder takes it), the member list (offline members by their record name), and Leave (not for the founder). Found, deposit, withdraw, buy, lease, and leave are refused unless anEL_OrganizationDeskActionstands withinEL_OrganizationPlayer.DESK_RANGE(5 m) (#EL-Org_TooFar; testsecurity/service-range). The page closes at 4 m from the desk, on death, and on MenuBack. Sounds: page turn on open, pencil on Sign, map close on close. LOGICorg/registry,org/invite-role. - ✅ (2026-11-08, M1-17) Invite and remove are face to face, in the
org_inviteslot of the player interaction menu (LF_OrgInviteRole, registered inEL_GameModeRoleplay.OnGameStart). A leader facing someone with no organization sees "Invite to " (offer,Gesture_Point); the invitee facing that leader sees "Join " and joins with one press (raised hand, hand-over sound); a leader facing a member sees "Remove from "; nobody can remove the founder. An offer is session state (LF_OrgInvites, not saved) and lapses after 2 minutes. The offer side reads the organization replicated on each character (LF_OrgReplication: id, name, rights bits, held offer; RplProps written on spawn, on change, and every 10 s); the server decides from the registry. Two-client proof:node agent-box/session.mjs org [--shot]. - ✅
ExportAll/ApplyAllreplace the cache, so a second apply does not compound the fund. The game-mode serializer writes these records at version 2, after survival. A version 1 save has no organization payload and does not clear a live fund. Version 3 appends hideout ownership after these records. Version 4 appends owned vehicles after the hideout records. Version 5 appends serving jail sentences after the vehicles.
LF_Hideout — Feature/Hideouts/ (verified 2026-10-11)
- ✅ One building owned by one organization. Members unlock
Roadmap_Hideout_Doorat213 0 120. A non-member is refused and the refusal is logged. The door isEL_Door_Basewith the inheritedEL_LockComponent. The organization id is written onto that lock. The door does not callLF_Breachand does not add a secondActionsManagerComponent. - ✅ (2026-11-08, M1-17) An organization takes the yard (
LF_Hideout.HIDEOUT_ID, gate302.4 0 288since M1-25) with "Take the lease" on the registry ledger; taking it locks the gate. The gate prompt on a client compares the organization replicated on the character with the owner on the lock and shows#EL-Hideout_NotMemberto a non-member; the server still decides from the registry. - ✅
Roadmap_Hideout_Machineat216 0 120uses the existingEL_ProcessActiononItemProcessor_Base. It runs only when the player's organization owns the hideout and has the processing upgrade. Otherwise it refuses and logs. The recipe turns one cabbage into one RowanBerries stack, the configured contraband prefab. A chain machine with no hideout component still readsResourceChains.conf. - ✅ Hideout stash holds items (2026-11-08, M1-15, tests
hideout/stash,hideout/stash-save, ande2e/pass2-verify-loadacross a real two-boot restart).Roadmap_Hideout_Stash(now at312 0 296in the village) is an instance of the reusable prefabPrefabs/Props/Hideouts/LF_HideoutStash.et(the base-gameChest_01.xobchest, a crate was too small to notice): a real container (LF_HideoutStashStorageComponent, 200 kg) whose items are entities. Open storage asks the server (SCR_PlayerController.LF_RequestOpenStash); a member of the organization that owns the hideout (m_sHideoutIdonLF_HideoutStashStorageManagerComponent) gets the inventory, anyone else hears a refusal sound and gets the#LF-HideoutStash_NotMembertoast, and the refusal is logged (stash refused: not a member). The server also refuses a non-member moving an item out (HandleAccessValidation) or in (CanStoreItem). Membership is the only key: an open or breached door does not open the stash. - ✅ Stash contents survive a restart through
LF_PersistentStorageComponentkeyedhideout:Roadmap_Hideout(not an entity id), so the stash keeps its items wherever the layer places it. See the placed-storage row under Carried inventory. - ✅
ExportAll/ApplyAllput the organization id and the lock flag back, and a second apply does not change them. The game-mode serializer writes these records at version 3. A version 2 save has no hideout payload and does not clear a live hideout. Player strings:#EL-Hideout_NotMember,#EL-Hideout_NoUpgrade,#EL-Hideout_Process,#LF-HideoutStash_Name,#LF-HideoutStash_Desc,#LF-HideoutStash_Title,#LF-HideoutStash_NotMember.
Notifications
EL_NotificationManagerComponent — Feature/Notifications/
- ✅ Server→client notification broadcast with toast renderer;
SendToPlayer/SendToJob/SendToAll+ staticNotifyPlayer/NotifyJob/NotifyAll. Before 2026-11-07 the toast layout{4C38FD68D45325D2}UI/Layouts/Notifications/EL_NotificationToast.layoutdid not exist, so every toast (fines, arrests, rank denials, lot, lock, jail) failed silently. It now exists: a paper slip in the UI kit palette with a stamp-ink type strip (Background,ColorStrip,Icon,Title,Message,EL_NotificationToastUIon the root). Slips that overlap in time stack downwards. Timing reads the real clock (System.GetTickCount), not an assumed 16 ms frame.notifications/toast-layout(WORLD) instantiates the layout on the dedicated server and checks every named widget;notifications/toast-timing(LOGIC) checks show/fade/remove. On-screen look is visual-only: manual check pending (verified 2026-11-07) - ✅ Toasts are visible on a game client (2026-11-08, #157): until now no toast
was ever drawn. The slip's frame slot had a height of -48 (
OffsetBottom -92underOffsetTop 140), the fills werePanelWidgets (near-transparent) nested inside anImageWidget(which draws no children), and the text column's negative stretch offsets ran the message past the slip. Now: 420x120 slip, sibling image fills, wrapped message, fade driven fromTick(EL_NotificationToastUI.OpacityAt). Seen at 1280x720 on a dedicated client (contraband refusal). Newtools/validation/validate-layout-slots.mjsfails a point-anchored slot whoseOffset*andSize*disagree (it also fixedEL_SurvivalHUD.layout). - ✅ Targeted delivery fixed (verified 2026-08-30):
RpcDo_ShowNotificationisRplRcver.Broadcastwith a client-sideplayerIdfilter (-1= all), plus a direct call for the listen-server host so the authority sees its own toast.SendToJobduty semantics now read the account'sIsOnDuty(), not ajob != UNEMPLOYEDtautology. - ✅ Private targeted delivery (verified 2026-11-07: dedicated/* tests, server side; client receipt is manual):
SendToPlayersends an Owner RPC on the target'sSCR_PlayerController(EL_DeliverNotification), so only that client receives it; the listen host's own controller calls the handler directly. Broadcast is left forSendToAllonly.SendToPolice/NotifyPolicereach connected on-duty police by account faction.EL_Utils.NotifyEntity(shop, robbery, duty, confiscate toasts) route through it;EL_Utils.Notifystill shows a hint on the calling machine only. Since 2026-11-08 the courier job (sign-up, paid, abandoned), gather/process rewards, weapon and vehicle robbery, and the contraband payout toast throughNotifyEntity; on a dedicated server they had reached nobody. enforce-lint L8 fails anEL_Utils.Notifyafter anif (!Replication.IsServer()) return;guard. - ⚠️ Hard-coded Spanish prefixes, emoji in logs,
m_Colornever transmitted.
Inventory / Quantity
EL_QuantityComponent — Feature/Quantity/EL_QuantityComponent.c
- ✅ Virtual stack system:
AddQuantity/RemoveQuantityclamp to[0, max](0 = unlimited),SetQuantity(0)deletes the entity,CanCombine/Combine(conservation: source decrease == dest increase == transferred),CombineQuantities(2 and 3 yield 5),Split,QuantityRemainingAfterSplit(5 into 2 leaves 3),SortByQuantity(descending),GetRemainingCapacity(verified 2026-10-11). - ✅ In-inventory split mint closed (verified 2026-11-07): the
split stack is sized and marked keep-separate before
TryInsertItemInStorage, soHandleOnItemAddedno longer merges the spawned init quantity (100 for MoneyStack) into the source. Split 1 used to add 99. Pin testsecurity/inventory-split-no-mint. - ✅ Stack tier meshes hold their
Resourcewhile applied, so a tier swap no longer logsUsage of released BaseResourceObject(2026-11-08,quantity/operationsvisual path). - ✅ Transfer intent lifetime (2026-11-08, test
robustness/lifetime):OnDeleteremoves the entity's intent (no dead keys); an intent older thanINTENT_TTL_MS(5 s) belonged to a failed move and is ignored by the next add.EL_DefaultLoadoutItemQuantityComponentlogsEL_Debug.Errorinstead of crashing on a prefab withoutEL_QuantityComponent. - ⚠️
SplitwithsplitSize0 spawns an item then deletes it viaSetQuantity(0);ExtractQuantityComponentsinserts null components with no check;HandleOnItemAddedmay refresh UI on a just-deleted entity.
EL_InventoryUtils (Quantity) — Feature/Quantity/EL_InventoryUtils.c
- ✅ Quantity-aware
GetAmount/AddAmount/RemoveAmountreturning actual amounts; AddAmount fills stacks then spawns; RemoveAmount drains smallest stacks first (ascending sort). - ⚠️
AddAmountfills largest stacks first (descending) while Remove drains smallest — asymmetric by design, untested. Non-quantityRemoveAmountpermanently deletes entities.GetAmountreturns −1 on null target.
ScriptedInventoryStorageManagerComponent (Quantity) — Feature/Quantity/ScriptedInventoryStorageManagerComponent.c
- ✅ RPC endpoints for split/transfer/intent (
RPC_EL_RequestQuantitySplit,RPC_EL_QuantityTransfer,RPC_EL_SetTransferIntent), all server-side, range-gated. - ✅ Ownership gate added (verified 2026-08-30):
EL_CanManipulateOwnedrejects another living player's carried items (only your own storage or unowned containers/ground loot); the old range-only anti-cheat is closed. - ✅ Locked owners refused (verified 2026-11-07):
EL_CanManipulateOwnedalso refuses a storage whose owner has a lockedEL_VehicleLockComponentorEL_LockComponent(EL_IsStorageOwnerLocked). Pin testsecurity/locked-storage-refused.
Hand-Carry — Feature/Character/Inventory/HandCarry/
- ✅
EL_HandInventoryStorageComponentstate machine (NONE→READY→AWAIT_HOLSTER→ACTIVE→AWAIT_SWAP→NONE), hands-only storage, modded vanilla SCR inventory/gadget wiring. - ✅ Missing
CharacterControllerComponent, weapon manager, or inventory manager logsEL_Debug.Error("HandCarry", ...)and skips that step instead of a null dereference (2026-11-07). - ⚠️ Weapon-holster flow is client-only (server never sees the transition);
OnGadgetModeSetlacks theHasLocalControlguard its siblings have;SCR_CharacterControllerComponentdestructor touchesGetOwner()at teardown.
Vehicles & License Plates
EL_VehicleLockComponent / EL_VehicleKeyComponent — Feature/VehicleLock/
- ✅ Server-assigned vehicle and debug-key identifiers explicitly bump their replicated state during initialization, so clients receive the identifier after picking up a key.
- ✅ The lock identifier, key id, owner, and lock flag are stored by
EL_VehicleOwnership. A restart writes that identifier back withSetVehicleIdentifier, so the key still matches. Purchased vehicles replace the vanillaSCR_BaseLockComponentslot{54D6FBC0EA806FE5}, so get-in reads this lock. (verified 2026-10-11)
EL_LicensePlateManager + generators — Feature/LicensePlate/
- ✅
EL_LicensePlateGeneratorGeneric.GenerateLicensePlate()is"AA BB NNNN". - ✅ The alphabet includes
Z(LetterAt(25)) and the number range includes9999.Math.RandomIntis exclusive at the top, so the generator rolls 26 letters and a number end of 10000. (verified 2026-10-11) - ✅
EL_LicensePlateRegistrykeeps live plates. A second issue of a live plate is refused, so two vehicles do not share one by construction. (verified 2026-10-11) - ✅ The registry is session memory, so
EL_VehicleOwnership.ApplyAllre-reserves every saved plate on load; a purchase after a restart cannot be issued a plate already on a saved vehicle (2026-11-07, testrestart/vehicle-plates-reserved, verified 2026-11-07).
Vehicle lot and garage — Feature/Vehicles/
- ✅ Playtest: two cars from the start (2026-11-08, WORLD
vehicles/test-cars): withEL_SpawnLogic.m_bGrantTestCars(on inGameMode_Roleplay_Debug.et, so DebugWorld and the playtest; off in the flagship mode) a newly registered character owns the lot's first two cars as records with their keys in hand: the S105 stored in the garage and the UAZ at the impound lot (EL_VehicleOwnership.GrantTestCars). Nothing spawns; retrieve and collect work at once. Granted only to an owner with no vehicle yet. - ✅
Roadmap_Vehicle_Lotsells vanilla S105, UAZ469, S1203, UAZ452, and M923. Each spawn is a thin child of that vanilla prefab withEL_VehicleLockComponentand{9D94B834AF514B4A}Prefabs/Items/Roleplay/VehicleKey.et. (verified 2026-10-11) - ✅ A price at or below the lot cash ceiling (500) debits MoneyStack. A higher price debits the ATM balance and does not convert cash. A short payment is refused and charges nothing. Above the ceiling the refusal reads
#LF-Vehicle_BankShort(pay from the bank; deposit at an ATM), so a buyer holding enough cash knows why (vehicles/bank-refusal). - ✅ Purchase spawns the locked child, sets the buyer as owner, locks it, issues a unique plate, and gives one key. Extra keys are an owner action at the configured cash price.
- ✅ The vehicle record is keyed by the lock identifier.
Export/Applyrestore the owner and the key id.EL_PersistenceComponentSerializerversion 4 writes those records after the hideout records; a version 3 save has hideouts and no vehicle payload. Store keeps the prefab and the lock flag; retrieve spawns on the garage's own pad (EL_VehicleGarageComponent.m_sPadIdnames anEL_VehicleSpawnPointComponent), not at the last world coordinates. - ✅ Garage it to keep it (M1-16, #101, decision 2026-11-07; tests
vehicles/impound,restart/vehicle-street-impounded,garage/ledger-card): a vehicle survives a restart only when it is stored in a garage. After a load,EL_VehicleOwnership.ImpoundStreetVehiclesleaves every owned vehicle that was not stored unspawned: it is at the impound lot (IsImpounded= owned, not stored, no live entity; derived, so no save field and no version bump). No key is spawned at load.Roadmap_Impound_Desk(246 0 269, at the gate of the net-fenced yard behind the garage, x 236..256, z 271..283) releases the owner's latest impounded vehicle forLF_GarageConfig.m_iImpoundFee(Configs/Vehicles/LF_Garage.conf, 250) in carried cash onto a yard pad, once; short cash, a stranger, a full yard, or a second collect is refused and charges nothing. A collect or retrieve hands the owner a key bound to the car when they hold no live one (a restart unbinds carried keys). A purchase is followed by a toast that teaches the rule (#LF-Garage_BuyHint); the garage sign and a newSign_Impoundboard at the desk explain store, retrieve, the ledger, and the fee. - ✅
Garage_E_01({A548C1C5370D0734}Prefabs/Structures/Houses/Garage/Garage_E_01/Garage_E_01.et) is the garage shell. DebugWorld places the use pointRoadmap_Garageat238 0 256.6onStreet_Vehicles.layer, with store, retrieve, and read the ledger. A retrieved car is parked along the kerb in front of the doors (254.6, then 7 m west). Store takes the owner's car parked within 15 m of that garage; the lot is no longer a garage. The shell is its child 5.9 m north, turned so its doors face the street; the player stands in front of the doors. The lot counter isRoadmap_Vehicle_Lot: bought cars land on a pad 5 m west and 5 m south of it, then 8 m steps north across the concrete lot. The showroom body isRoadmap_Vehicle_S105. (verified 2026-10-11; moved with M1-25) - ✅
LF_Garageholds the park rules and the ledger. Park refuses a seated player or a missing garage, and a vehicle that is not within the same garage's radius as the player (EL_VehicleGarageComponent.SharesGarage,#LF-Garage_VehicleTooFar, testsecurity/garage-store-range), then stops the engine and teleports occupants out before store. Reading the garage or the impound desk sends the reader's rows from the server (SCR_PlayerController.LF_ShowGarageLedger) and opensLF_GarageLedger, a read-only paper card: model, plate, and where each vehicle is (in the garage, at the impound lot), plus the release fee when one is impounded. The old catalog screen (buy, paint, preview, stats), unreachable from the world and a duplicate of the lot's buy path, is deleted withUI/Layouts/Garage/LF_GarageVehicleRow.layout. (2026-11-08)
UI / Layouts / Localization
UI kit — Scripts/Game/Feature/UIKit/
- Post-release polish.
LF_UIThemeholds the 1989 type, padding, and paper/metal colors.LF_UIScreenassembles a frame, title, body line, WLib edit box (ink, no text outline or shadow; the box is not aTextWidget), list row, status row, quantity slider, and pause-menu button withWorkspaceWidget.CreateWidgets, wiresSCR_ButtonTextComponent, and activatesMenuContextevery frame (MenuTextEditContextwhile an edit box is focused,MenuWithEditorContextso a pad can move). Close removes the widgets and that call. It does not use MenuManager. - Input (verified 2026-11-07): focus lands on the first non-field control at open, so MenuSelect works without MenuDown first. MenuBack closes.
GetOnButton()hands the feature the part name of a pressed BUTTON, STATUS action, or list row action.GetOnClosed()fires once for any close. A LIST part (LF_UIList.layout,AddListRow/ClearList) scrolls to the focused row. Close defers the removal of its per-frame call by one tick, because it can run inside that call.ui-kit/initial-focusandui-kit/button-invoker(LOGIC) cover the pure parts; the on-screen focus ring is visual-only. LF_UIMenuInputgives the remaining MenuManager menus (ATM, shop, quest, police) and the workspace pickers (faction, spawn location, death, organization) the same input: MenuContext plus MenuWithEditorContext every frame, MenuUp/MenuDown focus ring, MenuSelect on the focused button, MenuBack closes (not on the mandatory faction, spawn and death pickers), and a 150 ms debounce so a click and the poll do not double-fire. The ATM has a Close button and labelled Deposit/Withdraw buttons, tells the player when the amount is blank or zero, and names deposit and withdraw refusals separately. The shop and quest Close buttons work. Shop and ATM moved onto the kit in M1-13 (2026-11-08); their MenuManager presets are gone. The shop, quest, organization desk, and police menu actions areHasLocalEffectOnlyScript, so the menu opens only on the acting client.- A line can be rewritten after open. A status row has one action that hides when the action is empty. A quantity splits one stack into keep and take. The sheet can close on death, on losing the character, or past a distance from an anchor. Nothing in the world opens the sample.
node agent-box/harness.mjs kitscreenshots the sheet, a rewritten balance, a hidden action, both split faces, and focus on the field. It reuses a play session that is already up. Faction, character creation, death, quest, police, and spawn menus were not migrated. - (2026-11-08, M1-13) Skins:
LF_UIScreen.Open(parts, skin)paints from anLF_UISkin(Paper()is the old look and the default,Terminal()the ATM,PriceList()the shop): frame colours, face, sheet width, blur on or off, and, for skins that ask, the vanilla button text colours (and its arrow glyph hidden). A KEYS part (LF_UIKeyRow.layout,LF_UIPart.CreateKeys) lays buttons side by side; MenuLeft/MenuRight walk the row and MenuUp/MenuDown keep the column in the next row.BindHotkey(action, name)reports an input action as a button press while no field is focused.SetButtonText,SetListHeight.LF_UISound.Playplays a UI sound event. Seen on a dedicated client (agent-box session, 640x360): the framePaperpanel draws nearly transparent and the blur darkens it, so paper sheets read dark; the price list turns the blur off to stay legible. The registration form shows the same on that client. - ✅ (2026-11-08) Opaque paper. The frame's
Metal,Paper,Ruleand the row plates werePanelWidgets, which draw the engine default style's panel image under their colour, so the sheet read dark and see-through. They are untexturedImageWidgets now (the vanilla solid-fill pattern,DialogBackground.layout), the paper is opaque warm off-white (238, 230, 212), and a full-screenBackgroundBlurbehind theSheetdims and softens the world. Paper skins paint button labels in ink with a red focus. A field gets an ink caption above it (<Part>Caption) because the vanilla placeholder draws light grey.LF_UIEditBox.layoutnow overrides the baseSCR_EditBoxComponentby its GUID, which removes the stray "Editbox" label and applies the paper-coloured well. The sheet is 900 tall so the price list's Leave and the ATM's EXIT fit. Seen on a session client (WORLDui-kit/paper-sheet, flowui-paper). - ✅ (2026-11-09) List rows span the sheet. A row sized to its text and sat centred in the scroll, so on the 900-wide price list the car lot cut "needs $2,500" to "needs $2," and "$1,000" to "$1,00".
AddListRowsets each row to the sheet width lessLIST_ROW_INSET(list and row padding, scrollbar). Seen on a laptop session client at 1920x1080: the lot and the general store.
Signs — Scripts/Game/Feature/Signs/
- ✅ Diegetic signs (M1-01).
LF_SignComponent.m_sSignIdnames a row inConfigs/Signs/LF_Signs.conf(id, title, what, how, needs, rules, next). A value starting with#is a localization key; any other value is shown as written.LF_SignReadActionis local-effect only and names itselfRead: <title>(#LF-Sign_Read). Perform opensLF_SignCardon the reading client: a UI kit sheet with the title, one line per filled section (What, How, Needs, Rules, Next; empty ones omitted), and Close. Close, MenuBack, death, losing the character, or walking 4 m from the sign closes it. No RPC. An unknown or empty id, or a row with no title, logsEL_Debug.Error("Signs", ...)once and the action hides. LOGICsigns/catalog,signs/missing-id,signs/localization; WORLDsigns/prefabs. - ✅ Prefabs
Prefabs/Props/Signs/LF_Sign_Board(cork board),LF_Sign_Pub(shop sign),LF_Sign_Notice(A4 notice),LF_Sign_Chalkboard(standing chalkboard; the vanillaCardboardSign_01lives in the Rally addon and does not resolve for this mod) inherit the vanilla props (no new meshes) and add the component, anLF_Signaction context on the face, and the Read action. A placed instance sets the id. The title is also printed on the prop on rendering clients:LF_SignComponentdrawsUI/Layouts/Signs/LF_SignFace.layoutinto anRTTextureWidgetbound (SetRenderTarget) to a mesh whose materialAssets/Signs/Data/LF_SignFace.ematsamples$rendertarget.LF_Sign_Boardspawns a localLF_SignFacesheet (vanilla A4 poster mesh) on its face;LF_Sign_Noticeretargets its own poster material. A dedicated server skips the face.LF_Sign_PubandLF_Sign_Chalkboardship with the face off until their sheet placement is checked in a rendering client. - ✅ The printed title fits the shown part of the render target at any window size. The target is the layout size times the screen DPI scale, but the widgets inside lay out one reference unit per pixel, so a 720p Workbench viewport printed "MINI / ARE".
LF_SignComponent.FaceTitleRectplaces the title box and font byDPIScale(1). Board and notice photographed reading MINING AREA, not mirrored, in a Workbench play session (2026-11-08). LOGICsigns/face-fit. - ✅ Game client face size (M1-09, 2026-11-08). On a 640x360 dedicated-server client the DPI-scaled title printed tiny in the sheet's top-left third. Without the DPI scaling, the full 432-unit box at font 96 cut WELCOME to WELCO at 640x360 and at 1920x1080 alike: only about 328 of the 512 units across reach the paper. The DPI compensation is now Workbench only (
#ifdef WORKBENCH); a game client usesLF_SignComponent.ClientTitleRect(box x 20..310, font 96 or smaller so the longest word fits). All 18 board titles photographed whole and unmirrored at 1920x1080 on a dedicated-server client. LOGICsigns/face-client. - ✅ Placed in DebugWorld (M1-09, 2026-11-08).
DebugWorld_Layers/Signs.layerholds one sign per town manifest site, within 8 m of it, facing the street:Sign_TownMapat the spawn square (town_map, "Welcome to Everon": the loop in order and where each place stands, left and right walking east), then post office, ATM, bank, general store, market, vehicle lot, garage, police station, jail (inside the yard), fuel station, hospital, organization office, hideout and convoy drop (in the yard), convoy pickup at the barn, and farm, allLF_Sign_Board. The back room is anLF_Sign_Chalkboardwith no printed title.Sign_PoliceWhitelistplacesEL_WhitelistSignbeside the whitelist station. Cards are#LF-Sign_<Site>_<Field>keys: what, how with the exact action names, needs, rules, next (where to go after this place, so a stranger can follow the loop from sign to sign; LOGICsigns/next), with the DebugWorld numbers (prices, pay, cooldowns). WORLDsigns/coverage: every placed sign resolves a titled catalog row, every catalog row is placed, and each title and What resolves. - ✅
EL_MiningSignreadsmining(Read: Mining area) andEL_WhitelistSignreadspolice_whitelistbeside its whitelist action.EL_TextLabelComponent(floating debug text) is deleted. - ✅ Signs belong to the building (M1 polish, 2026-11-08). Nothing official stands on the grass any more. Building signs are vanilla sign bodies with the title painted on their own face:
LF_Sign_Plate(enamel wall plate,SignWall_01_115x67: police by the porch, post office beside the post box, ATM, garage, jail, hospital beside a red cross cloth, organization office),LF_Sign_PlateSmall(SignWall_01_90x45, spare),LF_Sign_Fascia(shop fascia,SignCommercial_01, over the bank door and the general store front),LF_Sign_Posts(large board on two posts,SignLarge_01: town map, market, vehicle lot) andLF_Sign_PolePlate(plate on a pole: safe zones, impound, fuel, farm). Each carriesLF_SignComponentwithm_bFaceOnOwner(orm_bFaceOnChildfor the pole plate) andUI/Layouts/Signs/LF_SignPaintFace.layout: Rim and Paint images sized tom_vFaceArea(the part of the target the mesh UVs show, measured with a quadrant test card on a 1080p client) and coloured per placement (m_vRimColor,m_vPaintColor,m_vTextColor).PaintedTitleFitpicks the largest font at which the title, broken at spaces, fits the title box. A-frame pavement boards (LF_Sign_Pub) stand at the general store and the market; the contraband barn and the back room carry a plain chalkboard; the pub wears the vanilla HOSTINEC sign. The duty desk and the armory locker stand under the police porch (the empty weapon-rack frame on the lawn is gone). The corkLF_Sign_Boardkeeps notices (wanted board under the police porch, convoy): its A4 sheet is turned on its side (m_vFaceAngles 0 0 90, scale 4.7) and the title turns 90 degrees with it; a turned text is clipped to its unturned rectangle, so the widget is a square around the box. Panel widgets do not draw inside a render target; the faces use image widgets. Harnessstandputs the reader 1.4 m in front of a sign face (EL_AgentHarness.SIGN_STAND_FRONT), since beside a wall plate is inside the wall. LOGICsigns/board-face. - ✅ Props match what they give (M1 polish, 2026-11-08). The pub-terrace survival points were a hand pump that gave plums ("Tomar Ciruela") and a wooden box that gave apples ("Tomar Manzana"); they are now a plum tree (
SurvivalPoint_Water) and an apple tree (SurvivalPoint_Food) whose gather prompt is the localizedGather <fruit>.LogPile_Debughad no mesh; it is a vanilla wood pile (WoodPile_02_A). The ATM prompt reads "ATM" (#EL-ATM_Title), not "ATM Machine". - ⚠️ MenuBack closes the card through the UI kit. The printed face, the card render, the prompt, and the walk-away close are not yet seen in a rendering client (manual steps in the M1-01 PR).
Quests
LF_QuestTrackerComponent — Scripts/Game/Feature/Quests/
- ✅ Server-authoritative quest definitions, acceptance, objective progress, timed expiry, collect recounts across managed nested storage, rewards, and client quest-log replication.
- ✅
QuestGiver_StartandDebugWorldprovide a world-facing onboarding example with localized menu layouts and registered logic/world tests. - ✅ Turn-in loop closed (verified 2026-11-07): turn-in removes each
COLLECT_REPORTobjective's target amount throughEL_InventoryUtilsbefore the reward, all or nothing (a short removal puts the items back and refuses). Client open/accept/turn-in (RequestOpenGiver/RequestAccept/RequestTurnIn) require the registered giver that offers the quest withinLF_QuestManager.GIVER_RADIUS(5 m); an unknown giver id is refused. Server scripts may still callAcceptQuest/TurnInQuestdirectly. Pin testsecurity/quest-turnin-takes-items. - ✅ Guided arrival chain (M1-18, 2026-11-08):
QuestGiver_Clerk.et(Quest_Clerkon the square, 134 0 243.5) runs five steps:arrival_register(talk; hands over the ID card),arrival_board(SIGNAL sign.read:town_map),arrival_work(SIGNAL job.run_complete:parcel),arrival_bank(SIGNAL atm.deposit),arrival_done(talk; $250 and "the town is yours" with pointers to the rest of town). Steps 2-4 start and finish on their own (m_bAutoAccept,m_bAutoTurnIn), so the player never walks back between errands; each new step's errand shows as a 12 s hint. A fresh spawn without the card is pointed at the clerk. A giver's menu lists one chain step at a time and never re-lists a one-off quest already turned in (LF_QuestDefinition.IsListed), so a returning player is not offered the chain; a player who left mid-chain gets the next step back on spawn (SettleChainafterRestoreTurnedIn). Accepting at a giver counts that giver's talk objective at once. No waypoints. The clerk is a converse giver (m_bConverse): "Talk to the clerk" opens no menu; one talk turns in what is done, takes on the next quest, and the clerk's lines show as slips (RequestConverse); mid-errand he repeats the errand, after the chain his idle line (m_sIdleKey). A second NPC with a placed giver's id no longer takes the giver over. Harness:agent-box/flows/arrival-chain.txt;stateaddsidcard=andquests=;stand/usereach a replicated NPC (no layer name on a client). - ✅
SIGNALevent type with a string key (LF_QuestManager.ReportSignal). Hooks inLF_QuestHooks.c(modded classes; the job, ATM, and sign files are untouched):LF_JobDropoffActionreportsjob.run_complete:<job>for a paid run,EL_CharacterATMComponent.ReplyResultreportsatm.depositfor a successful deposit, andLF_SignReadActionasks the server (RpcAsk_SignRead) to reportsign.read:<id>, counted only with that sign within 6 m.QuestGiver_Start(test quests, "Test quest board") moved to the test annex (Quest_TestGiver, 104 0 70). Tests:quests/signal-event(LOGIC),quests/arrival-chain(WORLD).
Layouts (loaded by EL_Test_Data)
- ✅
EL_SplitQuantityDialog,EL_CharacterCreationMenu,EL_ATMMenu,EL_SurvivalHUD,ShopMenu,PoliceMenu,DeathScreenload (WORLD tier). TheSurvivalHUDmenu preset and itsEL_SurvivalHUDclass were never opened and are removed (2026-11-07);EL_BetaHuddraws the gauges. The layout stays only as this load fixture. The unused falling-fruit widget (EL_FallingFruit, which loaded a layout that never existed) is removed;EHarvestJobTypemoved toFeature/Jobs/EL_HarvestJobType.c. - ✅
EL_DebugMenuis a development menu opened with F10 (or its rebind), with server-routed controls for cash, wanted state, faction, jobs, survival, XP, and skill points. Actions affect only the requesting player's state (verified 2026-08-30).EL_DevGate(Core/EL_DevGate.c) checks every action on the server. It logs[ELDebug:DebugMenu] debug action refused: ...and refuses unless the game mode setsm_bDevToolsEnabledor the sender holdsEPlayerRole.ADMINISTRATOR. OnlyGameMode_Roleplay_Debug.et(DebugWorld) sets the flag. The productionGameMode_Roleplay.etleaves it off. The open key runs the same check on the client and logsopen refusedinstead of opening.EL_DevGate.IsAllowedhas the LOGIC testdebug-menu/gate(2026-11-07). - ✅ The playtest mission
Missions/LF_PlaytestTown.conf(LF_MissionHeader,Core/LF_MissionHeader.c) runs DebugWorld withm_bLF_DevToolsOff 1: the header overrides the DebugWorld game mode opt-in, so only administrators passEL_DevGatethere. A bare world (Workbench play,toolscli test) has no LF header and keeps the opt-in. LOGIC testhost/playtest-mission(2026-11-08). - ⚠️ Localization keys
#EL-*are frequently passed throughstring.Format(corrupting keys with%1) and/or handed toSCR_HintManagerComponent.ShowCustomHint, which does not localize#Key— the player sees raw keys. Spanish literals in notifications remain. The job, level and licence texts, the duty status words, the robbery, gather and process action texts, and the DebugWorld shop names moved to#LF-*keys on 2026-11-07, worded as employment rather than XP. Violates the AGENTS.md localization contract until the notification prefixes move too.
DebugWorld coverage
- ℹ️
Worlds/DebugWorld/DebugWorld.entis intentionally 0 bytes. All DebugWorld content lives inWorlds/DebugWorld/DebugWorld_Layers/*.layer;default.layercarries theGenericWorldEntity worldBSP and terrain entity. This layer loads before gameplay layers. Placing the world entity inTerrain.layerleft earlier props without physics bodies even when they hadRigidBodyandTRACEABLEconfigured. Do not repair the empty.ent.MainWorld.entdiffers because it is aSubScenewrapper overworlds/Eden/Eden.ent. - ℹ️ Direct collision fixtures need
Flags 0x403 0. This includes theTraceableflag. Without it, aMeshObject+RigidBodycan render while character movement and projectile traces pass through it. Prefab fixtures such asEL_TestRunnerinherit the flag; hand-authoredGenericEntityfixtures must set it explicitly. - ✅ DebugWorld is a one-street 1989 Everon village (M1-25, 2026-11-08;
docs/roadmap/m1-25-village.mdhas the map). The main street runs east-west at z = 250 on the flat 512 m terrain (world 0..512,TERRAIN_MAXintools/street/manifest.mjs; every heightmap sample is 0, so ground props sit at Y = 0). Sites stand in arrival order in front of vanilla buildings: the square (spawn, clerk, pub, church), post office, bank, general store and market, vehicle lot and garage, police station with a jail yard, hospital and fuel station, org registry, and the hideout down a side alley; the contraband barn is out of town on the east track. The farm, quarry, builders' yard and forest edge are on the outskirts.Village_Roads,Village_BuildingsandVillage_Dressinghold the roads (road generator splines saved asRoadEntity), vanilla buildings, walls, lamps and trees;Annex.layeris the net fence of the test annex at x 20..112, z 20..112, which holds the test runner, the PR fixtures, and the legacy debug stations (with their boundaries) on the ground where world tests spawn their characters. Sites keep their names; chain layers moved as blocks. The terrain material (Assets/World/GridSurface.emat, name kept so the terrain reference holds) is the vanilla lawn surface without clutter, so grass does not grow through roads and floors.tools\cli validaterunsvalidate-debugworld.ps1to guard the layer set. - ✅ Town tidy (M1-22, 2026-11-08): the street carries no debug artefacts. The
old police duty table (
PoliceStation,PoliceStation_Debug) and the whitelist table (WhitelistStation) moved into the annex (100 0 40 and 100 0 52, names kept). The daylight blue kerosene lamps left the market, quarry, forest, and pub tables. The pub's survival points are an apple tree (SurvivalPoint_Food) and a plum tree (SurvivalPoint_Water) instead of a fridge and a US water trailer.Village_Dressingadds 98 vanilla props: bins, planters and flower boxes, a poster column and notice boards, the bus stop sign, hydrants, benches, shop crates, a garbage container, yards with a clothesline, firewood and a dog house, a fenced cabbage plot, trees behind both rows, and four parked cars. The cars are the mod's lockable S105, S1203 and UAZ-469 with the vanillam_bIsLockedset, so no key opens them. - ✅ The WORLD-tier
e2e/narrative-loopexercises the live Apple gather → trader cash payout → shop purchase → ATM deposit path. The robbery step callsEL_RobAction.CommitRobbery(2026-11-07; it used to hand-write the payout); its proceeds are asserted as cash and are not added to the bank. - ✅
debugworld/entity-collisionchecks live physics bodies, geometry, and sphere sweeps through both apple trade crates and all three trader tables. Flags alone cannot prove collision. World initialization runs first indefault.layer(verified 2026-09-19). - Manual collision smoke after integrating the change: reopen DebugWorld, start a fresh play session, spawn as Civilian, and walk into each of the three apple trader tables from both sides. The character must stop at the tables, and the apple trade actions must remain usable. Repeat on a client joining a running dedicated server. These client checks are not automated.
Agent box
EL_AgentHarness — Feature/AgentHarness/
- ✅ Dev-only. A rendering play session polls
$profile:agent-box/command.txt.press FcallsSCR_InteractionHandlerComponent.ActionPerform. Wheel scroll callsActionScroll.clickinvokes the same button invoker orOnClicka mouse click does.usestill checksCanBeShownScriptandCanBePerformedScriptbeforePerformAction. Menu keys are an allowlist (MenuUp,MenuDown,MenuLeft,MenuRight,MenuSelect,MenuBack,EL_OpenDebugMenu); movement actions are refused. - ✅
standis the placement exception: the server moves the local character beside a named entity. The poll does not start on a dedicated server (RplMode.Dedicated). The server checks thestand,pose, anduseRPCs withEL_DevGate, the same gate as the debug menu. It refuses and logs ([ELDebug:AgentHarness] <request> refused: ...) unless the game mode setsm_bDevToolsEnabledor the sender is an administrator. On a production or listen-hosted server a client cannot teleport or reach past action range (2026-11-07). On a client it retries until a local controller exists, including while faction and spawn menus are up. - ✅ Screenshots go through
EL_ScreenshotCapture. UI shots leave the camera alone. A world shot teleports the character to the camera offset (default10 3 10, orox oy ozfor the row), faces the target, and screenshots the player view. That view faces -Z, so a picture stands on the +Z side of its target. The pose is applied again 700 ms later so gravity does not drop the body before the picture.SetCameradoes not move that view. (verified 2026-10-11) On a dedicated server the server also sends the pose to the owning client (RpcDo_EL_HarnessPose), which would otherwise overwrite it; session clients take world shots this way (verified 2026-11-08, M1-25). A filtered row's wide shot stands just north of that row when the street is further north. If the new PNG matches the previous PNG,report roadmaprecordscamera_stuckonce and stops.Roadmap_Jail_DoorandRoadmap_Jail_Cellare painted-light when look returns an action. Any other white mesh fails. Menu shots useshot ui; the report judgesclassifyand the widget list. Names afterroadmapwalk only that row.node agent-box/harness.mjs kitphotographs the UI kit sample in the current play session when play is already up, and enters play only when it is not.node agent-box/harness.mjs report roadmapwritesagent-box/reports/roadmap.htmlfrom a play session. The Node harness, MCP tools, layers, and named flows live inagent-box/. Seeagent-box/README.md. (verified 2026-10-11) - ✅ A world-shot pose turns off damage handling on the posed character for
8 s, so the fall from the camera height no longer kills it (a 4.5 m pose
ended on the death screen; verified 2026-11-08 with an 18 m row shot).
shot world <name> x y z ox oy oztakes a close camera offset. The poll restarts when a new game mode appears, so a second Play in the same Workbench no longer answersno_session. The command file is shared by every default-profile session on the machine; theagent-box-playlock ties it to one checkout's Workbench or game process. - (2026-11-08, #108) Screens:
clickaccepts every UI kit button by the name the screen gave it (LF_UIButtonRelay: ATM keys,IdClose, the sign card'sClose, the garage'sBuy).ui textreads a sheet askind:name=text(button labels, typed fields, printed lines,#keystranslated);wait-textpolls it;ui-shotisshot ui.standturns the character to face the entity.session.mjs up --mission playtestrunsLF_PlaytestTownwith-scrDefine LF_HARNESSon server and clients: harness RPCs passEL_DevGate.CheckHarnessRequestin that build only, the debug menu stays closed, a client without the define is refused by the script checksum, andhost startnever passes it.session.mjs restart(playtest) stops with the host stop file (SHUTDOWN save), loads, and rejoins the same profiles. - (2026-11-08, #108)
node agent-box/journey.mjsplays the M1 journey on a two-client session from the player's view (job, parcel run, ATM keypad, price list, car, give cash, till robbery and alert, org desk, garage, and on the playtest mission a restart), judgesstatebefore and after each step, and writesagent-box/reports/journey.htmlwith the client screenshots.tools/validation/agent-box-selftest.mjsruns the harness, session, and journey selftests invalidate. Every step ends with a picture from each client it touched. Clients default to 1280x720 (session.mjs up --res 1920x1080for evidence) on the hidden desktop.journey.mjs --filmrecords each step inside the game client (LF_FilmRecorder), one mp4 per step plus the joined run, linked from the report; it works on the hidden desktop.
EL_AgentHarness — Feature/AgentHarness/
- ✅ Dev-only. A rendering play session polls
$profile:agent-box/command.txt.press FcallsSCR_InteractionHandlerComponent.ActionPerform. Wheel scroll callsActionScroll.clickinvokes the same button invoker orOnClicka mouse click does.usestill checksCanBeShownScriptandCanBePerformedScriptbeforePerformAction. Menu keys are an allowlist (MenuUp,MenuDown,MenuLeft,MenuRight,MenuSelect,MenuBack,EL_OpenDebugMenu); movement actions are refused. - ✅
standis the placement exception: the server moves the local character beside a named entity. The poll does not start on a dedicated server (RplMode.Dedicated). The server checks thestand,pose, anduseRPCs withEL_DevGate, the same gate as the debug menu. It refuses and logs ([ELDebug:AgentHarness] <request> refused: ...) unless the game mode setsm_bDevToolsEnabledor the sender is an administrator. On a production or listen-hosted server a client cannot teleport or reach past action range (2026-11-07). On a client it retries until a local controller exists, including while faction and spawn menus are up. - ✅ Screenshots go through
EL_ScreenshotCapture. UI shots leave the camera alone. A world shot teleports the character to the camera offset (default10 3 10, orox oy ozfor the row), faces the target, and screenshots the player view. That view faces -Z, so a picture stands on the +Z side of its target. The pose is applied again 700 ms later so gravity does not drop the body before the picture.SetCameradoes not move that view. (verified 2026-10-11) A filtered row's wide shot stands just north of that row when the street is further north. If the new PNG matches the previous PNG,report roadmaprecordscamera_stuckonce and stops.Roadmap_Jail_DoorandRoadmap_Jail_Cellare painted-light when look returns an action. Any other white mesh fails. Menu shots useshot ui; the report judgesclassifyand the widget list. Names afterroadmapwalk only that row.node agent-box/harness.mjs kitphotographs the UI kit sample in the current play session when play is already up, and enters play only when it is not.node agent-box/harness.mjs report roadmapwritesagent-box/reports/roadmap.htmlfrom a play session. The Node harness, MCP tools, layers, and named flows live inagent-box/. Seeagent-box/README.md. (verified 2026-10-11) - ✅ Town manifest (
agent-box/town.mjs): every M1 site by entity name (or prefab, for the unnamed spawn points), the actions look must offer, its sign id, and its journey step. No positions: sites and signs resolve against the layers, so a site can move layers without a manifest edit.node agent-box/harness.mjs townandvalidate-town-manifest.mjscheck it offline;report town [site-id...]walks it in play mode and writesagent-box/reports/town.html. Sign problems (none within 8 m, blank Read card) fail the row:requireSignsis true since M1-09 placed the signs. The spawn site is the named civilian town spawnSpawn_Town; the clerk (QuestGiver_Start, prefab mode) is a site so the square meets the 30 m neighbour rule. Play-mode run not yet recorded.
LF_FilmCameraPath — Feature/Film/ (filming harness)
- ✅ Dev-only, like the agent harness.
LF_FilmCameraPathholds timed camera keys (position, look-at, FOV);Sample(t)eases between keys with smoothstep and clamps at both ends. Out-of-order keys are rejected and logged ([ELDebug:Film]). Covered byfilm/camera-path(LOGIC). - ⚠️
LF_FilmCameraRunner.Play(path)spawns a manual camera with input off, hides the HUD, moves the camera each frame, then restores the previous camera and HUD. It compiles but has not run in a rendering session and no agent-box verb calls it yet. - ✅
tools/film/film.mjsrecords a window or region to mp4 (ddagrab + NVENC; gdigrab when ddagrab delivers no frame in 5 s), makes a GIF preview, films an agent-box flow, and publishes a clip to thefilm-evidenceprerelease for a PR comment. Proven in game 2026-11-08: a game client on DebugWorld, flowfilm-debugworld, 20.7 s at 1280x720, 2.8 MB (PR #111). Screen capture records a frozen frame when the window is unfocused and cannot see the hiddenLF-Agentsdesktop (#91). - ✅
LF_FilmRecorderrecords inside the game client: a per-frame callqueue tick callsSystem.MakeScreenshotat a fixed rate (default 15 fps) into$logs:el-film/<name>/, with HUD and menus, and writesindex.txtwith each shot's real time and frame-time stats ([ELDebug:Film]). Harness verbfilm start <name> <fps> [max s]/film stop; a dedicated server refuses (error no_renderer).tools/film/engine.mjsencodes the BMPs at true speed (concat with per-frame durations, NVENC) and deletes them. Wired asfilm.mjs engine|encode,film.mjs flow <flow> --engine,journey.mjs --film, andsession.mjs film/proof --film. Covered byfilm/recorder(LOGIC). Proven 2026-11-08 on the hidden desktop, 1280x720, client capped at 30 fps: a 64.5 s journey clip (arrive, job, earn, fund, atm, shop) and a 27.9 sworld-atmclip, capture 14.7-15.1 fps, game 29.5-30 fps, frame after a shot 33.3-33.5 ms vs 33.2-34.5 ms otherwise, one 243 ms stall in 64 s (shop menu, not a shot frame).
Cross-cutting risk summary (re-audited 2026-08-30; see per-feature rows)
- Money — one canonical path, all payouts cash. The entity bank
EL_BankAccountComponentis deleted;EL_ATMManager/EL_BankAccountis the only bank and the ATM is the only cash↔account boundary. Duty pay, benefits, trader sells, shop sells, robberies and job rewards all payEL_MoneyUtilscash. Shop buy/sell correctness closed (partial refunds), ATM deposit/ withdraw moves real cash with rollback. Remaining: no shop stock / per-item max quantity. - Security — mostly closed. Fruit-catcher score is server-clamped
(pin test);
RpcAsk_DebugGrantLicenseremoved; quantity RPCs have an ownership gate; gather/process actions re-validate server-side and reject malformed configurations. Remaining: multiplayer proof for the RPC edges. - Persistence — improved. Cache-eviction churn fixed (account stays
resident). The serializer writes a
versionheader first (4) andvalidate-persistence-version.ps1checks it against the payload order. Remaining: jail, breach, and bank-vault state hasExport/Applybut is not in the save serializer, so it is lost on restart. - Compile risk — resolved.
EL_EFactionTypeis defined inEL_WhitelistManager.c. - Client/server breaks — mostly closed. Police arrest/fine bridge and notification targeting are fixed; the death screen (client) → respawn RPC (server) bridge is new and needs a manual multiplay pass. Remaining: hand-carry holster flow is client-only.
Toys
LF_JumpPad — Feature/Toys/LF_JumpPad.c
- ✅ A trigger sets the entering player's velocity upward once for that enter.
A second callback before they leave applies the zero vector. Leaving clears
the enter, so the next entry launches again. A disabled pad applies the zero
vector and does not call
SetVelocity. Ported from EveronLifeEL_JumpTrigger(MIT, Copyright (c) 2022 EveronLife). (verified 2026-10-11)
LF_SpikeStrip — Feature/Toys/LF_SpikeStrip.c
- ✅ A trigger. A wheeled vehicle that enters has each wheel inside the strip
box set to zero health. Ported from EveronLife
EL_SpikeTriggerComponent(MIT, Copyright (c) 2022 EveronLife). (verified 2026-10-11)