Merge branch 'main' of https://github.com/dethrace-labs/dethrace into opponent_matching

This commit is contained in:
Dethrace Labs 2025-10-01 21:19:06 +13:00
commit 66380ea3f6
9 changed files with 75 additions and 24 deletions

View File

@ -6,6 +6,7 @@ To run MSVC 4.20 outside of a non-Windows environment, you can use a Docker imag
We are targetting being accurate to CARM95.EXE.
- Created date: "16 October 1997"
- SHA256 hash: `c6040203856b71e6a22d2a29053a1eadd1a2ab41bce97b6031d745079bc07bdf`
- [Download from archive.org](https://archive.org/download/carm-95/CARM95.EXE)
## Build container image
```sh
@ -23,10 +24,21 @@ When running this container, you must define:
| `/build` | `/code/dethrace/build-msvc420` | Volume mount. This directory must exist but can start off empty. Note that this build directory _cannot be_ the same as your "regular" build directory. |
| `/original` | `/games/carma` | Volume mount. Path to a directory with a copy of the original CARM95.EXE file |
### Generating an HTML diff
### Diffing a single function
This is the primary flow for making a change to the code and viewing the comparison to the original executable.
```sh
docker run --platform linux/amd64 \
-e CMAKE_FLAGS="-G Ninja -DCMAKE_BUILD_TYPE=Debug -DMSVC_42_FOR_RECCMP=on" \
-v <PATH_TO_DETHRACE_DIR>:/source \
-v <PATH_TO_DETHRACE_BUILD_DIR>:/build \
-v <PATH_TO_CARMA_DIR>:/original:ro \
msvc420-wine -- \
reccmp-reccmp --target CARM95 --verbose FUNCTION_ADDRESS
```
### Generating an HTML diff
```sh
docker run --platform linux/amd64 \
-e CMAKE_FLAGS="-G Ninja -DCMAKE_BUILD_TYPE=Debug -DMSVC_42_FOR_RECCMP=on" \
@ -39,8 +51,42 @@ docker run --platform linux/amd64 \
After running, a `report.html` file will be created in the build-msvc420 directory.
### Wrapping it up
I wrapped this logic up into a little `zsh` function, which makes it easy to run from command line.
- `dr-reccmp` to generate the overall html report
- `dr-reccmp 0x123456` to diff a single function.
```zsh
function dr-reccmp {
local arg=()
if [[ -n "$1" ]]; then
arg+=(--verbose "$1")
else
# only download latest report if changed
curl -fsSL --etag-save reccmp-report-etag-new.txt --etag-compare reccmp-report-etag.txt -o reccmp-report-main.json https://raw.githubusercontent.com/dethrace-labs/reccmp-report/main/report.json
if [ -s reccmp-report-etag-new.txt ]; then
mv -f reccmp-report-etag-new.txt reccmp-report-etag.txt
fi
arg=()
arg+=(--html /source/reccmp-report.html)
arg+=(--diff /source/reccmp-report-main.json)
arg+=(--svg /source/reccmp.svg)
fi
docker run --platform linux/amd64 \
-e CMAKE_FLAGS="-G Ninja -DCMAKE_BUILD_TYPE=Debug -DMSVC_42_FOR_RECCMP=on" \
-v .:/source \
-v ./build_msvc42:/build \
-v /opt/carma/:/original:ro \
msvc420-wine -- \
reccmp-reccmp --target CARM95 "${arg[@]}"
}
```
### Make a pull request change
reccmp will run against the code in the PR branch. If any functions decrease in accuracy the PR validation will fail.
reccmp will run against the code in the PR branch. If any functions decrease in accuracy the PR validation will emit a warning. If the functions which decreased were not changed in the PR, the PR can still be accepted, as the decrease will generally be due to compiler entropy.
When the PR is merged, the updated report is stored in https://github.com/dethrace-labs/reccmp-report, and this is used to compare the next PR
When the PR is merged into `main`, the updated report is stored in https://github.com/dethrace-labs/reccmp-report, and this is used to compare the next PR

View File

@ -472,6 +472,10 @@ void InitialiseCar2(tCar_spec* pCar, int pClear_disabled_flag) {
case eDriver_local_human:
pCar->car_ID = 0;
break;
#ifdef DETHRACE_FIX_BUGS
default:
break;
#endif
}
PossibleService();
pCar->box_face_ref = gFace_num__car - 2;
@ -1327,7 +1331,7 @@ void ApplyPhysicsToCars(tU32 last_frame_time, tU32 pTime_difference) {
}
for (i = 0; i < gNum_active_cars; i++) {
car = gActive_car_list[i];
car_info = car;
car_info = (tCollision_info*)car;
car->dt = -1.f;
if (car->message.type == NETMSGID_MECHANICS && car->message.time >= gLast_mechanics_time && car->message.time <= gLast_mechanics_time + time_step) {
// time between car message and next mechanics
@ -1370,11 +1374,11 @@ void ApplyPhysicsToCars(tU32 last_frame_time, tU32 pTime_difference) {
if (non_car->collision_info.doing_nothing_flag) {
continue;
}
car_info = non_car;
car_info = (tCollision_info*)non_car;
car_info->dt = -1.f;
if (car_info->message.type == NETMSGID_NONCAR_INFO && car_info->message.time >= gLast_mechanics_time && gLast_mechanics_time + time_step >= car_info->message.time) {
car_info->dt = (gLast_mechanics_time + time_step - car_info->message.time) / 1000.0f;
GetNetPos(car_info);
GetNetPos((tCar_spec*)car_info);
}
if (car_info->box_face_ref != gFace_num__car
&& (car_info->box_face_ref != gFace_num__car - 1

View File

@ -1177,7 +1177,7 @@ int DoCrashEarnings(tCar_spec* pCar1, tCar_spec* pCar2) {
credits_squared = sqr(0.7f / victim->car_model_actors[victim->principal_car_actor].crush_data.softness_factor) * gWasted_creds[gProgram_state.skill_level] + 50.0f;
credits = 100 * (int)(credits_squared / 100.0f);
if (gNet_mode) {
message = NetBuildMessage(0x18u, 0);
message = NetBuildMessage(NETMSGID_WASTED, 0);
message->contents.data.wasted.victim = NetPlayerFromCar(victim)->ID;
if (NetPlayerFromCar(culprit)) {
message->contents.data.wasted.culprit = NetPlayerFromCar(culprit)->ID;

View File

@ -260,9 +260,9 @@ br_scalar EdgeU(br_angle pSky, br_angle pView, br_angle pPerfect) {
br_scalar b;
br_scalar c;
a = cos(BrAngleToRadian(pPerfect)) * cos(BrAngleToRadian(pPerfect));
b = sin(BrAngleToRadian(pView));
c = cos(BrAngleToRadian(pView) + 1.0f) * BrAngleToRadian(pSky);
a = BR_COS(pPerfect) * BR_COS(pPerfect);
b = BR_SIN(pView);
c = (BR_COS(pView) + 1.0f) * BrAngleToRadian(pSky);
return b * a / c;
}

View File

@ -324,7 +324,7 @@ void ProcessOilSpills(tU32 pFrame_period) {
gOily_spills[i].stop_time = 0;
SetInitialOilStuff(&gOily_spills[i], the_model);
if (gNet_mode != eNet_mode_none) {
message = NetBuildMessage(30, 0);
message = NetBuildMessage(NETMSGID_OILSPILL, 0);
message->contents.data.oil_spill.player = NetPlayerFromCar(gOily_spills[i].car)->ID;
message->contents.data.oil_spill.full_size = gOily_spills[i].full_size;
message->contents.data.oil_spill.grow_rate = gOily_spills[i].grow_rate;

View File

@ -179,7 +179,7 @@ void LosePowerupX(tPowerup* pThe_powerup, int pTell_net_players) {
pThe_powerup->lose_proc(pThe_powerup, pThe_powerup->car);
}
if (gNet_mode != eNet_mode_none) {
the_message = NetBuildMessage(21, 0);
the_message = NetBuildMessage(NETMSGID_POWERUP, 0);
the_message->contents.data.powerup.event = ePowerup_lost;
the_message->contents.data.powerup.player = gLocal_net_ID;
the_message->contents.data.powerup.event = GET_POWERUP_INDEX(pThe_powerup);
@ -287,7 +287,7 @@ int GotPowerupX(tCar_spec* pCar, int pIndex, int pTell_net_players, int pDisplay
PratcamEvent(the_powerup->prat_cam_event);
}
if (gNet_mode != eNet_mode_none && pTell_net_players && pIndex == original_index && !ps_power) {
the_message = NetBuildMessage(21, 0);
the_message = NetBuildMessage(NETMSGID_POWERUP, 0);
the_message->contents.data.powerup.event = ePowerup_gained;
the_message->contents.data.powerup.player = gLocal_net_ID;
the_message->contents.data.powerup.powerup_index = pIndex;

View File

@ -37,7 +37,7 @@ int gRandom_Rockin_MIDI_tunes[3] = { 9500, 9501, 9502 };
// GLOBAL: CARM95 0x00514958
int gRandom_CDA_tunes[8] = { 9600, 9601, 9602, 9603, 9604, 9605, 9606, 9607 }; /* dethrace: Changed to size 8 */
// GLOBAL: CARM95 0x00514978
// GLOBAL: CARM95 0x0051498c
int gCDA_is_playing;
// GLOBAL: CARM95 0x0051497c
@ -52,7 +52,7 @@ int gSound_sources_inited;
// GLOBAL: CARM95 0x00514988
int gMusic_available;
// GLOBAL: CARM95 0x0051498c
// GLOBAL: CARM95 0x00514978
tS3_sound_tag gCDA_tag;
// GLOBAL: CARM95 0x00514990
@ -750,13 +750,13 @@ int DRS3StartCDA(tS3_sound_id pCDA_id) {
} while (pCDA_id == gLast_tune);
}
gLast_tune = pCDA_id;
gCDA_is_playing = DRS3StartSoundNoPiping(gMusic_outlet, pCDA_id);
gCDA_tag = DRS3StartSoundNoPiping(gMusic_outlet, pCDA_id);
#if defined(DETHRACE_FIX_BUGS)
// Initial CD music volume was not set correctly
DRS3SetOutletVolume(gMusic_outlet, 42 * gProgram_state.music_volume);
#endif
gCDA_tag = gCDA_is_playing;
if (!gCDA_is_playing) {
gCDA_is_playing = gCDA_tag != 0;
if (gCDA_tag == 0) {
gCD_is_disabled = 1;
S3DisableCDA();
}
@ -765,7 +765,7 @@ int DRS3StartCDA(tS3_sound_id pCDA_id) {
}
}
}
return gCDA_tag;
return gCDA_is_playing;
}
// IDA: int __cdecl DRS3StopCDA()
@ -777,14 +777,14 @@ int DRS3StopCDA(void) {
gCDA_is_playing = 0;
gCDA_tag = 0;
}
return gCDA_tag;
return gCDA_is_playing;
}
// IDA: void __cdecl StartMusic()
// FUNCTION: CARM95 0x00465899
void StartMusic(void) {
if (gCD_fully_installed) {
gCDA_tag = DRS3StartCDA(9999);
gCDA_is_playing = DRS3StartCDA(9999);
}
}

View File

@ -1254,7 +1254,8 @@ tS3_channel* S3GetChannelForTag(tS3_sound_tag tag) {
if (!tag) {
return 0;
}
for (o = gS3_outlets; o && o->id != tag; o = o->next) {
// the first char of tag is the outlet id. See `S3GenerateTag`
for (o = gS3_outlets; o && o->id != (tag & 0xff); o = o->next) {
;
}
if (!o) {

View File

@ -69,7 +69,7 @@ if(MSVC_42_FOR_RECCMP)
target_sources(harness PRIVATE os/null.c)
elseif(WIN32)
target_sources(harness PRIVATE os/windows.c)
target_link_libraries(harness PRIVATE dbghelp ws2_32)
target_link_libraries(harness PRIVATE dbghelp ws2_32 iphlpapi)
elseif(APPLE)
target_sources(harness PRIVATE os/macos.c)
elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux")