# The Ref="Hero" bug, and what else is wrong underneath it Captivity Events writes captive-state skills to the captive but reads them back from the player. Roughly 860 conditions across five add-on modules can never pass, and the defect is still live upstream as of the November 2025 branch. Separately, and costing more content than that bug does: CE discards an **entire event file** when any single token in it fails schema validation. Twelve files were being discarded, holding 436 events. **This document describes the mods as they ship.** Everything below is present in a clean install and is written in the present tense on purpose — section 07 is addressed to the people who maintain these modules. `CHANGES.md`, alongside this file, is the separate question of what a patch actually changed. Method: static analysis of an installed copy. Mono.Cecil for CE's IL, `System.Xml` validation against CE's own XSDs, XML parsing for the event graph. CE version 1.4.5.1400, DLL md5 `55a160776dfa61474c701fc03e19283d` — byte-identical to the official archive, no community patches involved. --- ## 01 — The asymmetry The **write** path resolves `Ref="Hero"` by context. In `SharedCallBackHelper::ConsequenceChangeSkill`, the target depends on whether the event carries the captor flag: ``` flag 40 present? (this is a captor event) no -> Hero slot = Hero::get_MainHero player is the captive yes -> Hero slot = listedEvent.Captive the NPC captive ``` So `Ref="Hero"` means *the subject of this event* — always the captive, whoever that is. The **read** path never implements the mirror. `CEEventChecker::SkillsCheck` hardcodes it: ``` IL_008D ldloc.s V_4 IL_008F callvirt SkillRequired::get_Ref() IL_0094 ldstr "Hero" IL_0099 call String::op_Equality IL_009E brtrue.s IL_00A3 IL_00A0 ldarg.1 else: the passed-in character IL_00A3 call CharacterObject::get_PlayerCharacter "Hero" => always the PLAYER IL_00A8 stloc.s V_5 ``` `CEEventChecker::TraitsCheck` has the identical defect — same signature, same hardcode. **The consequence:** in a captor event, `` writes to the captive while `` reads the player. Since the player never accumulates `CaptiveLust*`, `CaptiveEsteem` or `Broken`, every `Min` check fails permanently and every `Max` check passes permanently. --- ## 02 — Why `Ref="Captive"` is not a drop-in This is the part that trips up XML-side fixes. `"Captive"` and `"Captor"` are not alternative targets — they are **context gates**. ``` Ref == "Captive" -> V_8 = 0 --+ Ref == "Captor" -> V_8 = 2 --+ else -------------------------+-> IL_008D evaluate, no gate | IL_0074 if (isRandomEvent) -> SKIP IL_0065 "Captive": if (captor) -> SKIP else -> evaluate IL_006F "Captor" : if (!captor) -> SKIP else -> evaluate IL_0088 br IL_02A5 <- the loop increment: continue, never evaluated ``` The two callers pass opposite context: | Caller | character | captor | isRandomEvent | |-----------------------------|-------------|--------|---------------| | `FlagsDoMatchEventConditions`| the captive | false | computed | | `CaptorSkillsCheck` | the captor | true | false | Resolution table: | `Ref` | main path | captor path | random event | |----------------|----------------------|---------------------|--------------| | `"Captive"` | evaluates vs captive | skipped | skipped | | `"Captor"` | skipped | evaluates vs captor | skipped | | `"Hero"` | vs PlayerCharacter | vs PlayerCharacter | vs PlayerCharacter | | anything else | vs character | vs character | vs character | Two things follow. **Converting `Ref="Hero"` to `Ref="Captive"` inside a random event does not retarget the check — it deletes it.** And an unrecognised value (a typo, or the attribute omitted) falls through ungated to the correct character, which is undocumented and reachable only by accident. --- ## 03 — Do not patch the DLL The obvious fix is to make the read path mirror the write path. It was tried and it breaks gameplay. Both read sites were patched to resolve `"Hero"` to the passed-in character — an offset-preserving five-byte edit at `0x34392` and `0x34740`, replacing `call get_PlayerCharacter` with `ldarg.1` plus padding. It verified clean: 327 types, 3,284 methods, zero unparseable. In game, captor event chains advanced one step and dumped the player back to the menu. **Why it fails.** Because `"Hero"` resolved to the player, roughly 860 captive-flag conditions were permanently true. The mod content was authored and playtested on top of that. Making them all evaluate at once gates chains the authors never expected to be gated, and CE has no fallback when no candidate event passes — it returns to the menu. The bug is real. The content depends on it. The XML route survives precisely *because* `Ref="Captive"` is gated: over-conversion degrades to a skipped check rather than a dead chain. That safety valve is what the DLL patch bypasses. --- ## 04 — What a correct XML conversion looks like Rule: inside `` elements only, rewrite `Ref="Hero"` -> `Ref="Captive"` when the `Id` is a custom captive-state flag. Vanilla Bannerlord skills keep `Ref="Hero"` — they genuinely mean the player. `` writes are never touched. Applied to the community build, that is a further 171 checks across 17 files, on top of the 606 it already converts: ``` CaptiveLustV 37 CaptiveLustC 17 CaptiveEquus 4 CaptiveLustO 32 CaptiveLustT 14 CaptiveCanis 4 CaptiveLustA 23 CaptiveAffection 9 Concubine 3 CaptiveEsteem 18 FlowerGirlFavor 5 Fame 2 Blackmailed 2 MindBroken 1 ``` Split by direction: **45 `Min` checks** (content locked out entirely, since the player's value is always 0) and **127 `Max` checks** (content that fired regardless of state). **Deliberately not converted:** 153 checks in `a_BCCaptorConditions.xml`. 136 of them sit in condition blocks no event references via `UseConditions` — dead code. Only 17 are live. That file is where over-conversion breaks everything at once, so the risk-to-reward does not justify it. Observed result in play: courting a fresh captive now correctly gates on her having no experience and redirects to the dating chain, and the dating chain produces varied outcomes instead of always falling through to the "too tired" event. --- ## 05 — Upstream is still unfixed Checked against SkipWestcott's `BCCaptorXML` repository, `development` branch, last commit **2025-11-10** — the build the release thread advertises as current. `Events/BCCaptorDating.xml` on that branch: | Check | Upstream | Community build | |---------------------------------------|----------|-----------------| | `SkillRequired Ref="Captive"` | 0 | present | | `SkillRequired Ref="Hero"` | 30 | reduced | | `BC_event_date_tired` SocialEnergy | `Max=10` | `Max=5` | Zero `Ref="Captive"` anywhere in the author's live branch, so the read/write asymmetry is unaddressed at source. And `SocialEnergy Max="10"` overlaps `fake_tired`'s `Min="5"` across a five-point band, meaning both tired events qualify simultaneously — the community narrowing to `Max="5"` is a real improvement the author does not have. ### Where the community fixes came from Three successive repair attempts circulate in the release thread, none from the mod author: | Date | Author | Build | |------------|---------------|----------------------------------------------------| | 2026-06-16 | Erudain | `BCCaptorEventsFixed.rar` | | 2026-07-07 | rottingMan | `zCEzBCCaptorGear.zip` — AI-assisted, by his own description | | 2026-08-02 | the_big_weeb | `..._Rotten_Weeb_Fix.rar` — rottingMan's, extended | rottingMan's automated pass is the origin of the *"Repaired archive notes"* section that appears in the redistributed README but not in the author's. It references `BCCaptor_REPAIR_REPORT.md` and `BCCaptor_REPAIR_LOG.json` — neither ships in the archive, so the repair record is unrecoverable. The Sad Ciri claim does check out: `BCOverrideSadCiriEvents.xml` is a bare `` carrying the comment *"Optional Sad Ciri override disabled: the required external SS_* event pack is not declared by this module."* ### The TB prefix Roughly three quarters of the event files carry a `TB_` prefix rather than `BC_`. Per the author's release post, BC Captor was "inspired by the great framework from TBCaptivityEvents" — a separate, earlier CE framework whose events and skill family were carried across. It explains the naming and nothing more: **BC Captor has no dangling references at all.** (An earlier draft of this writeup claimed three; those targets are defined in `zCaptivityEvents\ModuleLoader\CaptivityRequired\Events`, a directory the first scan never looked in.) ### Independently corroborated The same defect has been reported from the player side in that thread without the cause being identified — most precisely by Erudain: *"no affection is being actually gained by NPC/captives despite the UI saying so."* That is exactly what a write-to-captive, read-from-player asymmetry looks like from inside the game. --- ## 06 — Other defects found along the way ### Schema rejections discard the whole file — 12 files, 436 events The most consequential finding here. `CECustomHandler::GetAllVerifiedXSEFSEvents`: ``` XMLFileCompliesWithStandardXSD(file) true -> AllEvents.AddRange(DeserializeXML(file)) false -> br // next file. every event in this one is discarded ``` One invalid enum value anywhere in a file costs every event in it. There is no partial load and no per-event skip. Failures are itemised in `zCaptivityEvents\ModuleLogs\LoadingFailedXML.txt`, one line per file — and CE reports only the **first** error it hits, so fixing the named problem simply reveals the next. | File discarded | Events | Cause | |-----------------------------------------------|--------|---------------------------| | `zCEDefaults/DefaultCaptiveEventsCommon.xml` | 86 | `ChangeSkill` consequence | | `zCEDefaults/DefaultCaptiveEventsFemale.xml` | 42 | `Straight` flag | | `zCEDefaults/DefaultSlaveryEventsFemale.xml` | 40 | `Straight` flag | | `zCEDefaults/DefaultCaptorEventsFemale.xml` | 25 | `Straight` flag | | `zCEDefaults/DefaultCaptorEventsMale.xml` | 23 | `Straight` flag | | `zCEDefaults/DefaultCaptorEventsCommon.xml` | 9 | `Straight` flag | | `zCEDefaults/DefaultCaptiveEventsMale.xml` | 8 | `Straight` flag | | `zSadSunsEvents` — all four files | 203 | `WeightedChanceOfOccuring` x406 | | `zCEzBCCaptorGear/BCOverrideSadCiriEvents.xml`| 0 | file contains no `CEEvent`| **zCEDefaults loses 233 events** — seven of its nineteen event files never load. Six die on a single unrecognised flag value, `Straight`, which is absent from the 122-value `RestrictedListOfFlagsType` enum and is not declared as a custom flag either. It has therefore never done anything except prevent its own file from loading. **SadSuns loses everything.** All four files spell the weight element `WeightedChanceOfOccuring` — one `r` short, 406 times, with zero correct spellings anywhere in the module. BC Captor spells it correctly 2353 times. Validating directly against the XSD rather than reading the log surfaces the full set — 75 offending lines, not the dozen the log implies: | Token | Kind | Count | In the schema? | |---------------------------------|-------------|-------|----------------| | `Straight` | flag | 49 | no | | `GiveXP` | consequence | 8 | no | | `TraitTotal` / `TraitToLevel` | element | 8 | no | | `ChangeCaptorTrait` | consequence | 4 | no | | `Lesbian` | flag | 3 | no | | `SkillToLevel` / `SkillXPTotal` | element | 2 | no | | `ChangeSkill` | consequence | 1 | no | The `Skill*` and `Trait*` elements are an older flat CE syntax (`Medicine` with a sibling ``), superseded by the nested `` form. The rest were never valid at all. Three one-token defects — a missing `r`, an undefined flag name, an undefined consequence name — suppress more content than every dead chain link in this report combined. None is a design problem. ### How CE resolves a duplicate name Worth stating precisely, because it reads backwards. Two mechanisms combine: ``` CEHelper::GetModulePaths paths.Insert(0, path) // walking launcher order CESubModule::OnBefore...SetAsRoot skip = incoming.Flags.Contains(Overwritable) && nameAlreadyClaimed ``` Because the path list is built with `Insert(0, …)` rather than `Add`, it comes out **reversed**: the module *lowest* in the launcher is parsed *first*. And `Overwritable` does not mean "others may replace me" — it means **"discard me if my name is already taken."** It is a yield flag. So: first parsed wins, and the loser must carry `Overwritable` or it stays in the list as a live duplicate with selection left to chance. An override pack must sit *below* the pack it overrides. Across this install that machinery works — **243 of 246** duplicate resolutions behave exactly as designed. The exceptions: - `zCEDefaults` has **7 events missing `Overwritable`** while the other 217 in the same pack carry it. Two of the seven (`CE_spouse_event_female_1` and `…_city_1`) are overridden by BC Captor, so both copies stay live and the player gets one at random. - Three `WaitingMenu` events — `CE_prostitution_waiting_menu_3`, `_4` and `CE_waiting_menu_general_2` — are claimed by both BC Captor and Boukensha's, with Boukensha's copies carrying no `Overwritable`. **Leave these alone.** Boukensha's ships a seven-band prostitution ladder and deliberately squats on the CE names for bands three and four, so its `_3` covers prostitute level 50–100 and its `_4` covers 100–150, while BC Captor's cover 0–200 and 200+. Flagging the "loser" would discard Boukensha's copies outright and delete two rungs of that ladder. The duplicate is load-bearing. Waiting-menu selection works the same way throughout: `WaitingList::CEWaitingList` builds a weighted pool of every candidate whose conditions pass and draws one with `HelperMBRandom`. There is no priority order and no fallback chain, which is why a gate on one copy of a duplicated menu does not defer to anything. **But the options do not follow the draw.** Observed in play: with both copies of `CE_prostitution_waiting_menu_3` live, the menu shows Boukensha's *text* and the **union of both copies' options** — four entries, in event order. So a duplicated menu name takes its text and background from whichever event wins the draw, while every same-named event contributes its options to the one game menu. The visible consequence, for anyone running BC Captor and Boukensha's together: at prostitute level 75–100 the brothel wait menu shows **two "Leave." entries**, one from each copy. Both work. It is cosmetic, and it cannot be fixed from either mod alone without reopening a real gap — the two bands genuinely overlap. ### Dead chain links Events whose `TriggerEvents` name a target that exists in no installed module. The raw reference count badly overstates the impact, so the unit that matters is the *option group*: only when every target in a group is missing does the option actually dead-end. | Module | Hard dead ends | Degraded only | |--------------------------|----------------|---------------| | `zBoukenshasBinder` | 13 | 13 | | `zCEzBCCaptorGear` | 0 | 0 | | `zAdvancedSlaveryEvents` | 0 | 0 | Of 26 trigger groups containing a dead reference, 13 still have surviving targets — CE picks one from the weighted list and the chain continues. Only the 13 hard dead ends return the player to the menu. `BB_BetrothedTeaching` accounts for three of the thirteen and is the one worth an author's attention: three option groups of 3, 4 and 4 targets, each losing *every* target. Whichever choice the player makes, the event dead-ends. One missing child is misspelled (`BB_BetrothedTeachingObediance`), suggesting a rename that left its references behind. The remaining ten are single-option dead ends in otherwise working features: `CityWhores`, `RaidingHero`, the tavern card game. ### Names that collide A dead reference is not always missing content. Sometimes the target exists under the wrong name — and the wrong name is a *duplicate* of something else, so one of the two events was unreachable as well: | Module | Duplicated name | Should have been | |--------------------------|---------------------------------------|------------------------------------| | `zBoukenshasBinder` | `BB_UngenderedGaySoldiersRefusal` | `BB_UngenderedfemboyExposedRefused`| | `zBoukenshasBinder` | `BB_CagedCarousing…Invited` | `BB_FemCagedCarousing…Invited` | | `zAdvancedSlaveryEvents` | `AdvancedSlavery_pussygrab2continue` | `…continueb` | The first is the clearest: a second event carrying the gay-soldiers name sits in the middle of the femboy chain, its `BackgroundName` is `BB_FemboyCumFloor`, and its text answers the femboy options word for word. Three options pointed at `BB_UngenderedfemboyExposedRefused`, a name nothing defined. **One paste error, three dead ends.** ### Content with no way in BC Captor ships seven `BC_prostitution_waiting_menu_animation_*` events plus `BC_slavery_waiting_menu_animation_walk` — roughly 550 lines with video backgrounds. Each name appears exactly once across every installed module: its own definition. Nothing references them, the DLL does not name them, and all eight carry `WeightedChanceOfOccurring=0`, so they cannot be drawn either. Their intended entry point is still in the file, commented out. Four menus (`menu_4`, `menu_5`, `siege_menu_1`, `siege_menu_2`) each carry a disabled `