Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion libzhl/functions/ASM.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -249,4 +249,10 @@ asm EntityList_destructor_RemoveEntityMainEL "8b0c??8b01ff50??463b77??72??33f6";
asm EntityList_destructor_RemoveEntityPersistentEL "8b0c??8b01ff50??463b77??72??8d9f";
asm EntityList_Reset_RemoveNonPersistentEntity "8b0c??8b01ff50??463b73";
asm EntityList_Update_RemoveEntityMainEL "8b028bcaff50??8b47";
asm EntityList_Update_RemoveEntityPersistentEL "8a82????????8bca";
asm EntityList_Update_RemoveEntityPersistentEL "8a82????????8bca";

asm AutoReassignControllers_Unassign "e8????????8b4f??46";
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";
1 change: 1 addition & 0 deletions libzhl/functions/Entity.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,7 @@ struct Entity depends (AnimationState, ANM2, RNG, Vector, EntityPtr) {
inline Entity_NPC* ToNPC() { return Downcast<Entity_NPC*>(__ptr_EntityNPCRTTI); }
inline Entity_Player* ToPlayer() { return Downcast<Entity_Player*>(__ptr_EntityPlayerRTTI); }
inline Entity_Familiar* ToFamiliar() { return Downcast<Entity_Familiar*>(__ptr_EntityFamiliarRTTI); }
inline Entity_Pickup* ToPickup() { return Downcast<Entity_Pickup*>(__ptr_EntityPickupRTTI); }
inline Entity_Projectile* ToProjectile() { return Downcast<Entity_Projectile*>(__ptr_EntityProjectileRTTI); }

Vector LIBZHL_API Entity::GetPredictedTargetPosition(Entity* target, float multiplier);
Expand Down
3 changes: 3 additions & 0 deletions libzhl/functions/EntityPickup.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -145,3 +145,6 @@ struct Entity_Pickup depends (ANM2, KAGE_SmartPointer_EntitySaveState) : public
ANM2 _altPedestalANM2 : 0x5d0;

} : 0x6f0;

"68(????????)68????????6a0050e8????????83c41485c074??8378??05":
reference void* EntityPickupRTTI;
10 changes: 6 additions & 4 deletions libzhl/functions/EntityPlayer.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -503,7 +503,7 @@ __thiscall void Entity_Player::RevivePlayerGhost();
__thiscall void Entity_Player::SalvageCollectible(Vector * pos, int subtype, unsigned int seed, int poolType);

"558bec6aff68????????64a1????????5083ec14535657a1????????33c5508d45??64a3????????8bf9ffb7":
__thiscall void Entity_Player::SetControllerIndex(unsigned int index, bool includePlayerOwned);
__thiscall void Entity_Player::SetControllerIndex(int index, bool includePlayerOwned);

