From e93e024e5ccf318e4e0963e39455e9055120af3a Mon Sep 17 00:00:00 2001 From: Syntax-Sculptor <143585501+Syntax-Sculptor@users.noreply.github.com> Date: Wed, 8 Jul 2026 22:18:32 -0700 Subject: [PATCH 1/4] chore: EntityNPCPlus def --- repentogon/Patches/EntityPlus.cpp | 4 ++++ repentogon/Patches/EntityPlus.h | 7 +++++++ 2 files changed, 11 insertions(+) diff --git a/repentogon/Patches/EntityPlus.cpp b/repentogon/Patches/EntityPlus.cpp index 9c78b3976..34e853f3e 100644 --- a/repentogon/Patches/EntityPlus.cpp +++ b/repentogon/Patches/EntityPlus.cpp @@ -136,6 +136,10 @@ EntityKnifePlus* GetEntityKnifePlus(Entity_Knife* knife) { return dynamic_cast(GetEntityPlusHolder(knife, true)->data.get()); } +EntityNPCPlus* GetEntityNPCPlus(Entity_NPC* npc) { + return dynamic_cast(GetEntityPlusHolder(npc, true)->data.get()); +} + // ---------------------------------------------------------------------------------------------------- // -- Make entities obey EntityPlus.isFlyingOverride diff --git a/repentogon/Patches/EntityPlus.h b/repentogon/Patches/EntityPlus.h index 23b5f46dc..4b63c0579 100644 --- a/repentogon/Patches/EntityPlus.h +++ b/repentogon/Patches/EntityPlus.h @@ -56,6 +56,12 @@ class EntityKnifePlus : public EntityPlus { EntityPtr hitboxSource; }; +// Attributes for EntityNPC. +class EntityNPCPlus : public EntityPlus { + public: + int gridPathThreshold = 900; +}; + // Returns a ptr to the base EntityPlus class held by the entity. // In theory should typically not be nullptr as it, or a child class, is initialized on entity Init. EntityPlus* GetEntityPlus(Entity* entity); @@ -67,5 +73,6 @@ EntityFamiliarPlus* GetEntityFamiliarPlus(Entity_Familiar* familiar); EntityTearPlus* GetEntityTearPlus(Entity_Tear* tear); EntityLaserPlus* GetEntityLaserPlus(Entity_Laser* laser); EntityKnifePlus* GetEntityKnifePlus(Entity_Knife* knife); +EntityNPCPlus* GetEntityNPCPlus(Entity_NPC* npc); void ASMPatchesForEntityPlus(); From 738945d9b10410a969e83b1fd56c99b12e8ea2ca Mon Sep 17 00:00:00 2001 From: Syntax-Sculptor <143585501+Syntax-Sculptor@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:37:02 -0700 Subject: [PATCH 2/4] feat: line of sight ASM patch --- libzhl/functions/ASM.zhl | 4 ++ .../LuaInterfaces/Entities/LuaEntity.cpp | 39 +++++++++++++++++++ repentogon/Patches/ASMPatches.cpp | 1 + .../Patches/ASMPatches/ASMEntityNPC.cpp | 36 +++++++++++++++++ repentogon/Patches/ASMPatches/ASMEntityNPC.h | 3 +- repentogon/Patches/EntityPlus.h | 2 +- 6 files changed, 83 insertions(+), 2 deletions(-) diff --git a/libzhl/functions/ASM.zhl b/libzhl/functions/ASM.zhl index 82104b1cc..0a924d0bf 100644 --- a/libzhl/functions/ASM.zhl +++ b/libzhl/functions/ASM.zhl @@ -282,3 +282,7 @@ asm AutoReassignControllers_Reassign "e8????????836d??018b4d"; asm ControllerDisconnected_Unassign "e8????????c645??018d8d"; asm ControllerDisconnected_Reassign "e8????????85ff75??8935"; asm ControllerDisconnected_LoopEnd "8945??3b45??0f82????????85d2"; + +// Pathfinding + +asm NPCAI_Pathfinder_LineOfSightThresholdOverride "516a008d45??8bce"; diff --git a/repentogon/LuaInterfaces/Entities/LuaEntity.cpp b/repentogon/LuaInterfaces/Entities/LuaEntity.cpp index 59ca9ef9d..e48ae5bb7 100644 --- a/repentogon/LuaInterfaces/Entities/LuaEntity.cpp +++ b/repentogon/LuaInterfaces/Entities/LuaEntity.cpp @@ -861,6 +861,42 @@ LUA_FUNCTION(Lua_EntitySetVariant) { return 0; } +LUA_FUNCTION(Lua_EntityGetLineOfSightCostThreshold) { + Entity* ent = lua::GetLuabridgeUserdata(L, 1, lua::Metatables::ENTITY, "Entity"); + EntityPlus* plus = GetEntityPlus(ent); + + if (plus) { + lua_pushinteger(L, plus->lineOfSightCostThreshold); + } + else { + lua_pushinteger(L, 900); + } + + return 1; +} + +LUA_FUNCTION(Lua_EntitySetLineOfSightCostThreshold) { + Entity* ent = lua::GetLuabridgeUserdata(L, 1, lua::Metatables::ENTITY, "Entity"); + int threshold = (int)luaL_checkinteger(L, 2); + EntityPlus* plus = GetEntityPlus(ent); + + if (plus) { + plus->lineOfSightCostThreshold = threshold; + } + + return 0; +} + +LUA_FUNCTION(Lua_EntityResetLineOfSightCostThreshold) { + Entity* ent = lua::GetLuabridgeUserdata(L, 1, lua::Metatables::ENTITY, "Entity"); + EntityPlus* plus = GetEntityPlus(ent); + + if (plus) { + plus->lineOfSightCostThreshold = 900; + } + + return 0; +} HOOK_METHOD(LuaEngine, RegisterClasses, () -> void) { super(); @@ -955,6 +991,9 @@ HOOK_METHOD(LuaEngine, RegisterClasses, () -> void) { { "SetWaterClipFlags", Lua_EntitySetWaterClipFlags }, { "ResetWaterClipFlags", Lua_EntityResetWaterClipFlags }, { "CanDevolve", Lua_EntityCanDevolve }, + { "GetLineOfSightCostThreshold", Lua_EntityGetLineOfSightCostThreshold }, + { "SetLineOfSightCostThreshold", Lua_EntitySetLineOfSightCostThreshold }, + { "ResetLineOfSightCostThreshold", Lua_EntityResetLineOfSightCostThreshold }, { NULL, NULL } }; lua::RegisterFunctions(_state, lua::Metatables::ENTITY, functions); diff --git a/repentogon/Patches/ASMPatches.cpp b/repentogon/Patches/ASMPatches.cpp index 11eb53b80..565a7cd82 100644 --- a/repentogon/Patches/ASMPatches.cpp +++ b/repentogon/Patches/ASMPatches.cpp @@ -210,6 +210,7 @@ void PerformASMPatches() { ASMPatchHushBug(); ASMPatchFireProjectiles(); ASMPatchFireBossProjectiles(); + ASMPatchLineOfSightThreshold(); //ASMPatchApplyFrozenEnemyDeathEffects(); // This was disabled prior to rep+, ignore it! // EntityKnife diff --git a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp index 86e21a9c7..7b0bfa4d5 100644 --- a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp +++ b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp @@ -2,9 +2,11 @@ #include "../ASMPatches.h" #include "ASMEntityNPC.h" +#include "../EntityPlus.h" #include "HookSystem.h" #include "../XMLData.h" +#include "ASMDefinition.h" thread_local FireProjectilesStorage projectilesStorage; @@ -134,6 +136,40 @@ void ASMPatchApplyFrozenEnemyDeathEffects() { sASMPatcher.PatchAt(addr, &patch); } + +int __stdcall GetLineOfSightThresholdOverride(Entity* entity) { + EntityPlus* plus = GetEntityPlus(entity); + + if (plus) { + return plus->lineOfSightCostThreshold; + } + else { + return 900; + } +} + +void ASMPatchLineOfSightThreshold() { + void* addr = sASMDefinitionHolder->GetDefinition(&AsmDefinitions::NPCAI_Pathfinder_LineOfSightThresholdOverride); + printf("[REPENTOGON] Patching NPCAI_Pathfinder::FindGridPath line of sight threshold at %p\n", addr); + + ASMPatch::SavedRegisters reg(ASMPatch::SavedRegisters::GP_REGISTERS_STACKLESS & ~ASMPatch::SavedRegisters::Registers::EAX, true); + + ASMPatch patch; + patch.PreserveRegisters(reg) + .Push(ASMPatch::Registers::ECX) // Original threshold + .AddBytes("\x8B\x07") // mov eax, [edi] (Get Entity* from Pathfinder) + .Push(ASMPatch::Registers::EAX) + .AddInternalCall(GetLineOfSightThresholdOverride) + // Restore our registers and call our functions as is. + .RestoreRegisters(reg) + .Push(ASMPatch::Registers::EAX) + .Push((int8_t)0) + .LoadEffectiveAddress(ASMPatch::Registers::EBP, -0x48, ASMPatch::Registers::EAX) + .AddRelativeJump((char*)addr + 0x6); + + sASMPatcher.PatchAt(addr, &patch); +} + HOOK_METHOD(Entity, IsActiveEnemy, (bool includeDead) -> bool) { if (this->_type >= ENTITY_GAPER && this->_type < ENTITY_EFFECT) { const std::string attr = XMLStuff.EntityData->GetAttributeByTypeVarSub(this->_type, this->_variant, this->_subtype, false, "isactiveenemy"); diff --git a/repentogon/Patches/ASMPatches/ASMEntityNPC.h b/repentogon/Patches/ASMPatches/ASMEntityNPC.h index 9c3781823..3125ff273 100644 --- a/repentogon/Patches/ASMPatches/ASMEntityNPC.h +++ b/repentogon/Patches/ASMPatches/ASMEntityNPC.h @@ -19,4 +19,5 @@ static std::vector& InitProjectileStorage() { void ASMPatchHushBug(); void ASMPatchFireProjectiles(); void ASMPatchFireBossProjectiles(); -void ASMPatchApplyFrozenEnemyDeathEffects(); \ No newline at end of file +void ASMPatchApplyFrozenEnemyDeathEffects(); +void ASMPatchLineOfSightThreshold(); \ No newline at end of file diff --git a/repentogon/Patches/EntityPlus.h b/repentogon/Patches/EntityPlus.h index 4b63c0579..ea74bdcd4 100644 --- a/repentogon/Patches/EntityPlus.h +++ b/repentogon/Patches/EntityPlus.h @@ -11,6 +11,7 @@ class EntityPlus { public: virtual ~EntityPlus() {} + int lineOfSightCostThreshold = 900; std::optional isFlyingOverride = std::nullopt; std::optional waterClipInfoFlagsOverride = std::nullopt; }; @@ -59,7 +60,6 @@ class EntityKnifePlus : public EntityPlus { // Attributes for EntityNPC. class EntityNPCPlus : public EntityPlus { public: - int gridPathThreshold = 900; }; // Returns a ptr to the base EntityPlus class held by the entity. From 2abe303f1f36de6a5acbc0010205d54fe3820b7d Mon Sep 17 00:00:00 2001 From: Syntax-Sculptor <143585501+Syntax-Sculptor@users.noreply.github.com> Date: Thu, 9 Jul 2026 16:54:02 -0700 Subject: [PATCH 3/4] fix: removed redundant push causing crash --- repentogon/Patches/ASMPatches/ASMEntityNPC.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp index 7b0bfa4d5..5b900cdd0 100644 --- a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp +++ b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp @@ -136,7 +136,6 @@ void ASMPatchApplyFrozenEnemyDeathEffects() { sASMPatcher.PatchAt(addr, &patch); } - int __stdcall GetLineOfSightThresholdOverride(Entity* entity) { EntityPlus* plus = GetEntityPlus(entity); @@ -158,7 +157,6 @@ void ASMPatchLineOfSightThreshold() { patch.PreserveRegisters(reg) .Push(ASMPatch::Registers::ECX) // Original threshold .AddBytes("\x8B\x07") // mov eax, [edi] (Get Entity* from Pathfinder) - .Push(ASMPatch::Registers::EAX) .AddInternalCall(GetLineOfSightThresholdOverride) // Restore our registers and call our functions as is. .RestoreRegisters(reg) From aa7c6a38546675df900d4ea167f76e08004c9b93 Mon Sep 17 00:00:00 2001 From: Syntax-Sculptor <143585501+Syntax-Sculptor@users.noreply.github.com> Date: Thu, 9 Jul 2026 18:15:47 -0700 Subject: [PATCH 4/4] refactor: rely on std::optional for safety --- repentogon/LuaInterfaces/Entities/LuaEntity.cpp | 8 ++++---- repentogon/Patches/ASMPatches/ASMEntityNPC.cpp | 13 +++++++------ repentogon/Patches/EntityPlus.h | 2 +- repentogon/resources/scripts/enums_ex.lua | 7 +++++++ 4 files changed, 19 insertions(+), 11 deletions(-) diff --git a/repentogon/LuaInterfaces/Entities/LuaEntity.cpp b/repentogon/LuaInterfaces/Entities/LuaEntity.cpp index e48ae5bb7..1cb189a1a 100644 --- a/repentogon/LuaInterfaces/Entities/LuaEntity.cpp +++ b/repentogon/LuaInterfaces/Entities/LuaEntity.cpp @@ -865,13 +865,13 @@ LUA_FUNCTION(Lua_EntityGetLineOfSightCostThreshold) { Entity* ent = lua::GetLuabridgeUserdata(L, 1, lua::Metatables::ENTITY, "Entity"); EntityPlus* plus = GetEntityPlus(ent); - if (plus) { - lua_pushinteger(L, plus->lineOfSightCostThreshold); + if (plus && plus->lineOfSightCostThreshold.has_value()) { + lua_pushinteger(L, plus->lineOfSightCostThreshold.value()); } else { + // TODO: Have fallback match what decomp does internally lua_pushinteger(L, 900); } - return 1; } @@ -892,7 +892,7 @@ LUA_FUNCTION(Lua_EntityResetLineOfSightCostThreshold) { EntityPlus* plus = GetEntityPlus(ent); if (plus) { - plus->lineOfSightCostThreshold = 900; + plus->lineOfSightCostThreshold = std::nullopt; } return 0; diff --git a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp index 5b900cdd0..12089f60a 100644 --- a/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp +++ b/repentogon/Patches/ASMPatches/ASMEntityNPC.cpp @@ -136,15 +136,15 @@ void ASMPatchApplyFrozenEnemyDeathEffects() { sASMPatcher.PatchAt(addr, &patch); } -int __stdcall GetLineOfSightThresholdOverride(Entity* entity) { + +int __stdcall GetLineOfSightThresholdOverride(Entity* entity, int originalThreshold) { EntityPlus* plus = GetEntityPlus(entity); - if (plus) { - return plus->lineOfSightCostThreshold; - } - else { - return 900; + if (plus && plus->lineOfSightCostThreshold.has_value()) { + return plus->lineOfSightCostThreshold.value(); } + + return originalThreshold; } void ASMPatchLineOfSightThreshold() { @@ -157,6 +157,7 @@ void ASMPatchLineOfSightThreshold() { patch.PreserveRegisters(reg) .Push(ASMPatch::Registers::ECX) // Original threshold .AddBytes("\x8B\x07") // mov eax, [edi] (Get Entity* from Pathfinder) + .Push(ASMPatch::Registers::EAX) .AddInternalCall(GetLineOfSightThresholdOverride) // Restore our registers and call our functions as is. .RestoreRegisters(reg) diff --git a/repentogon/Patches/EntityPlus.h b/repentogon/Patches/EntityPlus.h index ea74bdcd4..d8565ff95 100644 --- a/repentogon/Patches/EntityPlus.h +++ b/repentogon/Patches/EntityPlus.h @@ -11,7 +11,7 @@ class EntityPlus { public: virtual ~EntityPlus() {} - int lineOfSightCostThreshold = 900; + std::optional lineOfSightCostThreshold = std::nullopt; std::optional isFlyingOverride = std::nullopt; std::optional waterClipInfoFlagsOverride = std::nullopt; }; diff --git a/repentogon/resources/scripts/enums_ex.lua b/repentogon/resources/scripts/enums_ex.lua index 11ab700f7..b98943a09 100644 --- a/repentogon/resources/scripts/enums_ex.lua +++ b/repentogon/resources/scripts/enums_ex.lua @@ -3174,6 +3174,13 @@ TrinketSlot = { BELLY_BUTTON = 1, } +GridPathThreshold = { + DEFAULT = 900, + IGNORE_DECAY = 950, + IGNORE_FIREPLACES_AND_DECAY = 1000, + IGNORE_EVERYTHING = 1001 +} + --deprecated enums Achievement.REVERSED_THE_HEIROPHANT = 529