///////////////////////////////////////////////////////////////// /// /// Spyro Reignited Trilogy Autosplitter v1.14 /// ///////////////////////////////////////////////////////////////// /// /// Author: Dinopony (@DinoponyRuns) /// Special thanks to: /// - CptBrian for his precious knowledge about UE4 games structure and his work unifying pointers /// - Zic3 for helping finding RAM addresses /// - All the Spyro community for being helpful and supportive in the process of making this beast :) /// ///////////////////////////////////////////////////////////////// state("Spyro-Win64-Shipping") { // Set to 0 when loading, set to 1 otherwise (foundable as a 4-byte searching for 256) byte isNotLoading : 0x03415F30, 0xF8, 0x4A8, 0xE19; // Set to 0 in game, 1 if in menu, 15 if in graphics submenu byte inMenu : 0x034160D0, 0x20, 0x218, 0x60; // Set to 0 in title screen and main menu, set to 1 everywhere else byte inGame : 0x03415F30, 0xF0, 0x378, 0x564; // Counts Ripto's 3rd phase health (init at 8 from the very beginning of Ripto 1 fight, can be frozen at 0 to end the fight) byte healthRipto3 : 0x03415F30, 0x110, 0x50, 0x140, 0x8, 0x1D0, 0x134; // Counts Sorceress's 2nd phase health (init at 10 from the very beginning of Sorc 1 fight, can be frozen at 0 to end the fight) // byte healthSorceress2 : 0x0; // TODO // ID of the map the player is being in string256 map : 0x03415F30, 0x138, 0xB0, 0xB0, 0x598, 0x210, 0xB8, 0x148, 0x190, 0x0; } /* Old unstable pointer values found, may be useful if anybody has a problem with current ones state("Spyro-Win64-Shipping") { byte isNotLoading : 0x034149E8, 0x18, 0xE0, 0x4A8, 0xE19; byte inMenu : 0x03658048, 0x68, 0x218, 0x60; byte inGame : 0x03659C60, 0x7E8, 0x2D0, 0x70, 0xE0, 0x564; byte healthRipto3 : 0x03415F30, 0x88, 0x48, 0x138, 0x140, 0x8, 0x1D0, 0x134; string255 gvasRoot : 0x36A2010, 0x1E0, 0x0, 0x10, 0x20, 0x0; } */ startup { // Maps info tuples contains : // 1) internal map ID (string) // 2) English display name (string) vars.maps = new Dictionary> { // Spyro 1 maps { "s1_stone_hill", new Tuple("/LS102_StoneHill/Maps/", "Stone Hill") }, { "s1_dark_hollow", new Tuple("/LS103_DarkHollow/Maps/", "Dark Hollow") }, { "s1_town_square", new Tuple("/LS104_Townsquare/Maps/", "Town Square") }, { "s1_sunny_flight", new Tuple("/LS105_Sunnyflight/Maps/", "Sunny Flight") }, { "s1_toasty", new Tuple("/LS106_Toasty/Maps/", "Toasty") }, { "s1_dry_canyon", new Tuple("/LS108_DryCanyon/Maps/", "Dry Canyon") }, { "s1_cliff_town", new Tuple("/LS109_CliffTown/Maps/", "Cliff Town") }, { "s1_ice_cavern", new Tuple("/LS110_IceCavern/Maps/", "Ice Cavern") }, { "s1_night_flight", new Tuple("/LS111_NightFlight/Maps/", "Night Flight") }, { "s1_doctor_shemp", new Tuple("/LS112_DrShemp/Maps/", "Doctor Shemp") }, { "s1_alpine_ridge", new Tuple("/LS114_AlpineRidge/Maps/", "Alpine Ridge") }, { "s1_high_caves", new Tuple("/LS115_HighCaves/Maps/", "High Caves") }, { "s1_wizard_peak", new Tuple("/LS116_WizardPeak/Maps/", "Wizard Peak") }, { "s1_crystal_flight", new Tuple("/LS117_CrystalFlight/Maps/", "Crystal Flight") }, { "s1_blowhard", new Tuple("/LS118_Blowhard/Maps/", "Blowhard") }, { "s1_terrace_village", new Tuple("/LS120_TerraceVillage/Maps/", "Terrace Village") }, { "s1_misty_bog", new Tuple("/LS121_MistyBog/Maps/", "Misty Bog") }, { "s1_tree_tops", new Tuple("/LS122_TreeTops/Maps/", "Tree Tops") }, { "s1_wild_flight", new Tuple("/LS123_WildFlight/Maps/", "Wild Flight") }, { "s1_metalhead", new Tuple("/LS124_MetalHead/Maps/", "Metalhead") }, { "s1_dark_passage", new Tuple("/LS126_DarkPassage/Maps/", "Dark Passage") }, { "s1_lofty_castle", new Tuple("/LS127_LoftyCastle/Maps/", "Lofty Castle") }, { "s1_haunted_towers", new Tuple("/LS128_HauntedTowers/Maps/", "Haunted Towers") }, { "s1_icy_flight", new Tuple("/LS129_IcyFlight/Maps/", "Icy Flight") }, { "s1_jacques", new Tuple("/LS130_Jacques/Maps/", "Jacques") }, { "s1_gnorc_cove", new Tuple("/LS132_GnorcCove/Maps/", "Gnorc Cove") }, { "s1_twilight_harbor", new Tuple("/LS133_TwlightHarbour/Maps/", "Twilight Harbor") }, { "s1_gnasty_gnorc", new Tuple("/LS134_GnastyGnorc/Maps/", "Gnasty Gnorc") }, { "s1_gnastys_loot", new Tuple("/LS135_GnastyLoot/Maps/", "Gnasty's Loot") }, // Spyro 2 maps { "s2_glimmer", new Tuple("/LS202_Glimmer/Maps/", "Glimmer") }, { "s2_idol_springs", new Tuple("/LS203_IdolSprings/Maps/", "Idol Springs") }, { "s2_colossus", new Tuple("/LS204_Colossus/Maps/", "Colossus") }, { "s2_hurricos", new Tuple("/LS205_Hurricos/Maps/", "Hurricos") }, { "s2_sunny_beach", new Tuple("/LS206_SunnyBeach/Maps/", "Sunny Beach") }, { "s2_aquaria_towers", new Tuple("/LS207_AquariaTowers/Maps/", "Aquaria Towers") }, { "s2_crushs_dungeon", new Tuple("/LS208_CrushsDungeon/Maps/", "Crush's Dungeon") }, { "s2_ocean_speedway", new Tuple("/LS209_OceanSpeedway/Maps/", "Ocean Speedway") }, { "s2_crystal_glacier", new Tuple("/LS211_CrystalGlacier/Maps/", "Crystal Glacier") }, { "s2_skelos_badlands", new Tuple("/LS212_SkelosBadlands/Maps/", "Skelos Badlands") }, { "s2_zephyr", new Tuple("/LS213_Zephyr/Maps/", "Zephyr") }, { "s2_breeze_harbor", new Tuple("/LS214_BreezeHarbor/Maps/", "Breeze Harbor") }, { "s2_scorch", new Tuple("/LS215_Scorch/Maps/", "Scorch") }, { "s2_fracture_hills", new Tuple("/LS216_FractureHills/Maps/", "Fracture Hills") }, { "s2_magma_cone", new Tuple("/LS217_MagmaCone/Maps/", "Magma Cone") }, { "s2_shady_oasis", new Tuple("/LS218_ShadyOasis/Maps/", "Shady Oasis") }, { "s2_gulps_overlook", new Tuple("/LS219_GulpsOverlook/Maps/", "Gulp's Overlook") }, { "s2_icy_speedway", new Tuple("/LS220_IcySpeedway/Maps/", "Icy Speedway") }, { "s2_metro_speedway", new Tuple("/LS221_MetroSpeedway/Maps/", "Metro Speedway") }, { "s2_mystic_marsh", new Tuple("/LS223_MysticMarsh/Maps/", "Mystic Marsh") }, { "s2_cloud_temples", new Tuple("/LS224_CloudTemples/Maps/", "Cloud Temples") }, { "s2_metropolis", new Tuple("/LS225_Metropolis/Maps/", "Metropolis") }, { "s2_robotica_farms", new Tuple("/LS226_RoboticaFarms/Maps/", "Robotica Farms") }, { "s2_riptos_arena", new Tuple("/LS227_RiptosArena/Maps/", "Ripto's Arena") }, { "s2_canyon_speedway", new Tuple("/LS228_CanyonSpeedway/Maps/", "Canyon Speedway") }, { "s2_dragon_shores", new Tuple("/LS229_DragonShores/Maps/", "Dragon Shores") }, // Spyro 3 maps { "s3_sunny_villa", new Tuple("/LS302_SunnyVilla/Maps/", "Sunny Villa") }, { "s3_cloud_spires", new Tuple("/LS303_CloudSpires/Maps/", "Cloud Spires") }, { "s3_molten_crater", new Tuple("/LS304_MoltenCrater/Maps/", "Molten Crater") }, { "s3_seashell_shore", new Tuple("/LS305_SeashellShore/Maps/", "Seashell Shore") }, { "s3_sheilas_alp", new Tuple("/LS306_SheilasAlp/Maps/", "Sheila's Alp") }, { "s3_mushroom_speedway", new Tuple("/LS307_MushroomSpeedway/Maps/", "Mushroom Speedway") }, { "s3_buzzs_dungeon", new Tuple("/LS308_BuzzsDungeon/Maps/", "Buzz's Dungeon") }, { "s3_crawdad_farms", new Tuple("/LS309_CrawdadFarm/Maps/", "Crawdad Farm") }, { "s3_icy_peak", new Tuple("/LS311_IcyPeak/Maps/", "Icy Peak") }, { "s3_enchanted_towers", new Tuple("/LS312_EnchantedTowers/Maps/", "Enchanted Towers") }, { "s3_spooky_swamp", new Tuple("/LS313_SpookySwamp/Maps/", "Spooky Swamp") }, { "s3_bamboo_terrace", new Tuple("/LS314_BambooTerrace/Maps/", "Bamboo Terrace") }, { "s3_sgt_byrds_base", new Tuple("/LS315_SgtByrdsBase/Maps/", "Sgt. Byrd's Base") }, { "s3_country_speedway", new Tuple("/LS316_CountrySpeedway/Maps/", "Country Speedway") }, { "s3_spikes_arena", new Tuple("/LS317_SpikesArena/Maps/", "Spike's Arena") }, { "s3_spider_town", new Tuple("/LS318_SpiderTown/Maps/", "Spider Town") }, { "s3_lost_fleet", new Tuple("/LS320_LostFleet/Maps/", "Lost Fleet") }, { "s3_frozen_altars", new Tuple("/LS321_FrozenAltars/Maps/", "Frozen Altars") }, { "s3_fireworks_factory", new Tuple("/LS322_FireworksFactory/Maps/", "Fireworks Factory") }, { "s3_charmed_ridge", new Tuple("/LS323_CharmedRidge/Maps/", "Charmed Ridge") }, { "s3_bentleys_outpost", new Tuple("/LS324_BentleysOutpost/Maps/", "Bentleys Outpost") }, { "s3_honey_speedway", new Tuple("/LS325_HoneySpeedway/Maps/", "Honey Speedway") }, { "s3_scorchs_pit", new Tuple("/LS326_ScorchsPit/Maps/", "Scorchs Pit") }, { "s3_starfish_reef", new Tuple("/LS327_StarfishReef/Maps/", "Starfish Reef") }, { "s3_crystal_islands", new Tuple("/LS329_CrystalIslands/Maps/", "Crystal Islands") }, { "s3_desert_ruins", new Tuple("/LS330_DesertRuins/Maps/", "Desert Ruins") }, { "s3_haunted_tomb", new Tuple("/LS331_HauntedTomb/Maps/", "Haunted Tomb") }, { "s3_dino_mines", new Tuple("/LS332_DinoMines/Maps/", "Dino Mines") }, { "s3_agent_9s_lab", new Tuple("/LS333_Agent9sLab/Maps/", "Agent 9's Lab") }, { "s3_harbor_speedway", new Tuple("/LS334_HarborSpeedway/Maps/", "Harbor Speedway") }, { "s3_sorceresss_lair", new Tuple("/LS335_SorceresssLair/Maps/", "Sorceress's Lair") }, { "s3_bugbot_factory", new Tuple("/LS336_BugbotFactory/Maps/", "Bugbot Factory") }, { "s3_super_bonus", new Tuple("/LS337_SuperBonusRound/Maps/", "Super Bonus Round") } }; // This dictionary defines which autosplits require a specific transition to be triggered, and to which map the transition must lead. // This is especially useful for boss fights, where leaving the level without completing it must not split (only happens in level storage contexts) vars.specificMapTransitions = new Dictionary { { "s2_crushs_dungeon", "/LS210_AutumnPlains_Home/Maps/" }, { "s2_gulps_overlook", "/LS222_WinterTundra_Home/Maps/" }, { "s2_riptos_arena", "/LS229_DragonShores/Maps/" } }; settings.Add("reset", false, "Reset timer on title screen"); settings.Add("ignore_fast_exits", true, "Ignore fast exits (time spent in level < 15s)"); settings.Add("s1", true, "Spyro the Dragon"); settings.Add("s1_first", true, "Level exits (first time)", "s1"); settings.Add("s1_everytime", true, "Level exits (every time)", "s1"); // settings.Add("s1_kill_gnasty", true, "Gnasty Gnorc (on kill)", "s1"); settings.Add("s2", true, "Spyro 2: Ripto's Rage!"); settings.Add("s2_first", true, "Level exits (first time)", "s2"); settings.Add("s2_everytime", true, "Level exits (every time)", "s2"); settings.Add("s2_enter_ripto", false, "Enter Ripto's Arena", "s2"); settings.Add("s2_kill_ripto", true, "Ripto (on last blow) [EXPERIMENTAL]", "s2"); settings.Add("s3", true, "Spyro: Year of the Dragon"); settings.Add("s3_first", true, "Level exits (first time)", "s3"); settings.Add("s3_everytime", true, "Level exits (every time)", "s3"); // settings.Add("s3_kill_sorceress", true, "Sorceress (on last blow)", "s3"); // Initialize settings for autosplits from the map list foreach(KeyValuePair> entry in vars.maps) { string splitCode = entry.Key; string mapName = entry.Value.Item2; string gamePrefix = splitCode.Substring(0,2); settings.Add(splitCode + "_first", true, mapName, gamePrefix + "_first"); settings.Add(splitCode + "_everytime", false, mapName, gamePrefix + "_everytime"); } } init { refreshRate = 30; if (modules.First().ModuleMemorySize == 61046784) { print("Spyro Reignited Trilogy ASL started (game version detected: Release)"); version = "Release"; } else { print("Spyro Reignited Trilogy ASL started (unknown game version)"); } vars.alreadyTriggeredSplits = new HashSet(); vars.lastLevelExitTimestamp = 0; } update { // print("isNotLoading = " + current.isNotLoading.ToString()); // print("inMenu = " + current.inMenu.ToString()); // print("inGame = " + current.inGame.ToString()); // print("healthRipto3 = " + current.healthRipto3.ToString()) // print("map = " + current.map.ToString()); } start { if(current.inGame == 1 && old.inGame == 0) { vars.alreadyTriggeredSplits.Clear(); vars.lastLevelExitTimestamp = 0; return true; } return false; } reset { return settings["reset"] && current.inGame == 0; } split { // For each map... foreach(KeyValuePair> entry in vars.maps) { string splitCode = entry.Key; string mapID = entry.Value.Item1; // An autosplit can only happen if we were in the currently processed map, and if we aren't in it anymore. // If that's not the case, no need to continue. if(old.map != mapID || current.map == mapID || current.map == null) continue; bool isFastExit = (timer.CurrentTime.GameTime.Value.TotalSeconds - vars.lastLevelExitTimestamp < 15); vars.lastLevelExitTimestamp = timer.CurrentTime.GameTime.Value.TotalSeconds; if(settings["ignore_fast_exits"] && isFastExit) break; // This autosplit needs to be verified if it's always enabled, or if it's enabled for first exit check and it has not yet been triggered. if(settings[splitCode + "_everytime"] || (settings[splitCode + "_first"] && !vars.alreadyTriggeredSplits.Contains(entry.Key))) { bool shouldAutosplit = true; // If a specific map transition is required, we autosplit only if we go from map A to map B if(vars.specificMapTransitions.ContainsKey(entry.Key)) shouldAutosplit = (current.map == vars.specificMapTransitions[entry.Key]); if(shouldAutosplit) { // print("Autosplitting going from map '" + old.map.ToString() + "' to map '" + current.map.ToString() + "'"); vars.alreadyTriggeredSplits.Add(entry.Key); return true; } } break; } if(current.map == vars.maps["s2_riptos_arena"].Item1) { // "Enter Ripto's Arena" specific handling if(settings["s2_enter_ripto"] && old.map != current.map) return true; // "Ripto (on last blow)" specific handling if(settings["s2_kill_ripto"] && old.healthRipto3 == 1 && current.healthRipto3 == 0) return true; } // Gnasty Gnorc kill specific handling // if(settings["s1_kill_gnasty"] && current.map == vars.maps["s1_gnasty_gnorc"].Item1) // return true; // Sorceress (last blow) specific handling // if(settings["s3_kill_sorceress"] && old.healthSorc2 == 1 && current.healthSorc2 == 0 && current.map == vars.maps["s3_sorceresss_lair"].Item1) // return true; return false; } isLoading { // Game must be loading something to pause the timer if(current.isNotLoading != 0) return false; // Timer must never be paused on title screen if(current.inGame == 0) return false; // Timer must not be paused when inside menu to prevent abusing this by buffering a loading and pausing the game at the exact same frame. // We also check that the run didn't just start, because the "inMenu" state is active during the fade to black after game is selected // (which would cause 0.9s to elapse on run start whereas something is indeed loading). if(current.inMenu > 0 && timer.CurrentTime.RealTime.Value.TotalSeconds >= 3) return false; return true; }