Skip to content

Instantly share code, notes, and snippets.

@FranklyGD
Last active March 1, 2024 11:28
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 4 You must be signed in to fork a gist
  • Save FranklyGD/c2cb3e35a14ba42f4b3890852b86a320 to your computer and use it in GitHub Desktop.
Save FranklyGD/c2cb3e35a14ba42f4b3890852b86a320 to your computer and use it in GitHub Desktop.
An Auto Splitter script for Spyro the Dragon on select emulators
/*
Spyro the Dragon (NTSC-U) Autosplitter
By: FranklyGD
Contributers: lucx40 (lucx1 on Github), ThirstyWraith, goofball564
Special thanks to Laura and lucx40 for helping me test and understand the requirements of
the speedruns of this game
-- FranklyGD
I just gave this autosplitter some updates, enjoy!
-- lucx40
Added support for DuckStation, XEBRA and several updated emulator versions.
-- ThirstyWraith
Added support for more versions of Duckstation (hopefully all of them) and Mednafen :)
-- goofball564
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 {
settings.Add("loadPause", true, "Pause loadless timer when loading levels (does not apply to Real Time)");
settings.Add("dragonPause", true, "Pause loadless timer during dragon loads (does not apply to Real Time)");
settings.Add("titleReset", true, "Reset timer when returning to title screen");
settings.Add("balloonSplit", true, "Split when traveling between homeworlds");
settings.Add("levelSplit", true, "Split when traveling within a homeworld (i.e. returning home and specific level entries defined below)");
settings.Add("gnexusSplit", false, "(Any%) Do not split when returning from Gnorc Cove and Twilight Harbor, split upon entering Gnasty Gnorc");
settings.Add("gnastySplit", true, "(Any%) Split on final hit on Gnasty Gnorc");
settings.Add("lootSplit", false, "(120%) Split when entering Gnasty's Loot");
settings.Add("tucoSplit", false, "(Vortex) Split when Tuco warps you to another world that he suggests to go to");
settings.Add("vortexSplit", false, "(Vortex) Do not split when returning from Dry Canyon or Cliff Town, split upon entering Dr. Shemp");
settings.Add("dragonSplit", false, "(80 Dragons) Split when the freed dragon count reaches 80");
settings.Add("eggSplit", false, "(All Eggs) Split when 12th egg is collected"); //debated split time, this should do though
// settings.Add("pinkSplit", false, "(Pink Gem%) Split on final Pink Gem collected by Gnasty Gnorc");
settings.Add("balloonStart", false, "(Homeworld Practice) Start timer when travelling between homeworlds");
// 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;
vars.homeworldIDs = new List<uint> {
0x0a, // Artisans
0x14, // Peace Keepers
0x1e, // Magic Crafters
0x28, // Beast Makers
0x32, // Dream Weavers
0x3c, // Gnorc Gnexus
};
vars.tucoSuggestionIDs = new List<uint> {
// 0x0b, // Stone Hill (Unused in any categories)
0x15, // Dry Canyon
0x16, // Cliff Town
};
vars.gnexusIDs = new List<uint> {
0x3d, // Gnorc Cove
0x3e, // Twilight Harbor
};
//Predefining those variables seems completely unnecessary yet for some reason things break in the weirdest ways possible
//if I don't, so this stays :)
vars.dragonInRAM = false;
vars.anAdventureBegan = false;
// vars.gnastyFinalPinkGems = 0;
// vars.killedGnasty = false; //I use this for dirty implementation of Pink Gem
}
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.
// States
vars.gameStateAddress = vars.baseRAMAddress + 0x757d8;
vars.loadStateAddress = vars.baseRAMAddress + 0x75864;
vars.dragonStateAddress = vars.baseRAMAddress + 0x77058;
vars.finalBossStateAddress = vars.baseRAMAddress + 0x160f08;
// Counters
vars.gemCountAddress = vars.baseRAMAddress + 0x75860;
vars.dragonCountAddress = vars.baseRAMAddress + 0x75750;
vars.eggCountAddress = vars.baseRAMAddress + 0x75810;
vars.eggVisualAddress = vars.baseRAMAddress + 0x77FD4;
// Etc
vars.controlLockAddress = vars.baseRAMAddress + 0x78c48;
vars.worldIDAddress = vars.baseRAMAddress + 0x7596c;
vars.nextSuggestedWorldIDAddress = vars.baseRAMAddress + 0x758b4;
// Read memory
current.gameState = memory.ReadValue<uint>((IntPtr)vars.gameStateAddress);
current.loadState = memory.ReadValue<int>((IntPtr)vars.loadStateAddress);
current.dragonState = memory.ReadValue<uint>((IntPtr)vars.dragonStateAddress);
current.gemCount = memory.ReadValue<uint>((IntPtr)vars.gemCountAddress);
current.dragonCount = memory.ReadValue<uint>((IntPtr)vars.dragonCountAddress);
current.eggCount = memory.ReadValue<uint>((IntPtr)vars.eggCountAddress);
current.eggVisual = memory.ReadValue<uint>((IntPtr)vars.eggVisualAddress);
current.controlLock = memory.ReadValue<uint>((IntPtr)vars.controlLockAddress);
current.worldID = memory.ReadValue<uint>((IntPtr)vars.worldIDAddress);
current.nextSuggestedWorldID = memory.ReadValue<uint>((IntPtr)vars.nextSuggestedWorldIDAddress);
current.finalBossState = memory.ReadValue<byte>((IntPtr)vars.finalBossStateAddress);
}
start {
// Check to start timer if the game has given control to the player or has been paused immediately
// Also should only happen if coming from the title screen
if (current.gameState == 0x0d) {
vars.anAdventureBegan = true;
}
if (vars.anAdventureBegan && old.controlLock != 0 && (current.gameState == 0x03 || current.gameState == 0x02)) {
vars.anAdventureBegan = false;
return true;
}
// Check if the controls are no longer locked at the beginning of the game
if (vars.anAdventureBegan && current.gameState == 0x00 && old.controlLock != 0 && current.controlLock == 0) {
vars.anAdventureBegan = false;
return true;
}
if (settings["balloonStart"]) {
// Start timer when travelling between homeworlds if said option is checked
if (old.worldID != current.worldID && vars.homeworldIDs.Contains(old.worldID) && vars.homeworldIDs.Contains(current.worldID)) {
return true;
}
}
return false;
}
reset {
// Reset when the game is currently on the main menu
return settings["titleReset"] && current.gameState == 0x0d;
}
split {
// On world changed...
if (old.worldID != current.worldID && (
// If the option is enabled, check if changing between homeworlds
settings["balloonSplit"] && vars.homeworldIDs.Contains(old.worldID) && vars.homeworldIDs.Contains(current.worldID) ||
// If the option is enabled, check if changing directly to the specified worlds from Tuco (not entering worlds from Peace Keepers)
settings["tucoSplit"] && old.worldID != 0x14 && vars.tucoSuggestionIDs.Contains(current.worldID) ||
settings["levelSplit"] && (
// Check if leaving a world and returning to homeworld, also make sure it was not done via gameover
// Ignore returning from Tuco's suggested worlds (Dry/Cliffs), Gnorc Cove, Twilight Harbor and Gnasty Gnorc
!vars.homeworldIDs.Contains(old.worldID) && vars.homeworldIDs.Contains(current.worldID) && current.gameState != 5 && !vars.tucoSuggestionIDs.Contains(old.worldID) && !vars.gnexusIDs.Contains(old.worldID) && old.worldID != 0x3f ||
// If the option is disabled, split when returning from Tuco's suggested worlds to Peace Keepers
!settings["vortexSplit"] && vars.tucoSuggestionIDs.Contains(old.worldID) && current.worldID == 0x14 && current.gameState != 5 ||
// If the option is enabled, split upon entering Shemp portal
settings["vortexSplit"] && current.worldID == 0x18 ||
// If the option is disabled, split when returning from Gnorc Cove or Twilight Harbor to Gnorc Gnexus
!settings["gnexusSplit"] && vars.gnexusIDs.Contains(old.worldID) && current.worldID == 0x3c && current.gameState != 5 ||
// If the option is enabled, split upon entering Gnasty Gnorc
settings["gnexusSplit"] && current.worldID == 0x3f ||
// If the option is enabled, split upon entering Gnasty's Loot portal
settings["lootSplit"] && current.worldID == 0x40
)
)) {
return true;
}
// "Any%" category conditions if the game is in the final boss world, check Gnasty's state if he just died
if (current.worldID == 0x3f && old.finalBossState != 0x08 && current.finalBossState == 0x08) {
// if (settings["pinkSplit"]) {
// vars.killedGnasty = true;
// }
if (settings["gnastySplit"]) {
return true;
}
}
//Appreciate my Pink Gems ok
// if (vars.killedGnasty && old.gemCount == (current.gemCount - 25)) {
// vars.gnastyFinalPinkGems = vars.gnastyFinalPinkGems + 1;
// if (vars.gnastyFinalPinkGems == 4) {
// vars.killedGnasty = false;
// vars.gnastyFinalPinkGems = 0;
// return true;
// }
// }
// "80 Dragons" category conditions when the visual number flips to 80
if (settings["dragonSplit"] && current.dragonCount == 80 && old.dragonState == 5 && current.dragonState == 6) {
vars.dragonFrame = timer.CurrentTime.RealTime.Value.TotalMilliseconds;
vars.dragonInRAM = true;
}
if (vars.dragonInRAM && timer.CurrentTime.RealTime.Value.TotalMilliseconds > vars.dragonFrame + 533) {
vars.dragonInRAM = false;
return true;
}
// If in Gnasty's Loot with 120% progress and the screen turns completely black
// Doesn't require an option since this only triggers for 120% runs
if (current.worldID == 0x40 && current.gemCount == 14000 && current.dragonCount == 80 && current.eggCount == 12 && old.gameState != 14 && current.gameState == 14) {
return true;
}
// Egg is popular now so...
// Commented code is unreliable so using another RAM address that is close enough (also final timing seems debated anyway)
if (settings["eggSplit"] && current.eggVisual == 0x0c && old.eggVisual == 0x0b) {
return true;
}
return false;
}
isLoading {
return
// If the option is enabled, pause timer when the loading state is "idle" or not "active"; pause timer also when game is loading during a Game Over - This only happen on tranfer back to the homeworld. Also, do not pause timer during Any% or 120% Cutscene
settings["loadPause"] && current.loadState != -1 && current.loadState != 11 && current.gameState != 14 ||
// If the option is enabled, at specific stages of the the dragon statue cutscene pause the timer
settings["dragonPause"] && (
current.dragonState > 1 && current.dragonState < 4 || current.dragonState > 4 && current.dragonState != 7
);
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment