Skip to content

ControllerIndex/Twin Preservation & EntitySaveStateEx#832

Merged
ConnorForan merged 1 commit intomainfrom
controller-disconnected
Feb 25, 2026
Merged

ControllerIndex/Twin Preservation & EntitySaveStateEx#832
ConnorForan merged 1 commit intomainfrom
controller-disconnected

Conversation

@ConnorForan
Copy link
Collaborator

This PR aims to automatically handle cases where the "Controller Disconnected" popup may appear when continuing modded runs, primarily from attempts to utilize coplayers and twins. It also restores "twin" (InitTwin) relationships after gamestate load.

There are a few different things going on here, but they all serve the same end purpose.

I'm reasonably confident with my approaches, but I am open to suggestions to make any component of this simpler/more secure.

1. ControllerIndex

There are two primary places where the game remaps controllers: PlayerManager::AutoReassignControllers (runs when continuing a run) and PauseScreen::ControllerDisconnectHandling (self explanatory).

In both cases, the game does this by first setting the ControllerIndex of all players to -1 (in the case of ControllerDisconnectHandling, only the players associated with the disconnected controller), then re-assigning available controllers. SetControllerIndex automatically handles setting subplayers/twins/etc when appropriate.

Currently, coplayers or twins spawned by mods are typically mistaken for co-op players, and the game will want to assign them to different controllers.

Notably, the GameState retains the players' ControllerIndexes, they are just ignored/overwritten by AutoReassignControllers on run continue (since those ControllerIndexes may no longer be valid).

In order to try to universally handle this for mods, I've made the following assumption: If multiple players previously had the same ControllerIndex, they should all still have the same ControllerIndex. I've yet to think of a case where handling it this way would be inappropriate (implying that two players had the same ControllerIndex in the GameState but should actually be split apart?)

Basically, in AutoReassignControllers and ControllerDisconnectHandling, when the game "unassigns" a player to -1, we'll do the same to other loose players with their same ControllerIndex, and keep track of all their original ControllerIndex. Then afterwards, when the game "reassigns" a new controller to the player, we'll also assign that controller to the other players we previously assigned/tracked.

Additionally, there's actually a bug in ControllerDisconnectHandling on vanilla Rep+ .15 with no mods: If you try to continue a run where Jacob & Esau are not player 1, without a second controller connected yet, you will be stuck in the "Controller Disconnected" prompt. There seems to be some flaw in the logic where the game assigns a new controller to Jacob (& Esau), then when it gets to Esau it thinks it should unassign Esau (& Jacob), possibly due to still referring to outdated information not realizing that Jacob set Esau's controller too. This repeats. I currently handle this by breaking the player loop after any successful controller reassignment, since it would impact our purposes as well. Any further reassigning that needs to be done will be handled (properly) the next update, after the game re-checks everything and handles it properly.

2. Twins & EntitySaveStateEx

Unlike for the ControllerIndex, we do not have enough information available to restore twin relationships properly by default. So in order to support twins, I've implemented EntitySaveStateEx (piggybacking off of the EntitySaveStateManagement code) to help manage/serialize/deserialize REPENTOGON-internal data that we want to maintain as GameState.

The internals use the same IDs as ESSM. The Clear/Copy/Save/Restore logic, and the write/load/delete of save files, are all called alongside equivalent operations in ESSM. Data is saved as json.

New data needs only to update the Serialize/Deserialize functions & fields in their respective EntitySaveStateEx subclass. For the twins purpose, I write the playerindex of the main twin into the save state. We do not rely on the playerindex actually being the same after quit+continue - we just look for a pair of players with the same value from the restored state and restore their twin relationship (post-PlayerManager::RestoreGameState).

…and continue. To support that, implement EntitySaveStateEx to support persistence of rgon-internal entity data.
@ConnorForan ConnorForan self-assigned this Feb 24, 2026
Copy link
Contributor

@Guantol-Lemat Guantol-Lemat left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The approaches taken to resolve the issues look good to me.
We may want to find a more elegant fix for the ControllerDisconnectHandling bug in the future, but the current solution doesn't seem to be causing any issues.

Since this piggybacks on EntitySaveStateManagement, it might be worth introducing a centralized version of the main operations. That said, it’s up to you whether you’d like to handle that in this PR.

Aside from that, feel free to merge this whenever you want.

{
std::visit([&](auto* obj) { ClearState(obj, clearedIds); }, entity);
}
EntitySaveStateEx::ClearSaveStates(clearedIds);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As an extra it may be worth adding a Notify function for the the main operations. It would take the ID arrays and run the EntitySaveStateEx notify and the Lua callback.

The current approach is fine, due to most call sites being the sole notifiers for a specific event. However since I don't really see a reason for a current or potentially future system to not be notified, making a centralized function would prevent accidentally missing a notification in future changes to EntitySaveStateManagement.

@ConnorForan ConnorForan merged commit dfbcb3a into main Feb 25, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants