state("aamfp","Steam 1.01")
{
	byte 	menuload1 	 : 0x7664E4, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x7664E4, 0x38, 0x7C;
	
	string9 map			 : 0x74EA04, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x74EA04, 0x84, 0x58;
	float 	pLookSpeedMul: 0x74EA04, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x74EA04, 0x84, 0xDC;
	float 	loadingFade	 : 0x74EA04, 0xAC, 0x164;
}

state("aamfp","NoDRM 1.01")
{
	byte 	menuload1 	 : 0x7664E4, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x7664E4, 0x38, 0x7C;
	
	string9 map			 : 0x74CA04, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x74CA04, 0x84, 0x58;
	float 	pLookSpeedMul: 0x74CA04, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x74CA04, 0x84, 0xDC;
	float 	loadingFade	 : 0x74CA04, 0xAC, 0x164;
}

state("aamfp_NoSteam","NoSteam 1.01")
{
	byte 	menuload1 	 : 0x7664E4, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x7664E4, 0x38, 0x7C;
	
	string9 map			 : 0x74CA04, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x74CA04, 0x84, 0x58;
	float 	pLookSpeedMul: 0x74CA04, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x74CA04, 0x84, 0xDC;
	float 	loadingFade	 : 0x74CA04, 0xAC, 0x164;
}

state("aamfp","NoDRM 1.03")
{
	byte 	menuload1 	 : 0x76E99C, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x76E99C, 0x38, 0x7C;
	
	string9 map			 : 0x74FB84, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x74FB84, 0x84, 0x58;
	float 	pLookSpeedMul: 0x74FB84, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x74FB84, 0x84, 0xDC;
	float 	loadingFade	 : 0x74FB84, 0xAC, 0x164;
}

state("aamfp_NoSteam","NoSteam 1.03")
{
	byte 	menuload1 	 : 0x76E99C, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x76E99C, 0x38, 0x7C;
	
	string9 map			 : 0x74FB84, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x74FB84, 0x84, 0x58;
	float 	pLookSpeedMul: 0x74FB84, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x74FB84, 0x84, 0xDC;
	float 	loadingFade	 : 0x74FB84, 0xAC, 0x164;
}

state("aamfp","Steam 1.03")
{
	byte 	menuload1 	 : 0x76984C, 0x38, 0x7C, 0x4;
	byte 	menuload2	 : 0x76984C, 0x38, 0x7C;
	
	string9 map			 : 0x754CD4, 0x5C, 0x60, 0x38;
	bool 	pActive		 : 0x754CD4, 0x84, 0x58;
	float 	pLookSpeedMul: 0x754CD4, 0x84, 0xCC;
	float 	pMoveSpeedMul: 0x754CD4, 0x84, 0xDC;
	float 	loadingFade	 : 0x754CD4, 0xAC, 0x164;
}

