Last active
August 11, 2023 02:41
-
-
Save lucx1/0eebd00141c90400d404e73d77f82583 to your computer and use it in GitHub Desktop.
A scriptable LiveSplit Autosplitter for Spyro Year of the Dragon (NTSC-U 1.0) for supported emulators
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
Spyro Year of the Dragon (1.0, NTSC-U) Autosplitter | |
Forked from the Spyro the Dragon Autosplitter by: FranklyGD, ThirstyWraith, goofball564 and LuminescentSky | |
Modified for Spyro Year of the Dragon by: LuminescentSky | |
If there are any modification requests leave a comment below | |
or join https://discord.gg/spyrospeedrunning and ask me there | |
*/ | |
state ("EmuHawk", "2.6.1") { } | |
state ("EmuHawk", "2.6.2") { } | |
state ("EmuHawk", "2.6.3") { } | |
state ("mednafen", "1.24.1 32bit") { } | |
state ("mednafen", "1.24.1 64bit") { } | |
state ("mednafen", "1.24.2 32bit") { } | |
state ("mednafen", "1.24.2 64bit") { } | |
state ("mednafen", "1.24.3 32bit") { } | |
state ("mednafen", "1.24.3 64bit") { } | |
state ("mednafen", "1.26.1 32bit") { } | |
state ("mednafen", "1.26.1 64bit") { } | |
state ("mednafen", "1.27.1 32bit") { } | |
state ("mednafen", "1.27.1 64bit") { } | |
state ("mednafen", "1.29.0 32bit") { } | |
state ("mednafen", "1.29.0 64bit") { } | |
state ("ePSXe", "1.9.0") { } | |
state ("ePSXe", "1.9.25") { } | |
state ("ePSXe", "2.0.0") { } | |
state ("XEBRA", "210423d") { } | |
state ("duckstation-qt-x64-ReleaseLTCG", "any") { } | |
state ("duckstation-nogui-x64-ReleaseLTCG", "any") { } | |
startup { | |
// Duckstation Vars | |
vars.duckstationProcessNames = new List<string> { | |
"duckstation-qt-x64-ReleaseLTCG", | |
"duckstation-nogui-x64-ReleaseLTCG", | |
}; | |
vars.duckstation = false; | |
vars.duckstationBaseRAMAddressFound = false; | |
vars.duckstationStopwatch = new Stopwatch(); | |
vars.DUCKSTATION_ADDRESS_SEARCH_INTERVAL = 1000; | |
vars.baseRAMAddress = IntPtr.Zero; | |
settings.Add("anySplit", true, "Split when dealing the final hit on Sorceress in Sorceress' Lair (Any%)"); | |
settings.Add("sbrSplit", true, "Split when dealing the final hit on Sorceress in Super Bonus Round (117%)"); | |
settings.Add("exitSplit", true, "Split when returning back to the homeworld (e.g. Sunny Villa to Sunrise Spring Home)"); | |
settings.Add("bossSplit", true, "Split when travelling to the next homeworld after a boss fight (e.g. Buzz's Dungeon to Midday Garden Home"); | |
settings.Add("homeworldSplit", true, "Split when travelling to another homeworld from a homeworld (e.g. Sunrise Spring Home to Midday Garden Home"); | |
settings.Add("bossExitSplit", false, "Split when exiting a boss fight to return to the homeworld (e.g. Buzz's Dungeon to Sunrise Spring Home)"); | |
settings.Add("gameOverSplit", true, "Split even if returning back to the homeworld via Game Over"); | |
settings.Add("sorceressEnterSplit", false, "Split when entering Sorceress' Lair"); | |
settings.Add("sbrEnterSplit", false, "Split when entering Super Bonus Round"); | |
vars.homeworldLevelIDs = new List<uint> { | |
10, // Sunrise Spring | |
20, // Midday Garden | |
30, // Evening Lake | |
40, // Midnight Mountain | |
}; | |
vars.bossLevelIDs = new List<uint> { | |
17, // Buzz | |
27, // Spike | |
37, // Scorch | |
47, // Sorceress | |
}; | |
// Other variables used for the execution of the autosplitter | |
vars.titleScreenSeen = false; | |
vars.openingCutsceneCounter = 0; | |
} | |
init { | |
refreshRate = 30; | |
var mainModule = modules.First(); | |
switch (mainModule.ModuleMemorySize) { | |
// Bizhawk | |
case 0x456000: | |
version = "2.6.1"; | |
vars.baseRAMAddress = modules.Where(x => x.ModuleName == "octoshock.dll").First().BaseAddress + 0x310f80; | |
break; | |
case 0x454000: | |
version = "2.6.2"; | |
vars.baseRAMAddress = modules.Where(x => x.ModuleName == "octoshock.dll").First().BaseAddress + 0x30df80; | |
break; | |
case 0x45a000: | |
version = "2.6.3"; | |
vars.baseRAMAddress = modules.Where(x => x.ModuleName == "octoshock.dll").First().BaseAddress + 0x30df80; | |
break; | |
// Mednafen | |
case 0x42c9000: | |
version = "1.24.1 32bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1c96560; | |
break; | |
case 0x5eef000: | |
version = "1.24.1 64bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x25bf280; | |
break; | |
case 0x42c6000: | |
version = "1.24.2 32bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1c93560; | |
break; | |
case 0x5eec000: | |
version = "1.24.2/1.24.3 64bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x25bc280; | |
break; | |
case 0x42c7000: | |
version = "1.24.3/1.26.1 32bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1c94560; | |
break; | |
case 0x5e83000: | |
version = "1.26.1 64 bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x2553280; | |
break; | |
case 0x3a44000: | |
version = "1.27.1 32bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x13ff160; | |
break; | |
case 0x55f1000: | |
version = "1.27.1 64bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1cade80; | |
break; | |
case 0x3C81000: | |
version = "1.29.0 32bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1438160; | |
break; | |
case 0x574B000: | |
version = "1.29.0 64bit"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x1C03E80; | |
break; | |
// ePSXe | |
case 0x9d3000: | |
version = "1.9.0"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x6579A0; | |
break; | |
case 0xa08000: | |
version = "1.9.25"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x68b6a0; | |
break; | |
case 0x1359000: | |
version = "2.0.0"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x81a020; | |
break; | |
// XEBRA | |
case 0xbd000: | |
version = "210423d"; | |
vars.baseRAMAddress = mainModule.BaseAddress + 0x3110000; | |
break; | |
// DuckStation or unsupported | |
default: | |
break; | |
} | |
// Unfortunately, duckstation doesn't have a static base RAM address, | |
// so we'll have to keep track of it in the update block. | |
if (vars.duckstationProcessNames.Contains(game.ProcessName)) { | |
vars.duckstation = true; | |
version = "any"; | |
vars.baseRAMAddress = IntPtr.Zero; | |
} | |
} | |
update { | |
if (version == "") { | |
return false; | |
} | |
if (vars.duckstation) { | |
// Find base RAM address in Duckstation by searching its memory pages. | |
// Do this periodically (using stopwatch to determine when to search again) | |
// instead of every update to reduce unnecessary computation. | |
if (!vars.duckstationBaseRAMAddressFound) { | |
if (!vars.duckstationStopwatch.IsRunning || vars.duckstationStopwatch.ElapsedMilliseconds > vars.DUCKSTATION_ADDRESS_SEARCH_INTERVAL) { | |
vars.duckstationStopwatch.Start(); | |
vars.baseRAMAddress = game.MemoryPages(true).Where(p => p.Type == MemPageType.MEM_MAPPED && p.RegionSize == (UIntPtr)0x200000).FirstOrDefault().BaseAddress; | |
if (vars.baseRAMAddress == IntPtr.Zero) { | |
vars.duckstationStopwatch.Restart(); | |
return false; | |
} | |
else { | |
vars.duckstationStopwatch.Reset(); | |
vars.duckstationBaseRAMAddressFound = true; | |
} | |
} | |
else { | |
return false; | |
} | |
} | |
// Verify base RAM address is still valid on each update | |
IntPtr temp1 = vars.baseRAMAddress; | |
IntPtr temp2 = IntPtr.Zero; | |
if (!game.ReadPointer(temp1, out temp2)) { | |
vars.duckstationBaseRAMAddressFound = false; | |
vars.baseRAMAddress = IntPtr.Zero; | |
return false; | |
} | |
} | |
// Address assignment has been moved to update block to support Duckstation's | |
// changing base RAM address. The performance impact of this should | |
// be negligible for non-Duckstation users, | |
// and it reduces code complexity to have it once here. | |
//Memory Addresses | |
vars.gameStateAddress = vars.baseRAMAddress + 0x6e344; | |
vars.levelIDAddress = vars.baseRAMAddress + 0x6c5bc; | |
vars.anySorceressAddress = vars.baseRAMAddress + 0x151874; | |
vars.sbrSorceressAddress = vars.baseRAMAddress + 0x169d90; | |
current.gameState = memory.ReadValue<uint>((IntPtr)vars.gameStateAddress); | |
current.levelID = memory.ReadValue<uint>((IntPtr)vars.levelIDAddress); | |
current.anySorceress = memory.ReadValue<uint>((IntPtr)vars.anySorceressAddress); | |
current.sbrSorceress = memory.ReadValue<uint>((IntPtr)vars.sbrSorceressAddress); | |
} | |
start { | |
// Prepare by using a variable to see whether runner has been on title screen | |
if (current.gameState == 11) { | |
vars.titleScreenSeen = true; | |
vars.openingCutsceneCounter = 0; | |
} | |
// Prepare by counting cutscenes | |
if (vars.titleScreenSeen && (current.gameState == 5 && old.gameState != 5 || current.gameState == 6 && old.gameState != 6)) { | |
vars.openingCutsceneCounter++; | |
} | |
// If current game state is gameplay and title screen was seen | |
if (current.gameState == 0 && vars.titleScreenSeen) { | |
// Start the timer if the previous game state was the fade in and all cutscenes were played | |
if (vars.openingCutsceneCounter == 7 && old.gameState == 3) { | |
return true; | |
} | |
// Reset condition even if timer wasn't started to prevent a later incorrect start of the timer | |
vars.titleScreenSeen = false; | |
} | |
return false; | |
} | |
reset { | |
// Reset when the game is currently on the title screen | |
return current.gameState == 11; | |
} | |
split { | |
if ( | |
( | |
// if exiting regular level | |
settings["exitSplit"] && vars.homeworldLevelIDs.Contains(current.levelID) && !vars.homeworldLevelIDs.Contains(old.levelID) && !vars.bossLevelIDs.Contains(old.levelID) | |
|| | |
// if exiting a boss level | |
settings["bossExitSplit"] && vars.homeworldLevelIDs.Contains(current.levelID) && old.levelID == (current.levelID + 7) | |
) && ( | |
// if game overing to exit level | |
settings["gameOverSplit"] || current.gameState != 9 | |
) || | |
// if travelling to the next homeworld from a boss (or the cutscene after defeating Scorch) | |
settings["bossSplit"] && vars.homeworldLevelIDs.Contains(current.levelID) && (old.levelID == (current.levelID - 3) || old.levelID == 68) | |
|| | |
// if travelling from homeworld to homeworld | |
settings["homeworldSplit"] && vars.homeworldLevelIDs.Contains(current.levelID) && vars.homeworldLevelIDs.Contains(old.levelID) && current.levelID != old.levelID | |
|| | |
// when entering Sorceress' Lair | |
settings["sorceressEnterSplit"] && current.levelID == 47 && old.levelID == 40 | |
|| | |
// when entering Super Bonus Round | |
settings["sbrEnterSplit"] && current.levelID == 50 && old.levelID == 40 | |
|| | |
// when dealing last hit to Sorceress 1 | |
settings["anySplit"] && current.levelID == 47 && current.anySorceress == 0 && old.anySorceress == 1 | |
|| | |
// when dealing last hit to Sorceress 2 | |
settings["sbrSplit"] && current.levelID == 50 && current.sbrSorceress == 0 && old.sbrSorceress == 1 | |
) { | |
return true; | |
} | |
return false; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment