// Official Resident Evil 4 UHD (Steam) Autosplitter and Load Remover Timer (LRT). 
// Developed by Yuushi with help from Sawken.
// Special thanks to Wipe, Mysterion and Pitted for their work on previous autosplitters of this game which has served as inspiration for this one!
// Special thanks to all the runners who helped by testing the different iterations of this .asl!
// Version 1.0.0 (last modified February 15th, 2025).

// Version 1.1.0 (Latest)
state("bio4", "1.1.0")
{
    // Framecounter components
    byte frameRate        : 0x82B7A0;
    long totalFrames      : 0xCECB18;

    // Misc
    byte menuType         : 0x87AD04;
    byte character        : 0x85F728;
    byte chapter          : 0x85F6FA;
    byte item             : 0x858EE4;
    short room            : 0x85A788;
    uint igt              : 0x85F704;

    // Cutscenes
    string7 movie         : 0x86CE8C;
    string7 cutscene      : 0x803C6E;

    // Loadings
    byte screenState      : 0x85A780;
    byte screenTransition : 0x858F88;

    // Assignment Ada
    long sample           : 0x85F9EC;
    bool isMissionText    : 0x817840;

    // SRT variables
    byte difficulty       : 0x862BDC;
    short da              : 0x85F6F4;
    short health          : 0x85F714;
    short chapterKills    : 0x862BC4;
    uint money            : 0x85F708;
}

// Version 1.0.6 (Old)
state("bio4", "1.0.6")
{
    // Framecounter components
    byte frameRate        : 0x827F38;
    long totalFrames      : 0xCE9298;

    // Misc
    byte menuType         : 0x877484;
    byte character        : 0x85BEA8;
    byte chapter          : 0x85BE7A;
    byte item             : 0x855664;
    short room            : 0x856F08;
    uint igt              : 0x85BE84;

    // Cutscenes
    string7 movie         : 0x8695FC;
    string7 cutscene      : 0x802C6E;

    // Loadings
    byte screenState      : 0x856F00;
    byte screenTransition : 0x855708;

    // Assignment Ada
    long sample           : 0x85C16C;
    bool isMissionText    : 0x814030;

    // SRT variables
    byte difficulty       : 0x85F35C;
    short da              : 0x85BE74;
    short health          : 0x85BE94;
    short chapterKills    : 0x85F344;
    uint money            : 0x85BE88;
}

// Version 1.0.6 (Latest in Japan)
state("bio4", "1.0.6 (Japan)")
{
    // Framecounter components
    byte frameRate        : 0x827F48;
    long totalFrames      : 0xCE9298;

    // Misc
    byte menuType         : 0x877484;
    byte character        : 0x85BEA8;
    byte chapter          : 0x85BE7A;
    byte item             : 0x855664;
    short room            : 0x856F08;
    uint igt              : 0x85BE84;

    // Cutscenes
    string7 movie         : 0x8695FC;
    string7 cutscene      : 0x802C6E;

    // Loadings
    byte screenState      : 0x856F00;
    byte screenTransition : 0x855708;

    // Assignment Ada
    long sample           : 0x85C16C;
    bool isMissionText    : 0x814040;

    // SRT variables
    byte difficulty       : 0x85F35C;
    short da              : 0x85BE74;
    short health          : 0x85BE94;
    short chapterKills    : 0x85F344;
    uint money            : 0x85BE88;
}

startup
{
    // Load the settings
    Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Basic");
    vars.Helper.Settings.CreateFromXml("Components/RE4Splitter.Settings.xml");

    // Check the timing method
    if (timer.CurrentTimingMethod == TimingMethod.RealTime)
    {
        DialogResult timingMessage = MessageBox.Show
        (
            "This game uses Load Removal Time (LRT) as the main timing method.\n"
            + "LiveSplit is currently set to show Real Time (RTA).\n"
            + "Would you like to set the timing method to Game Time?",
            "LiveSplit | Resident Evil 4",
            MessageBoxButtons.YesNo, MessageBoxIcon.Question
        );

        if (timingMessage == DialogResult.Yes)
        {
            timer.CurrentTimingMethod = TimingMethod.GameTime;
        }
    }
}

init
{
    // Check the version of the game
    switch (modules.First().FileVersionInfo.FileVersion)
    {
        case "1.0.0.0":
        case "1.0.0RELEASE_DEV.0":
            version = "1.1.0";
            break;

        case "1.0.18384.1":
        case "1.0.18384.2":
            version = "1.0.6";
            break;

        case "1.0.18384.3":
            version = "1.0.6 (Japan)";
            break;

        default:
            version = "Unknown";
            break;
    }

    // ------------------------------------ Functions ------------------------------------

    // Convert room IDs to short and create a tuple
    Func<int, int, Tuple<short, short>> createRoomIDsTuple = (room1, room2) =>
    {
        return Tuple.Create((short)room1, (short)room2);
    };

    // Update the text component
    vars.updateTextComponent = (Func<string, dynamic>)((text) =>
    {
        // Create the text component
        Func<string, dynamic> createTextComponent = (text1) =>
        {
            dynamic textComponentAssembly = Assembly.LoadFrom("Components\\LiveSplit.Text.dll");
            dynamic newTextComponent = Activator.CreateInstance(textComponentAssembly.GetType("LiveSplit.UI.Components.TextComponent"), timer);
            timer.Layout.LayoutComponents.Add(new LiveSplit.UI.Components.LayoutComponent("LiveSplit.Text.dll", newTextComponent as LiveSplit.UI.Components.IComponent));
            newTextComponent.Settings.Text1 = text1;
            return newTextComponent;
        };

        // Find the text component
        dynamic textComponent = timer.Layout.Components.FirstOrDefault((dynamic tc) => tc.GetType().Name == "TextComponent" && tc.Settings.Text1 == text);
        textComponent = textComponent ?? createTextComponent(text);
        return textComponent.Settings;
    });

    // Initialize the variables when the timer is start or reset
    vars.resetVariables = (Action)(() =>
    {
        vars.completedDoors = new HashSet<Tuple<short, short>>(); // Store the rooms passed
        vars.playedCutscenes = new HashSet<string>();             // Store the cutscenes played
        vars.obtainedKeyItems = new HashSet<string>();            // Store the key items obtained
        vars.obtainedPlagaSamples = new HashSet<long>();          // Store the plaga samples obtained
        vars.elapsedFrames = 0;                                   // Frames elapsed with load removed
        vars.totalPauseCount = 0;                                 // Pauses done in total
        vars.totalPauseBufferCount = 0;                           // Pause buffers done in total
        vars.totalInvCount = 0;                                   // Inventories opened in total
        
        // Timers
        vars.inventoryTime = new Stopwatch();                     
        vars.doorLoadsTime = new Stopwatch();
        vars.optionsTime = new Stopwatch();
        vars.gameplayTime = new Stopwatch();
    });

    // ------------------------------------ Functions ------------------------------------

    // ------------------------------------ Global variables ------------------------------------

    // Create the TimerModel
    vars.timerModel = new TimerModel { CurrentState = timer };

    // Game Modes
    vars.gameModes = new Dictionary<string, string>()
    {
        { "Idle", "Idle" },
        { "MG", "Main Game" },
        { "SW", "Separate Ways" },
        { "AA", "Assignment Ada" }
    };

    // Characters
    vars.characters = new string[] { "Leon", "Ashley", "Ada", "Hunk", "Krauser", "Wesker" };

    // Current Game Mode
    vars.gameMode = vars.gameModes["Idle"];

    // Store the room IDs that are not split
    vars.unsplittedDoors = new HashSet<Tuple<short, short>>()
    {
        // Main Game
        createRoomIDsTuple(262, 260), // Chapter 1-1 End
        createRoomIDsTuple(267, 283), // Chapter 1-3 End
        createRoomIDsTuple(527, 518), // Chapter 3-3 End
        createRoomIDsTuple(525, 518), // Chapter 3-4 End
        createRoomIDsTuple(545, 555), // Chapter 4-1 End
        createRoomIDsTuple(541, 549), // Chapter 4-2 End
        createRoomIDsTuple(549, 550), // Chapter 4-3 End
        createRoomIDsTuple(554, 768), // Chapter 4-4 End
        createRoomIDsTuple(789, 790), // Chapter 5-2 End
        createRoomIDsTuple(796, 800), // Chapter 5-3 End
        createRoomIDsTuple(288, 256), // Main Menu → Footpath to the Village
        createRoomIDsTuple(519, 514), // Barracks → Castle Wall
        createRoomIDsTuple(536, 533), // Gatekeeper Hallway → Lord's Room
        createRoomIDsTuple(554, 552), // Pier → Tower Summit
        createRoomIDsTuple(555, 544), // Prophet's Room (Cutscene) → Area before the Mine
        createRoomIDsTuple(790, 778), // Machine Room Entry → Communications Tower (Cutscene)
        createRoomIDsTuple(778, 790), // Communications Tower (Cutscene) → Machine Room Entry
        createRoomIDsTuple(818, 817), // Steel Tower → Before the Steel Tower

        // Separate Ways
        createRoomIDsTuple(1283, 1286), // Chapter 1 End
        createRoomIDsTuple(1289, 1294), // Chapter 2 End
        createRoomIDsTuple(1292, 1298), // Chapter 3 End
        createRoomIDsTuple(1303, 1304), // Chapter 4 End

        // Assignment Ada
        createRoomIDsTuple(288, 1029) // AA Start
    };

    vars.difficultyMaxDA = new Dictionary<byte, Tuple<string, short>>()
    {
        { 1, Tuple.Create("Amateur", (short)3999) },
        { 3, Tuple.Create("Easy", (short)5999) },
        { 5, Tuple.Create("Normal", (short)10999) },
        { 6, Tuple.Create("Pro", (short)10999) }
    };

    // ------------------------------------ Global variables ------------------------------------

    vars.resetVariables();
}

