Switch Roads (#1103)

This commit is contained in:
Seemann 2025-10-27 15:22:09 -04:00 committed by GitHub
parent 931177ec43
commit 8853a890d4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 226 additions and 43 deletions

View File

@ -7,6 +7,8 @@
#include "StdInc.h"
#include "PathFind.h"
#include <reversiblebugfixes/Bugs.hpp>
// TODO: Move into the class itself
CVector& s_pathsNeededPosn = *(CVector*)0x977B70;
bool& s_bLoadPathsNeeded = *(bool*)0x96F030;
@ -63,9 +65,9 @@ void CPathFind::InjectHooks() {
//RH_ScopedInstall(AddDynamicLinkBetween2Nodes, 0x4512D0);
RH_ScopedOverloadedInstall(LoadPathFindData, "Area", 0x452F40, void(CPathFind::*)(int32));
RH_ScopedOverloadedInstall(LoadPathFindData, "FromStream", 0x4529F0, void(CPathFind::*)(RwStream*, int32));
//RH_ScopedInstall(SwitchPedRoadsOffInArea, 0x452F00);
//RH_ScopedInstall(SwitchRoadsOffInArea, 0x452C80);
//RH_ScopedInstall(SwitchRoadsOffInAreaForOneRegion, 0x452820);
RH_ScopedInstall(SwitchPedRoadsOffInArea, 0x452F00);
RH_ScopedInstall(SwitchRoadsOffInArea, 0x452C80);
RH_ScopedInstall(SwitchRoadsOffInAreaForOneRegion, 0x452820);
RH_ScopedInstall(ComputeRoute, 0x452760, { .reversed = false });
//RH_ScopedInstall(CompleteNewInterior, 0x452270);
RH_ScopedInstall(SwitchOffNodeAndNeighbours, 0x452160);
@ -114,7 +116,7 @@ void CPathFind::Init() {
ZoneScoped;
static int32 NumTempExternalNodes = 0; // Unused
m_nNumForbiddenAreas = 0;
m_nNumNodeSwitches = 0;
m_loadAreaRequestPending = false;
for (auto i = 0u; i < NUM_TOTAL_PATH_NODE_AREAS; ++i) {
@ -124,7 +126,7 @@ void CPathFind::Init() {
m_pLinkLengths[i] = nullptr;
m_pPathIntersections[i] = nullptr;
m_pNaviLinks[i] = nullptr; // BUG: Out of array bounds write, same as in original code
m_aUnused[i] = nullptr; // BUG: Out of array bounds write, same as in original code
m_aTempNodes[i] = nullptr; // BUG: Out of array bounds write, same as in original code
}
rng::fill(m_interiorIDs, (uint32)-1);
@ -132,7 +134,7 @@ void CPathFind::Init() {
// 0x44E4E0
void CPathFind::ReInit() {
m_nNumForbiddenAreas = 0;
m_nNumNodeSwitches = 0;
m_loadAreaRequestPending = false;
}
@ -201,7 +203,7 @@ bool CPathFind::ThisNodeWillLeadIntoADeadEnd(CPathNode* startNode, CPathNode* en
// 0x44D3B0
void CPathFind::TidyUpNodeSwitchesAfterMission() {
m_nNumForbiddenAreas = std::min(54u, m_nNumForbiddenAreas); // todo: magic number
m_nNumNodeSwitches = std::min(54u, m_nNumNodeSwitches); // todo: magic number
}
// 0x44D400
@ -584,10 +586,38 @@ void CPathFind::MarkRoadNodeAsDontWander(float x, float y, float z) {
}
// 0x452820
void CPathFind::SwitchRoadsOffInAreaForOneRegion(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bLowTraffic, uint8 nodeType, int32 areaId,
uint8 bUnused) {
return plugin::CallMethod<0x452820, CPathFind*, float, float, float, float, float, float, bool, char, int32, bool>(this, xMin, xMax, yMin, yMax, zMin, zMax, bLowTraffic,
nodeType, areaId, bUnused);
void CPathFind::SwitchRoadsOffInAreaForOneRegion(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bCars, int areaId, bool bBackToOriginal) {
assert(areaId >= 0 && areaId < NUM_PATH_MAP_AREAS);
if (!IsAreaLoaded(areaId)) {
return;
}
auto start = bCars ? 0 : m_anNumVehicleNodes[areaId];
auto end = bCars ? m_anNumVehicleNodes[areaId] : m_anNumNodes[areaId];
for (auto nodeIdx = start; nodeIdx < end; ++nodeIdx) {
CPathNode& node = m_pPathNodes[areaId][nodeIdx];
const auto position = node.GetPosition();
if (position.x < xMin || position.x > xMax || position.y < yMin || position.y > yMax || position.z < zMin || position.z > zMax) {
continue;
}
if (!ThisNodeHasToBeSwitchedOff(&node) || node.m_isSwitchedOff == (bBackToOriginal ? node.m_isSwitchedOffOriginal : bSwitchOff)) {
continue;
}
CPathNode* next1{};
CPathNode* next2{};
SwitchOffNodeAndNeighbours(&node, next1, &next2, bSwitchOff, bBackToOriginal);
for (auto iter = next1; iter;) {
SwitchOffNodeAndNeighbours(iter, iter, nullptr, bSwitchOff, bBackToOriginal);
}
for (auto iter = next2; iter;) {
SwitchOffNodeAndNeighbours(iter, iter, nullptr, bSwitchOff, bBackToOriginal);
}
}
}
// NOTSA
@ -659,12 +689,12 @@ void CPathFind::LoadPathFindData(RwStream* stream, int32 areaId) {
for (auto i = 0u; i < numNodes; ++i) {
auto& node = m_pPathNodes[areaId][i];
node.m_isOriginallyOnDeadEnd = node.m_onDeadEnd;
node.m_isSwitchedOffOriginal = node.m_isSwitchedOff;
}
for (auto i = 0u; i < m_nNumForbiddenAreas; ++i) {
auto& area = m_aForbiddenAreas[i];
SwitchRoadsOffInAreaForOneRegion(area.m_fXMin, area.m_fXMax, area.m_fYMin, area.m_fYMax, area.m_fZMin, area.m_fZMax, area.m_bEnable, area.m_nType, areaId, false);
for (auto i = 0u; i < m_nNumNodeSwitches; ++i) {
auto& area = m_aNodeSwitches[i];
SwitchRoadsOffInAreaForOneRegion(area.xMin, area.xMax, area.yMin, area.yMax, area.zMin, area.zMax, area.isOff, area.isCars, areaId, false);
}
for (auto i = 0u; i < NUM_DYNAMIC_LINKS_PER_AREA; ++i) {
rng::fill(m_aDynamicLinksBaseIds[i], -1);
@ -966,8 +996,8 @@ CNodeAddress CPathFind::FindNodeClosestToCoorsFavourDirection(CVector pos, ePath
// 0x5D34C0
bool CPathFind::Save() {
CGenericGameStorage::SaveDataToWorkBuffer(m_nNumForbiddenAreas);
for (auto& area : std::span{ m_aForbiddenAreas, m_nNumForbiddenAreas }) {
CGenericGameStorage::SaveDataToWorkBuffer(m_nNumNodeSwitches);
for (auto& area : std::span{ m_aNodeSwitches, m_nNumNodeSwitches }) {
CGenericGameStorage::SaveDataToWorkBuffer(area);
}
return true;
@ -975,8 +1005,8 @@ bool CPathFind::Save() {
// 0x5D3500
bool CPathFind::Load() {
CGenericGameStorage::LoadDataFromWorkBuffer(m_nNumForbiddenAreas);
for (auto& area : std::span{ m_aForbiddenAreas, m_nNumForbiddenAreas }) {
CGenericGameStorage::LoadDataFromWorkBuffer(m_nNumNodeSwitches);
for (auto& area : std::span{ m_aNodeSwitches, m_nNumNodeSwitches }) {
CGenericGameStorage::LoadDataFromWorkBuffer(area);
}
return true;
@ -1040,11 +1070,8 @@ float CPathFind::FindNodeOrientationForCarPlacement(CNodeAddress nodeInfo) {
}
// 0x452160
void CPathFind::SwitchOffNodeAndNeighbours(CPathNode* node, CPathNode*& outNext1, CPathNode** outNext2, bool isOnDeadEnd, bool setIsDeadEndToOriginal) {
const auto isNodeOnDeadEnd = setIsDeadEndToOriginal
? node->m_isOriginallyOnDeadEnd
: isOnDeadEnd;
node->m_onDeadEnd = isNodeOnDeadEnd;
void CPathFind::SwitchOffNodeAndNeighbours(CPathNode* node, CPathNode*& outNext1, CPathNode** outNext2, bool bWhatToSwitchTo, bool bBackToOriginal) {
node->m_isSwitchedOff = bBackToOriginal ? node->m_isSwitchedOffOriginal : bWhatToSwitchTo;
outNext1 = nullptr;
if (outNext2) {
@ -1059,7 +1086,7 @@ void CPathFind::SwitchOffNodeAndNeighbours(CPathNode* node, CPathNode*& outNext1
if (!linked.HasToBeSwitchedOff()) {
continue;
}
if (linked.m_onDeadEnd == isNodeOnDeadEnd) {
if (linked.m_isSwitchedOff == bWhatToSwitchTo) {
continue;
}
if (CountNeighboursToBeSwitchedOff(*node) > 2) {
@ -1248,3 +1275,63 @@ void CPathFind::AddNodeToList(CPathNode* node, int32 distFromOrigin) {
m_totalNumNodesInPathFindHashTable++;
}
// 0x452C80
void CPathFind::SwitchRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bCars, bool bBackToOriginal) {
for (auto areaId = 0u; areaId < NUM_PATH_MAP_AREAS; ++areaId) {
SwitchRoadsOffInAreaForOneRegion(xMin, xMax, yMin, yMax, zMin, zMax, bSwitchOff, bCars, areaId, bBackToOriginal);
}
for (auto i = 0u; i < m_nNumNodeSwitches; ++i) {
auto* pArea = &m_aNodeSwitches[i];
if (notsa::bugfixes::CPathFind_SwitchRoadsOffInArea_StrayAreas) {
// some missions create both types of switches at the same area, so we store them separately
if (pArea->isCars != bCars) {
continue;
}
// avoid creating stray areas, potentially leaving no space for important areas later in game
// ideally, the script would use SWITCH_ROADS_BACK_TO_ORIGINAL or SWITCH_PED_ROADS_BACK_TO_ORIGINAL but that's not always the case
// so we consider toggling the same area as "back to original"
if (pArea->xMin == xMin && pArea->yMin == yMin && pArea->zMin == zMin && pArea->xMax == xMax && pArea->yMax == yMax && pArea->zMax == zMax && pArea->isOff != bSwitchOff) {
bBackToOriginal = true;
}
}
// If the existing area is completely inside the area we are switching off, remove it
if (pArea->xMin < xMin || pArea->yMin < yMin || pArea->zMin < zMin || pArea->xMax > xMax || pArea->yMax > yMax || pArea->zMax > zMax) {
continue;
}
for (auto j = i; j < m_nNumNodeSwitches - 1; ++j) {
if (notsa::bugfixes::CPathFind_SwitchRoadsOffInArea_StrayAreas) {
m_aNodeSwitches[j] = m_aNodeSwitches[j + 1];
} else {
// R* bug, they messed up with the index
m_aNodeSwitches[i] = m_aNodeSwitches[i + 1];
}
}
--m_nNumNodeSwitches;
--i;
}
if (!bBackToOriginal && m_nNumNodeSwitches < NUM_PATH_MAP_AREAS) {
auto& area = m_aNodeSwitches[m_nNumNodeSwitches];
area.xMin = xMin;
area.xMax = xMax;
area.yMin = yMin;
area.yMax = yMax;
area.zMin = zMin;
area.zMax = zMax;
area.isOff = bSwitchOff;
area.isCars = bCars;
m_nNumNodeSwitches++;
}
}
// 0x452F00
void CPathFind::SwitchPedRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bBackToOriginal) {
SwitchRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, bSwitchOff, false, bBackToOriginal);
}

View File

@ -34,18 +34,18 @@ enum eTrafficLevel {
TRAFFIC_LOW = 3,
};
class CForbiddenArea {
class CNodesSwitchedOnOrOff {
public:
float m_fXMin;
float m_fXMax;
float m_fYMin;
float m_fYMax;
float m_fZMin;
float m_fZMax;
bool m_bEnable;
uint8 m_nType;
float xMin;
float xMax;
float yMin;
float yMax;
float zMin;
float zMax;
bool isOff;
bool isCars;
};
VALIDATE_SIZE(CForbiddenArea, 0x1C);
VALIDATE_SIZE(CNodesSwitchedOnOrOff, 0x1C);
class CCarPathLinkAddress {
public:
@ -137,7 +137,7 @@ public:
uint32 m_bWaterNode : 1;
// byte 1
uint32 m_isOriginallyOnDeadEnd : 1;
uint32 m_isSwitchedOffOriginal : 1;
uint32 unk1 : 1; // not used in paths data files
uint32 m_bDontWander : 1;
uint32 unk2 : 1; // not used in paths data files
@ -202,8 +202,7 @@ public:
uint8* m_pLinkLengths[NUM_TOTAL_PATH_NODE_AREAS]; // 0xB64
CPathIntersectionInfo* m_pPathIntersections[NUM_TOTAL_PATH_NODE_AREAS]; // 0xC84
CCarPathLinkAddress* m_pNaviLinks[NUM_PATH_MAP_AREAS]; // 0xDA4
void* m_aUnused[22]; // 0xEA4
uint32 m_aUnk[NUM_PATH_MAP_AREAS - 22]; // 0xEFC
void* m_aTempNodes[NUM_PATH_MAP_AREAS]; // 0xEA4
uint32 m_anNumNodes[NUM_TOTAL_PATH_NODE_AREAS]; // 0xFA4
uint32 m_anNumVehicleNodes[NUM_TOTAL_PATH_NODE_AREAS]; // 0x10C4
uint32 m_anNumPedNodes[NUM_TOTAL_PATH_NODE_AREAS]; // 0x11E4
@ -213,8 +212,8 @@ public:
int32 m_aDynamicLinksIds[NUM_PATH_MAP_AREAS][NUM_DYNAMIC_LINKS_PER_AREA];
uint32 m_totalNumNodesInPathFindHashTable; // Number of items in total in all buckets of `m_pathFindHashTable`
uint32 m_interiorIDs[NUM_PATH_INTERIOR_AREAS];
uint32 m_nNumForbiddenAreas;
CForbiddenArea m_aForbiddenAreas[NUM_PATH_MAP_AREAS];
uint32 m_nNumNodeSwitches;
CNodesSwitchedOnOrOff m_aNodeSwitches[NUM_PATH_MAP_AREAS];
bool m_loadAreaRequestPending; /// Whenever an area to be loaded was requested via `MakeRequestForNodesToBeLoaded`
// TODO: Replace below with CRect
@ -290,9 +289,9 @@ public:
bool ThisNodeHasToBeSwitchedOff(CPathNode* node);
size_t CountNeighboursToBeSwitchedOff(const CPathNode& node);
void SwitchOffNodeAndNeighbours(CPathNode* node, CPathNode*& outNext1, CPathNode** outNext2, bool lowTraffic, bool backToOriginal);
void SwitchRoadsOffInAreaForOneRegion(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bLowTraffic, uint8 nodeType, int areaId, uint8 bUnused);
void SwitchRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bLowTraffic, uint8 nodeType, bool bForbidden);
void SwitchPedRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bLowTraffic, uint8 nodeType);
void SwitchRoadsOffInAreaForOneRegion(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bCars, int areaId, bool bBackToOriginal);
void SwitchRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bCars, bool bBackToOriginal);
void SwitchPedRoadsOffInArea(float xMin, float xMax, float yMin, float yMax, float zMin, float zMax, bool bSwitchOff, bool bBackToOriginal);
void LoadPathFindData(RwStream* stream, int32 areaId);
void LoadPathFindData(int32 areaId);
void UnMarkAllRoadNodesAsDontWander();

View File

@ -15,6 +15,7 @@ namespace math { void RegisterHandlers(); };
namespace mission { void RegisterHandlers(); };
namespace object { void RegisterHandlers(); };
namespace pad { void RegisterHandlers(); };
namespace path { void RegisterHandlers(); };
namespace ped { void RegisterHandlers(); };
namespace player { void RegisterHandlers(); };
namespace script { void RegisterHandlers(); };

View File

@ -0,0 +1,91 @@
#include <StdInc.h>
#include "Commands.hpp"
#include <CommandParser/Parser.hpp>
#include "PathFind.h"
using namespace notsa::script;
/*!
* Various path commands
*/
namespace {
/// SWITCH_ROADS_ON(01E7)
void SwitchRoadsOn(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, false, true, false);
}
/// SWITCH_ROADS_OFF(01E8)
void SwitchRoadsOff(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, true, true, false);
}
/// SWITCH_ROADS_BACK_TO_ORIGINAL(091D)
void SwitchRoadsBackToOriginal(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, false, true, true);
}
/// SWITCH_PED_ROADS_ON(022A)
void SwitchPedRoadsOn(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchPedRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, false, false);
}
/// SWITCH_PED_ROADS_OFF(022B)
void SwitchPedRoadsOff(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchPedRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, true, false);
}
/// SWITCH_PED_ROADS_BACK_TO_ORIGINAL(091E)
void SwitchPedRoadsBackToOriginal(float xA, float yA, float zA, float xB, float yB, float zB) {
const auto xMin = std::min(xA, xB);
const auto xMax = std::max(xA, xB);
const auto yMin = std::min(yA, yB);
const auto yMax = std::max(yA, yB);
const auto zMin = std::min(zA, zB);
const auto zMax = std::max(zA, zB);
ThePaths.SwitchPedRoadsOffInArea(xMin, xMax, yMin, yMax, zMin, zMax, false, true);
}
};
void notsa::script::commands::path::RegisterHandlers() {
REGISTER_COMMAND_HANDLER_BEGIN("Path");
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_ROADS_ON, SwitchRoadsOn);
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_ROADS_OFF, SwitchRoadsOff);
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_ROADS_BACK_TO_ORIGINAL, SwitchRoadsBackToOriginal);
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_PED_ROADS_ON, SwitchPedRoadsOn);
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_PED_ROADS_OFF, SwitchPedRoadsOff);
REGISTER_COMMAND_HANDLER(COMMAND_SWITCH_PED_ROADS_BACK_TO_ORIGINAL, SwitchPedRoadsBackToOriginal);
}

View File

@ -69,4 +69,9 @@ inline const ReversibleBugFix CAEVehicleAudioEntity_PlayAircraftSound_VolumeFix{
.Description = "Original code didn't account for event base volume",
.Credit = "Pirulax"
};
inline const ReversibleBugFix CPathFind_SwitchRoadsOffInArea_StrayAreas{
.Name = "CPathFind::SwitchRoadsOffInArea Stray-Areas",
.Description = "Fix multiple issues related to saving unused path areas after missions",
.Credit = "Contributors"
};
};