Skip to content

Commit 6d37ffa

Browse files
committed
add: new module, ReferenceHandleLimitWarning. chore: minor cleanup and safety checks in other modules
1 parent d1c63d7 commit 6d37ffa

12 files changed

Lines changed: 188 additions & 21 deletions

.Build/F4SE/Plugins/Addictol.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,9 @@ bImageSpaceAdapter = true
159159
# Warns if you have two AddonNode forms with the same index in your load order, which will cause errors with visual effects.
160160
bDuplicateAddonNodeIndex = true
161161

162+
# Warns if you are approaching the reference handle limoit or exceed the reference handle limit, which can cause CTDs.
163+
bReferenceHandleLimit = true
164+
162165

163166
[Additional]
164167

Addictol/Include/AdGameUtils.h

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ namespace RE
99
class TESObjectREFR;
1010
}
1111

12+
1213
namespace Addictol
1314
{
1415
using namespace std::literals;
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
#pragma once
2+
3+
#include <AdModule.h>
4+
5+
namespace Addictol
6+
{
7+
class ModuleReferenceHandleLimitWarning :
8+
public Module
9+
{
10+
public:
11+
ModuleReferenceHandleLimitWarning();
12+
virtual ~ModuleReferenceHandleLimitWarning() = default;
13+
14+
[[nodiscard]] virtual bool DoQuery() const noexcept override;
15+
[[nodiscard]] virtual bool DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override;
16+
[[nodiscard]] virtual bool DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg = nullptr) noexcept override;
17+
[[nodiscard]] virtual bool DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept override;
18+
};
19+
}

Addictol/Source/AdConfigValidation.cpp

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ namespace Addictol
1111

