There is no trick, no visual indicator, no tile pattern, and no relationship to any other map feature.
The direction is a single pseudorandom number derived from the map seed. It cannot be predicted in-game.
The Summoner direction is assigned in a single function called immediately after the maze is grown.
It lives in D2Common.dll, in what the original source called Maze.cpp
(the path ..\Source\D2Common\DRLG\Maze.cpp is embedded in the binary as an assert string).
; ---- Post-MazeGrow Endpoint Assignment ---- ; Called after the maze corridors are fully generated. ; Picks one of 4 directions for the Summoner platform. push ecx mov eax, [esi+0x1C4] ; load PRNG seed (low 32 bits) mov ecx, 0x6AC690C5 ; LCG multiplier constant mul ecx ; edx:eax = seed_lo * multiplier mov ecx, [esi+0x1C8] ; load PRNG seed (high 32 bits) push edi xor edi, edi add eax, ecx ; new_lo = (seed_lo * mult) + seed_hi adc edx, edi ; new_hi = overflow carry mov [esi+0x1C8], edx ; store new seed high mov [esi+0x1C4], eax ; store new seed low and eax, 3 ; direction = seed_lo & 3 (0, 1, 2, or 3) lea edx, [esp+4] push edx mov edx, eax shl edx, 4 ; direction * 16 (struct size) add edx, 0x6FDE6888 ; index into direction preset table push esi mov [esp+0xC], eax ; save direction for later call 0x6FD606B0 ; PlaceEndpoints(maze, &table[direction])
void AssignSummonerDirection(MazeData* maze) { // Step the 64-bit LCG uint64_t seed = (uint64_t)maze->seed_lo * 0x6AC690C5 + maze->seed_hi; maze->seed_lo = (uint32_t)(seed & 0xFFFFFFFF); maze->seed_hi = (uint32_t)(seed >> 32); // Pick direction: bottom 2 bits int direction = maze->seed_lo & 3; // 0=N, 1=E, 2=S, 3=W // Look up presets for this direction and assign endpoints DirectionPreset* preset = &g_directionTable[direction]; PlaceEndpoints(maze, preset); // For level 0x23 (35), place a second set of endpoints if (maze->levelId == 0x23) { DirectionPreset* preset2 = &g_directionTable2[direction]; PlaceEndpoints(maze, preset2); } }
seed & 3. Two bits of pseudorandom state.
No external input, no conditional logic, no level-specific override. The same PRNG that grew the maze
corridors is stepped once more, and the bottom two bits pick the Summoner arm.
The direction value (0-3) indexes into a hardcoded table at 0x6FDE6888 in D2Common.dll's
.rdata section. Each entry is 16 bytes (4 DWORDs):
// At VA 0x6FDE6888 in D2Common.dll (.rdata) struct DirectionPreset { int dead_end_def; // LvlPrest Def for the dead-end piece int summoner_def; // LvlPrest Def for the Summoner platform int sentinel; // -1 (terminator) int opposite_dir; // opposite direction index };
| seed & 3 | Direction | Dead-End Def (v1.13c / D2R) | Summoner Def (v1.13c) | Opposite |
|---|---|---|---|---|
| 0 | North | 265 / 517 (sanctN.ds1) | 294 (summN.ds1) | 3 (West) |
| 1 | East | 259 / 511 (sanctE.ds1) | 292 (summE.ds1) | 0 (North) |
| 2 | South | 261 / 513 (sanctS.ds1) | 293 (summS.ds1) | 1 (East) |
| 3 | West | 258 / 510 (sanctW.ds1) | 291 (summW.ds1) | 2 (South) |
The PlaceEndpoints function (at 0x6FD606B0) walks the completed maze,
finds each dead-end room, checks which direction it faces, and assigns it either the Summoner platform
preset or the dead-end preset from the table. The arm matching the selected direction gets the
Summoner; the other three arms get dead-ends.
Diablo 2 uses a 64-bit Linear Congruential Generator throughout its map generation system. The same PRNG drives corridor placement, room branching, and endpoint assignment.
// D2's map PRNG -- used everywhere in the DRLG // Multiplier: 0x6AC690C5 (decimal 1791398085) // State: 64-bit split across two 32-bit fields uint32_t seed_lo = maze->seed_lo; uint32_t seed_hi = maze->seed_hi; // One PRNG step uint64_t product = (uint64_t)seed_lo * 0x6AC690C5; seed_lo = (uint32_t)(product + seed_hi); seed_hi = (uint32_t)((product + seed_hi) >> 32); // Extract result (varies by use) int direction = seed_lo & 3; // 2 bits for direction int room_index = seed_lo % count; // modulo for room selection
The data files confirm there is nothing to see:
All 4 arms draw from the same pool of corridor pieces (sanctNS, sanctEW, sanctNE, etc.). The maze generator picks variants randomly. There is no "Summoner arm corridor" piece.
LvlSub.txt has zero entries for the Arcane Sanctuary.
No tile swaps or visual variations are applied based on direction.
The center junction (sanctNSEW4.ds1) contains the portal and waypoint at
fixed positions. It looks identical regardless of which arm has the Summoner.
AutoMap.txt defines the same tile mappings for all Arcane floor and wall types.
No directional automap indicator exists.
| Property | Dead-End (sanctN/S/E/W) | Summoner (summN/S/E/W) |
|---|---|---|
| File size | 3,012 bytes | 4,304 bytes |
| Wall layers | 1 | 2 |
| Objects | GoldProxy, PlaceUniqueChest, FloorTrap | ArcaneTome, 6x PlaceArcaneThingamajig |
| Monsters | 2x wraith4 (generic) | 1x Summoner spawn point |
These differences are only apparent once you reach the end of an arm. From the center, all four arms are visually and structurally identical.
The Arcane Sanctuary consists of 61 rooms of 12x12 tiles, built from DS1 preset pieces stored
in data/global/tiles/ACT2/Arcane/. A total of 53 DS1 files define the maze:
| Piece Type | Count | Connectivity | Variants |
|---|---|---|---|
| Dead-end (sanctN/S/E/W) | 4 | Single exit | 1 each (fixed) |
| Summoner (summN/S/E/W) | 4 | Single exit | 1 each (fixed) |
| Corridor (NS, EW) | 2 | Straight-through | 3-4 each |
| Corner (NE, NW, SE, SW) | 4 | 90-degree turn | 3-4 each |
| T-junction (NSE, NSW, NEW, SEW) | 4 | 3-way | 3-4 each |
| 4-way (NSEW) | 1 | 4-way | 5 variants (one has portal + waypoint) |
All tiles use a single tileset: Act2/Arcane/Sanctuary.dt1.
The LvlPrest.txt Scan flag is only set on the NSEW junction piece (Def 524),
meaning it gets automap coverage, but no directional data is encoded.
# Dead-end pieces: 11 objects each # All four are structurally identical (rotated) sanctN.ds1: 5x GoldProxy, 3x PlaceUniqueChest, 1x FloorTrap, 2x wraith4 sanctS.ds1: 5x GoldProxy, 3x PlaceUniqueChest, 1x FloorTrap, 2x wraith4 sanctE.ds1: 5x GoldProxy, 2x PlaceUniqueChest, 1x LargeChestR, 1x FloorTrap, 2x wraith4 sanctW.ds1: 5x GoldProxy, 3x PlaceUniqueChest, 1x FloorTrap, 2x wraith4 # Summoner pieces: 8 objects each summN.ds1: 6x PlaceArcaneThingamajig, 1x ArcaneTome, 1x monster_spawn summS.ds1: 6x PlaceArcaneThingamajig, 1x ArcaneTome, 1x monster_spawn summE.ds1: 6x PlaceArcaneThingamajig, 1x ArcaneTome, 1x monster_spawn summW.ds1: 6x PlaceArcaneThingamajig, 1x ArcaneTome, 1x monster_spawn
Multiple independent efforts have reached the same conclusion through empirical testing:
All of these findings are consistent with what the disassembled code shows: rand() & 3,
with no side-channel or observable indicator. Credit is due to these researchers for years of work
arriving at the same conclusion from the empirical side that the disassembly now confirms from
the code side.
This analysis was only possible because classic D2's DLLs are unprotected. D2R uses a completely different protection scheme:
| Property | Classic D2 (v1.13c) | D2R (v3.1) |
|---|---|---|
| Binary format | Separate DLLs (D2Common.dll, D2Game.dll, etc.) | Single executable (D2R.exe) |
| Code section entropy | 6.45 (normal compiled code) | 8.00 (maximum -- fully encrypted) |
| Protection | None | Eidolon (Blizzard anti-tamper) |
| Disassembly | Possible | Impossible (static) |
| Loader | Standard Windows PE | D2R_loader.dll (exports: eidolon_run) |
| Data section (.rdata) | Readable | Readable (strings intact, code encrypted) |
The D2R executable's .rdata section is not encrypted, which allowed us to find
string references like "killed summoner for quest", "DrlgType", and
"LvlMaze". But the actual code that uses these strings is in the encrypted
.text section and cannot be statically analyzed.
The classic D2 v1.13c DRLG algorithm is believed to be functionally identical to D2R's, as the maze generation system was not rewritten for the remaster. The same LvlPrest/LvlMaze/LvlTypes data files drive both versions (with updated Def numbering).