/*
==Backrooms: Escape Together Autosplitter==
	Authored by Frogfucius
		Thanks to Reokin for the Match State variables :)

==Documentation Notes==

	Better and more general documentation for LiveSplit autosplitters can be found at https://github.com/LiveSplit/LiveSplit.AutoSplitters/blob/master/README.md
	Timer code can be found at https://github.com/LiveSplit/LiveSplit/blob/master/LiveSplit/LiveSplit.Core/Model/TimerModel.cs

	"vars" is a persistent object that is able to contain persistent variables
	"old" contains the values of all the defined variables in the last update
	"current" contains the current values of all the defined variables
	"settings" is an object used to add or get settings 
*/

state("betgame-Win64-Shipping", "0.8.0+")
{

}

startup
{
	//settings.Add("disable_restart_time_removal", false, "Disable pausing of autosplitter when restarting levels");
}

init
{
	game.Suspend();
	
	// Latent action UUIDs to check
	// Keep last one as 0 so we know when to stop
	// 0.8.0 - escape level_run
	// sp 0000000095F6E373
	// sp 000001FA26A9C400 
	var UUIDs = new ulong[] {0x0000000095F6E373, 0};
	vars.arrUUIDs = game.AllocateMemory(8 * UUIDs.Length);
	
	for (int i = 0; i < UUIDs.Length; i++)
	{
		memory.WriteValue<ulong>((IntPtr)vars.arrUUIDs + (i * 8), UUIDs[i]);
	}
	
	vars.watchers = new MemoryWatcherList();
	vars.MainMenuAddr = game.AllocateMemory(40);
	vars.Level_0Addr = game.AllocateMemory(40);
	vars.MainMenu = "/Game/Maps/BR_MainMenu/BR_MainMenu";
	vars.Level_0 = "/Game/Maps/MainLevels/Level_0/Level_0";
	for (int i = 0; i < vars.MainMenu.Length; i++)
	{
		memory.WriteValue<byte>((IntPtr)(vars.MainMenuAddr + (i * 2)), (byte)vars.MainMenu[i]);
		memory.WriteValue<byte>((IntPtr)(vars.MainMenuAddr + (i * 2) + 1), 0);
	}
	for (int i = 0; i < vars.Level_0.Length; i++)
	{
		memory.WriteValue<byte>((IntPtr)(vars.Level_0Addr + (i * 2)), (byte)vars.Level_0[i]);
		memory.WriteValue<byte>((IntPtr)(vars.Level_0Addr + (i * 2) + 1), 0);
	}	
	var scanner = new SignatureScanner(game, game.MainModule.BaseAddress, modules.First().ModuleMemorySize);


	version = "0.8.0+";

	print("[BET Autosplitter] DEBUG: BET Autosplitter loaded");

	// Match State hook -- Tells us the current state of the match
	IntPtr ptrmatchStateAddr = scanner.Scan(new SigScanTarget(0, // target the 0th bytes
	//48 8B 4C 24 20 48 8B 7C 24 ?? 48 8B ?? 24 50 48 85 C9 74 ?? E8 ?? ?? ?? ?? 48 8B 4C 24 ?? 48 85 C9 74 ?? E8 ?? ?? ?? ?? 48 8B 54 24 ??
	"48 8B 4C 24 20", // mov rcx, qword ptr ss:[rsp+0x20]
	"48 8B 7C 24 ??", // mov rdi, qword ptr ss:[rsp+0x??]
	"48 8B ?? 24 50", // mov ??, qword ptr ss:[rsp+0x50]
	"48 85 C9 74 ?? E8 ?? ?? ?? ?? 48 8B 4C 24 ?? 48 85 C9 74 ?? E8 ?? ?? ?? ?? 48 8B 54 24 ??"
	));
	
	if (ptrmatchStateAddr == IntPtr.Zero)
	{
		game.Resume();
		throw new Exception("Could not find matchState detour!");
	}

	// matchState == 0x0065006D00610047 GameStarted
	// matchState == 0x0065006A0062004F ObjectiveStarted
	// matchState == 0x007600610065004C LeavingMap
	// matchState == 0x0074006900610057 WaitingOnPlayers

	vars.matchState = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for my variable
	memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0 which is the default "not loading" state
	vars.matchStateDetour = vars.matchState + 8; // skip over the first two bytes plus 16 to account for our map strings		
	vars.watchers.Add(new MemoryWatcher<ulong>(new DeepPointer((IntPtr)vars.matchState)){ Name = "matchState" });
	
	// Offset bytes
	byte byte1 = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrmatchStateAddr + 4)) + 0x10);
	byte byte2 = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrmatchStateAddr + 9)) + 0x10);
	byte byte3 = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrmatchStateAddr + 14)) + 0x10);
	
	// Register byte
	byte bytereg = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrmatchStateAddr + 12)));
	
	print("Current: " + byte1.ToString());
	print("Current: " + byte2.ToString());
	print("Current: " + byte3.ToString());
	var matchStateDetourBytes = new byte[]
	{
		// Check to see new match state
		0x50, // push rax
		0x48, 0x8b, 0x44, 0x24, 0x48, // movsxd rax, dword ptr ds:[rsp+0x30]
		0x48, 0x8b, 0x00, // mov rax, [rax]
		0x48, 0x89, 0x05, 0xE8, 0xFF, 0xFF, 0xFF,// mov [string1], rax
		// original instructions
		0x58, // pop rax
		// add 8 to original instructions since they reference RSP and we pushed to the stack
		0x48, 0x8B, 0x4C, 0x24, byte1, // mov rcx, qword ptr ss:[rsp+0x?? + 0x10]
		0x48, 0x8B, 0x7C, 0x24, byte2, // mov rdi, qword ptr ss:[rsp+0x?? + 0x10]
		0x48, 0x8B, bytereg, 0x24, byte3, // mov ??, qword ptr ss:[rsp+0x?? + 0x10]
		0xC3 // ret
	};
	
	// bytes to detour load start function
	var matchStateHookBytes = new List<byte>()
	{
		0x50,														// push rax
		0x48, 0xB8													// mov rax, jumploc
	};
	matchStateHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.matchStateDetour));
	matchStateHookBytes.AddRange(new byte[] {
		0xFF, 0xD0,													// call rax			
		0x58,														// pop rax
		0x90
	});
	
	
	// isChangingLevel hook -- Tells if we are changing levels
	IntPtr ptrisChangingLevelAddr = scanner.Scan(new SigScanTarget(0, // target the 0th bytes
	//49 8d ?? C0 00 00 00 41 83 F1 01 48 8D 4C 24 40
	"49 8D ?? C0 00 00 00", // lea rdx, ds:[??+0xC0]
	"41 83 F1 01", // xor r9d, 0x01
	"48 8D 4C 24 40" // lea rcx, ss:[rsp+0x40]
	));
	
	if (ptrisChangingLevelAddr == IntPtr.Zero)
	{
		game.Resume();
		throw new Exception("Could not find isChangingLevel detour!");
	}

	// isChangingLevel == 1 means changing level
	// isChangingLevel == 0 means not changing level
	
	vars.isChangingLevel = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for my variable
	memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0); // Set isChangingLevel to 0 which is the default "not loading" state
	memory.WriteValue<ulong>((IntPtr)(vars.isChangingLevel + 2), (ulong)vars.MainMenuAddr); // Save address of the MainMenu path
	memory.WriteValue<ulong>((IntPtr)(vars.isChangingLevel + 10), (ulong)vars.Level_0Addr); // Save address of the MainMenu path
	vars.isChangingLevelDetour = vars.isChangingLevel + 2 + 8 + 8; // skip over the first two bytes plus 16 to account for our map strings	
	vars.watchers.Add(new MemoryWatcher<byte>(new DeepPointer((IntPtr)vars.isChangingLevel)){ Name = "isChangingLevel" });
	
	byte byte4 = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrisChangingLevelAddr + 2)));
	byte byte5 = 0;
	byte byte6 = 0;
	if (byte4 == 0x97)
	{
		// 0x97 means r15
		byte5 = 0xB7;
		byte6 = 0x8F;
	}
	else if (byte4 == 0x95)
	{
		// 0x95 means r13
		byte5 = 0xB5;
		byte6 = 0x8D;
	}
	
	byte byte7 = (byte)(memory.ReadValue<byte>((IntPtr)((ulong)ptrisChangingLevelAddr - 13)));
	byte byte8 = 0;
	byte byte9 = 0;
	byte byte10 = 0;
	byte byte11 = 0;
	if (byte7 == 0x06)
	{
		// 0x06 means r14
		byte8 = 0x4C;
		byte9 = 0xF7;
		byte10 = 0x49;
		byte11 = 0x3E;
	}
	else if (byte7 == 0x03)
	{
		// 0x03 means rbx
		byte8 = 0x48;
		byte9 = 0xDf;
		byte10 = 0x48;
		byte11 = 0x3f;
	}
	var isChangingLevelDetourBytes = new byte[]
	{
		// Check to see what map we are loading
		0x57, // push rdi
		0x56, // push rsi
		0x51, // push rcx
		0x52, // push rdx
		byte8, 0x89, byte9, // mov rdi, ??
		0x49, 0x8D, byte5, 0xE8, 0x00, 0x00, 0x00, // lea rsi, ds:[r15+0xE8] -- address of &unreal + 0x28 for current level
		0x49, 0x63, byte6, 0xF0, 0x00, 0x00, 0x00,// movsxd rcx, dword ptr ds:[r15+0xF0]
		0x80, 0xe9, 0x01, // sub cl, 1
		0x48, 0xD1, 0xE1, // shl rcx, 0x01
		byte10, 0x8B, byte11, // mov rdi, qword ptr ds:[??]
		0x48, 0x8B, 0x36, // mov rsi, qword ptr ds:[rsi]
		0xF3, 0xA6, // repe cmpsb
		0x74, 0x50, //je blah -- if the strings are the same then don't set variable
		// now check to see if we're going to the main menu
		0x48, 0x89, 0xDF, // mov rdi, rbx
		byte10, 0x8B, byte11, // mov rdi, qword ptr ds:[??]
		0xE8, 0x00, 0x00, 0x00, 0x00,// call 0
		0x5E, // pop rsi
		0x48, 0x83, 0xEE, 0x40, //sub rsi, 0x38
		0x48, 0x8B, 0x36, // mov rsi, qword ptr ds:[rsi]
		0x48, 0xC7, 0xC1, 0x44, 0x00, 0x00, 0x00, // mov rcx, 0x22
		0xF3, 0xA6, // repe cmpsb
		0x74, 0x32, //je blah -- if the strings are the same then don't set variable
		//0x66, 0xC7, 0x05, 0xAA, 0xFF, 0xFf, 0xFF, 0x01, 0x00,// mov [var], 1
		// now check to see if we're going to level_0
		0x48, 0x89, 0xDF, // mov rdi, rbx
		byte10, 0x8B, byte11, // mov rdi, qword ptr ds:[??]
		0xE8, 0x00, 0x00, 0x00, 0x00,// call 0
		0x5E, // pop rsi
		0x48, 0x83, 0xEE, 0x56, //sub rsi, 0x38
		0x48, 0x8B, 0x36, // mov rsi, qword ptr ds:[rsi]
		0x48, 0xC7, 0xC1, 0x4A, 0x00, 0x00, 0x00, // mov rcx, 0x22
		0xF3, 0xA6, // repe cmpsb
		0x75, 0x0B, //je blah -- if not level_0 then set var to 1. otherwise 2
		0x66, 0xC7, 0x05, 0x84, 0xFF, 0xFf, 0xFF, 0x02, 0x00,// mov [var], 2
		0xEB, 0x09, // jmp end
		0x66, 0xC7, 0x05, 0x79, 0xFF, 0xFf, 0xFF, 0x01, 0x00,// mov [var], 1
		// original instructions
		0x5A, // pop rax
		0x59, // pop rax
		0x5E, // pop rax
		0x5F, // pop rax
		// add 8 to original instructions since they reference RSP and we pushed to the stack
		0x49, 0x8D, byte4, 0xC0, 0x00, 0x00, 0x00, // lea rdx, ds:[r15+0xC0]
		0x41, 0x83, 0xF1, 0x01, // xor r9d, 0x01
		0x48, 0x8D, 0x4C, 0x24, 0x50, // lea rcx, ss:[rsp+0x40 + 0x10]
		0xC3 // ret
	};
	
	// bytes to detour load start function
	var isChangingLevelHookBytes = new List<byte>()
	{
		0x50,														// push rax
		0x48, 0xB8													// mov rax, jumploc
	};
	isChangingLevelHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.isChangingLevelDetour));
	isChangingLevelHookBytes.AddRange(new byte[] {
		0xFF, 0xD0,													// call rax			
		0x58,														// pop rax
		0x90, 0x90
	});
	// suspend game while writing so it doesn't crash
	
	
	// Place hook on FLatentActionManager::AddNewAction	 
	IntPtr ptrIsExitingZoneAddr = scanner.Scan(new SigScanTarget(0, // target the 0th bytes
	//53 55
	"53",// push rbx
	"55", // push rbp
	"41 56", // push r14
	"48 83 EC 50", // sub rsp, 0x50
	"48 8B D9", // mov rbx, rcx
	"48 C7 44 24 78 00 00 00 00", // mov qword ptr ss:[rsp+0x78], 0x00
	"48 8D 4C 24 78" // lea rcx, ss:[rsp+0x78]
	));
	
	if (ptrIsExitingZoneAddr == IntPtr.Zero)
	{
		game.Resume();
		throw new Exception("Could not find ptrIsExitingZoneAddr detour!");
	}

	// isExitingZone == 2 means we already split but need to wait for level change
	// isExitingZone == 1 means exiting zone
	// isExitingZone == 0 means not exiting zone
	
	vars.isExitingZone = game.AllocateMemory(80); // allocate 80 bytes for detour, first two bytes are for variable
	memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 0); // Set isExitingZone to 0 which is the default "not exiting" state
	memory.WriteValue<ulong>((IntPtr)vars.isExitingZone + 2, (ulong)vars.arrUUIDs); // Set isExitingZone to 0 which is the default "not exiting" state
	vars.isExitingZoneDetour = vars.isExitingZone + 2 + 8; // skip over the first 2 bytes plus 8 for our array address
	vars.watchers.Add(new MemoryWatcher<byte>(new DeepPointer((IntPtr)vars.isExitingZone)){ Name = "isExitingZone" });
	var isExitingZoneDetourBytes = new byte[]
	{
		0x53,//push rbx
		0x50,//push rax
//		0x41, 0x50, // push r8
		0x57, //push rdi
		0x48, 0x31, 0xc0,// xor rax, rax
		0x48, 0x8b, 0x1d, 0xEB, 0xFF, 0xFF, 0xFF,//mov rbx, array
//		0x4c, 0x8B, 0x44, 0x24, 0x38, // mov r8, qword ptr ss:[rsp+0x38]
		// loop
		0x48, 0x8b, 0x3c, 0x03,// mov rdi, [rbx + rax]
		0x48, 0x83, 0xff, 0x00,// cmp rdi, 0
		0x74, 0x14,// je end
		0x4c, 0x39, 0xc7,// cmp rdi, r8
		0x74, 0x06,//je set
		0x48, 0x83, 0xc0, 0x08,// add rax,8
		0xeb, 0xeb,//jmp loop
		0x66, 0xC7, 0x05, 0xCB, 0xFF, 0xFF, 0xFF, 0x01, 0x00,	// mov word ptr ds:[7FF78D6366BD],1 (set isExitingZone to 1)
		0x5F, // pop rdi
//		0x41, 0x58, //pop r8
		0x58, //pop rax
		0x5b, //pop rbx
		// original instructions
		0x48, 0x8B, 0xD9,// mov rbx, rcx
		0x48, 0xC7, 0x84, 0x24, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, // mov qword ptr ss:[rsp+0x78 + 0x8 - 0x50 - 0x18], 0x00
		0x48, 0x8D, 0x8C, 0x24, 0x18, 0x00, 0x00, 0x00, // lea rcx, ss:[rsp+0x78 + 0x8 - 0x50 - 0x18]
		0xC3 // ret
	};
	
	// bytes to detour load start function
	var isExitingZoneHookBytes = new List<byte>()
	{
		//0x50,														// push rax
		0x48, 0xB8													// mov rax, jumploc
	};
	isExitingZoneHookBytes.AddRange(BitConverter.GetBytes((ulong)vars.isExitingZoneDetour));
	isExitingZoneHookBytes.AddRange(new byte[] {
		0xFF, 0xD0,													// call rax 
		0x53, 
		0x55,
		0x41, 0x56,
		0x48, 0x83, 0xEC, 0x50,
		0x90, 0x90, 0x90, 0x90, 0x90
	});
	
	try
	{
		// write the detour code at the allocated memory address
		game.WriteBytes((IntPtr)vars.matchStateDetour, matchStateDetourBytes);
		game.WriteBytes((IntPtr)vars.isChangingLevelDetour, isChangingLevelDetourBytes);
		game.WriteBytes((IntPtr)vars.isExitingZoneDetour, isExitingZoneDetourBytes);
		// write detour calls
		game.WriteBytes(ptrmatchStateAddr, matchStateHookBytes.ToArray());
		game.WriteBytes(ptrisChangingLevelAddr, isChangingLevelHookBytes.ToArray());
		game.WriteBytes(ptrIsExitingZoneAddr, isExitingZoneHookBytes.ToArray());
	}
	catch
	{
		vars.FreeMemory(game);
		game.Resume();
		throw;
	}
	finally
	{
		game.Resume();
	}	
}

