User File #63202701655134999

Upload All User Files

#63202701655134999 - Luminous Arc map bot

Luminous_Arc_coroutine_map_bot.lua
Game: Luminous Arc ( DS, see all files )
307 downloads
Uploaded 5/8/2020 8:01 AM by Nietono (see all 5)
A bot for executing strategies in the USA version of Luminous Arc. For use with DeSmuME.
Corrected a lot of false assumptions regarding handling of lag frames, fixed a number of wait times, and generally cleaned up the code.
There may still be some issues when selecting a tile with buttons (though ideally tiles shouldn't need to be selected with buttons). Using items is still unfinished.
-- For use with the USA version of Luminous Arc
-- Developed by Nietono

-- Enumeration
magic, skill, flashDrive, synergy, item = 0, 1, 2, 3, 4
north, east, south, west = 0, 1, 3, 2
shiftLeft, shiftRight = -2, 2
touchModeR, touchModeL, buttonMode = 0, 1, 2
fastDelay, normalDelay, slowDelay = 0, 1, 2

-- States
storageEnabled = true
storedInputExists = false
targetWithButtons = false
resetAfterButtonTarget = true
toWait = normalDelay
toWaitForWait = normalDelay
toWaitForAttack = normalDelay
saveStatesEnabled = false
saveSlot = 0
minimiseHold = false;

-- Memory addresses
function GetCurrentUnit()
	return memory.readbyte(0x02159C68)
end

function GetUIMovement()
	return memory.readbyte(0x02257DC8)
end

function GetMenuType()
	return memory.readbyte(0x02258254)
end

function GetTransitionState()
	return memory.readbyte(0x022578E4)
end

function GetOtherTransitionState()
	return memory.readbyte(0x02136AEC)
end

function GetMenuScroll()
	return memory.readbyte(0x02257DC8)
end

function LostFrameOnConfirm()
	return memory.readbyte(0x0225C754)
end

function GetDefaultFacing()
	return memory.readbyte(0x022581F2)
end

function GetScrollX()
	return memory.readword(0x0214F324)
end

function GetScrollY()
	return memory.readword(0x0214F326)
end

function MenuVisible()
	return memory.readbyte(0x0225C4FA) ~= 0
end

function MenuOnLeft()
	return memory.readbyte(0x02257924) == 1
end

function GetInputMode()
	return memory.readbyte(0x02257910)
end

function GetMenuSelectionPos()
	return memory.readbyte(0x022581F2)
end

function GetCursorX()
	return memory.readbyte(0x02257900)
end

function GetCursorY()
	return memory.readbyte(0x02257904)
end

function GetTotalUnitCount()
	return memory.readbyte(0x02257D9A)
end

function GetPlayerUnitCount()
	return memory.readbyte(0x02257D9C)
end

function GetEnemyUnitCount()
	return GetTotalUnitCount() -  GetPlayerUnitCount()
end

function AllEnemiesKilled()
	return GetTotalUnitCount() ==  GetPlayerUnitCount()
end

function GetUnitCoords(unit)
	local nameAddress = 0x021591DC + 0xA6 * unit
	local xAddress, yAddress = nameAddress + 0x9A, nameAddress + 0x9C
	return memory.readbyte(xAddress), memory.readbyte(yAddress)
end

function GetUnitFacing(unit)
	local nameAddress = 0x021591DC + 0xA6 * unit
	return memory.readbyte(nameAddress + 0xA0)
end

-- This currently only works correctly for player units
function GetUnitSpriteFlip(unit)
	local address = 0x0225DDAE + 0x1F8 * unit
	return memory.readbyte(address)
end

-- This currently only works correctly for player units
function GetUnitSpriteDirection(unit)
	local address = 0x0225DE22 + 0x1F8 * unit
	return memory.readbyte(address)
end

-- This currently only works correctly for player units
function GetUnitInterimFacing(unit)
	local spriteFlip, spriteFacing = GetUnitSpriteFlip(unit), GetUnitSpriteDirection(unit)
	return 2 * (1 - spriteFacing) + spriteFlip
end

-- This currently only works correctly for player units
function GetUnitOtherCount(unit, otherType)
	if otherType == item then
		-- TODO: find an address for this 
		return 255
	else
		local address = 0x0225DE2A + 0x1F8 * unit + 0x2 * otherType
		return memory.readbyte(address)
	end
end

-- Menu IDs
function GetWaitPosFromMenuID(id)
	if id == 12 then
		return 3
	elseif id == 13 or id == 24 then
		return 2
	elseif id == 25 then
		return 1
	else
		return -1
	end
end

function GetActionPosFromMenuID(id)
	if id == 12 then
		return 2
	elseif id == 13 then
		return 1
	else
		return -1
	end
end

function GetOtherPosFromActionMenuID(otherType, id)
	if id == 23 then
		if otherType == item then
			return 2
		else
			return -1
		end
	elseif id == 22 then
		if otherType == magic then
			return 2
		elseif otherType == item then
			return 3
		else
			return -1
		end
	elseif id == 21 then
		if otherType == skill then
			return 2
		elseif otherType == item then
			return 3
		else
			return -1
		end
	elseif id == 20 then
		if otherType == magic then
			return 2
		elseif otherType == skill then
			return 3
		elseif otherType == item then
			return 4
		else
			return -1
		end
	elseif id == 19 then
		if otherType == skill then
			return 2
		elseif otherType == flashDrive then
			return 3
		elseif otherType == synergy then
			return 4
		elseif otherType == item then
			return 5
		else
			return -1
		end
	elseif id == 18 then
		if otherType == magic then
			return 2
		elseif otherType == flashDrive then
			return 3
		elseif otherType == synergy then
			return 4
		elseif otherType == item then
			return 5
		else
			return -1
		end
	elseif id == 17 then
		if otherType == magic then
			return 2
		elseif otherType == skill then
			return 3
		elseif otherType == flashDrive then
			return 4
		elseif otherType == synergy then
			return 5
		elseif otherType == item then
			return 6
		else
			return -1
		end
	elseif id == 16 then
		if otherType == magic then
			return 2
		elseif otherType == flashDrive then
			return 3
		elseif otherType == item then
			return 4
		else
			return -1
		end
	elseif id == 15 then
		if otherType == skill then
			return 2
		elseif otherType == flashDrive then
			return 3
		elseif otherType == item then
			return 4
		else
			return -1
		end
	elseif id == 14 then
		if otherType == magic then
			return 2
		elseif otherType == skill then
			return 3
		elseif otherType == flashDrive then
			return 4
		elseif otherType == item then
			return 5
		else
			return -1
		end
	else
		return -1
	end
end

function IsBaseMenu(id)
	return id == 12 or id == 13 or id == 24 or id == 25
end

function IsActionMenu(id)
	return id >= 14 and id <= 23
end

function IsOtherActionMenu(id)
	return id == 65 or id == 69 or id == 74 or id == 78 or id == 83 
end

function GetOtherMenuID(otherType)
	if otherType == magic then
		return 74
	elseif otherType == skill then
		return 69
	elseif otherType == flashDrive then
		return 78
	elseif otherType == synergy then
		return 83
	elseif otherType == item then
		return 65
	else
		return -1
	end
end

function GetOtherTypeFromID(id)
	if id == 65 then
		return item
	elseif id == 69 then
		return skill
	elseif id == 74 then
		return magic
	elseif id == 78 then
		return flashDrive
	elseif id == 83 then
		return synergy
	end
end

function GetOptionCountFromMenuID(id)
	if id == 17 then
		return 7
	elseif id == 14 or id == 18 or id == 19 then
		return 6
	elseif id == 12 or id == 15 or id == 16 or id == 20 then
		return 5
	elseif id == 13 or id == 21 or id == 22 then
		return 4
	elseif id == 23 or id == 24 or id == 25 then
		return 3
	elseif IsOtherActionMenu(id) then
		return GetUnitOtherCount(GetCurrentUnit(), GetOtherTypeFromID(id))
	else
		return 0
	end
end

-- Conversion and useful functions
function MapToScreenCoords(mapX, mapY, height)
	local scrollX, scrollY = GetScrollX(), GetScrollY()
	local newX = 256 - scrollX + 16 * (mapX - mapY)
	local newY = 632 - scrollY - 8 * (mapX + mapY + height)
	return newX, newY
end

function GetRelativeFacing(unit, target)
	if unit == target then
		return GetUnitFacing(unit)
	else
		local unitX, unitY = GetUnitCoords(unit)
		local targetX, targetY = GetUnitCoords(target)
		return GetRelativeFacingFromPositions(unitX, unitY, targetX, targetY)
	end
end

function GetRelativeFacingFromPositions(unitX, unitY, targetX, targetY)
	local xDiff, yDiff = math.abs(unitX - targetX), math.abs(unitY - targetY)
	if yDiff > xDiff then
		if unitY > targetY then
			return south
		else
			return north
		end
	else
		if unitX > targetX then
			return west
		else
			return east
		end
	end
end

function FindStepsToOption(optionPos, id)
	local currentPos, maxOption = GetMenuSelectionPos() + 1, GetOptionCountFromMenuID(id)
	local downSteps = (optionPos - currentPos + maxOption) % maxOption
	local upSteps = maxOption - downSteps

	if (upSteps < downSteps) then
		return -1 * upSteps
	end

	return downSteps
end

-- Selection and holding
function SelectNthOptionOnLeft(n)
	stylus.set{x = 15, y = 24 * n, touch = true}
end

function SelectNthOptionOnRight(n)
	stylus.set{x = 240, y = 24 * n, touch = true}
end

function SelectNthOption(n)
	if MenuOnLeft() then
		SelectNthOptionOnLeft(n)
	else
		SelectNthOptionOnRight(n)
	end
end

function SelectNthOtherOption(n)
	stylus.set{x = 148, y = 8 + 16 * n, touch = true}
end

function HoldForNFrames(xIn, yIn, n)
	for i = 1, n do
		HoldThroughLag(function() stylus.set{x = xIn, y = yIn, touch = true} end)
	end
end

function HoldNthOptionOnLeft(n, frames)
	HoldForNFrames(15, 24 * n, frames)
end

function HoldNthOptionOnRight(n, frames)
	HoldForNFrames(240, 24 * n, frames)
end

function HoldNthOption(n, frames)
	if MenuOnLeft() then
		HoldNthOptionOnLeft(n, frames)
	else
		HoldNthOptionOnRight(n, frames)
	end
end

function HoldNthOtherOption(n, frames)
	HoldForNFrames(148, 8 + 16 * n, frames)
end

function HoldUntilState(xIn, yIn, minFrames, targetState)
	if saveStatesEnabled and minimiseHold then
		-- Assuming the min hold will be 2 frames
		FindTwoFrameHoldStart(function() return GetTransitionState() == targetState end)
	end

	HoldForNFrames(xIn, yIn, minFrames)

	while GetTransitionState() ~= targetState do
		stylus.set{x = xIn, y = yIn, touch = true}
		coroutine.yield()
	end
end

function HoldUntilOtherState(xIn, yIn, minFrames, targetState)
	if saveStatesEnabled and minimiseHold then
		-- Assuming the min hold will be 2 frames
		FindTwoFrameHoldStart(function() return GetOtherTransitionState() == targetState end)
	end

	HoldForNFrames(xIn, yIn, minFrames)

	while GetOtherTransitionState() ~= targetState do
		stylus.set{x = xIn, y = yIn, touch = true}
		coroutine.yield()
	end
end

function HoldOptionUntilStateOnSide(selectionPos, minFrames, targetState, switchMode, holdFunction, selectFunction)
	switchMode = switchMode or false
	
	local done = false

	if GetInputMode() ~= buttonMode then
		if saveStatesEnabled and minimiseHold then
			local function StateReached()
				return GetTransitionState() == targetState or switchMode and GetMenuScroll() == 0 and GetInputMode() == touchModeR
			end

			-- Assuming the min hold will be 2 frames
			FindTwoFrameHoldStart(StateReached)
		end

		holdFunction(selectionPos, minFrames)
	end

	while GetTransitionState() ~= targetState and not done do
		if GetInputMode() ~= buttonMode then
			selectFunction(selectionPos)

			if switchMode and GetMenuScroll() == 0 and GetInputMode() == touchModeR then
				SetInputMode(touchModeL)
				done = true
			end
		end

		coroutine.yield()
	end
end

function HoldOptionUntilState(selectionPos, minFrames, targetState, switchMode)
	if MenuOnLeft() then
		HoldOptionUntilStateOnSide(selectionPos, minFrames, targetState, switchMode, HoldNthOptionOnLeft, SelectNthOptionOnLeft)
	else
		HoldOptionUntilStateOnSide(selectionPos, minFrames, targetState, switchMode, HoldNthOptionOnRight, SelectNthOptionOnRight)
	end
end

function HandleOptionSelection(selectionPos, id, minFrames, targetState, switchMode)
	if GetInputMode() == buttonMode then
		WaitUntilState(targetState)
		SelectOptionWithButtons(selectionPos, id)
	else
		HoldOptionUntilState(selectionPos, minFrames, targetState, switchMode)
	end
end

function HoldOtherOptionUntilState(selectionPos, minFrames, targetState)
	if GetInputMode() ~= buttonMode then
		if saveStatesEnabled and minimiseHold then
			-- Assuming the min hold will be 2 frames
			FindTwoFrameHoldStart(function() return GetTransitionState() == targetState end)
		end

		HoldNthOtherOption(selectionPos, minFrames)
	end

	while GetTransitionState() ~= targetState do
		if GetInputMode() ~= buttonMode then
			SelectNthOtherOption(selectionPos)
		end

		coroutine.yield()
	end
end

-- TODO: test whether my fixes broke this
function SelectOptionWithButtons(optionPos, id)
	local steps = FindStepsToOption(optionPos, id)
	local down = steps > 0
	local optionCount = GetOptionCountFromMenuID(id)

	if not down then
		steps = steps * -1
	end

	optionPos = optionPos - 1
	
	local currentPos  = GetMenuSelectionPos()
	local nextPos = currentPos

	for i = 1, steps do
		if down then
			HoldThroughLag(function() joypad.set{down = true} end)
			nextPos = (nextPos + 1) % optionCount
		else
			HoldThroughLag(function() joypad.set{up = true} end)
			nextPos = (nextPos - 1 + optionCount) % optionCount
		end
		
		currentPos = GetMenuSelectionPos()

		if down then
			HoldThroughLag(function() joypad.set{down = true} end)
		else
			HoldThroughLag(function() joypad.set{up = true} end)
		end

		if GetMenuSelectionPos() ~= optionPos then
			WaitNFrames(1)
		end
	end

	while (GetMenuType() == id) do
		joypad.set{A = true}
		WaitNFrames(1)
	end
end

function SetTouchAndMode(storedX, storedY)
	stylus.set{x = storedX, y = storedY, touch = true}
	joypad.set{select = true}
end

function SetInputMode(mode, storedX, storedY)
	local currentMode = GetInputMode()
	local storeInput = storedX and storedY
	local changes = (mode - currentMode + 3) % 3

	for i = 1, changes do
		if storeInput and mode == buttonMode and currentMode == touchModeL then
			HoldThroughLag(function() stylus.set{x = storedX, y = storedY, touch = true} end)
			HoldThroughLag(function() SetTouchAndMode(storedX, storedY) end)
			storedInputExists = true
		else
			storedInputExists = false
			HoldThroughLag(function() joypad.set{select = true} end)
		end

		if i ~= changes then
			WaitNFrames(1)
		end
	end
end

-- Waiting
function WaitNFrames(n)	
	for i = 1, n do
		coroutine.yield()
	end
end

function HoldThroughLag(pressAction)
	pressAction()
	coroutine.yield()

	while emu.lagged() do
		pressAction()
		coroutine.yield()
	end
end

function WaitForScrollStop()
	local currentScrollX, currentScrollY = GetScrollX(), GetScrollY()
	local oldScrollX, oldScrollY = currentScrollX, currentScrollY
	local stopped = false

	while not stopped do
		coroutine.yield()
		currentScrollX, currentScrollY = GetScrollX(), GetScrollY()
		stopped = currentScrollX == oldScrollX and currentScrollY == oldScrollY
		oldScrollX, oldScrollY = currentScrollX, currentScrollY
	end
end

function WaitForVisibleMenu()
	while not MenuVisible() do
		coroutine.yield()
	end
end

function WaitForUnitTurn(unit)
	local currentUnit = GetCurrentUnit()

	while currentUnit ~= unit do
		coroutine.yield()
		currentUnit = GetCurrentUnit()
	end
end

function WaitForUnitTurnAndVisibleMenu(unit)
	WaitForUnitTurn(unit)
	WaitForVisibleMenu()
end

function WaitForPlayerTurn()
	local currentUnit = GetCurrentUnit()

	while currentUnit > 7 do
		coroutine.yield()
		currentUnit = GetCurrentUnit()
	end
end

function WaitForBaseMenuID()
	local id = GetMenuType()

	while not IsBaseMenu(id) do
		coroutine.yield()
		id = GetMenuType()
	end

	return id
end

function WaitForActionMenuID()
	local id = GetMenuType()

	while not IsActionMenu(id) do
		coroutine.yield()
		id = GetMenuType()
	end

	return id
end

function WaitForMenuType(id)
	while GetMenuType() ~= id do
		coroutine.yield()
	end
end

function WaitUntilState(targetState)
	while GetTransitionState() ~= targetState do
		coroutine.yield()
	end
end

function WaitUntilOtherState(targetState)
	while GetOtherTransitionState() ~= targetState do
		coroutine.yield()
	end
end

function WaitUntilMenuScroll(targetScroll)
	while GetMenuScroll() ~= targetScroll do
		coroutine.yield()
	end
end

function WaitPastMenuScroll(oldState)
	while GetMenuScroll() == oldState do
		coroutine.yield()
	end
end

function WaitForAllEnemiesKilled()
	while not AllEnemiesKilled() do
		coroutine.yield()
	end
end

-- Save states
function SetSaveState(useSaveStates, newSaveSlot)
	saveSlot = newSaveSlot or saveSlot
	saveStatesEnabled = useSaveStates
end

function FindTwoFrameHoldStart(condition)
	local holdFrame, followingFrame = emu.framecount(), emu.framecount()

	savestate.save(saveSlot)

	while not condition() do
		coroutine.yield()

		if not emu.lagged() then
			holdFrame = followingFrame
			followingFrame = emu.framecount() - 1
		end
	end

	savestate.load(saveSlot)

	while emu.framecount() < holdFrame do
		coroutine.yield()
	end
end

-- Actions
function SetButtonTargeting(useButtons)
	targetWithButtons = useButtons
	resetAfterButtonTarget = not useButtons
end

function UseButtonsForNextTarget()
	targetWithButtons = true
end

function SetStorage(useStorage)
	storageEnabled = useStorage
end

function SetMinimiseHold(minimise)
	 minimiseHold = minimise
end

function SetNextWaitDelay(delay)
	toWaitForWait = delay
end

function UpdateWaitConfirmDelay()
	toWait = toWaitForWait
	toWaitForWait = normalDelay
end

function SetNextAttackDelay(delay)
	toWaitForAttack = delay
end

function UpdateAttackConfirmDelay()
	toWait = toWaitForAttack
	toWaitForAttack = normalDelay
end

function UpdateRemaining(remaining)
	if (remaining > 0) then
		remaining = remaining - 1
	else
		remaining = remaining + 1
	end

	return remaining
end

function SelectTileWithButtons(tileX, tileY)
	local currentX, currentY = GetCursorX(), GetCursorY()
	local remainingX, remainingY = tileX - currentX, tileY - currentY
	local absX, absY = math.abs(remainingX), math.abs(remainingY)
	local xFirst, steps = absX > absY, math.max(absX, absY)

	for i = 1, steps do
		if xFirst then
			MoveCursorLeftOrRight(remainingX)
			remainingX = UpdateRemaining(remainingX)

			if (remainingY ~= 0) then
				MoveCursorUpOrDown(remainingY)
				remainingY = UpdateRemaining(remainingY)
			elseif remainingX ~= 0 then
				WaitNFrames(1)
			end
		else
			MoveCursorUpOrDown(remainingY)
			remainingY = UpdateRemaining(remainingY)

			if (remainingX ~= 0) then
				MoveCursorLeftOrRight(remainingX)
				remainingX = UpdateRemaining(remainingX)
			elseif remainingY ~= 0 then
				WaitNFrames(1)
			end
		end
	end
	
	joypad.set{A = true}
	WaitNFrames(1)
end

function MoveCursorUpOrDown(direction)
	if direction > 0 then
		HoldThroughLag(function() joypad.set{up = true} end)
	else
		HoldThroughLag(function() joypad.set{down = true} end)
	end
end

function MoveCursorLeftOrRight(direction)
	if direction > 0 then
		HoldThroughLag(function() joypad.set{right = true} end)
	else
		HoldThroughLag(function() joypad.set{left = true} end)
	end
end

function SelectConfirm()
	if GetInputMode() ~= buttonMode then
		if saveStatesEnabled and minimiseHold then
			FindTwoFrameHoldStart(function() return GetMenuScroll() == 80 end)
		end

		HoldForNFrames(80, 168, 2)
	end

	while GetMenuScroll() ~= 80 do
		if GetInputMode() ~= buttonMode then
			stylus.set{x = 80, y = 168, touch = true}
		end

		coroutine.yield()
	end

	for i = 1, toWait do
		if GetInputMode() ~= buttonMode then
			stylus.set{x = 80, y = 168, touch = true}
		end

		coroutine.yield()
	end

	toWait = normalDelay

	if GetInputMode() == buttonMode then
		while GetTransitionState() == 29 do
			joypad.set{A = true}
			coroutine.yield()
		end
	end
	
	WaitNFrames(1)
end

function WaitInDirection(targetDirection)
	local x, y = 0, 0
	local currentDirection = GetDefaultFacing()

	if targetDirection == 0 then
		x, y = 0, 0
	elseif targetDirection == 1 then
		x, y = 255, 0
	elseif targetDirection == 2 then
		x, y = 0, 192
	elseif targetDirection == 3 then
		x, y = 255, 192
	end

	if GetInputMode() == buttonMode or storedInputExists then
		if targetDirection == north then
			HoldThroughLag(function() joypad.set{up = true} end)
		elseif targetDirection == east then
			HoldThroughLag(function() joypad.set{right = true} end)
		elseif targetDirection == south then
			HoldThroughLag(function() joypad.set{down = true} end)
		elseif targetDirection == west then
			HoldThroughLag(function() joypad.set{left = true} end)
		end
			
	else
		HoldForNFrames(x, y, 2)
		WaitNFrames(1)
	end

	if currentDirection ~= targetDirection then
		if GetInputMode() == buttonMode then
			joypad.set{A = true}
		else
			HoldForNFrames(x, y, 2)
		end
	end

	WaitNFrames(1)
end

function SelectWait(targetDirection)
	local id = WaitForBaseMenuID()
	local targetState = id
	local selectionPos = GetWaitPosFromMenuID(id)
	local useButtons = storageEnabled and GetUnitInterimFacing(GetCurrentUnit()) ~= targetDirection
	
	if GetTransitionState() ~= id then
		WaitUntilState(11)
	end
	
	HandleOptionSelection(selectionPos, id, 2, targetState, useButtons)
	
	WaitUntilMenuScroll(80)
	WaitUntilMenuScroll(0)

	if GetInputMode() == touchModeL and useButtons then
		SetInputMode(buttonMode, 80, 168)
	elseif GetInputMode() == buttonMode then
		WaitNFrames(2)
	end
	
	WaitInDirection(targetDirection)
	UpdateWaitConfirmDelay()

	if GetInputMode() == buttonMode then
		WaitUntilMenuScroll(80)
		coroutine.yield()

		if storedInputExists then
			SetInputMode(touchModeR)
		else
			SelectConfirm()
		end
	else
		SelectConfirm()
	end
end

function SelectMove(moveX, moveY, height)
	local id = WaitForBaseMenuID()
	local targetState = id
	
	if GetTransitionState() ~= id then
		WaitUntilState(11)
	end

	HandleOptionSelection(1, id, 2, targetState)

	local touchX, touchY = MapToScreenCoords(moveX, moveY, height)
	
	WaitNFrames(1)
	WaitUntilState(11)

	if GetInputMode() == buttonMode then
		WaitUntilOtherState(44)
	else
		HoldUntilOtherState(touchX, touchY, 2, 44)
	end

	if GetInputMode() == buttonMode then
		coroutine.yield()
		SelectTileWithButtons(moveX, moveY)
	else
		WaitNFrames(1)
	end
end

function SelectAction()
	local id = WaitForBaseMenuID()
	local targetState = id
	local selectionPos = GetActionPosFromMenuID(id)

	WaitUntilState(11)
	HandleOptionSelection(selectionPos, id, 2, targetState)

	WaitNFrames(1)
end

function SelectAttack(targetUnit, height, xShift, yShift)
	xShift, yShift = xShift or 0, yShift or 0
	SelectAction()

	local id = WaitForActionMenuID()
	local targetState = id
	local targetX, targetY = GetUnitCoords(targetUnit)
	local touchX, touchY = MapToScreenCoords(targetX, targetY, height)
	touchX, touchY = touchX + xShift, touchY - yShift - 4
	
	WaitUntilState(11)
	HandleOptionSelection(1, id, 2, targetState, targetWithButtons)

	WaitNFrames(1)
	WaitUntilState(11)

	if targetWithButtons then
		WaitUntilMenuScroll(0)
	elseif GetInputMode() == buttonMode then
		WaitUntilState(64)
	else
		HoldUntilState(touchX, touchY, 2, 64)
	end

	if GetInputMode() == touchModeL and targetWithButtons then
		if storageEnabled then
			SetInputMode(buttonMode, 80, 168)
		else
			SetInputMode(buttonMode)
			WaitNFrames(2)
		end

		WaitNFrames(2)
	end

	if GetInputMode() == buttonMode then
		SelectTileWithButtons(targetX, targetY)
	end

	UpdateAttackConfirmDelay()

	if GetInputMode() == buttonMode then
		WaitUntilMenuScroll(80)

		for i = 1, toWait do
			coroutine.yield()
		end

		toWait = normalDelay

		if storedInputExists then
			SetInputMode(touchModeR)

			if resetAfterButtonTarget then
				targetWithButtons = false
			end
		else
			if not storageEnabled then
				SetInputMode(touchModeR)
			end

			SelectConfirm()
		end
	else
		WaitNFrames(2)
		SelectConfirm()
	end
end

function SelectOtherAction(targetUnit, otherType, listPos, height, xShift, yShift)
	xShift, yShift = xShift or 0, yShift or 0
	local targetX, targetY = GetUnitCoords(targetUnit)
	local touchX, touchY = MapToScreenCoords(targetX, targetY, height)
	touchX, touchY = touchX + xShift, touchY - yShift - 4
	SelectOtherActionAtPosition(touchX, touchY, otherType, listPos, height, targetX, targetY)
end

function SelectOtherActionAtPosition(touchX, touchY, otherType, listPos, height, targetX, targetY)
	SelectAction()

	local id = WaitForActionMenuID()
	local otherPos = GetOtherPosFromActionMenuID(otherType, id)
	local otherID = GetOtherMenuID(otherType)
	local targetState = id
	local othertargetState = otherID + 1
	-- Verify if this can save time for other positions
	local useButtons = storageEnabled and listPos == 1
	
	WaitUntilState(11)
	HoldOptionUntilState(otherPos, 2, targetState, useButtons or targetWithButtons)

	if GetInputMode() == buttonMode then
		SelectOptionWithButtons(otherPos, id)
	end

	WaitNFrames(1)
	WaitForMenuType(otherID)
	WaitUntilState(11)

	if GetInputMode() == touchModeL and useButtons then
		WaitUntilState(othertargetState - 1)

		if targetWithButtons then
			SetInputMode(buttonMode, 80, 168)
		else
			SetInputMode(buttonMode, touchX, touchY)
		end

		coroutine.yield()
	else
		HoldOtherOptionUntilState(listPos, 2, othertargetState)
		WaitNFrames(2)
		HoldNthOtherOption(listPos, 2)
	end

	if GetInputMode() == buttonMode then
		SelectOptionWithButtons(listPos, otherID)
	end

	WaitNFrames(1)
	othertargetState = othertargetState + 1

	if GetInputMode() == buttonMode or useButtons or targetWithButtons then
		WaitNFrames(2)
	else
		HoldForNFrames(touchX, touchY, 2)
	end
	
	WaitUntilState(11)
	-- Flash drives sometimes use 82 instead of 81 (81 instead of 80 the frame before)
	while GetTransitionState() ~= othertargetState and GetTransitionState() ~= 81 do
		if GetInputMode() ~= buttonMode then
			stylus.set{x = touchX, y = touchY, touch = true}
		end

		coroutine.yield()
	end
	
	if GetInputMode() == touchModeL and targetWithButtons then
		if storageEnabled then
			SetInputMode(buttonMode, 80, 168)
		else
			SetInputMode(buttonMode)
		end
	end

	if GetInputMode() == buttonMode then
		if storedInputExists and not targetWithButtons then
			SetInputMode(touchModeR)
		else
			WaitNFrames(2)
			SelectTileWithButtons(targetX, targetY)
		end
	else
		stylus.set{x = touchX, y = touchY, touch = true}
		coroutine.yield()
	end

	UpdateAttackConfirmDelay()
	coroutine.yield()

	if GetInputMode() == buttonMode then
		WaitUntilMenuScroll(80)
		
		if storedInputExists then
			for i = 1, toWait do
				coroutine.yield()
			end

			toWait = normalDelay
		end

		if storedInputExists then
			SetInputMode(touchModeR)

			if resetAfterButtonTarget then
				targetWithButtons = false
			end
		else
			if not storageEnabled then
				SetInputMode(touchModeR)
			end

			SelectConfirm()
		end
	else
		WaitNFrames(1)
		SelectConfirm()
	end
end

-- Combined actions
function JustWait(unit, targetDirection)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectWait(targetDirection)
end

function MoveAndWait(unit, moveX, moveY, height, targetDirection)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectMove(moveX, moveY, height)
	WaitForVisibleMenu()
	SelectWait(targetDirection)
end

function MoveAndAttack(unit, targetUnit, moveX, moveY, moveHeight, targetHeight, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectMove(moveX, moveY, moveHeight)
	WaitForVisibleMenu()
	SelectAttack(targetUnit, targetHeight, xShift, yShift)
end

function AttackAndWait(unit, targetUnit, height, targetDirection, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectAttack(targetUnit, height, xShift, yShift)
	SelectWait(targetDirection)
end

function AttackAndMove(unit, targetUnit, moveX, moveY, targetHeight, moveHeight, targetDirection, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectAttack(targetUnit, targetHeight, xShift, yShift)
	SelectMove(moveX, moveY, moveHeight)
	SelectWait(targetDirection)
end

function AttackToWin(unit, targetUnit, height, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectAttack(targetUnit, height, xShift, yShift)
end

function MoveAndDoOther(unit, targetUnit, otherType, listPos, moveX, moveY, moveHeight, targetHeight, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectMove(moveX, moveY, moveHeight)
	WaitForVisibleMenu()
	SelectOtherAction(targetUnit, otherType, listPos, targetHeight, xShift, yShift)
end

function MoveAndDoOtherAtPosition(unit, targetX, targetY, otherType, listPos, moveX, moveY, moveHeight, targetHeight)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectMove(moveX, moveY, moveHeight)
	WaitForVisibleMenu()
	SelectOtherActionAtPosition(targetX, targetY, otherType, listPos, targetHeight)
end

function DoOtherAndWait(unit, targetUnit, otherType, listPos, height, targetDirection, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherAction(targetUnit, otherType, listPos, height, xShift, yShift)
	SelectWait(targetDirection)
end

function DoOtherAtPositionAndWait(unit, targetX, targetY, otherType, listPos, height, targetDirection)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherActionAtPosition(targetX, targetY, otherType, listPos, height)
	SelectWait(targetDirection)
end

function DoOtherAndMove(unit, targetUnit, otherType, listPos, moveX, moveY, targetHeight, moveHeight, targetDirection, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherAction(targetUnit, otherType, listPos, targetHeight, xShift, yShift)
	SelectMove(moveX, moveY, moveHeight)
	SelectWait(targetDirection)
end

function DoOtherAtPositionAndMove(unit, targetX, targetY, otherType, listPos, moveX, moveY, targetHeight, moveHeight, targetDirection)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherActionAtPosition(targetX, targetY, otherType, listPos, targetHeight)
	SelectMove(moveX, moveY, moveHeight)
	SelectWait(targetDirection)
end

function DoOtherToWin(unit, targetUnit, otherType, listPos, height, xShift, yShift)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherActionAtPosition(targetUnit, otherType, listPos, height, xShift, yShift)
end

function DoOtherAtPositionToWin(unit, targetX, targetY, otherType, listPos, height)
	WaitForUnitTurnAndVisibleMenu(unit)
	SelectOtherAction(targetX, targetY, otherType, listPos, height)
end

-- GUI
function drawGUI()
	local unit = GetCurrentUnit()
	local x, y = GetUnitCoords(unit)
	gui.text(1, 105, "Total lag: " .. emu.lagcount())
	gui.text(1, 115, "Am I lagging? " .. tostring(emu.lagged()))
	gui.text(1, 135, "Current unit: " .. unit)
	gui.text(1, 145, "Current co-ords: " .. x .. ", " .. y)
	gui.text(1, 155, "Current facing: " .. GetUnitFacing(unit))
	gui.text(1, 165, "Default facing: " .. GetDefaultFacing())
end

gui.register(drawGUI)

-- Testing
released = true

function TestLoop()
	while true do
		menuType = GetMenuType()
		state = GetTransitionState()
		currentUnit = GetCurrentUnit()

		if currentUnit > 7 then
			xMid, yMid = MapToScreenCoords(16, 16, 2)
			stylus.set{x = xMid, y = yMid, touch = true}
		elseif menuType == 12 and (state ~= 12 or released) then
			SelectNthOption(3)
			released = false
		elseif menuType == 44 and (state ~= 45 or released)  then
			SelectNthOption(1)
			released = false
		elseif menuType == 59 then
			if state == 29 then
				HoldForNFrames(80, 168, 2)
			else
				WaitInDirection(math.random(0,3))
			end
			released = false
		else
			released = true
		end
	
		coroutine.yield()
	end
end

-- Strats
function OldMap1StratNoStoredMagic()
	local Alph, Theo, Leon, Heath = 0, 1, 2, 3
	local Argata, Zonar, Flurg, Laz = 8, 9, 10, 11

	SetNextWaitDelay(slowDelay)
	MoveAndWait(Heath, 11, 11, 1, east)
	MoveAndWait(Theo, 9, 15, 1, north)
	MoveAndWait(Alph, 12, 14, 1, west)
	MoveAndWait(Leon, 10, 14, 1, north)
	MoveAndAttack(Heath, Argata, 14, 11, 2, 2)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Theo, Laz, 1, east)
	AttackAndWait(Alph, Laz, 1, east)
	MoveAndAttack(Leon, Laz, 13, 15, 1, 1)
	SetNextAttackDelay(fastDelay)
	MoveAndAttack(Theo, Laz, 10, 15, 1, 1)
	SetNextAttackDelay(fastDelay)
	MoveAndAttack(Alph, Flurg, 13, 17, 2, 2)
	AttackAndWait(Heath, Argata, 2, GetRelativeFacing(Heath, Argata))
	AttackAndWait(Leon, Flurg, 2, south)
	SetStorage(false)
	MoveAndDoOther(Heath, Zonar, magic, 1, 14, 14, 2, 2)
	SetStorage(true)
	MoveAndAttack(Leon, Flurg, 16, 16, 2, 2)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Theo, Zonar, 2, east)
	SetNextAttackDelay(fastDelay)
	AttackToWin(Alph, Zonar, 2)
end

function OldMap1Strat()
	local Alph, Theo, Leon, Heath = 0, 1, 2, 3
	local Argata, Zonar, Flurg, Laz = 8, 9, 10, 11
	
	-- SetSaveState(true, 8)
	-- SetMinimiseHold(true)

	SetNextWaitDelay(slowDelay)
	MoveAndWait(Heath, 11, 11, 1, east)
	MoveAndWait(Theo, 9, 15, 1, north)
	MoveAndWait(Alph, 12, 14, 1, west)
	MoveAndWait(Leon, 10, 14, 1, north)
	MoveAndAttack(Heath, Argata, 14, 11, 2, 2)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Theo, Laz, 1, east)
	AttackAndWait(Alph, Laz, 1, east)
	MoveAndAttack(Leon, Laz, 13, 15, 1, 1)
	SetNextAttackDelay(fastDelay)
	MoveAndAttack(Theo, Laz, 10, 15, 1, 1)
	SetNextAttackDelay(fastDelay)
	MoveAndAttack(Alph, Flurg, 13, 17, 2, 2)
	AttackAndWait(Heath, Argata, 2, GetRelativeFacing(Heath, Argata))
	AttackAndWait(Leon, Flurg, 2, south)
	MoveAndDoOther(Heath, Zonar, magic, 1, 14, 14, 2, 2)
	MoveAndAttack(Leon, Flurg, 16, 16, 2, 2)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Theo, Zonar, 2, east)
	AttackToWin(Alph, Zonar, 2)
end

function OldMap1StratWithButtons()
	local Alph, Theo, Leon, Heath = 0, 1, 2, 3
	local Argata, Zonar, Flurg, Laz = 8, 9, 10, 11

	SetButtonTargeting(true)
	SetNextWaitDelay(slowDelay)
	MoveAndWait(Heath, 11, 11, 1, east)
	MoveAndWait(Theo, 9, 15, 1, north)
	MoveAndWait(Alph, 12, 14, 1, west)
	MoveAndWait(Leon, 10, 14, 1, north)
	SetNextAttackDelay(slowDelay)
	MoveAndAttack(Heath, Argata, 14, 11, 2, 2)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Theo, Laz, 1, east)
	SetNextAttackDelay(fastDelay)
	AttackAndWait(Alph, Laz, 1, east)
	MoveAndAttack(Leon, Laz, 13, 15, 1, 1)
	SetNextAttackDelay(slowDelay)
	MoveAndAttack(Theo, Laz, 10, 15, 1, 1)
	MoveAndAttack(Alph, Flurg, 13, 17, 2, 2)
	SetNextAttackDelay(4)
	AttackAndWait(Heath, Argata, 2, GetRelativeFacing(Heath, Argata))
	AttackAndWait(Leon, Flurg, 2, south)
	MoveAndDoOther(Heath, Zonar, magic, 1, 14, 14, 2, 2)
	MoveAndAttack(Leon, Flurg, 16, 16, 2, 2)
	AttackAndWait(Theo, Zonar, 2, east)
	AttackToWin(Alph, Zonar, 2)
end

function OldMap2Strat()
	local Alph, Lucia = 0, 1
	local Eman, Warloak, Bruneed = 8, 9, 11

	MoveAndWait(Lucia, 13, 16, 2, east)
	MoveAndWait(Alph, 12, 15, 2, west)
	AttackAndWait(Lucia, Eman, 2, west)
	AttackAndWait(Alph, Eman, 2, south)
	DoOtherAndWait(Lucia, Warloak, magic, 1, 1, west)
	AttackAndWait(Alph, Warloak, 1, west)
	AttackAndWait(Lucia, Bruneed, 3, east)
	MoveAndAttack(Alph, Bruneed, 15, 15, 3, 3)
end

function Map1Strat()
	local Alph, Theo, Leon, Heath = 0, 1, 2, 3
	local Argata, Zonar, Flurg, Laz = 8, 9, 10, 11
--[
	MoveAndWait(Heath, 11, 11, 1, north)
    SetNextAttackDelay(fastDelay)
	MoveAndAttack(Theo, Leon, 12, 11, 2, 1)
	MoveAndWait(Alph, 13, 12, 2, east)
	MoveAndWait(Leon, 13, 10, 2, east)
	MoveAndWait(Heath, 12, 12, 2, east)
	MoveAndAttack(Alph, Argata, 16, 12, 2, 2)
	MoveAndAttack(Leon, Argata, 16, 10, 2, 2)
	AttackAndWait(Theo, Flurg, 2, north, 0, 16)
	MoveAndAttack(Heath, Flurg, 14, 13, 2, 2)
    SetNextAttackDelay(fastDelay)
	MoveAndAttack(Alph, Zonar, 19, 13, 2, 2)
	MoveAndAttack(Leon, Zonar, 18, 12, 2, 2)
	JustWait(Theo, north)
	AttackToWin(Heath, Laz, 2)
end

function Map2Strat()
    local Alph, Lucia = 0, 1
    local Eman, Warloak, Bruneed = 8, 9, 11
--[[
    SetNextWaitDelay(slowDelay)
    JustWait(Lucia, south)
    JustWait(Alph, north)
    MoveAndAttack(Lucia, Warloak, 12, 12, 2, 1)
    MoveAndAttack(Alph, Warloak, 11, 12, 2, 1)
    AttackAndWait(Lucia, Eman, 3, east)
    MoveAndAttack(Alph, Eman, 13, 13, 3, 3)
    MoveAndAttack(Lucia, Bruneed, 12, 14, 3, 3)
    AttackToWin(Alph, Bruneed, 3)
--]]
--[[
	-- New strat, saves 8f by latest optimizations; Bruneed 2W 1S is faster but is it possible to not get 3S after it which makes it slower?
    SetNextWaitDelay(slowDelay)
    JustWait(Lucia, south)
    JustWait(Alph, north)
    MoveAndAttack(Lucia, Warloak, 13, 11, 3, 2)
    MoveAndAttack(Alph, Warloak, 12, 11, 2, 2)
    AttackAndWait(Lucia, Eman, 3, south, -8, 8)
    MoveAndAttack(Alph, Eman, 12, 12, 2, 3, -14, 18)
    MoveAndAttack(Lucia, Bruneed, 13, 12, 3, 3, -8, 18)
    UseButtonsForNextTarget()
    SetNextAttackDelay(fastDelay)
    MoveAndAttack(Alph, Bruneed, 12, 13, 3, 3)
--]]
    SetNextWaitDelay(slowDelay)
    JustWait(Lucia, south)
    JustWait(Alph, north)
    MoveAndAttack(Lucia, Warloak, 12, 12, 2, 1)
    MoveAndAttack(Alph, Warloak, 11, 12, 2, 1)
    AttackAndWait(Lucia, Eman, 3, south)
    MoveAndAttack(Alph, Eman, 13, 13, 3, 3)
    MoveAndAttack(Lucia, Bruneed, 12, 13, 3, 3, -4, 0)
    MoveAndAttack(Alph, Bruneed, 13, 14, 3, 3, -4, 0)
end

function Map5Strat()
	local Alph, Heath, Leon, Theo = 0, 1, 2, 3
	local Vanessa, Youknix, Pucelle, Beestah, Gul, Gal = 8, 9, 10, 11, 12, 13

	MoveAndWait(Heath, 13, 12, 2, north)
	MoveAndWait(Alph, 13, 13, 3, north)
	MoveAndWait(Theo, 12, 11, 2, north)
	MoveAndWait(Leon, 12, 12, 2, north)
	MoveAndAttack(Heath, Beestah, 13, 16, 4, 4)
	MoveAndAttack(Alph, Pucelle, 15, 13, 10, 10)
	MoveAndAttack(Theo, Pucelle, 13, 14, 4, 10, 0, 20)
	MoveAndWait(Leon, 13, 15, 4, north)
	MoveAndAttack(Leon, Gal, 14, 17, 4, 4)
	AttackAndWait(Heath, Gal, 4, north)
	MoveAndWait(Theo, 13, 15, 4, north)
	JustWait(Alph, east)
	JustWait(Alph, east)
	AttackAndWait(Theo, Vanessa, 4, north)
	MoveAndAttack(Heath, Vanessa, 14, 17, 4, 4)
	MoveAndAttack(Alph, Youknix, 16, 11, 9, 7)
	MoveAndAttack(Theo, Youknix, 13, 11, 2, 7)
	MoveAndAttack(Heath, Vanessa, 15, 20, 5, 5)
end

function DoStrat()
	--OldMap1Strat()
	--Map1Strat()
	--OldMap2Strat()
	Map2Strat()
	--Map5Strat()
	WaitForAllEnemiesKilled()
	emu.pause()
end

function DoPrinting()
	confirmCheck = LostFrameOnConfirm()
	currentFrame = emu.framecount()

	if confirmCheck == 1 then
		print("Possible confirm frame loss at frame " .. currentFrame)
	end
end

function ContinueStrat()
	DoPrinting()
	coroutine.resume(strat)
end

strat = coroutine.create(DoStrat)
emu.registerbefore(ContinueStrat)