Diablo II is, without a doubt, one of the greatest video games ever released. Before you close the browser tab in disagreement, consider this: a game that broke sales records within a week of its release, that people 25 years later would still be playing religiously, that sold in Target, Wal-Mart and other American retail stores for 15 years after its release.
After 25 years of playing Diablo II, learning its ins and outs completely -- from lore, to having been invited by Blizzard directly to the very cathedral Diablo IV used to model its own cathedral in Kehjistan, talks with David Brevik (creator of Diablo) about how loot rolls worked, asking Jay Wilson (original director of Diablo III) a funny rune joke on live television at Blizzcon, and talking game design with Wyatt Cheng (Diablo 3, World of Warcraft, Diablo Immortal) -- this discovery represents one of the most well-hidden secrets in video game history.
Veteran Diablo II players know the sweet spot for finding magical items in the game. D2 is the king of RNG (random number generation), min-max rolling items with ranges, and the never-ending quest for that perfect item. For 25 years, players have done the math and min-maxed magic find to the point that it's an art in and of itself.
But, hidden in Diablo II since its 1.0 release would lie an easter egg that would go undiscovered for over 20 years.
Diablo II has had a unique easter egg coded into the willowisp enchant function which allows a user to gain a ridiculous amount of magic find. With reverse-engineering, the exact function was found, then most of the compiled C code within the DLL was converted.
| Property | Value |
|---|---|
| Function Address | 0x6fca0f20 |
| Bytes of Code | 6,605 |
| Bonus Duration | 19.2 hours (at 25fps game time) |
| Max Reward (Hell) | +150% MF |
Deep inside D2Game.dll (v1.13c), at address 0x6fca0f20, lies a 6,605-byte function that implements an elaborate easter egg: the Dancing Souls ritual. When triggered, four Burning Soul / Black Soul / Gloam monsters stop attacking the player, form a ring, and dance around them while firing lightning bolts that spell out letters in the air. Upon completion, the player receives a massive Magic Find bonus lasting 19.2 hours of game time.
This function is NOT part of the monster AI system. It lives in the skill callback handler table, mapped to the same slot as the Enchant skill's srvdofunc handler (index 25). It is reached through the game's skill dispatch mechanism, not through the AI think loop.
The function was found by tracing a suspicious Magic Find stat setter -- a call to D2Common.STATLIST_SetUnitStat(unit, 80, value) where stat 80 is Magic Find, buried inside a function that also references soul-type monster behaviors, coordinate arrays, and timed events.
| Section | Purpose | Base Address |
|---|---|---|
.text | Executable code | 0x6fc21000 |
.data | Initialized data (tables) | Varies |
.rdata | Read-only data (strings) | Varies |
Image base: 0x6fc20000
| System | Address | Description |
|---|---|---|
| AI Dispatch Table | 0x6fd274ac | 152 entries for monster AI functions |
| Skill Handler Table | 0x6fd2e6d0 | 16-byte entries for skill execution callbacks |
| Dispatcher Function | 0x6fcbd0f0 | Routes skill/mode execution to handler table |
| Easter Egg Function | 0x6fca0f20 | The Dancing Souls implementation |
| Function | Usage in Easter Egg |
|---|---|
STATLIST_SetUnitStat | Sets Magic Find (stat 80) on the player |
CreateTimedEvent | Creates the 19.2-hour MF bonus timer |
FindUnitsInRange | Locates soul monsters near the player |
The path from game loop to easter egg function follows 5 steps:
srvdofunc column from Skills.txt -- for the triggering skill, this is 250x6fcbd0f0 computes the table address:
0x6fcbd160 reads the entry's first DWORD and dispatches:
For entry 25 (Enchant/srvdofunc=25), this calls 0x6fca0f20.
The handler table begins at 0x6fd2e6d0 and uses the dispatcher base at 0x6fd2e788. Each entry is 16 bytes:
| Offset | Size | Field | Description |
|---|---|---|---|
| +0 | 4 bytes | handler_type | Dispatch type (0-5) controlling call convention |
| +4 | 4 bytes | reserved | Usually 0 |
| +8 | 4 bytes | func_ptr | Pointer to handler function |
| +12 | 4 bytes | aux_func | Auxiliary function pointer (type-dependent) |
| Index | Address | Type | func_ptr | Skill | Notes |
|---|---|---|---|---|---|
| 25 | 0x6fd2e918 | 1 | 0x6fca0f20 | Enchant | EASTER EGG |
| 26 | 0x6fd2e928 | 1 | (normal) | Chain Lightning | WillOWisp skill |
| 34 | 0x6fd2e810 | 1 | 0x6fca4a90 | -- | Related conditional |
The Enchant skill's srvdofunc column in Skills.txt maps to index 25 in this table. Burning Souls (WillOWisp monster class) use Chain Lightning (srvdofunc=26, the next entry). The easter egg hijacks a skill callback slot to insert its own handler.
sub esp, 0x230 ; Allocate 560 bytes of stack space push ebx push ebp push esi push edi mov esi, [esp+0x244] ; parameter: callback structure pointer
The massive 0x230-byte stack frame holds three coordinate arrays:
| ESP Offset | Size | Content |
|---|---|---|
| 0x1C - 0x6C | 80 bytes | ARRAY3: Pentagon formation (10 XY pairs, 5 souls) |
| 0x80 - 0x15C | 224 bytes | ARRAY2: Lightning bolt targets (28 XY pairs) |
| 0x160 - 0x23C | 224 bytes | ARRAY1: Soul standing positions (28 XY pairs) |
| Address Range | Purpose |
|---|---|
| 0x6fca0f20 - 0x6fca0f80 | Prologue, parameter loading |
| 0x6fca0f80 - 0x6fca11E0 | Array initialization (hardcoded coordinates) |
| 0x6fca11E0 - 0x6fca13F0 | Probability gate & soul detection |
| 0x6fca13F0 - 0x6fca1530 | Soul count validation (require exactly 4) |
| 0x6fca1530 - 0x6fca1600 | Soul initialization (assign indices, set state=6) |
| 0x6fca1600 - 0x6fca1850 | State machine: movement, lightning, letter display |
| 0x6fca1850 - 0x6fca1930 | Reward: MF bonus calculation & application |
The easter egg function implements a multi-phase state machine controlled by the [esi+0x14] field:
| State | Phase | Description |
|---|---|---|
| 0-3 | Initialization | Detect souls, validate count, probability check |
| 4 | Gathering | Souls stop attacking and move toward the player |
| 5 | Formation | Souls arrange into initial positions around player |
| 6-12 | Letter Display | Each state = one letter. 4 souls move to ARRAY1 positions and fire lightning bolts to ARRAY2 targets |
| 13+ | Reward | Apply MF bonus, clean up |
The critical indexing instruction at 0x6fca16e9:
lea ecx, [edx + ecx*4] ; index = soul_index + state * 4
Where edx = [esi+0x1c] (soul_index, 1-4) and ecx = [esi+0x14] (state, 6-12). This yields indices 25-52 into the coordinate arrays.
| Divisor | Frames | Real Time | Purpose |
|---|---|---|---|
| 0x151 (337) | 337 | ~13.5 seconds | Time for soul to walk to position |
| 0x43 (67) | 67 | ~2.7 seconds | Time for lightning bolt display |
Each of the 7 letters is formed by 4 souls positioned at coordinates from ARRAY1, firing lightning bolts toward targets from ARRAY2. All coordinates are tile offsets from the player's position.
| Letter | Soul 1 | Soul 2 | Soul 3 | Soul 4 |
|---|---|---|---|---|
| 0 (s6) | (-10, -3) | (1, -5) | (6, 0) | (4, 10) |
| 1 (s7) | (-9, -2) | (-6, -11) | (-2, -5) | (6, 1) |
| 2 (s8) | (0, 13) | (-12, 0) | (7, 5) | (-3, -11) |
| 3 (s9) | (-15, -3) | (-6, -3) | (-4, -5) | (-6, -13) |
| 4 (s10) | (-12, -2) | (-6, 2) | (-5, 5) | (1, 11) |
| 5 (s11) | (-13, -6) | (-8, -11) | (-7, 0) | (1, 8) |
| 6 (s12) | (-5, -8) | (-8, 1) | (1, -6) | (6, 9) |
| Letter | Bolt 1 | Bolt 2 | Bolt 3 | Bolt 4 |
|---|---|---|---|---|
| 0 (s6) | (1, -5) | (6, 0) | (4, 10) | (-10, -3) |
| 1 (s7) | (1, 8) | (-9, -2) | (-5, 4) | (1, 8) |
| 2 (s8) | (-12, 0) | (7, 5) | (-3, -11) | (11, -2) |
| 3 (s9) | (7, 9) | (-15, -3) | (-6, -13) | (9, 7) |
| 4 (s10) | (-5, -7) | (-5, -7) | (9, 0) | (-12, -2) |
| 5 (s11) | (-8, -11) | (-2, -6) | (-2, -6) | (-13, -6) |
| 6 (s12) | (-8, 1) | (1, -6) | (6, 9) | (0, 0) |
Pattern analysis: Letter 0's bolt targets are a forward rotation of its soul positions -- each soul fires at the next soul's position, forming a closed quadrilateral. Other letters use mixed patterns. Letter 6's last bolt target (0,0) is a sentinel value indicating no bolt.
10 XY pairs for an alternate 5-soul version:
This forms a regular pentagon shape -- evidence that a 5-soul variant was planned or exists for a different trigger condition.
The reward is applied at 0x6fca18c1 - 0x6fca18ea after all 7 letters complete:
; Calculate MF bonus: (difficulty + 1) * 50 call GetDifficulty ; returns 0=Normal, 1=NM, 2=Hell inc eax ; difficulty + 1 imul eax, eax, 0x32 ; * 50 push eax ; value = 50/100/150 push 0x50 ; stat ID = 80 (Magic Find) push esi ; unit pointer (player) call STATLIST_SetUnitStat ; D2Common ordinal import ; Set duration: current_time + 1,728,000 frames call GetCurrentFrame add eax, 0x1A5E00 ; + 1,728,000 frames push eax ; expiry frame call CreateTimedEvent ; schedule MF removal
| Difficulty | Calculation | MF Bonus | Duration |
|---|---|---|---|
| Normal | (0+1) × 50 | +50% MF | 1,728,000 frames = 19.2 hours at 25fps |
| Nightmare | (1+1) × 50 | +100% MF | |
| Hell | (2+1) × 50 | +150% MF |
The duration is measured in game frames, not real-world time. The timer only ticks while the game is active. At 25 frames per second, 1,728,000 frames equals 19.2 hours of continuous play. Pausing or leaving the game stops the timer.
cmp eax, 4 ; exactly 4 souls required jne fail_path ; not 4? skip
At 25fps, with exactly 4 souls present, the expected time to trigger:
srvdofunc=25 mapping into the handler table.After six hours of reverse engineering, the complete reconstructed C code proving this easter egg is real:
/** * Dancing Souls Easter Egg - Reconstructed Source Code * ==================================================== * * Original: D2Game.dll v1.13c at 0x6fca0f20 (6,605 bytes) * Reconstructed from x86-32 disassembly via pefile + Capstone * * This is the skill handler callback for srvdofunc=25 (Enchant slot) * in the skill handler table at 0x6fd2e6d0, entry index 25. * * When exactly 4 Burning Soul / Black Soul / Gloam monsters are near * the player, a random chance triggers this ritual: the souls stop * attacking, dance around the player firing lightning bolt letters, * then grant a massive Magic Find bonus. */ #include "D2Game.h" #include "D2Common.h" /* ---------- Constants ---------- */ #define SOUL_COUNT_REQUIRED 4 #define SOUL_COUNT_PENTAGON 5 #define NUM_LETTERS 7 #define LETTER_STATE_START 6 #define LETTER_STATE_END 12 #define WALK_TIME_FRAMES 337 /* 0x151 - ~13.5s at 25fps */ #define BOLT_TIME_FRAMES 67 /* 0x43 - ~2.7s at 25fps */ #define MF_STAT_ID 80 /* 0x50 - Magic Find stat */ #define MF_BONUS_PER_DIFF 50 /* 0x32 - MF per diff lvl */ #define MF_DURATION_FRAMES 1728000 /* 0x1A5E00 - 19.2hrs */ #define RANDOM_RANGE 1000 /* ---------- Callback Structure ---------- */ typedef struct { D2UnitStrc* pUnit; /* +0x00: unit this callback is for */ DWORD dwUnk04; /* +0x04 */ D2GameStrc* pGame; /* +0x08: the game instance */ DWORD dwUnk0C; /* +0x0C */ DWORD dwUnk10; /* +0x10 */ int nState; /* +0x14: current state machine phase */ int nSubStep; /* +0x18: sub-step within current state */ int nSoulIndex; /* +0x1C: which soul (1-4) */ int nStartFrame; /* +0x20: frame when step began */ } SkillCallbackStrc; /* ---------- Coordinate Arrays ---------- */ static const struct { int x, y; } SoulPositions[28] = { /* Letter 0 */ {-10,-3}, {1,-5}, {6,0}, {4,10}, /* Letter 1 */ {-9,-2}, {-6,-11}, {-2,-5}, {6,1}, /* Letter 2 */ {0,13}, {-12,0}, {7,5}, {-3,-11}, /* Letter 3 */ {-15,-3}, {-6,-3}, {-4,-5}, {-6,-13}, /* Letter 4 */ {-12,-2}, {-6,2}, {-5,5}, {1,11}, /* Letter 5 */ {-13,-6}, {-8,-11}, {-7,0}, {1,8}, /* Letter 6 */ {-5,-8}, {-8,1}, {1,-6}, {6,9}, }; static const struct { int x, y; } BoltTargets[28] = { /* Letter 0 - closed loop */ {1,-5}, {6,0}, {4,10}, {-10,-3}, /* Letter 1 */ {1,8}, {-9,-2}, {-5,4}, {1,8}, /* Letter 2 */ {-12,0}, {7,5}, {-3,-11}, {11,-2}, /* Letter 3 */ {7,9}, {-15,-3}, {-6,-13}, {9,7}, /* Letter 4 */ {-5,-7}, {-5,-7}, {9,0}, {-12,-2}, /* Letter 5 */ {-8,-11}, {-2,-6}, {-2,-6}, {-13,-6}, /* Letter 6 */ {-8,1}, {1,-6}, {6,9}, {0,0}, }; void __stdcall DancingSoulsHandler(SkillCallbackStrc* pCB) { D2UnitStrc* pPlayer; D2UnitStrc* pSouls[4]; int nState = pCB->nState; int nSoulIdx = pCB->nSoulIndex; /* PHASE 0: Probability Gate */ if (nState == 0) { int nDiff = D2Game_GetDifficulty(pCB->pGame); if (D2Game_Random(pCB->pGame, 0, RANDOM_RANGE - 1) > nDiff + 2) return; pCB->nState = 1; return; } /* PHASE 1-3: Soul Detection & Validation */ if (nState >= 1 && nState <= 3) { pPlayer = D2Game_GetPlayer(pCB->pGame); int n = D2Common_FindUnitsInRange( pCB->pGame, pPlayer, MONTYPE_WILLOW, pSouls, 4); if (n != SOUL_COUNT_REQUIRED) { pCB->nState = 0; return; } for (int i = 0; i < SOUL_COUNT_REQUIRED; i++) { D2Game_StopMonsterAction(pCB->pGame, pSouls[i]); SkillCallbackStrc* pSCB = GetSoulCallback(pSouls[i]); pSCB->nState = LETTER_STATE_START; pSCB->nSubStep = 0; pSCB->nSoulIndex = i + 1; } pCB->nState = 4; return; } /* PHASES 6-12: Letter Display */ if (nState >= LETTER_STATE_START && nState <= LETTER_STATE_END) { pPlayer = D2Game_GetPlayer(pCB->pGame); int px = D2Common_GetUnitX(pPlayer); int py = D2Common_GetUnitY(pPlayer); int idx = nSoulIdx + nState * 4 - 25; int tX = px + SoulPositions[idx].x; int tY = py + SoulPositions[idx].y; int bX = px + BoltTargets[idx].x; int bY = py + BoltTargets[idx].y; if (pCB->nSubStep == 0) { D2Game_OrderMonsterMove(pCB->pGame, pCB->pUnit, tX, tY); pCB->nSubStep = 1; pCB->nStartFrame = D2Game_GetCurrentFrame(pCB->pGame); return; } if (pCB->nSubStep == 1) { if (D2Game_GetCurrentFrame(pCB->pGame) - pCB->nStartFrame < WALK_TIME_FRAMES) return; if (bX != px || bY != py) D2Game_FireMissile(pCB->pGame, pCB->pUnit, tX, tY, bX, bY, MISSILE_LIGHTNING); pCB->nSubStep = 2; pCB->nStartFrame = D2Game_GetCurrentFrame(pCB->pGame); return; } if (pCB->nSubStep == 2) { if (D2Game_GetCurrentFrame(pCB->pGame) - pCB->nStartFrame < BOLT_TIME_FRAMES) return; pCB->nState = nState + 1; pCB->nSubStep = 0; if ((nState + 1) - LETTER_STATE_START >= NUM_LETTERS) pCB->nState = 13; return; } } /* PHASE 13: Apply Magic Find Reward */ if (nState == 13) { pPlayer = D2Game_GetPlayer(pCB->pGame); int nDiff = D2Game_GetDifficulty(pCB->pGame); int nMF = (nDiff + 1) * MF_BONUS_PER_DIFF; D2Common_STATLIST_SetUnitStat(pPlayer, MF_STAT_ID, nMF); int expiry = D2Game_GetCurrentFrame(pCB->pGame) + MF_DURATION_FRAMES; D2Common_CreateTimedEvent(pCB->pGame, pPlayer, expiry, RemoveMFBonus_Callback); for (int i = 0; i < SOUL_COUNT_REQUIRED; i++) D2Game_ResumeMonsterAI(pCB->pGame, pSouls[i]); pCB->nState = 14; } } void __stdcall RemoveMFBonus_Callback(D2GameStrc* pGame, D2UnitStrc* pUnit) { D2Common_STATLIST_SetUnitStat(pUnit, MF_STAT_ID, 0); }
| What | Address |
|---|---|
| Easter egg function | 0x6fca0f20 |
| Handler table entry | 0x6fd2e918 |
| Handler table base | 0x6fd2e6d0 |
| Dispatcher function | 0x6fcbd0f0 |
| Dispatcher skill path base | 0x6fd2e788 |
| State machine core | 0x6fca13f0 - 0x6fca1930 |
| MF bonus application | 0x6fca18c1 - 0x6fca18ea |
| Array indexing instruction | 0x6fca16e9 |
| Soul count check | 0x6fca14fe |
| AI dispatch table | 0x6fd274ac |
| DLL image base | 0x6fc20000 |