mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
Expose read-only device power info to mods
This commit is contained in:
@@ -255,7 +255,8 @@ engine's globals. Every chunk you author gets it: `main.lua`, your
|
||||
| `package`, `dofile`, `loadfile`, `debug`, `getfenv`, `setfenv` | `require` for the supported engine modules |
|
||||
| `require("ffi")`, `require("love.*")` | the `love` table you are given |
|
||||
| `love.filesystem` | `mod.storage` (per-mod, per-playthrough) and `mod:read` |
|
||||
| `love.thread`, `love.system`, `love.event` | `mod.events`, `mod.hooks` |
|
||||
| `love.thread`, `love.event` | `mod.events`, `mod.hooks` |
|
||||
| `love.system` | `mod.device:powerInfo()` for battery information |
|
||||
|
||||
The rest of `love` passes through unchanged, so graphics, audio, timers and
|
||||
input work as they always have.
|
||||
|
||||
@@ -514,3 +514,17 @@ local both = mod.datetime:dateTime(game, createdAt)
|
||||
The live `game` supplies only the current option context. Formatting never
|
||||
mutates the save, options, or timestamp, and invalid timestamps return
|
||||
`"----"`.
|
||||
|
||||
## Device power information
|
||||
|
||||
Sandboxed mods can read the host's battery state without receiving the rest
|
||||
of `love.system`:
|
||||
|
||||
```lua
|
||||
local state, percent = mod.device:powerInfo()
|
||||
```
|
||||
|
||||
`state` follows LÖVE's values: `"unknown"`, `"battery"`, `"nobattery"`,
|
||||
`"charging"`, or `"charged"`. `percent` is `0` through `100`, or `nil` when
|
||||
the platform cannot report it. The facade is read-only and does not expose
|
||||
URL launching, clipboard access, or other system operations.
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
# RFC 0008 — Read-only device power information for sandboxed mods
|
||||
|
||||
## Status
|
||||
|
||||
Proposed. Engine: `Loader.lua`, `Sandbox.lua`. Test:
|
||||
`tests/modkit/cases/device_power_info.lua`.
|
||||
|
||||
## Motivation
|
||||
|
||||
A handheld UI mod can show the player's battery state and warn before power
|
||||
loss. The sandbox correctly removes `love.system` because that module also
|
||||
launches URLs and exposes other host operations, but it leaves no scoped way
|
||||
to read the harmless power values that LÖVE already provides.
|
||||
|
||||
## The decision it extends
|
||||
|
||||
Extends the mod sandbox in `src/mods/Sandbox.lua`: blocked host modules stay
|
||||
blocked while legitimate operations receive narrow engine-owned facades.
|
||||
|
||||
## The exact API delta
|
||||
|
||||
Add `mod.device:powerInfo() -> state, percent`.
|
||||
|
||||
The engine calls `love.system.getPowerInfo()` outside the mod sandbox and
|
||||
returns only its first two values. `state` is one of LÖVE's standard power
|
||||
states. `percent` is `0` through `100` or `nil`. When the platform has no
|
||||
power-information backend, the result is `"unknown", nil`.
|
||||
|
||||
No permission grants access to `love.system`; URL launching, clipboard access,
|
||||
OS identification, and the module table itself remain unavailable.
|
||||
|
||||
## Migration note for existing mods
|
||||
|
||||
Mods that used `love.system.getPowerInfo()` replace that call with
|
||||
`mod.device:powerInfo()`. No other mod changes.
|
||||
|
||||
## Parity tests
|
||||
|
||||
- **No mod:** loading no mods does not call the platform power backend.
|
||||
- **Mod API:** a fixture mod loaded through the public loader receives state
|
||||
and percentage through `mod.device`, while the existing sandbox suite keeps
|
||||
proving that direct `love.system` access is refused.
|
||||
- **Unavailable backend:** the public facade returns `"unknown", nil` rather
|
||||
than inventing battery data or failing mod load.
|
||||
|
||||
## Deprecation etiquette
|
||||
|
||||
Nothing deprecated. The facade is additive; the sandbox's `love.system` block
|
||||
remains in force.
|
||||
@@ -999,6 +999,16 @@ function Loader:_api(mod)
|
||||
return DateTime.dateTime(game, timestamp)
|
||||
end,
|
||||
},
|
||||
-- The read-only part of love.system that device UIs legitimately need.
|
||||
-- Do not expose the module: openURL and clipboard access stay sandboxed.
|
||||
device = {
|
||||
powerInfo = function()
|
||||
local getPowerInfo = love and love.system and love.system.getPowerInfo
|
||||
if not getPowerInfo then return "unknown", nil end
|
||||
local state, percent = getPowerInfo()
|
||||
return state, percent
|
||||
end,
|
||||
},
|
||||
-- namespaced per mod; M11 backs these with save.modData /
|
||||
-- options.modOptions, the shape mods compile against is already final
|
||||
save = {
|
||||
|
||||
@@ -69,7 +69,7 @@ end
|
||||
-- value is the replacement to name in the error, or true when there is none
|
||||
local BLOCKED_LOVE = {
|
||||
filesystem = "mod.storage and mod:read", thread = true,
|
||||
system = true, event = true,
|
||||
system = "mod.device:powerInfo() for battery information", event = true,
|
||||
}
|
||||
|
||||
local loveProxy
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
-- A sandboxed mod may read battery state without receiving love.system and
|
||||
-- its process-launching surface.
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.modkit")
|
||||
|
||||
local FIXTURE = {
|
||||
["mods/power_probe/manifest.json"] = [[{
|
||||
"id": "power_probe",
|
||||
"name": "Power Probe",
|
||||
"version": "1.0.0",
|
||||
"entry": "main.lua",
|
||||
"api": 2
|
||||
}]],
|
||||
["mods/power_probe/main.lua"] = [[
|
||||
local mod = ...
|
||||
mod.exports.state, mod.exports.percent = mod.device:powerInfo()
|
||||
]],
|
||||
}
|
||||
|
||||
local saved = T.love.system.getPowerInfo
|
||||
local calls = 0
|
||||
T.love.system.getPowerInfo = function()
|
||||
calls = calls + 1
|
||||
return "charging", 42, 900
|
||||
end
|
||||
|
||||
local vanilla = T.sdk.loadNone({})
|
||||
T.eq(calls, 0, "no mod leaves the device power backend cold")
|
||||
vanilla.release()
|
||||
|
||||
local run = T.sdk.loadMods({ "mods/power_probe" },
|
||||
{ fs = T.sdk.memfs(FIXTURE) })
|
||||
T.eq(#run.errors, 0,
|
||||
"the sandboxed power probe loads clean (" .. tostring(run.errors[1]) .. ")")
|
||||
local out = run.loader.exports.power_probe or {}
|
||||
T.eq(out.state, "charging", "the public facade reports battery state")
|
||||
T.eq(out.percent, 42, "the public facade reports battery percentage")
|
||||
T.eq(calls, 1, "one facade read makes one platform call")
|
||||
run.release()
|
||||
|
||||
T.love.system.getPowerInfo = nil
|
||||
local unavailable = T.sdk.loadMods({ "mods/power_probe" },
|
||||
{ fs = T.sdk.memfs(FIXTURE) })
|
||||
out = unavailable.loader.exports.power_probe or {}
|
||||
T.eq(out.state, "unknown", "a missing platform backend has a stable state")
|
||||
T.eq(out.percent, nil, "a missing platform backend has no invented percentage")
|
||||
unavailable.release()
|
||||
|
||||
T.love.system.getPowerInfo = saved
|
||||
T.finish("device_power_info")
|
||||
@@ -125,7 +125,8 @@ T.eq(type(out.requireSemver), "table",
|
||||
T.check(out.loveFilesystem and out.loveFilesystem:find("mod.storage", 1, true),
|
||||
"love.filesystem is refused and names the replacement")
|
||||
T.check(out.loveThread ~= false, "love.thread is refused: it opens a full Lua state")
|
||||
T.check(out.loveSystem ~= false, "love.system is refused: openURL launches anything")
|
||||
T.check(out.loveSystem and out.loveSystem:find("mod.device:powerInfo()", 1, true),
|
||||
"love.system is refused and names the scoped power replacement")
|
||||
T.eq(out.loveGraphics, "table", "the rest of love passes through")
|
||||
T.check(out.loveAssign ~= false, "a mod cannot assign into the love facade")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user