update
{
	vars.watchers.UpdateAll(game);
}

start
{
	// matchState == 0x0065006D00610047 GameStarted
	// matchState == 0x0065006A0062004F ObjectiveStarted
	// matchState == 0x007600610065004C LeavingMap
	// matchState == 0x0074006900610057 WaitingOnPlayers
	var doStart = (vars.watchers["matchState"].Current > 0x0 && vars.watchers["matchState"].Old == 0x0) || vars.watchers["isChangingLevel"].Current == 2;
	//var waitingOrStarted = (vars.watchers["matchState"].Current == 0x0074006900610057) || (vars.watchers["matchState"].Current == 0x0065006A0062004F) || (vars.watchers["matchState"].Current == 0x0065006D00610047);
	//var loadingMap2 = (waitingOrStarted && vars.watchers["matchState"].Old == 0x0);
	
	//if (loadingMap2)
	//{
		//print("loadaidj")
	//}
	
	// print("Current: " + vars.watchers["matchState"].Current.ToString());
	// print("Old: " + vars.watchers["matchState"].Old.ToString());
	if (doStart)
	{
		if(vars.watchers["isChangingLevel"].Current == 2)
		{
			print("blah!!!");
			// We are loading level_0
			
			if (vars.watchers["matchState"].Current == 0x0065006D00610047 || vars.watchers["matchState"].Current == 0x0065006A0062004F) //GameStarted
			{
				memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
				memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0); // Set isChangingLevel to 0
				vars.watchers["matchState"].Current = 0x0;
				vars.watchers["matchState"].Old = 0x0;
				vars.watchers["isChangingLevel"].Current = 0;
				return true;
			}
			
			return false;
		}
		else if (vars.watchers["isChangingLevel"].Current == 1)
		{
			memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
			memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0); // Set isChangingLevel to 0
			vars.watchers["matchState"].Current = 0x0;
			vars.watchers["matchState"].Old = 0x0;
			vars.watchers["isChangingLevel"].Current = 0;
			return true;
		}
		else
		{
			print("blah!!!1");
			if(vars.watchers["matchState"].Current == 0x007600610065004C)
			{
				print("blah!!!2");
				// If state is LeavingMap then ignore it and set it back to 0
				memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
				vars.watchers["matchState"].Current = 0x0;
				vars.watchers["matchState"].Old = 0x0;
				return false;
			}
			//if(vars.watchers["matchState"].Current == 0x0065006A0062004F && vars.watchers["matchState"].Old == 0x0)
			//{
			//	print("blah!!!3");
			//	memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
			//	vars.watchers["matchState"].Current = 0x0;
			//	vars.watchers["matchState"].Old = 0x0;
			//	return false;
			//}
			if(vars.watchers["matchState"].Current == 0x0074006900610057 && vars.watchers["matchState"].Old == 0x0)
			{
				memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
				vars.watchers["matchState"].Current = 0x0;
				vars.watchers["matchState"].Old = 0x0;
				return false;
			}
			print("blah!!!4");
			memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
			memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0); // Set isChangingLevel to 0
			vars.watchers["matchState"].Current = 0x0;
			vars.watchers["matchState"].Old = 0x0;
			vars.watchers["isChangingLevel"].Current = 0;
			return true;
		}
		
	}

	return false;
}

split
{
	if (vars.watchers["isChangingLevel"].Current == 1)
	{
		vars.watchers["isChangingLevel"].Current = 0;
		memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0); // Set isChangingLevel to 0
		if (vars.watchers["isExitingZone"].Current == 0x02)
		{
			vars.watchers["isExitingZone"].Current = 0x00;
			memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 0); // Set isExitingZone to 0
			return false;
		}
		return true;
	}
	if (vars.watchers["isExitingZone"].Current == 0x01)
	{
		vars.watchers["isExitingZone"].Current = 0x02;
		memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 2); // Set isExitingZone to 2
		return true;
	}

	return false;
}

isLoading
{	
	if (vars.watchers["matchState"].Current == 0x007600610065004C)
	{
		return true;
	}
	return false;
}

shutdown 
{
}

onReset
{
	try
	{
		memory.WriteValue<ulong>((IntPtr)vars.matchState, 0); // Set matchState to 0
		memory.WriteValue<byte>((IntPtr)vars.isChangingLevel, 0);
		memory.WriteValue<byte>((IntPtr)vars.isExitingZone, 0);
		vars.watchers["matchState"].Current = 0x0;
	}
	catch
	{
	}
}