Local game bridge IPC for remote apps and services#1276
Draft
dgruss wants to merge 111 commits into
Draft
Conversation
6233ca4 to
c8d25d3
Compare
…ror, and fix interaction with Alt+V on macOS
# Conflicts: # src/screens/UScreenSong.pas
# Conflicts: # src/screens/UScreenScore.pas # src/screens/UScreenSong.pas # src/screens/UScreenTop5.pas
# Conflicts: # src/screens/UScreenSong.pas
# Conflicts: # game/languages/English.ini # game/languages/German.ini # src/base/UDraw.pas # src/base/UThemes.pas # src/screens/UScreenJukebox.pas # src/screens/UScreenJukeboxOptions.pas # src/screens/UScreenJukeboxPlaylist.pas # src/screens/UScreenOptionsJukebox.pas # src/screens/UScreenSong.pas
# Conflicts: # game/themes/Modern.ini # src/base/UIni.pas # src/base/USingScores.pas # src/screens/UScreenName.pas # src/screens/UScreenScore.pas # src/screens/views/UScreenSingView.pas
# Conflicts: # game/themes/Modern/[name]noavatar.png # game/themes/Modern/[sing.player]lyric_active.png # game/themes/Modern/[sing.player]lyric_inactive.png # src/screens/UScreenSong.pas
# Conflicts: # src/screens/UScreenEditSub.pas # src/screens/views/UScreenSingView.pas
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
This PR adds a local JSONL IPC interface between USDX and a local bridge process.
The bridges process can be a phone remote bridge, remote-control app, public high-score
service, bot, web service, or other integration. USDX only speaks the local
game-facing protocol; everything beyond the adapter is implementation-specific.
What Changed
[RemoteBridge].UMain.micDelayMs.song.ended.playersscore output.docs/usdx-game-bridge-ipc.md.USDX Game Bridge IPC
JSON message contract between USDX and a local bridge process (adapter to remote services).
Out of scope: browser API, relay internals, WebRTC signaling, deployment,
storage, and room hosting.
Overview
The game talks only to a local adapter process. Everything beyond that adapter
is implementation-specific: a remote-control app, phone web clients, a public
high-score service, a Discord bot, a Jitsi service, or another controller can
all use this boundary.
Default startup:
Default generated command:
[RemoteBridge] StartCommandreplaces the generated command. OtherwiseNodeExecutable,BridgeScriptPath,ServerUrl,IpcHost, andIpcPortcontrol the generated command.
Steady-state data flow:
Input flow:
Party mode flow:
Transport
The transport must provide ordered, bidirectional, framed UTF-8 JSON objects.
Newline-delimited JSON over localhost TCP is valid.
Every message is a JSON object with a string
type.All
*Usfields are integer microseconds.Common fields:
displayCodeplayerIdslotrolesingerorcontroller.songSeqmediaStartUsdurationUshostSongStartUsAdapter To Game
Connection And Room
bridge.readydisplayCode,hostMonoUsdisplayCode.bridge.ponghostMonoUsbridge.ping.bridge.disconnectedroom.createddisplayCode,expiresAtroom.resumeddisplayCode,expiresAtroom.created.Example:
{ "type": "room.created", "displayCode": "ABCD", "expiresAt": 1770000000000 }Player State
Preferred full roster update:
{ "type": "remote.players.snapshot", "gamePlayerCount": 3, "players": [ { "playerId": "p_opaque", "name": "Lava", "slot": 3, "role": "singer", "connected": true, "difficultyMode": "hard", "difficulty": "hard", "avatarId": "avatar_id", "avatarFile": "avatar_file.webp", "micDelayMs": 0 } ] }Rules:
playersis the complete remote roster.slot: 0means unassigned.gamePlayerCountis the desired game player count.Incremental player messages:
player.joinedplayer.assignedplayer.disconnectedplayer.leftPlayer fields:
nameconnecteddifficultyModeeasy,medium,hard, orfree.difficultyavatarIdavatarFilemicDelayMsPitch
pitch.batchsends detected pitch, not audio.{ "type": "pitch.batch", "playerId": "p_opaque", "slot": 1, "role": "singer", "songSeq": 17, "batchSeq": 42, "baseSongTimeUs": 12000000, "frameDurUs": 10000, "frames": [ [0, 6900], [10000, 6912] ] }The game ignores the batch if
slotis invalid orframesis not an array.Frame layout:
0baseSongTimeUsif present.16900.Every accepted frame is a valid tone observation.
Control
{ "type": "control.command", "playerId": "p_opaque", "slot": 1, "role": "controller", "commandId": 123, "command": "menu.select", "args": {}, "clientIssuedUs": 123456000 }The game executes valid commands for the current state and answers with
ack.Command arguments:
textsongIdsplaylistIndexsongIdindexplaylistIndexwhen no dedicated field exists.itemIndexCommand reference:
menu.up{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1001,"command":"menu.up","args":{}}menu.down{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1002,"command":"menu.down","args":{}}menu.left{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1003,"command":"menu.left","args":{}}menu.right{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1004,"command":"menu.right","args":{}}menu.select{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1005,"command":"menu.select","args":{}}menu.back{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1006,"command":"menu.back","args":{}}game.pause{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1007,"command":"game.pause","args":{}}game.resume{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1008,"command":"game.resume","args":{}}song.skipIntro{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1009,"command":"song.skipIntro","args":{}}song.restart{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1010,"command":"song.restart","args":{}}song.menu{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1011,"command":"song.menu","args":{}}song.playlistMenu{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1012,"command":"song.playlistMenu","args":{}}song.jumpTo{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1013,"command":"song.jumpTo","args":{}}song.random{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1014,"command":"song.random","args":{}}song.select{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1015,"command":"song.select","args":{"songId":123}}song.start{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1016,"command":"song.start","args":{}}song.skip{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1017,"command":"song.skip","args":{}}preview.start{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1018,"command":"preview.start","args":{}}preview.stop{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1019,"command":"preview.stop","args":{}}search.setText{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1020,"command":"search.setText","args":{"text":"queen"}}playlist.load{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1021,"command":"playlist.load","args":{"playlistIndex":0}}playlist.createtext.{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1022,"command":"playlist.create","args":{"text":"Party"}}playlist.delete{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1023,"command":"playlist.delete","args":{"playlistIndex":0}}playlist.removeItem{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1024,"command":"playlist.removeItem","args":{"playlistIndex":0,"itemIndex":2}}playlist.addSelected{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1025,"command":"playlist.addSelected","args":{"playlistIndex":0}}playlist.removeSelected{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1026,"command":"playlist.removeSelected","args":{"playlistIndex":0}}playlist.addSongssongIdsto a playlist. Existing items are skipped.{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1027,"command":"playlist.addSongs","args":{"playlistIndex":0,"songIds":[123,456]}}playlist.removeSong{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1028,"command":"playlist.removeSong","args":{"playlistIndex":0,"songId":123}}playlist.moveItemUp{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1029,"command":"playlist.moveItemUp","args":{"playlistIndex":0,"itemIndex":2}}playlist.moveItemDown{"type":"control.command","playerId":"p1","slot":1,"role":"controller","commandId":1030,"command":"playlist.moveItemDown","args":{"playlistIndex":0,"itemIndex":2}}Commands outside their valid state are acknowledged with
accepted: false.Adapter Error
{ "type": "error", "code": "join.bad_code", "reason": "optional text", "source": "adapter" }Game To Adapter
Connection
{ "type": "bridge.ping" }Adapter replies with
bridge.pong.Game State
{ "type": "game.state", "seq": 18, "state": "song_select", "songSeq": 0, "players": [ { "slot": 1, "playerId": "p_opaque", "name": "Lava", "connected": true, "singing": true, "role": "singer" } ], "controllerPlayerId": "p_opaque", "playlistSize": 0 }Treat as state, not an event stream. Known
statevalues:Song Select State
Current song-selection UI state. Not a full song catalog transfer.
{ "type": "song.select.state", "selectedSongId": 123, "currentSong": { "songId": 123, "artist": "Artist", "title": "Title" }, "visibleSongs": 30, "totalSongs": 28223, "searchActive": true, "playlistActive": false, "results": [], "playlists": [], "playlistItems": [], "curPlaylist": 0 }Playlist summaries may include
fixedOrder.Song Clock
song.started:{ "type": "song.started", "songSeq": 17, "songId": 123, "mediaStartUs": 0, "durationUs": 210000000, "title": "Title", "artist": "Artist", "hostSongStartUs": 1234500000 }These share
songSeq,mediaStartUs, anddurationUs:song.pausedsong.resumedsong.positionsong.endedplayersscores.song.ended:{ "type": "song.ended", "songSeq": 17, "mediaStartUs": 210000000, "durationUs": 210000000, "players": [ { "slot": 1, "score": 5000, "scoreLine": 1200, "scoreGolden": 300, "scoreTotal": 6500 } ] }Lyrics
{ "type": "song.lyrics", "songSeq": 17, "mediaStartUs": 0, "tracks": [ { "track": 0, "line": 4, "text": "lyrics", "lineStartUs": 12000000, "lineEndUs": 15000000, "notes": [], "nextLines": [] } ] }Scores
score.snapshotreports current scores for all game player slots. USDX sendsone when singing starts and throttled updates while singing. Consumers that only
need final results can read
song.ended.players.{ "type": "score.snapshot", "songSeq": 17, "mediaStartUs": 43000000, "durationUs": 210000000, "players": [ { "slot": 1, "score": 5000, "scoreLine": 1200, "scoreGolden": 300, "scoreTotal": 6500 } ] }Party Mode
Party mode is adapter-owned at this boundary. The adapter drives USDX with the
same game-facing messages as normal play:
remote.players.snapshot,control.commandwithsong.select,song.start,song.skipsong.started,song.ended,score.snapshot,game.stateParty adapter sequence:
USDX does not receive party votes, matchmaking state, leaderboard state, or
phone notifications over this API.
Replies
player.assignedconfirms a slot:{ "type": "player.assigned", "playerId": "p_opaque", "slot": 1, "role": "singer" }ackconfirms a command:{ "type": "ack", "playerId": "p_opaque", "commandId": 123, "accepted": true, "reason": null, "gameStateSeq": 18 }Use
accepted: falsewithreasonwhen the command was understood but not run.Errors
{ "type": "error", "code": "message.invalid", "reason": "optional text", "source": "game_or_adapter" }Recommended codes:
message.unknown_typetype.message.invalidcommand.rejected