Diablo 1 Source Code

f you are behind the times some people used WebAssembly to let you play diablo 1 in your browser cross platform

https://d07riv.github.io/diabloweb/

If you click around the GIT your linked to the actual C code of diablo 1.

https://github.com/d07RiV/devilution

If you download VS2017 and plop the MPQ file into your root project folder, well…..
we’ll see how this goes.

ok off to a great start.
1.) download the source into a folder
2.) Download install VS2017. Open the project from step 1.
3.) get diablo 1 from SIG or whatever, and plop the MPQ data file into your project folder..
4.) Hit run

Goal #1…..
When you cast a spell. instead of casting that spell…. cast every spell



https://github.com/d07RiV/devilution/blob/master/Source/spells.cpp
Modify the CASTSPELL function to loop through every single spell.


I got tired of casting 50 spells and exploding to death.

so I said


if I am dead

no


Within the player class we have

void SyncPlrKill(int pnum, int earflag) { int ma, i; SetPlayerHitPoints(pnum, 9999); return;


The code isn’t parameterized. It’s like they didn’t understand OOO.
There are 3 DO_WALK functions that are basically the same code with a different param.


https://github.com/d07RiV/devilution/blob/master/Source/player.cpp

each walk function has a

anim_len = 8;

Where if we set it to 1 instead, we go ‘8 times faster’. So twice as fast would be 4.

There are a few things to mention. The structure on how ‘fast’ characters go is based on if they are in town or not.

The logic is, if they are in town they all ahve one set velocity.
If they are not, then its class bassed.
Well, every class + the default town velocity is exactly the same.
Maybe code changes it later to go faster?

However, increasing the velocity increases how fast you go – but its programmed to say when you reach the end of an animation, set your global position to the next ‘square’. So if I set my velocity to increase how fast I am going at the end of the walk cycle I will only ever traverse 1 square.
Now you can increase that proportionally to the velocity, but it doesn’t seem to do hitdetection or pathing correctly as they are all tightly coupled together.
However, just shortening the overall animation frame these all operate under effectively speeds up the walk speed.


We can increase the framerate really easily too – but first
https://docs.microsoft.com/en-us/windows/win32/api/sysinfoapi/nf-sysinfoapi-gettickcount
The elapsed time is stored as a DWORD value. Therefore, the time will wrap around to zero if the system is run continuously for 49.7 days.


I think if you run 32 bit diablo, after 49.7 days the game will explode LOL


I searched the code for keywords such as ‘tick’ ‘framerate’ ‘counter’ and ‘clock’.

All came back too, and eventually I realized you just need to adjust the sleep timer in the main do{ }while loop

https://github.com/d07RiV/devilution/blob/master/Source/main.cpp

while (GameState::current()) { if (PeekMessage(&msg, NULL, 0, 0, PM_NOREMOVE)) { SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL); while (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message == WM_QUIT) { gbRunGameResult = FALSE; gbRunGame = FALSE; GameState::activate(nullptr); break; } TranslateMessage(&msg); DispatchMessage(&msg); } bool bLoop = gbRunGame;// && nthread_has_500ms_passed(FALSE); SetThreadPriority(GetCurrentThread(), THREAD_PRIORITY_NORMAL); if (!bLoop) { continue; } } DApi_Render(GetTickCount()); Sleep(50); }

50 MS is 20 FPS? maybe? So make it 10 MS thats 100 FPS.. or..

The whole game runs faster. If you just want it tied to animation that might be a big effort.

Eventually tracing the tick tock clock we find it calling the game logic in a nice function

void game_logic()
{
if (PauseMode == 2) {
return;
}
if (PauseMode == 1) {
PauseMode = 2;
}
if (gbMaxPlayers == 1 && gmenu_exception()) {
drawpanflag |= 1;
return;
}

if (!gmenu_exception() && sgnTimeoutCurs == 0) {
    CheckCursMove();
    track_process();
}
if (gbProcessPlayers) {
    ProcessPlayers();
}
if (leveltype != DTYPE_TOWN) {
    ProcessMonsters();
    ProcessObjects();
    ProcessMissiles();
    ProcessItems();
    ProcessLightList();
    ProcessVisionList();
} else {
    ProcessTowners();
    ProcessItems();
    ProcessMissiles();
}

//#ifdef _DEBUG
// if (debug_mode_key_inverted_v && GetAsyncKeyState(KeyCode::SHIFT) & 0x8000) {
// ScrollView();
// }
//#endif

sound_update();
ClearPlrMsg();
CheckTriggers();
CheckQuests();
drawpanflag |= 1;
pfile_update(FALSE);

}

If we increase how fast the game runs, but then add a timer on when to actually call monster / player logic such that it allows animations but does not increase AI / Damage / movement ticks you might be able to increase just the animations of the things drawn.

I will certainly attempt something

Now the logic is for every player and monster, only process logic unrelated to animation every 5th ‘tick’.
Animations are free.

This is why your walking animation and attacking animation are going nuts.
You could then modify the animations to increase distance between step and stride animations.


https://github.com/d07RiV/devilution/blob/master/Source/player.cpp

We modify the processPlayers function (same for monsters in monster.cpp) such that all logic is untouched, but we carefully wrap all non-animation logic in a checker to see.

void ProcessPlayers() {
procCtr += 1;

int pnum;
BOOL tplayer;
if (procCtr == 5) {

if ((DWORD) myplr >= MAX_PLRS) {
  app_fatal("ProcessPlayers: illegal player %d", myplr);
}

if (plr[myplr].pLvlLoad > 0) {
  plr[myplr].pLvlLoad--;
}

if (sfxdelay > 0) {
  sfxdelay--;
  if (sfxdelay == 0) {
    PlaySFX(sfxdnum);
  }
}

ValidatePlayer();

}
for (pnum = 0; pnum < MAX_PLRS; pnum++) {
if (plr[pnum].plractive && currlevel == plr[pnum].plrlevel && (pnum == myplr || !plr[pnum]._pLvlChanging)) {
if (procCtr == 5) {
CheckCheatStats(pnum);

    if (!PlrDeathModeOK(pnum) && (plr[pnum]._pHitPoints >> 6) <= 0) {
      SyncPlrKill(pnum, -1);
    }

    if (pnum == myplr) {

      if ((plr[pnum]._pIFlags & ISPL_DRAINLIFE) && currlevel != 0) {
        plr[pnum]._pHitPoints -= 4;
        plr[pnum]._pHPBase -= 4;
        if ((plr[pnum]._pHitPoints >> 6) <= 0) {
          SyncPlrKill(pnum, 0);
        }
        drawhpflag = TRUE;
      }
      if (plr[pnum]._pIFlags & ISPL_NOMANA && plr[pnum]._pManaBase > 0) {
        plr[pnum]._pManaBase -= plr[pnum]._pMana;
        plr[pnum]._pMana = 0;
        drawmanaflag = TRUE;
      }
    }

    tplayer = FALSE;
    do {
      switch (plr[pnum]._pmode) {
      case PM_STAND:
        tplayer = PM_DoStand(pnum);
        break;
      case PM_WALK:
        tplayer = PM_DoWalk(pnum);
        break;
      case PM_WALK2:
        tplayer = PM_DoWalk2(pnum);
        break;
      case PM_WALK3:
        tplayer = PM_DoWalk3(pnum);
        break;
      case PM_ATTACK:
        tplayer = PM_DoAttack(pnum);
        break;
      case PM_RATTACK:
        tplayer = PM_DoRangeAttack(pnum);
        break;
      case PM_BLOCK:
        tplayer = PM_DoBlock(pnum);
        break;
      case PM_SPELL:
        tplayer = PM_DoSpell(pnum);
        break;
      case PM_GOTHIT:
        tplayer = PM_DoGotHit(pnum);
        break;
      case PM_DEATH:
        tplayer = PM_DoDeath(pnum);
        break;
      case PM_NEWLVL:
        tplayer = PM_DoNewLvl(pnum);
        break;
      }
      CheckNewPath(pnum);
    } while (tplayer);
  }
  plr[pnum]._pAnimCnt++;
  if (plr[pnum]._pAnimCnt > plr[pnum]._pAnimDelay) {
    plr[pnum]._pAnimCnt = 0;
    plr[pnum]._pAnimFrame++;
    if (plr[pnum]._pAnimFrame > plr[pnum]._pAnimLen) {
      plr[pnum]._pAnimFrame = 1;
    }
  }
}

}

if (procCtr == 5) {
procCtr = 0;
}
}


Ideally you would do the same with objects, missles, spells.
Animate 60 FPS but don’t do other things.

make spells return a cost of 0 mana,

shoot thm in 3 directions instead of just 1….

and…

Now we will attempt a 1080p mod..

Here we go.

First we want to tell the HWND to draw a bigger rectangle for our game.
https://github.com/d07RiV/devilution/blob/master/Source/main.cpp

RECT rc = {0, 0, 640, 480};
to
RECT rc = {0, 0, 1920, 1080};

Ok step 1 of a million is done.
All we did was say draw the window bigger. But all of the graphics stuff is still the same. no one told it to draw more screen stuff. For now, I wil ignore the movie that plays in the beginning, and the main menus, and concentrate on getting the actual game to be bigger.

Before I move on fromt his feature, I won’t leave it hard coded. I’ll modify SCREEN_WIDTH and SCREEN_HEIGHT here.
https://github.com/d07RiV/devilution/blob/master/Source/defs.h

Then use them.
RECT rc = {0, 0, SCREEN_WIDTH, SCREEN_HEIGHT};


next up is
void T_DrawGame(int x, int y)
within :
https://github.com/d07RiV/devilution/blob/master/Source/town.cpp

Everything tied to the town drawing is here.
First we want to draw our shit in the center of the screen not the top left.

sx = ScrollInfo._sxoff + 64; sy = ScrollInfo._syoff + 175;
will change to

sx = ScrollInfo._sxoff + ((SCREEN_WIDTH / 2) – 200); sy = ScrollInfo._syoff + ((SCREEN_HEIGHT / 2) + 100);



The top left has a rendering of what was last drawn from the loading screen. I move the rendering box of the level to the center of my world, but that never gets drawn as theres an area the game determines is off screen and doe snot draw my level tiles.
We still see that the character is drawn here, but since there’s nothing re-drawing over him every frame we see his animations stack ontop of eachother.

The health /mana etc stuff is all seperate entitie from the town.cpp Tile set. we have yet to move that. so there isa lot going on in that picture.

So the game actively codes to draw to a certain section a certain amount of tiles.

We are here to say no thanks, draw more. (green vs red.)

We take a look at the culprit for controlling that stuff.
https://github.com/d07RiV/devilution/blob/master/Source/render.cpp

RenderLine() function has…

ifdef NO_OVERDRAW

if(zoomflag) {
    if((*dst) < &gpBuffer[(1) * BUFFER_WIDTH]
    || (*dst) > &gpBuffer[(SCREEN_HEIGHT) * BUFFER_WIDTH]) {
        (*src) += n;
        (*dst) += n;
        return;
    }
} else {
    if((*dst) < &gpBuffer[(-17 + 160) * BUFFER_WIDTH]
    || (*dst) > &gpBuffer[(160 + 160) * BUFFER_WIDTH]) {

    }
}

endif

I changed the boundaries to allow us to see more. See the github code for original..

ADditionally in the town.cpp we modifiy
T_DRAWGAME
to increase the chunks ( More X axis drawn)
and increase the blocks (more Y axis)

Additionally we draw more upwards by modifying sx , sy, x , y

chunks = 20;
blocks = 15;
sx -= 64 * 5;
x -= 5;
y += 5;

what’s that you say? increase chunks for more X axis? OK…

overflow wraps around
oooops.

Going to stop linking full class links and just say their names.
defs.h has some values in it.
I couldn’t figur eout for a long time in a big enough resolution why the bottom of the screen was just…black. but I put these to very low and it resolved. Bugs for later? maybe.

define BORDER_LEFT 1

define BORDER_TOP 1

define BORDER_RIGHT 1

define BORDER_BOTTOM 1

So with the borders out of the way and the render.cpp NO OVERDRAW set. We can start messing with the values within TOWN.CPP to ‘draw more’.

So with that said. The menus and stuff are still set in their old resolutions. But the game will load more data in town.

The same logic will flow once we touch the dungeons.


Though there is a lot of clean up still left for town.


The way mouse input works is it seems to have its own ‘space’ too. So if I want to mvoe right now I have to click on the original 800×600 locations, not lining up with the pixels on the screen.

But a big post coming on pointers, and how we have to first stop everything from overflowing.

Before, I don’t think you could get far enough to the edges to run out of screen data to load, but now that it is drawing so much more, we are constantly hitting hard coded length issues where there is no more bitmap data to draw.
So I am constantly adding in checks to stop that. more on that maybe

Welcome to uh. my nightmares?

I Tripped into a solution real fast

So the goal here is to get as many players as I want on 1 screen. Local non-lan co op.
You can open as many instances as you want and have them all join your game with ports n shit even on the smae PC but who has time for that? not me.

Everyone keeps asking me whats the point of this feature. I don’t want to OPEN 4 DIABLO 1S
I want local co op boom.
anyway.
Let’s travel through the functions to see what’s going on


Entering netInit

Net Init is your entry point. It might go a few steps back before this but for the logic of the game it starts here.


Entering multi_init_multi

There are 2 inits. Multi and single. Even though my goal here is to make SINGLE PLAYER multiplayer, there are some flags that appear to be set in multi mode that actually let me draw more than 1 char.
So for now, we are going to activate multi mode regardless of what i click on (single/multy).
Later I’ll gut those flags out so I just click on single player.


Entering pfile_ui_set_hero_infos

This grabs all the possible save files to choose.

Enter UnPackPlayer
Enter InitPlayer

When the menu pops up with your choices of characters to load up, its from this function, this is going to take your character bytes and ‘unpack the information’ into some player object.
After we have all the player info we’re going to initialize him.
Shit like. oh save file says this dudes a sorcerer, initiate sorcerer graphics.


Entering SetPlrAnims

After initiation we set the animations from some animations file.


Entering NewPlrAnim

We create a new animation then we’re back up into ‘pfile_ui_set_hero_infos’ function again.
Here I drop the code that says. Well I see your loading my player from a file to ‘MY PLAYER’ object.
While your at it literally do the same thing, but inside ‘PLAYER 2’
Thus, we call all the same functions….

Enter UnPackPlayer
Enter InitPlayer


Entering SetPlrAnims


Entering NewPlrAnim


Entering pfile_create_player_description


Entering pfile_read_player_from_save

Dive in to unpack player 1
Enter UnPackPlayer
Enter InitPlayer

we unpack player 1 as per usual….


Entering SetPlrAnims


Entering NewPlrAnim

Dive in to unpack player 2
Enter UnPackPlayer
Enter InitPlayer

We do the same and call it player 2….


Entering SetPlrAnims


Entering NewPlrAnim

Initiate Class ChangeX
pnum 1 was here

During the new player animation sequence, I say, if im pnum 1, or player 2, change my class to sorcerer….


Entering pfile_read_player_from_save

Dive in to unpack player 1
Enter UnPackPlayer
Enter InitPlayer


Entering SetPlrAnims


Entering NewPlrAnim

Dive in to unpack player 2
Enter UnPackPlayer
Enter InitPlayer


Entering SetPlrAnims


Entering NewPlrAnim

Initiate Class ChangeX
pnum 1 was here

kinda repeats when it does animations. Anyway, thats why in the screenshot we see a pally and a sorc. They are both actually the exact same save file loaded twice, with some hard coded tweaks.
Now to figure out other things. Like how to move them both.

Normally battle net is sending packets back and forth with information on what the player did. instead I’m going to try and do whatever happens locally to player 1, to other players.

The big thing about rendering stuff to the screen is how its done.

We have some destination object which gets populated with data from a source object. A source object loads data from some file with tile information.
IF the town has 500 tilesthat could be drawn, it will draw whatever is in the view of the window at that time and not all of them, typical game stuff not rendering everything.
Well when it draws a line of pixels it says.
Increment my pointer, to point to the next spot in the array, and change its tile/pixel data to something else.
Well, no where in the code does it say when the array is empty. Eventually we get to the 501st tile and it wants to start writing to memory it has no access to. Things blow up.
As a result there are many places of code where we have to say ok , create a pointer to the end of my array, now, anytime you go to populate the destination with data, first make sure your not beyond this pointer. If you are , stop.

So here we create a new pointer, grabbing from the graphics buffer.

BYTE *myPt = &gpBuffer[BUFFER_HEIGHT * BUFFER_WIDTH];

Well now the code makes a destination and source and populates it

BYTE *src, *dst;
src = pRLEBytes;
dst = pBuff;

eventually the code does whatever IT WANTS

dst[0] = src[0];

and I’m like
NOT SO FAST SETO KAIBA

if (dst < myPt) {
dst[0] = src[0];
}

now do this. 1000 times.
Every so often other things break because why not so you adjust edge cases.

two players, 1 instance of d1.

Coding mistakes were made…

WOW finally.
I wanted it so that when player 1 went down to the dungeons his ally came with him.

I’m debating between if only palyer 1 can walk through to next zones or everyone.
I’m thinking its much easier and just better play style to say if 1 guy walks through a door you all go through.
worse case, oops someone walks in a door you gotta go back right? who cares.
much more convenitn for play, and coding. i guess.

But also, I wanted to say, woah if your too far from player 1, come back.

Interestingly enough theres a lot of stuff that happens when your walking, and the reaosn why the one screenshot had 50 mages was because I only called half the equation which was basically, create a new sorc instances and walk him to some spot. so it kept crapping out a new mage every time. no idea why.

right now the distance is commically short like 3 blocks. just to get the idea.
let’s talk some code.
diablo.cpp PressKey function
Here we house the listening for when you press key strokes.
Normally your mouse clicks on some screen realestate, and it translates that into X and Y coordinates.
NetSendCmdLoc(TRUE, CMD_WALKXY,mouseX, mouseY1);
Normally your diablo screen is drawn much smaller resolution like this box:

But we’ve actually translated it to the center of the screen, and clearly drawn more tiles ine very direction.

However, to register the green X click in the red box, you actually have to click on the green X in the green box. Because the handler for mouse clicks ins’t really adjusted.
cursor.cpp
seems to handle the offsets of this, but it’s so crazy to look at that I’m not even going to bother. I don’t care rightnow fi moouse clicks line up, my edition of the game will use keyboard… which leads us to our discussion of how we move today.

Instead of a mouse click triggering some position in space and sending a CMD_WALKXY through some NETSENDCMD handler, we’re instead going to do this in the keyboard section.
if (vkey == VK_LEFT) {
NetSendCmdLoc(TRUE, CMD_WALKXY, plr[0].WorldX, plr[0].WorldY + 1);
}
Interestingly enough it seems to follow this ‘net send’ structure for online play too, where your sending something to a host/client. In single player its like the same functions just operating a little differently.
Conveinietly for me I found that I can pass my players positions in the X Y space and just increment them for whatever direction.
IE. LEFT key is worldY + 1 tile.
It might be best to do + 2 tiles, as it seems when you click just 1 tile away it walks, stops, stands, walks. and is 1 frame of jitter. Not sure yet if I need to do a ‘while key down’ instead, to remediate this issue.
It’s not really a big deal. 1 frame of standing instead of continuous walking.
eventually in there the code gets to a section where it calls player.cpp do_WALK or something similar and actually moves the character.
Now what I’ve learned is, if I hard change a characters position for example
if (player2.worldX is 3 blocks away from player1.worldX) then
I’m too far away to lets teleport him next to player one
player2.worldX = player1.worldx+1;
player2.worldY = player2.worldx+1;
However, player 2 doesn’t move at all. Because thetre has been no update call to him to be drawn somewhere else. His game code position is change,d but his drawn animation is not??? I don’t really understand.
Anyway, remediate this by calling an update to his animations.
plr[1]._pmode = PM_WALK;
There are other states of animation. stand, attack, spell, whatever.
So when your character moves down to the dungeon layers, you want stuff to happen like, bring all the other players with you.
drlg_l2.cpp seems to handle level 2?
Rather than that theres a better solution
player.cpp
has a StartNewLVL whenever a player goes to town or anywhere else.
If player[0] is player 1, then to bring player 2 down with him I modify StartNewLVL() function to include
plr[1].plrlevel = plr[0].plrlevel;
plr[1].WorldX = plr[0].WorldX;
plr[1].WorldY = plr[0].WorldY + 1;
plr[1]._pmode = PM_WALK;
for some reason i have to take 1 step before it actually brings my home boy down, might be something im missing.


Leave a Reply

Your email address will not be published. Required fields are marked *