Usage - Guantol-Lemat/Isaac.Glowing-Hourglass-Manager GitHub Wiki

Preface

I'm going to be referring to the variables needed to track what is happening in the current frame as a "Game State".
Given that you will need to save copies of these variables in order to remember an older game state in case of a rewind, I suggest inserting all this variables inside of a GamaState table so that you can easily copy the contents without the need to constantly update the code when a new variable needs to be tracked.

NOTE: If you do choose to use this approach you will need to DeepCopy the table rather than using the assignment operator (=), , even though it is used in the code snippets, as that merely creates an Alias for the same table rather than a copy.

Usage

In order to properly synchronize your modded data with the actual game state you should keep track of 3 "Game States":

  1. RewindState
  2. PhantomRewindState
  3. PreviousFloorState
  4. PreCurseRoomDamageHealthState (Only track this if you need information about the player's health)

(You can technically not track PreCurseRoomDamageHealthState, and maybe even PreviousFloorState, and only track PhantomRewindState, if you want ore info on how to remove them you can refer to the Additional Notes)

I'm also going to be referring to the current variables you are tracking as the CurrentGameState

When any of these variables are supposed to be updated the ON_GLOWING_HOURGLASS_GAME_STATE_UPDATE Custom Callback will fire. So you should register a callback function that handles the GameState changes to this custom Callback by using:

    local function OnGHUpdate(_, TransactionID, UpdateType, ShouldOverwriteHealthState, WasPreviousFloorStateNull)
        -- Your Code Here
    end

    YourModReference:AddCallback(GHManager.Callbacks.ON_GLOWING_HOURGLASS_GAME_STATE_UPDATE, OnGHUpdate)

Alternatively, you can also create unique function for each Hourglass Update Type:

    local function OnGHUpdate_New_Stage()
        -- Your Code Here
    end

    YourModReference:AddCallback(GHManager.Callbacks.ON_GLOWING_HOURGLASS_GAME_STATE_UPDATE, OnGHUpdate_New_Stage, GHManager.HourglassUpdate.New_Stage)

â„šī¸ INFO: The Callback is fired most of the times during MC_POST_NEW_ROOM and only during specific situations (HourglassUpdate is of Type "Save_Pre_Room_Clear_State" and "Save_Pre_Curse_Damage_Health") it will fire during MC_PRE_SPAWN_CLEAN_AWARD and MC_ENTITY_TAKE_DMG (respectively).

The way the data is supposed to be updated depends on the type of HourglassUpdate

New_Session

Occurs either when starting a New run or when Continuing a run in which the CanStartTrueCoop flag is still enabled.
Also occurs after R Key is used

    RewindState = CurrentGameState
    PreviousFloorState = nil
    PhantomRewindState = nil

Continued_Session

Occurs once every play session when Continuing a run in which the CanStartTrueCoop flag is disabled

    RewindState = CurrentGameState
    PreviousFloorState = CurrentGameState
    PhantomRewindState = nil

New_State

If you need to track health remember to read the ShouldOverwriteHealthState function argument

    RewindState = CurrentGameState
    PreviousFloorState = CurrentGameState
    PhantomRewindState = nil

    -- add this if you need to track the player's health
    if ShouldOverwriteHealthState then
        RewindState.Health = PreCurseRoomDamageHealthState
        PreviousFloorState.Health = PreCurseRoomDamageHealthState
    end

New_State_Warped

If you need to track health remember to read the ShouldOverwriteHealthState function argument

    if PhantomRewindState then
        RewindState = PhantomRewindState
        PreviousFloorState = PhantomRewindState
    end
    PhantomRewindState = CurrentGameState

Rewind_Previous_Room

Make the callback function read the WasPreviousFloorStateNull function argument

    CurrentGameState = RewindState
    PhantomRewindState = nil

    if WasPreviousFloorStateNull then
        PreviousFloorState = nil
    else
        PreviousFloorState = RewindState -- This is important
    end

Rewind_Current_Room

Functionally the same as Rewind_Previous_Room except that it is unnecessary to update the PreviousFloorState

    CurrentGameState = RewindState
    PhantomRewindState = nil

New_Stage

    RewindState = CurrentGameState
    PhantomRewindState = nil

New_Absolute_Stage

The only difference between this and New_Stage is that this only occurs when PreviousFloorState is null when performing a stage transition, and the player can therefore never rewind back to the previous floor. Attempting a rewind here will trigger the Failed_Stage_Return HourglassUpdate.

Essentially this is akin to a New_Session in almost every aspect, if not for the way it is achieved

    RewindState = CurrentGameState
    PreviousFloorState = nil -- Technically unnecessary as for this event to occur the state must already be nil
    PhantomRewindState = nil

Previous_Stage_Last_Room

    CurrentGameState = PreviousFloorState
    RewindState = PreviousFloorState
    PhantomRewindState = nil

Previous_Stage_Penultimate_Room

The only difference between this and Previous_Stage_Last_Room is that when this executes you are sent to the Penultimate_Room you were in before you left the floor, regardless of whether the room was cleared or not (if the penultimate room was an uncleared Boss Room you will be sent there).

This specific scenario occurs ONLY if the last room transition before the Stage Transition was from anywhere to a CLEARED and VISITED room.

EXCEPT that this rule is broken if the stage transition is done using Forget Me Now, in which case you get the Previous_Stage_Last_Room update type (What the f**k?)

    CurrentGameState = PreviousFloorState
    RewindState = PreviousFloorState
    PhantomRewindState = nil

Failed_Stage_Return

It's essentially a Reskinned Rewind_Current_Room (This only occurs if PreviousFloorState = nil / when you can still start True Coop).

    RewindState = CurrentGameState
    PreviousFloorState = nil
    PhantomRewindState = nil

Save_Pre_Room_Clear_State

Triggered during MC_PRE_SPAWN_CLEAN_AWARD

    PreviousFloorState = CurrentGameState
    PhantomRewindState = CurrentGameState

Save_Pre_Curse_Damage_Health

Triggered ONLY during the FIRST MC_ENTITY_TAKE_DMG where the player is damaged by a Curse Door

    PreviousFloorState = CurrentGameState
    PhantomRewindState = CurrentGameState
    PreCurseRoomDamageHealthState = CurrentGameState

Additional Notes

It seems that the game doesn't have 3 tracked RewindStates (and a 4th just for the player's Health), but only 2 (RewindState and PhantomRewindState).

By my understanding, whenever you use the Glowing Hourglass you are always brought back to the Rewind State, and, whilst the Rewind State constantly updates as you move trough the floor, there are very specific situations in which a Temporary "Phantom" Rewind State is saved rather than the regular Rewind State, which can then overwrite the data that is Present or that is being Recorded inside the Rewind State under even more specific situations.

The reason I decided to keep the other 2 "fake" states, is mostly to for simplicity's sake, though it is admittedly VERY Memory Inefficient, based on the amount of data you need to store within the Game State, but the cases in which you Overwrite the Rewind State with the Temporary data can be seen as confusing, at first.

For PreviousFloorState The problem is caused by the fact that Stage Transitions (New_Stage) act differently compared to Warps (New_State_Warped).

If you "chain" multiple consecutive Warps (you only transition between rooms using Warps) you will rewind back to the Penultimate Phantom Rewind State that was created, hence why this piece of code exists within New_State_Warped:

    if PhantomRewindState then
        RewindState = PhantomRewindState
    end

However if you "chain" multiple consecutive Stage Transitions (you never go trough another room, but only trough another stage), you will Rewind Back to the Start of the chain, no matter how long it was. (Unless you trigger a New_Absolute_Stage which essentially creates a New_Session each time).

So you would be inclined to believe that what happens is that, on a New_Stage you create a Phantom Rewind State but you never attempt to modify the Rewind State. But that doesn't accurately represent what would actually happen, because if a Phantom State does exist before you ever start a Stage Transition chain then the Rewind State does indeed overwrite the Rewind State on the first New_Stage.

So you would need to memorize what caused the Phantom Rewind State to be created (if it exists) and then on a New_Stage, if the cause is not a Stage Transition, then the Rewind State should be fully overwritten by the Phantom Rewind State, then create a new Phantom Rewind State (It needs to be created because, in the case that the player performs a Warp right after they trigger a Stage Transition, you need to know what the Game State was when the new Floor was started).

Moving on to PreCurseRoomDamageHealthState, this is easier to get rid of as the condition is that if a regular Stage Transition is triggered (New_State) and you either move TO or FROM a Curse Room then only the Health of the Rewind State is overwritten by the one stored within the Phantom Rewind State

This does not need to be tracked however since the ShouldOverwriteHealthState function argument already details whether or not this overwrite should take place.