Skip to content

Latest commit

 

History

History
 
 

map

@diablo2/map

Diablo 2 Map generator for v1.13c

Command line

Usage:
    d2-map.exe [D2 Game Path] [options]

Options:
    --seed [-s]          Map Seed
    --difficulty [-d]    Game Difficulty [0: Normal, 1: Nightmare, 2: Hell]
    --act [-a]           Dump a specific act [0: ActI, 1: ActII, 2: ActIII, 3: ActIV, 4: Act5]
    --map [-m]           Dump a specific Map [0: Rogue Encampment ...]
    --verbose [-v]       Increase logging level

Examples:

    # Dump ActI from Normal mode for seed 1122334 
    d2-map.exe /home/diablo2 --seed 1122334 --difficulty 0 --act 0

    # Dump all acts from Hell mode for seed 1122334 
    d2-map.exe /home/diablo2 --seed 1122334 --difficulty 2
{
    /** Level code */
    "id": 74,
    /** Name given back by diablo2 client */
    "name": "Arcane Sanctuary",
    /** how far offset this map is from the top left of the game world */
    "offset": {
        "x": 25000,
        "y": 5000
    },
    /** Dimensions of the map */
    "size": {
        "width": 1000,
        "height": 1000
    },
    /** Important objects / NPCs found in this level */
    "objects": [
        {"id": 53, "type": "exit", "x": 137, "y": 0, "name": "Palace Cellar Level 2" },
        {"id": 250, "type": "npc", "x": 440, "y": 20, "name": "The Summoner"},
        {"id": 371, "type": "npc", "x": 458, "y": 203, "name": "Lightning Spire"},
        {"id": 305, "type": "object", "x": 237, "y": 401, "name": "teleportation pad", "op": 27}
        {"id": 402, "type": "object", "x": 449, "y": 449, "name": "Waypoint", "op": 23},
        {"id": 298, "type": "object", "x": 427, "y": 426, "name": "portal", "op": 34}
    ],
    /** Map Collision data */
    "map": [
        // Map data for x offset 0 - Using run length encoding
        [1, 149] // 1 pixel of collision, 149 pixels of open space, 150 - map.size.width pixels of collision
    ]
}

Map Data

Collision maps are encoded using a simple run length encoding to save on space

Given this small map

[1,5,1],
[2,3,2],
[1,5,1]

It would generate the following word where X is collision and . is open space

X.....X
XX...XX
X.....X

A simple rendering engine could be using a HTMLCanvas's ctx.fillRect(x, y, width, height) function to draw one row at a time

for (let y = 0; y < map.length; y++){
    let x = 0;
    let fill = true;
    for (const offset of row) {
        if (fill) ctx.fillRect(x, y, offset, 1, "black");
        x = x + offset;
        fill = !fill
    }
}

Getting started

Installation

  • npm v16
  • yarn
  • docker

Diablo 2

This Map generation client can be used with

  • Diablo 2 LOD 1.14d & ProjectD2 or
  • Diablo 2 LOD 1.13c

Building from docker image

This is the easiest method to get working:

docker pull blacha/diablo2
docker run -it -v "/E/Games/Diablo II":/app/game docker.io/blacha/diablo2:latest /bin/bash
wine regedit /app/d2.install.reg
wine bin/d2-map.exe game --seed 10 --level 1 --difficulty 0

The last wine command should generate the JSON for one level, this is to test that it works.

Building from source (windows)

From the source code folder: Remember to change "/E/Games/Diablo II" in the below commands to your D2 installation folder.

yarn install
yarn build
cd packages/map
yarn bundle-server
yarn bundle-www
xcopy static dist\www
docker build . -t diablo2/map
docker run -it -v "/E/Games/Diablo II":/app/game diablo2/map /bin/bash
wine regedit /app/d2.install.reg
wine bin/d2-map.exe game --seed 10 --level 1 --difficulty 0
exit

The above wine command should generate the JSON for one level, this is to test that it works. You can try using different seeds, levels and difficulties this way if you like.

Starting the server

Now you run this server so you can send requests for seeds/difficulties to generate all the maps for that given seed:

docker run -v "/E/Games/Diablo II":/app/game -p 8899:8899 diablo2/map

or if you're using the public docker image:

docker run -v "/E/Games/Diablo II":/app/game -p 8899:8899 docker.io/blacha/diablo2:latest

Then you can do a simple curl command to generate:

curl localhost:8899/v1/map/:seed/:difficulty/:act.json

e.g. curl localhost:8899/v1/map/0x3607656c/Normal/ActI.json

Numbers can be used for Act/Difficulty instead

Act 1 in Normal

/v1/map/0x3607656c/0/0.json

Act 5 in Hell

/v1/map/0x3607656c/2/4.json

Server Images

The map server can also generate images

curl localhost:8899/v1/map/:seed/:difficulty/:level.png

Tower cellar level 3 in hell

http://localhost:8899/v1/map/0xff00ff/Hell/23.png

Multiple processes

The server can control multiple map processes, when starting the server the $DIABLO2_CLUSTER_SIZE environment variable controls how many map processes to start.

Fixing offsets

When the diablo 2game client update the offsets need to call change

Inside d2_ptrs.h is a list of offsets needed to initialize the game

The easiest way to trace the initialization steps is to run Diablo inside of wine with WINEDEBUG=+snoop this logs every external call made by the initialization of the game.

export WINEDEBUG=+snoop
wine Game.exe 2> snoop.log

This generates a massive log file however inside are common call patterns

Here is the function call for Fog_10021 this can be used to trace the initialization

0024:CALL Fog.10021(<unknown, check return>) ret=0040829b
...
0024:RET  Fog.10021() retval=00000028 ret=0040829b

Which relates to the FUNCPTR in d2_ptrs.h

FUNCPTR(FOG, 10021, VOID __fastcall, (CHAR * szProg), -10021)

The initialization of D2 always starts with a few Fog.dll calls followed by two D2Win.dll calls

// d2_client.c
FOG_10021("D2");
FOG_10019(DIABLO_2, (DWORD)ExceptionHandler, DIABLO_2_VERSION, 1);
FOG_10101(1, 0);
FOG_10089(1);
if (!FOG_10218()) {
    log_error("InitFailed", lk_s("dll", "Fog.dll"));
    ExitProcess(1);
}

Looking for the final RET Fog.10218 will generally show on the next line the first D2Win.dll CAll

0024:CALL Fog.10218(<unknown, check return>) ret=00407636
0024:RET  Fog.10218() retval=00000001 ret=00407636
0024:CALL D2Win.10086(<unknown, check return>) ret=00407644
...
0024:RET  D2Win.10086() retval=00000001 ret=00407644
0024:CALL D2Win.10005(<unknown, check return>) ret=00407659

So in this client the first D2Win calls are D2Win_10086 and D2Win_10005