Crime/witness event extraction — verifying a secondhand AI's claims against CommonLibSSE-NG headers and primary sources¶
Document File ID: docs/research/29-crime-witness-event-extraction.md Date: 2026-08-27
TL;DR¶
An external AI's 7 claims about Skyrim/Oblivion crime mechanics and a proposed
TESCrimeEvent ChronicleBridge slice were checked against a local
CommonLibSSE-NG header checkout, UESP, and Nexus/mod primary sources. No
RE::TESCrimeEvent type exists anywhere in CommonLibSSE-NG — this was the
load-bearing technical claim (item 7) and it is wrong as stated. The engine
does have internal BSTEventSink<Bounty::Event>, BSTEventSink<AssaultCrime::Event>,
and BSTEventSink<MurderCrime::Event> types (confirmed via RTTI/VTABLE
signature offsets), but CommonLibSSE-NG has never wrapped them into public,
subscribable RE:: headers the way TESDeathEvent is wrapped — they exist
only as raw addresses used internally by the game's own achievement/misc-stat
counters (_MiscStatIncrementer, _BountyToMiscStatHandler). This directly
confirms and sharpens docs/research/22's own already-filed finding: crime/
bounty has no ScriptEventSourceHolder-registrable sink, and the only path
in is a reverse-engineered vtable/function hook — the same risk category as
report 26/28's vendor-price-hook work, not the DeathEventSink pattern.
Of the 7 claims: 1 is essentially correct (initial-vs-locked-in witnesses,
terminology aside), 2 is refuted (Responsibility is Oblivion-only, absent
from Skyrim), 3 is verified but the transfer argument is weak (the mod is
real, but Oblivion's Papyrus-native crime-witness hooks are architecturally
unlike Skyrim/SKSE's closed engine), 4 is overstated (conflates a real
but narrow vanilla Easter-egg mechanic with a "Reputation system" that is
actually third-party mod territory), 5 is real but mischaracterized (the
mod exists, but the cause is non-persistent-reference unloading, not a
"Papyrus crashes" bug, and the fix is an SKSE C++ plugin, not console
commands), 6 is independently corroborated by this project's own report
15, and 7's core recommendation is wrong on the mechanism (no clean
event-sink exists) but right on the underlying idea (crime-witness data is
real, present in the engine's Crime struct, and extractable — just not for
free).
Findings¶
1. [REFUTED — mechanism, VERIFIED — architecture claim] TESCrimeEvent does not exist; crime data does¶
Direct grep of /home/geoff/projects/skyrim-re-toolkit/type-importer/vendor/CommonLibSSE-NG
for TESCrimeEvent returns zero matches, in headers or sources. There is no
RE::TESCrimeEvent, no RE::BSTEventSource<TESCrimeEvent>, nothing
registerable via RE::ScriptEventSourceHolder the way DeathEventSink.h
sinks RE::TESDeathEvent.
What does exist:
RE/C/Crime.hdefines a realRE::Crimestruct (sizeof == 0x78), reverse-engineered from the game binary. It has a genuine witness list:
struct Crime {
...
BSTArray<ActorHandle> actorsKnowOfCrime; // 28 — the witness list
...
TESFaction* crimeFaction; // 60
mutable BSReadWriteLock lock; // 68
};
Everything else in the struct is unlabeled unk fields — no crimeType,
no per-witness "has reported" flag, no timestamp, no location. The witness
set is real and directly matches item 7's witness_id idea; nothing else
in the payload wishlist (crime_type, detection_level, per-witness
report status) is exposed by this struct as reverse-engineered so far.
-
RE/E/ExtraPlayerCrimeList.hexposesBSSimpleList<Crime*>* crimes— how aTESObjectREFR(the player) accumulates its ownCrime*list. -
RE/P/PlayerCharacter.handRE/T/TESFaction.h/.cppexpose the bounty-value side:CrimeGoldStruct{ witnessed, unwitnessed },GetCrimeGoldValue/ModCrimeGoldValue/SetCrimeGoldValuevirtuals onActor, andTESFaction::ModCrimeGold/GetCrimeGold*wrappers. These are real, callable functions (virtual-table slots, not raw signature-scanned addresses) — so the bounty value is easier to read/react to than the crime event is. -
Internal-only crime event sinks exist but are unwrapped.
Offsets_RTTI.handOffsets_VTABLE.hcontain real signature-scanned entries for:
RTTI_BSTEventSink_Bounty__Event_
RTTI___BountyToMiscStatHandler
RTTI_BSTEventSink_AssaultCrime__Event_
RTTI___MiscStatIncrementer_AssaultCrime__Event_
RTTI_BSTEventSink_MurderCrime__Event_
RTTI___MiscStatIncrementer_MurderCrime__Event_
These prove the engine internally dispatches Bounty::Event,
AssaultCrime::Event, and MurderCrime::Event through the standard
BSTEventSink template — structurally identical machinery to
TESDeathEvent. But CommonLibSSE-NG has not defined the Bounty,
AssaultCrime, or MurderCrime namespaces/structs anywhere in include/
— no Event struct layout, no BSTEventSource<Event>::GetSingleton()
accessor. They are visible only as anonymous vtable addresses (used
internally by _MiscStatIncrementer/_BountyToMiscStatHandler, the
achievement/stat-tracking classes), not as a plugin-consumable header the
way DeathEventSink.h consumes TESDeathEvent.
This is the precise resolution the task asked for. docs/research/22's own
already-filed TL;DR is correct and this pass corroborates it directly from
the header source: "Crime accumulation is processed directly inside
faction structures and player character logic. Because no global crime sink
exists in ScriptEventSourceHolder, tracking bounty changes requires an
inline C++ function hook or detour... on RE::TESFaction::ModBounty or
RE::PlayerCharacter::ModBounty." The secondhand AI's claim 7 — that this is
a simple TESCrimeEvent-style sink hook — is wrong. The type doesn't exist
in the public surface; the closest analogues (Bounty::Event et al.) exist
only as unmapped internal vtables, a strictly harder starting point than
report 26/28's vendor-price hook (which at least had a documented,
Address-Library-backed RE::VTABLE_BarterMenu).
2. [VERIFIED, terminology loose] Initial vs. reported/locked-in witnesses¶
UESP and community sources (GameFAQs, gamerant "How to Get Rid of Bounties") confirm: if detected committing a crime, killing every witness before any of them reaches a guard (or before a guard who directly witnessed it) removes the bounty entirely, with an on-screen confirmation. Once a witness reaches a guard (or a guard is itself a witness), the bounty is "locked in" and further witness-killing has no effect. Animals count as witnesses too.
UESP does not use the exact phrase "initial witnesses vs. reported witnesses," but the underlying mechanic the claim describes — a window during which witnesses can be silenced before the crime is irrevocably reported — is real. Verdict: [VERIFIED], modulo invented terminology.
3. [REFUTED] Responsibility as a Skyrim mechanic¶
UESP (Oblivion:Responsibility) and Elder Scrolls Fandom both describe
Responsibility as a hidden 0–100 NPC attribute specific to Oblivion:
NPCs at 100 report crimes unconditionally (even undetected sneak-attack
kills trigger a bounty); 90–30 tolerate some theft and don't report unless
disposition drops; below 30 they ignore crimes and will fence stolen goods.
It also feeds Oblivion's Infamy-driven disposition penalty.
Skyrim removed the entire Attributes system (Strength/Intelligence/etc. and,
with it, most hidden per-NPC 0–100 stats of this kind) in favor of a
simplified detection/alarm/faction-flag model (FACTION_DATA::Flag values
like kIgnoresCrimes_Assult/kIgnoresCrimes_Stealing/etc., confirmed
directly in TESFaction.h/.cpp above). There is no Responsibility
symbol, field, or actor value anywhere in the CommonLibSSE-NG headers, and
no source claims Skyrim retained it. Verdict: [REFUTED] — Responsibility
is Oblivion-only; Skyrim's witness/report gating is coarser (mostly
faction-level flags + detection state), not a per-NPC 0-100 dial.
4. [VERIFIED that a real mod exists — REFUTED that it's an Oblivion crime-hook precedent transferable to Skyrim]¶
Crime has witnesses (Nexus Oblivion mod 22894) and Crime Has Witnesses -
Responsibility Tweak (mod 33682) are both real, and the KCWWQuest.forceWitnesses
/ ModCrimeGold script-variable pattern described in the claim matches the
mod's own documentation (set forceWitnesses to 1 before an ad-hoc
ModCrimeGold call to force witness generation; it resets to 0 after).
But the transfer argument in the original claim is weak, and should be
called out plainly rather than repeated: Oblivion's crime system is exposed
almost entirely through Papyrus-predecessor OBSE/vanilla script
functions operating on quest-script-visible globals (KCWWQuest is a
quest script variable, directly settable from any script). Skyrim/SKSE's
crime system, by contrast, lives in unmanaged C++ structs (RE::Crime,
ExtraPlayerCrimeList) with no Papyrus-visible witness-forcing hook at all
— Skyrim's own Papyrus API exposes only CrimeGold getters/setters on
Faction, never anything witness-shaped. The Oblivion mod's trick works
because Oblivion's crime system was already script-native; Skyrim's isn't.
Verdict: mod is real [VERIFIED], but it is a considerably weaker analogy
for SKSE/C++ work than the original framing implied — it proves witness
mechanics were scriptable in a different, older, more script-exposed engine,
not that they're similarly reachable in Skyrim.
5. [OVERSTATED] "Reputation" tracking in vanilla Skyrim¶
Two things get conflated in the claim. What's real and vanilla:
An enemy's gratitude(a genuine, UESP-documented vanilla mechanic, fixed multiple times by USSEP): killing an NPC can trigger a courier letter from one of that NPC's pre-authored enemies (per static relationship data, rank ≤ "foe"), sometimes with a gold reward. This is real, but it is a narrow, pre-authored, per-relationship-pair Easter egg — not a general reputation system.setpcfame/setpcinfamyconsole commands exist in Skyrim, evidence of leftover Fame/Infamy global variables from Oblivion, but community sources (GameFAQs, Nexus forums) agree these have no meaningful vanilla gameplay effect in Skyrim — Oblivion's Infamy→disposition→Responsibility pipeline was not carried forward.- What actually matches the claim's fuller description ("4000 lines of dialogue arranged on a reputation spectrum," Companions recognizing fame/infamy for questline shortcuts) is the "Skyrim Reputation" mod (Nexus 22374 / 95269) — a third-party mod, not a vanilla mechanic.
Verdict: [REFUTED as stated] — the claim describes mod functionality as if it were a vanilla "Reputation" system; the one genuine vanilla piece (An enemy's gratitude) is real but far narrower (a handful of scripted relationship pairs, not a morality score) than "reputation tracking... detected vs. undetected crimes, faction reactions via courier notes" implies.
6. [VERIFIED, mechanism mischaracterized] Persistent Relations¶
"Persistent Relations - Generic NPC Amnesia Fix" (author zhitsak, Nexus, requires Address Library) is a real SKSE C++ plugin for SE/AE/VR. Per its own description: it monitors generic (non-persistent-reference) NPCs as they unload, remembers the player's relationship rank with them, and restores it on reload/unload cycles by storing the data in the save's co-save data.
Two specific parts of the claim don't hold up:
- It does not work "via console commands" — it's a native plugin writing to SKSE co-save data, invisible to the player.
- The stated cause is not "Papyrus's own relationship functions crash" —
it's that vanilla non-persistent NPC references (
ObjectReferences that aren't marked persistent) get their in-memory relationship state discarded when they unload, a reference-lifecycle/memory-management quirk, not a Papyrus VM crash.
Verdict: [VERIFIED mod exists, REFUTED mechanism description]. The
underlying lesson the claim wants to draw — vanilla Papyrus-side social
state is fragile and something needs to persist it more reliably outside
per-reference game memory — is directionally reasonable and consistent with
this project's own architecture (state lives in chronicle's Python layer,
not in Papyrus globals), but the specific evidence cited to support it is
wrong on both mechanism and cause.
7. [BUILD-ON, independently corroborated] No existing mod does per-NPC witness memory / rumor propagation¶
This project's own docs/research/15-skyrim-social-reactivity-mods.md
already concludes, independently and before this task: "no prior Skyrim
reputation/rumor mod solves per-NPC belief with provenance or genuine rumor
propagation," and that the engine's own Rumors dialogue subtype is
"restricted to the innkeeper job faction" — never a general NPC-to-NPC path.
Report 17 adds that SkyrimNet's community IntelEngine plugin comes closest
with LLM-driven gossip chains, but that's LLM-authored freeform content, not
symbolic witness-tracked crime memory. Verdict: [VERIFIED / independently
corroborated] — this genuinely does appear to be open ground, on the
evidence of two independent research passes in this project plus this
session's targeted check.
Recommendation¶
Crime-witness extraction is R&D-spike-tier, not routine DeathEventSink-tier
work. There is no TESCrimeEvent (or equivalent) sink to register with
RE::ScriptEventSourceHolder. A future ChronicleBridge "crime witnessed"
slice would need to choose one of:
- Function-hook the mutation points — detour/hook
RE::Actor::ModCrimeGoldValue/SetCrimeGoldValue(virtual, so a vtable swap works, similar in spirit to report 28'sRE::VTABLE_BarterMenutrick) orTESFaction::ModCrimeGold. This tells you that a crime happened and its gold value/faction, on the main thread, synchronously — but not the witness list. - Walk
RE::Crime::actorsKnowOfCrimedirectly, reached viaExtraPlayerCrimeList::crimeson the player'sTESObjectREFRextra-data list, polled or read at the same hook point as (1). This is the one path that actually gets you thewitness_idlist the original recommendation wanted — but every field beyond that array is an unlabeledunkin the current CommonLibSSE-NG header, socrime_type/detection_level/ timestamp would need either separate correlation (e.g., cross-reference with the faction/gold-delta from step 1) or their own reverse-engineering pass to identify the unk offsets. - Reverse-engineer and wrap the internal
Bounty::Event/AssaultCrime::Event/MurderCrime::Eventsinks found in the RTTI/VTABLE tables — the "correct-shape" answer structurally (a realBSTEventSink), but nobody has published theEventstruct layout, so this is new reverse-engineering work, not a documented, ready-to-use header the wayTESDeathEventis.
Any of these is squarely in the same risk category docs/research/22
already flagged and docs/research/26/28 already worked through for
vendor pricing — not a copy of the DeathEventSink pattern. Recommend
scoping a future "crime witness" slice as its own spike (most promising
starting point: option 2, since the witness array is already a known,
sized, reverse-engineered field — cheaper than options 1 or 3), explicitly
not bundled with any hydration/avoidance-tier next steps, exactly as report
26 scoped the vendor-price hook.
Caveats¶
- The CommonLibSSE-NG checkout inspected is the vendored copy under
skyrim-re-toolkit/type-importer/vendor/CommonLibSSE-NG, not necessarily the exact copy vendored insideadapters/skyrim/ChronicleBridge/itself — if the two diverge in version, field offsets/struct completeness forRE::Crimecould differ slightly, though the "noTESCrimeEventexists" finding is a strongly-held negative (grep found zero occurrences of the string across the entire tree) and unlikely to be version-sensitive. RE::Crime's unlabeledunkfields were not reverse-engineered as part of this pass — only the fact that they're unlabeled (and thus not ready-to-use) was established. A future spike could plausibly recovercrimeTypeand a per-witness "reported" bit from these offsets by comparing against Papyrus'sFaction.GetCrimeGold-family behavior or existing xEdit/CE structure dumps, which this pass did not attempt.- UESP's
Skyrim:Crimepage itself returned HTTP 403 to direct fetch; the witness-locking mechanic (finding 2) is corroborated by search-engine summaries of that page plus independent GameFAQs/gamerant sources, not a direct primary-source read of the UESP article text. - This report does not touch
adapters/skyrim/ChronicleBridge/code, thechronicle/Python layer, ordocs/research/00-index.md, per task instructions.