# OG Sound Remaster

**Ungodly madly vastly enhanced original audio for Deadly Dozen Reloaded**

Author: **StixsworldHD (StixsmasterHD4k)**
Version: **1.0.1**
Target: **Deadly Dozen Reloaded** (Steam app 1297430, `DeadlyDozenReloaded.exe`)
Framework: **BepInEx 6 — Unity.IL2CPP, win-x64**

---

Deadly Dozen Reloaded is a remake. This plugin puts the **2002 original's entire soundtrack back
into it** — every weapon, every squad voice line, every bullet impact, the German patrol chatter,
the dog, the tanks and trucks, the ambience beds, the mission music, and all thirty briefing and
debriefing narration tracks.

Nothing is approximated where an exact original exists. The remake exposes its whole audio graph
as typed fields, so the great majority of replacements are bound **by meaning, not by guessing at
file names** — `WeaponMP40.fireSound` gets the MP40 recording because it is the MP40's fire sound
slot, not because two strings happened to look alike.

---

## What changed in 1.0.1

**1.0.0 crashed on mission load.** The reported fault:

```
Fatal error. System.AccessViolationException: Attempted to read or write protected memory.
   at Il2CppInterop.Runtime.IL2CPP.il2cpp_object_get_class(IntPtr)
   at Il2CppInterop.Runtime.Runtime.Il2CppObjectPool.Get[__Canon](IntPtr)
   at Il2CppInterop.Runtime.InteropTypes.Arrays.Il2CppReferenceArray`1[__Canon].WrapElement(IntPtr)
   at OGSoundRemaster.Binding.RoleBinder.BindClipArray(...)
   at OGSoundRemaster.Binding.RoleBinder.BindController(ControllerBase)
   at DynamicClass.DMD<SquadMember::Start>(SquadMember)
