mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-23 14:07:34 +02:00
18d61779eb
We already ship the DLL, but mods can't ffi-load it under the sandbox, and the compat love.system shim wasn't passing tls* through either. So wss:// still died on stock builds. Engine loads the dialer at boot; compat forwards those keys; clipboard/openURL stay stubbed. Co-authored-by: Cursor <cursoragent@cursor.com>
957 lines
29 KiB
Lua
957 lines
29 KiB
Lua
local Logger = require("src.core.Logger")
|
|
local SafePath = require("src.mods.SafePath")
|
|
|
|
local okSave, SaveData = pcall(require, "src.core.SaveData")
|
|
if not okSave then SaveData = nil end
|
|
local okStorage, Storage = pcall(require, "src.mods.Storage")
|
|
if not okStorage then Storage = nil end
|
|
|
|
local LegacyCompat = {}
|
|
|
|
local ROOT = "mod_compat"
|
|
local OWN = "_own"
|
|
local MAX_SEGMENTS = 24
|
|
|
|
LegacyCompat.reports = {}
|
|
|
|
function LegacyCompat.reset()
|
|
LegacyCompat.reports = {}
|
|
end
|
|
|
|
function LegacyCompat.report(modId)
|
|
if modId then
|
|
local entry = LegacyCompat.reports[modId]
|
|
return entry and entry.order or {}
|
|
end
|
|
local out = {}
|
|
for id, entry in pairs(LegacyCompat.reports) do
|
|
out[id] = entry.order
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function note(ctx, call, advice)
|
|
local entry = LegacyCompat.reports[ctx.modId]
|
|
if not entry then
|
|
entry = { calls = {}, order = {} }
|
|
LegacyCompat.reports[ctx.modId] = entry
|
|
end
|
|
local row = entry.calls[call]
|
|
if row then
|
|
row.count = row.count + 1
|
|
return
|
|
end
|
|
row = { call = call, advice = advice, count = 1 }
|
|
entry.calls[call] = row
|
|
entry.order[#entry.order + 1] = row
|
|
Logger.warn("[%s] %s was removed from the mod sandbox; %s", ctx.modId, call,
|
|
advice)
|
|
end
|
|
|
|
local function refuse(ctx, call, advice)
|
|
note(ctx, call, advice)
|
|
return nil, ("%s is not available to mods; %s"):format(call, advice)
|
|
end
|
|
|
|
-- ------- path routing
|
|
|
|
local function normalize(path)
|
|
if type(path) ~= "string" or path == "" then return nil end
|
|
path = path:gsub("\\", "/")
|
|
while path:find("//", 1, true) do path = (path:gsub("//", "/")) end
|
|
if path ~= "/" then path = (path:gsub("/$", "")) end
|
|
return path
|
|
end
|
|
|
|
local function flatten(path)
|
|
local parts = {}
|
|
for segment in path:gmatch("[^/]+") do
|
|
if segment ~= "." and segment ~= ".." then
|
|
parts[#parts + 1] = (segment:gsub("[^%w_%.%-]", "_"))
|
|
end
|
|
end
|
|
if #parts == 0 then return nil end
|
|
while #parts > MAX_SEGMENTS do table.remove(parts, 1) end
|
|
return table.concat(parts, "/")
|
|
end
|
|
|
|
local function storageKey(key)
|
|
local parts = {}
|
|
for segment in key:gmatch("[^/]+") do parts[#parts + 1] = segment end
|
|
if #parts == 0 then return nil end
|
|
parts[#parts] = (parts[#parts]:gsub("%.[^%.]*$", ""))
|
|
for i = 1, #parts do
|
|
local cleaned = (parts[i]:gsub("[^%w_%-]", "_"))
|
|
if cleaned == "" then return nil end
|
|
parts[i] = cleaned
|
|
end
|
|
return table.concat(parts, "/")
|
|
end
|
|
|
|
local function ownRelative(ctx, path)
|
|
if not ctx.modPath then return nil end
|
|
if path == ctx.modPath then return "" end
|
|
if path:sub(1, #ctx.modPath + 1) == ctx.modPath .. "/" then
|
|
return path:sub(#ctx.modPath + 2)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function ownFull(ctx, rel)
|
|
if rel == "" then return ctx.modPath end
|
|
local safe = SafePath.safe(rel)
|
|
if not safe then return nil end
|
|
return ctx.modPath .. "/" .. safe
|
|
end
|
|
|
|
local function ownExists(ctx, rel)
|
|
local full = ownFull(ctx, rel)
|
|
local fs = ctx.fs
|
|
if not (full and fs and fs.getInfo) then return nil end
|
|
return fs.getInfo(full)
|
|
end
|
|
|
|
local function classify(ctx, path)
|
|
path = normalize(path)
|
|
if not path then return nil end
|
|
|
|
local root = ctx.virtualRoot
|
|
if path == root then return { key = "", dir = true } end
|
|
if path:sub(1, #root + 1) == root .. "/" then
|
|
return { key = flatten(path:sub(#root + 2)) }
|
|
end
|
|
|
|
local rel = ownRelative(ctx, path)
|
|
if not rel and path:sub(1, 1) ~= "/" and not path:match("^%a:")
|
|
and ownExists(ctx, path) then
|
|
rel = path
|
|
end
|
|
if rel then
|
|
if rel == "" then return { key = OWN, rel = "", dir = true } end
|
|
local flat = flatten(rel)
|
|
return { key = flat and (OWN .. "/" .. flat) or nil, rel = rel }
|
|
end
|
|
|
|
return { key = flatten(path) }
|
|
end
|
|
|
|
-- ------- the overlay
|
|
|
|
local function persistFs(ctx)
|
|
if SaveData and SaveData.persistenceFs then
|
|
return SaveData.persistenceFs(ctx.fs)
|
|
end
|
|
return ctx.fs
|
|
end
|
|
|
|
local function overlayPath(ctx, key)
|
|
if key == nil or key == "" then return ROOT .. "/" .. ctx.modId end
|
|
return ROOT .. "/" .. ctx.modId .. "/" .. key
|
|
end
|
|
|
|
local function ensureParent(fs, path)
|
|
if not fs.createDirectory then return end
|
|
local dir = path:match("^(.*)/[^/]+$")
|
|
if not dir then return end
|
|
local built = nil
|
|
for segment in dir:gmatch("[^/]+") do
|
|
built = built and (built .. "/" .. segment) or segment
|
|
fs.createDirectory(built)
|
|
end
|
|
end
|
|
|
|
local function overlayRead(ctx, key)
|
|
local fs = persistFs(ctx)
|
|
if not (fs and fs.read and fs.getInfo) then return nil end
|
|
local path = overlayPath(ctx, key)
|
|
local info = fs.getInfo(path)
|
|
if not info or info.type == "directory" then return nil end
|
|
local body = fs.read(path)
|
|
if type(body) ~= "string" then return nil end
|
|
return body
|
|
end
|
|
|
|
local function overlayWrite(ctx, key, data)
|
|
local fs = persistFs(ctx)
|
|
if not (fs and fs.write) then
|
|
return false, "no writable filesystem is available"
|
|
end
|
|
local path = overlayPath(ctx, key)
|
|
ensureParent(fs, path)
|
|
local ok, err = fs.write(path, data)
|
|
if ok == false then return false, err or "write failed" end
|
|
return true
|
|
end
|
|
|
|
local function overlayRemove(ctx, key)
|
|
local fs = persistFs(ctx)
|
|
if not (fs and fs.remove and fs.getInfo) then return false end
|
|
local path = overlayPath(ctx, key)
|
|
if not fs.getInfo(path) then return false end
|
|
fs.remove(path)
|
|
return true
|
|
end
|
|
|
|
local function overlayInfo(ctx, key)
|
|
local fs = persistFs(ctx)
|
|
if not (fs and fs.getInfo) then return nil end
|
|
return fs.getInfo(overlayPath(ctx, key))
|
|
end
|
|
|
|
local function storageRead(ctx, key)
|
|
if not (Storage and key and key ~= "") then return nil end
|
|
if key:sub(1, #OWN + 1) == OWN .. "/" then return nil end
|
|
local game = ctx.game and ctx.game()
|
|
if not game then return nil end
|
|
local sk = storageKey(key)
|
|
if not sk then return nil end
|
|
ctx.storage = ctx.storage or Storage.new(ctx.modId, ctx.fs)
|
|
local ok, bytes = pcall(ctx.storage.readBytes, ctx.storage, game, sk)
|
|
if ok and type(bytes) == "string" then return bytes end
|
|
return nil
|
|
end
|
|
|
|
local function readPath(ctx, path)
|
|
local at = classify(ctx, path)
|
|
if not at then return nil, "invalid path" end
|
|
local body = at.key and overlayRead(ctx, at.key)
|
|
if body then return body end
|
|
if at.rel then
|
|
local full = ownFull(ctx, at.rel)
|
|
local fs = ctx.fs
|
|
if full and fs and fs.read then
|
|
local packaged = fs.read(full)
|
|
if type(packaged) == "string" then return packaged end
|
|
end
|
|
end
|
|
body = at.key and storageRead(ctx, at.key)
|
|
if body then return body end
|
|
return nil, "could not open " .. tostring(path)
|
|
end
|
|
|
|
local function writePath(ctx, path, data)
|
|
local at = classify(ctx, path)
|
|
if not (at and at.key) then return false, "invalid path" end
|
|
return overlayWrite(ctx, at.key, data)
|
|
end
|
|
|
|
local function infoPath(ctx, path)
|
|
local at = classify(ctx, path)
|
|
if not at then return nil end
|
|
if at.key then
|
|
local info = overlayInfo(ctx, at.key)
|
|
if info then
|
|
return { type = info.type, size = info.size, modtime = info.modtime }
|
|
end
|
|
end
|
|
if at.rel then
|
|
local info = ownExists(ctx, at.rel)
|
|
if info then
|
|
return { type = info.type, size = info.size, modtime = info.modtime }
|
|
end
|
|
end
|
|
local stored = at.key and storageRead(ctx, at.key)
|
|
if stored then return { type = "file", size = #stored } end
|
|
return nil
|
|
end
|
|
|
|
local function listPath(ctx, path)
|
|
local at = classify(ctx, path)
|
|
local seen, out = {}, {}
|
|
local function add(name)
|
|
if name and name ~= "" and not seen[name] then
|
|
seen[name] = true
|
|
out[#out + 1] = name
|
|
end
|
|
end
|
|
if at and at.rel then
|
|
local full = ownFull(ctx, at.rel)
|
|
local fs = ctx.fs
|
|
if full and fs and fs.getDirectoryItems then
|
|
for _, name in ipairs(fs.getDirectoryItems(full) or {}) do add(name) end
|
|
end
|
|
end
|
|
if at and at.key then
|
|
local fs = persistFs(ctx)
|
|
if fs and fs.getDirectoryItems then
|
|
for _, name in ipairs(fs.getDirectoryItems(overlayPath(ctx, at.key)) or {}) do
|
|
add(name)
|
|
end
|
|
end
|
|
end
|
|
table.sort(out)
|
|
return out
|
|
end
|
|
|
|
-- ------- buffered file handles
|
|
|
|
local function openBuffer(ctx, path, mode)
|
|
mode = tostring(mode or "r"):gsub("b", "")
|
|
local writable = mode:find("[wa+]") ~= nil
|
|
local body
|
|
if mode:sub(1, 1) == "w" then
|
|
body = ""
|
|
else
|
|
body = readPath(ctx, path)
|
|
if not body then
|
|
if not writable then return nil, "could not open " .. tostring(path) end
|
|
body = ""
|
|
end
|
|
end
|
|
local state = {
|
|
ctx = ctx, path = path, buf = body, pos = 1,
|
|
writable = writable, closed = false, dirty = false,
|
|
}
|
|
if mode:sub(1, 1) == "a" then state.pos = #body + 1 end
|
|
return state
|
|
end
|
|
|
|
local function bufferFlush(state)
|
|
if not (state.writable and state.dirty) then return true end
|
|
local ok, err = writePath(state.ctx, state.path, state.buf)
|
|
if ok then state.dirty = false end
|
|
return ok, err
|
|
end
|
|
|
|
local function bufferWrite(state, text)
|
|
if not state.writable then return false, "file is not open for writing" end
|
|
local head = state.buf:sub(1, state.pos - 1)
|
|
if #head < state.pos - 1 then head = head .. string.rep("\0", state.pos - 1 - #head) end
|
|
local tail = state.buf:sub(state.pos + #text)
|
|
state.buf = head .. text .. tail
|
|
state.pos = state.pos + #text
|
|
state.dirty = true
|
|
return true
|
|
end
|
|
|
|
local function bufferRead(state, fmt)
|
|
if fmt == nil then fmt = "*l" end
|
|
if type(fmt) == "number" then
|
|
if fmt == 0 then return state.pos <= #state.buf and "" or nil end
|
|
if state.pos > #state.buf then return nil end
|
|
local chunk = state.buf:sub(state.pos, state.pos + fmt - 1)
|
|
state.pos = state.pos + #chunk
|
|
return chunk
|
|
end
|
|
fmt = tostring(fmt):gsub("^%*", "")
|
|
if fmt == "a" then
|
|
local rest = state.buf:sub(state.pos)
|
|
state.pos = #state.buf + 1
|
|
return rest
|
|
end
|
|
if fmt == "l" or fmt == "L" then
|
|
if state.pos > #state.buf then return nil end
|
|
local nl = state.buf:find("\n", state.pos, true)
|
|
local line
|
|
if nl then
|
|
line = state.buf:sub(state.pos, fmt == "L" and nl or nl - 1)
|
|
state.pos = nl + 1
|
|
else
|
|
line = state.buf:sub(state.pos)
|
|
state.pos = #state.buf + 1
|
|
end
|
|
return line
|
|
end
|
|
if fmt == "n" then
|
|
local rest = state.buf:sub(state.pos)
|
|
local text, after = rest:match("^%s*(%-?%d+%.?%d*[eE]?[-+]?%d*)()")
|
|
if not text then return nil end
|
|
state.pos = state.pos + after - 1
|
|
return tonumber(text)
|
|
end
|
|
return nil
|
|
end
|
|
|
|
local function bufferSeek(state, whence, offset)
|
|
whence = whence or "cur"
|
|
offset = offset or 0
|
|
if whence == "set" then state.pos = offset + 1
|
|
elseif whence == "cur" then state.pos = state.pos + offset
|
|
elseif whence == "end" then state.pos = #state.buf + offset + 1
|
|
else return nil, "bad seek base" end
|
|
if state.pos < 1 then state.pos = 1 end
|
|
return state.pos - 1
|
|
end
|
|
|
|
local function bufferClose(state)
|
|
if state.closed then return true end
|
|
local ok, err = bufferFlush(state)
|
|
state.closed = true
|
|
return ok, err
|
|
end
|
|
|
|
local function ioFile(ctx, path, mode)
|
|
local state, err = openBuffer(ctx, path, mode)
|
|
if not state then return nil, err end
|
|
local file = {}
|
|
function file:read(...)
|
|
local count = select("#", ...)
|
|
if count == 0 then return bufferRead(state, "*l") end
|
|
local out = {}
|
|
for i = 1, count do out[i] = bufferRead(state, (select(i, ...))) end
|
|
return unpack(out, 1, count)
|
|
end
|
|
function file:write(...)
|
|
for i = 1, select("#", ...) do
|
|
local value = select(i, ...)
|
|
local ok, writeErr = bufferWrite(state, tostring(value))
|
|
if not ok then return nil, writeErr end
|
|
end
|
|
return file
|
|
end
|
|
function file:lines(fmt)
|
|
return function() return bufferRead(state, fmt or "*l") end
|
|
end
|
|
function file:seek(whence, offset) return bufferSeek(state, whence, offset) end
|
|
function file:flush() bufferFlush(state) return file end
|
|
function file:close() return bufferClose(state) end
|
|
function file:setvbuf() return true end
|
|
return file
|
|
end
|
|
|
|
local function loveFile(ctx, path, mode)
|
|
local state = nil
|
|
local file = {}
|
|
function file:open(openMode)
|
|
local opened, err = openBuffer(ctx, path, openMode or mode or "r")
|
|
if not opened then return false, err end
|
|
state = opened
|
|
return true
|
|
end
|
|
function file:isOpen() return state ~= nil and not state.closed end
|
|
function file:read(bytes)
|
|
if not state and not file:open("r") then return nil, 0 end
|
|
local body = bytes and bufferRead(state, bytes) or bufferRead(state, "*a")
|
|
if not body then return nil, 0 end
|
|
return body, #body
|
|
end
|
|
function file:write(data, size)
|
|
if not state and not file:open(mode or "w") then return false end
|
|
data = tostring(data)
|
|
if size then data = data:sub(1, size) end
|
|
local ok, err = bufferWrite(state, data)
|
|
if not ok then return false, err end
|
|
bufferFlush(state)
|
|
return true
|
|
end
|
|
function file:lines()
|
|
if not state then file:open("r") end
|
|
return function() return state and bufferRead(state, "*l") or nil end
|
|
end
|
|
function file:seek(offset)
|
|
if not state then return false end
|
|
bufferSeek(state, "set", offset or 0)
|
|
return true
|
|
end
|
|
function file:tell() return state and (state.pos - 1) or 0 end
|
|
function file:getSize()
|
|
if state then return #state.buf end
|
|
local body = readPath(ctx, path)
|
|
return body and #body or 0
|
|
end
|
|
function file:flush() if state then bufferFlush(state) end return true end
|
|
function file:close()
|
|
if state then bufferClose(state) end
|
|
state = nil
|
|
return true
|
|
end
|
|
function file:getFilename() return path end
|
|
function file:getMode() return mode or "c" end
|
|
function file:setBuffer() return true end
|
|
if mode and mode ~= "c" then file:open(mode) end
|
|
return file
|
|
end
|
|
|
|
-- ------- the love.filesystem stand-in
|
|
|
|
local function realFilesystem()
|
|
return _G.love and _G.love.filesystem or nil
|
|
end
|
|
|
|
local function filesystemShim(ctx)
|
|
local fsShim = {}
|
|
|
|
local function readAdvice() return "reads now come from mod:read and mod.storage" end
|
|
|
|
function fsShim.read(a, b, c)
|
|
local path, size = a, b
|
|
if (a == "string" or a == "data") and type(b) == "string" then
|
|
path, size = b, c
|
|
end
|
|
note(ctx, "love.filesystem.read", readAdvice())
|
|
local body, err = readPath(ctx, path)
|
|
if not body then return nil, err end
|
|
if size and size >= 0 then body = body:sub(1, size) end
|
|
return body, #body
|
|
end
|
|
|
|
function fsShim.write(path, data, size)
|
|
note(ctx, "love.filesystem.write",
|
|
"writes are rerouted to this mod's private compat storage; migrate to mod.storage")
|
|
data = tostring(data)
|
|
if size then data = data:sub(1, size) end
|
|
local ok, err = writePath(ctx, path, data)
|
|
if not ok then return false, err end
|
|
return true
|
|
end
|
|
|
|
function fsShim.append(path, data, size)
|
|
note(ctx, "love.filesystem.append",
|
|
"writes are rerouted to this mod's private compat storage; migrate to mod.storage")
|
|
data = tostring(data)
|
|
if size then data = data:sub(1, size) end
|
|
local existing = readPath(ctx, path) or ""
|
|
local ok, err = writePath(ctx, path, existing .. data)
|
|
if not ok then return false, err end
|
|
return true
|
|
end
|
|
|
|
function fsShim.lines(path)
|
|
note(ctx, "love.filesystem.lines", readAdvice())
|
|
local body = readPath(ctx, path)
|
|
if not body then error("could not open " .. tostring(path), 2) end
|
|
local pos = 1
|
|
return function()
|
|
if pos > #body then return nil end
|
|
local nl = body:find("\n", pos, true)
|
|
local line
|
|
if nl then
|
|
line = body:sub(pos, nl - 1)
|
|
pos = nl + 1
|
|
else
|
|
line = body:sub(pos)
|
|
pos = #body + 1
|
|
end
|
|
return line
|
|
end
|
|
end
|
|
|
|
function fsShim.load(path)
|
|
note(ctx, "love.filesystem.load",
|
|
"the chunk is compiled into this mod's sandbox; prefer require or mod:read plus load")
|
|
local body = readPath(ctx, path)
|
|
if not body then return nil, "could not open " .. tostring(path) end
|
|
if not ctx.compile then return nil, "no sandbox is bound yet" end
|
|
return ctx.compile(body, "@" .. tostring(path))
|
|
end
|
|
|
|
function fsShim.getInfo(path, a, b)
|
|
local info = infoPath(ctx, path)
|
|
local filter = type(a) == "string" and a or nil
|
|
local into = type(a) == "table" and a or (type(b) == "table" and b or nil)
|
|
if not info then return nil end
|
|
if filter and info.type ~= filter then return nil end
|
|
if into then
|
|
into.type, into.size, into.modtime = info.type, info.size, info.modtime
|
|
return into
|
|
end
|
|
return info
|
|
end
|
|
|
|
function fsShim.getDirectoryItems(path)
|
|
note(ctx, "love.filesystem.getDirectoryItems", "use mod.assets:list")
|
|
return listPath(ctx, path)
|
|
end
|
|
|
|
function fsShim.createDirectory(path)
|
|
local at = classify(ctx, path)
|
|
if not (at and at.key) then return false end
|
|
local fs = persistFs(ctx)
|
|
if not (fs and fs.createDirectory) then return false end
|
|
ensureParent(fs, overlayPath(ctx, at.key) .. "/.")
|
|
fs.createDirectory(overlayPath(ctx, at.key))
|
|
return true
|
|
end
|
|
|
|
function fsShim.remove(path)
|
|
local at = classify(ctx, path)
|
|
if not (at and at.key) then return false end
|
|
return overlayRemove(ctx, at.key)
|
|
end
|
|
|
|
function fsShim.exists(path) return infoPath(ctx, path) ~= nil end
|
|
|
|
function fsShim.isFile(path)
|
|
local info = infoPath(ctx, path)
|
|
return info ~= nil and info.type == "file"
|
|
end
|
|
|
|
function fsShim.isDirectory(path)
|
|
local info = infoPath(ctx, path)
|
|
return info ~= nil and info.type == "directory"
|
|
end
|
|
|
|
function fsShim.getSize(path)
|
|
local info = infoPath(ctx, path)
|
|
if not info then return nil, "could not open " .. tostring(path) end
|
|
return info.size or 0
|
|
end
|
|
|
|
function fsShim.getLastModified(path)
|
|
local info = infoPath(ctx, path)
|
|
if not info then return nil, "could not open " .. tostring(path) end
|
|
return info.modtime or 0
|
|
end
|
|
|
|
local function virtual(call)
|
|
note(ctx, call, "paths are virtual now; everything under the returned root "
|
|
.. "lands in this mod's private compat storage")
|
|
return ctx.virtualRoot
|
|
end
|
|
|
|
function fsShim.getSaveDirectory() return virtual("love.filesystem.getSaveDirectory") end
|
|
function fsShim.getWorkingDirectory() return virtual("love.filesystem.getWorkingDirectory") end
|
|
function fsShim.getUserDirectory() return virtual("love.filesystem.getUserDirectory") end
|
|
function fsShim.getAppdataDirectory() return virtual("love.filesystem.getAppdataDirectory") end
|
|
function fsShim.getSourceBaseDirectory() return virtual("love.filesystem.getSourceBaseDirectory") end
|
|
function fsShim.getRealDirectory() return ctx.virtualRoot end
|
|
|
|
function fsShim.getIdentity() return ctx.modId end
|
|
|
|
function fsShim.setIdentity()
|
|
note(ctx, "love.filesystem.setIdentity",
|
|
"a mod cannot repoint the save directory; the call does nothing")
|
|
return false
|
|
end
|
|
|
|
function fsShim.getRequirePath() return "" end
|
|
function fsShim.setRequirePath() return false end
|
|
function fsShim.getCRequirePath() return "" end
|
|
function fsShim.setCRequirePath() return false end
|
|
|
|
function fsShim.mount()
|
|
note(ctx, "love.filesystem.mount",
|
|
"mounting is refused; ship the files inside your mod and use mod:read")
|
|
return false
|
|
end
|
|
fsShim.unmount = fsShim.mount
|
|
|
|
function fsShim.newFile(path, mode) return loveFile(ctx, path, mode) end
|
|
|
|
function fsShim.newFileData(a, b)
|
|
local real = realFilesystem()
|
|
if type(a) == "string" and b == nil then
|
|
note(ctx, "love.filesystem.newFileData", readAdvice())
|
|
local body = readPath(ctx, a)
|
|
if not body then return nil, "could not open " .. tostring(a) end
|
|
if real and real.newFileData then return real.newFileData(body, a) end
|
|
return nil, "no filesystem"
|
|
end
|
|
if real and real.newFileData then return real.newFileData(a, b) end
|
|
return nil, "no filesystem"
|
|
end
|
|
|
|
function fsShim.isFused()
|
|
local real = realFilesystem()
|
|
return real and real.isFused and real.isFused() or false
|
|
end
|
|
|
|
function fsShim.areSymlinksEnabled() return false end
|
|
function fsShim.setSymlinksEnabled() return false end
|
|
function fsShim.init() return false end
|
|
function fsShim.setSource() return false end
|
|
|
|
return setmetatable(fsShim, { __index = function(_, key)
|
|
note(ctx, "love.filesystem." .. tostring(key),
|
|
"there is no compat stand-in for it; use mod.storage or mod:read")
|
|
return nil
|
|
end })
|
|
end
|
|
|
|
-- ------- the rest of the removed surface
|
|
|
|
local function systemShim(ctx)
|
|
local function real() return _G.love and _G.love.system or nil end
|
|
-- tls* comes from the engine (Android JNI, or desktop gen1tls hung on
|
|
-- love.system at boot). Forward those; keep clipboard / openURL stubbed.
|
|
local TLS = {
|
|
tlsOpen = true, tlsStatus = true, tlsSend = true,
|
|
tlsReceive = true, tlsError = true, tlsClose = true,
|
|
}
|
|
local shim = {
|
|
getOS = function()
|
|
local sys = real()
|
|
return sys and sys.getOS and sys.getOS() or "Unknown"
|
|
end,
|
|
getPowerInfo = function()
|
|
local sys = real()
|
|
if not (sys and sys.getPowerInfo) then return "unknown", nil, nil end
|
|
return sys.getPowerInfo()
|
|
end,
|
|
getProcessorCount = function()
|
|
local sys = real()
|
|
return sys and sys.getProcessorCount and sys.getProcessorCount() or 1
|
|
end,
|
|
getClipboardText = function()
|
|
note(ctx, "love.system.getClipboardText",
|
|
"clipboard access stays sandboxed; the call returns an empty string")
|
|
return ""
|
|
end,
|
|
setClipboardText = function()
|
|
note(ctx, "love.system.setClipboardText",
|
|
"clipboard access stays sandboxed; the call does nothing")
|
|
return false
|
|
end,
|
|
openURL = function()
|
|
note(ctx, "love.system.openURL",
|
|
"launching a URL stays sandboxed; the call does nothing")
|
|
return false
|
|
end,
|
|
vibrate = function(...)
|
|
local sys = real()
|
|
if sys and sys.vibrate then return sys.vibrate(...) end
|
|
return false
|
|
end,
|
|
}
|
|
return setmetatable(shim, {
|
|
__index = function(_, key)
|
|
if not TLS[key] then return nil end
|
|
local sys = real()
|
|
return sys and sys[key]
|
|
end,
|
|
})
|
|
end
|
|
|
|
local function eventShim(ctx)
|
|
local function real() return _G.love and _G.love.event or nil end
|
|
local shim = {
|
|
quit = function()
|
|
note(ctx, "love.event.quit",
|
|
"a mod cannot close the game out from under the player; the call does nothing")
|
|
return false
|
|
end,
|
|
}
|
|
shim.push = function(name, ...)
|
|
if name == "quit" then return shim.quit() end
|
|
local ev = real()
|
|
if ev and ev.push then return ev.push(name, ...) end
|
|
return false
|
|
end
|
|
return setmetatable(shim, { __index = function(_, key)
|
|
local ev = real()
|
|
return ev and ev[key] or nil
|
|
end })
|
|
end
|
|
|
|
local function ioShim(ctx)
|
|
local stream = {
|
|
write = function(self, ...)
|
|
for i = 1, select("#", ...) do io.write(tostring((select(i, ...)))) end
|
|
return self
|
|
end,
|
|
flush = function(self) return self end,
|
|
close = function() return true end,
|
|
read = function() return nil end,
|
|
lines = function() return function() return nil end end,
|
|
seek = function() return 0 end,
|
|
setvbuf = function() return true end,
|
|
}
|
|
local out = setmetatable({}, { __index = stream })
|
|
local shim = {
|
|
open = function(path, mode)
|
|
note(ctx, "io.open",
|
|
"the handle is backed by mod:read and this mod's private compat storage")
|
|
local file, err = ioFile(ctx, path, mode)
|
|
if not file then return nil, err, 2 end
|
|
return file
|
|
end,
|
|
lines = function(path, fmt)
|
|
if path == nil then return function() return nil end end
|
|
note(ctx, "io.lines",
|
|
"the handle is backed by mod:read and this mod's private compat storage")
|
|
local file, err = ioFile(ctx, path, "r")
|
|
if not file then error(err, 2) end
|
|
return file:lines(fmt)
|
|
end,
|
|
close = function(file) if file and file.close then return file:close() end return true end,
|
|
type = function(file)
|
|
if type(file) == "table" and file.read and file.close then return "file" end
|
|
return nil
|
|
end,
|
|
read = function() return nil end,
|
|
write = function(...) return out:write(...) end,
|
|
input = function() return out end,
|
|
output = function() return out end,
|
|
stdout = out,
|
|
stderr = out,
|
|
stdin = setmetatable({}, { __index = stream }),
|
|
popen = function()
|
|
return refuse(ctx, "io.popen", "spawning a process is refused")
|
|
end,
|
|
tmpfile = function()
|
|
return ioFile(ctx, ctx.virtualRoot .. "/io_tmpfile", "w+")
|
|
end,
|
|
}
|
|
return shim
|
|
end
|
|
|
|
local function osShim(ctx)
|
|
local HOME = { HOME = true, APPDATA = true, LOCALAPPDATA = true,
|
|
USERPROFILE = true, XDG_DATA_HOME = true,
|
|
XDG_CONFIG_HOME = true, TMPDIR = true, TEMP = true, TMP = true }
|
|
return {
|
|
getenv = function(name)
|
|
note(ctx, "os.getenv",
|
|
"the environment is hidden; home-like names answer with this mod's virtual root")
|
|
if type(name) == "string" and HOME[name:upper()] then
|
|
return ctx.virtualRoot
|
|
end
|
|
return nil
|
|
end,
|
|
remove = function(path)
|
|
note(ctx, "os.remove", "the delete lands in this mod's private compat storage")
|
|
local at = classify(ctx, path)
|
|
if not (at and at.key and overlayRemove(ctx, at.key)) then
|
|
return nil, tostring(path) .. ": no such file"
|
|
end
|
|
return true
|
|
end,
|
|
rename = function(from, to)
|
|
note(ctx, "os.rename", "the move lands in this mod's private compat storage")
|
|
local body = readPath(ctx, from)
|
|
if not body then return nil, tostring(from) .. ": no such file" end
|
|
local ok, err = writePath(ctx, to, body)
|
|
if not ok then return nil, err end
|
|
local at = classify(ctx, from)
|
|
if at and at.key then overlayRemove(ctx, at.key) end
|
|
return true
|
|
end,
|
|
tmpname = function()
|
|
return ctx.virtualRoot .. "/tmp/os_tmpname"
|
|
end,
|
|
execute = function()
|
|
note(ctx, "os.execute", "running a command is refused")
|
|
return false
|
|
end,
|
|
exit = function()
|
|
note(ctx, "os.exit",
|
|
"a mod cannot end the process; the call does nothing")
|
|
return false
|
|
end,
|
|
setlocale = function() return "C" end,
|
|
}
|
|
end
|
|
|
|
local function packageShim(ctx)
|
|
local shim = { path = "", cpath = "", preload = {}, loaded = {}, loaders = {} }
|
|
return setmetatable(shim, {
|
|
__index = function(_, key)
|
|
note(ctx, "package." .. tostring(key),
|
|
"the module loader is sandboxed; use require for the supported engine modules")
|
|
return nil
|
|
end,
|
|
})
|
|
end
|
|
|
|
-- ------- love callbacks
|
|
|
|
local CALLBACKS = {
|
|
load = true, update = true, draw = true, quit = true, lowmemory = true,
|
|
threaderror = true, keypressed = true, keyreleased = true,
|
|
textinput = true, textedited = true, mousemoved = true,
|
|
mousepressed = true, mousereleased = true, wheelmoved = true,
|
|
mousefocus = true, touchpressed = true, touchreleased = true,
|
|
touchmoved = true, joystickadded = true, joystickremoved = true,
|
|
joystickpressed = true, joystickreleased = true, joystickaxis = true,
|
|
joystickhat = true, gamepadpressed = true, gamepadreleased = true,
|
|
gamepadaxis = true, focus = true, visible = true, resize = true,
|
|
filedropped = true, directorydropped = true, displayrotated = true,
|
|
audiodevicechanged = true, localechanged = true,
|
|
}
|
|
|
|
local REFUSED = {
|
|
run = "love.run is the engine's fixed-step loop; use mod.hooks and mod.events",
|
|
errorhandler = "love.errorhandler is how a crash reaches the player; "
|
|
.. "use mod.events",
|
|
}
|
|
|
|
-- ------- assembly
|
|
|
|
function LegacyCompat.new(opts)
|
|
opts = opts or {}
|
|
local ctx = {
|
|
modId = opts.modId or "mod",
|
|
modPath = opts.modPath,
|
|
fs = opts.fs,
|
|
game = opts.game,
|
|
compile = nil,
|
|
}
|
|
ctx.virtualRoot = "/pokeport/" .. ctx.modId
|
|
|
|
local filesystem = filesystemShim(ctx)
|
|
local io_ = ioShim(ctx)
|
|
local system = systemShim(ctx)
|
|
local event = eventShim(ctx)
|
|
|
|
local compat = {
|
|
ctx = ctx,
|
|
love = { filesystem = filesystem, system = system, event = event },
|
|
os = osShim(ctx),
|
|
globals = {
|
|
io = io_,
|
|
package = packageShim(ctx),
|
|
loadfile = function(path)
|
|
note(ctx, "loadfile",
|
|
"the chunk is compiled into this mod's sandbox; prefer require or mod:read")
|
|
local body = readPath(ctx, path)
|
|
if not body then return nil, "could not open " .. tostring(path) end
|
|
if not ctx.compile then return nil, "no sandbox is bound yet" end
|
|
return ctx.compile(body, "@" .. tostring(path))
|
|
end,
|
|
},
|
|
modules = {
|
|
io = io_,
|
|
["love.filesystem"] = filesystem,
|
|
["love.system"] = system,
|
|
["love.event"] = event,
|
|
},
|
|
}
|
|
|
|
compat.globals.dofile = function(path)
|
|
local chunk, err = compat.globals.loadfile(path)
|
|
if not chunk then error(err or ("could not open " .. tostring(path)), 2) end
|
|
return chunk()
|
|
end
|
|
|
|
function compat.module(name)
|
|
if type(name) ~= "string" then return nil end
|
|
local substitute = compat.modules[name]
|
|
if substitute then
|
|
note(ctx, ('require("%s")'):format(name),
|
|
"it now answers with the compat stand-in, not the real module")
|
|
return substitute
|
|
end
|
|
if name == "os" then
|
|
note(ctx, 'require("os")',
|
|
"it now answers with the compat stand-in, not the real module")
|
|
return compat.osTable
|
|
end
|
|
return nil
|
|
end
|
|
|
|
-- true when the assignment is a callback chain, which is what a mod
|
|
-- wrapping love.mousemoved did before the sandbox; false plus a reason for
|
|
-- the two names the engine owns, false alone for a module table.
|
|
function compat.assign(key, value)
|
|
if REFUSED[key] then
|
|
note(ctx, ("love.%s assignment"):format(key), REFUSED[key])
|
|
return false, ("[%s] %s"):format(ctx.modId, REFUSED[key])
|
|
end
|
|
if not CALLBACKS[key] then return false end
|
|
if value ~= nil and type(value) ~= "function" then return false end
|
|
note(ctx, ("love.%s assignment"):format(key),
|
|
"the callback lands on the real love table the way it did before the "
|
|
.. "sandbox; prefer mod.hooks and mod.events")
|
|
return true
|
|
end
|
|
|
|
function compat.bind(env, compile)
|
|
ctx.compile = compile
|
|
compat.osTable = env and env.os or nil
|
|
end
|
|
|
|
return compat
|
|
end
|
|
|
|
return LegacyCompat
|