update
{
    // Disable the script if the game version is unknown
    if (version == "Unknown")
    {
        return false;
    }

    // ------------------------------------ When the timer pauses ------------------------------------

    // Door Loads
    bool isDoorLoads = current.screenState != 3 && current.screenState != 6 && current.room != 288;

    bool isOptions = current.screenState == 6;

    // Tutorials (2nd room of 1-1 and last room of 2-1)
    bool isTutorials = current.menuType == 64 && (current.room == 257 || current.room == 279);

    // Add frames only if we're not in any of these situations
    if (!isDoorLoads && !isOptions && !isTutorials)
    {
        vars.elapsedFrames += current.totalFrames - old.totalFrames;
    }

    // ------------------------------------ When the timer pauses ------------------------------------

    // ------------------------------------ SRT text variables ------------------------------------

    // Show Pause Buffer Count (always present)
    vars.gameplayTime.Start();
    bool isPauseBuffer = false;
    if (current.screenState == 6 && old.screenState != 6)
    {
        vars.gameplayTime.Stop();
        if (vars.gameplayTime.Elapsed < TimeSpan.FromSeconds(2))
        {
            isPauseBuffer = true;
        }
    }
    else
    {
        if (current.screenState != 6 && old.screenState == 6)
        {
            vars.gameplayTime.Restart();
        }
    }

    if (isPauseBuffer)
    {
        vars.totalPauseBufferCount++;
        var componentPauseBuffers = vars.updateTextComponent("Pause Buffer Count");
        componentPauseBuffers.Text2 = string.Format("Total: {0}", vars.totalPauseBufferCount);
    }

    // Show Money
    if (settings["ShowMoney"] && current.money != old.money)
    {
        var componentMoney = vars.updateTextComponent("Money");
        componentMoney.Text2 = string.Format("{0} PESETAS", current.money);
    }

    // Show Kills
    if (settings["ShowKills"] && current.chapterKills != old.chapterKills)
    {
        var componentKills = vars.updateTextComponent("Kills");
        componentKills.Text2 = string.Format("Chapter: {0}", current.chapterKills);
    }

    // Show In Game Time
    if (settings["ShowIGT"] && current.igt != old.igt)
    {
        var componentIGT = vars.updateTextComponent("In Game Time");
        componentIGT.Text2 = TimeSpan.FromSeconds(current.igt).ToString("hh\\:mm\\:ss");
    }

    // Show DA
    if (settings["ShowDA"] && current.da != old.da)
    {
        var difficultyNameAndMaxDA = vars.difficultyMaxDA[current.difficulty];
        string difficultyName = difficultyNameAndMaxDA.Item1;
        short maxDA = difficultyNameAndMaxDA.Item2;
        double computedDA = Math.Floor(current.da / ((double)10999 / maxDA));

        var componentDA = vars.updateTextComponent("DA");
        componentDA.Text2 = string.Format("{0} ({1})", computedDA, difficultyName);
    }

    // Show Health
    if (settings["ShowHealth"] && (current.health != old.health || current.character != old.character))
    {
        var componentHealth = vars.updateTextComponent("Health");
        componentHealth.Text2 = string.Format("{0} ({1})", current.health, vars.characters[current.character]);
    }

    // Show Inventory Count
    if (settings["ShowInventoryCount"] && ((current.menuType == 1 || current.menuType == 128) && old.menuType == 0))
    {
        vars.totalInvCount++;
        var componentInvCount = vars.updateTextComponent("Inventory Count");
        componentInvCount.Text2 = string.Format("Total: {0}", vars.totalInvCount);
    }

    // Show Pause Count
    if (settings["ShowPauseCount"] && (current.screenState == 6 && old.screenState != 6))
    {
        vars.totalPauseCount++;
        var componentPauseCount = vars.updateTextComponent("Pause Count");
        componentPauseCount.Text2 = string.Format("Total: {0}", vars.totalPauseCount);
    }

    // Show Inventory Time
    if (settings["ShowInventoryTime"] && (current.menuType == 1 || current.menuType == 128))
    {
        var componentInvTime = vars.updateTextComponent("Inventory Time");
        componentInvTime.Text2 = vars.inventoryTime.Elapsed.ToString("ss\\.fff");

        // Start the timer
        if (current.screenTransition < old.screenTransition && old.screenTransition == 2)
        {
            vars.inventoryTime.Restart();
        }

        // Stop the timer
        if (current.screenTransition > old.screenTransition && old.screenTransition == 0)
        {
            vars.inventoryTime.Stop();
        }
    }

    // Show time passed on doorloads
    if (settings["ShowDoorloadsTime"]) 
    {
        var componentDoorLoads = vars.updateTextComponent("Door Loads");
        componentDoorLoads.Text2 = vars.doorLoadsTime.Elapsed.ToString("mm\\:ss\\.ff");
        if (isDoorLoads) vars.doorLoadsTime.Start();
        else vars.doorLoadsTime.Stop();
    }

    // Show time spent on options
    if (settings["ShowOptionsTime"]) 
    {
        var componentOptions = vars.updateTextComponent("Options");
        componentOptions.Text2 = vars.optionsTime.Elapsed.ToString("mm\\:ss\\.ff");
        if (isOptions) vars.optionsTime.Start();
        else vars.optionsTime.Stop();
    }

    // Reset pause buffers text to 0 when going to the main menu
    if (current.igt == 0 && old.igt > 0) 
    {
        var componentPauseBuffers = vars.updateTextComponent("Pause Buffer Count");
        componentPauseBuffers.Text2 = string.Format("Total: {0}", 0);
    }

    // ------------------------------------ SRT text variables ------------------------------------
}

onStart
{
    // Initialize the variables when the timer starts
    vars.resetVariables();
}

start
{
    if (settings["Segmented"]) 
    {
        // Start the timer when you load any Main Game save (except the very first room) if Seg is checked
        if (settings["MainGameSplits"] && current.room != 288 && current.room != 256 && old.room == 288 && vars.characters[current.character] == "Leon")
        {
            vars.gameMode = vars.gameModes["MG"];
            return true;
        }

        // Start the timer when you load any Separate Ways save (except the very first room)  if Seg is checked
        if (settings["SeparateWaysSplits"] && vars.characters[current.character] == "Ada" && current.room != 288 && current.room != 1280 && old.room == 288)
        {
            vars.gameMode = vars.gameModes["SW"];
            return true;
        }
    }
    else 
    {
        // Start the timer after the Main Game's FMV is skipped if Seg isn't checked
        if (settings["MainGameSplits"] && current.room == 256 && old.room == 288 && vars.characters[current.character] == "Leon")
        {
            vars.gameMode = vars.gameModes["MG"];
            return true;
        }

        // Start the timer after the Separate Ways map is skipped if Seg isn't checked
        if (settings["SeparateWaysSplits"] && current.menuType == 0 && old.menuType == 2 && vars.characters[current.character] == "Ada" && current.room == 1280 && current.da == 3500)
        {
            vars.gameMode = vars.gameModes["SW"];
            return true;
        }
    }

    // Start the timer after the Assignment Ada text is skipped
    if (settings["AssignmentAdaSplits"] && !current.isMissionText && old.isMissionText && vars.characters[current.character] == "Ada" && current.room == 288 && current.da == 4500)
    {
        vars.gameMode = vars.gameModes["AA"];
        return true;
    }
    return false;
}