// Allegedly the boolean is "rightfoot" but I'm not so sure that's true based on the decomp of SetFootprintColor. Seems more like a "force"-type deal.
"558bec566aff":
Expand Down Expand Up @@ -585,7 +585,7 @@ __thiscall void Entity_Player::RestoreGameState_PostLevelInit(GameStatePlayer* s
"558bec83e4f883ec1453568bf157897424":
__thiscall Entity_Pickup* Entity_Player::DropTrinket(Vector* DropPos, bool ReplaceTick);

struct Entity_Player depends (Vector, ANM2, ColorMod, KColor, PlayerCostumeMap, TemporaryEffects, ConsumableData, History, BagOfCraftingOutput, EntityDesc, GameStatePlayer) : public Entity { {{
struct Entity_Player depends (Vector, ANM2, ColorMod, KColor, PlayerCostumeMap, TemporaryEffects, ConsumableData, History, BagOfCraftingOutput, EntityDesc, GameStatePlayer, EntityPtr) : public Entity { {{
inline int* GetMaxHearts() {return &this->_maxHearts; }
inline int* GetRedHearts() {return &this->_redHearts; }
inline int* GetEternalHearts() {return &this->_eternalHearts; }
Expand Down Expand Up @@ -695,7 +695,9 @@ struct Entity_Player depends (Vector, ANM2, ColorMod, KColor, PlayerCostumeMap,
inline BagOfCraftingOutput* GetBagOfCraftingOutput() { return &this->_bagOfCraftingOutput; }
inline uint8_t* GetWildCardItemType() { return &this->_wildCardItemType; }
inline int* GetWildCardItem() { return &this->_wildCardItem; }
inline Entity_Player* GetBackupPlayer() { return this->_backupPlayer; }

inline Entity_Player* GetTwinPlayer() { return (Entity_Player*)this->_twinPlayer.GetReference(); }
inline void SetTwinPlayer(Entity_Player* player) { this->_twinPlayer.SetReference(player); }
}}

ANM2 _bodySprite : 0x41c;
Expand Down Expand Up @@ -798,7 +800,7 @@ struct Entity_Player depends (Vector, ANM2, ColorMod, KColor, PlayerCostumeMap,
int _numGigaBombs : 0x1dc0;
History _history : 0x1dd8;
deque_afterImageFrames _afterImageFrames : 0x1e38;
Entity_Player* _twinPlayer : 0x1e4c;
EntityPtr _twinPlayer : 0x1e4c;
Entity_Player* _backupPlayer : 0x1e50;
int _redStewBonusDuration : 0x1e58;
float _rockBottomMoveSpeed : 0x1e94, _rockBottomMaxFireDelay : 0x1e98, _rockBottomDamage : 0x1e9c, _rockBottomTearRange : 0x1ea0, _rockBottomShotSpeed : 0x1ea4, _rockBottomLuck : 0x1ea8;
Expand Down
1 change: 1 addition & 0 deletions libzhl/functions/GameStatePlayer.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ struct GameStatePlayer depends (History, BagOfCraftingOutput) {
vector_FamiliarData _familiarData : 0x374;
History _history : 0x3b0;
BagOfCraftingOutput _bagOfCraftingOutput : 0x4c4;
int _controllerIndex : 0x518;
} : 0x5bc;

struct FamiliarData {
Expand Down
8 changes: 7 additions & 1 deletion libzhl/functions/PlayerManager.zhl
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,15 @@ __thiscall void PlayerManager::spawn_selected_baby(int param_1, int param_2);
"558bec6aff68????????64a1????????5081ec10010000": //duplicate?
__thiscall void PlayerManager::RemoveCoPlayer(Entity_Player * player, bool unusedBool);

"558bec6aff68????????64a1????????5083ec30535657a1????????33c5508d45??64a3????????894d??8b45":
"558bec6aff68????????64a1????????5083ec28535657a1????????33c5508d45??64a3????????8bc1":
__thiscall void PlayerManager::RestoreGameState(GameState * state);

"558bec6aff68????????64a1????????5083ec10535657a1????????33c5508d45??64a3????????8bf9897d??8b5d":
__thiscall void PlayerManager::RestoreGameState_PostLevelInit(GameState * state);

"558bec6aff68????????64a1????????5083ec24535657a1????????33c5508d45??64a3????????8bf9897d??8b4f":
__thiscall void PlayerManager::AutoReassignControllers();

struct PlayerManager depends (RNG)
{
vector_EntityPlayerPtr _playerList : 0x0;
Expand Down
1 change: 1 addition & 0 deletions repentogon/Patches/ASMPatches.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -275,6 +275,7 @@ void PerformASMPatches() {
//PlayerManager
ASMPatchSpawnSelectedBaby();
ASMPatchCoopWheelRespectModdedAchievements();
ASMPatchReassignControllers();

// Camera
ASMPatchCameraBoundClampOverride();
Expand Down
182 changes: 180 additions & 2 deletions repentogon/Patches/ASMPatches/ASMPlayerManager.cpp
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
#include "ASMDefinition.h"
#include "ASMPatcher.hpp"
#include "HookSystem.h"
#include "../ASMPatches.h"
#include "../XMLPlayerExtras.h"

#include "ASMEntityNPC.h"
#include "../EntityPlus.h"

void ASMPatchSpawnSelectedBaby() {
SigScan scanner("8b4d??6affe8????????8bf8");
Expand Down Expand Up @@ -37,3 +38,180 @@ void ASMPatchCoopWheelRespectModdedAchievements() {
.AddRelativeJump((char*)addr + 0x7);
sASMPatcher.PatchAt(addr, &patch);
}

/*
* The below patches primarily enable preservation of the ControllerIndex for twins and coplayers spawned by mods, preventing the "Controller Disconnected" popups.
*
* For the ControllerIndexes, there are two places where this is relevant: PlayerManager::AutoReassignControllers and PauseScreen::ControllerDisconnectHandling.
* AutoReassignControllers runs once after continuing a run, first setting the ControllerIndex of all players to -1 and then re-assigning the available controllers.
* Vanilla twins/subplayers/etc are handled appropriately by the SetControllerIndex function, but ones created by mods are mistaken for separate co-op players.
* If a player cannot be assigned a controller here (or if a controller is disconnected later) the "Controller Disconnected" popup will kick in.
*
* It is worth noting that GameState remembers and restores the ControllerIndex values for all players, however on continues it is overridden by AutoReassignControllers.
*
* To generally account for mods, we make the assumption that any players that have the same ControllerIndex should be reassigned to the same ControllerIndex.
* This assumption likely makes no difference for vanilla, where twins/subplayers/etc are all already handled. And in general, it feels reasonable.
*
* When the game "unassigns" a controller in either location, we'll unassign every player with the same original ControllerIndex.
* Then, when the game "reassigns" a new controller, we'll also reassign every other player that originally shared their ControllerIndex.
*
* Additionally, the vanilla game's ControllerDisconnectHandling is bugged and will not work in some cases, such as if Jacob & Esau are a co-op player.
* In vanilla Repentance+, attempting to continue a run where Jacob & Esau was player 2+ will cause you to get stuck in the "Controller Disconnected" popup.
* This is because after Jacob's ControllerIndex is reassigned, it will unassign Esau's controller again afterwards, due to referring to outdated information.
* Jacob & Esau's ControllerIndexes are both set at the same time in either case, so this repeats.
*
* To fix this, we force the the game to break its player loop after reassigning a controller in ControllerDisconnectHandling.
* This is a bit of an ungraceful fix, but it works fine in practice because if there are still players to assign, the game will do it next update.
* If there are no more players to assign, the game will properly realize that (due to properly re-checking the status of all controllers).
*/

// Patches over where the game unassigns a controller (PlayerManager::AutoReassignControllers and PauseScreen::ControllerDisconnectHandling).
// Unassign other stray players with the same ControllerIndex and keep track of all their prior ControllerIndexes (ie, modded twins and coplayers).
void __stdcall UnassignController(Entity_Player* player) {
int previousControllerIndex = player->_controllerIndex;

player->SetControllerIndex(-1, true);

EntityPlayerPlus* playerPlus = GetEntityPlayerPlus(player);
if (playerPlus && previousControllerIndex > -1) {
for (Entity_Player* otherPlayer : g_Game->GetPlayerManager()->_playerList) {
EntityPlayerPlus* otherPlayerPlus = GetEntityPlayerPlus(otherPlayer);
if (otherPlayerPlus && otherPlayer->_controllerIndex == previousControllerIndex) {
ZHL::Log("[ASMPlayerManager::UnassignController] Transitively unassigning ControllerIndex %d from player %d (via player %d)\n", previousControllerIndex, otherPlayer->_playerIndex, player->_playerIndex);
otherPlayer->SetControllerIndex(-1, false);
otherPlayerPlus->previousControllerIndex = previousControllerIndex;
}
}
playerPlus->previousControllerIndex = previousControllerIndex;
}
}
static void PatchUnassignController(char* def) {
void* addr = sASMDefinitionHolder->GetDefinition(def);

ZHL::Log("[REPENTOGON] PatchUnassignController @ %p\n", addr);

ASMPatch::SavedRegisters savedRegisters(ASMPatch::SavedRegisters::Registers::GP_REGISTERS_STACKLESS, true);
ASMPatch patch;
patch.Pop(ASMPatch::Registers::EAX)
.Pop(ASMPatch::Registers::EAX)
.PreserveRegisters(savedRegisters)
.Push(ASMPatch::Registers::ECX)
.AddInternalCall(UnassignController)
.RestoreRegisters(savedRegisters)
.AddRelativeJump((char*)addr + 0x5);
sASMPatcher.PatchAt(addr, &patch);
}

// Used to remember that a controller was reassigned in ControllerDisconnectHandling between two patches.
// The second patch is at the end of the for loop and cannot be missed.
bool reassignedDisconnectedController = false;

// Patches over where the game reassigns a controller (PlayerManager::AutoReassignControllers and PauseScreen::ControllerDisconnectHandling).
// Assign the same controller to any other players that previously shared this player's ControllerIndex (ie, modded twins and coplayers).
// For ControllerDisconnectHandling, additionally track that a controller was just reassigned, so that we can force ControllerDisconnectHandling
// to stop and properly re-check everything next update to circumvnet flaws with the vanilla handling.
void __stdcall ReassignController(Entity_Player* player, int newIndex, bool disconnectedController) {
const bool wasUnset = player->_controllerIndex == -1;

player->SetControllerIndex(newIndex, true);

EntityPlayerPlus* playerPlus = GetEntityPlayerPlus(player);
if (wasUnset && playerPlus && playerPlus->previousControllerIndex > -1) {
for (Entity_Player* otherPlayer : g_Game->GetPlayerManager()->_playerList) {
EntityPlayerPlus* otherPlayerPlus = GetEntityPlayerPlus(otherPlayer);
if (otherPlayerPlus && otherPlayer->_controllerIndex == -1 && otherPlayerPlus->previousControllerIndex == playerPlus->previousControllerIndex) {
if (otherPlayer->_controllerIndex != newIndex) {
ZHL::Log("[ASMPlayerManager::ReassignController] Transitively re-assigning ControllerIndex %d to player %d (via player %d)\n", newIndex, otherPlayer->_playerIndex, player->_controllerIndex);
otherPlayer->SetControllerIndex(newIndex, false);
}
otherPlayerPlus->previousControllerIndex = -1;
}
}
playerPlus->previousControllerIndex = -1;
}

if (wasUnset && disconnectedController) {
reassignedDisconnectedController = true;
}
}
static void PatchReassignController(char* def, bool disconnectedController) {
void* addr = sASMDefinitionHolder->GetDefinition(def);

ZHL::Log("[REPENTOGON] PatchReassignController @ %p\n", addr);

ASMPatch::SavedRegisters savedRegisters(ASMPatch::SavedRegisters::Registers::GP_REGISTERS_STACKLESS, true);
ASMPatch patch;
patch.Pop(ASMPatch::Registers::EAX)
.PreserveRegisters(savedRegisters)
.Push(disconnectedController)
.Push(ASMPatch::Registers::EAX)
.Push(ASMPatch::Registers::ECX)
.AddInternalCall(ReassignController)
.RestoreRegisters(savedRegisters)
.Pop(ASMPatch::Registers::EAX)
.AddRelativeJump((char*)addr + 0x5);
sASMPatcher.PatchAt(addr, &patch);
}

// Patch at the end of the player loop in ControllerDisconnectHandling.
// Break out of the loop if we just reassigned a controller.
// The game will not handle things properly if we don't, even for completely vanilla cases,
// such as Jacob & Esau as player 2 (this issue exists in vanilla Rep+).
bool __stdcall PostReassignDisconnectedController() {
if (reassignedDisconnectedController) {
reassignedDisconnectedController = false;
return true;
}
return false;
}
static void PatchPostReassignDisconnectedController() {
void* addr = sASMDefinitionHolder->GetDefinition(&AsmDefinitions::ControllerDisconnected_LoopEnd);

ZHL::Log("[REPENTOGON] PatchPostReassignDisconnectedController @ %p\n", addr);

ASMPatch::SavedRegisters savedRegisters(ASMPatch::SavedRegisters::Registers::GP_REGISTERS_STACKLESS, true);
ASMPatch patch;
patch.PreserveRegisters(savedRegisters)
.AddInternalCall(PostReassignDisconnectedController)
.RestoreRegisters(savedRegisters)
.AddConditionalRelativeJump(ASMPatcher::CondJumps::JNZ, (char*)addr + 0xC) // Jump for true
.AddBytes(ByteBuffer().AddAny((char*)addr, 0x6)) // Restore the bytes we overwrote.
.AddRelativeJump((char*)addr + 0x6);
sASMPatcher.PatchAt(addr, &patch);
}

void ASMPatchReassignControllers() {
PatchUnassignController(&AsmDefinitions::AutoReassignControllers_Unassign);
PatchReassignController(&AsmDefinitions::AutoReassignControllers_Reassign, false);

PatchUnassignController(&AsmDefinitions::ControllerDisconnected_Unassign);
PatchReassignController(&AsmDefinitions::ControllerDisconnected_Reassign, true);

PatchPostReassignDisconnectedController();
}

// Hook after RestoreGameState to restore modded twins.
// We remember twin relationships ourselves (see EntitySaveStateEx.cpp) and restore that relationship here.
// ControllerIndexes are handled fine by the above code.
// restoreTwinID is the index of the main twin when the state was saved.
// We do not rely on the index still being the same - we only look for the pair of players with the same restoreTwinID.
HOOK_METHOD(PlayerManager, RestoreGameState, (GameState* state) -> void) {
super(state);

for (int i = 0; i < _playerList.size(); i++) {
Entity_Player* player = _playerList[i];
if (EntityPlayerPlus* playerPlus = GetEntityPlayerPlus(player); playerPlus && playerPlus->restoreTwinID > -1) {
for (int j = i + 1; j < _playerList.size(); j++) {
Entity_Player* otherPlayer = _playerList[j];
if (EntityPlayerPlus* otherPlayerPlus = GetEntityPlayerPlus(otherPlayer); otherPlayerPlus && otherPlayerPlus->restoreTwinID == playerPlus->restoreTwinID) {
ZHL::Log("[ASMPlayerManager] Restoring twin relationship for players %d and %d\n", player->_playerIndex, otherPlayer->_playerIndex);
player->SetTwinPlayer(otherPlayer);
playerPlus->restoreTwinID = -1;
otherPlayer->SetTwinPlayer(player);
otherPlayerPlus->restoreTwinID = -1;
break;
}
}
}
}
}
1 change: 1 addition & 0 deletions repentogon/Patches/ASMPatches/ASMPlayerManager.h
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,4 @@

void ASMPatchSpawnSelectedBaby();
void ASMPatchCoopWheelRespectModdedAchievements();
void ASMPatchReassignControllers();
2 changes: 2 additions & 0 deletions repentogon/Patches/EntityPlus.h
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ class EntityPlayerPlus : public EntityPlus {
bool evaluatingHealthType = false;
bool disableHealthTypeModification = false;
bool camoOverride = false;
int previousControllerIndex = -1;
int restoreTwinID = -1;
};

// Attributes for EntityFamiliar.
Expand Down
Loading