Skip to content

Commit

Permalink
Updated custom button API
Browse files Browse the repository at this point in the history
- Tap and hold APIs with single or multiple keys
- Fixed button queuing problem
- Updated button documentation for all pages
- Updated design doc
  • Loading branch information
nikouu committed Mar 22, 2024
1 parent 6378c6b commit 31ad7c8
Show file tree
Hide file tree
Showing 9 changed files with 184 additions and 61 deletions.
10 changes: 1 addition & 9 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,6 @@ For a more in-depth guide with pictures, see the [Full Guide](docs/FullGuide.md)
## Limitations
- No frame perfect calls. There is network latency between your application to mGBA-http and again latency between mGBA-http and mGBA. This will not be accurate for frame perfect manipulation and is meant for more general usage such as for "Twitch plays", AI playing bot, or other non frame specific application. For high accuracy manipulation see [Bizhawk](https://tasvideos.org/BizHawk/) which is used for TASBots.
- Not all scripting calls are implemented. See [ImplementedApis.md](docs/ImplementedApis.md) for the list of what is implemented.
- When very quickly sending requests, the requests may queue and be actioned on long after the input requests stop. Or in other words, no key inputs are eaten. For example, holding down on the d-pad with original hardware didn't trigger the down action long after d-pad release.
- You may want to implement a rate limiter in your code. For instance, only send a request every x milliseconds and ignore input between.
- The OSX and Linux binaries are experimental.

## Why?
Expand All @@ -62,35 +60,29 @@ Future projects such as the proof of concept: [CPU Plays Pokemon](https://github
If you know Lua, GameBoy/Advance, or mGBA specifics, I'd love for help.

### Development

If you're a .NET developer, the setup simple and familiar opening the [solution file](src/CmGBAHttpServer.sln). I use Visual Studio (At least VS 17.9) and the latest .NET. However, if you choose to develop C# without Visual Studio, that's fine too.

If you're not a .NET developer, check out the comprehensive [C# learning website](https://dotnet.microsoft.com/en-us/learn/csharp) from Microsoft. You can program in C# on whatever platform whether it's Windows, Mac, or Linux.

In terms of the .NET work, the project uses [ASP.NET Core minimal API](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-8.0).

#### Design Document

The rough design philosphy is outlined in the [design document](docs/Design.md). Please understand and follow this when considering a contribution.

### Build

The PowerShell release script [ReleaseBuild.ps1](ReleaseBuild.ps1) creates the final binaries. PowerShell is cross platform and can be downloaded via the [PowerShell download documentation](https://learn.microsoft.com/en-us/powershell/scripting/install/installing-powershell?view=powershell-7.3).

The script generates binaries closely aligned with the operating systems and architectures that [mGBA provides downloads](https://mgba.io/downloads.html) for.

#### Cross Platform

To reduce the barrier of entry, mGBA-http also has [self-contained](https://learn.microsoft.com/en-us/dotnet/core/deploying/#publish-self-contained) builds. These are the larger binaries with "self-contained" in the filename and bring the entirity of .NET needed to run the executable - meaning the user does not need to download the [.NET runtime](https://dotnet.microsoft.com/en-us/download/dotnet/8.0) to use mGBA-http.

## Contact
If there's a problem feel free to start an issue, otherwise see [my about page](https://www.nikouusitalo.com/about/#contact) on how to contact me.



## Acknowledgments
- The mGBA GitHub team for having socket examples
- [Zachary Handley](https://zachhandley.com/) for paving the way with [button press code](https://discord.com/channels/453962671499509772/979634439237816360/1124075643143995522)
- [Zachary Handley](https://zachhandley.com/) for paving the way with the initial [button press code](https://discord.com/channels/453962671499509772/979634439237816360/1124075643143995522)
- [heroldev/AGB-buttontest](https://github.com/heroldev/AGB-buttontest) for a simple button testing ROM

[References.md](docs/References.md) has useful links during development.
Expand Down
73 changes: 72 additions & 1 deletion docs/ApiDocumentation.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ The following is generated from the [swagger.json](swagger.json) file via https:
GitHub Repository
https://github.com/nikouu/mGBA-http/

### /mgba-http/button
### /mgba-http/button/tap

#### POST
##### Summary:
Expand All @@ -29,6 +29,77 @@ A custom convenience API that implements a key press and release. This is as opp
| ---- | ----------- |
| 200 | OK |

### /mgba-http/button/tapmany

#### POST
##### Summary:

Sends multiple button presses simultaneously.

##### Description:

A custom convenience API that implements multiple simultaneously keys being pressed and released. This is as opposed to the key based core API that sends only either a press or release message.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ---- |
| keys | query | Key array containing any of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L. | Yes | [ [KeysEnum](#KeysEnum) ] |

##### Responses

| Code | Description |
| ---- | ----------- |
| 200 | OK |

### /mgba-http/button/hold

#### POST
##### Summary:

Sends a held down button with a given duration in frames.

##### Description:

A custom convenience API that implements a held down button for a given duration in frames. This is as opposed to the key based core API that sends only either a press or release message.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ---- |
| key | query | Key value of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L. | Yes | string |
| duration | query | | Yes | integer |

##### Responses

| Code | Description |
| ---- | ----------- |
| 200 | OK |

### /mgba-http/button/holdmany

#### POST
##### Summary:

Sends multiple button presses simultaneously.

##### Description:

A custom convenience API that implements multiple simultaneously keys being pressed and released for a given duration in frames. This is as opposed to the key based core API that sends only either a press or release message.

##### Parameters

| Name | Located in | Description | Required | Schema |
| ---- | ---------- | ----------- | -------- | ---- |
| keys | query | Key array containing any of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L. | Yes | [ [KeysEnum](#KeysEnum) ] |
| duration | query | | Yes | integer |

##### Responses

| Code | Description |
| ---- | ----------- |
| 200 | OK |

### /console/error

#### POST
Expand Down
6 changes: 3 additions & 3 deletions docs/Design.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,8 @@ Similarly to *Cross Platform* above if a developer wants to clone/fork the code

It should be easy to develop against mGBA-http. Unless there is a compelling reason, just GET and POST calls should be used. Swagger is included in this project both as the `swagger.json` file and the interactive SwaggerUI to help and ease protyping.

Keeping most logic complexity in the Lua file further enables others who want to create their own wrappers as the key logic is there for them already to hook into. Where this wouldn't be the case if it were mostly in the C# code of mGBA-http.

## Languages

While mGBA-http is talked about as a singular piece, it's made up of both the C# server and the Lua script that loads into mGBA.
Expand All @@ -77,6 +79,4 @@ There are accepted limitations to mGBA-http.

- It takes time for an HTTP request from origin to mGBA-http then time again from mGBA-http to mGBA.
- This makes mGBA-http unsuitable for frame sensitive inputs.
- Not all mGBA API calls are simple and may return complex objects. Unless there is a complelling reason, these will not be implemented.
- Many quickly sent requests may queue.
- For inputs this may mean the key inputs happen long after the user has stopped them.
- Not all mGBA API calls are simple and may return complex objects. Unless there is a complelling reason, these will not be implemented.
2 changes: 2 additions & 0 deletions docs/Examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ Using the C# console app mGBAHttpServer.TestClient included in this repository,

https://github.com/nikouu/mGBA-http/assets/983351/691d5379-ed7d-4f58-9f93-24223e35f275

*API has been updated since this video, but the functionality is the same.*

### Code
See [mGBAHttpServer.TestClient](../src/mGBAHttpServer.TestClient).

Expand Down
9 changes: 6 additions & 3 deletions docs/ImplementedApis.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,9 @@ _Unstable_ APIs may not work as expected and may be fixed in a future update.

## Button - Custom API

| mGBA call | lua endpoint key | mGBA-http endpoint |
| :-------: | ---------------- | ------------------ |
| - | mgba-http.button | /mgba-http/button |
| mGBA call | lua endpoint key | mGBA-http endpoint |
| :-------: | ------------------------- | -------------------------- |
| - | mgba-http.button.tap | /mgba-http/button/tap |
| - | mgba-http.button.tapmany | /mgba-http/button/tapmany |
| - | mgba-http.button.hold | /mgba-http/button/hold |
| - | mgba-http.button.holdmany | /mgba-http/button/holdmany |
2 changes: 2 additions & 0 deletions docs/ReleaseChecklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@ Ensure the following are done, if needed, before each release.

- Version incremented
- Readme updated
- API documentation page updated (swagger.json to docs)
- Implemented API doc updated
- Examples doc updated
- Full Guide doc updated
- C# OpenAPI codegen doc calls updated
- Binaries published with latest .NET version
- Create the list of changes for the release notes
103 changes: 61 additions & 42 deletions mGBASocketServer.lua
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,10 @@ function messageRouter(rawMessage)

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" then pressKey(messageValue1)
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()
Expand Down Expand Up @@ -205,51 +208,59 @@ function clearKey(keyLetter)
emu:clearKey(key)
end

-- code via ZachHandley
-- https://discord.com/channels/453962671499509772/979634439237816360/1124075643143995522
local keyQueue = {}
local head = 1
local tail = 1
local totalDuration = 0
local keyEventQueue = {}

-- Function to update key presses
function updateKeys()
-- Check if the queue is empty
if head ~= tail then
-- Get the key press at the head of the queue
local keyPress = keyQueue[head]
--formattedLog("currentFrame: " .. emu:currentFrame() .. "startFrame: " .. keyPress.startFrame .. " endFrame: " .. keyPress.endFrame .. " key: " .. keyPress.key)
-- Check if the current frame is within the key press duration
if emu:currentFrame() >= keyPress.startFrame and emu:currentFrame() <= keyPress.endFrame and not keyPress.keyPressed then
-- If the current frame is within the key press duration, press the key
emu:addKey(keyPress.key)
keyPress.keyPressed = true
formattedLog("Pressed: " .. keyPress.key)
elseif emu:currentFrame() > keyPress.endFrame then
-- If the key press duration has ended, release the key and remove it from the queue
emu:clearKey(keyPress.key)
head = head + 1
-- If the queue is now empty, reset the total duration
if head == tail then
totalDuration = 0
end
end
end
function manageButton(keyLetter, duration)
duration = duration or 15
local key = keyValues[keyLetter]
local bitmask = toBitmask({key})
enqueueButtons(bitmask, duration)
end

-- Function to add a key press
function pressKey(keyLetter, duration)
function manageButtons(keyLetters, duration)
duration = duration or 15
local key = keyValues[keyLetter];
-- Calculate the start and end frames for this key press
local startFrame = emu:currentFrame() + totalDuration
local endFrame = startFrame + duration + 1
formattedLog("pressKey: " .. startFrame .. " - " .. endFrame .. " CF: " .. emu:currentFrame())
-- Add the key, start frame, and end frame to the queue
keyQueue[tail] = {key = key, startFrame = startFrame, endFrame = endFrame, keyPressed = false}
tail = tail + 1
-- Update the total duration
totalDuration = totalDuration + duration
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)
Expand Down Expand Up @@ -296,6 +307,14 @@ function computeChecksum()
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
-- ***********************
Expand Down
2 changes: 1 addition & 1 deletion src/mGBAHttpServer.TestClient/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@

async Task SendKey(string key)
{
var request = new HttpRequestMessage(HttpMethod.Post, $"http://localhost:5000/button?key={key}");
var request = new HttpRequestMessage(HttpMethod.Post, $"http://localhost:5000/mgba-http/button/tap?key={key}");
var response = await client.SendAsync(request);
response.EnsureSuccessStatusCode();
}
38 changes: 36 additions & 2 deletions src/mGBAHttpServer/Endpoints/ButtonApi.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using mGBAHttpServer.Models;
using mGBAHttpServer.Services;
using Microsoft.AspNetCore.Mvc;

namespace mGBAHttpServer.Endpoints
{
Expand All @@ -10,9 +11,9 @@ public static RouteGroupBuilder MapButtonEndpoints(this IEndpointRouteBuilder ro
var group = routes.MapGroup("/mgba-http/button");
group.WithTags("Button");

group.MapPost("/", async (SocketService socket, KeysEnum key) =>
group.MapPost("/tap", async (SocketService socket, KeysEnum key) =>
{
await socket.SendMessageAsync(new MessageModel("mgba-http.button", key.ToString()));
await socket.SendMessageAsync(new MessageModel("mgba-http.button.tap", key.ToString()));
}).WithOpenApi(o =>
{
o.Summary = "Sends button presses.";
Expand All @@ -21,6 +22,39 @@ public static RouteGroupBuilder MapButtonEndpoints(this IEndpointRouteBuilder ro
return o;
});

group.MapPost("/tapmany", async (SocketService socket, [FromQuery] KeysEnum[] keys) =>
{
await socket.SendMessageAsync(new MessageModel("mgba-http.button.tapmany", string.Join(";", keys)));
}).WithOpenApi(o =>
{
o.Summary = "Sends multiple button presses simultaneously.";
o.Description = "A custom convenience API that implements multiple simultaneously keys being pressed and released. This is as opposed to the key based core API that sends only either a press or release message.";
o.Parameters[0].Description = "Key array containing any of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L.";
return o;
});

group.MapPost("/hold", async (SocketService socket, KeysEnum key, int duration) =>
{
await socket.SendMessageAsync(new MessageModel("mgba-http.button.hold", key.ToString(), duration.ToString()));
}).WithOpenApi(o =>
{
o.Summary = "Sends a held down button for a given duration in frames.";
o.Description = "A custom convenience API that implements a held down button for a given duration in frames. This is as opposed to the key based core API that sends only either a press or release message.";
o.Parameters[0].Description = "Key value of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L.";
return o;
});

group.MapPost("/holdmany", async (SocketService socket, [FromQuery] KeysEnum[] keys, int duration) =>
{
await socket.SendMessageAsync(new MessageModel("mgba-http.button.holdmany", string.Join(";", keys), duration.ToString()));
}).WithOpenApi(o =>
{
o.Summary = "Sends multiple button presses simultaneously for a given duration in frames.";
o.Description = "A custom convenience API that implements multiple simultaneously keys being pressed and released for a given duration in frames. This is as opposed to the key based core API that sends only either a press or release message.";
o.Parameters[0].Description = "Key array containing any of: A, B, Start, Select, Start, Right, Left, Up, Down, R, or L.";
return o;
});

return group;
}
}
Expand Down

0 comments on commit 31ad7c8

Please sign in to comment.