Skip to content

Commit 24a3197

Browse files
authored
Merge branch 'master' into feature/hud-component-crosshair
2 parents e892d68 + 53521fa commit 24a3197

74 files changed

Lines changed: 31249 additions & 433 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/tests.yml

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
name: "Tests"
2+
3+
on:
4+
push:
5+
branches: ["master", "tests"]
6+
pull_request:
7+
branches: ["master"]
8+
workflow_dispatch:
9+
10+
jobs:
11+
client-tests:
12+
name: Client Tests (x86 Debug)
13+
runs-on: windows-latest
14+
steps:
15+
- name: Checkout
16+
uses: actions/checkout@v4
17+
18+
- name: Setup MSBuild
19+
uses: microsoft/setup-msbuild@v2
20+
21+
- name: Generate project files
22+
shell: cmd
23+
run: |
24+
utils\premake5.exe install_cef
25+
utils\premake5.exe install_unifont
26+
utils\premake5.exe install_discord
27+
utils\premake5.exe vs2026
28+
29+
- name: Build Tests_Client
30+
shell: cmd
31+
run: msbuild Build\Tests_Client.vcxproj /p:Configuration=Debug /p:Platform=Win32 /p:PlatformToolset=v143 /nologo /v:minimal
32+
33+
- name: Run tests
34+
run: Bin\tests\Tests_Client_d.exe --gtest_output=xml:Bin\tests\test_results.xml
35+
36+
- name: Upload test results
37+
if: always()
38+
uses: actions/upload-artifact@v4
39+
with:
40+
name: test-results
41+
path: Bin/tests/test_results.xml

Client/game_sa/CModelInfoSA.cpp

Lines changed: 76 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -608,7 +608,8 @@ void CModelInfoSA::Remove()
608608
// Remove our reference
609609
if (m_pInterface->usNumberOfRefs > 0)
610610
{
611-
if (CTxdStore_GetTxd(m_pInterface->usTextureDictionary) != nullptr)
611+
if (pGame && !pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(m_pInterface->usTextureDictionary) &&
612+
CTxdStore_GetTxd(m_pInterface->usTextureDictionary) != nullptr)
612613
CTxdStore_RemoveRef(m_pInterface->usTextureDictionary);
613614
m_pInterface->usNumberOfRefs--;
614615
}
@@ -655,6 +656,33 @@ bool CModelInfoSA::IsLoaded()
655656
return false;
656657
}
657658

