//Written by SabulineHorizon

//Current version as of Oct 16 2024
state("SquirrelGun-Win64-Shipping", "1.2.0.11") {
	// string30 projectVersion: 0x765B470, 0x1B8, 0x2B0, 0x30, 0x0;			//GWorld, OwningGameInstance, ProjectVersionData, FString Version
	double bossMaxHealth: 0x765B470, 0x30, 0x98, 0x4A0, 0x340, 0xC8, 0x8;		//GWorld, PersistentLevel, ???, BP_BlackHawk_C_1?, AC_HealthAndStats_C, StatsPlus, MaxHealth
	double bossHealth: 0x765B470, 0x30, 0x98, 0x4A0, 0x340, 0xC8, 0x10;		//GWorld, PersistentLevel, ???, BP_BlackHawk_C_1?, AC_HealthAndStats_C, StatsPlus, Health
	byte cutsceneActive: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0xA22;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CinematicActive
	bool notLoading: 0x762BB58;							//0 loading, 1 not - brief false positives during gameplay, this address probably needs to be replaced
	double playerX: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x128;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+0
	double playerY: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x130;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+8
	double playerZ: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x138;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+16
	double checkpointX: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0xAC0;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CurrentCheckPointXF+20
	double checkpointY: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0xAC8;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CurrentCheckPointXF+28
	string200 mapFile: 0x7657930, 0xAF8, 0x0;					//GEngine, FString TransitionDescription
	double maxAmmo: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x640, 0xC8, 0x88;	//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, AC_HealthAndStats, StatsPlus, MaxAmmo
	byte accessory: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2A;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Accessory
	byte fur: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2B;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Fur
	byte clothing: 0x765B470, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2C;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Clothing
	string200 nutName: 0x7657930, 0x190, 0xE90, 0x1A8, 0x78, 0x508, 0x0;		//GEngine, WorldSettingsClass, ??, ??, ??, Fstring ??
}

// //Todo: Add code to detect version and add this back in
// //Older version 1.0.2.25
// state("SquirrelGun-Win64-Shipping", "1.0.2.25") {
	// double bossMaxHealth: 0x765A2F0, 0x30, 0x98, 0x4A0, 0x340, 0xC8, 0x8;	//GWorld, PersistentLevel, ???, BP_BlackHawk_C_1?, AC_HealthAndStats_C, StatsPlus, MaxHealth
	// double bossHealth: 0x765A2F0, 0x30, 0x98, 0x4A0, 0x340, 0xC8, 0x10;		//GWorld, PersistentLevel, ???, BP_BlackHawk_C_1?, AC_HealthAndStats_C, StatsPlus, Health
	// byte cutsceneActive: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0xA22;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CinematicActive
	// bool notLoading: 0x762A9D8;							//0 loading, 1 not - brief false positives during gameplay, this address probably needs to be replaced
	// double playerX: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x128;	//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+0
	// double playerY: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x130;	//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+8
	// double playerZ: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x328, 0x138;	//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, CapsuleComponent, RelativeLocation+16
	// double checkpointX: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0xAC0;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CurrentCheckPointXF+20
	// double checkpointY: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0xAC8;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, CurrentCheckPointXF+28
	// string200 mapFile: 0x76567B0, 0xAF8, 0x0;					//GEngine, FString TransitionDescription
	// double maxAmmo: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0x640, 0xC8, 0x88;	//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, AC_HealthAndStats, StatsPlus, MaxAmmo
	// byte accessory: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2A;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Accessory
	// byte fur: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2B;			//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Fur
	// byte clothing: 0x765A2F0, 0x1B8, 0x38, 0x0, 0x30, 0x2E0, 0xE2C;		//GWorld, OwningGameInstance, LocalPlayers, index[0], PlayerController, Character, Clothing
	// string200 nutName: 0x76567B0, 0x190, 0xE90, 0x1A8, 0x78, 0x508, 0x0;		//GEngine, WorldSettingsClass, ??, ??, ??, Fstring ??
// }

