Skip to content

Bug fixes#50

Merged
ifBars merged 5 commits intoifBars:stablefrom
HazDS:haz/bug-fixes
Feb 24, 2026
Merged

Bug fixes#50
ifBars merged 5 commits intoifBars:stablefrom
HazDS:haz/bug-fixes

Conversation

@HazDS
Copy link
Collaborator

@HazDS HazDS commented Feb 24, 2026

Bug Fixes & Stability Improvements

Summary

  • Fixed static state not being reset on scene save/reload, preventing stale data across sessions
  • Improved DriveToCarPark action with better logging and a vehicle visibility fix - Patched DealerManagementApp dropdown to work correctly with modded dealers
  • Fixed dealers resolving to incorrect home locations
  • Restored storage configuration names being lost after loading a save

Changes

Reset static state on scene save/reload
Adds SceneStateCleaner and TimeManagerShim to properly clear static caches (dialogue listeners, injectors, NPC dealers, contacts, NPC patches) when scenes are saved or reloaded.

Improve DriveToCarPark logging and visibility fix
Reworks DriveToCarParkSpec with improved debug logging and fixes a bug where land vehicles could become invisible via LandVehiclePatches.

Patch DealerManagementApp dropdown
Adds DealerManagementAppPatches to fix the dealer management dropdown not populating correctly for modded NPC dealers.

Fix dealers home location
Introduces NPCPrefabIdentity to correctly resolve dealer home locations, fixing misrouted NPCs.

Restore storage configuration name from save
Adds StoragePatches to restore the storage configuration name that was being lost when loading a saved game.

Test plan

  • Load a save and verify storage names persist
  • Add a modded dealer and confirm the management app dropdown works
  • Verify NPC dealers navigate to correct home locations
  • Save/reload a scene and confirm no stale static state
  • Observe DriveToCarPark behavior and confirm vehicles remain visible

Release Notes

  • Reset dialogue system state: Added ResetState() methods to DialogueChoiceListener and DialogueInjector to clear static state across save/reload cycles, preventing stale listeners and pending injections
  • Clear dealer callbacks: Introduced ClearStaticDelegates() in NPCDealer to prevent accumulation of dead wrapper delegates on scene changes
  • Synchronize dealer home locations: Enhanced NPCDealer.Home setter to update Dealer.HomeEvent.Building field via reflection, ensuring correct NPC routing and preventing home location mismatches
  • Improved DriveToCarParkSpec robustness: Added comprehensive logging, vehicle spawn failure handling, and introduced VehiclesAtNoSpotLots tracking to prevent in-game hiding of vehicles assigned to parking lots without spots
  • Prevent vehicle visibility override: Added Harmony prefix patch to LandVehicle.SetVisible to keep tracked vehicles visible and preserve their state
  • Fixed dealer dropdown UI: Introduced DealerManagementAppPatches to refresh and repopulate the dealer dropdown on open/refresh with proper selected index restoration, supporting modded NPC dealers
  • Restore storage custom names: Added StoragePatches with JSON parsing to extract and reapply RenamableConfigurationData names that were lost on save load
  • Comprehensive scene cleanup: Added SceneStateCleaner to reset all static caches on scene save/reload including dialogue, contacts, NPC patches, time manager, and dealer state
  • Reset time manager delegates: Introduced ResetDelegates() method in TimeManagerShim to clear previously registered sleep and hour-pass callbacks
  • Reset contacts app state: Added ResetState() to ContactsAppPatches to reinitialize on scene transitions

Contribution Summary

Author Lines Added Lines Removed
HazDS 403 12

Add a small JSON parser (ParseConfigurationName) to extract the custom name from RenamableConfigurationData JSON (handles compact and pretty-printed forms). After loading storage contents, parse and apply the Configuration.Name directly to placeableStorage so renames are restored from save data instead of relying on deferred onLoadComplete. Parsing and assignment are wrapped in try/catch with a logged warning on failure.
Ensure Dealer.HomeEvent.Building is updated when setting NPC dealer Home so the schedule system knows where to send the dealer. Adds reflection-based logic (field or property access depending on MONOMELON) in both NPCDealer.cs and NPCPrefabIdentity.cs to retrieve the HomeEvent object and set its Building member after Home is resolved. Wraps the operations in try/catch; one location logs a warning on failure, the other silently ignores exceptions. This fixes timing issues where Dealer.Awake() runs before S1API sets Home.
Add Harmony patches to refresh the DealerManagementApp dropdown when the app is opened or refreshed. This calls the internal RefreshDropdown (with reflection fallbacks for different build targets) and restores the selected dealer index to avoid stale mugshot sprites and region text for custom dealers whose properties are set after initial population. Includes platform-specific handling (IL2CPP vs Mono/BepInEx) and logs failures.
Add detailed logging and error handling to DriveToCarParkSpec: log unresolved parking lots, warn about lots with no ParkingSpot children, log vehicle creation/spawn failures and exceptions, and catch unexpected errors. Introduce VehiclesAtNoSpotLots HashSet to track vehicles assigned to lots with no spots and remove entries when vehicles are removed. Add a Harmony prefix patch on LandVehicle.SetVisible to prevent the game from hiding tracked vehicles (keeps them visible when the game would call SetVisible(false)). Also include a small build-define fix and include the schedule namespace in the land-vehicle patches.
Add reset/cleanup routines to prevent stale static state and leaked delegates across save loads and scene changes. SceneStateCleaner now invokes these resets. Changes include:

- DialogueChoiceListener: added ResetState to clear expected choice and callback.
- DialogueInjector: added ResetState to clear pending injections and hooked flag.
- NPCDealer: added ClearStaticDelegates to clear Dealer.onDealerRecruited static field.
- TimeManagerShim: added ResetDelegates to clear accumulated delegates and IL2CPP lists.
- ContactsAppPatches: added ResetState to clear cached initialization flags.
- NPCPatches: added ResetState to clear saved addiction map and loading dealers list.

These updates prevent dead wrapper delegates, duplicated hooks, stale caches, and other cross-load leaks so systems reinitialize correctly on next load.
@HazDS HazDS requested a review from ifBars February 24, 2026 21:26
@coderabbitai
Copy link

coderabbitai bot commented Feb 24, 2026

📝 Walkthrough

Walkthrough

The PR adds comprehensive state-reset mechanisms across multiple subsystems (dialogue, contacts, NPC patches, time manager, dealer callbacks) invoked on scene unload to prevent stale references. It enhances error logging and vehicle tracking in DriveToCarParkSpec, introduces dealer management UI synchronization via Harmony patching, adds vehicle visibility guards for no-spot parking lots, and implements custom configuration name persistence in storage.

Changes

Cohort / File(s) Summary
State Reset Infrastructure
S1API/Dialogues/DialogueChoiceListener.cs, S1API/Dialogues/DialogueInjector.cs, S1API/Internal/Patches/ContactsAppPatches.cs, S1API/Internal/Patches/NPCPatches.cs, S1API/Internal/Lifecycle/TimeManagerShim.cs
Added ResetState() or equivalent methods to clear static fields (callbacks, pending injections, caches, collections, delegates) enabling correct subsystem behavior across scene transitions.
Scene Lifecycle Management
S1API/Internal/Lifecycle/SceneStateCleaner.cs
Integrated calls to all state-reset methods during scene unload to ensure comprehensive cleanup of dialogue, contacts, NPC patches, dealer callbacks, and time manager state.
Dealer State & Synchronization
S1API/Entities/NPCDealer.cs, S1API/Internal/Entities/NPCPrefabIdentity.cs
Added static delegate clearing on NPCDealer and reflection-based HomeEvent.Building synchronization in NPCPrefabIdentity to maintain dealer home consistency across saves.
Enhanced Error Handling & Logging
S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs
Introduced Logger instance, comprehensive validation, and warning/error logging at failure points; added vehicle tracking collection VehiclesAtNoSpotLots for cross-component coordination.
UI Management
S1API/Internal/Patches/DealerManagementAppPatches.cs
New Harmony patch file with conditional compilation support to refresh and synchronize dealer dropdown on UI open/refresh via reflection-based method invocation and field access.
Vehicle Visibility Management
S1API/Internal/Patches/LandVehiclePatches.cs
Added SetVisible_Prefix patch to prevent hiding vehicles assigned to no-spot parking lots; extended cleanup to remove vehicles from tracking collection on destroy.
Storage Configuration
S1API/Internal/Patches/StoragePatches.cs
Added ParseConfigurationName() helper to extract custom configuration names from JSON; integrated configuration name loading during storage hydration with error handling.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested labels

bug

Suggested reviewers

  • ifBars

Poem

🐰 Scenes do unload, states must reset,
Dealers and dialogues, we're cleaning our nest,
Vehicles parked with care, configurations saved true,
No stale references left—the scene born anew!

🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 65.00% which is insufficient. The required threshold is 70.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Title check ❓ Inconclusive The title 'Bug fixes' is overly vague and generic; it does not convey the specific nature or scope of the changes, which involve multiple subsystems (dialogue, dealers, vehicles, storage, scene cleanup). Consider a more descriptive title that captures the main changes, such as 'Fix scene state leaks and dealer/storage issues' or 'Refactor state management and fix multiple subsystem bugs'.
✅ Passed checks (1 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai bot added the bug Something isn't working label Feb 24, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
S1API/Internal/Patches/DealerManagementAppPatches.cs (1)

74-88: dealers cast may silently fail on IL2CPP at runtime

dealersObj as System.Collections.Generic.List<S1Economy.Dealer> (line 76) will yield null if the IL2CPP runtime exposes dealers as an Il2CppSystem.Collections.Generic.List<T> (or another IL2CPP wrapper collection). This would silently skip the index restoration step with no diagnostic. If the two branches are merged (see above), add a null-path log here so failures are observable.

💡 Suggested guard
             var dealers = dealersObj as System.Collections.Generic.List<S1Economy.Dealer>;
             if (dealers == null)
+            {
+                Logger.Warning($"Could not cast 'dealers' to List<Dealer>; actual type: {dealersObj?.GetType()}");
                 return;
+            }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Internal/Patches/DealerManagementAppPatches.cs` around lines 74 - 88,
The cast "var dealers = dealersObj as
System.Collections.Generic.List<S1Economy.Dealer>"/null-path can silently fail
on IL2CPP; update the block around
ReflectionUtils.TryGetFieldOrProperty(instance, "dealers") to detect when
dealersObj is non-null but the cast yields null, log a warning/error (including
dealersObj.GetType().FullName and context like selectedDealer) so failures are
observable, and keep the existing early-return behavior; also ensure similar
logging is added for the dropdown path (dropdownObj / dropdown and
dropdown.SetValueWithoutNotify) so both failure modes are visible at runtime.
S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs (2)

247-273: Remove the redundant wrapper alias — v is already in scope.

var wrapper = v; on line 249 is a no-op alias. Use v directly throughout the spawn block.

♻️ Proposed cleanup
-                    var wrapper = v;
-                    if (wrapper != null && wrapper.S1LandVehicle != null)
+                    if (v != null && v.S1LandVehicle != null)
                     {
                         try
                         {
                             ...
-                            wrapper.Spawn(VehicleSpawnPosition, spawnRot);
+                            v.Spawn(VehicleSpawnPosition, spawnRot);
                         }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs` around lines 247 -
273, Remove the redundant local alias "wrapper" and use the existing variable
"v" directly: delete the line that declares "var wrapper = v;" and replace
subsequent references to "wrapper" in the spawn block with "v" (including the
null-check against v.S1LandVehicle, the Spawn call using VehicleSpawnPosition
and VehicleSpawnRotation, and any logging that references VehicleCode). Keep the
try/catch and network manager logic (nm and nm.IsServer) unchanged; ensure you
still check v != null && v.S1LandVehicle != null before calling v.Spawn(...).

211-215: Extract the no-spots guard into a local variable to avoid duplication.

The condition gameLot != null && (gameLot.ParkingSpots == null || gameLot.ParkingSpots.Count == 0) is evaluated identically at line 211 and again at line 284. Hoisting it once eliminates the duplication and ensures consistency.

♻️ Proposed refactor
+               bool lotHasNoSpots = gameLot != null &&
+                   (gameLot.ParkingSpots == null || gameLot.ParkingSpots.Count == 0);
+
                // Warn if the lot has no parking spots
-               if (gameLot != null && (gameLot.ParkingSpots == null || gameLot.ParkingSpots.Count == 0))
+               if (lotHasNoSpots)
                {
                    Logger.Warning(...);
                }
                ...
                // Track vehicles created for lots with no spots
-               if (gameLot != null && (gameLot.ParkingSpots == null || gameLot.ParkingSpots.Count == 0))
+               if (lotHasNoSpots)
                {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs` around lines 211 -
215, Extract the repeated guard into a single local boolean to avoid duplicate
evaluation: introduce a local like hasNoSpots (e.g. bool hasNoSpots = gameLot !=
null && (gameLot.ParkingSpots == null || gameLot.ParkingSpots.Count == 0))
inside the method in DriveToCarParkSpec where gameLot is available, then replace
both occurrences of the original condition with hasNoSpots and keep the existing
Logger.Warning call and any other branches intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs`:
- Around line 202-205: The warning logged when ResolveGameLot() returns null
omits the wrapper's path and thus prints empty Name/GUID, making diagnostics
ambiguous; update the Logger.Warning call in DriveToCarParkSpec (where lotObj is
null after calling ResolveGameLot()) to include whether ParkingLot wrapper was
used and its object path (e.g., reference ParkingLot and its Path or wrapper
identifier) along with ParkingLotName and ParkingLotGUID so the message clearly
shows the wrapper-object path and that resolution was deferred/failed.
- Around line 314-319: DriveToCarParkSpec.VehiclesAtNoSpotLots can retain stale
references across scene reloads if OnDestroy removal fails; add
VehiclesAtNoSpotLots.Clear() inside SceneStateCleaner.ResetForSceneChange() in
the afterUnload branch (alongside the other static clears) to fully reset the
cache, and also update the XML summary on DriveToCarParkSpec to reference the
actual patch method SetVisible_Prefix instead of "LandVehicle.Park".

In `@S1API/Internal/Patches/DealerManagementAppPatches.cs`:
- Around line 35-52: The branch for IL2CPPMELON attempts a direct call to
instance.RefreshDropdown() which is a compile-time error and cannot be caught;
replace that branch with the same reflection-only approach used for the other
targets: use typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
BindingFlags.NonPublic | BindingFlags.Instance) to find the method, null-check
the returned MethodInfo, and call method.Invoke(instance, null) instead of
calling instance.RefreshDropdown() so the code compiles for IL2CPP builds.

In `@S1API/Internal/Patches/StoragePatches.cs`:
- Around line 52-85: ParseConfigurationName currently uses IndexOf('"', ...)
which fails on escaped quotes and non-string values; instead, update
ParseConfigurationName to do a small stateful scan starting at the colon after
the "Value" key: skip whitespace, check the first non-space char—if it's a
quote, iterate character-by-character collecting characters until you find a
non-escaped closing quote (treat backslash as an escape for the next char); if
it's 'n','t','f' (null/true/false) or a digit/minus, treat as non-string and
return null; on any malformed sequence return null; keep try/catch behavior and
use these checks inside ParseConfigurationName to reliably extract the string
value while handling escaped quotes and avoiding false matches.

---

Nitpick comments:
In `@S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs`:
- Around line 247-273: Remove the redundant local alias "wrapper" and use the
existing variable "v" directly: delete the line that declares "var wrapper = v;"
and replace subsequent references to "wrapper" in the spawn block with "v"
(including the null-check against v.S1LandVehicle, the Spawn call using
VehicleSpawnPosition and VehicleSpawnRotation, and any logging that references
VehicleCode). Keep the try/catch and network manager logic (nm and nm.IsServer)
unchanged; ensure you still check v != null && v.S1LandVehicle != null before
calling v.Spawn(...).
- Around line 211-215: Extract the repeated guard into a single local boolean to
avoid duplicate evaluation: introduce a local like hasNoSpots (e.g. bool
hasNoSpots = gameLot != null && (gameLot.ParkingSpots == null ||
gameLot.ParkingSpots.Count == 0)) inside the method in DriveToCarParkSpec where
gameLot is available, then replace both occurrences of the original condition
with hasNoSpots and keep the existing Logger.Warning call and any other branches
intact.

In `@S1API/Internal/Patches/DealerManagementAppPatches.cs`:
- Around line 74-88: The cast "var dealers = dealersObj as
System.Collections.Generic.List<S1Economy.Dealer>"/null-path can silently fail
on IL2CPP; update the block around
ReflectionUtils.TryGetFieldOrProperty(instance, "dealers") to detect when
dealersObj is non-null but the cast yields null, log a warning/error (including
dealersObj.GetType().FullName and context like selectedDealer) so failures are
observable, and keep the existing early-return behavior; also ensure similar
logging is added for the dropdown path (dropdownObj / dropdown and
dropdown.SetValueWithoutNotify) so both failure modes are visible at runtime.

ℹ️ Review info

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6a90dfd and 4cddc8d.

📒 Files selected for processing (12)
  • S1API/Dialogues/DialogueChoiceListener.cs
  • S1API/Dialogues/DialogueInjector.cs
  • S1API/Entities/NPCDealer.cs
  • S1API/Entities/Schedule/ActionSpecs/DriveToCarParkSpec.cs
  • S1API/Internal/Entities/NPCPrefabIdentity.cs
  • S1API/Internal/Lifecycle/SceneStateCleaner.cs
  • S1API/Internal/Lifecycle/TimeManagerShim.cs
  • S1API/Internal/Patches/ContactsAppPatches.cs
  • S1API/Internal/Patches/DealerManagementAppPatches.cs
  • S1API/Internal/Patches/LandVehiclePatches.cs
  • S1API/Internal/Patches/NPCPatches.cs
  • S1API/Internal/Patches/StoragePatches.cs

Comment on lines +35 to +52
#if (IL2CPPMELON)
try
{
instance.RefreshDropdown();
}
catch
{
var method = typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
method?.Invoke(instance, null);
}
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
var method = typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
BindingFlags.NonPublic | BindingFlags.Instance);
if (method == null)
return;
method.Invoke(instance, null);
#endif
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

instance.RefreshDropdown() is a compile-time error — try/catch cannot rescue it

This is the confirmed root cause of the pipeline failure. The try { instance.RefreshDropdown(); } catch { ... reflection fallback ... } pattern is fatally flawed: a catch block only handles runtime exceptions; it has no effect on a compile-time symbol-not-found error. The reflection fallback inside the catch is therefore permanently dead code.

The IL2CPPMELON branch should be collapsed into the same reflection-only call already used by the other targets.

🔧 Proposed fix — unify both branches to reflection-only
-#if (IL2CPPMELON)
-            try
-            {
-                instance.RefreshDropdown();
-            }
-            catch
-            {
-                var method = typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
-                    BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance);
-                method?.Invoke(instance, null);
-            }
-#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
+#if (IL2CPPMELON || MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
             var method = typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
                 BindingFlags.NonPublic | BindingFlags.Instance);
             if (method == null)
+            {
+                Logger.Warning("RefreshDropdown method not found on S1DealerManagementApp");
                 return;
+            }
             method.Invoke(instance, null);
 `#endif`
🧰 Tools
🪛 GitHub Actions: IL2CPP Build Check

[error] 38-38: Build failed: DealerManagementApp does not contain a definition for 'RefreshDropdown' and no accessible extension method 'RefreshDropdown' accepting a first argument of type 'DealerManagementApp' could be found (are you missing a using directive or an assembly reference?). The failing command was: 'dotnet build S1API/S1API.csproj --no-restore -c Il2CppMelon -v normal'.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Internal/Patches/DealerManagementAppPatches.cs` around lines 35 - 52,
The branch for IL2CPPMELON attempts a direct call to instance.RefreshDropdown()
which is a compile-time error and cannot be caught; replace that branch with the
same reflection-only approach used for the other targets: use
typeof(S1DealerManagementApp).GetMethod("RefreshDropdown",
BindingFlags.NonPublic | BindingFlags.Instance) to find the method, null-check
the returned MethodInfo, and call method.Invoke(instance, null) instead of
calling instance.RefreshDropdown() so the code compiles for IL2CPP builds.

@ifBars ifBars merged commit b28999c into ifBars:stable Feb 24, 2026
4 of 5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants