Skip to content

v3.5.0: fix NPC schedules, monster health, mine spawn-in-wall, dropped items#18

Open
0Aredjay0 wants to merge 5 commits into
mainfrom
fix/mid-day-save-npc-monster-mine-debris
Open

v3.5.0: fix NPC schedules, monster health, mine spawn-in-wall, dropped items#18
0Aredjay0 wants to merge 5 commits into
mainfrom
fix/mid-day-save-npc-monster-mine-debris

Conversation

@0Aredjay0

Copy link
Copy Markdown
Collaborator

Summary

Fixes the long-standing "characters disappearing / walking on water / schedules resetting" bug family (multiple open reports on the Nexus bug tracker going back to v3.2.7), plus closes a save-scum exploit and a few correctness gaps found while investigating. Root-caused against the actual decompiled game (1.6.15), not guessed — happy to share the specific game-code references for any of these on request.

  • NPC schedules after a mid-day reload. The game precomputes each NPC's daily route from their morning position and replays it with no collision checks. After a mid-day reload, NPCs are standing somewhere else, so the old code's TryLoadSchedule() + clock-advance ordering reset/raced their state, sending them beelining through walls and water or leaving them frozen. Villagers now get a fresh, collision-valid path to their current schedule destination computed from wherever they actually are, and NPCs already at their destination resume idle behavior immediately instead of standing blank.
  • Monster health was being deleted on every mid-day save. cleanMonsters() stripped every monster from the world before the real save ran; the game's own save code never touches monsters, so this was a mod-introduced regression, not a vanilla limitation. It let players save-scum out of any fight. Vanilla monsters now save/reload with their current health; monster types from other mods are still excluded (some aren't safe to serialize).
  • Fixed a real crash (ArgumentException) on a location's second mid-day save in a session — RestoreMonsters()'s tracking dictionary was only cleared on one code path.
  • Player could spawn inside solid rock in mines/Volcano Dungeon. Those floors regenerate a new random layout every entry and were never part of the save file, so a saved tile could be a wall in the new layout. Now validated against the actual location before warping, using the same tile-recovery the game itself uses for a stale mail/warp target.
  • Dropped items (loot, quest items, tools/weapons, forage, and resource piles like wood/stone/coal from chopping/mining) no longer vanish on reload. These were never part of the save file by vanilla design (normal saves only happen after the ground is already clear at night).
  • Saving no longer corrupts tomorrow's weather (was deleting rain forecasts / corrupting festival-day weather).
  • Multiplayer hang fixed — mid-day saving with farmhands connected now shows a clear message instead of freezing the game forever on the vanilla ready-check barrier.
  • Skipped the ~1.5s "has been saved" sparkle-text animation during a mid-day save (cosmetic; doesn't touch the actual save-driving logic).
  • A handful of smaller correctness fixes: null/farmhand guards on load, re-entrancy guard on save, off-map NPCs (Marlon) no longer saved/restored, CI's dotnet-version fixed to match the net6.0 target, ModBuildConfig bumped to 4.4.0.

Full changelog with more detail is in the README diff.

Test plan

All of the above verified in-game this session on SMAPI 4.3.2 / SDV 1.6.15:

  • Mid-day save + reload: NPCs resume schedules cleanly across two consecutive cycles (clean fresh paths, atDest NPCs resuming idle behavior, cross-map walkers routing correctly)
  • Damage (don't kill) a monster, mid-day save, reload → present with reduced health
  • Descend several mine levels forcing different regenerated layouts, save mid-level each time, reload → always lands on walkable ground
  • Drop a stacked/quality item indoors and outdoors, chop a tree and save before collecting the wood, reload → all reappear at the same tile with correct stack/quality, no duplication
  • Confirmed the animation skip doesn't affect the underlying save (file write completes normally, multiplayer ready-check path untouched)

🤖 Generated with Claude Code

0Aredjay0 and others added 5 commits July 2, 2026 23:54
…ropped items

- Fix villagers freezing or walking through walls/water after loading a
  mid-day save. Vanilla precomputes each NPC's daily route from their
  morning position and replays it without collision checks; after a
  mid-day reload NPCs weren't where those routes expected. Villagers
  now get a fresh route to their current schedule destination computed
  from the tile they actually stand on (using the game's own schedule
  pathfinding), with the clock held back via ignoreScheduleToday until
  positions are restored, instead of the previous TryLoadSchedule()
  call (which reset schedule state) racing the clock advance.
- Stop stripping vanilla monsters before a mid-day save. The mod's own
  cleanMonsters() was deleting every monster from the save file (the
  game's real save code never touches them); reloading meant any fight
  in progress reset to full health or vanished, letting players
  save-scum through combat. Only monster types from other mods'
  assemblies are still stripped-and-restored in-session, since those
  aren't covered by the base game's XmlInclude list and would crash
  the save otherwise.
- Fix RestoreMonsters() only clearing its tracking dictionary on one
  code path, which threw an ArgumentException on a location's second
  mid-day save of a session; add a null guard for the dictionary and
  the Forest lookup.
- Validate the player's saved tile against the actual location before
  warping on load, using the game's own recursiveFindOpenTileForCharacter
  (the same recovery it uses for a stale mail/warp target). Mine and
  Volcano Dungeon floors regenerate a new random layout every time
  they're entered and were never part of the save file to begin with,
  so a saved tile could land inside solid rock in the new layout.
- Preserve single-item ground debris (loot, quest items, tools/weapons,
  unpicked forage) across a mid-day save via a new DebrisData model and
  GetDebris/RestoreDebris pair in SaveManager, reconstructing dropped
  items through the game's own ItemRegistry factory on load. Loose
  resource chunks from a tool swing are out of scope (re-swingable, not
  an exploit).
- Stop overwriting Game1.weatherForTomorrow at save time; it deleted
  rain forecasts and corrupted weather on festival days.
- Block mid-day saves with remote players connected; the vanilla save
  menu's ready-check barrier can never complete mid-day, which hard-froze
  multiplayer sessions. Add a save-in-progress guard against re-triggering
  a mid-day save while one is already running.
- Guard OnSaveLoaded/LoadData against farmhands connecting (they'd warp
  to the host's saved position) and against missing/corrupted save data
  (would otherwise null-ref on load).
- Skip hidden off-map NPCs (Marlon at -42,-42) in saved position data.
- Bump ModBuildConfig 4.1.1 -> 4.4.0; fix CI workflow's dotnet-version
  (5.0.x -> 6.0.x) to match the net6.0 target.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tion

- GetDebris()/RestoreDebris() iterated Game1.locations directly, which
  does NOT include building interiors (farmhouse, sheds, coops, barns,
  cabins) — those live under each Building's own .indoors and are only
  reachable via Utility.ForEachLocation(includeInteriors: true). Any
  item dropped indoors was silently invisible to the debris save/restore,
  which is almost certainly why it looked like dropped items weren't
  being preserved at all. Also switched from one try/catch wrapping the
  entire scan (a single bad debris object silently zeroed out everything)
  to per-item/per-location handling, and added Trace-level [SA] logging
  of capture/restore counts for the next round of testing.
- Skip SaveGameMenu's ~1.5s "has been saved" sparkle-text hold during a
  mid-day save. The actual file write is done the moment
  Game1.game1.IsSaving flips back to false; at that point we now set the
  menu's public `quit` field early (the same externally-settable field
  vanilla itself uses to close the menu when a client times out), so its
  own update() closes it on the very next tick instead of waiting out
  the decorative animation. Doesn't touch the actual save-driving or
  multiplayer-sync logic at all, just skips the cosmetic wait.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The debris fix so far only covered debris backed by a live .item
(regular single-item drops) -- confirmed via decompile that resource
piles from a tool swing never get one; InitializeResource/InitializeItem
only set the raw itemId string for those, since Debris.collect() treats
each chunk as a separate 1-unit pickup regardless of any stack value.
This is exactly the case the user hit: chop a tree, save before walking
over the wood, reload, wood is gone.

- DebrisData gains an IsResourcePile flag.
- GetDebris no longer requires a live .item; it also captures resource
  piles via the raw itemId field, using the remaining chunk count (not
  a stack value, which collect() would ignore) as the quantity.
- RestoreDebris reconstructs a resource pile as that many separate
  single-item Debris objects (matching the original one-chunk-per-pickup
  behavior) instead of one item with a big stack, which would have
  silently discarded everything past the first pickup.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant