# Work Summary - MysticAssistantRedux - 2026-02-07

## Feature: Boss Skin "New" Alert Badge Fix

### Status: COMPLETE

**User Report:** Boss skin "new" alert badge doesn't appear in the follower forms menu after purchasing from the Mystic Assistant shop.

### Root Causes (Three Layers)

#### 1. Invariant skins filtered from forms menu
`WorshipperData.GetSkinsFromLocation()` unconditionally excludes `Invariant=true` skins. Three boss skins (Boss Death Cat, Boss Aym, Boss Baal) are Invariant, so no `IndoctrinationFormItem` was ever created for them — no UI element means no badge.

#### 2. `_singleAlerts` persistence blocks `AddOnce`
`AlertCategory<T>.AddOnce()` calls `Add()`, which rejects alerts already in `_singleAlerts` (the permanent "shown once" tracker). Once a boss skin alert was registered even once, `_singleAlerts` persisted it across sessions via MessagePack serialization, permanently blocking future `AddOnce` calls — even after `ReplaceDeprication` cleared the alert from `_alerts`.

#### 3. Auto-selection removes badge instantly
`UIFollowerFormsMenuController.OnShowStarted()` auto-selects the first form item via `OverrideDefaultOnce`. Boss skins (not in `MiscOrder`) sort to index -1 (before named entries), making them the first item. `IndoctrinationFormItem.OnSelect` fires `TryRemoveAlert()`, killing the badge in the same frame it was created.

### Fixes Applied

#### 1. `GetSkinsFromLocation_Postfix` — Include unlocked Invariant boss skins in forms menu
Postfix on `WorshipperData.GetSkinsFromLocation` that adds back unlocked Invariant boss skins. Scoped to skins where `Invariant=true`, `Skin[0].Skin.Contains("Boss")`, unlocked, and matching the queried `DropLocation`.

#### 2. `RegisterSkinAlert` — Clear `_singleAlerts` before `AddOnce`
Removes the alert key from `_singleAlerts` before calling `AddOnce`, ensuring purchase-time alerts always register regardless of previous session state.

#### 3. `ReplaceDeprication` Prefix+Postfix — Preserve boss skin alerts across save/load
Prefix snapshots active boss skin alerts (containing "Boss") from `_alerts` before `ReplaceDeprication` clears them. Postfix restores them directly to `_alerts`, bypassing the `_singleAlerts` check. Only restores alerts that were active before cleanup (user hasn't viewed them yet).

#### 4. `OnSelect` Suppression — Prevent auto-select from killing badges
Prefix on `UIFollowerFormsMenuController.OnShowStarted` sets a suppression flag. Prefix on `IndoctrinationFormItem.OnSelect` skips `TryRemoveAlert` while the flag is set. Postfix on `OnShowStarted` starts a coroutine to clear the flag after one frame.

#### 5. Cleaned up all diagnostic logging
Removed `[SkinAlert]`, `[SkinForms]`, `[BadgeDebug]`, and `[ClothingDebug]` verbose diagnostic logging.

### Files Modified

| File | Changes |
|------|---------|
| `MysticAssistantRedux/Patches/MysticShopPatches.cs` | All 5 fixes above |
| `MysticAssistantRedux/InventoryManager.cs` | Cleaned up `PopulateBossSkins` diagnostic logging (earlier session) |

### Architecture Notes

**Four-layer fix:**
1. **Data layer** (`GetSkinsFromLocation_Postfix`) — Makes Invariant boss skins visible in forms menus
2. **Alert layer** (`RegisterSkinAlert`) — Clears `_singleAlerts` blocker, registers correct alert key
3. **Persistence layer** (`ReplaceDeprication` prefix+postfix) — Preserves alerts across save/load via snapshot+restore
4. **UI layer** (`OnSelect` suppression) — Prevents auto-selection from removing badges on menu open

**Why patching GetSkinsFromLocation is the right chokepoint:**
- Single method used by all 3 forms menu controllers
- Also affects `SetUnlockedText` counters (X/Y collected)
- Postfix scoped to boss skins only via `Contains("Boss")` check

### Key Game Code References

| Code | Location | Relevance |
|------|----------|-----------|
| `WorshipperData.GetSkinsFromLocation()` | WorshipperData.cs:175-192 | Invariant filter |
| `AlertCategory<T>.Add()` | AlertCategory\`1.cs:34-43 | Checks both `_alerts` AND `_singleAlerts` |
| `AlertCategory<T>.AddOnce()` | AlertCategory\`1.cs:45-51 | Adds to `_singleAlerts` permanent tracker |
| `AlertBadge<T>.Configure()` | AlertBadge\`1.cs:24-35 | Badge show/hide logic |
| `IndoctrinationFormItem.OnSelect()` | IndoctrinationFormItem.cs:39 | Calls `TryRemoveAlert()` |
| `DataManager.ReplaceDeprication()` | DataManager.cs:4508-4511 | Strips Invariant alerts |
| `UIFollowerFormsMenuController.OnShowStarted()` | UIFollowerFormsMenuController.cs:204-264 | Auto-selects first item |

---

## Feature: Gate Non-Wheel Content Behind Config Options

### Status: COMPLETE

DLC necklaces and boss skins are opt-in via `ConfigEntry<bool>` (default: off).

### Changes Made

| File | Changes |
|------|---------|
| `Plugin.cs` | Added `EnableDLCNecklaces` and `EnableBossSkins` config entries (section "01. Extra Content", Order 2/1) |
| `InventoryInfo.cs` | DLC necklaces gated behind `Plugin.EnableDLCNecklaces.Value && DataManager.Instance.MAJOR_DLC`; boss skin shop item gated behind `Plugin.EnableBossSkins.Value` |
| `InventoryManager.cs` | `PopulateBossSkins()` early-returns when config off |
| `MysticShopPatches.cs` | All 6 boss skin patches gated: `GetSkinsFromLocation_Postfix`, `ReplaceDeprication` prefix/postfix, `OnShowStarted` prefix/postfix, `OnSelect` prefix |

Config is fully dynamic — shop inventory rebuilt each open. Descriptions note "Re-open the shop for changes to take effect."

---

## Feature: Localized Config UI

### Status: COMPLETE

### Changes Made

| File | Changes |
|------|---------|
| `Localization.cs` | **NEW** — 15-language dictionary (same pattern as `Namify/Localization.cs`). Keys: `DLCNecklacesName`, `DLCNecklacesDesc`, `BossSkinsName`, `BossSkinsDesc`. Uses `LocalizationManager.CurrentLanguage` with English fallback. |
| `Plugin.cs` | Config bindings use `Localization.*` for `DispName` and description via `ConfigurationManagerAttributes` |

Languages: English, Japanese, Russian, French, German, Spanish, Portuguese (Brazil), Chinese (Simplified), Chinese (Traditional), Korean, Italian, Dutch, Turkish, French (Canadian), Arabic

---

## Feature: Documentation Updates

### Status: COMPLETE

| File | Changes |
|------|---------|
| `Thunderstore/mysticassistantredux/CHANGELOG.md` | Reorganized 0.1.1: exclusive content (Apple skins/clothing/fleece) separate from optional config content (DLC necklaces, boss skins). Removed dev-only bug fix lines. Added localization line. Removed Apple decorations (not working). |
| `Thunderstore/mysticassistantredux/README.md` | Split into "Exclusive Content" (Apple skins/clothing/fleece) and "Optional Content (Config)" (DLC necklaces, boss skins). Removed Apple decorations. |
| `Thunderstore/mysticassistantredux/nexusmods_description.txt` | Same changes in BBCode format. |

---

## Feature: Apple Decoration Placement Fix

### Status: COMPLETE (tested, working in-game)

**Problem:** Apple decorations (Bush, Lantern, Statue, Vase, Well) crash with `InvalidKeyException` when placed from build menu. The `Addr_PlacementObject` GUIDs in `TypeAndPlacementObjects` scene data only resolve on Apple Arcade builds.

### Root Cause

Crash flow: `BuildMenuItem.OnButtonClicked()` → `Interaction_PlacementRegion.PlaceBuilding()` line 224 → `TypeAndPlacementObjects.GetByType(type).PlacementObject` → lazy-loads via `Addressables.LoadAssetAsync(Addr_PlacementObject)` → GUID doesn't exist in PC catalog → `InvalidKeyException` → null → `Instantiate(null)` → crash.

Runtime probe confirmed all 5 Apple decoration PREFABS load fine on PC — only the placement ghost GUIDs are broken.

### Failed Approaches

1. **TypeAndPlacementObjects.Awake() postfix** — `WaitForCompletion()` deadlocks during scene init
2. **Donor decoration ghost** — Worked but showed generic footprint instead of actual decoration
3. **Inactive runtime ghost template** — `Object.Instantiate` preserves active state → inactive clone → game freezes (TimeScale=0, no UI)
4. **Active template with empty ToBuildAsset** — `PlaceObject()` line 782 waits for `GetComponentInChildren<Structure>()` in infinite loop (timeout uses `Time.deltaTime` which is 0 at timeScale=0)
5. **Active template WITH ToBuildAsset** — `PlacementObject.Start()` modifies path in-place → clone inherits modified path → double-prefix

### Final Fix: Runtime ghost + PlacementObject.Start() prefix

Three-part approach:
1. **`PlaceBuilding_Prefix`** — Creates active `PlacementObject` ghost template (empty `ToBuildAsset`, `Bounds(1,1)`, `RotatedObject` child), caches per-type in `_ghostCache`, assigns to `entry._placementObject`
2. **`PlacementObject_Start_Prefix`** — Detects clone instances by `"(Clone)"` in name, injects correct `ToBuildAsset` path from `ExclusiveContent.AppleDecorationPrefabPaths`
3. Original `PlacementObject.Start()` runs → modifies path → loads real Apple decoration prefab → `Structure` component appears → `PlaceObject()` wait loop exits

### Files Modified

| File | Changes |
|------|---------|
| `MysticShopPatches.cs` | `PlaceBuilding_Prefix`, `CreateAppleDecorationGhost()`, `PlacementObject_Start_Prefix` |
| `ExclusiveContent.cs` | Added `AppleDecorationPrefabPaths` dictionary, updated doc comment |

### Key Game Code References

| Code | Location | Relevance |
|------|----------|-----------|
| `TypeAndPlacementObject.PlacementObject` | TypeAndPlacementObject.cs:26-40 | Lazy-load from Addr_PlacementObject, checks `_placementObject != null` |
| `PlacementRegion.PlayRoutine()` | PlacementRegion.cs:351 | `Object.Instantiate(PlacementGameObject)` |
| `PlacementObject.Start()` | PlacementObject.cs:40-66 | Modifies ToBuildAsset in-place, loads via Addressables |
| `PlacementRegion.PlaceObject()` | PlacementRegion.cs:782 | Waits for `GetComponentInChildren<Structure>()` — infinite loop without it |

---

## Fix: SecondaryLabel Garbled Text

### Status: COMPLETE

Changed `SecondaryLabel` from `DataManager.Instance.MysticKeeperName + "'s assistant"` to fixed string `"Mystic Assistant"`. The dynamic NPC name could contain arbitrary user-entered text from save data.

| File | Changes |
|------|---------|
| `MysticShopPatches.cs` | Line 17: `SecondaryLabel = "Mystic Assistant"` |

---

## Defensive Fix: Item Selector Stale Text Prevention

### Status: COMPLETE

Added `ItemSelector_OnShowStarted_Postfix` on `UIItemSelectorOverlayController.OnShowStarted` that calls `RefreshContextText()` when items exist. The vanilla game never calls `RefreshContextText()` during `OnShowStarted` — `OverrideDefault()` sets the default item but doesn't trigger `OnItemSelected()`. This means `_buttonPromptText` retains stale text from whatever last used the component. The postfix ensures the text is initialized correctly on open.

| File | Changes |
|------|---------|
| `MysticShopPatches.cs` | `ItemSelector_OnShowStarted_Postfix` |

---

## Feature: Configurable God Tear Cost with Per-Save Lock

### Status: COMPLETE

Configurable cost per item (1-10, default 1) with a one-way ratchet per save slot.

### Implementation

- `ConfigEntry<int> GodTearCost` with `AcceptableValueRange(1, 10)`, default 1
- **Slider-based locking via `SettingChanged`**: No separate lock button. Moving the slider up triggers a confirmation popup. Confirming saves the cost to PlayerPrefs for that save slot. Cancelling reverts the slider.
- **No save loaded guard**: If `!SaveAndLoad.Loaded`, slider reverts to 1 (prevents accidental writes to `SAVE_SLOT=5` default)
- **One-way ratchet**: Moving slider below locked value silently reverts. `GetEffectiveCost()` returns `Math.Max(configValue, lockedValue)`.
- **Per-save persistence**: `PlayerPrefs` keyed by `MysticAssistant_Cost_{SaveAndLoad.SAVE_SLOT}`
- **Save cleanup**: Postfix patches on `SaveAndLoad.DeleteSaveSlot` and `SaveAndLoad.ResetSave` clear the key
- **No re-fire on programmatic set**: Setting `ConfigEntry.Value` programmatically does not re-trigger `SettingChanged`, so no guard flag needed

### Previous Implementation (replaced)

Originally used a `DrawLockCostButton` custom drawer with explicit "Lock at X" / "Increase to Y" buttons. Replaced with `SettingChanged` approach because:
- The lock button was vulnerable to `SaveAndLoad.SAVE_SLOT=5` default on main menu (no save loaded)
- Slider-based confirmation is more intuitive — the act of changing IS the lock

### Files Modified

| File | Changes |
|------|---------|
| `Plugin.cs` | `GodTearCost` config (default 1), `OnGodTearCostChanged` handler, `GetEffectiveCost()`, `ClearCostForSlot()`, PopupManager setup |
| `InventoryInfo.cs` | `CreateTraderItem()` uses `Plugin.GetEffectiveCost()` |
| `Localization.cs` | `GodTearCostName`, `GodTearCostDesc` in 15 languages |
| `MysticShopPatches.cs` | `DeleteSaveSlot_Postfix`, `ResetSave_Postfix` for cleanup |
| `Shared/PopupManager.cs` | **NEW** — Generic popup with confirmation support, moved from CultOfQoL |
| `CultOfQoL/Core/PopupManager.cs` | **DELETED** — Replaced by shared version |
| `CultOfQoL/Plugin.cs` | Added `PopupManager.Title = PluginName` |
| `CultOfQoL/CultOfQoL.csproj` | Added shared PopupManager link |
| `MysticAssistantRedux.csproj` | Added shared PopupManager link |

---

## Fix: God Tear Cost Slider Broken on Title Screen

### Status: COMPLETE

**User Report:** Slider can't be moved past 1, no popup appears.

### Root Cause

`OnGodTearCostChanged` checked `!SaveAndLoad.Loaded` as its first guard. On the title screen (before loading a save), `Loaded` is `false`, so the handler silently reset the value to 1 and returned before reaching any popup code. No feedback was given to the user.

Secondary bug: In the confirm callback, `GodTearCost.Value = newVal` ran before `PlayerPrefs.SetInt(key, newVal)`, causing the re-entrant `SettingChanged` handler to see `locked = 0` and revert + show another popup (infinite popup loop).

### Fixes Applied

1. **Re-entrancy guard** (`_changingCost` static bool) — prevents cascading `SettingChanged` events. Set in both the outer handler and the confirm callback.
2. **Title screen feedback** — `!SaveAndLoad.Loaded` still reverts to 1, but now shows a localized popup: "Please load a save before changing the God Tear cost." (`CostRequiresSave` in 15 languages)
3. **Callback ordering** — `PlayerPrefs.SetInt` now runs before `GodTearCost.Value = newVal` so the re-entrant handler sees the correct locked value.

### Files Modified

| File | Changes |
|------|---------|
| `Plugin.cs` | Added `_changingCost` guard, title-screen popup, callback reorder |
| `Localization.cs` | Added `CostRequiresSave` property + string in all 15 languages |

### Also: CLAUDE.md Cleanup

Removed stale ConfigurationManagerEnhanced (CME) section and dependency reference from `CLAUDE.md`. CME no longer exists; standard BepInEx Configuration Manager is used.

---

## Documentation Updates (Later Session)

### Cross-Mod Links
- Added Mystic Assistant Redux (self) and Quick Menus to "My Other Mods" in README.md and nexusmods_description.txt

### Installation Instructions
- Added BepInEx Configuration Manager to installation sections in both README.md and nexusmods_description.txt (was missing)

### Thunderstore Manifest
- Added `p1xel8ted-BepInEx_Configuration_Manager-18.4.1` dependency

### Files Modified
- `Thunderstore/mysticassistantredux/README.md` — added MAR + QM to My Other Mods, added CM to installation
- `Thunderstore/mysticassistantredux/nexusmods_description.txt` — same
- `Thunderstore/mysticassistantredux/manifest.json` — added CM dependency
