state("Chants Of Sennaar", "v.1.0.0.9")
{
}

startup
{
    // Load the asl-help script and settings
    Assembly.Load(File.ReadAllBytes("Components/asl-help")).CreateInstance("Unity");
    vars.Helper.GameName = "Chants of Sennaar";
    vars.Helper.Settings.CreateFromXml("Components/chantsofsennaar.Settings.xml");
}

init
{
    vars.Helper.TryLoad = (Func<dynamic, bool>)(mono =>
    {
        vars.Helper["cursorOff"] = mono.Make<bool>("GameController", "staticInstance", "inputsController", "cursorOff");

        vars.Helper["titleScreenPtr"] = mono.Make<ulong>("GameController", "staticInstance", "placeController", "titleScreen");
        vars.Helper["currentPlacePtr"] = mono.Make<ulong>("GameController", "staticInstance", "placeController", "currentPlace");
        vars.Helper["currentGameSaveId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "currentGameSaveId");

        vars.Helper["gameSave1LevelId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x20, "currentPlaceId", "level");
        vars.Helper["gameSave1PlaceId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x20, "currentPlaceId", "id");
        vars.Helper["gameSave1PortalId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x20, "currentPortalId");

        vars.Helper["gameSave2LevelId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x28, "currentPlaceId", "level");
        vars.Helper["gameSave2PlaceId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x28, "currentPlaceId", "id");
        vars.Helper["gameSave2PortalId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x28, "currentPortalId");

        vars.Helper["gameSave3LevelId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x30, "currentPlaceId", "level");
        vars.Helper["gameSave3PlaceId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x30, "currentPlaceId", "id");
        vars.Helper["gameSave3PortalId"] = mono.Make<int>("GameController", "staticInstance", "placeController", "gameSaves", 0x30, "currentPortalId");

        // lastMovementDirection is a nested Vector3, but we only need the X coordinate to see if player has moved after intro cutscene.
        vars.Helper["playerLastMovementDirectionX"] = mono.Make<float>("GameController", "staticInstance", "playerController", "playerMove", "lastMovementDirection");
        vars.Helper["canPlayerRun"] = mono.Make<bool>("GameController", "staticInstance", "playerController", "playerMove", "canRun");

        vars.Helper["inventoryState"] = mono.Make<int>("GameController", "staticInstance", "inventory", "state");
        vars.Helper["isInventoryNeedOpen"] = mono.Make<bool>("GameController", "staticInstance", "inventory", "needOpen");
        // Gets the number of lines solved in the linking terminal.
        vars.Helper["terminalProgress"] = mono.Make<int>("GameController", "staticInstance", "uiController", "terminalUI", "terminalLinkUI", "overed");

        return true;
    });

    // Gets the last DateTime that player was on the title screen.
    vars.lastDateTimeOnTitleScreen = null;

    // Gets whether the player went from the title screen to the first cutscene.
    vars.isTitleScreenToNewSave = false;

    // Gets whether the inventory *needs* to be forced open.
    vars.isInventoryForcedOpenNeeded = false;

    // Gets whether the inventory is *actually* forced open.
    // The split block should check this variable to see if an item is picked up.
    vars.isInventoryForcedOpen = false;

    /* Variables for old place */
    vars.oldLevelId = -1;
    vars.oldPlaceId = -1;

    /* Variables for current place */
    vars.currentLevelId = -1;
    vars.currentPlaceId = -1;
    vars.currentPortalId = -1;

    // Function checks if we should split, based on desired old and current place ids, and provided setting names.
    // We use vars.splitDict to ensure we don't split again.
    vars.splitDict = new Dictionary<string, bool>();
    vars.CheckSplit = (Func<int?, int?, string, string, bool>)((oldPlaceId, currentPlaceId, settingName1, settingName2) =>
    {
        var key1 = string.IsNullOrEmpty(settingName1) ? string.Empty : settingName1;
        var key2 = string.IsNullOrEmpty(settingName2) ? string.Empty : settingName2;
        var key = key1 + "||" + key2;
        if(!vars.splitDict.ContainsKey(key))
        {
            vars.splitDict[key] = false;
        }

        var checkOldPlaceId = oldPlaceId == null ? true : vars.oldPlaceId == oldPlaceId;
        var checkCurrentPlaceId = currentPlaceId == null ? true : vars.currentPlaceId == currentPlaceId;
        var checkPlaceIds = checkOldPlaceId && checkCurrentPlaceId;

        var checkSetting1 = string.IsNullOrEmpty(settingName1) ? false : settings[settingName1];
        var checkSetting2 = string.IsNullOrEmpty(settingName2) ? false : settings[settingName2];
        var checkSettings = checkSetting1 || checkSetting2;

        var shouldSplit = checkPlaceIds && checkSettings && !vars.splitDict[key];
        if (shouldSplit)
        {
            vars.splitDict[key] = true;
        }

        return shouldSplit;
    });
}

update
{
    // Determine save slot based on setting, and populate vars with old and current level/place ids.
    if (current.currentGameSaveId == 0)
    {
        vars.oldLevelId = old.gameSave1LevelId;
        vars.oldPlaceId = old.gameSave1PlaceId;
        vars.currentLevelId = current.gameSave1LevelId;
        vars.currentPlaceId = current.gameSave1PlaceId;
        vars.currentPortalId = current.gameSave1PortalId;
    }
    else if (current.currentGameSaveId == 1)
    {
        vars.oldLevelId = old.gameSave2LevelId;
        vars.oldPlaceId = old.gameSave2PlaceId;
        vars.currentLevelId = current.gameSave2LevelId;
        vars.currentPlaceId = current.gameSave2PlaceId;
        vars.currentPortalId = current.gameSave2PortalId;
    }
    else if (current.currentGameSaveId == 2)
    {
        vars.oldLevelId = old.gameSave3LevelId;
        vars.oldPlaceId = old.gameSave3PlaceId;
        vars.currentLevelId = current.gameSave3LevelId;
        vars.currentPlaceId = current.gameSave3PlaceId;
        vars.currentPortalId = current.gameSave3PortalId;
    }
    else
    {
        // No save slot has been selected yet.
        print("No save slot selected yet, exiting early.");
        return false;
    }

    /* The next section checks if inventory needs to be forced open, so we can split when the inventory is actually open. */
    var isInventoryStateOpen = current.inventoryState == 5;

    // Check if inventory was forced open.
    if (vars.isInventoryForcedOpen)
    {
        // Value should only be true for one tick so we split once.
        vars.isInventoryForcedOpen = false;
    }
    // Check if inventory needs to be forced open and is finally open.
    else if (vars.isInventoryForcedOpenNeeded && isInventoryStateOpen)
    {
        vars.isInventoryForcedOpen = true;
        vars.isInventoryForcedOpenNeeded = false;

        return;
    }
    // Check if inventory needs to be forced open.
    else if (!old.isInventoryNeedOpen && current.isInventoryNeedOpen)
    {
        vars.isInventoryForcedOpenNeeded = true;
    }
}

reset
{
    // Check if player is on title screen.
    return current.currentPlacePtr == current.titleScreenPtr && 
        !(vars.oldLevelId == 0 && vars.oldPlaceId == 0) && 
        vars.currentLevelId == 0 &&
        vars.currentPlaceId == 0;
}

onReset
{
    // Reset vars.
    vars.lastDateTimeOnTitleScreen = null;
    vars.isTitleScreenToNewSave = false;
    vars.isInventoryForcedOpenNeeded = false;
    vars.isInventoryForcedOpen = false;

    vars.splitDict = new Dictionary<string, bool>();
}

start
{
    // Check if player is on title screen.
    if (current.currentPlacePtr == current.titleScreenPtr)
    {
        // Store current time and exit early.
        vars.lastDateTimeOnTitleScreen = DateTime.Now;
        vars.isTitleScreenToNewSave = false;
        return false;
    }

    // Check if script started while not on the title screen.
    if (vars.lastDateTimeOnTitleScreen == null)
    {
        return false;
    }

    // Check if player moved from title screen to first cutscene.
    if (vars.isTitleScreenToNewSave)
    {
        /** 
         * Start timer when player's last movement direction changed.
         * Checking `isMoving` or `canRun` doesn't work if player uses controller and keeps moving stick after hitting "New Game".
         * Checking `run` doesn't work if player uses mouse and clicks close to character so they walk. */
        return current.playerLastMovementDirectionX != 0;
    }

    // Otherwise, check if player moved from title screen to first room's intro cutscene.
    var isFreshFirstRoom = vars.currentLevelId == 0 && vars.currentPlaceId == 0 /*&& vars.currentPortalId == 0*/;  // Portal id is 0 from a new save, and 1 otherwise (e.g. go into next room and back, then save).
    var isNoLongerOnTitleScreen = DateTime.Now.Subtract(vars.lastDateTimeOnTitleScreen).TotalSeconds > 1;  // Leniency needed when resetting to title screen.
    var inCutscene = current.cursorOff;  // Cursor is off during a cutscene, even when using controller.
    if (isFreshFirstRoom && isNoLongerOnTitleScreen && inCutscene)
    {
        vars.isTitleScreenToNewSave = true;
    }
}

onStart
{
    // Reset vars.
    vars.lastDateTimeOnTitleScreen = null;
    vars.isTitleScreenToNewSave = false;
    vars.isInventoryForcedOpenNeeded = false;
    vars.isInventoryForcedOpen = false;

    vars.splitDict = new Dictionary<string, bool>();
}

split
{
    // Crypt + Abbey (Devotees) splits
    if (vars.oldLevelId == 0 && vars.currentLevelId == 0)
    {
        /* Crypt splits */
        // Finish the 1st journal entry and exit the room.
        var firstJournal = vars.CheckSplit(3, 4, "a1s_first_journal", "t1s_first_journal");
        // Crypt -> Abbey (finish the water locks puzzle and exit the room)
        var cryptExit = vars.CheckSplit(5, 6, "a1s_crypt_exit", "t1s_crypt_exit");

        /* Abbey splits */
        // Finish hide and seek and enter the stealth room.
        var hideAndSeek = vars.CheckSplit(9, 11, "a2s_hide_and_seek", "t2s_hide_and_seek");
        // Pick up the coin item by finishing the bed puzzle.
        var pickUpCoin = vars.isInventoryForcedOpen && vars.CheckSplit(17, 17, "a2s_pick_up_coin", "t2s_pick_up_coin");
        // Enter the church.
        var enterChurch = vars.CheckSplit(12, 21, "a2s_enter_church", "t2s_enter_church");
        // Pick up the lens item after getting the key from the jar and opening the door.
        var pickUpLens = vars.isInventoryForcedOpen && vars.CheckSplit(23, 23, "a2s_pick_up_lens", "t2s_pick_up_lens");

        // True Ending - Devotee-Alchemist link 
        var devoteeAlchemistLink = old.terminalProgress < 5 && current.terminalProgress == 5 && vars.CheckSplit(16, 16, "t7s_devo_alch", null);

        return firstJournal || cryptExit || hideAndSeek || pickUpCoin || enterChurch || pickUpLens || devoteeAlchemistLink;
    }

    // Abbey -> Fortress
    if (vars.oldLevelId == 0 && vars.currentLevelId == 1 && vars.CheckSplit(null, null, "a2s_abbey_exit", "t2s_abbey_exit"))
    {
        return true;
    }

    // Fortress (Warriors) splits
    if (vars.oldLevelId == 1 && vars.currentLevelId == 1)
    {
        // Exit the room with the spear.
        var spearRoom = vars.CheckSplit(7, 8, "a3s_spear_room", "t3s_spear_room");
        // Enter the first stealth room.
        var stealthStart = vars.CheckSplit(9, 11, "a3s_stealth_start", "t3s_stealth_start");
        // Exit the stealth corridor.
        var stealthCorridor = vars.CheckSplit(0, 12, "a3s_stealth_corridor", "t3s_stealth_corridor");
        // Exit the stealth storage room (has an elevator wtih 2 boxes).
        var stealthStorageRoom = vars.CheckSplit(13, 14, "a3s_stealth_storage_room", "t3s_stealth_storage_room");
        // Exit the armory room after disguising as a guard.
        var armoryExit = vars.CheckSplit(16, 14, "a3s_armory_exit", "t3s_armory_exit");

        // True Ending - Warrior-Alchemist link
        var warriorAlchemistLink = old.terminalProgress < 5 && current.terminalProgress == 5 && vars.CheckSplit(21, 21, "t7s_warr_alch", null);

        return spearRoom || stealthStart || stealthCorridor || stealthStorageRoom || armoryExit || warriorAlchemistLink;
    }

    // Fortress -> Gardens.
    if (vars.oldLevelId == 1 && vars.currentLevelId == 2 && vars.CheckSplit(null, null,"a3s_fortress_exit", "t3s_fortress_exit"))
    {
        return true;
    }

    // Gardens (Bards) splits    
    if (vars.oldLevelId == 2 && vars.currentLevelId == 2)
    {
        // Exit through the servant's door.
        var servantDoor = vars.CheckSplit(2, 5, "a4s_servant_door", "t4s_servant_door");
        // Enter sewers.
        var enterSewers = vars.CheckSplit(15, 11, "a4s_enter_sewers", null);
        // Get theatre ticket.
        var theatreTicket = vars.isInventoryForcedOpen && vars.CheckSplit(17, 17, "t4s_theatre_ticket", null);
        // Watch the show.
        var theatreWatched = vars.CheckSplit(23, 24, "t4s_theatre_watched", null);
        // Exit sewers.
        var exitSewers = vars.CheckSplit(11, 15, "a4s_exit_sewers", "t4s_exit_sewers");
        // Pick up torch item at windmill.
        var pickUpTorch = vars.isInventoryForcedOpen && vars.CheckSplit(18, 18, "a4s_pick_up_windmill_torch", "t4s_pick_up_windmill_torch");

        // True Ending - Devotee-Bard link
        var devoteeBardLink = old.terminalProgress < 5 && current.terminalProgress == 5 && vars.CheckSplit(25, 25, "t7s_devo_bard", null);

        return servantDoor || enterSewers || theatreTicket || theatreWatched || exitSewers || pickUpTorch || devoteeBardLink;
    }

    /* Skipping Gardens -> Tunnels split, since we're considering the maze as part of Gardens */

    // Factory (Alchemists) splits (+ maze)
    if (vars.oldLevelId == 3 && vars.currentLevelId == 3)
    {
        // Exit the maze.
        var mazeExit = vars.CheckSplit(7, 8, "a4s_maze_exit", "t4s_maze_exit");
        // Escape the monster.
        var escapeMonster = vars.CheckSplit(13, 14, "a5s_escape_monster", "t5s_escape_monster");
        // Trigger the canteen timer (i.e. enter the canteen foyer for the 1st time).
        var triggerCanteenTimer = vars.CheckSplit(31, 32, "a5s_trigger_canteen_timer", "t5s_trigger_canteen_timer");
        // Pick up silverware item at canteen.
        var pickUpSilverware = vars.isInventoryForcedOpen && vars.CheckSplit(33, 33, "a5s_pick_up_silverware", "t5s_pick_up_silverware");
        // Pick up silver bar item after melting the silverware.
        var pickUpSilverBar = vars.isInventoryForcedOpen && vars.CheckSplit(22, 22, "a5s_pick_up_silver_bar", "t5s_pick_up_silver_bar");

        // True Ending - Bard-Alchemist split
        var bardAlchemistSplit = old.terminalProgress < 5 && current.terminalProgress == 5 && vars.CheckSplit(19, 19, "t7s_bard_alch", null);

        return mazeExit || escapeMonster || triggerCanteenTimer || pickUpSilverware || pickUpSilverBar || bardAlchemistSplit;
    }

    // Factory -> Exile.
    if (vars.oldLevelId == 3 && vars.currentLevelId == 4 && vars.CheckSplit(null, null, "a5s_factory_exit", "t5s_factory_exit"))
    {
        return true;
    }

    // Exile (Anchorites) splits
    if (vars.oldLevelId == 4 && vars.currentLevelId == 4)
    {
        /* Exile splits */
        // Enter the Creator's room after entering the 3-glyph code in the keypad.
        var exileCreatorRoom = vars.CheckSplit(15, 6, "a6s_creator_room", "t6s_creator_room");
        // Pick up Exile key.
        var pickUpExileKey = vars.isInventoryForcedOpen && vars.CheckSplit(24, 24, "a6s_pick_up_exile_key", "t6s_pick_up_exile_key");
        
        var checkFinalCutscene = !current.canPlayerRun && !old.cursorOff && current.cursorOff;
        // Final split (not optional) for Any% category. Fake tower split for True Ending category
        var sadTower = checkFinalCutscene && vars.CheckSplit(2, 2, "any_category", "t7s_fake_tower");
        // Final split (not optional) for True Ending category
        var happyTower = checkFinalCutscene && vars.CheckSplit(3, 3, "true_ending_category", null);

        /* Laboratories True Ending splits */
        var abbeyLab = vars.CheckSplit(9, 16, "t7s_abbey_lab", null);
        var fortressLab = vars.CheckSplit(9, 17, "t7s_fortress_lab", null);
        var gardensLab = vars.CheckSplit(9, 18, "t7s_gardens_lab", null);
        var factoryLab = vars.CheckSplit(9, 19, "t7s_factory_lab", null);

        return exileCreatorRoom || pickUpExileKey || sadTower || happyTower || abbeyLab || fortressLab || gardensLab || factoryLab;
    }

    // Simulation splits
    if (vars.oldLevelId == 5 && vars.currentLevelId == 5)
    {
        var terminal1 = vars.CheckSplit(4, 7, "t8s_terminal_1", null);
        var terminal2 = vars.CheckSplit(11, 12, "t8s_terminal_2", null);
        var terminal3 = vars.CheckSplit(13, 16, "t8s_terminal_3", null);

        return terminal1 || terminal2 || terminal3;
    }

    // Simuation end
    if (vars.oldLevelId == 5 && vars.currentLevelId == 4 && vars.CheckSplit(null, null, "t8s_exile_shutdown", null))
    {
        return true;
    }
}