659+
static void UnlinkStreamingInfoNeighbors(CStreamingInfo* pStreamInfo)
660+
{
661+
if (!pStreamInfo || !pGame)
662+
return;
663+
664+
if (pStreamInfo->loadState == eModelLoadState::LOADSTATE_NOT_LOADED)
665+
return;
666+
667+
CStreaming* pStreaming = pGame->GetStreaming();
668+
if (!pStreaming)
669+
return;
670+
671+
constexpr unsigned short kInvalid = static_cast<unsigned short>(-1);
672+
const unsigned short prev = pStreamInfo->prevId;
673+
const unsigned short next = pStreamInfo->nextId;
674+
675+
CStreamingInfo* pPrev = (prev != kInvalid) ? pStreaming->GetStreamingInfo(prev) : nullptr;
676+
CStreamingInfo* pNext = (next != kInvalid) ? pStreaming->GetStreamingInfo(next) : nullptr;
677+
678+
// If one side already points outside the streaming array, cut the
679+
// reachable side instead of copying the bad link forward.
680+
if (pPrev)
681+
pPrev->nextId = (next == kInvalid || pNext) ? next : kInvalid;
682+
if (pNext)
683+
pNext->prevId = (prev == kInvalid || pPrev) ? prev : kInvalid;
684+
}
685+
658686
bool CModelInfoSA::DoIsLoaded()
659687
{
660688
// return (BOOL)*(BYTE *)(ARRAY_ModelLoaded + 20*dwModelID);
@@ -673,24 +701,7 @@ bool CModelInfoSA::DoIsLoaded()
673701
if (pStreamInfo)
674702
{
675703
// Unlink from SA's loaded-entry list before zeroing the link fields to avoid linked list corruptin
676-
if (pStreamInfo->loadState != eModelLoadState::LOADSTATE_NOT_LOADED)
677-
{
678-
constexpr unsigned short kInvalid = static_cast<unsigned short>(-1);
679-
const unsigned short prev = pStreamInfo->prevId;
680-
const unsigned short next = pStreamInfo->nextId;
681-
if (prev != kInvalid)
682-
{
683-
CStreamingInfo* pPrev = pGame->GetStreaming()->GetStreamingInfo(prev);
684-
if (pPrev)
685-
pPrev->nextId = next;
686-
}
687-
if (next != kInvalid)
688-
{
689-
CStreamingInfo* pNext = pGame->GetStreaming()->GetStreamingInfo(next);
690-
if (pNext)
691-
pNext->prevId = prev;
692-
}
693-
}
704+
UnlinkStreamingInfoNeighbors(pStreamInfo);
694705
pStreamInfo->prevId = static_cast<unsigned short>(-1);
695706
pStreamInfo->nextId = static_cast<unsigned short>(-1);
696707
pStreamInfo->nextInImg = static_cast<unsigned short>(-1);
@@ -1094,25 +1105,39 @@ void CModelInfoSA::SetTextureDictionaryID(unsigned short usID)
10941105
if (usOldTxdId == usID)
10951106
return;
10961107

1108+
// CTxdStore functions (GetTxd, AddRef, RemoveRef) crash on free pool entries.
1109+
if (!pGame || pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(usID))
1110+
return;
1111+
10971112
// Slot allocated (e.g. via engineRequestTXD) but TXD data isn't loaded yet.
1098-
// For unloaded models, just record the new TXD ID; SA streaming handles refs
1099-
// and texture resolution when it loads both the model and its TXD dependency.
1100-
// Loaded models fall through to the ref-transfer path below. CTxdStore_AddRef
1101-
// only touches usUsagesCount (never dereferences rwTexDictonary), and
1102-
// BuildTxdTextureMap returns an empty map for null TXDs, making the rebind a
1103-
// no-op. The real texture data arrives later via engineImageLinkTXD + restream.
1113+
// For unloaded models without geometry, record the new TXD ID and transfer
1114+
// entity refs so callers' pre-added refs are consumed. Loaded models fall
1115+
// through to the ref-transfer path below. CTxdStore_AddRef only touches
1116+
// usUsagesCount (never dereferences rwTexDictonary), and BuildTxdTextureMap
1117+
// returns an empty map for null TXDs, making the rebind a no-op. The real
1118+
// texture data arrives later via engineImageLinkTXD + restream.
11041119
if (CTxdStore_GetTxd(usID) == nullptr)
11051120
{
1106-
if (!pGame || pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(usID))
1107-
return;
1108-
11091121
if (!m_pInterface->pRwObject)
11101122
{
11111123
if (!MapContains(ms_DefaultTxdIDMap, static_cast<unsigned short>(m_dwModelID)))
11121124
ms_DefaultTxdIDMap[static_cast<unsigned short>(m_dwModelID)] = usOldTxdId;
11131125

11141126
++ms_uiTxdAssignmentGeneration;
11151127
m_pInterface->usTextureDictionary = usID;
1128+
1129+
// Transfer entity refs between TXDs even without geometry loaded.
1130+
// Callers that pre-add refs to prevent underflow expect them to be
1131+
// consumed here; skipping this leaks refs on the old TXD.
1132+
size_t referencesCount = m_pInterface->usNumberOfRefs;
1133+
for (size_t i = 0; i < referencesCount; i++)
1134+
CTxdStore_AddRef(usID);
1135+
1136+
if (!pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(usOldTxdId) && CTxdStore_GetTxd(usOldTxdId) != nullptr)
1137+
{
1138+
for (size_t i = 0; i < referencesCount; i++)
1139+
CTxdStore_RemoveRef(usOldTxdId);
1140+
}
11161141
return;
11171142
}
11181143

@@ -1173,9 +1198,9 @@ void CModelInfoSA::SetTextureDictionaryID(unsigned short usID)
11731198
}
11741199
}
11751200

1176-
// Release old TXD refs after rebinding completes
1177-
// Only release if old slot is still valid to avoid crash on stale/orphaned TXD slots
1178-
if (CTxdStore_GetTxd(usOldTxdId) != nullptr)
1201+
// Release old TXD refs after rebinding. Skip if the slot was freed between
1202+
// capturing usOldTxdId and here - CTxdStore functions crash on free entries.
1203+
if (!pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(usOldTxdId) && CTxdStore_GetTxd(usOldTxdId) != nullptr)
11791204
{
11801205
for (size_t i = 0; i < referencesCount; i++)
11811206
CTxdStore_RemoveRef(usOldTxdId);
@@ -2173,6 +2198,13 @@ void CModelInfoSA::ResetVehicleDummies(bool bRemoveFromDummiesMap)
21732198
pVehicleModel = static_cast<CVehicleModelInfoSAInterface*>(GetInterface());
21742199
if (!pVehicleModel || !pVehicleModel->pVisualInfo)
21752200
{
2201+
if (pVehicleModel && pVehicleModel->usNumberOfRefs > 0)
2202+
{
2203+
if (pGame && !pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(pVehicleModel->usTextureDictionary) &&
2204+
CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
2205+
CTxdStore_RemoveRef(pVehicleModel->usTextureDictionary);
2206+
pVehicleModel->usNumberOfRefs--;
2207+
}
21762208
if (bRemoveFromDummiesMap)
21772209
ms_ModelDefaultDummiesPosition.erase(iter);
21782210
return;
@@ -2186,7 +2218,8 @@ void CModelInfoSA::ResetVehicleDummies(bool bRemoveFromDummiesMap)
21862218

21872219
if (pVehicleModel->usNumberOfRefs > 0)
21882220
{
2189-
if (CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
2221+
if (pGame && !pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(pVehicleModel->usTextureDictionary) &&
2222+
CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
21902223
CTxdStore_RemoveRef(pVehicleModel->usTextureDictionary);
21912224
pVehicleModel->usNumberOfRefs--;
21922225
}
@@ -2215,6 +2248,13 @@ void CModelInfoSA::ResetAllVehicleDummies()
22152248
pVehicleModel = static_cast<CVehicleModelInfoSAInterface*>(pModelInfoSA->GetInterface());
22162249
if (!pVehicleModel || !pVehicleModel->pVisualInfo)
22172250
{
2251+
if (pVehicleModel && pVehicleModel->usNumberOfRefs > 0)
2252+
{
2253+
if (pGame && !pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(pVehicleModel->usTextureDictionary) &&
2254+
CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
2255+
CTxdStore_RemoveRef(pVehicleModel->usTextureDictionary);
2256+
pVehicleModel->usNumberOfRefs--;
2257+
}
22182258
it = ms_ModelDefaultDummiesPosition.erase(it);
22192259
continue;
22202260
}
@@ -2225,7 +2265,8 @@ void CModelInfoSA::ResetAllVehicleDummies()
22252265

22262266
if (pVehicleModel->usNumberOfRefs > 0)
22272267
{
2228-
if (CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
2268+
if (pGame && !pGame->GetPools()->GetTxdPool().IsFreeTextureDictonarySlot(pVehicleModel->usTextureDictionary) &&
2269+
CTxdStore_GetTxd(pVehicleModel->usTextureDictionary) != nullptr)
22292270
CTxdStore_RemoveRef(pVehicleModel->usTextureDictionary);
22302271
pVehicleModel->usNumberOfRefs--;
22312272
}
@@ -2764,24 +2805,7 @@ static void UnlinkAndResetStreamingInfo(CStreamingInfo* pStreamInfo)
27642805
if (!pStreamInfo)
27652806
return;
27662807

2767-
if (pStreamInfo->loadState != eModelLoadState::LOADSTATE_NOT_LOADED && pGame && pGame->GetStreaming())
2768-
{
2769-
constexpr unsigned short kInvalid = static_cast<unsigned short>(-1);
2770-
const unsigned short prev = pStreamInfo->prevId;
2771-
const unsigned short next = pStreamInfo->nextId;
2772-
if (prev != kInvalid)
2773-
{
2774-
CStreamingInfo* pPrev = pGame->GetStreaming()->GetStreamingInfo(prev);
2775-
if (pPrev)
2776-
pPrev->nextId = next;
2777-
}
2778-
if (next != kInvalid)
2779-
{
2780-
CStreamingInfo* pNext = pGame->GetStreaming()->GetStreamingInfo(next);
2781-
if (pNext)
2782-
pNext->prevId = prev;
2783-
}
2784-
}
2808+
UnlinkStreamingInfoNeighbors(pStreamInfo);
27852809
*pStreamInfo = CStreamingInfo{};
27862810
pStreamInfo->prevId = static_cast<unsigned short>(-1);
27872811
pStreamInfo->nextId = static_cast<unsigned short>(-1);
@@ -2915,7 +2939,7 @@ void CModelInfoSA::MakeTimedObjectModel(ushort usBaseID)
29152939
pNewInterface->pRwObject = nullptr;
29162940
pNewInterface->usUnknown = 65535;
29172941
pNewInterface->usDynamicIndex = 65535;
2918-
pNewInterface->timeInfo.m_wOtherTimeModel = 0;
2942+
pNewInterface->timeInfo.m_wOtherTimeModel = -1;
29192943

29202944
ppModelInfo[m_dwModelID] = pNewInterface;
29212945

0 commit comments

Comments
 (0)