v0.57.4 - Editor Hotfix + Companion Bag Persistence
Bugs reported within hours of the v0.57.3 editor shipping, several of them fatal to the editor itself. This hotfix addresses four editor bugs (JSON options mismatch, long-menu scrolling, stat-edit persistence, and WeapPow/ArmPow no-op edits) plus the v0.57.3 companion-bag persistence fix that shipped slightly too late in the cycle to get its own release.
Editor save changes didn't persist
The editor wrote JSON with default JsonSerializerOptions (WriteIndented = true, nothing else), producing PascalCase property names like "Gold": 5000. But the game's FileSaveBackend both reads AND writes with PropertyNamingPolicy = JsonNamingPolicy.CamelCase + IncludeFields = true — so when the game next loaded the edited save, System.Text.Json looked up "gold", didn't find it (no case-insensitive fallback on the read side), defaulted the field to 0, and the next autosave then overwrote the editor's file with the zeroed state. Net effect: every single edit looked saved but was silently discarded on first load.
Fix: the editor's serializer options now EXACTLY mirror FileSaveBackend's. Read and write share a single static GameSaveJsonOptions constant in PlayerSaveEditor. Edits now round-trip through the game's save pipeline unchanged.
Adjacent fix caught while auditing the file-path logic: the editor used char.IsLetterOrDigit to sanitize player names into file names, but FileSaveBackend.GetSaveFileName uses Path.GetInvalidFileNameChars. Names containing hyphens or apostrophes (John-Smith, Fastfinge'1) sanitized to different filenames between the two — the editor's File.Exists check failed and reported "save file not found" even though the save was right there. The editor now trusts the on-disk filename already returned by FileSaveBackend.GetAllSaves() instead of reconstructing it.
Long menus scrolled items off the top
RunArrowMenu rendered every label on every redraw using a relative "cursor up N lines" ANSI sequence. On a 30-row terminal with 60 entries, the first render pushed the top 30 rows into scrollback, then the next redraw (triggered on any keystroke) tried to move up 63+ lines — which the terminal clamped to the visible top, so subsequent redraws piled on top of each other and the early entries stayed stuck in scrollback forever. Console.ReadKey also swallowed any mouse-scroll input, so there was no way to scroll the terminal back manually.
Fix: menus are now viewport-scrolled. The menu picks a viewportSize based on Console.WindowHeight (safe default 24 rows, minimum 5 rows) and renders only that many labels at a time. Arrow keys still move the selection — when the selection leaves the visible window, the window follows. PgUp/PgDn paginate by the viewport size. Home/End jump to first/last. Number shortcuts still work and scroll the viewport to their target. Overflow indicators (... N more above / ... N more below) and a showing X-Y/Z counter in the title bar make the window position visible.
Stats reverted on load
Caught while testing the JSON options fix: PlayerSaveEditor.EditStats was writing to PlayerData.Strength, PlayerData.MaxHP, etc. The save file correctly contained those edits. But on load, the game calls Character.RecalculateStats() which resets every derived stat from its Base* counterpart and then layers equipment bonuses on top:
Strength = BaseStrength;
Dexterity = BaseDexterity;
MaxHP = BaseMaxHP;
// ... then add equipment bonuses ...
So editing Strength without touching BaseStrength was a no-op: the edit survived the file round-trip but got wiped the instant the game applied RecalculateStats. Only Level and Gold (fields with no derived-vs-base split) appeared to persist.
Fix: EditStats now prompts for and writes the Base* fields for every core stat (STR, DEX, CON, INT, WIS, CHA, DEF, STA, AGI, MaxHP, MaxMana). The derived field is also mirrored to the edited value so the editor's summary views remain accurate pre-save. Current HP and Mana are still editable but the label now says they're clamped to final MaxHP/MaxMana at load time.
WeapPow and ArmPow are removed from editing — they're derived entirely from equipped items at runtime (RecalculateStats zeroes them then sums equipment contributions), so any direct edit was always a no-op. The edit menu now points users at the Inventory & Equipment menu instead.
Companion bag persistence
The v0.57.2 Party Inventory Viewer let players SEE items companions were carrying, but the underlying Companion class had no inventory field at all — the Character wrapper returned from GetCompanionsAsCharacters() started every call with a fresh empty list, and the save format never stored any companion-side inventory. Items transferred to Mira via the combat loot </> cycler + [T] prompt, or stashed via Home / Team Corner / dungeon party-bag viewer, worked on screen until the dungeon reload or logout discarded the wrapper.
Fix is structural. Companion now holds its own List<Item> Inventory. GetCompanionsAsCharacters() assigns that list by reference to the wrapper, so every take/give on the wrapper mutates the companion's actual bag. CompanionSaveData / CompanionSaveInfo gain a matching List<InventoryItemData> Inventory field, with the same Item ↔ InventoryItemData conversion the player inventory already uses (including LootEffects). Combat's [T] transfer to a companion now flushes AutoSave + (online) SaveAllSharedState immediately, mirroring the Party Inventory Viewer's take-back path, so a mid-dungeon disconnect can't lose the item between transfer and combat-end save. Existing saves load cleanly — missing Inventory field defaults to empty.
-
Achievement editing removed. Granting achievements via the editor was a Steam-leaderboard cheat vector — and even more concerning, the achievement-definitions editor (
AchievementEditor) let users edit the built-infirst_stepsachievement to pay out 100000 gold, which the game would then unlock normally viaTryUnlockat runtime, firing the matching Steam achievement. Both paths are now hard-disabled on every platform with a clear message. Modders who want custom achievements can still hand-editGameData/achievements.json— the editor just doesn't facilitate it. -
NPC editor warns about MUD-mode override. In online / MUD mode, NPC state lives in the server's
world_stateSQLite and is restored viaOnlineStateManager.LoadSharedNPCsat login, which OVERRIDESnpcs.json. NPC edits are only visible in single-player / Steam builds. Warning shown at editor entry. -
Equipment editor validates IDs on save. Built-in equipment IDs are reserved (< 200000) and silently dropped by the game if they appear in
equipment.json. Save now refuses to write if any item is in the reserved range or has a duplicate ID, with a list of offenders — prevents silent-ignore surprises for users who hand-edited the file between editor sessions. -
Quest edits warned as cosmetic.
QuestSystem.RestoreFromSaveDataaccepts whateverStatusthe save says without re-validating objectives, so marking a quest Completed via the editor doesn't re-fire rewards or objective hooks. Editor now explicitly says so; users wanting rewards should edit Gold directly. -
Old God state edits warned about prereqs. The game loads Old God states as-is with no prerequisite enforcement — setting Manwe Defeated while Maelketh is Dormant is a technically-valid save but can soft-lock dungeon events and endings because progression assumes floor order (Maelketh 25 → Veloura 40 → Thorgrim 55 → Noctura 70 → Aurelion 85 → Terravok 95 → Manwe 100). Warning shown at editor entry.
-
Relationship edits warned as dialogue-only. The per-NPC
Relationshipsscore dictionary is independent from the marriage / lover / ex tracking inRomanceTracker, which lives in a separate save field and is restored from a different code path atGameEngine.cs:4350. Setting a spouse's relationship score to -500 doesn't divorce them — it just makes their dialogue tone hostile. Editor now says so; for actual romance / marriage changes, use in-game church / temple actions. -
Verified as fine (no changes needed): companion stat edits persist correctly (
CompanionSystem.Deserialize'shasScaledSecondaryStatsheuristic preserves user edits), quest edits won't crash (just produce stale state),RomanceDataIS restored on load (the audit agent's grep missed the restore site — false alarm). -
Inventory add / equip / unequip / remove: raw-ID prompts replaced with pickers.
AddInventoryItemandEquipItemInSlotused to ask for an integer equipment ID ("Equipment ID to copy into inventory (built-ins start at 1000, custom at 200000)") and offered no way to discover valid IDs.EquipItemInSlotwould even let the user write a non-existent ID into the save; on load,Character.RecalculateStatssilently skips unknown IDs, so the slot looked "equipped" but contributed nothing. All four operations now use arrow-key pickers with a slot filter (where relevant) and a name substring filter. IDs and name discovery are no longer user responsibility. -
Spell-grant for non-caster classes warns.
GrantAllSpellswrote the full spell matrix to the save regardless of class. For Warrior / Barbarian / Ranger / Assassin / Jester / Paladin / Bard / Alchemist (all non-mana classes; Paladin / Bard / Alchemist moved to stamina in v0.49.5), the edit was a silent no-op —RecalculateStatszeroesMaxManaso nothing is castable. Editor now warns and requires explicit confirmation. -
Language: free-text replaced with picker over installed languages. A typo'd language code used to silently fall back to English at runtime.
EditSettingsnow shows an arrow-key picker overLoc.AvailableLanguagesso users only see languages actually shipped with the build. -
Single-player-only notes added to
EditWorldState(king / treasury / town pot),EditCharacterInfo(King / Immortal flags), andEditTeamAndGuild(Team / IsTeamLeader). In online / MUD mode these fields live in the server's SQLite (world_state.royal_court, teams table, guild tables) and are authoritative — local save edits never sync to the server. Editor is always local, but users who dual-play locally AND online could otherwise wonder why their local edits don't show up on the server. -
Companion state-machine desync.
CompanionSystem.GetActiveCompanions()iterates the top-levelactiveCompanionslist, NOT the per-companionIsActivebool. The editor's Recruit action only flipped the bool — so companions looked recruited in the list but never joined the active party on load. Also, reviving a companion markedIsDead = falsebut left them inFallenCompanions, andCompanionSystem.Deserializerebuilds thefallenCompanionsdict from that list — so revived companions could stay "permanently dead" in gameplay UI. Fix: recruit/dismiss/revive all syncActiveCompanionIdsANDFallenCompanionstogether. Recruit refuses to mark a 5th companion active when the 4-slot cap is already full. -
Dynamic equipment names showed
<unknown>.ResolveItemNamequeriedEquipmentDatabaseby ID, but the editor never registered dungeon-loot equipment (IDs in the 10000+ range stored inSaveGameData.DynamicEquipment) into that database at load time. So any equipped loot slot rendered as<unknown>even though the save had the full stats. Fix: editor now mirrorsGameEngine.LoadSaveByFileName's dynamic-equipment registration step right after deserializing the save. -
Skill-proficiency picker wrote dead keys.
EditSkillsAndTrainingoffered a vocabulary of"sword", "axe", "mace", "dagger", "spear", "bow", "staff", ...but the game stores proficiencies under IDs likebasic_attack,backstab,cleric_spell_3. Editing a "sword" proficiency wrote a dict entry nothing in the game ever reads. Fix: picker now enumerates real skill IDs —basic_attack+ every registeredClassAbilitySystemability + class-appropriate spell slots for casters. Value range corrected from 0-10 to 0-8 (the actualProficiencyLevelenum range: Untrained → Legendary). -
Inventory/equip pickers. Follow-up from pass 2 —
AddInventoryItem/RemoveInventoryItem/EquipItemInSlot/UnequipSlotall converted from raw-integer-ID prompts to arrow-key pickers with slot and name filters. Raw-ID entry was a UX dead end — nobody knows internal IDs — andEquipItemInSlotwould silently let you write a non-existent ID that produced a ghost "equipped" slot contributing nothing on load. -
Gold fields min-bounded.
Gold,BankGold,BankLoan,BankInterest,BankWage,RoyalLoanAmountall accepted negative values (they're stored aslong). The game's affordability / loan / interest checks don't handle negatives gracefully. All clamped tomin: 0. -
Spell grant warns on non-caster class.
GrantAllSpellswrote the spell matrix regardless of class, butRecalculateStatszeroesMaxManafor non-caster classes (Paladin / Bard / Alchemist moved to stamina in v0.49.5). The edit was a silent no-op for ~8 classes. Editor now warns and requires explicit confirmation. -
Language picker.
EditSettings.Languagewas free-text; typos silently fell back to English at runtime. Now a picker overLoc.AvailableLanguages. -
NG+ cycle edit warns + offers XP multiplier co-edit. Setting
CurrentCyclealone bumps monster scaling (viaGameConfig.GetNGPlus*Multiplier) but leavesCycleExpMultiplierat 1.0x — harder monsters, no XP bonus. Editor now explains the asymmetry and offers to setCycleExpMultiplierto the cycle-appropriate value (1.0x, 1.25x, 1.5x, ...) in a follow-up confirmation. -
Kids counter desync note.
p.Kidsis just a counter; the actual children (names, ages, classes) live inStorySystemsData.Children. Editing the counter without touching the list produces a "ghost 5 kids" state — say you have 5 children but Home Location has zero to interact with. Editor now says so. -
Monster ability free-text → picker.
MonsterEditor.EditFamilyTiers"Add ability to tier" prompted"Ability ID (e.g. poison_bite, power_strike)"— free text. Typos silently fell throughEnum.TryParse<MonsterAbilities.AbilityType>inCombatEngine.TryMonsterAbilityand produced no runtime effect (the game skipped the ability). Now a picker over the realMonsterAbilities.AbilityTypeenum (minusNone), so invalid values are impossible. -
Equipment custom-item field bounds.
EquipmentEditor.EditItemFieldslet users write negativeWeaponPower(weapon that subtracts damage),ArmorClass(armor that hurts defense),ShieldBonus,Value. Addedmin: 0on those;BlockChancenow clamped0-100(percentage). Stat-bonus fields (STR+/DEX+/ etc.) intentionally left unbounded — cursed items legitimately carry negative stat bonuses, and modders may want e.g. "iron cuirass: -2 STR, +8 Defence". -
Chivalry / Darkness capped at editor time. Game's
AlignmentSystem.ChangeAlignmentclamps both to 0-1000 on every modification, but a raw file edit let values above 1000 into the save. They loaded fine but clamped back to 1000 the next time the value changed in-game — so the editor value didn't match what the user saw in play. Editor now enforces the same 0-1000 cap at entry. -
SaveFileManager + PlayerSaveEditor:
SanitizeFileNamemirrors backend now. Both files had achar.IsLetterOrDigitsanitizer that didn't matchFileSaveBackend.GetSaveFileName'sPath.GetInvalidFileNameChars-based sanitizer. Names with hyphens / apostrophes / periods produced mismatched paths in Clone / Delete / Load flows — File.Exists checks failed even though the save existed. Both sanitizers now use the backend's algorithm exactly, AND the primary lookup path usesSaveInfo.FileName(the on-disk filename fromGetAllSaves) so the sanitizer is only a last-resort fallback. -
Verified fine:
NPCEditor.EditFields(all picker-backed, no stat field issues), editor-mode CLI gating (--editorshort-circuits before any server code runs), concurrency warning already in place at editor entry.
NPC / Companion inventory — fifth pass
Deep audit of the NPC and Companion inventory pipeline turned up two more structural bugs, same class as the report that kicked off v0.57.3:
- NPC teammate inventories never serialized.
Companion.Inventorywas fixed in v0.57.3, but regular-NPC teammates (spouses, recruited citizens, party members added via Team Corner) go through a completely different save path —NPCDatain the player save and in the MUDworld_stateSQLite. That class hadGold,Items(legacy Pascal equipped-item-IDs),MarketInventory(shop stock NPCs sell), andEquippedItems— but NOInventoryfield. So items transferred to an NPC teammate via combat[T]/ Home / Team Corner / dungeon Party Inventory viewer populatedNPC.Inventoryat runtime, but the very next save silently dropped them. On reload the NPC's bag was empty. Same shape as the companion bug, same fix: addedList<InventoryItemData> InventorytoNPCData, wired it throughSaveSystem+OnlineStateManager(write) andGameEngine.RestoreNPCs+WorldSimService.RestoreNPCsFromData(read). Also extendedCombatEngine.SyncNPCTeammateToActiveNPCsto copyInventorywhen the world sim reloadsActiveNPCsbetween dungeon-side and canonical NPC objects (MUD mode only concern, but safe in single-player too). - Companion permadeath lost everything in their bag.
CompanionSystem.KillCompanionandTriggerCompanionDeathByParadoxcarefully returnedEquippedItemsto the player but completely ignoredcompanion.Inventory. If a companion died with potions, herbs, or loot in their bag, those items evaporated — the companion stayed inFallenCompanionswith their inventory still intact but no code path ever reached it. Fix: new sharedReturnCompanionGearOnDeathhelper consolidates both paths. Returns equipped gear AND bag contents to the player, respecting the player's 50-item cap (overflow is explicitly reported as "dropped on the battlefield" rather than silently disappearing). Clears both collections on the companion after transfer.
Verified fine in this pass (no changes)
- Companion capacity —
Character.IsInventoryFullusesGameConfig.MaxInventoryItems = 50, and the companion wrapper IS a Character, so companions inherit the same 50-item cap. Combat[T]transfer checksplayer.IsInventoryFullAFTERplayerhas been reassigned to the selected companion wrapper, so the cap check is correct. - NG+ — v0.57.3's
ResetAllCompanionsalready clearsInventory; confirmed no regressions. - Shared-reference integrity — only three sites reassign
*.Inventory: player-load (fresh player), wrapper creation (intentional shared-ref setup), companion Deserialize (replaces list from save data, wrappers are short-lived and never stashed across save/load boundaries). - Combat loot auto-pickup displacement path — when a companion auto-equips a dropped item and their displaced gear moves out, existing code correctly pushes it to the player's inventory (or drops it past the cap). No change needed.
Files Changed
Scripts/Core/GameConfig.cs— Version 0.57.4.Scripts/Editor/PlayerSaveEditor.cs— NewGameSaveJsonOptionsstatic (PropertyNamingPolicy.CamelCase+IncludeFields = true+WriteIndented = true) used by both read and write paths. Editor now trustsSaveInfo.FileNamefromFileSaveBackend.GetAllSaves()instead of re-sanitizing the player name.EditStatsrewritten to writeBaseStrength/BaseDexterity/BaseConstitution/BaseIntelligence/BaseWisdom/BaseCharisma/BaseDefence/BaseAgility/BaseStamina/BaseMaxHP/BaseMaxMana(the fields that surviveCharacter.RecalculateStatson load); derived-stat fields are mirrored to the Base value so pre-save previews stay consistent.WeapPow/ArmPowremoved from editing with a warn note pointing at the Inventory & Equipment menu (they're zeroed and re-derived from equipment on load).Scripts/Editor/EditorIO.cs—RunArrowMenuviewport-scrolls now. UsesConsole.WindowHeight(fallback 24) to computeviewportSize = max(5, min(labels.Count, height - 7)), recenters the view on the selected row each redraw, emits... N more above/belowoverflow markers, and adds ashowing X-Y/Zcounter to the hint line.PgUp/PgDnpaginate by viewport size;Home/Endjump to extremes.Scripts/Systems/CompanionSystem.cs— NewList<Item> InventoryonCompanion.GetCompanionsAsCharacters()assigns the companion's list to the wrapper by reference so wrapper edits persist to the companion.ResetAllCompanions()now clearsInventory. New privateItemToData+DataToItemhelpers mirror the player'sItem ↔ InventoryItemDataround-trip (includingLootEffects).Serialize()andDeserialize()write/readInventory. NewInventoryfield onCompanionSaveData.Scripts/Systems/SaveDataStructures.cs— NewList<InventoryItemData> Inventoryfield onCompanionSaveInfo(the on-disk shape — missing on legacy saves defaults to empty).Scripts/Systems/SaveSystem.cs—CompanionSaveData ↔ CompanionSaveInfoconversions in both directions (SaveandLoad) now pass theInventoryfield through.Scripts/Systems/CombatEngine.cs— Combat loot[T]transfer to a companion now callsSyncNPCTeammateToActiveNPCs+AutoSave+ (online)SaveAllSharedStateimmediately, so a mid-dungeon disconnect doesn't lose the item between transfer and combat-end save. Mirrors the Party Inventory Viewer's take-back persistence pattern.Scripts/Editor/EditorMain.cs— Achievement definitions editor disabled (Steam-cheat vector viaTryUnlock-fired rewards). Placeholder message points modders at direct JSON edits.Scripts/Editor/PlayerSaveEditor.cs— Player-save achievement-granting menu disabled (same rationale). Warnings added at the top ofEditQuests(state edits not re-validated, treat as cosmetic),EditStoryAndGods(no prerequisite enforcement — floor order listed so users understand soft-lock risk), andEditRelationshipsAndFamily(Relationship dict is independent of marriage / lover / ex tracking in RomanceTracker).EditAchievementsmethod body removed (~70 lines).Scripts/Editor/NPCEditor.cs— Entry-time warning that NPC edits only apply to single-player / Steam builds; online / MUD mode restores from the server'sworld_stateSQLite at login, overridingnpcs.json.Scripts/Editor/EquipmentEditor.cs—SaveCustomItemsnow validates every item has ID ≥GameDataLoader.ModdedEquipmentIdStart(200000) and that all IDs are unique. Violations are listed and the save is refused. Stops silent-ignore on hand-edited JSON.Scripts/Editor/PlayerSaveEditor.cs— UX pass:AddInventoryItem,RemoveInventoryItem,EquipItemInSlot,UnequipSlotreplaced raw-integer-ID prompts with arrow-key pickers (overEquipmentDatabase.GetAll()for add/equip, over the actual inventory / equipped slots for remove/unequip). Equip picker filters by chosen slot + optional name substring so hundreds-of-items catalogs stay navigable.GrantAllSpellswarns when called against a non-caster class and requires explicit confirmation (non-mana classes can never cast, so the edit was a silent no-op).EditSettingslanguage field swapped from free-text to a picker overLoc.AvailableLanguages.EditWorldState,EditCharacterInfo,EditTeamAndGuildnow explicitly note that their fields are authoritative in single-player saves only — in online / MUD mode the shared world, kingship, team and guild state live in the server's SQLite and are overridden at login.