CLOSES #223, CLOSES #233, CLOSES #236, CLOSES #240, CLOSES #241, CLOSES #249, CLOSES #252, CLOSES #255, CLOSES #257, CLOSES #258, CLOSES #263, CLOSES #265, CLOSES #274, CLOSES #275, CLOSES #276, CLOSES #279, CLOSES #280, CLOSES #282, CLOSES #283, CLOSES #287, CLOSES #291, CLOSES #292, CLOSES #293, CLOSES #301, CLOSES #304, CLOSES #315, CLOSES #316, CLOSES #317, CLOSES #321, CLOSES #322, CLOSES #330

This commit is contained in:
bryanthaboi
2026-07-28 10:29:05 -04:00
parent 3b1032cc63
commit f9f38d161f
103 changed files with 11157 additions and 838 deletions
+43
View File
@@ -75,6 +75,32 @@ local function resolveMkdir()
return mkdirFn
end
-- Lazily-resolved windowless rmdir, the mirror of resolveMkdir above:
-- function(absolutePath) or false when FFI is unavailable. Both syscalls
-- refuse a non-empty directory, so a caller has to delete the files first.
local rmdirFn = nil
local function resolveRmdir()
if rmdirFn ~= nil then return rmdirFn end
rmdirFn = false
local ok, ffi = pcall(require, "ffi")
if not ok then return rmdirFn end
if ffi.os == "Windows" then
pcall(ffi.cdef, "int RemoveDirectoryA(const char *lpPathName);")
local resolved = pcall(function() return ffi.C.RemoveDirectoryA end)
if resolved then
rmdirFn = function(path) pcall(ffi.C.RemoveDirectoryA, path) end
end
else
pcall(ffi.cdef, "int rmdir(const char *pathname);")
local resolved = pcall(function() return ffi.C.rmdir end)
if resolved then
rmdirFn = function(path) pcall(ffi.C.rmdir, path) end
end
end
return rmdirFn
end
-- Mount an external directory onto the physfs read path (appended, so the
-- game's own source always wins a name clash). Returns true on success.
--
@@ -229,6 +255,23 @@ function CacheFs.remove(rel)
love.filesystem.remove(rel)
end
-- Remove a single cache-relative directory once its files are gone. Needed
-- because os.remove cannot delete a directory on Windows and
-- love.filesystem.remove never reaches outside the save directory, so the
-- portable game folder gets the same FFI-syscall treatment as its mkdir
-- (issue #74: os.execute would flash a console window per call). Used by the
-- mod installer so an uninstall leaves nothing behind (#330).
function CacheFs.removeDir(rel)
rel = withPrefix(rel)
local root = CacheFs.root()
if root then
local rmdir = resolveRmdir()
if rmdir then rmdir(realPath(root, rel)) end
return
end
love.filesystem.remove(rel)
end
-- Remove the game-folder copy of a cache subtree before a fresh import, so a
-- cache-format bump does not leave orphaned files behind. No-op when the
-- portable cache is inactive (the save-directory copy is cleared by
+30 -1
View File
@@ -255,7 +255,36 @@ local function fileUrl(path)
return "file://" .. encoded
end
-- The native pickers below block the whole loop inside io.popen, and they are
-- opened straight out of mousepressed -- with the button still physically
-- down. SDL auto-captures the pointer for the length of a press (on X11 an
-- XGrabPointer with owner_events) and only drops that capture when it
-- processes the matching button-up, which it cannot do while we sit in popen
-- and never pump. The grab then outlives the click and every pointer event
-- over the file chooser is still routed to our window: the dialog draws and
-- keyboard-navigates (keyboard focus is a separate grab) but ignores the
-- mouse entirely -- issue #254 on Linux. Whether it bites is a race with how
-- long the click was held, which is why the same build picks one ROM fine and
-- then hangs the mouse on the next. So pump until no button is held, letting
-- SDL see the release and let go first; bounded, so a stuck button costs a
-- moment and never the launcher. pump() only drains OS events into LOVE's
-- queue -- it dispatches nothing -- so there is no reentry into mousepressed
-- and the release is still delivered normally on the next frame.
local function releasePointerGrab()
if not (love.mouse and love.mouse.isDown and love.event and love.event.pump
and love.timer) then
return
end
local deadline = love.timer.getTime() + 1
while love.mouse.isDown(1, 2, 3) do
love.event.pump()
if love.timer.getTime() > deadline then break end
love.timer.sleep(0.005)
end
end
local function commandOutput(command)
releasePointerGrab()
local pipe = io.popen(command, "r")
if not pipe then return nil end
local result = pipe:read("*a")
@@ -2476,7 +2505,7 @@ function RomImporter:_drawModsPanel(x, y, w, h)
love.graphics.printf(m.description, nx, ny + nameH + 6 * s, L.leftW, "left")
end
-- right cluster: status chip, toggle, Delete vertically centred
-- right cluster: status chip, toggle, Delete -- vertically centred
local clusterX = x + w - padH - L.clusterW
local clusterY = cy + (cardH - clusterH) / 2
local _, chipColor = modStatusChip(m.status)