Skip to content

Instantly share code, notes, and snippets.

@lucx1
Last active August 11, 2023 02:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save lucx1/0eebd00141c90400d404e73d77f82583 to your computer and use it in GitHub Desktop.
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
/*
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