Jump to content
Search In
  • More options...
Find results that contain...
Find results in...
GarrettChan

How RNG works for Super Shotgun in Classic Doom?

Recommended Posts

Firstly, I have to admit I'm a bad reader and searcher since I may misread or skip existed information, and I couldn't find any complete reading material about this.

 

Reference:

Super Shotgun on doomwiki.org

Pseudorandom Number Generator on doomwiki.org

An External Reading I Got Today

 

So, here are what I got from the pages:

 

1. According to the SSG description in doomwiki, it fires 20 pellets with 5~15 damage per pellet. This means the theoretical damage range would be 100~300 (20*5~20*15). However, the chart in the later part of this page says that the actual damage range would be 150~225 with vanilla RNG. (This also contradicts with the number provided in Combat Characteristic section)

 

2. According to the Generator description in doomwiki, it says the mean value of generated numbers is not 127.5.

 

3. According to the external reading material, it says there are two codes about firing shotguns, and they assume that one is particular for Super Shotgun.

 

Then, here's how my thought (maybe logic) works:

 

1. From info 1, I think that some or all of the actual damage output is dependent, rather than being all individual, so it prevents players from getting minimum 100 damage and maximum 300 damage. Also, with the data provided that player have to use at least 3 SSG blasts to kill a Hell Knight/Arachnotron (500 HP), this tells us SSG can't do more than 250 damage per shot. I think this proves that the range is not 100~300.

 

2. From info 2, what I got is that the generated numbers are biased. Maybe some numbers tend to have more. However, I don't think this is related to the damage range. If every pellet is independent, you can still get maximum rolls for every single pellet.

 

3. Actually I don't really understand info 3, so I assume they meant to say there is a different type of control for the SSG RNG.

 

Lastly, here are my guesses:

 

1. Every number from 0~255 represents a preset of 20 damage numbers for the SSG. That means draw one number, and this number determines the damage for every pellet, and the total damage by these presets is limited to 150~225.

 

2. The system actually draws 20 numbers, but numbers may be dependent in order to prevent all maximum or minimum damage.

 

------------------------------------------------------------------------------------------------------------

 

If I made any misunderstand of what I read or any illogical assumption, please point it/them out for me. Also, if there are additional reading material, please tell me. Thanks a lot for reading.

 

What's more, it seems that there's a similar mechanic to prevent BFG from having maximum damage (over 4k), given the fact that you can't kill a Cyberdemon with one full BFG shot.

Share this post


Link to post

As always, the best way to find out is to look at the source code https://github.com/id-Software/DOOM/blob/77735c3ff0772609e9c8d29e3ce2ab42ff54d20b/linuxdoom-1.10/p_pspr.c#L716

for (i=0 ; i<20 ; i++)
    {
	damage = 5*(P_Random ()%3+1);
	angle = player->mo->angle;
	angle += (P_Random()-P_Random())<<19;
	P_LineAttack (player->mo,
		      angle,
		      MISSILERANGE,
		      bulletslope + ((P_Random()-P_Random())<<5), damage);
    }

So basically the engine does a loop where 20 times in a row, it chooses a number from 1 to 3, multiplies it by 5 (so you get 5, 10, or 15), and then shoots a "bullet" out at a random angle.

 

Then we can look at the P_Random() function: https://github.com/id-Software/DOOM/blob/77735c3ff0772609e9c8d29e3ce2ab42ff54d20b/linuxdoom-1.10/m_random.c#L31

 

P_Random() uses a fixed 256-length array of values from 0 to 255 and just steps through the array, returning each value in turn as it gets called. The thing is that whereas a truly "random" number generator could theoretically return a value of 0 all 20 times in a row, the fixed PRNG table will always step through the same numbers in the same order. This means that it's impossible for the SSG to do anywhere near the "theoretical" min and max damage values.

Share this post


Link to post

This is because the vanilla Doom engine and most source ports that try to remain highly faithful don't actually use a ( relatively for computers ) "true" random function; they just have a table of pre-generate random values and a variable to keep track of the current index of the table, and whenever they call for a "random" number they just take the value in the table at that index and increment the index by one. Thus, every random value is dependent on how many times a random value was generated previously. And the SSG calls for quite a few of those, so between that and the pre-generated values ...

 

If you're curious how those pre-generated values result in damage values of exactly 5, 10, or 15, it's because it takes the value, modulos it by 3, adds 1, then multiplies it by 5. So 244 would result in 10, 18 would result in 5, 26 would result in 15, etc.

Share this post


Link to post

Thanks for the code and explanation. Then I have some questions:

 

The "P_GunShot" function is to calculate damage for any hitscan weapons in the game, but they just didn't actually put this function in the SSG fire function for some reason and directly jump to P_LineAttack. I can't fully understand the codes, but I think P_GunShot is for calculation of damage/angle, and P_LineAttack is to actually apply the damage to the target?

 

As I read the "A_FireShotgun", it also directly draws 7 numbers from the pool consecutively, so the actual damage range for a Shotgun is not 35~105 (7*5~7*15) either?

Share this post


Link to post

P_GunShot isn't called by the SSG since P_GunShot uses the fixed spread used by the shotgun, chaingun, and pistol, which has no vertical spread. Everything P_GunShot does is replicated within the A_FireShotgun2 function though. P_LineAttack actually does the damage. It does not calculate the damage, rather it is calculated beforehand and then passed to P_LineAttack. Both A_FireShotgun2 and P_GunShot do this damage calculation in the same manner.

Share this post


Link to post
2 minutes ago, InsanityBringer said:

P_GunShot isn't called by the SSG since P_GunShot uses the fixed spread used by the shotgun, chaingun, and pistol, which has no vertical spread. Everything P_GunShot does is replicated within the A_FireShotgun2 function though. P_LineAttack actually does the damage. It does not calculate the damage, rather it is calculated beforehand and then passed to P_LineAttack. Both A_FireShotgun2 and P_GunShot do this damage calculation in the same manner.

Oh, got it. This means the spreads of Pistol, Shotgun and Chaingun are the same, right?

 

1 minute ago, rdwpa said:

Just a heads-up, DoomWiki's values are incorrect. The values in this table under the header 'correction' are accurate. 

Yeah, I figured, but I wasn't confident enough to say so. Probably I can manually group up 7 and 20 numbers to calculate all of them in order to figure out the real min/max, but it's too much work...

Share this post


Link to post

Yeah, bullets fired from the pistol, shotgun, and chaingun are all effectively identical. The only difference is that the shotgun fires seven of them in one go.

Share this post


Link to post
2 minutes ago, InsanityBringer said:

Yeah, bullets fired from the pistol, shotgun, and chaingun are all effectively identical. The only difference is that the shotgun fires seven of them in one go.

One more stupid question... As the first shot of the Pistol and the first two shots of the Chaingun are always accurate, should at least one shot from the Shotgun hit the dead center of where you aim at?

Share this post


Link to post

Nope. Look at that last parameter for P_GunShot; if it's set to true, it skips the calculation that causes the shot to be inaccurate. While A_FirePistol and A_FireCGun set that to the opposite of the player's refire state, A_FireShotgun always sets it to false, meaning it will never fire a truly accurate shot unless the odds just line up that way.

 

Consequently, the logic behind the pistol and chaingun's accuracy isn't caused by "the first shot" but the player's refire state. Which is fairly simple: if the player has the fire button held down when A_Refire is called ( which is what's responsible for autofire being faster than tap firing with almost every gun, most visible with the plasma rifle, as well as what allows the rocket launcher and BFG to autofire at all ), it's set to true, otherwise it's set to false. That's why the first two shots of the chaingun are perfectly accurate; its Fire state shoots twice before A_Refire is called, meaning both of those calls of A_FireCGun go off before the refire state is given a chance to be set.

Share this post


Link to post

Because I have nothing constructive to do, I did this:

Spoiler