```

### Cause

`BindClipArray` seeded its replacement array by **reading elements out of the game's existing
`clips` array**. That read goes `Il2CppReferenceArray.get_Item` → `WrapElement` → `Il2CppObjectPool.Get`
→ `il2cpp_object_get_class`. Decompiling Il2CppInterop 1.5.3 confirms both guards are present —
`GetElementPointer` bounds-checks, and `Il2CppObjectPool.Get` null-checks — so neither a bad index
nor a null slot could have caused it. The slot held a **non-zero dangling pointer**, and
`il2cpp_object_get_class` faulted dereferencing it.

### Why the existing error handling didn't stop it

Every one of those reads was inside a `try/catch`. It made no difference:
`AccessViolationException` is a **Corrupted State Exception**, and .NET Core does not deliver those
to `catch` blocks — the process terminates. Defensive exception handling is not protection against
this class of failure. The unsafe operation has to simply not happen.

### The fixes

**1. Clip arrays are now write-only.** `BindClipArray` builds a complete array from
original-game clips and swaps it in with a single assignment. It never reads an element out of an
array the game owns, and it never reads back out of its own. If part of the set is still decoding,
the remake's array is **left completely alone** until the full replacement exists — so the game
also never sees a half-populated array or a null element, which 1.0.0 briefly could.

**2. All il2cpp access is now strictly main-thread.** 1.0.0 had a second, separate bug I found
while tracing this one: when a decode failed, `ClipStore` invoked its completion callbacks
**from the decode worker thread**, and those callbacks assign Unity objects. Calling into il2cpp
from an unattached thread is undefined behaviour and a textbook source of exactly this crash.
Worker threads now only ever touch managed byte and float buffers; every callback is routed
through the frame pump.

**3. Unowned sound players are deferred instead of raced.** `RandomSoundPlayer.Awake` fires
*before* its owner's `Start`, so 1.0.0's fallback hook bound the array and the role binder then
rewrote it moments later. Loose players are now parked for half a second; if their owner claimed
them in the meantime they are skipped, and only genuinely unowned players fall through to name
matching.

**4. Where an element read is unavoidable** — the voice-pack list, the mission-info array, the
results of `Resources.FindObjectsOfTypeAll` — the container is snapshotted into a managed array in
one tight pass with no il2cpp allocation between reads, then processed from the snapshot.

### Verification

The compiled assembly is audited directly at the IL level, so these are checked facts rather than
intentions:

| Audit | Result |
|---|---|
| `Il2CppReferenceArray<AudioClip>::get_Item` call sites | **none** — the crash site no longer exists |
| `FlushWaiters` callers | only `PumpMainThread` and `MaterialiseNow`, both main-thread |
| `WorkerLoop` / `DecodeInto` / `Fail` | **zero** Unity or il2cpp calls |
| Hook pre-flight against the game assemblies | **33/33** targets present |
| Decoder vs. FFmpeg, full bank | **526/526 bit-exact** |
| Curated sound tables | **180/180 resolve**, briefing VO **30/30** |

No features were removed. Every binding present in 1.0.0 is still present in 1.0.1.

---

## Installation

1. **Install BepInEx 6 (Unity.IL2CPP, win-x64)** into your Deadly Dozen Reloaded folder — the one
   containing `DeadlyDozenReloaded.exe`. Build `6.0.0-be.785` or newer is recommended; this plugin
   is compiled against Il2CppInterop `1.5.3.0`, which is what that build ships.

2. **Run the game once** and let it close. BepInEx needs one launch to generate its interop
   assemblies. This first run takes a few minutes — that is normal and only happens once.

3. **Copy the `BepInEx` folder from this archive** over the `BepInEx` folder in your game
   directory, merging when prompted. You should end up with:

   ```
   <game folder>/
     DeadlyDozenReloaded.exe
     BepInEx/
       plugins/
         OGSoundRemaster/
           OGSoundRemaster.dll
           OGSound/          <- 526 audio files + songs.txt
   ```

   The `OGSound` folder **must** sit next to the DLL. The plugin also accepts it at
   `BepInEx/plugins/OGSound/`, and will search the plugin tree as a last resort, but the layout
   above is the intended one.

4. **Launch the game.** The BepInEx console (if enabled) will report the bank size, the memory
   budget it chose, and the number of hooks it installed.

**Upgrading from 1.0.0:** overwrite `OGSoundRemaster.dll`. The `OGSound` folder is unchanged, so
there is no need to re-copy 130 MB of audio.

To uninstall, delete the `OGSoundRemaster` folder. Nothing else is touched.

---

## No configuration file, no interface — by request

This build ships **exactly as asked**: zero GUI and zero config file. It never calls `Config.Bind`,
so BepInEx does not create a `.cfg` for it. There is nothing to tune and nothing to open.

Everything that would normally be a setting is derived instead:

| Would-be setting | How it is decided |
|---|---|
| Memory budget | 6% of system RAM, clamped to 96–384 MB |
| Decode thread count | `min(3, CPU cores - 1)`, minimum 1 |
| Which sounds stay resident | Anything ≤ 3.5 s is pinned; longer clips load on demand |
| Loop vs. single-shot weapon fire | Read from `fireSound.loop` at bind time |
| Mission music | Taken from the original game's own `songs.txt` |
| Vehicle recordings | Chassis family inferred from the vehicle root object's name |

*(The `BepInEx/config/BepInEx.cfg` you may see belongs to BepInEx itself, not to this plugin.)*

**If you later want a config file** — volume trims, per-category on/off switches, a music playlist
override, an ambience toggle — say the word and I will add one with documented min/max ranges in
the usual format. The internals are already structured for it; the tables in `SoundMap.cs` are the
only thing that would need exposing.

---

## What gets replaced

`MAPPING.txt` in this archive lists **every single binding** — it is generated directly from the
plugin's own tables, so it cannot drift out of date. Summary:

### Weapons
Colt .45, M1 Garand (including the clip ping), Kar98, M1903A4 Springfield, Thompson, MP40, MG42,
Walther P38, M1A1 Bazooka and Panzerschreck — fire, reload, and the burst tail-off for the three
automatics. Plus weapon draw/switch and the combat knife.

### Voices
- **Squad:** all four original American voice packs (`AVoice1/2/3/5`), cycled across your soldiers,
  covering attack orders, fire-at-will, follow, hold fire, hold position, switch-to, kill
  acknowledgements, objectives complete, taking damage, friendly fire, death and low ammo.
- **Germans:** alert, notice, damage, death, search-abandoned, and the "player is down" lines.
- **Dog:** attack, alert, hurt, death.

### Impacts and movement
Bullet hits on flesh, metal, glass, and dirt (the original's rock impacts are folded into the dirt
pool, since the remake exposes four surface categories and the original shipped six — this keeps
eight distinct variations alive instead of four). Footsteps, foliage, wading, swimming, landings.

### Explosives and equipment
AP and AT mines, dynamite (place, beep, timer tick, detonation), grenades (draw, throw, bounce,
detonation), coins, medkit, binoculars, item pickup and drop, rockets in flight and on impact,
generic explosions, minefields.

### Vehicles and armour
Tank idle, movement, turret traverse loop and start/stop, and main gun — German and American
variants selected by object name. Jeeps, trucks and motorcycles get family-correct engine idle,
driving, start and stop recordings, plus crashes, gear changes, skids, brakes, horn and suspension.

### Music and narration
- Per-mission music **transcribed verbatim from the original `Music/songs.txt`** — mission 1 gets
  `sneaky02` + `tension01`, mission 10 gets `Action01` + `Action03`, and so on for all ten.
- Main menu, briefing screen, combat music, victory and death cameras, and both debrief stings.
- All **30 briefing / success / failure narration tracks**.

### Ambience
Wind beds and gusts, daytime birds, night crickets and owls, ocean, rivers, rain, thunder, distant
gunfire and mortars, aircraft flyovers, and the interface clicks and beeps.

---

## Design notes

### Derived single-shot weapon sounds

The original game ships **burst loops** for the three automatics, not single shots — `mp40-loop` is
1.137 s of continuous fire, `tmp-loop` is 1.528 s, `MG42-loop` is 0.911 s. Reloaded, however,
triggers `fireSound` **once per bullet** (`SoundManager.PlaySoundCopy` instantiates the source,
plays the clip through, and destroys it after `clip.length`). Feeding a one-second burst loop into
a per-bullet trigger would stack a dozen overlapping copies into mush.

So the shot rate was measured out of each loop by transient detection and one clean shot was cut
from it:

| Source | Detected rate | Derived file | Length |
|---|---|---|---|
| `mp40-loop.wav` | 87.7 ms / 684 RPM | `Derived/mp40-single.wav` | 192.8 ms |
| `tmp-loop.wav` | 48.0 ms / 1250 RPM | `Derived/tmp-single.wav` | 105.5 ms |
| `MG42-loop.wav` | 52.2 ms / 1148 RPM | `Derived/MG42-single.wav` | 114.9 ms |

*(The MG42 figure landing on ~1148 RPM against the real weapon's ~1200 RPM is a good sign the
detection is measuring what it should be.)*

Each cut runs from the first transient — backed up 4 ms to catch the attack — through 2.2 shot
periods, with a raised-cosine fade over the last 55% so the following shot's transient is buried
rather than clipped, a 0.5 ms de-click on the attack, and peak normalisation to the source.

**These three files are the only synthesised audio in the package**, they live in their own
`Derived/` folder, and the plugin only reaches for them when it sees `fireSound.loop == false`. If
a future patch makes those sources loop, the original loop files are used instead, automatically.

### MP3 → IMA ADPCM

The original's music and narration are MP3, and this Unity build ships **without**
`UnityWebRequestAudioModule`, so there is no engine-side MP3 path to hand them to. Rather than
embed an MP3 decoder, the 56 MP3s were transcoded to **IMA ADPCM WAV** at their native rate and
channel count. ADPCM is block-aligned (so seeking is O(1)), decodes several times faster than MP3,
and the decoder is about sixty lines — small enough to verify exhaustively, which it was.

### Decoder correctness

The bundled decoder handles PCM u8/s16/s24/s32, IEEE float32/64, IMA ADPCM and
`WAVE_FORMAT_EXTENSIBLE`. It was checked against FFmpeg reference decodes across the **entire
526-file bank**: every file came back **bit-exact**, with matching frame counts. The harness that
proves this is included at `Source/Tools/DecoderVerification` — you can re-run it yourself.

### Memory and threading

Decoding every sound at once would need **736 MB** of float32 PCM, so nothing is loaded eagerly.

- 368 short sounds (≤ 3.5 s, **39 MB** total) are pinned for the session — that is the hot working
  set, and it is always instant.
- Longer clips load on demand and are evicted once idle for 25 s, unbound, and confirmed inaudible.
- Decoding runs on 1–3 **background threads** which touch nothing but managed buffers. `AudioClip`
  objects can only be created on the main thread, so finished PCM is drained by a pump with a
  **2 ms per-frame budget** — a 98-second music track can never stall a frame.
- Sounds under 2.5 s take a synchronous fast path when needed, because they decode in well under a
  millisecond.

Realistic steady-state footprint is around **110 MB**.

### Interception

Only **three** patches sit on the audio hot path — `AudioSource.Play()` and both `PlayOneShot`
overloads. Every managed playback in the engine funnels through them, including
`AudioSource.PlayClipAtPoint`, which internally assigns a clip and calls `Play()`.

`PlayOneShot` is redirected straight to `PlayOneShotHelper` rather than relying on Harmony `ref`
parameter write-back, so nothing depends on assumptions about il2cpp argument marshalling. Results
are memoised per clip pointer, so steady-state cost is **one dictionary probe on an integer** — no
strings, no allocation, no metadata access.

The remaining ~30 hooks are one-shot postfixes on the game's own `Awake`/`Start` methods. They fire
once per component and do the deterministic role binding. Nothing polls, nothing scans per frame.

AudioSources with `playOnAwake` set in the editor never route through managed `Play()`, so those
are caught by a scene sweep three frames after each scene change.

---

## The report file

While the game runs, the plugin writes
`BepInEx/plugins/OGSoundRemaster/OGSoundRemaster.report.txt`, refreshed every 30 seconds and on
exit. It is **diagnostics only** — deleting it changes nothing.

It records the indexed bank, resident clip count and memory against budget, evictions, decode
failures, how many callbacks were deferred off decode threads, which hooks applied and which did
not, remap hit/miss counts, how many fields and clip sets were bound, how many unowned sound
players were name-matched, every clip matched by name with its confidence score, and — most
usefully — a list of every remake sound that had **no confident original counterpart** and was
deliberately left alone. If something in your game still sounds like the remake, that list is where
to look.

---

## Troubleshooting

**No original audio at all, and the log says the OGSound folder was not found.**
The `OGSound` folder must sit beside `OGSoundRemaster.dll`. Check for a doubled folder such as
`OGSoundRemaster/OGSoundRemaster/OGSound`.

**The plugin never loads.**
Confirm you have BepInEx 6 **Unity.IL2CPP win-x64** — not BepInEx 5, and not the Mono build. The
game must have been launched once already so BepInEx could generate its interop assemblies.

**Some hooks report as unavailable in the log.**
A game update renamed or removed something. Everything else still binds; the report file names the
specific hooks. Send me that list and I will re-target them.

**A particular sound is still the remake's.**
Check the "left untouched" section of the report. If it is something with a clear original
counterpart, tell me the name from that list and it becomes a one-line addition to `SoundMap.cs`.

**Music starts on the remake's track and switches a moment later.**
Expected on the very first mission load if the track had not finished decoding. It is prefetched at
scene start, so it will not happen again for that track in the same session.

**Any crash at all.**
Send me `BepInEx/LogOutput.log` and the report file. A stack trace that names an
`OGSoundRemaster` frame is actionable, as the 1.0.1 fix above shows.

---

## Building from source

Full source is under `Source/`. Requirements: .NET SDK 6 or newer, and the game's BepInEx interop
assemblies.

```
dotnet build -c Release \
  -p:LibDir=<path to BepInEx/core> \
  -p:GameLibDir=<path to BepInEx/interop> \
  -o <output folder>