split
{
    // Door Splits
    if (settings["DoorSplits"] && current.room != old.room && vars.gameMode != vars.gameModes["Idle"])
    {
        // Split if the unsplitted doors or completed doors doesn't contains the old and current room IDs
        if (!vars.unsplittedDoors.Contains(Tuple.Create(old.room, current.room)) && !vars.completedDoors.Contains(Tuple.Create(old.room, current.room)))
        {
            vars.completedDoors.Add(Tuple.Create(old.room, current.room));
            return true;
        }
    }

    // Chapter Splits
    if (settings[string.Format("Chapter{0}", current.chapter)]
        && current.chapter > old.chapter 
        && (vars.gameMode == vars.gameModes["MG"] || vars.gameMode == vars.gameModes["SW"]))
    {
        return true;
    }

    // Key Item Splits
    String itemId = current.room.ToString() + current.item.ToString();
    if (settings[string.Format("Item{0}", itemId)] 
        && current.item != old.item 
        && !vars.obtainedKeyItems.Contains(itemId) 
        && (vars.gameMode == vars.gameModes["MG"] || vars.gameMode == vars.gameModes["SW"]))
    {
        vars.obtainedKeyItems.Add(itemId);
        return true;
    }

    // Event Splits
    String cutsceneId = current.cutscene != "" ? current.room.ToString() + current.cutscene.Substring(4) : "";
    if (settings[string.Format("Event{0}", cutsceneId)]
        && current.cutscene != old.cutscene 
        && !vars.playedCutscenes.Contains(cutsceneId) 
        && (vars.gameMode == vars.gameModes["MG"] || vars.gameMode == vars.gameModes["SW"]))
    {
        vars.playedCutscenes.Add(cutsceneId);
        return true;
    }

    String movieId = current.movie != "" ? current.room.ToString() + current.movie.Substring(4) : "";
    if (settings[string.Format("Event{0}", movieId)]
        && current.movie != old.movie 
        && !vars.playedCutscenes.Contains(movieId) 
        && (vars.gameMode == vars.gameModes["MG"] || vars.gameMode == vars.gameModes["SW"]))
    {
        vars.playedCutscenes.Add(movieId);
        return true;
    }

    // Plaga Sample Splits
    if (settings[string.Format("Sample{0}", vars.obtainedPlagaSamples.Count + 1)] 
        && current.sample > old.sample 
        && !vars.obtainedPlagaSamples.Contains(current.sample) 
        && vars.gameMode == vars.gameModes["AA"])
    {
        vars.obtainedPlagaSamples.Add(current.sample);
        return true;
    }

    // Split at the door you go through to get the Broken Butterfly (for No Merchant)
    if (settings["DoorSplits"]
        && vars.gameMode == vars.gameModes["MG"]
        && current.chapter == 10
        && ((current.room == 527 && old.room == 524) || (current.room == 518 && old.room == 527)))
    {
        return true;
    }

    // Main Game Ending
    if (vars.gameMode == vars.gameModes["MG"] && current.movie != old.movie && movieId == "819ng.")
    {
        return true;
    }

    // Separate Ways Ending
    if (vars.gameMode == vars.gameModes["SW"] && current.movie != old.movie && movieId == "1310s10")
    {
        return true;
    }

    // Assignment Ada Ending
    if (vars.gameMode == vars.gameModes["AA"] && current.cutscene != old.cutscene && cutsceneId == "1038s00")
    {
        return true;
    }
    return false;
}

isLoading
{
    return true;
}

gameTime
{
    // Synchronize the timer with LRT
    return TimeSpan.FromSeconds((double)vars.elapsedFrames / current.frameRate);
}

onReset
{
    // Initialize the variables when the timer resets
    vars.gameMode = vars.gameModes["Idle"];
    vars.resetVariables();
}

reset
{
    // Reset the timer when the IGT is 0
    return current.igt == 0 && old.igt > 0;
}

exit
{
    // Reset the timer when we exit the game
    if (timer.CurrentPhase != TimerPhase.Ended)
    {
        vars.timerModel.Reset();
    }
}