public class Simple{
    public static void main(String args[]){
        int t[] = {
             0,   8, 109, 220, 222, 241, 149, 107,  75, 248, 254, 140,  16,  66 ,
            74,  21, 211,  47,  80, 242, 154,  27, 205, 128, 161,  89,  77,  36 ,
            95, 110,  85,  48, 212, 140, 211, 249,  22,  79, 200,  50,  28, 188 ,
            52, 140, 202, 120,  68, 145,  62,  70, 184, 190,  91, 197, 152, 224 ,
            149, 104,  25, 178, 252, 182, 202, 182, 141, 197,   4,  81, 181, 242 ,
            145,  42,  39, 227, 156, 198, 225, 193, 219,  93, 122, 175, 249,   0 ,
            175, 143,  70, 239,  46, 246, 163,  53, 163, 109, 168, 135,   2, 235 ,
            25,  92,  20, 145, 138,  77,  69, 166,  78, 176, 173, 212, 166, 113 ,
            94, 161,  41,  50, 239,  49, 111, 164,  70,  60,   2,  37, 171,  75 ,
            136, 156,  11,  56,  42, 146, 138, 229,  73, 146,  77,  61,  98, 196 ,
            135, 106,  63, 197, 195,  86,  96, 203, 113, 101, 170, 247, 181, 113 ,
            80, 250, 108,   7, 255, 237, 129, 226,  79, 107, 112, 166, 103, 241 ,
            24, 223, 239, 120, 198,  58,  60,  82, 128,   3, 184,  66, 143, 224 ,
            145, 224,  81, 206, 163,  45,  63,  90, 168, 114,  59,  33, 159,  95 ,
            28, 139, 123,  98, 125, 196,  15,  70, 194, 253,  54,  14, 109, 226 ,
            71,  17, 161,  93, 186,  87, 244, 138,  20,  52, 123, 251,  26,  36 ,
            17,  46,  52, 231, 232,  76,  31, 221,  84,  37, 216, 165, 212, 106 ,
            197, 242,  98,  43,  39, 175, 254, 145, 190,  84, 118, 222, 187, 136 ,
            120, 163, 236, 249
        };
        int damage = 0;
        int min = 300;
        int max = 0;
        int minind = 0;
        int maxind = 0;
        int i, j;

        for (i = 0; i <= 255; i++) {
            damage = 0;
            for (j = 0; j <= 19; j++) {
                damage += (t[(i + j) % 256] % 3 + 1) * 5;
                //System.out.println((t[(i + j) % 256] % 3 + 1) * 5);
            }
            if (damage < min) {min = damage; minind = i;}
            if (damage > max) {max = damage; maxind = i;}
        }

        System.out.println("Min Damage Index = " + minind + ";   Min Damage = " + min + ";   t[" + minind + "] = " + t[minind]);
        System.out.println("Max Damage Index = " + maxind + ";   Max Damage = " + max + ";   t[" + maxind + "] = " + t[maxind]);
    }

}

 

What I got from this is:

Min Damage Index = 64; Min Damage = 165; t[64] = 141
Max Damage Index = 38; Max Damage = 250; t[38] = 200

 

The damage is 5 points off for both min and max in the thread rdwpa posted. I don't trust my programming and computer, so I did this manual verification:

Spoiler

596311ec1d8de_mindamage.jpg.e0a6b82f08353c08b0cd38021223d503.jpg

 

Did I miss something if someone actually read my code? Does the system pull number from rndtable[0] after using rndtable[255]?

 

Also:

for (j=0;j<15;j++)

damage += (P_Random()&7) + 1;

 

This is from the A_BFGSpray function, so a single tracer damage is actually calculated by 15 numbers? If so, 40 tracers eat up 600 numbers, which doesn't make any sense to me... I hope I don't bug anyone by doing all these.

 

 

Edited by GarrettChan

Share this post


Link to post

Looks to me you forgot to factor in the fact that four P_Random calls happen after each P_Random call within the SSG's damage calculation.

 

Also, yes, that's exactly what the A_BFGSpray does for its damage. It is pretty silly, but it's also a more "even" way of doing a full 7 - 105 damage range with how Doom's PRNG works. If it was just P_Random()&98 + 7, there would be a significant amount of high numbers that would have a lower chance of being used in comparison to lower numbers. Not to mention, 15d7 is a lot more biased to the median numbers than 1d105 just due to how plain probability works out.

Share this post


Link to post
53 minutes ago, Arctangent said:

Looks to me you forgot to factor in the fact that four P_Random calls happen after each P_Random call within the SSG's damage calculation.

Hmm, yes. I saw 4 more calls after calling for damage, and I looked into P_LineAttack and found no P_Random calls. (The modified part is in bold)

Spoiler

for (i = 0; i <= 255; i++) {
    damage = 0;
    for (j = 0; j <= 95; j += 5) {
        damage += (t[(i + j) % 256] % 3 + 1) * 5;
        //System.out.println((t[(i + j) % 256] % 3 + 1) * 5);
    }
    if (damage < min) {min = damage; minind = i;}
    if (damage > max) {max = damage; maxind = i;}
}

Well, now I got the correct answer:

Min Damage Index = 157; Min Damage = 170; t[157] = 7
Max Damage Index = 18; Max Damage = 255; t[18] = 80

 

Later, I'll try to do the BFG one. Yeah, 15D7 is definitely biased to the median number. It's a permutation/combination. Thanks a lot, @Arctangent.

Share this post


Link to post

Doesn't the game also calculate blood splats or bullet puffs for each SSG pellet as it's going along, which also eat P_Random calls?

Share this post


Link to post
9 hours ago, Arctangent said:

This is because the vanilla Doom engine and most source ports that try to remain highly faithful don't actually use a ( relatively for computers ) "true" random function; they just have a table of pre-generate random values and a variable to keep track of the current index of the table,

 

Actually, even Boom changed this outside of demo compatibility. There's probably only a handful of very basic ports that stick to the original table during normal gameplay.

 

Share this post


Link to post

I had no idea I would learn about DooM. 

This is kinda neat to check out, I Never knew how the SSG worked when it came to spread and damage. I always thought it was just "throw the hitscans everywhere and add up the damage."

Share this post


Link to post
12 hours ago, Linguica said:

Doesn't the game also calculate blood splats or bullet puffs for each SSG pellet as it's going along, which also eat P_Random calls?

Isn't the function just straight up doing damage in that 20 iteration? Or I've actually fixed my program correctly already?

 

7 hours ago, Graf Zahl said:

Actually, even Boom changed this outside of demo compatibility. There's probably only a handful of very basic ports that stick to the original table during normal gameplay.

Well, I didn't notice any crazy RNG happened during other ports though, or are they just THAT rare...?

 

Share this post


Link to post
On 7/10/2017 at 3:14 AM, Linguica said:

Doesn't the game also calculate blood splats or bullet puffs for each SSG pellet as it's going along, which also eat P_Random calls?

Yep. Other stuff too. Here's what happens when the player fires the shotgun (single barrel) at a pistol guy, and some shotgunner guys in tic #1491 of the Doom 1.9 E1M5 demo, as reported by my SyncDebug output: (I don't have any double-barrel SyncDebug output with me right now to post, but the concept is similar. The single-barrel fires 7 times.)

 

Spoiler

