-
-
Notifications
You must be signed in to change notification settings - Fork 0
/
mGBASocketServer.lua
321 lines (279 loc) · 11.2 KB
/
mGBASocketServer.lua
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
-- ***********************
-- mGBA-http
-- Version: 0.4.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
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()