1212
// Known config keys by section, derived from REX::TOML declarations across all modules.
1313
// Update this when adding or removing a config key.
14-
static const std::unordered_map<std::string, std::unordered_set<std::string>> s_knownKeys = {
14+
static const std::unordered_map<std::string_view, std::unordered_set<std::string_view>> s_knownKeys = {
1515
{ "Patches", {
1616
"bThreads", "bLibDeflate", "bLoadScreen", "bProfile", "bAchievements",
1717
"bFacegen", "bMemoryManager", "bSmallBlockAllocator", "bScaleformAllocator",
@@ -32,7 +32,7 @@ namespace Addictol
3232
"bUtilityShader", "bPipBoyCursorConstraints"
3333
}},
3434
{ "Warnings", {
35-
"bImageSpaceAdapter", "bDuplicateAddonNodeIndex"
35+
"bImageSpaceAdapter", "bDuplicateAddonNodeIndex", "bReferenceHandleLimit"
3636
}},
3737
{ "Additional", {
3838
"bDbgFacegenOutput", "bUseNewRedistributable",

Addictol/Source/AdGameUtils.cpp

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
// Causes crashes if put above RE/S/Script.h
1010
#include <RE/C/ConsoleLog.h>
1111

12+
1213
namespace Addictol
1314
{
1415
bool ExecuteCommand(std::string_view a_command, RE::TESObjectREFR *a_targetRef, bool a_silent)
@@ -49,9 +50,7 @@ namespace Addictol
4950
std::string GetFormInfo(RE::TESForm *a_form)
5051
{
5152
if (!a_form)
52-
{
5353
return "{ERROR_NULL_FORM}";
54-
}
5554

5655
RE::TESFile *file = a_form->GetFile(0);
5756
std::string_view editorID = a_form->GetFormEditorID();

Addictol/Source/AdMain.cpp

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#include <AdUtils.h>
22
#include <AdPlugin.h>
33
#include <Windows.h>
4+
#include <string_view>
45

56
#define MAKE_EXE_VERSION_EX(major, minor, build, sub) ((((major) & 0xFF) << 24) | (((minor) & 0xFF) << 16) | (((build) & 0xFFF) << 4) | ((sub) & 0xF))
67
#define MAKE_EXE_VERSION(major, minor, build) MAKE_EXE_VERSION_EX(major, minor, build, 0)
@@ -76,7 +77,7 @@ F4SE_EXPORT bool F4SEAPI F4SEPlugin_Query(const F4SE::QueryInterface* a_f4se, F4
7677
a_info->version = MAKE_EXE_VERSION(VERSION_MAJOR, VERSION_MINOR, VERSION_BUILD);
7778
a_info->name = _PluginName;
7879

79-
if (!std::filesystem::exists(AdGetRuntimeDirectory() + "Data\\F4SE\\Plugins\\version-1-10-163-0.bin"))
80+
if (!std::filesystem::exists(std::format("{}Data\\F4SE\\Plugins\\version-1-10-163-0.bin", AdGetRuntimeDirectory())))
8081
{
8182
MessageBoxA(0, "" _PluginName ": disabled, address library needs to be updated", "Warnings", MB_OK | MB_ICONWARNING);
8283

Addictol/Source/AdRegisterModules.cpp

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@
5454
#include <Modules/AdModulePipBoyCursorConstraints.h>
5555
//#include <Modules/AdModuleAnimSignedCrash.h>
5656
//#include <Modules/AdModuleBethesdaNetCrash.h>
57+
#include <Modules/AdModuleReferenceHandleLimitWarning.h>
5758

5859
// Create patches
5960
static auto sModuleThreads = std::make_shared<Addictol::ModuleThreads>();
@@ -110,6 +111,7 @@ static auto sModuleUtilityShader = std::make_shared<Addictol::ModuleUtilityS
110111
static auto sModulePipBoyCursorConstraints = std::make_shared<Addictol::ModulePipBoyCursorConstraints>();
111112
//static auto sModuleAnimSignedCrash = std::make_shared<Addictol::ModuleAnimSignedCrash>();
112113
//static auto sModuleBethesdaNetCrash = std::make_shared<Addictol::ModuleBethesdaNetCrash>();
114+
static auto sModuleReferenceHandleLimitWarning = std::make_shared<Addictol::ModuleReferenceHandleLimitWarning>();
113115

114116
void AdRegisterPreloadModules()
115117
{
@@ -178,24 +180,27 @@ void AdRegisterModules()
178180
modules.Register(sModuleActorCauseSaveBloat);
179181
//modules.Register(sModuleAnimSignedCrash);
180182
//modules.Register(sModuleBethesdaNetCrash);
183+
//modules.Register(sModuleReferenceHandleLimitWarning);
181184

182185
// Registers other patches
183-
modules.Register(sModuleThreads, kGameDataReady);
184-
modules.Register(sModuleFacegen, kGameDataReady);
185-
modules.Register(sModuleSafeExit, kGameDataReady);
186-
modules.Register(sModuleInteriorNavCut, kGameDataReady);
187-
modules.Register(sModuleControlSamplers, kGameDataReady);
188-
modules.Register(sModuleDuplicateAddonNodeIndex, kGameDataReady);
189-
modules.Register(sModuleLeveledListCrash, kGameDataReady);
190-
modules.Register(sModuleCombatMusic, kGameDataReady);
191-
modules.Register(sModulePipBoyCursorConstraints, kGameDataReady);
192-
modules.Register(sModuleEncounterZoneReset, kGameLoaded);
193-
modules.Register(sModuleInputSwitch, kGameLoaded);
194-
modules.Register(sModuleLoadScreen, kGameLoaded);
195-
modules.Register(sModuleSaveAddedSoundCategories, kGameLoaded);
196-
modules.Register(sModuleUtilityShader, kGameLoaded);
197-
modules.Register(sModuleMaxPapyrusOps, kPostLoad);
198-
modules.Register(sModulePapyrusGC, kPostLoad);
186+
modules.Register(sModuleThreads, kGameDataReady);
187+
modules.Register(sModuleFacegen, kGameDataReady);
188+
modules.Register(sModuleSafeExit, kGameDataReady);
189+
modules.Register(sModuleInteriorNavCut, kGameDataReady);
190+
modules.Register(sModuleControlSamplers, kGameDataReady);
191+
modules.Register(sModuleDuplicateAddonNodeIndex, kGameDataReady);
192+
modules.Register(sModuleLeveledListCrash, kGameDataReady);
193+
modules.Register(sModuleCombatMusic, kGameDataReady);
194+
modules.Register(sModulePipBoyCursorConstraints, kGameDataReady);
195+
modules.Register(sModuleReferenceHandleLimitWarning, kGameDataReady);
196+
modules.Register(sModuleEncounterZoneReset, kGameLoaded);
197+
modules.Register(sModuleInputSwitch, kGameLoaded);
198+
modules.Register(sModuleLoadScreen, kGameLoaded);
199+
modules.Register(sModuleSaveAddedSoundCategories, kGameLoaded);
200+
modules.Register(sModuleUtilityShader, kGameLoaded);
201+
modules.Register(sModuleReferenceHandleLimitWarning, kGameLoaded);
202+
modules.Register(sModuleMaxPapyrusOps, kPostLoad);
203+
modules.Register(sModulePapyrusGC, kPostLoad);
199204

200205
// Profiler - registered at load stage, listener at GameDataReady for report generation
201206
modules.Register(sModuleProfiler);

Addictol/Source/Modules/AdModuleDuplicateAddonNodeIndex.cpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@ namespace Addictol
2020

2121
bool ModuleDuplicateAddonNodeIndex::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message *a_msg) noexcept
2222
{
23+
if (!a_msg)
24+
return true;
25+
2326
RE::TESDataHandler *dataHandler = RE::TESDataHandler::GetSingleton();
2427
if (!dataHandler)
2528
return false;
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
#include <Modules/AdModuleReferenceHandleLimitWarning.h>
2+
#include <AdUtils.h>
3+
#include <AdGameUtils.h>
4+
5+
#include <RE/C/ConsoleLog.h>
6+
7+
// the ratio at which we start warning in console log.
8+
// this can be set to anything, i just set it to 80% for right now
9+
#define WARNING_MIN_RATIO 0.80f
10+
11+
namespace Addictol
12+
{
13+
// credit: Warnings::ReferenceHandleCap in Daytripper 4 by mith077 under MIT
14+
// https://www.nexusmods.com/fallout4/mods/91141?tab=files
15+
16+
// data
17+
static inline constexpr uint32_t MAX_HANDLE_LIMIT = 0x100000;
18+
static const std::string WarningMessage_ReferenceHandleLimitExceeded = "Addictol::ReferenceHandleLimitWarning: "
19+
"Reference Handle Limit Exceeded! This can cause CTDs. Consider trimming your load order for your next save.";
20+
static const std::string WarningMessage_ReferenceHandleLimitAlmostExceeded = "Addictol::ReferenceHandleLimitWarning: "
21+
"TODO";
22+
23+
// cached values
24+
static uint32_t lastCount = 0;
25+
static float lastRatio = 0.0f;
26+
27+
// tested on AE, but should be fine on OG/NG since daytripper's addresses work on OG/NG
28+
static const auto ReferenceHandleArray_addresses = REL::VariantID(1103816, 2688744).address();
29+
static const auto ReferenceHandleArray = reinterpret_cast<uint64_t*>(ReferenceHandleArray_addresses);
30+
31+
static REX::TOML::Bool<> bWarningsReferenceHandleLimit{ "Warnings"sv, "bReferenceHandleLimit"sv, true };
32+
33+
ModuleReferenceHandleLimitWarning::ModuleReferenceHandleLimitWarning() :
34+
Module("Reference Handle Limit Warning", &bWarningsReferenceHandleLimit)
35+
{}
36+
37+
// note: could be useful to have in AdGameUtils but compiler was being annoying
38+
std::string GetMessagingInterfaceString(F4SE::MessagingInterface::Message* a_msg)
39+
{
40+
if (!a_msg)
41+
return "ERROR_NULL_MESSAGE";
42+
43+
switch (a_msg->type) {
44+
case F4SE::MessagingInterface::kPostLoad:
45+
return "kPostLoad";
46+
case F4SE::MessagingInterface::kPostPostLoad:
47+
return "kPostPostLoad";
48+
case F4SE::MessagingInterface::kPreLoadGame:
49+
return "kPreLoadGame";
50+
case F4SE::MessagingInterface::kPostLoadGame:
51+
return "kPostLoadGame";
52+
case F4SE::MessagingInterface::kPreSaveGame:
53+
return "kPreSaveGame";
54+
case F4SE::MessagingInterface::kPostSaveGame:
55+
return "kPostSaveGame";
56+
case F4SE::MessagingInterface::kDeleteGame:
57+
return "kDeleteGame";
58+
case F4SE::MessagingInterface::kInputLoaded:
59+
return "kInputLoaded";
60+
case F4SE::MessagingInterface::kNewGame:
61+
return "kNewGame";
62+
case F4SE::MessagingInterface::kGameLoaded:
63+
return "kGameLoaded";
64+
case F4SE::MessagingInterface::kGameDataReady:
65+
return "kGameDataReady";
66+
default:
67+
return "ERROR_NO_TYPE";
68+
}
69+
70+
return "ERROR_IDEK_HONESTLY";
71+
}
72+
73+
uint32_t GetReferenceHandleCount() noexcept
74+
{
75+
uint32_t count = 0;
76+
for (uint32_t i = 0; i < MAX_HANDLE_LIMIT; i++) {
77+
if ((ReferenceHandleArray[i] >> 26) & 1) {
78+
count++;
79+
}
80+
}
81+
82+
return count;
83+
}
84+
85+
void CheckReferenceHandleLimit(std::string_view eventName, bool cacheResults = true) noexcept
86+
{
87+
uint32_t count = GetReferenceHandleCount();
88+
float ratio = static_cast<float>(count) / static_cast<float>(MAX_HANDLE_LIMIT);
89+
90+
if (cacheResults) {
91+
// we can cache it so we dont have to iterate again unless we need to measure it again
92+
lastCount = count;
93+
lastRatio = ratio;
94+
}
95+
96+
REX::INFO("Reference Handle Count ({}): {}. Ratio: {:.4}%"sv, eventName, count, ratio);
97+
98+
// warn if needed
99+
if (ratio >= WARNING_MIN_RATIO) {
100+
REX::WARN(std::string_view(WarningMessage_ReferenceHandleLimitExceeded));
101+
102+
auto* consoleLog = RE::ConsoleLog::GetSingleton();
103+
if (consoleLog) {
104+
consoleLog->AddString(WarningMessage_ReferenceHandleLimitExceeded.c_str());
105+
}
106+
}
107+
}
108+
109+
bool ModuleReferenceHandleLimitWarning::DoQuery() const noexcept
110+
{
111+
return true;
112+
}
113+
114+
bool ModuleReferenceHandleLimitWarning::DoInstall([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept
115+
{
116+
if (!a_msg)
117+
return true;
118+
119+
std::string eventName = GetMessagingInterfaceString(a_msg);
120+
CheckReferenceHandleLimit(eventName);
121+
122+
return true;
123+
}
124+
125+
bool ModuleReferenceHandleLimitWarning::DoListener([[maybe_unused]] F4SE::MessagingInterface::Message* a_msg) noexcept
126+
{
127+
return true;
128+
}
129+
130+
bool ModuleReferenceHandleLimitWarning::DoPapyrusListener([[maybe_unused]] RE::BSScript::IVirtualMachine* a_vm) noexcept
131+
{
132+
return true;
133+
}
134+
}

VC/Addictol.vcxproj

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@
6868
<ClCompile Include="..\Addictol\Source\Modules\AdModulePipBoyLightInv.cpp" />
6969
<ClCompile Include="..\Addictol\Source\Modules\AdModuleProfile.cpp" />
7070
<ClCompile Include="..\Addictol\Source\Modules\AdModuleProfiler.cpp" />
71+
<ClCompile Include="..\Addictol\Source\Modules\AdModuleReferenceHandleLimitWarning.cpp" />
7172
<ClCompile Include="..\Addictol\Source\Modules\AdModuleSafeExit.cpp" />
7273
<ClCompile Include="..\Addictol\Source\Modules\AdModuleSaveAddedSoundCategories.cpp" />
7374
<ClCompile Include="..\Addictol\Source\Modules\AdModuleSmallblockAllocator.cpp" />
@@ -139,6 +140,7 @@
139140
<ClInclude Include="..\Addictol\Include\Modules\AdModulePipBoyLightInv.h" />
140141
<ClInclude Include="..\Addictol\Include\Modules\AdModuleProfile.h" />
141142
<ClInclude Include="..\Addictol\Include\Modules\AdModuleProfiler.h" />
143+
<ClInclude Include="..\Addictol\Include\Modules\AdModuleReferenceHandleLimitWarning.h" />
142144
<ClInclude Include="..\Addictol\Include\Modules\AdModuleSafeExit.h" />
143145
<ClInclude Include="..\Addictol\Include\Modules\AdModuleSaveAddedSoundCategories.h" />
144146
<ClInclude Include="..\Addictol\Include\Modules\AdModuleScaleformAllocator.h" />

0 commit comments

Comments
 (0)