player[0] // "Green"
{
	P_MovePlayer()
	{
		P_Thrust()
		{
			.momxy = {0xffff0b94, 0xfffbf0f5};
			.angle = 0x3c000000;
			.move = 0xffffe800;
		}
	}

	P_CalcHeight()
	{
		.momxy = {0xffff093c, 0xfffbd912};
		.z = 0x0;
		.viewz = 0x2b0dbe;
		.bob = 0x54abf;
	}

	P_Random("gunshot", 25) = 89;
	P_Random("misfire", 26) = 77;
	P_Random("misfire", 27) = 36;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3ca40000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0xf;
		P_Random("spawnblood", 28) = 95;
		P_Random("spawnblood", 29) = 110;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 30) = 85;
			new mobj[398] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffd35dc0, 0x1159fd5};
				.z = 0x1bf409;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 31) = 48;
		P_DamageMobj()
		{
			.damage = 15;
			.target = mobj[133]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 30;
			P_Random("painchance", 32) = 212;
		}
	}

	P_Random("gunshot", 33) = 140;
	P_Random("misfire", 34) = 211;
	P_Random("misfire", 35) = 249;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3b680000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0xf;
		P_Random("spawnblood", 36) = 22;
		P_Random("spawnblood", 37) = 79;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 38) = 200;
			new mobj[399] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffdaa0c1, 0x10e19c0};
				.z = 0x1b7e5d;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 39) = 50;
		P_DamageMobj()
		{
			.damage = 15;
			.target = mobj[133]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 15;
			P_KillMobj()
			{
				.target = mobj[133]; // "MT_SHOTGUY"
				.source = mobj[0]; // "MT_PLAYER"
				P_Random("killtics", 40) = 28;
				SpawnMobj("MT_SHOTGUN")
				{
					P_Random("lastlook", 41) = 188;
					new mobj[400] // "MT_SHOTGUN"
					{
						.type = 77;
						.doomednum = 2001;
						.xy = {0xffd31180, 0x120ba80};
						.z = 0x0;
						.ang = 0x0;
						.momxy = {0x0, 0x0};
						.momz = 0x0;
						.hlth = 1000;
						.flgs = 0x1; //  SPECIAL
					}
				}
			}
		}
	}

	P_Random("gunshot", 42) = 52;
	P_Random("misfire", 43) = 140;
	P_Random("misfire", 44) = 202;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3b080000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0xa;
		P_Random("spawnblood", 45) = 120;
		P_Random("spawnblood", 46) = 68;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 47) = 145;
			new mobj[401] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffda4ee0, 0xf78c58};
				.z = 0x1dd8e5;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 48) = 62;
		P_DamageMobj()
		{
			.damage = 10;
			.target = mobj[64]; // "MT_POSSESSED"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 8;
			P_KillMobj()
			{
				.target = mobj[64]; // "MT_POSSESSED"
				.source = mobj[0]; // "MT_PLAYER"
				P_Random("killtics", 49) = 70;
				SpawnMobj("MT_CLIP")
				{
					P_Random("lastlook", 50) = 184;
					new mobj[402] // "MT_CLIP"
					{
						.type = 63;
						.doomednum = 2007;
						.xy = {0xffeef456, 0xee0b3a};
						.z = 0x0;
						.ang = 0x0;
						.momxy = {0x0, 0x0};
						.momz = 0x0;
						.hlth = 1000;
						.flgs = 0x1; //  SPECIAL
					}
				}
			}
		}
	}

	P_Random("gunshot", 51) = 190;
	P_Random("misfire", 52) = 91;
	P_Random("misfire", 53) = 197;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3a580000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0xa;
		P_Random("spawnblood", 54) = 152;
		P_Random("spawnblood", 55) = 224;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 56) = 149;
			new mobj[403] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffe737a7, 0x136d721};
				.z = 0x1a0aa5;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 57) = 104;
		P_DamageMobj()
		{
			.damage = 10;
			.target = mobj[134]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 30;
			P_Random("painchance", 58) = 25;
		}
	}

	P_Random("gunshot", 59) = 178;
	P_Random("misfire", 60) = 252;
	P_Random("misfire", 61) = 182;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3d180000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0xa;
		P_Random("spawnblood", 62) = 202;
		P_Random("spawnblood", 63) = 182;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 64) = 141;
			new mobj[404] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffd40625, 0x14aa2c9};
				.z = 0x1af75f;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 65) = 197;
		P_DamageMobj()
		{
			.damage = 10;
			.target = mobj[134]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 20;
			P_Random("painchance", 66) = 4;
		}
	}

	P_Random("gunshot", 67) = 81;
	P_Random("misfire", 68) = 181;
	P_Random("misfire", 69) = 242;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3b0c0000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0x5;
		P_Random("spawnblood", 70) = 145;
		P_Random("spawnblood", 71) = 42;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 72) = 39;
			new mobj[405] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffe29f42, 0x13b9ab7};
				.z = 0x1ca808;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 73) = 227;
		P_DamageMobj()
		{
			.damage = 5;
			.target = mobj[134]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 10;
			P_Random("painchance", 74) = 156;
		}
	}

	P_Random("gunshot", 75) = 198;
	P_Random("misfire", 76) = 225;
	P_Random("misfire", 77) = 193;
	P_LineAttack()
	{
		.thing = mobj[0]; // "MT_PLAYER"
		.thing_xy = {0xffbd2cce, 0x91291};
		.thing_z = 0x0;
		.thing_height = 0x380000;
		.angle = 0x3c800000;
		.distance = 0x8000000;
		.slope = 0xfffff894;
		.damage = 0x5;
		P_Random("spawnblood", 78) = 219;
		P_Random("spawnblood", 79) = 93;
		SpawnMobj("MT_BLOOD")
		{
			P_Random("lastlook", 80) = 122;
			new mobj[406] // "MT_BLOOD"
			{
				.type = 38;
				.doomednum = -1;
				.xy = {0xffd85b7a, 0x146302d};
				.z = 0x1cbdc1;
				.ang = 0x0;
				.momxy = {0x0, 0x0};
				.momz = 0x0;
				.hlth = 1000;
				.flgs = 0x400; //  DROPOFF
			}
		}

		P_Random("spawnblood", 81) = 175;
		P_DamageMobj()
		{
			.damage = 5;
			.target = mobj[134]; // "MT_SHOTGUY"
			.inflictor = mobj[0]; // "MT_PLAYER"
			.source = mobj[0]; // "MT_PLAYER"
			.target_health = 5;
			P_KillMobj()
			{
				.target = mobj[134]; // "MT_SHOTGUY"
				.source = mobj[0]; // "MT_PLAYER"
				P_Random("killtics", 82) = 249;
				SpawnMobj("MT_SHOTGUN")
				{
					P_Random("lastlook", 83) = 0;
					new mobj[407] // "MT_SHOTGUN"
					{
						.type = 77;
						.doomednum = 2001;
						.xy = {0xffdf6ec0, 0x149f0c0};
						.z = 0x0;
						.ang = 0x0;
						.momxy = {0x0, 0x0};
						.momz = 0x0;
						.hlth = 1000;
						.flgs = 0x1; //  SPECIAL
					}
				}
			}
		}
	}
}

 

 

The last pellet actually kills a shotgunner exactly (he has 5 health, and the bullet causes 5 points of damage), causing him to drop his shotgun. Note that the entire tic's actions are not shown in this excerpt, just the action of Player 1 moving, and firing the shotgun. This code is from KBDoom. I use it to analyze network and demo sync to the sub-tic level. It is implemented as a set of drop-in C modules, with only minimal tie-in code required in Doom's base modules. It does depend on the implementation of Boom, though it could be modified to work in vanilla without too much effort. If I ever get around to it, I'll eventually upload this code with instructions on how to drop SyncDebug into your port of choice.

 

As you can see, there are RNG calls in SpawnBlood, and calls for PainChance, LastLook, GunShot, MisFire. A double-barrel shot causes even more calls to P_Random. I'll try to dig one up. Calculating the minimum and maximum damage is a complicated task, indeed!

Edited by kb1

Share this post


Link to post

WTF is misfire? That doesn't show up in the original source, at all.

 

edit: OK, it's some sort of Boom shim to prevent successive P_Random calls from being called in different orders under different compilers, I guess?

Share this post


Link to post

I have a few questions regarding the P_Random tables and ports. This topic seems like it is full of very knowledgeable people about the subject, so I will ask here.

 

1) In vanilla Doom, are all the calls going into only one P_Random table or are there more than one?

 

2) If all ports deal with this random generation by their own means, how much do they differ from the original? What other methods for handling this problem (random number generation) are used in other ports? 

 

I remember reading that ZDoom for example, implemented multiple tables for different things going on in the game, consequence of which is for example that after loading a game, a zombieman (or any other hitscan attack) will always hit you for the same damage no matter what other things you do.

 

3) Is there any consensus in regards to what method of number generation is the best for Doom?

 

I read that the original one is faulty and many of the values can never even be picked at all. The ZDoom method doesn't seem the best to me as it makes the game predictable to a certain degree and removes a lot of the dynamic.

Example: get killed by a revenant missile that dealt 80 damage. Load a save and now you know you cannot get hit by a revenant unless you have over 80 HP, whereas in the original, he could do different damage after loading a game.

Share this post


Link to post
1 hour ago, idbeholdME said:

1) In vanilla Doom, are all the calls going into only one P_Random table or are there more than one?

 

There are two, actually: P_Random and M_Random - one for sync-related stuff, the other for UI related stuff.

 

 

1 hour ago, idbeholdME said:

 

2) If all ports deal with this random generation by their own means, how much do they differ from the original? What other methods for handling this problem (random number generation) are used in other ports? 

 

Most ports use the same approach as Boom: named RNGs with better algorithms. Some use a single RNG with better algorithms.

A few just keep everything as it was. But even then, there's no guarantee that the results are the same as Doom.exe: Just one small change that causes an additional P_Random call and any 'defined' sequences will be broken.

 

 

1 hour ago, idbeholdME said:

 

I remember reading that ZDoom for example, implemented multiple tables for different things going on in the game, consequence of which is for example that after loading a game, a zombieman (or any other hitscan attack) will always hit you for the same damage no matter what other things you do.

 

ZDoom uses named RNGs, just like Boom. It doesn't use any random number 'tables' but a "real" random number generation algorithm.

Boom did something similar, but they just copied the algorithm from some popular compiler of its time (Maybe Microsoft's but this I cannot verify.)

Both ports seed their RNGs before a game starts so sequences should be different each time.

 

 

1 hour ago, idbeholdME said:

 

3) Is there any consensus in regards to what method of number generation is the best for Doom?

 

Outside of vanilla demo compatibility, no.

 

 

 

1 hour ago, idbeholdME said:

 

I read that the original one is faulty and many of the values can never even be picked at all. The ZDoom method doesn't seem the best to me as it makes the game predictable to a certain degree and removes a lot of the dynamic.

 

The original table is indeed missing some values.

Using multiple RNGs like Boom or ZDoom will indeed be problematic if you also use the original tables. This approach only works with better algorithms that are more 'random' so that identical sequences are avoided.

 

 

1 hour ago, idbeholdME said:

Example: get killed by a revenant missile that dealt 80 damage. Load a save and now you know you cannot get hit by a revenant unless you have over 80 HP, whereas in the original, he could do different damage after loading a game.

 

This has nothing to do with different algorithms but with ZDoom saving the RNG state in a savegame. Most ports don't do that.

 

Share this post


Link to post
1 hour ago, Graf Zahl said:

Outside of vanilla demo compatibility, no.

Almost.  Competitive players can tell when Doom's random table is enabled, because the SSG does more damage more consistently.  This is because the random table has more numbers that result in 10 and 15 damage pellets than a true random distribution.

 

There's also guaranteed initial deathmatch spawns that are specific to Doom's random table as well - changing the table or the method either changes or completely randomizes them.  It's not as widely known or complained about though, of the major multiplayer ports I think only Odamex emulates that.

Share this post


Link to post
15 hours ago, Linguica said:

WTF is misfire? That doesn't show up in the original source, at all.

 

edit: OK, it's some sort of Boom shim to prevent successive P_Random calls from being called in different orders under different compilers, I guess?

Yep, it's a Boom enum, in an original function P_GunShot. Boom used an array to hold multiple P_Random indexes, in an attempt to minimize the effect of an erroneous call to P_Random in one area, affecting demo sync in another area. These "areas" were each given an "enum", and the one in P_GunShot is called "pr_misfire". In SyncDebug, I take advantage of this fact, by identifying which enum is used, for every call to P_Random, which allows me to prove which function the P_Random call came from. This would work perfectly, except for the fact that Boom did not use a unique enum for every single P_Random call. Usually, but not always.

 

So, in the output, it shows that 2 P_Random calls were made in P_GunShot, right before the P_LineAttack call.

 

By the way, I question the usefulness of Boom's multiple index approach. If ANY P_Random calls happen out of sequence, the demo is going to go out of sync. I don't think the multiple index approach does much to prevent that, especially over time. Am I right about that, or have I missed something. Any thoughts on that?

 

7 hours ago, idbeholdME said:

I have a few questions regarding the P_Random tables and ports. This topic seems like it is full of very knowledgeable people about the subject, so I will ask here.

 

1) In vanilla Doom, are all the calls going into only one P_Random table or are there more than one?

 

2) If all ports deal with this random generation by their own means, how much do they differ from the original? What other methods for handling this problem (random number generation) are used in other ports? 

I remember reading that ZDoom for example, implemented multiple tables for different things going on in the game, consequence of which is for example that after loading a game, a zombieman (or any other hitscan attack) will always hit you for the same damage no matter what other things you do.

 

3) Is there any consensus in regards to what method of number generation is the best for Doom?

 

I read that the original one is faulty and many of the values can never even be picked at all. The ZDoom method doesn't seem the best to me as it makes the game predictable to a certain degree and removes a lot of the dynamic.

Example: get killed by a revenant missile that dealt 80 damage. Load a save and now you know you cannot get hit by a revenant unless you have over 80 HP, whereas in the original, he could do different damage after loading a game.

See Graf's answers. Also:

 

It was Boom that split up P_Random, so each call to P_Random used it's own index into the same list of 256 numbers. If the zombieman was preparing to attack you, and there was either no time for the player to make a move that affected the RNG, or the player moves the exact same way, then, yes, the zombieman will act the same on each load. But, it's very easy to alter the scenario just enough to change the order of calls, and thus the results.

 

3. Yes, the table is missing values, and doubles and triples up on some values. This could potentially affect a port that used the original RNG for scripting. But, in normal gameplay, it really doesn't have a noticable effect. I am very surprised of what AlexMax mentions: That people can notice the SSG spread characteristics change with different RNG implementations - interesting. The deathmatch spawn order is probably the most obvious side effect of the RNG approach.

 

But, in normal gameplay, every sighting, every bullet/missile fire, and constantly as monsters move, that same RNG is being called, by each of them. In all but the most quiet scenes, the RNG index "rolls over" many many times per second, and is virtually guaranteed to be in a different spot for each noticeable action.

 

Having said that, though, there is an exception, and it's what has been discussed in this thread: The shotgun and the SSG: Each of these consume a lot of RNG calls, some of which cause other RNG calls to happen, but in a predictable way. This all happens "atomically", meaning that, for a given P_Random index, shooting the SSG at a wall can create, at most, 256 different spread patterns, which could become familiar with time. It's different if you shoot at a monster, cause that can cause blood, damage, death, etc, which can affect the RNG.

Share this post


Link to post
17 hours ago, Graf Zahl said:

This has nothing to do with different algorithms but with ZDoom saving the RNG state in a savegame. Most ports don't do that.

 