startup {
	settings.Add("splits", false, "Optional Splits");
		settings.Add("splitBunker", false, "Split when leaving Bunker", "splits");
		settings.SetToolTip("splitBunker", "Split while loading from Bunker into Neighborhood for the first time");
		settings.Add("splitTough", false, "Split when skipping to Tough Streets", "splits");
		settings.SetToolTip("splitTough", "Split while reloading checkpoint to skip from Mean Streets to Tough Streets for the first time");
		settings.Add("splitTower", false, "Split when entering Tower", "splits");
		settings.SetToolTip("splitTower", "Split while loading from Neighborhood into Tower for the first time");
		settings.Add("splitGoldNut", false, "Split on Golden Nuts", "splits");
		settings.SetToolTip("splitGoldNut", "Split when a golden nut is collected for the first time");
			settings.Add("Top Secret Acorn", false, "Top Secret Acorn", "splitGoldNut");
			settings.Add("Hiding Underneath", false, "Hiding Underneath", "splitGoldNut");
			settings.Add("Scorching Walkway", false, "Scorching Walkway", "splitGoldNut");
			settings.Add("Polecat... Err... Polesquirrel", false, "Polecat... Err... Polesquirrel", "splitGoldNut");
			settings.Add("Tippy Top Secret Acorn", false, "Tippy Top Secret Acorn", "splitGoldNut");
			settings.Add("Brolly Bouncing", false, "Brolly Bouncing", "splitGoldNut");
			settings.Add("Jogger Stopper", false, "Jogger Stopper", "splitGoldNut");
			settings.Add("Top Shelf Prize", false, "Top Shelf Prize", "splitGoldNut");
			settings.Add("Climb And Leap To The Roof", false, "Climb And Leap To The Roof", "splitGoldNut");
			settings.Add("The Floor Is Lava", false, "The Floor Is Lava", "splitGoldNut");
			settings.Add("Crawl Space Maze", false, "Crawl Space Maze", "splitGoldNut");
			settings.Add("Top O' The Yellow House", false, "Top O' The Yellow House", "splitGoldNut");
			settings.Add("Light The Golden Gas Tank", false, "Light The Golden Gas Tank", "splitGoldNut");
			settings.Add("Flash Fried Patties", false, "Flash Fried Patties", "splitGoldNut");
			settings.Add("Chiropractor", false, "Chiropractor", "splitGoldNut");
			settings.Add("Squirrel On A Hot Tin Roof", false, "Squirrel On A Hot Tin Roof", "splitGoldNut");
			settings.Add("The Sovereign Citizen", false, "The Sovereign Citizen", "splitGoldNut");
			settings.Add("In Landmine Guy's House", false, "In Landmine Guy's House", "splitGoldNut");
			settings.Add("High In The Skate Park", false, "High In The Skate Park", "splitGoldNut");
			settings.Add("Highest In The Skate Park", false, "Highest In The Skate Park", "splitGoldNut");
			settings.Add("Gold Agents On The Road", false, "Gold Agents On The Road", "splitGoldNut");
			settings.Add("Not So Straight-And-Narrow", false, "Not So Straight-And-Narrow", "splitGoldNut");
			settings.Add("Canopy Crossing", false, "Canopy Crossing", "splitGoldNut");
			settings.Add("Under Old Management", false, "Under Old Management", "splitGoldNut");
			settings.Add("Cactupuncture", false, "Cactupuncture", "splitGoldNut");
			settings.Add("Salooney Goons", false, "Salooney Goons", "splitGoldNut");
			settings.Add("Stealing From The Till", false, "Stealing From The Till", "splitGoldNut");
			settings.Add("Party Pole", false, "Party Pole", "splitGoldNut");
			settings.Add("Certified Greek", false, "Certified Greek", "splitGoldNut");
			settings.Add("Bachelor Party", false, "Bachelor Party", "splitGoldNut");
			settings.Add("Pro Basketball", false, "Pro Basketball", "splitGoldNut");
			settings.Add("Touchdown", false, "Touchdown", "splitGoldNut");
			settings.Add("Field Goal", false, "Field Goal", "splitGoldNut");
			settings.Add("Gold Agents In The Waterway", false, "Gold Agents In The Waterway", "splitGoldNut");
			settings.Add("High-Speed Pneumatic Tube", false, "High-Speed Pneumatic Tube", "splitGoldNut");
			settings.Add("Water Ski Adventure", false, "Water Ski Adventure", "splitGoldNut");
			settings.Add("Industrial Drainage Pipeline", false, "Industrial Drainage Pipeline", "splitGoldNut");
			settings.Add("Tower Reservoir", false, "Tower Reservoir", "splitGoldNut");
			settings.Add("Gold Agents On The Highway", false, "Gold Agents On The Highway", "splitGoldNut");
			settings.Add("Truss Me", false, "Truss Me", "splitGoldNut");
			settings.Add("Sniper Squirrel", false, "Sniper Squirrel", "splitGoldNut");
			settings.Add("Water Slide Fast Pass", false, "Water Slide Fast Pass", "splitGoldNut");
			settings.Add("Rocket Jump!", false, "Rocket Jump!", "splitGoldNut");
			settings.Add("High Above The Bouncy Castle", false, "High Above The Bouncy Castle", "splitGoldNut");
			settings.Add("Bouncy Rocket Blockers", false, "Bouncy Rocket Blockers", "splitGoldNut");
			settings.Add("Rocket Ragdolls", false, "Rocket Ragdolls", "splitGoldNut");
			settings.Add("Anchor Management", false, "Anchor Management", "splitGoldNut");
			settings.Add("Rocky Smashing Squirrel", false, "Rocky Smashing Squirrel", "splitGoldNut");
			settings.Add("Jelly Jumping", false, "Jelly Jumping", "splitGoldNut");
			settings.Add("Patch O' Jack-O'-Lanterns", false, "Patch O' Jack-O'-Lanterns", "splitGoldNut");
			settings.Add("Spooky Roof", false, "Spooky Roof", "splitGoldNut");
			settings.Add("Door To The Spooky House", false, "Door To The Spooky House", "splitGoldNut");
			settings.Add("Light My Fire", false, "Light My Fire", "splitGoldNut");
			settings.Add("Resident Squirrel", false, "Resident Squirrel", "splitGoldNut");
			settings.Add("Bonking The Big Bottom Button ", false, "Bonking The Big Bottom Button ", "splitGoldNut");
			settings.Add("Hitting The Huge Higher Button", false, "Hitting The Huge Higher Button", "splitGoldNut");
		settings.Add("splitWedge", false, "Split on Ammo Wedge", "splits");
		settings.SetToolTip("splitWedge", "Split every time a reload upgrade wedge is collected");
		settings.Add("splitOutfit", false, "Split on Outfit", "splits");
		settings.SetToolTip("splitOutfit", "Split every time a new outfit piece is equipped for the first time");
			settings.Add("splitAccessory", true, "Split on Accessory", "splitOutfit");
			settings.SetToolTip("splitAccessory", "Split every time a new outfit accessory (hat) is equipped for the first time");
			settings.Add("splitClothing", true, "Split on Clothing", "splitOutfit");
			settings.SetToolTip("splitClothing", "Split every time a new Clothing (body) is equipped for the first time");
			settings.Add("splitFur", true, "Split on Fur", "splitOutfit");
			settings.SetToolTip("splitFur", "Split every time a new outfit fur is equipped for the first time");
			settings.Add("splitChipmunk", true, "Split on Chipmunk", "splitOutfit");
			settings.SetToolTip("splitChipmunk", "Split when the Chipmunk fur type is equipped the first time. This is usually the end condition of the 100% run");
				settings.Add("spamChipmunkSplit", true, "Spam Chipmunk Split", "splitChipmunk");
				settings.SetToolTip("spamChipmunkSplit", "Chipmunk split triggers repeatedly to end the timer in case there are too many splits remaining");
	settings.Add("splitFinalBoss", true, "Split final boss");
	settings.SetToolTip("splitFinalBoss", "Split when Mother's health reaches 0 in the helicopter final boss fight");
	settings.Add("spamFinalSplit", true, "Spam Final Split");
	settings.SetToolTip("spamFinalSplit", "Final split (Mother boss defeated) triggers repeatedly to end the timer in case there are too many splits remaining");
	settings.Add("experimentalLoads", false, "Experimental Load Remover");
	settings.SetToolTip("experimentalLoads", "This might not be reliable, it needs more testing to know");
	
	//declare variables. These get initialized in onStart{}
	vars.bunkerSplit = false;
	vars.toughSplit = false;
	vars.towerSplit = false;
	vars.completedNutSplits = new List<string>();
	vars.finalSplitTriggered = false;
	vars.maxAmmoCount = 0.0;
	vars.accessoryCollected = new List<byte>();
	vars.furCollected = new List<byte>();
	vars.clothingCollected = new List<byte>();
}