```

Also included:

- `Source/Shared/WavDecoder.cs` — the decoder, shared verbatim between the plugin and the test harness.
- `Source/Tools/DecoderVerification` — decodes the whole bank and diffs it against FFmpeg. Requires `ffmpeg` on PATH.
- `Source/Tools/SoundMapValidation` — resolves every curated table entry against the shipped bank and reports anything empty. Currently: **180 sets, 0 empty.**

---

## Contents

```
OG Sound Remaster v1.0.1/
├── README.md
├── MAPPING.txt                       every binding, generated from the live tables
├── BepInEx/plugins/OGSoundRemaster/
│   ├── OGSoundRemaster.dll
│   └── OGSound/                      526 audio files + songs.txt
│       ├── AVoice1 / Avoice2 / Avoice3 / Avoice5   squad voice packs
│       ├── Voice/                    German and NPC lines
│       ├── HitSounds/                bullet impacts by surface
│       ├── Music/                    score, plus the original songs.txt
│       ├── Briefing/                 mission narration
│       ├── Derived/                  the three single-shot cuts
│       └── (root)                    weapons, ambience, vehicles, equipment
└── Source/
    ├── OGSoundRemaster/              plugin source
    ├── Shared/                       WAV / IMA ADPCM decoder
    └── Tools/                        verification harnesses
```

---

## Changelog

**1.0.1**
- Fixed the fatal `AccessViolationException` in `RoleBinder.BindClipArray` during
  `SquadMember.Start`. Clip arrays are now built complete and swapped atomically; no element is
  ever read out of an array the game owns.
- Fixed decode-failure callbacks running on background threads and touching Unity objects.
- Unowned `RandomSoundPlayer` components are now deferred half a second so their owner can claim
  them first, removing a double-bind and the array churn it caused.
- Container reads (voice-pack list, mission-info array, scene sweep) snapshot in one allocation-free
  pass before any work begins.
- Clip arrays can no longer contain a null element at any point.
- Added IL-level self-audit to the build process.

**1.0.0**
- Initial release.

---

## Credits

Plugin, decoder, mapping and packaging: **StixsworldHD (StixsmasterHD4k)**

Original 2002 audio: *Deadly Dozen*, nFusion Interactive / Infogrames.
Remake: *Deadly Dozen Reloaded*, Rebelmind / Ziggurat Interactive.
This plugin ships no remake assets and modifies no game files — it only redirects playback at
runtime. You need to own the game for any of it to do anything.
