mirror of
https://github.com/cloudmaker97/TikTok-mGBA-Emulator.git
synced 2025-12-05 23:48:38 +00:00
322 lines
No EOL
11 KiB
Lua
322 lines
No EOL
11 KiB
Lua
-- ***********************
|
|
-- mGBA-http
|
|
-- Version: 0.2.0
|
|
-- Lua interface for mGBA-http
|
|
-- https://github.com/nikouu/mGBA-http
|
|
-- ***********************
|
|
|
|
local enableLogging = true
|
|
|
|
-- ***********************
|
|
-- Sockets
|
|
-- ***********************
|
|
|
|
local server = nil
|
|
local socketList = {}
|
|
local nextID = 1
|
|
local port = 8888
|
|
|
|
function beginSocket()
|
|
while not server do
|
|
server, error = socket.bind(nil, port)
|
|
if error then
|
|
if error == socket.ERRORS.ADDRESS_IN_USE then
|
|
port = port + 1
|
|
else
|
|
console:error(formatMessage("Bind", error, true))
|
|
break
|
|
end
|
|
else
|
|
local ok
|
|
ok, error = server:listen()
|
|
if error then
|
|
server:close()
|
|
console:error(formatMessage("Listen", error, true))
|
|
else
|
|
console:log("Socket Server: Listening on port " .. port)
|
|
server:add("received", socketAccept)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function socketAccept()
|
|
local sock, error = server:accept()
|
|
if error then
|
|
console:error(formatMessage("Accept", error, true))
|
|
return
|
|
end
|
|
local id = nextID
|
|
nextID = id + 1
|
|
socketList[id] = sock
|
|
sock:add("received", function() socketReceived(id) end)
|
|
sock:add("error", function() socketError(id) end)
|
|
formattedLog(formatMessage(id, "Connected"))
|
|
end
|
|
|
|
function socketReceived(id)
|
|
local sock = socketList[id]
|
|
if not sock then return end
|
|
while true do
|
|
local message, error = sock:receive(1024)
|
|
if message then
|
|
-- it seems that the value must be non-empty in order to actually send back?
|
|
-- thus the ACK message default
|
|
local returnValue = messageRouter(message:match("^(.-)%s*$"))
|
|
sock:send(returnValue)
|
|
elseif error then
|
|
-- seems to go into this SOCKETERRORAGAIN state for each call, but it seems fine.
|
|
if error ~= socket.ERRORS.AGAIN then
|
|
formattedLog("socketReceived 4")
|
|
console:error(formatMessage(id, error, true))
|
|
socketStop(id)
|
|
end
|
|
return
|
|
end
|
|
end
|
|
end
|
|
|
|
function socketStop(id)
|
|
local sock = socketList[id]
|
|
socketList[id] = nil
|
|
sock:close()
|
|
end
|
|
|
|
function socketError(id, error)
|
|
console:error(formatMessage(id, error, true))
|
|
socketStop(id)
|
|
end
|
|
|
|
function formatMessage(id, msg, isError)
|
|
local prefix = "Socket " .. id
|
|
if isError then
|
|
prefix = prefix .. " Error: "
|
|
else
|
|
prefix = prefix .. " Received: "
|
|
end
|
|
return prefix .. msg
|
|
end
|
|
|
|
-- ***********************
|
|
-- Message Router
|
|
-- ***********************
|
|
|
|
local keyValues = {
|
|
["A"] = 0,
|
|
["B"] = 1,
|
|
["Select"] = 2,
|
|
["Start"] = 3,
|
|
["Right"] = 4,
|
|
["Left"] = 5,
|
|
["Up"] = 6,
|
|
["Down"] = 7,
|
|
["R"] = 8,
|
|
["L"] = 9
|
|
}
|
|
|
|
function messageRouter(rawMessage)
|
|
local parsedInput = splitStringToTable(rawMessage, ",")
|
|
|
|
local messageType = parsedInput[1]
|
|
local messageValue1 = parsedInput[2]
|
|
local messageValue2 = parsedInput[3]
|
|
local messageValue3 = parsedInput[4]
|
|
|
|
local defaultReturnValue <const> = "<|ACK|>";
|
|
|
|
local returnValue = defaultReturnValue;
|
|
|
|
formattedLog("messageRouter: \n\tRaw message:" .. rawMessage .. "\n\tmessageType: " .. (messageType or "") .. "\n\tmessageValue1: " .. (messageValue1 or "") .. "\n\tmessageValue2: " .. (messageValue2 or "") .. "\n\tmessageValue3: " .. (messageValue3 or ""))
|
|
|
|
if messageType == "mgba-http.button.tap" then manageButton(messageValue1)
|
|
elseif messageType == "mgba-http.button.tapmany" then manageButtons(messageValue1)
|
|
elseif messageType == "mgba-http.button.hold" then manageButton(messageValue1, messageValue2)
|
|
elseif messageType == "mgba-http.button.holdmany" then manageButtons(messageValue1, messageValue2)
|
|
elseif messageType == "core.addKey" then addKey(messageValue1)
|
|
elseif messageType == "core.addKeys" then emu:addKeys(tonumber(messageValue1))
|
|
elseif messageType == "core.autoloadSave" then returnValue = emu:autoloadSave()
|
|
elseif messageType == "core.checksum" then returnValue = computeChecksum()
|
|
elseif messageType == "core.clearKey" then clearKey(messageValue1)
|
|
elseif messageType == "core.clearKeys" then emu:clearKeys(tonumber(messageValue1))
|
|
elseif messageType == "core.currentFrame" then returnValue = emu:currentFrame()
|
|
elseif messageType == "core.frameCycles" then returnValue = emu:frameCycles()
|
|
elseif messageType == "core.frequency" then returnValue = emu:frequency()
|
|
elseif messageType == "core.getGameCode" then returnValue = emu:getGameCode()
|
|
elseif messageType == "core.getGameTitle" then returnValue = emu:getGameTitle()
|
|
elseif messageType == "core.getKey" then returnValue = emu:getKey(keyValues[messageValue1])
|
|
elseif messageType == "core.getKeys" then returnValue = emu:getKeys()
|
|
elseif messageType == "core.loadFile" then returnValue = emu:loadFile(messageValue1)
|
|
elseif messageType == "core.loadSaveFile" then returnValue = emu:loadSaveFile(messageValue1, toBoolean(messageValue2))
|
|
elseif messageType == "core.loadStateBuffer" then returnValue = emu:loadStateBuffer(messageValue1, messageValue2)
|
|
elseif messageType == "core.loadStateFile" then returnValue = emu:loadStateFile(messageValue1, tonumber(messageValue2))
|
|
elseif messageType == "core.loadStateSlot" then returnValue = emu:loadStateSlot(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.platform" then returnValue = emu:platform()
|
|
elseif messageType == "core.read16" then returnValue = emu:read16(tonumber(messageValue1))
|
|
elseif messageType == "core.read32" then returnValue = emu:read32(tonumber(messageValue1))
|
|
elseif messageType == "core.read8" then returnValue = emu:read8(tonumber(messageValue1))
|
|
elseif messageType == "core.readRange" then returnValue = emu:readRange(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.readRegister" then returnValue = emu:readRegister(messageValue1)
|
|
elseif messageType == "core.romSize" then returnValue = emu:romSize()
|
|
elseif messageType == "core.runFrame" then emu:runFrame()
|
|
elseif messageType == "core.saveStateBuffer" then emu:saveStateBuffer(tonumber(messageValue1))
|
|
elseif messageType == "core.saveStateFile" then returnValue = emu:saveStateFile(messageValue1, tonumber(messageValue2))
|
|
elseif messageType == "core.saveStateSlot" then returnValue = emu:saveStateSlot(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.screenshot" then emu:screenshot(messageValue1)
|
|
elseif messageType == "core.setKeys" then emu:setKeys(tonumber(messageValue1))
|
|
elseif messageType == "core.step" then emu:step()
|
|
elseif messageType == "core.write16" then returnValue = emu:write16(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.write32" then returnValue = emu:write32(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.write8" then returnValue = emu:write8(tonumber(messageValue1), tonumber(messageValue2))
|
|
elseif messageType == "core.writeRegister" then returnValue = emu:writeRegister(messageValue1, tonumber(messageValue2))
|
|
elseif messageType == "console.error" then console:error(messageValue1)
|
|
elseif messageType == "console.log" then console:log(messageValue1)
|
|
elseif messageType == "console.warn" then console:warn(messageValue1)
|
|
elseif messageType == "coreAdapter.reset" then emu:reset()
|
|
elseif messageType == "memoryDomain.base" then returnValue = emu.memory[messageValue1]:base()
|
|
elseif messageType == "memoryDomain.bound" then returnValue = emu.memory[messageValue1]:bound()
|
|
elseif messageType == "memoryDomain.name" then returnValue = emu.memory[messageValue1]:name()
|
|
elseif messageType == "memoryDomain.read16" then returnValue = emu.memory[messageValue1]:read16(tonumber(messageValue2))
|
|
elseif messageType == "memoryDomain.read32" then returnValue = emu.memory[messageValue1]:read32(tonumber(messageValue2))
|
|
elseif messageType == "memoryDomain.read8" then returnValue = emu.memory[messageValue1]:read8(tonumber(messageValue2))
|
|
elseif messageType == "memoryDomain.readRange" then returnValue = emu.memory[messageValue1]:readRange(tonumber(messageValue2), tonumber(messageValue3))
|
|
elseif messageType == "memoryDomain.size" then returnValue = emu.memory[messageValue1]:size()
|
|
elseif messageType == "memoryDomain.write16" then returnValue = emu.memory[messageValue1]:write16(tonumber(messageValue2), tonumber(messageValue3))
|
|
elseif messageType == "memoryDomain.write32" then returnValue = emu.memory[messageValue1]:write32(tonumber(messageValue2), tonumber(messageValue3))
|
|
elseif messageType == "memoryDomain.write8" then returnValue = emu.memory[messageValue1]:write8(tonumber(messageValue2), tonumber(messageValue3))
|
|
elseif (rawMessage == "<|ACK|>") then formattedLog("Connecting.")
|
|
elseif (rawMessage ~= nil or rawMessage ~= '') then formattedLog("Unable to route raw message: " .. rawMessage)
|
|
else formattedLog(messageType)
|
|
end
|
|
|
|
returnValue = tostring(returnValue or defaultReturnValue);
|
|
|
|
formattedLog("Returning: " .. returnValue)
|
|
return returnValue;
|
|
end
|
|
|
|
-- ***********************
|
|
-- Button (Convenience abstraction)
|
|
-- ***********************
|
|
|
|
function addKey(keyLetter)
|
|
local key = keyValues[keyLetter];
|
|
emu:addKey(key)
|
|
end
|
|
|
|
function clearKey(keyLetter)
|
|
local key = keyValues[keyLetter];
|
|
emu:clearKey(key)
|
|
end
|
|
|
|
local keyEventQueue = {}
|
|
|
|
function manageButton(keyLetter, duration)
|
|
duration = duration or 15
|
|
local key = keyValues[keyLetter]
|
|
local bitmask = toBitmask({key})
|
|
enqueueButtons(bitmask, duration)
|
|
end
|
|
|
|
function manageButtons(keyLetters, duration)
|
|
duration = duration or 15
|
|
local keyLettersArray = splitStringToTable(keyLetters, ";")
|
|
local keys = {}
|
|
for i, keyLetter in ipairs(keyLettersArray) do
|
|
formattedLog("lmao 5 " ..keyLetter )
|
|
keys[i] = keyValues[keyLetter]
|
|
end
|
|
local bitmask = toBitmask(keys);
|
|
enqueueButtons(bitmask, duration);
|
|
end
|
|
|
|
function enqueueButtons(keyMask, duration)
|
|
local startFrame = emu:currentFrame()
|
|
local endFrame = startFrame + duration + 1
|
|
|
|
table.insert(keyEventQueue,
|
|
{
|
|
keyMask = keyMask,
|
|
startFrame = startFrame,
|
|
endFrame = endFrame,
|
|
pressed = false
|
|
});
|
|
|
|
formattedLog(keyMask)
|
|
end
|
|
|
|
function updateKeys()
|
|
local indexesToRemove = {}
|
|
|
|
for index, keyEvent in ipairs(keyEventQueue) do
|
|
|
|
if emu:currentFrame() >= keyEvent.startFrame and emu:currentFrame() <= keyEvent.endFrame and not keyEvent.pressed then
|
|
emu:addKeys(keyEvent.keyMask)
|
|
keyEvent.pressed = true
|
|
elseif emu:currentFrame() > keyEvent.endFrame then
|
|
emu:clearKeys(keyEvent.keyMask)
|
|
table.insert(indexesToRemove, index)
|
|
end
|
|
end
|
|
|
|
for _, i in ipairs(indexesToRemove) do
|
|
table.remove(keyEventQueue, i)
|
|
end
|
|
end
|
|
|
|
callbacks:add("frame", updateKeys)
|
|
|
|
-- ***********************
|
|
-- Utility
|
|
-- ***********************
|
|
|
|
function splitStringToTable(inputstr, sep)
|
|
if sep == nil then
|
|
sep = "%s"
|
|
end
|
|
local t={}
|
|
for str in string.gmatch(inputstr, "([^"..sep.."]+)") do
|
|
table.insert(t, str)
|
|
end
|
|
return t
|
|
end
|
|
|
|
function numberStringToHex(string)
|
|
return string.format('%x', tonumber(string, 16))
|
|
end
|
|
|
|
function toBoolean(str)
|
|
local bool = false
|
|
if string.lower(str) == "true" then
|
|
bool = true
|
|
end
|
|
return bool
|
|
end
|
|
|
|
function formattedLog(string)
|
|
if enableLogging then
|
|
local timestamp = "[" .. os.date("%X", os.time()) .. "] "
|
|
console:log(timestamp .. string)
|
|
end
|
|
end
|
|
|
|
function computeChecksum()
|
|
local checksum = 0
|
|
for i, v in ipairs({emu:checksum(C.CHECKSUM.CRC32):byte(1, 4)}) do
|
|
checksum = checksum * 256 + v
|
|
end
|
|
return checksum
|
|
end
|
|
|
|
function toBitmask(keys)
|
|
local mask = 0
|
|
for _, key in ipairs(keys) do
|
|
mask = mask | (1 << tonumber(key))
|
|
end
|
|
return mask
|
|
end
|
|
|
|
-- ***********************
|
|
-- Start
|
|
-- ***********************
|
|
|
|
beginSocket() |