update {
	// //debug output for mapFile
	// if(current.mapFile != old.mapFile)
		// print("mapFile changed to: " + current.mapFile.ToString() + " from: " + old.mapFile.ToString());
}

start {
	//Start if a cutscene finished while player is at the bunker start coordinates
	return(
		old.cutsceneActive == 1 && current.cutsceneActive == 0 &&
		current.mapFile == "/Game/Maps/Bunker0" &&
		(Math.Abs(current.playerX - (199.03)) < 10.0) &&
		(Math.Abs(current.playerY - (-1.38)) < 10.0) &&
		(Math.Abs(current.playerZ - (98.39)) < 10.0)
	);
}

onStart {
	//initialize split variables
	vars.bunkerSplit = false;
	vars.toughSplit = false;
	vars.towerSplit = false;
	vars.completedNutSplits = new List<string>();
	vars.finalSplitTriggered = false;
	vars.maxAmmoCount = current.maxAmmo;
	vars.accessoryCollected = new List<byte>(){0, current.accessory};
	vars.furCollected = new List<byte>(){0, current.fur};
	vars.clothingCollected = new List<byte>(){0, current.clothing};
}

split {
	//split if the player moves from Bunker to Neighborhood
	if(settings["splitBunker"] &&
			!vars.bunkerSplit &&
			current.mapFile == "/Game/Maps/Neighborhood" &&
			old.mapFile == "/Game/Maps/Bunker0"){
		vars.bunkerSplit = true;
		print("Split for leaving Bunker");
		return true;
	}
	//split if the player reloads the checkpoint that's used to skip from Mean Streets to Tough Streets
	else if(settings["splitTough"] &&
			!vars.toughSplit &&
			current.mapFile == "/Game/Maps/Neighborhood" &&
			(Math.Abs(current.checkpointX - (1600.0)) < 1.0) &&
			(Math.Abs(current.checkpointY - (-3690.0)) < 1.0) &&
			(Math.Abs(current.playerX - current.checkpointX) < 1.0) &&
			(Math.Abs(current.playerY - current.checkpointY) < 1.0)){
		vars.toughSplit = true;
		print("Split for skipping to Tough Streets");
		return true;
	}
	//split if the player moves from Neighborhood to Tower
	else if(settings["splitTower"] &&
			!vars.towerSplit &&
			current.mapFile == "/Game/Maps/SingleMapsInGame/Bunker2/Bunker2_DroneGauntlet" &&
			old.mapFile == "/Game/Maps/Neighborhood"){
		vars.towerSplit = true;
		print("Split for entering Tower");
		return true;
	}
	//Split when collecting an ammo wedge
	else if(settings["splitWedge"] &&
			current.maxAmmo == (vars.maxAmmoCount + 25) &&
			current.notLoading)
	{
		vars.maxAmmoCount = current.maxAmmo;
		print("Split for ammo wedge. current count: " + (current.maxAmmo / 25).ToString());
		return true;
	}
	
	//Split when collecting a golden nut
	else if(!String.IsNullOrEmpty(current.nutName) && settings.ContainsKey(current.nutName) && settings["" + current.nutName] && !vars.completedNutSplits.Contains(current.nutName)){
		vars.completedNutSplits.Add(current.nutName);
		print("Split for golden nut: " + current.nutName);
		return true;
	}
	
	//Split when wearing a new accessory (hat)
	else if(settings["splitAccessory"] &&
			!vars.accessoryCollected.Contains(current.accessory) &&
			current.accessory != 0 &&
			current.notLoading)
	{
		vars.accessoryCollected.Add(current.accessory);
		print("Split for new outfit accessory. current accessory id: " + current.accessory.ToString());
		return true;
	}
	//Split when wearing a new clothing (body)
	else if(settings["splitClothing"] &&
			!vars.clothingCollected.Contains(current.clothing) &&
			current.clothing != 0 &&
			current.notLoading)
	{
		vars.clothingCollected.Add(current.clothing);
		print("Split for new outfit clothing. current clothing id: " + current.clothing.ToString());
		return true;
	}
	//Split when wearing a new fur
	else if(settings["splitFur"] &&
			!vars.furCollected.Contains(current.fur) &&
			current.fur != 0 &&
			current.notLoading)
	{
		vars.furCollected.Add(current.fur);
		print("Split for new outfit fur. current fur id: " + current.fur.ToString());
		return true;
	}
	//Split when wearing chipmunk fur for the first time
	else if(settings["splitChipmunk"] &&
			current.fur == 11 && (old.fur != 11 || settings["spamChipmunkSplit"]) &&
			current.notLoading)
	{
		print("Split for chipmunk fur. current fur id: " + current.fur.ToString());
		return true;
	}
	//split if the final boss reaches 0 health
	else if(settings["splitFinalBoss"] &&
			current.mapFile == "/Game/Maps/SingleMapsInGame/Bunker2/Bunker2_Boss" &&
			(current.bossMaxHealth == 1180) && //this is to check if health is initialized
			(current.bossHealth == 0) &&
			(!vars.finalSplitTriggered || settings["spamFinalSplit"]))
	{
		vars.finalSplitTriggered = true;
		print("Split for final boss");
		return true;
	}
}

reset {
	//Reset if player is teleported from initial loading position in Bunker to the first cutscene start position
	return(
		current.mapFile == "/Game/Maps/Bunker0" &&
		(Math.Abs(old.playerX - (199.03)) < 1.0) &&
		(Math.Abs(old.playerY - (-1.38)) < 1.0) &&
		(Math.Abs(old.playerZ - (98.39)) < 1.0) &&
		(Math.Abs(current.playerX - (36.0)) < 1.0) &&
		(Math.Abs(current.playerY - (-9.47)) < 1.0) &&
		(Math.Abs(current.playerZ - (633.48)) < 1.0)
	);
}

isLoading {
	//This load address isn't completely reliable, it needs to be replaced with a better one
	//It flips to 0 for a single update tick every now and then during gameplay
	//It seems to stay consistent during loading, although it looks like it ends slightly before loading finishes?
	//The old.notLoading check just adds a one-update buffer to double check before pausing for loads
	return(!current.notLoading && !old.notLoading && settings["experimentalLoads"]);
}