Skip to content

Commit e8b5de6

Browse files
KrzyhauThisAMJ
authored andcommitted
feat: sar_trace_compare, trace logging system
Co-authored by: mlugg <mlugg@mlugg.co.uk>
1 parent 98adf2f commit e8b5de6

File tree

3 files changed

+228
-0
lines changed

3 files changed

+228
-0
lines changed

docs/cvars.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -617,6 +617,7 @@
617617
|sar_trace_bbox_use_hover|0|Move trace bbox to hovered trace point tick on given trace.|
618618
|sar_trace_clear|cmd|sar_trace_clear \<name> - Clear player trace with a given name|
619619
|sar_trace_clear_all|cmd|sar_trace_clear_all - Clear all the traces|
620+
|sar_trace_compare|cmd|sar_trace_compare \<trace 1> \<trace 2> - compares two given recorded traces and shows where differences occurred.|
620621
|sar_trace_draw|0|Display the recorded player trace. Requires cheats|
621622
|sar_trace_draw_hover|1|Display information about the trace at the hovered tick.|
622623
|sar_trace_draw_speed_deltas|0|Display the speed deltas. Requires sar_trace_draw|

src/Features/PlayerTrace.cpp

Lines changed: 207 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,9 @@ void PlayerTrace::AddPoint(std::string trace_name, void *player, int slot, bool
164164
camera->GetEyePos<true>(slot, eyepos, angles);
165165
}
166166

167+
this->EmitLog(Utils::ssprintf("ProcessMovement(%d)", session->GetTick()).c_str());
168+
this->EmitLog(Utils::ssprintf("player @ (%.6f,%.6f,%.6f)", pos.x, pos.y, pos.z).c_str());
169+
167170
HitboxList hitboxes = ConstructHitboxList(pos);
168171

169172
trace.positions[slot].push_back(pos);
@@ -178,6 +181,9 @@ void PlayerTrace::AddPoint(std::string trace_name, void *player, int slot, bool
178181
if (slot == 0) {
179182
PortalLocations portals = ConstructPortalLocations();
180183
trace.portals.push_back(portals);
184+
185+
VphysLocationList vphysLocations = ConstructVphysLocationList();
186+
trace.vphysLocations.push_back(vphysLocations);
181187
}
182188
}
183189
Trace *PlayerTrace::GetTrace(std::string trace_name) {
@@ -649,6 +655,49 @@ HitboxList PlayerTrace::ConstructHitboxList(Vector center) const {
649655
return list;
650656
}
651657

658+
VphysLocationList PlayerTrace::ConstructVphysLocationList() const {
659+
VphysLocationList locationList;
660+
661+
for (int i = 0; i < Offsets::NUM_ENT_ENTRIES; ++i) {
662+
void *ent = server->m_EntPtrArray[i].m_pEntity;
663+
if (!ent) continue;
664+
auto className = server->GetEntityClassName(ent);
665+
666+
const char *allowedClassNames[] = {
667+
"player",
668+
"prop_physics",
669+
"func_physbox",
670+
"prop_weighted_cube",
671+
"prop_monster_box",
672+
"npc_security_camera",
673+
"npc_portal_turret_floor",
674+
"func_brush",
675+
"prop_dynamic",
676+
};
677+
678+
int size = sizeof(allowedClassNames) / sizeof(allowedClassNames[0]);
679+
bool isAllowedEntity = false;
680+
681+
for (int i = 0; i < size; i++) {
682+
if (strcmp(className, allowedClassNames[i]) == 0) {
683+
isAllowedEntity = true;
684+
break;
685+
}
686+
}
687+
if (!isAllowedEntity) continue;
688+
689+
ICollideable *coll = &SE(ent)->collision();
690+
691+
locationList.locations[i] = {
692+
std::string(className),
693+
coll->GetCollisionOrigin(),
694+
coll->GetCollisionAngles()
695+
};
696+
}
697+
698+
return locationList;
699+
}
700+
652701
PortalLocations PlayerTrace::ConstructPortalLocations() const {
653702
if (!sar_trace_portal_record.GetBool()) return PortalLocations{};
654703

@@ -1020,3 +1069,161 @@ CON_COMMAND(sar_trace_export, "sar_trace_export <filename> [trace name] - Export
10201069

10211070
console->Print("Trace successfully exported to '%s'!\n", filename.c_str());
10221071
}
1072+
1073+
CON_COMMAND(sar_trace_compare, "sar_trace_compare <trace 1> <trace 2> - compares two given recorded traces and shows where differences occurred.\n") {
1074+
if (args.ArgC() != 3)
1075+
return console->Print(sar_trace_compare.ThisPtr()->m_pszHelpString);
1076+
1077+
auto trace1Name = args[1];
1078+
auto trace2Name = args[2];
1079+
1080+
auto trace1 = playerTrace->GetTrace(trace1Name);
1081+
auto trace2 = playerTrace->GetTrace(trace2Name);
1082+
if (trace1 == nullptr || trace2 == nullptr) {
1083+
return console->Print("Trace with name \"%s\" does not exist.\n", trace1 == nullptr ? trace1Name : trace2Name);
1084+
}
1085+
1086+
console->Print("Comparing traces \"%s\" and \"%s\":\n", trace1Name, trace2Name);
1087+
1088+
const auto goodColor = Color(110, 247, 76);
1089+
const auto badColor = Color(255, 100, 100);
1090+
1091+
auto startTick1 = tickInternalToUser(0, *trace1);
1092+
auto startTick2 = tickInternalToUser(0, *trace2);
1093+
if (startTick1 != startTick2) {
1094+
console->ColorMsg(badColor, "Mismatch in starting tick: %d <-> %d\n", startTick1, startTick2);
1095+
console->ColorMsg(badColor, "Comparison cancelled.\n");
1096+
return;
1097+
}
1098+
1099+
auto vphysList1 = trace1->vphysLocations;
1100+
auto vphysList2 = trace2->vphysLocations;
1101+
if (vphysList1.size() != vphysList2.size()) {
1102+
console->ColorMsg(badColor, "Mismatch in trace length: %d <-> %d\n", vphysList1.size(), vphysList2.size());
1103+
return;
1104+
}
1105+
1106+
int mismatchCount = 0;
1107+
1108+
auto maxLength = std::min(vphysList1.size(), vphysList2.size());
1109+
for (size_t i = 0; i < maxLength; ++i) {
1110+
1111+
auto userTick = tickInternalToUser(i, *trace1);
1112+
1113+
auto locationsList1 = vphysList1[i].locations;
1114+
auto locationsList2 = vphysList2[i].locations;
1115+
1116+
for (int ent_index = 0; ent_index < Offsets::NUM_ENT_ENTRIES; ++ent_index) {
1117+
auto location1Entry = locationsList1.find(ent_index);
1118+
auto location2Entry = locationsList2.find(ent_index);
1119+
1120+
auto location1Valid = (location1Entry != locationsList1.end());
1121+
auto location2Valid = (location2Entry != locationsList2.end());
1122+
1123+
if (!location1Valid && !location2Valid) {
1124+
continue;
1125+
} else if (!location1Valid || !location2Valid) {
1126+
auto entityName = location1Valid ? (*location1Entry).second.className.c_str() : (*location2Entry).second.className.c_str();
1127+
console->ColorMsg(badColor, "Tick %d Slot %d: entity %s exists in trace \"%s\", but not in trace \"%s\"\n",
1128+
userTick, ent_index, entityName, location1Valid ? trace1Name : trace2Name, location2Valid ? trace1Name : trace2Name);
1129+
mismatchCount++;
1130+
continue;
1131+
}
1132+
1133+
bool mismatch = false;
1134+
1135+
auto location1 = (*location1Entry).second;
1136+
auto location2 = (*location2Entry).second;
1137+
1138+
if (location1.className != location2.className) {
1139+
console->ColorMsg(badColor, "Tick %d Slot %d: mismatch in entity types:\n %s <-> %s\n",
1140+
userTick, ent_index, location1.className.c_str(), location2.className.c_str());
1141+
mismatch = true;
1142+
}
1143+
1144+
if (location1.pos != location2.pos) {
1145+
console->ColorMsg(badColor, "Tick %d Slot %d: mismatch in position of entity %s:\n (%.9f %.9f %.9f) <-> (%.9f %.9f %.9f)\n",
1146+
userTick, ent_index, location1.className.c_str(),
1147+
location1.pos.x, location1.pos.y, location1.pos.z,
1148+
location2.pos.x, location2.pos.y, location2.pos.z
1149+
);
1150+
mismatch = true;
1151+
}
1152+
1153+
if (QAngleToVector(location1.ang) != QAngleToVector(location2.ang)) {
1154+
console->ColorMsg(badColor, "Tick %d Slot %d: mismatch in rotation of entity %s:\n (%.9f %.9f %.9f) <-> (%.9f %.9f %.9f)\n",
1155+
userTick, ent_index, location1.className.c_str(),
1156+
location1.ang.x, location1.ang.y, location1.ang.z,
1157+
location2.ang.x, location2.ang.y, location2.ang.z
1158+
);
1159+
mismatch = true;
1160+
}
1161+
1162+
if (mismatch) {
1163+
mismatchCount++;
1164+
}
1165+
}
1166+
}
1167+
console->Print("Checked %d ticks, found mismatches: %d.\n", maxLength, mismatchCount);
1168+
1169+
if (mismatchCount > 0) {
1170+
console->Print("Mismatch detected: dumping trace logs.\n");
1171+
std::string dump_1_name = Utils::ssprintf("%s_dump.txt", trace1Name);
1172+
FILE *f1 = fopen(dump_1_name.c_str(), "w");
1173+
if (f1) {
1174+
for (auto line : trace1->log_lines) {
1175+
fprintf(f1, "%s\n", line.c_str());
1176+
}
1177+
fclose(f1);
1178+
console->Print("Trace '%s' log dumped to '%s'\n", trace1Name, dump_1_name.c_str());
1179+
}
1180+
std::string dump_2_name = Utils::ssprintf("%s_dump.txt", trace2Name);
1181+
FILE *f2 = fopen(dump_2_name.c_str(), "w");
1182+
if (f2) {
1183+
for (auto line : trace2->log_lines) {
1184+
fprintf(f2, "%s\n", line.c_str());
1185+
}
1186+
fclose(f2);
1187+
console->Print("Trace '%s' log dumped to '%s'\n", trace2Name, dump_2_name.c_str());
1188+
}
1189+
}
1190+
}
1191+
1192+
void PlayerTrace::EnterLogScope(const char *name) {
1193+
if (!playerTrace->ShouldRecord()) return;
1194+
auto trace = this->GetTrace(sar_trace_record.GetString());
1195+
if (!trace) return;
1196+
std::string line = "";
1197+
for (unsigned i = 0; i < trace->log_scope_stack.size(); ++i) {
1198+
line += " ";
1199+
}
1200+
line += Utils::ssprintf("[ENTER %s]", name);
1201+
trace->log_lines.push_back(line);
1202+
trace->log_scope_stack.push_back({name});
1203+
}
1204+
1205+
void PlayerTrace::ExitLogScope() {
1206+
if (!playerTrace->ShouldRecord()) return;
1207+
auto trace = this->GetTrace(sar_trace_record.GetString());
1208+
if (!trace) return;
1209+
std::string line = "";
1210+
if (trace->log_scope_stack.size() == 0) return; // trace probably stopped/started halfway through a log scope
1211+
for (unsigned i = 0; i < trace->log_scope_stack.size() - 1; ++i) {
1212+
line += " ";
1213+
}
1214+
line += Utils::ssprintf("[EXIT %s]", trace->log_scope_stack.back().c_str());
1215+
trace->log_lines.push_back(line);
1216+
trace->log_scope_stack.pop_back();
1217+
}
1218+
1219+
void PlayerTrace::EmitLog(const char *msg) {
1220+
if (!playerTrace->ShouldRecord()) return;
1221+
auto trace = this->GetTrace(sar_trace_record.GetString());
1222+
if (!trace) return;
1223+
std::string line = "";
1224+
for (unsigned i = 0; i < trace->log_scope_stack.size(); ++i) {
1225+
line += " ";
1226+
}
1227+
line += msg;
1228+
trace->log_lines.push_back(line);
1229+
}

src/Features/PlayerTrace.hpp

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,15 @@ struct HitboxList {
2222
std::vector<ObbBox> obb;
2323
};
2424

25+
struct VphysLocationList {
26+
struct Location {
27+
std::string className;
28+
Vector pos;
29+
QAngle ang;
30+
};
31+
std::map<int, Location> locations;
32+
};
33+
2534
struct PortalLocations {
2635
struct PortalLocation {
2736
Vector pos;
@@ -49,6 +58,10 @@ struct Trace {
4958
// Only have one of those, store all the portals in the map
5059
// indiscriminately of player (also ones placed by pedestals etc)
5160
std::vector<PortalLocations> portals;
61+
std::vector<VphysLocationList> vphysLocations;
62+
63+
std::vector<std::string> log_scope_stack;
64+
std::vector<std::string> log_lines;
5265
bool draw = true;
5366
};
5467

@@ -87,6 +100,8 @@ class PlayerTrace : public Feature {
87100
void TeleportAt(std::string trace_name, int slot, int tick, bool eye);
88101
// Construct a list of the hitboxes of all entities near a point
89102
HitboxList ConstructHitboxList(Vector center) const;
103+
// Construct a list of locations of all dynamic entities for verification purposes
104+
VphysLocationList ConstructVphysLocationList() const;
90105
// Construct a list of all portals in the map
91106
PortalLocations ConstructPortalLocations() const;
92107
// Draw info about all traces to a HUD context
@@ -97,6 +112,11 @@ class PlayerTrace : public Feature {
97112
void CheckTraceChanged();
98113
// Get the current trace bbox tick for TAS stuff, or -1 if there isn't one
99114
int GetTasTraceTick();
115+
116+
// Returns an identifier for the scope which should be passed to ExitLogScope
117+
void EnterLogScope(const char *name);
118+
void ExitLogScope();
119+
void EmitLog(const char *msg);
100120
};
101121

102122
extern PlayerTrace *playerTrace;

0 commit comments

Comments
 (0)