Hello.. I'm currently working on a RPG game I use VS 2019 and I use 'C' language.
I am a bit confuse, and I hope you guys make me understand why this is possible. I resolved the problem, and I understood what I did wrong in the code, now is working on both modes (debug/release), but I still need a clarification why in debug mode didn't warning me about the problem , and it was still working while the bug was in the code and in release same bug and NOT working like it should.
This is the function that I wrote where I fixed the bug:
...
..
.
if (mobFound)
{
if (gMonster[i].MobDead == FALSE)
CanMoveToDesiredTiles = FALSE;
else CanMoveToDesiredTiles = TRUE;
}
else...
if (mobFound) had to be dereferenced if (*mobFound), which in debug mode was working just fine , and I even not realize I did a mistake, still switching to release mode .. I saw the player can move at all, so is a bit hard to fix when you can't use debug tools.
Now I fixed and work just fine, but I just wanted someone can explain why did worked in debug and why not in realease. Thank you..
Just to clarify, line 64 ( if (mobFound) ) is the only line you changed to make the buggy code work? Or were there other lines you changed?
Clarification why in debug mode didn't warning me about the problem
The compiler doesn't know your intention. Maybe in your codebase, it's valid to pass a null pointer as mobFound. Thus, the check for if (mobFound) is a null-pointer check.
and it was still working while the bug was in the code and in release same bug and NOT working like it should.
Most likely, by default, your 'Release' build will have optimizations enabled, while the 'Debug' build will not have optimizations. The optimizer can aggressively change the underlying compiled code, especially if there is undefined behavior in the code.
That being said, I'm not quite sure why changing line 64 alone would make there be a difference in behavior between Debug and Release. Perhaps there's another issue somewhere else in your code. Maybe show how you're calling the function?
..Yes I only changed the line 64. and I'm trying to show the best way where I call the function, cause is a bit complicated and it doesn't allow me to paste 60.000+ lines.. still here is where the Player Process Input is calling that function..
void PPI_Overworld(void)
void PPI_Overworld(void)
{
// other code..
if (gPlayer.HeroDead == FALSE)
{
if (!gPlayer.MovementRemaining) // PLAYER HASN'T MOVE YET..
{
// DOWN
if ((gMouse_pointer.y > 390 && gGameInput.MouseLeftButtonIsDown && gMouse_pointer.x > 620 && gMouse_pointer.x < 748 && gMouse_pointer.y < 721))
{
int i = 0;
BOOL CanMoveToDesiredTiles = FALSE;
for (uint8_t Counter = 0; Counter < _countof(gPassableTiles); Counter++)
{
if ((gPlayer.WorldPos.y / 16) + 1 > (gOverworld01.TileMap.Height - 1))
break;
if (gOverworld01.TileMap.Map[(gPlayer.WorldPos.y / 16) + 1][gPlayer.WorldPos.x / 16] == gPassableTiles[Counter])
{
CanMoveToDesiredTiles = checkDesiredTileAndMobs(&mobFound, &i, DOWN);break;
}
}
checkDesiredTileAndMonsters(CanMoveToDesiredTiles, mobFound, gPlayer.ScreenPos, i, DOWN);
}
}
else // if movement remaining..
{
gPlayer.MovementRemaining -= gPlayer.RunSpeed;
// switch player directions
switch (gPlayer.Directions)
{
case DOWN:
{
if (gPlayer.ScreenPos.y < GAME_RES_HEIGHT - 160) // 128
{
gPlayer.ScreenPos.y++;
}
else
{
gCamera.y += gPlayer.RunSpeed;
}
gPlayer.WorldPos.y += gPlayer.RunSpeed;
break;
}
case LEFT:
{
if (gPlayer.ScreenPos.x > 208)
{
gPlayer.ScreenPos.x--;
}
else
{
if (gCamera.x > 0)
{
gCamera.x -= gPlayer.RunSpeed;
}
else
{
gPlayer.ScreenPos.x--;
}
}
gPlayer.WorldPos.x -= gPlayer.RunSpeed;
break;
}
case RIGHT:
{
if (gPlayer.ScreenPos.x < GAME_RES_WIDTH - 208)
{
gPlayer.ScreenPos.x++;
}
else
{
if (gCamera.x < (gOverworldArea.Area.right - 208))
{
gCamera.x += gPlayer.RunSpeed;
}
else
{
gPlayer.ScreenPos.x++;
}
}
gPlayer.WorldPos.x += gPlayer.RunSpeed;
break;
}
case UP:
{
if (gPlayer.ScreenPos.y > 160) // 128
{
gPlayer.ScreenPos.y--;
}
else
{
if (gCamera.y > 0)
{
gCamera.y -= gPlayer.RunSpeed;
}
else
{
gPlayer.ScreenPos.y--;
}
}
gPlayer.WorldPos.y -= gPlayer.RunSpeed;
break;
}
default:
break;
}
// SWITCH THE PLAYER MOVEMENT------------------------------------------------------------------|
switch (gPlayer.MovementRemaining)
{
case 15:
{
gPlayer.MovedSinceTeleport = TRUE;
gPlayer.SpriteIndex = 0;
break;
}
case 14: // This case is because when run and step to teleport not trigger at 16 or 15 case
{
// Drain Stamina .. when player run
if (gPlayer.RunSpeed == 2)
{
if (gStamina > 0)
{
if (gMiscItems[2].stamina == FALSE)
{
if (gPlayer.WorldPos.y > 512 || gPlayer.WorldPos.x >= 816)
{
gStamina -= gPlayer.RunSpeed;
}
}
if (gStamina <= 0)
{
gStamina = 0;
gPlayer.RunSpeed = 1;
}
}
}
gPlayer.MovedSinceTeleport = TRUE;
gPlayer.SpriteIndex = 0;
break;
}
case 12:
{
gPlayer.SpriteIndex = 1;
break;
}
case 8:
{
gPlayer.SpriteIndex = 0;
PlayGameSound(&gSoundPlayerSteps);
break;
}
case 4:
{
gPlayer.SpriteIndex = 2;
break;
}
case 0:
{
#ifdef _DEBUG
// Assert gPlayer movement if out of bounds
ASSERT(gPlayer.ScreenPos.x % 16 == 0, "gPlayer did not land on a position that is a multiple of 16!");
ASSERT(gPlayer.ScreenPos.y % 16 == 0, "gPlayer did not land on a position that is a multiple of 16!");
ASSERT(gPlayer.WorldPos.x % 16 == 0, "gPlayer did not land on a position that is a multiple of 16!");
ASSERT(gPlayer.WorldPos.y % 16 == 0, "gPlayer did not land on a position that is a multiple of 16!");
#endif // DEBUG
gPlayer.SpriteIndex = 0;
if (gOverworld01.TileMap.Map[gPlayer.WorldPos.y / 16][gPlayer.WorldPos.x / 16] == TILE_PORTAL_01)
{
if (gPlayer.MovedSinceTeleport == TRUE)
{
// Should we run into a random monster encounter..?
PortalHandler();
}
}
elseif ((gPlayer.WorldPos.x == gTownPortal[1].WorldPos.x) && (gPlayer.WorldPos.y == gTownPortal[1].WorldPos.y))
{
if (gPlayer.MovedSinceTeleport == TRUE)
{
if (gPlayer.skills[1].enable == TRUE)
{
TownPortalHandler();
}
}
}
elseif ((gPlayer.WorldPos.x == gTownPortal[0].WorldPos.x) && (gPlayer.WorldPos.y == gTownPortal[0].WorldPos.y))
{
if (gPlayer.MovedSinceTeleport == TRUE)
{
if (gPlayer.skills[1].enable == TRUE)
{
TownPortalHandler();
}
}
}
else
{
if (gPlayer.StepsTaken - gPlayer.StepsTakenSinceLastMonsterEncounter >= RANDOM_MOSTERS_GRACE_PERIOD_STEPS)
{
DWORD Random = 0;
rand_s((unsignedint*)&Random);
Random = Random % 100;
if (Random <= gPlayer.RandomEncouterPercentage)
{
gPlayer.StepsTakenSinceLastMonsterEncounter = gPlayer.StepsTaken;
CalculatePlayerProperties();
}
}
}
gPlayer.StepsTaken++;
break;
}
default:
break;
}
}
}
else
{
// Other code..
}
}
The optimizer can aggressively change the underlying compiled code
Another reason why you might experience different behaviour in debug mode and release mode (assuming you have undefined behaviour in your program) is because Microsoft's compiler will set uninitialised and freed memory to certain bit patterns in debug mode. This is not done in release mode. See https://stackoverflow.com/a/127404
I hope you have (re)set mobFound to false before calling this function because the function assumes that you have. To make it harder to use the checkDesiredTileAndMobs function incorrectly you might want to change it so that it always set *mobFound to false at the beginning, or perhaps rewrite it some other way to avoid this assumption.
If *mobFound has not been set to TRUE in the code above then i will equal MONSTERS_NUMBER so gMonster[i] on line 66 will be out of bounds.
Ahh right, good catch Peter.
So, in the buggy code, "if (mobFound)" was always being entered, but the gMonster[i] dereference was then the undefined behavior because 'i' could be out of bounds if the mob was not found within the "switch(dir)" logic.
Another improvement to the code: Limit the scope of i to the individual for loops. You already set 'mobIdx' when the mob is found, so use that variable in the "if found" logic.
Well the mobFound is needed 3 lines after in this function checkDesiredTileAndMonsters(CanMoveToDesiredTiles, mobFound, gPlayer.ScreenPos, i, DOWN);. So based on what checkDesiredTileAndMobs(&mobFound, &i, DOWN); founds, this information must be past far to the other function. cose if you can move to a desired tile , but if you encounter a mob, certainly you can't pass thru him, so you do other stuff. And yes I now realized I have 2 functions with almost same name, but they do different things , so that may confuse you too.
Another improvement to the code: Limit the scope of i to the individual for loops. You already set 'mobIdx' when the mob is found, so use that variable in the "if found" logic.
If the above is a reply to what I said about the mobFound parameter maybe not being necessary I retract that (I have already removed it from my previous post). It was a mistake.
Do you feel like your original question has been answered? It has been explained why the behaviour between debug and release mode can differ when you have undefined behaviour in your program. It has also been explained what caused the undefined behaviour.
this is not at all uncommon and you MUST test your code in release mode, as it will be compiled when you distribute it. Testing in debug mode is for using the debugger to find issues or first pass smoke tests (does it compile and run at all) only. And yes, there are times when the bug hides in debug mode making the debugger difficult to use. For those cases, adding prints or logging to the code that you can run in release mode is a simple way to work through it.
.. Yes, I understand my buggy code logic fully and I found that quite fast, and repaired, but about the (debug/release) mode I only understood that the compiler makes certain optimizations to the final product and it can have different behaviours from the debug mode. I'll read more about the compiler optimizations. I'm at 90% sure of the understanding, just because of my lack of knowledge about the compiler, which will be my next thing to read.
That being said until next time.. thank you all.