mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-17 19:24:01 +02:00
Merge pull request #1445 from AverageConsumer/codex/desktop-companion-display
This commit is contained in:
@@ -6,18 +6,30 @@ function love.conf(t)
|
|||||||
|
|
||||||
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
local editor = os.getenv("POKEPORT_EDITOR") == "1"
|
||||||
local developer = os.getenv("POKEPORT_DEV") == "1"
|
local developer = os.getenv("POKEPORT_DEV") == "1"
|
||||||
|
local companion = nil
|
||||||
if arg then
|
if arg then
|
||||||
for _, a in ipairs(arg) do
|
for _, a in ipairs(arg) do
|
||||||
if a == "--editor" then editor = true end
|
if a == "--editor" then editor = true end
|
||||||
if a == "--developer" then developer = true end
|
if a == "--developer" then developer = true end
|
||||||
|
local port, token = a:match("^%-%-display%-companion=(%d+),([%w]+)$")
|
||||||
|
if port then companion = { port = tonumber(port), token = token } end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
-- main.lua runs in the same Lua state right after conf.lua; stash the
|
||||||
-- decision in a global so it doesn't need to reparse `arg`.
|
-- decision in a global so it doesn't need to reparse `arg`.
|
||||||
_G.POKEPORT_EDITOR_MODE = editor
|
_G.POKEPORT_EDITOR_MODE = editor
|
||||||
_G.POKEPORT_DEV_MODE = developer
|
_G.POKEPORT_DEV_MODE = developer
|
||||||
|
_G.POKEPORT_DISPLAY_COMPANION = companion
|
||||||
|
|
||||||
if editor then
|
if companion then
|
||||||
|
t.identity = "pokemon-love2d-companion"
|
||||||
|
t.window.title = "gen1recomp Secondary Display"
|
||||||
|
t.window.width = 640
|
||||||
|
t.window.height = 576
|
||||||
|
t.window.minwidth = 160
|
||||||
|
t.window.minheight = 144
|
||||||
|
t.window.resizable = true
|
||||||
|
elseif editor then
|
||||||
-- Same identity as the game, deliberately: the editor edits the game's
|
-- Same identity as the game, deliberately: the editor edits the game's
|
||||||
-- saves and reads the game's ROM cache, both of which live under this
|
-- saves and reads the game's ROM cache, both of which live under this
|
||||||
-- folder. A private editor identity would point love.filesystem at an
|
-- folder. A private editor identity would point love.filesystem at an
|
||||||
@@ -51,7 +63,8 @@ function love.conf(t)
|
|||||||
end
|
end
|
||||||
t.version = love._os == "iOS" and "12.0" or "11.5"
|
t.version = love._os == "iOS" and "12.0" or "11.5"
|
||||||
t.window.vsync = 1
|
t.window.vsync = 1
|
||||||
t.modules.joystick = true
|
t.modules.audio = not companion
|
||||||
|
t.modules.joystick = not companion
|
||||||
t.modules.physics = false
|
t.modules.physics = false
|
||||||
|
|
||||||
-- love.system is not loaded during love.conf; love._os is set by the
|
-- love.system is not loaded during love.conf; love._os is set by the
|
||||||
|
|||||||
@@ -738,6 +738,10 @@ palette-correct blit of either canvas into an arbitrary screen rect, and the
|
|||||||
oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
|
oldest queued event as `"action,x,y"` in submitted-frame coordinates, or `nil`.
|
||||||
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
This is what lets a mod lay the two passes out as two stacked Game Boy screens,
|
||||||
or push one onto a second screen, without the engine knowing the layout.
|
or push one onto a second screen, without the engine knowing the layout.
|
||||||
|
On process-capable Windows, Linux and macOS hosts without a native display
|
||||||
|
bridge, enabling this facade opens a second resizable app window instead. It
|
||||||
|
uses the same `available`, `detected`, `push`, `pollTouch` and `setEnabled`
|
||||||
|
contract, so a mod does not need a desktop-specific rendering path.
|
||||||
|
|
||||||
`render.output_enabled` and `render.output` are the later, whole-window seam
|
`render.output_enabled` and `render.output` are the later, whole-window seam
|
||||||
for mods that need the engine's normal composite rather than its separate
|
for mods that need the engine's normal composite rather than its separate
|
||||||
|
|||||||
@@ -8,6 +8,11 @@
|
|||||||
-- opens the editor on that slot's file, and restores the launcher when
|
-- opens the editor on that slot's file, and restores the launcher when
|
||||||
-- the editor's Close button is pressed (openEditor / closeEditor below)
|
-- the editor's Close button is pressed (openEditor / closeEditor below)
|
||||||
|
|
||||||
|
if POKEPORT_DISPLAY_COMPANION then
|
||||||
|
return require("src.render.DesktopCompanion").install(
|
||||||
|
POKEPORT_DISPLAY_COMPANION)
|
||||||
|
end
|
||||||
|
|
||||||
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
local editorMode = os.getenv("POKEPORT_EDITOR") == "1" or POKEPORT_EDITOR_MODE == true
|
||||||
|
|
||||||
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
local SwitchDiagnostics = require("src.debug.SwitchDiagnostics")
|
||||||
|
|||||||
@@ -272,6 +272,38 @@ function HostShell.quote(s)
|
|||||||
return "'" .. s:gsub("'", "'\\''") .. "'"
|
return "'" .. s:gsub("'", "'\\''") .. "'"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Launch another instance of this packaged app without waiting for it. The
|
||||||
|
-- same path works on all process-capable desktop hosts; only the shell's
|
||||||
|
-- background spelling differs. Source checkouts include their game folder,
|
||||||
|
-- while fused releases and AppImages already carry it in the executable.
|
||||||
|
function HostShell.spawnSelfDetached(args)
|
||||||
|
if not require("src.core.Platform").canSpawnProcess() then return false end
|
||||||
|
local fs = love and love.filesystem
|
||||||
|
if not (fs and fs.getExecutablePath) then return false end
|
||||||
|
local executable = os.getenv("APPIMAGE") or fs.getExecutablePath()
|
||||||
|
if type(executable) ~= "string" or executable == "" then return false end
|
||||||
|
|
||||||
|
local argv = {}
|
||||||
|
local fused = fs.isFused and fs.isFused()
|
||||||
|
if not os.getenv("APPIMAGE") and not fused and fs.getSource then
|
||||||
|
argv[#argv + 1] = fs.getSource()
|
||||||
|
end
|
||||||
|
for _, value in ipairs(args or {}) do argv[#argv + 1] = tostring(value) end
|
||||||
|
|
||||||
|
local command = HostShell.quote(executable)
|
||||||
|
for _, value in ipairs(argv) do
|
||||||
|
command = command .. " " .. HostShell.quote(value)
|
||||||
|
end
|
||||||
|
local osName = love.system and love.system.getOS and love.system.getOS()
|
||||||
|
if osName == "Windows" then
|
||||||
|
command = 'start "" /b ' .. command .. " >NUL 2>&1"
|
||||||
|
else
|
||||||
|
command = HostShell.envPrefix() .. command .. " >/dev/null 2>&1 &"
|
||||||
|
end
|
||||||
|
local ok, _, code = os.execute(command)
|
||||||
|
return ok == true or ok == 0 or code == 0
|
||||||
|
end
|
||||||
|
|
||||||
-- MEMOISED per Lua state (so once per thread). This used to spawn a whole
|
-- MEMOISED per Lua state (so once per thread). This used to spawn a whole
|
||||||
-- `curl --version` process on every single fetch -- twice for a GET through
|
-- `curl --version` process on every single fetch -- twice for a GET through
|
||||||
-- the Android-bridge fallback -- which doubled the number of spawns the lock
|
-- the Android-bridge fallback -- which doubled the number of spawns the lock
|
||||||
|
|||||||
@@ -0,0 +1,125 @@
|
|||||||
|
-- Minimal second-window process for src/render/DesktopScreen.lua.
|
||||||
|
|
||||||
|
local DesktopCompanion = {}
|
||||||
|
|
||||||
|
function DesktopCompanion.install(config)
|
||||||
|
local enet = require("enet")
|
||||||
|
local host = assert(enet.host_create())
|
||||||
|
local peer = assert(host:connect(("127.0.0.1:%d"):format(config.port), 2))
|
||||||
|
local image, sourceW, sourceH, preference
|
||||||
|
local background = { 0, 0, 0, 1 }
|
||||||
|
local connected, commandedQuit = false, false
|
||||||
|
local pointerDown = false
|
||||||
|
local started = love.timer.getTime()
|
||||||
|
local lastContact = started
|
||||||
|
|
||||||
|
local function send(kind, payload)
|
||||||
|
if not connected then return end
|
||||||
|
pcall(peer.send, peer, kind .. config.token .. (payload or ""), 1, "reliable")
|
||||||
|
end
|
||||||
|
|
||||||
|
local function receiveFrame(data)
|
||||||
|
local prefix = "F" .. config.token .. "\n"
|
||||||
|
if data:sub(1, #prefix) ~= prefix then return end
|
||||||
|
local split = data:find("\n", #prefix + 1, true)
|
||||||
|
if not split then return end
|
||||||
|
local w, h, rgb, mode = data:sub(#prefix + 1, split - 1)
|
||||||
|
:match("^(%d+),(%d+),(%d+),([%w_:.-]+)$")
|
||||||
|
w, h, rgb = tonumber(w), tonumber(h), tonumber(rgb)
|
||||||
|
if not w or not h or w < 1 or h < 1 or w > 4096 or h > 4096 then return end
|
||||||
|
local ok, raw = pcall(love.data.decompress, "string", "lz4",
|
||||||
|
data:sub(split + 1))
|
||||||
|
if not ok or type(raw) ~= "string" or #raw ~= w * h * 4 then return end
|
||||||
|
local made, pixels = pcall(love.image.newImageData, w, h, "rgba8", raw)
|
||||||
|
if not made then return end
|
||||||
|
if not image or sourceW ~= w or sourceH ~= h then
|
||||||
|
if image and image.release then image:release() end
|
||||||
|
image = love.graphics.newImage(pixels)
|
||||||
|
else
|
||||||
|
image:replacePixels(pixels)
|
||||||
|
end
|
||||||
|
sourceW, sourceH, preference = w, h, mode
|
||||||
|
image:setFilter(mode:find("cover", 1, true) and "linear" or "nearest",
|
||||||
|
mode:find("cover", 1, true) and "linear" or "nearest")
|
||||||
|
background = {
|
||||||
|
math.floor(rgb / 0x10000) % 0x100 / 255,
|
||||||
|
math.floor(rgb / 0x100) % 0x100 / 255,
|
||||||
|
rgb % 0x100 / 255, 1,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function service()
|
||||||
|
while true do
|
||||||
|
local event = host:service(0)
|
||||||
|
if not event then break end
|
||||||
|
if event.type == "connect" then
|
||||||
|
connected, lastContact = true, love.timer.getTime()
|
||||||
|
send("H")
|
||||||
|
elseif event.type == "receive" then
|
||||||
|
lastContact = love.timer.getTime()
|
||||||
|
if event.data == "Q" .. config.token then
|
||||||
|
commandedQuit = true
|
||||||
|
love.event.quit()
|
||||||
|
elseif event.data ~= "P" .. config.token then
|
||||||
|
receiveFrame(event.data)
|
||||||
|
end
|
||||||
|
elseif event.type == "disconnect" then
|
||||||
|
love.event.quit()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
local function placement()
|
||||||
|
if not image then return 0, 0, 1 end
|
||||||
|
local ww, wh = love.graphics.getDimensions()
|
||||||
|
local cover = preference and preference:find("cover", 1, true)
|
||||||
|
local scale = (cover and math.max or math.min)(ww / sourceW, wh / sourceH)
|
||||||
|
return (ww - sourceW * scale) / 2, (wh - sourceH * scale) / 2, scale
|
||||||
|
end
|
||||||
|
|
||||||
|
local function input(action, x, y)
|
||||||
|
if not image then return false end
|
||||||
|
local dx, dy, scale = placement()
|
||||||
|
local sx, sy = math.floor((x - dx) / scale), math.floor((y - dy) / scale)
|
||||||
|
if sx < 0 or sy < 0 or sx >= sourceW or sy >= sourceH then return false end
|
||||||
|
send("I", ("\n%s,%d,%d"):format(action, sx, sy))
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.update()
|
||||||
|
service()
|
||||||
|
local t = love.timer.getTime()
|
||||||
|
if (not connected and t - started > 5) or t - lastContact > 5 then
|
||||||
|
love.event.quit()
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.draw()
|
||||||
|
love.graphics.clear(background[1], background[2], background[3], background[4])
|
||||||
|
if not image then return end
|
||||||
|
local x, y, scale = placement()
|
||||||
|
love.graphics.setColor(1, 1, 1, 1)
|
||||||
|
love.graphics.draw(image, x, y, 0, scale, scale)
|
||||||
|
end
|
||||||
|
|
||||||
|
function love.mousepressed(x, y, button)
|
||||||
|
if button == 1 then pointerDown = input("down", x, y) end
|
||||||
|
end
|
||||||
|
function love.mousereleased(x, y, button)
|
||||||
|
if button == 1 and pointerDown then
|
||||||
|
if not input("up", x, y) then send("I", "\ncancel,0,0") end
|
||||||
|
pointerDown = false
|
||||||
|
end
|
||||||
|
end
|
||||||
|
function love.touchpressed(_, x, y) input("down", x, y) end
|
||||||
|
function love.touchreleased(_, x, y) input("up", x, y) end
|
||||||
|
function love.keypressed(key)
|
||||||
|
if key == "escape" then love.event.quit() end
|
||||||
|
end
|
||||||
|
function love.quit()
|
||||||
|
if not commandedQuit then send("C") end
|
||||||
|
pcall(peer.disconnect_now, peer)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
return DesktopCompanion
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
-- Cross-platform desktop secondary display. LOVE owns one window, so a
|
||||||
|
-- second minimal instance of this same app owns the companion window. ENet
|
||||||
|
-- is bundled with LOVE; binding it to loopback keeps frames and input local.
|
||||||
|
|
||||||
|
local Platform = require("src.core.Platform")
|
||||||
|
local HostShell = require("src.core.HostShell")
|
||||||
|
local okEnet, enet = pcall(require, "enet")
|
||||||
|
|
||||||
|
local DesktopScreen = {}
|
||||||
|
local state = {
|
||||||
|
enabled = false, blocked = false, host = nil, peer = nil,
|
||||||
|
token = nil, port = nil, touches = {}, retryAt = 0, heartbeatAt = 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
local function now()
|
||||||
|
return love and love.timer and love.timer.getTime and love.timer.getTime()
|
||||||
|
or os.clock()
|
||||||
|
end
|
||||||
|
|
||||||
|
local function destroy(sendQuit)
|
||||||
|
if state.peer and sendQuit then
|
||||||
|
pcall(state.peer.send, state.peer, "Q" .. state.token, 1, "reliable")
|
||||||
|
end
|
||||||
|
if state.peer then pcall(state.peer.disconnect_now, state.peer) end
|
||||||
|
if state.host then pcall(state.host.destroy, state.host) end
|
||||||
|
state.host, state.peer, state.token, state.port = nil, nil, nil, nil
|
||||||
|
state.touches = {}
|
||||||
|
end
|
||||||
|
|
||||||
|
local function token()
|
||||||
|
local seed = table.concat({ tostring(os.time()), tostring(now()), tostring({}) }, ":")
|
||||||
|
local digest = love.data.hash("sha256", seed)
|
||||||
|
return love.data.encode("string", "hex", digest):sub(1, 24)
|
||||||
|
end
|
||||||
|
|
||||||
|
local function start()
|
||||||
|
if state.host or state.blocked or now() < state.retryAt then
|
||||||
|
return state.host ~= nil
|
||||||
|
end
|
||||||
|
local base = 49152 + math.floor(now() * 1000) % 12000
|
||||||
|
for attempt = 0, 31 do
|
||||||
|
local port = 49152 + (base - 49152 + attempt * 37) % 12000
|
||||||
|
local ok, host = pcall(enet.host_create,
|
||||||
|
("127.0.0.1:%d"):format(port), 1, 2)
|
||||||
|
if ok and host then
|
||||||
|
state.host, state.port, state.token = host, port, token()
|
||||||
|
local launched = HostShell.spawnSelfDetached({
|
||||||
|
("--display-companion=%d,%s"):format(port, state.token),
|
||||||
|
})
|
||||||
|
if launched then return true end
|
||||||
|
destroy(false)
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
state.retryAt = now() + 1
|
||||||
|
return false
|
||||||
|
end
|
||||||
|
|
||||||
|
local function service()
|
||||||
|
if not state.enabled or state.blocked then return end
|
||||||
|
if not state.host and not start() then return end
|
||||||
|
while state.host do
|
||||||
|
local ok, event = pcall(state.host.service, state.host, 0)
|
||||||
|
if not ok then
|
||||||
|
destroy(false)
|
||||||
|
state.retryAt = now() + 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
if not event then break end
|
||||||
|
if event.type == "receive" then
|
||||||
|
local data = event.data or ""
|
||||||
|
if data == "H" .. state.token then
|
||||||
|
state.peer = event.peer
|
||||||
|
elseif event.peer == state.peer
|
||||||
|
and data:sub(1, #state.token + 2) == "I" .. state.token .. "\n" then
|
||||||
|
state.touches[#state.touches + 1] = data:sub(#state.token + 3)
|
||||||
|
elseif event.peer == state.peer and data == "C" .. state.token then
|
||||||
|
state.blocked = true
|
||||||
|
destroy(false)
|
||||||
|
return
|
||||||
|
end
|
||||||
|
elseif event.type == "disconnect" and event.peer == state.peer then
|
||||||
|
destroy(false)
|
||||||
|
state.retryAt = now() + 1
|
||||||
|
return
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if state.peer and now() >= state.heartbeatAt then
|
||||||
|
state.heartbeatAt = now() + 1
|
||||||
|
pcall(state.peer.send, state.peer, "P" .. state.token, 1, "unreliable")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.usable()
|
||||||
|
return okEnet and enet ~= nil and Platform.canSpawnProcess()
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.available()
|
||||||
|
return DesktopScreen.detected()
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.detected()
|
||||||
|
service()
|
||||||
|
return state.peer ~= nil
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.push(imageData, w, h, background, preference)
|
||||||
|
service()
|
||||||
|
if not state.peer or not imageData or not imageData.getString then return false end
|
||||||
|
w, h = tonumber(w), tonumber(h)
|
||||||
|
if not w or not h or w < 1 or h < 1 or w > 4096 or h > 4096 then return false end
|
||||||
|
local ok, raw = pcall(imageData.getString, imageData)
|
||||||
|
if not ok or type(raw) ~= "string" or #raw ~= w * h * 4 then return false end
|
||||||
|
local packed = love.data.compress("string", "lz4", raw, 1)
|
||||||
|
local header = ("F%s\n%d,%d,%u,%s\n"):format(state.token, w, h,
|
||||||
|
tonumber(background) or 0, tostring(preference or "auto"):gsub("[^%w_:.-]", ""))
|
||||||
|
local sent = pcall(state.peer.send, state.peer, header .. packed, 0, "reliable")
|
||||||
|
return sent
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.pollTouch()
|
||||||
|
service()
|
||||||
|
return table.remove(state.touches, 1)
|
||||||
|
end
|
||||||
|
|
||||||
|
function DesktopScreen.setEnabled(on)
|
||||||
|
on = on == true
|
||||||
|
if on == state.enabled then
|
||||||
|
if on then service() end
|
||||||
|
return
|
||||||
|
end
|
||||||
|
state.enabled = on
|
||||||
|
state.blocked = false
|
||||||
|
if on then start(); service() else destroy(true) end
|
||||||
|
end
|
||||||
|
|
||||||
|
return DesktopScreen
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
-- Bridge to native secondary-display output (Android Presentation). The C
|
-- Shared secondary-display facade. Android uses its native Presentation
|
||||||
-- functions live in mobile/android/love/src/jni/love/src/common/android.cpp.
|
-- bridge; process-capable desktop hosts fall back to a companion window.
|
||||||
-- Everything is guarded: off Android, or if the symbols cannot be resolved,
|
-- Everything is guarded, so unsupported hosts keep the in-window layout.
|
||||||
-- this stays inert and the renderer keeps the in-window stacked layout.
|
|
||||||
|
|
||||||
local SecondScreen = {}
|
local SecondScreen = {}
|
||||||
local C = nil
|
local C = nil
|
||||||
local ffi = nil
|
local ffi = nil
|
||||||
|
local desktop = nil
|
||||||
|
|
||||||
local function log(msg)
|
local function log(msg)
|
||||||
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
pcall(function() require("src.core.Logger").info("SecondScreen: %s", msg) end)
|
||||||
@@ -37,17 +37,36 @@ do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
if not C then
|
||||||
|
local ok, backend = pcall(require, "src.render.DesktopScreen")
|
||||||
|
if ok and backend and backend.usable and backend.usable() then
|
||||||
|
desktop = backend
|
||||||
|
log("desktop companion backend ready")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
function SecondScreen.usable()
|
function SecondScreen.usable()
|
||||||
return C ~= nil
|
return C ~= nil or desktop ~= nil
|
||||||
end
|
end
|
||||||
|
|
||||||
function SecondScreen.available()
|
function SecondScreen.available()
|
||||||
|
if desktop then return desktop.available() end
|
||||||
if not C then return false end
|
if not C then return false end
|
||||||
local ok, r = pcall(C.love_android_secondary_ready)
|
local ok, r = pcall(C.love_android_secondary_ready)
|
||||||
return ok and r ~= 0
|
return ok and r ~= 0
|
||||||
end
|
end
|
||||||
|
|
||||||
function SecondScreen.push(imageData, w, h)
|
-- A connected display is not necessarily the current Presentation yet. This
|
||||||
|
-- distinction lets a companion retry its first frame after hotplug/re-target.
|
||||||
|
function SecondScreen.detected()
|
||||||
|
if desktop then return desktop.detected() end
|
||||||
|
return SecondScreen.available()
|
||||||
|
end
|
||||||
|
|
||||||
|
function SecondScreen.push(imageData, w, h, background, preference)
|
||||||
|
if desktop then
|
||||||
|
return desktop.push(imageData, w, h, background, preference)
|
||||||
|
end
|
||||||
if not C or not imageData then return false end
|
if not C or not imageData then return false end
|
||||||
return pcall(function()
|
return pcall(function()
|
||||||
C.love_android_push_secondary(imageData:getFFIPointer(), w, h)
|
C.love_android_push_secondary(imageData:getFFIPointer(), w, h)
|
||||||
@@ -57,6 +76,7 @@ end
|
|||||||
-- Returns the oldest queued secondary-display event as "action,x,y", where
|
-- Returns the oldest queued secondary-display event as "action,x,y", where
|
||||||
-- coordinates are in the submitted frame's pixel space.
|
-- coordinates are in the submitted frame's pixel space.
|
||||||
function SecondScreen.pollTouch()
|
function SecondScreen.pollTouch()
|
||||||
|
if desktop then return desktop.pollTouch() end
|
||||||
if not C then return nil end
|
if not C then return nil end
|
||||||
local ok, event = pcall(function()
|
local ok, event = pcall(function()
|
||||||
return C.love_android_poll_secondary_touch()
|
return C.love_android_poll_secondary_touch()
|
||||||
@@ -66,6 +86,7 @@ function SecondScreen.pollTouch()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function SecondScreen.setEnabled(on)
|
function SecondScreen.setEnabled(on)
|
||||||
|
if desktop then return desktop.setEnabled(on) end
|
||||||
if not C then return end
|
if not C then return end
|
||||||
pcall(function() C.love_android_secondary_enable(on and 1 or 0) end)
|
pcall(function() C.love_android_secondary_enable(on and 1 or 0) end)
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local clock, quit = 1, false
|
||||||
|
local sent, queue, draws = {}, {}, 0
|
||||||
|
local peer = {
|
||||||
|
send = function(_, data) sent[#sent + 1] = data end,
|
||||||
|
disconnect_now = function() end,
|
||||||
|
}
|
||||||
|
local host = {
|
||||||
|
connect = function() return peer end,
|
||||||
|
service = function()
|
||||||
|
if #queue == 0 then return nil end
|
||||||
|
return table.remove(queue, 1)
|
||||||
|
end,
|
||||||
|
}
|
||||||
|
local rendered = {
|
||||||
|
setFilter = function() end, replacePixels = function() end,
|
||||||
|
release = function() end,
|
||||||
|
}
|
||||||
|
love = {
|
||||||
|
timer = { getTime = function() return clock end },
|
||||||
|
data = { decompress = function(_, _, value) return value end },
|
||||||
|
image = { newImageData = function(w, h, format, raw)
|
||||||
|
assert(w == 1 and h == 1 and format == "rgba8" and raw == "rgba")
|
||||||
|
return {}
|
||||||
|
end },
|
||||||
|
graphics = {
|
||||||
|
newImage = function() return rendered end,
|
||||||
|
getDimensions = function() return 100, 100 end,
|
||||||
|
clear = function() end, setColor = function() end,
|
||||||
|
draw = function() draws = draws + 1 end,
|
||||||
|
},
|
||||||
|
event = { quit = function() quit = true end },
|
||||||
|
}
|
||||||
|
package.preload.enet = function()
|
||||||
|
return { host_create = function() return host end }
|
||||||
|
end
|
||||||
|
|
||||||
|
require("src.render.DesktopCompanion").install({ port = 50000, token = "token" })
|
||||||
|
queue[#queue + 1] = { type = "connect" }
|
||||||
|
love.update()
|
||||||
|
assert(sent[#sent] == "Htoken", "companion authenticates after connecting")
|
||||||
|
queue[#queue + 1] = {
|
||||||
|
type = "receive", data = "Ftoken\n1,1,0,auto\nrgba",
|
||||||
|
}
|
||||||
|
love.update()
|
||||||
|
love.draw()
|
||||||
|
assert(draws == 1, "companion draws a received frame")
|
||||||
|
love.mousepressed(50, 50, 1)
|
||||||
|
love.mousereleased(50, 50, 1)
|
||||||
|
assert(sent[#sent - 1] == "Itoken\ndown,0,0"
|
||||||
|
and sent[#sent] == "Itoken\nup,0,0", "mouse input maps back to source pixels")
|
||||||
|
queue[#queue + 1] = { type = "receive", data = "Qtoken" }
|
||||||
|
love.update()
|
||||||
|
assert(quit, "parent can close the companion")
|
||||||
|
print("desktop companion: ok")
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local clock = 1
|
||||||
|
love = {
|
||||||
|
timer = { getTime = function() return clock end },
|
||||||
|
data = {
|
||||||
|
hash = function() return "digest" end,
|
||||||
|
encode = function() return "0123456789abcdef0123456789abcdef" end,
|
||||||
|
compress = function(_, _, value) return value end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
local sent, spawned, queue = {}, nil, {}
|
||||||
|
local peer = {
|
||||||
|
send = function(_, data, channel, flag)
|
||||||
|
sent[#sent + 1] = { data = data, channel = channel, flag = flag }
|
||||||
|
end,
|
||||||
|
disconnect_now = function() end,
|
||||||
|
}
|
||||||
|
local host = {
|
||||||
|
service = function()
|
||||||
|
if #queue == 0 then return nil end
|
||||||
|
return table.remove(queue, 1)
|
||||||
|
end,
|
||||||
|
destroy = function() end,
|
||||||
|
}
|
||||||
|
|
||||||
|
package.loaded["src.core.Platform"] = { canSpawnProcess = function() return true end }
|
||||||
|
package.loaded["src.core.HostShell"] = {
|
||||||
|
spawnSelfDetached = function(args) spawned = args return true end,
|
||||||
|
}
|
||||||
|
package.preload.enet = function()
|
||||||
|
return { host_create = function() return host end }
|
||||||
|
end
|
||||||
|
|
||||||
|
local Screen = require("src.render.SecondScreen")
|
||||||
|
assert(Screen.usable(), "the shared facade selects the desktop backend")
|
||||||
|
Screen.setEnabled(true)
|
||||||
|
assert(spawned and spawned[1]:match("^%-%-display%-companion=%d+,[%w]+$"),
|
||||||
|
"enabling launches one companion of this app")
|
||||||
|
local token = spawned[1]:match(",([%w]+)$")
|
||||||
|
queue[#queue + 1] = { type = "receive", peer = peer, data = "H" .. token }
|
||||||
|
assert(Screen.detected(), "a token-authenticated companion becomes detected")
|
||||||
|
|
||||||
|
local pixels = { getString = function() return "rgba" end }
|
||||||
|
assert(Screen.push(pixels, 1, 1, 0x102030, "auto"),
|
||||||
|
"a connected companion accepts a frame")
|
||||||
|
assert(sent[#sent].data:find("^F" .. token .. "\n1,1,1056816,auto\nrgba"),
|
||||||
|
"frame metadata and pixels stay in one loopback packet")
|
||||||
|
|
||||||
|
queue[#queue + 1] = {
|
||||||
|
type = "receive", peer = peer, data = "I" .. token .. "\ndown,3,4",
|
||||||
|
}
|
||||||
|
assert(Screen.pollTouch() == "down,3,4", "companion input returns to the mod")
|
||||||
|
Screen.setEnabled(false)
|
||||||
|
assert(sent[#sent].data == "Q" .. token, "disabling closes the companion")
|
||||||
|
print("desktop second screen: ok")
|
||||||
@@ -0,0 +1,32 @@
|
|||||||
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||||
|
|
||||||
|
local osName, fused, command = "Windows", true, nil
|
||||||
|
love = {
|
||||||
|
system = { getOS = function() return osName end },
|
||||||
|
filesystem = {
|
||||||
|
getExecutablePath = function() return "C:\\Game\\gen1recomp.exe" end,
|
||||||
|
getSource = function() return "C:\\Source\\gen1recomp" end,
|
||||||
|
isFused = function() return fused end,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
package.loaded["src.core.Platform"] = { canSpawnProcess = function() return true end }
|
||||||
|
local execute = os.execute
|
||||||
|
os.execute = function(value) command = value return 0 end
|
||||||
|
|
||||||
|
local HostShell = require("src.core.HostShell")
|
||||||
|
assert(HostShell.spawnSelfDetached({ "--display-companion=50000,token" }))
|
||||||
|
assert(command:find('start "" /b ', 1, true)
|
||||||
|
and command:find('"C:\\Game\\gen1recomp.exe"', 1, true),
|
||||||
|
"Windows launches the fused app detached")
|
||||||
|
|
||||||
|
osName, fused = "Linux", false
|
||||||
|
assert(HostShell.spawnSelfDetached({ "--display-companion=50000,token" }))
|
||||||
|
assert(command:find("'C:\\Source\\gen1recomp'", 1, true)
|
||||||
|
and command:sub(-1) == "&", "Linux source runs include the game folder")
|
||||||
|
|
||||||
|
osName, fused = "OS X", true
|
||||||
|
assert(HostShell.spawnSelfDetached({ "--display-companion=50000,token" }))
|
||||||
|
assert(not command:find("start", 1, true) and command:sub(-1) == "&",
|
||||||
|
"macOS uses the same detached POSIX path")
|
||||||
|
os.execute = execute
|
||||||
|
print("spawn self detached: ok")
|
||||||
Reference in New Issue
Block a user