startup{
	vars.aslName = "AmnesiaASL AMFP";
	if(timer.CurrentTimingMethod == TimingMethod.RealTime){
		
		var timingMessage = MessageBox.Show(
			"This game uses Game Time (time without loads) as the main timing method.\n"+
			"LiveSplit is currently set to show Real Time (time INCLUDING loads).\n"+
			"Would you like the timing method to be set to Game Time for you?",
			vars.aslName+" | LiveSplit",
			MessageBoxButtons.YesNo,MessageBoxIcon.Question
		);
		if (timingMessage == DialogResult.Yes)
			timer.CurrentTimingMethod = TimingMethod.GameTime;
	}
	
	// Stores previous map
	vars.lastMap = "";
	vars.injected = false;
	
	settings.Add("fullSplit",true,"Split on level changes (If disabled, will only auto-start and auto-end)");
	settings.Add("autoend",false,"[EXPERIMENTAL] Enable auto-end options");
	settings.Add("autoendEdit",false,"Manual Edit method (see AmnesiaAMFP.asl in your LiveSplit components folder)","autoend");
	settings.Add("autoendDelay",false,"5s Delay method","autoend");
	
	vars.log = (Action<string,string>)((lvl,text) => {
		print("["+vars.aslName+"] "+lvl+": "+text.Replace("-"," ")); 
	});
	
	// Copy bytes from one address to another
	// Params:  game process, address of bytes to copy, length of bytes to copy, address to copy bytes to
	// Returns: boolean result of WriteBytes
	vars.CopyMemory = (Func<Process, IntPtr, int, IntPtr, bool>)((proc, src, len, dest) =>
	{
        var bytes = proc.ReadBytes(src, len);
		return proc.WriteBytes(dest, bytes);
	});
	
	// Write MOV instruction
	// Params:  game process, address to write instruction to, length of bytes to overwrite, address to change byte at, byte to set
	// Returns: boolean result of WriteBytes
	vars.WriteMov = (Func<Process, IntPtr, int, IntPtr, byte[], bool>)((proc, src, len, dest, val) =>
	{
		var bytes = proc.ReadBytes(src, len);
		var newBytes = new List<byte>(new byte[] { 0xC6, 0x05 });
		
		var address = BitConverter.GetBytes((int) dest);
		
		newBytes.AddRange(address);
		newBytes.AddRange(val);
		
		// nop the leftover bytes
		int extraBytes = len - newBytes.Count();
		if (extraBytes > 0)
		{
			var nops = Enumerable.Repeat((byte) 0x90, extraBytes).ToArray();
			newBytes.AddRange(nops);
		}
		
		return proc.WriteBytes(src, newBytes.ToArray());
	});
	
	vars.sigGametime = new SigScanTarget(0, "8B 8E ?? 00 00 00 C6 45 D3 01");	// AOB signature for Gametime. Use the address at 8B
	vars.sigMapload  = new SigScanTarget(3, "89 45 ?? A1 ?? ?? ?? ?? 8B 88");	// AOB signature for Mapload. Use the address at A1
	vars.sigMenuload = new SigScanTarget(0, "8B ?? 70 8B ?? 30 6?");			// AOB signature for Menu. Use the address at 8B
}

shutdown
{
	if(game != null && vars.injected)
	{
		// Replace injected code with the original code
		game.Suspend();
		vars.log("DEBUG","Restoring original code");
		
		vars.CopyMemory(game, (IntPtr) vars.origGametime, 6, (IntPtr) vars.ptrGametime);
		vars.log("DEBUG","Bytes at Gametime address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrGametime, 6)).Replace("-"," "));
		
		vars.CopyMemory(game, (IntPtr) vars.origMapload, 5, (IntPtr) vars.ptrMapload);
		vars.log("DEBUG","Bytes at Mapload address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrMapload, 5)).Replace("-"," "));
		
		vars.CopyMemory(game, (IntPtr) vars.origMenu, 6, (IntPtr) vars.ptrMenuload);
		vars.log("DEBUG","Bytes at Menu address: "+BitConverter.ToString(game.ReadBytes((IntPtr) vars.ptrMenuload, 6)).Replace("-"," "));
		
		game.FreeMemory((IntPtr) vars.aslMem);
		vars.injected = false;
		vars.log("DEBUG","Restored original code");
		game.Resume();
	}
}

init 
{	
	var module = modules.First();
	var name = module.ModuleName.ToLower();
	// Fix for rare occasions when NTDLL is loaded first
	if(!name.Contains("amfp")) return;
	
	var baseAddr = module.BaseAddress;
	var size = module.ModuleMemorySize;
	vars.lastMap = "";
	vars.injected = false;
	
    switch(size)
    {
		case 8585216:
			version = name == "aamfp.exe" ? "NoDRM 1.01" : "NoSteam 1.01";
			break;
		case 8593408:
			version = "Steam 1.01";
			break;
		case 8597504:
			version = name == "aamfp.exe" ? "NoDRM 1.03" : "NoSteam 1.03";
			break;
		case 8871936:
			version = "Steam 1.03";
			break;
		default:
			version = "Unknown";
			var gameMessageText = "Website/launcher you bought the game from\r\n"+name+"="+size;
			var gameMessage = MessageBox.Show(
				"It appears you're running an unknown version of the game.\n\n"+
				"Please @PrototypeAlpha#7561 on the HPL Games Speedrunning discord with "+
				"the following:\n"+gameMessageText+"\n\n"+
				"Press OK to copy the above info to the clipboard and close this message.",
				vars.aslName+" | LiveSplit",
				MessageBoxButtons.OKCancel,MessageBoxIcon.Warning
			);
			if (gameMessage == DialogResult.OK) Clipboard.SetText(gameMessageText);
			break;
	}
	vars.log("INFO",size+" = " + version);
	
	var scanner = new SignatureScanner(game, baseAddr, size);
	
	// Scan memory for Gametime signature
	IntPtr staticGametime	=	vars.ptrGametime	=	scanner.Scan((SigScanTarget) vars.sigGametime);
	// Scan memory for Mapload signature
	IntPtr staticMapload	=	vars.ptrMapload		=	scanner.Scan((SigScanTarget) vars.sigMapload);
	// Scan memory for Menuload signature
	IntPtr staticMenuload	=	vars.ptrMenuload	=	scanner.Scan((SigScanTarget) vars.sigMenuload);
	
	// Allocate memory for our code instead of looking for a code cave
	vars.log("DEBUG","Allocating memory...");
	var aslMem = vars.aslMem = game.AllocateMemory(32);
	
	var addrMem = BitConverter.GetBytes((int) aslMem).Reverse().ToArray();
	vars.log("DEBUG","aslMem address: "+BitConverter.ToString(addrMem).Replace("-",""));
	
	if(staticGametime == IntPtr.Zero || staticMapload == IntPtr.Zero || staticMenuload == IntPtr.Zero){
		vars.log("ERROR","Can't find signatures. Unknown game version?");
		game.FreeMemory((IntPtr) aslMem);
		MessageBox.Show(
			"Can't find signatures.\n"+
			"\nstaticGametime = "+staticGametime+
			"\nstaticMapload = "+staticMapload+
			"\nstaticMenuload = "+staticMenuload,
			vars.aslName+" | LiveSplit",
			MessageBoxButtons.OK,MessageBoxIcon.Error
		);
	}
	else
	{
		vars.log("INFO","Starting inject");
		game.Suspend();
		
		// Set loading var to 1 in case we're just starting up the game
		game.WriteBytes((IntPtr)aslMem, new byte[] {1});
		
		var addrGametime = BitConverter.GetBytes((int) staticGametime).Reverse().ToArray();
		vars.log("DEBUG","Gametime address: "+BitConverter.ToString(addrGametime).Replace("-",""));
		vars.log("DEBUG","Original bytes at Gametime address: "+BitConverter.ToString(game.ReadBytes(staticGametime, 6)).Replace("-"," "));
		
		IntPtr codeGametime = aslMem+1;
		vars.WriteMov(game, codeGametime, 6, aslMem, new byte[] {0});	// Write instruction to set isLoading to 0
		vars.CopyMemory(game, staticGametime, 6, codeGametime+7);		// Write original code
		game.WriteJumpInstruction(codeGametime+7+6, staticGametime+6);	// Write jump out
		game.WriteJumpInstruction(staticGametime, codeGametime);		// Write jump in
		
		var addrMapload = BitConverter.GetBytes((int) staticMapload).Reverse().ToArray();
		vars.log("DEBUG","staticMapload address: "+BitConverter.ToString(addrMapload).Replace("-",""));
		vars.log("DEBUG","Original bytes at staticMapload address: "+BitConverter.ToString(game.ReadBytes(staticMapload, 5)).Replace("-"," "));
		
		IntPtr codeMapload = codeGametime+7+6+5;
		vars.WriteMov(game, codeMapload, 7, aslMem, new byte[] {1});	// Write instruction to set isLoading to 1
		vars.CopyMemory(game, staticMapload, 5, codeMapload+7);			// Write original code
		game.WriteJumpInstruction(codeMapload+7+5, staticMapload+5);	// Write jump out
		game.WriteJumpInstruction(staticMapload, codeMapload);			// Write jump in
		
		var addrMenuload = BitConverter.GetBytes((int) staticMenuload).Reverse().ToArray();
		vars.log("DEBUG","Menu address: "+BitConverter.ToString(addrMenuload).Replace("-",""));
		vars.log("DEBUG","Original bytes at Menu address: "+BitConverter.ToString(game.ReadBytes(staticMenuload, 6)).Replace("-"," "));
		
		IntPtr codeMenuload = codeMapload+7+5+5;
		vars.WriteMov(game, codeMenuload, 6, aslMem, new byte[] {1});	// Write instruction to set isLoading to 1
		vars.CopyMemory(game, staticMenuload, 6, codeMenuload+7);		// Write original code
		game.WriteJumpInstruction(codeMenuload+7+6, staticMenuload+6);	// Write jump out
		game.WriteJumpInstruction(staticMenuload, codeMenuload);		// Write jump in
		
		vars.injected = true;
		
		vars.origGametime = codeGametime+7;
		vars.origMapload  = codeMapload+7;
		vars.origMenu     = codeMenuload+7;
		
		vars.log("INFO","Finished injecting");
		game.Resume();
	}
	
	vars.isLoading = new MemoryWatcher<bool>(aslMem);
}

isLoading{ return vars.isLoading.Current || current.loadingFade != 0; }

start
{
	vars.lastMap = "";
	
	if(current.map == "Mansion01")
	{
		if(!vars.isLoading.Current && vars.isLoading.Old)
		{
			// Set the start offset to 00:55 to account for loading a save after the start dialogue
			if(current.pActive && old.pActive)
				timer.Run.Offset = TimeSpan.Parse("00:00:55.75");
			// Set the start offset to 00:00 to force legacy timing (-01:16) to use the new timing
			else if((!current.pActive && !old.pActive) || timer.Run.Offset > TimeSpan.Parse("00:01:15"))
				timer.Run.Offset = TimeSpan.Parse("00:00:00.00");
		}
		
		return (current.pActive && !old.pActive) || (old.pMoveSpeedMul != current.pMoveSpeedMul && old.pMoveSpeedMul == 0f);
	}
}

reset{ return current.map == "Mansion01" && old.map != current.map; }

update
{
	vars.isLoading.Update(game);
	if(current.map != vars.lastMap && old.map != null && old.map != "") vars.lastMap = old.map;
}

split
{
	if(current.map != old.map && current.map == "Mansion01") return;
	
	if(current.map == "Temple" && settings["autoend"])
	{
		// Requires editing Machine for Pigs\maps\16_temple.hps
		// Find the following line:
		// RemoveTimer("AutoEnd");
		// and replace it with:
		// SetPlayerActive(false);
		if(settings["autoendEdit"] && !current.pActive)
			return !current.pActive && old.pActive;
		// Splits 5s after button press
		if(settings["autoendDelay"] && current.pMoveSpeedMul == 0f)
			return current.pLookSpeedMul == 0.02f && old.pLookSpeedMul == 0.05f;
	}
	
	// Prevent splitting when loading from menu
	if(current.menuload1 != current.menuload2) return;
	
	//if(current.map != null && current.map != "" && vars.lastMap != "" && vars.lastMap != current.map)
	//	vars.log("MAP",current.map+", was "+vars.lastMap);
	
	if(settings["fullSplit"] && current.map != null && current.map != "" && vars.lastMap != ""){
		if(old.map != null && old.map != "" )
			 return old.map != current.map;
		else return vars.lastMap != current.map;
	}
	
}