Thanks for the superb explanations. About the last point: Is there a way to make ZDoom regenerate the table after loading instead of saving the RNG state when a savegame is created? Maybe some hidden option I don't know about? Or is it hardcoded and impossible to change?

 

11 hours ago, kb1 said:

See Graf's answers. Also:

 

It was Boom that split up P_Random, so each call to P_Random used it's own index into the same list of 256 numbers. If the zombieman was preparing to attack you, and there was either no time for the player to make a move that affected the RNG, or the player moves the exact same way, then, yes, the zombieman will act the same on each load. But, it's very easy to alter the scenario just enough to change the order of calls, and thus the results.

 

But, in normal gameplay, every sighting, every bullet/missile fire, and constantly as monsters move, that same RNG is being called, by each of them. In all but the most quiet scenes, the RNG index "rolls over" many many times per second, and is virtually guaranteed to be in a different spot for each noticeable action.

I am aware of how the original table works, that you can change it by doing different things to change the random number as many calls are directed to the same table. The thing is, in ZDoom (the port I use), the scenario cannot be altered after loading a save (at least not easily, without quitting the game). That is pretty much my only gripe with this otherwise great port.

Share this post


Link to post
12 hours ago, kb1 said:

shooting the SSG at a wall can create, at most, 256 different spread patterns, which could become familiar with time. It's different if you shoot at a monster, cause that can cause blood, damage, death, etc, which can affect the RNG.

Actually, no. Some of the RNG calls are inside functions that depend on something getting hit. The 256 spread pattern limit only applies if nothing gets hit.

 

41 minutes ago, idbeholdME said:

Thanks for the superb explanations. About the last point: Is there a way to make ZDoom regenerate the table after loading instead of saving the RNG state when a savegame is created? Maybe some hidden option I don't know about? Or is it hardcoded and impossible to change?

No, there isn't. But this could be added if deemed necessary. Having the RNG table saved definitely makes it easier to debug savegames because the outcome is predictable.

Share this post


Link to post

Is there any reason why M_Random has all those preset numbers to pick out of instead of 1 through 255?

Share this post


Link to post

well, if it had 1-255 in sequence, it wouldn't be very random at all. Its easier to generate 256 random numbers in the range 0-255 than to take that range and shuffle it without any patterns showing up, I suspect.

Share this post


Link to post
20 hours ago, Graf Zahl said:

Actually, no. Some of the RNG calls are inside functions that depend on something getting hit. The 256 spread pattern limit only applies if nothing gets hit.

Actually, that's what I said :) Shooting at a wall is a consistent thing. To be more accurate, though: Shooting at a wall that does not trigger an action, or hit an object, should provide only 256 possible spreads. But, ports that spawn decals make over-complicate things. But, yes, hitting a trigger, monster, barrel, or player gives you vastly varied results, which is quite verbosely demonstrated by my previous SyncDebug post. (Did you check it out? It's in a spoiler. I was hoping someone would comment on it - it's something I'm very proud of.)

 

@idbeholdME Is it a gripe because you might save right before certain death, with no way to escape it? Personally, I want my save to restore the game as exactly as possible, but I see how that scenario could be annoying :)

Share this post


Link to post
On 12.7.2017 at 10:49 PM, InsanityBringer said:

well, if it had 1-255 in sequence, it wouldn't be very random at all. Its easier to generate 256 random numbers in the range 0-255 than to take that range and shuffle it without any patterns showing up, I suspect.

 

Easier, yes, but as has been said, such an unbalanced table ultimately yields biased results. You can twist it as much as you like - the entire method to calculate random numbers here is utter garbage. I'm pretty sure the only reason for this implementation was to avoid doing some research.

 

The RNG is just another element where id went cheap instead of well-defined. You do not need a near-'perfect' RNG like ZDoom does, but any simple algorithm that can be seeded for different but well-defined sequences would have been better than a 256-entry byte table.

 

Share this post


Link to post

Not having ever poured over the source like some of you guys and basing this off the examples I see in the thread, I see ID doing a lot of things with random numbers that are not great. Having a table of predefined random numbers is fine I guess for a game where they don't need to really be secret, but taking those numbers and using modulo or AND to clamp the entropy down or whatever probably skews distribution a bunch. If the code had to be cryptographically secure (which it doesn't) that would be a pretty bad thing to mess up. Even so, I'm sure it contributes to bias somehow within the game although I only know this stuff from the distance, I'm far from an expert.

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
×