From 2468d5042d1d253d8d6a9c701fe961971791c02a Mon Sep 17 00:00:00 2001 From: bryanthaboi Date: Thu, 20 Aug 2026 08:53:31 -0400 Subject: [PATCH] clean up and larger importer --- assets/launcher/bug.png | Bin 3151 -> 2714 bytes src/import/LauncherView.lua | 54 +++++++++++++++++++++++++++++++++-- src/import/RomImporter.lua | 34 ++++++++++++++++++---- src/mods/Manifest.lua | 4 +-- src/mods/RequiredImports.lua | 8 +++++- 5 files changed, 90 insertions(+), 10 deletions(-) diff --git a/assets/launcher/bug.png b/assets/launcher/bug.png index 6c7bc24d28a0dd4e4cb92dc72e097b3876f57216..263ca0d3f75e66dd86c88e1ec60548fdf8bfadf5 100644 GIT binary patch delta 2707 zcmV;E3T*Yy7@8H3BYz5ENkl-j0G=Tbz%v8_c!s*ixg#(DNBWwOU<@(%dVkjW?wd26Yanx0ngA)( z%xuxj8gd$m^u9A@g#`00VBd8igaPq)W_Dv{w^k7NBtQ&gG|g-oYatij0SPHGdO%Jd zCm<#C66PZzQlc2I!nXTisVdF&n6$q7pAju5#En>cOu2qK!3Ff>I zV)Tj-Lq!rGa(_5~&CIsUY|qR#&1?n#>tplRW_E67pYZoD*HijZ?j1`!7H5=nIr3V*i=4%|1hZ_MmRGkXC9EeSWa z&FnR}mljMiB{CM}q@_gu*37;K(e>2zaMGHhgN8zcX^39x6VSk>TMz|@K*|qhwu}EI z*z_4Vln5bL#^BbTh=<4#6Xbl(lOq2akpEv0ej5lkoVfS+zAq4c*J2UfRz(NYK!img zdmWs(hkrptw!rzff`~ehx(+0Dg-g|xOz?UI3SJ42a{dtrza{gn<8x>DeMJn~Ks z3W^=GP&XMtbnF1pF_4)e^0N?y$W{Uj@cAWt$0mrd2l>B+zq|7-<1<9MRm}=A@ib{? z4<>k>4RGUAaO6IeFl92GGb(5kr!A2avxe_!iGRsk&$vE1g_R%xovc1YOAp-L!Dx-( z$|eZW636To-!T-+ik3@XOjSK$OAveXx?Pb%z0<*(dYFsKIH>{w6!~3n^JiE)XJV@p zL9rlhRk%~FAacJFKtmVQISh3u;x*)PX=Wd>jten&mDrUa0NwpMHhTh*&;U_(!0k0mX7(r6)D}yN7FZ<+pq^zNNN$MjPt%g3W1eC58`%CS z;{V3@3$NQa72TZTP^HoViHM-K6O*Bjuqk%`xbLzt|96o!?md01;Um256!Gss6pA4A zKPo{0dbBksCLhUrvcM#SL^u5C1!dLwN-vrSG$>dF)vT5IYKWM-nXi5_6$Ks|&H3 zLa-JH=^N@;^8wa)fbay7+843Zhkv7Xik!s>%-NI}HjUDrg#Zk8F}ZStxc+}6!P1Z< zih8rI$coKBFoazPQ&6O+DO$rm?%*%{EuW#6KA0wlP@l2FfM(n?$u@l_8K*2^-N?!T zJkeCdlh!%}k!lh(rC#Vkk5E+2?F1|U$<|u73z5QczZdvSyw+GhWM}{Jv_K zPtDN}z`+@C(l3Glw=l(CbPsGkEGkXalya6t*rb%PYsslQpIMa=MeJpjD z2QW`pGvsu~0b?(xsNXzA%22}t=7c@gV3{4_=PV4sl<7xs{R!4_f`9Q2*}czrUm-ex zW#l{!*I2|G1lUsV_-hUBs<9`;LWrE-bc!pWS(Tr$P*)=8W^$bjPVMIP44`|4o)2?+^;C^|*WJAhlnD=G{h4*~G!H9f7o)DRTU_RiWZ216+ zovO!yQV96~&ydJ9SJn-Me86LakPmn`gnWQ!2>F1F5b^;bA21$5J|G)}e1L^~z(}qL z`2Y*~fJY4>AAgVyLO#GkKHy&v@&Oj|0a+mA19FDQ{6okGlmJ3Lz%zt=KoubGe851i z{8t?!gh#$e&1fm^2RxMug0CSTa8kzm0Sk}tnyO8)R^AUdEzA7?_Jv@todFg&>7sT6 zgS{nu*Qshj)g4Bbi`svH2**C|2Xvk5`Mfi5I;K^@(tn6n1sfcp-=0$maC@hqWD@&OA??|4V$J}z);GzwV1Ub=4#LB8(IVbIzaA6 zl$-w{tt;OG`Mme#el`~-DD6 zHSg|@DCxcv+nG0Ro;XC9)GKrnjQSj(6Yv3ue}5@r=Pgcz`;3B05P&{G$2kEBHmZG~ zub>~BhFB1yapn0 z@Ve_V6!@rXuRK*ZPIvaMj7!Y%7F>Uf)4;B|G&$!EK5g$IHhL*{j_FyyLMUSsqDTk( z%zv44L64x{bpa)M?u<_uZH#l_`l;;r$4r1k?)Bb*Ge1KNu$L!WT<*bf+m)Wq9DM49 z$$e!azYT(Zz<7^A>qeXMF$6K!R>fr_z(5=tUb^P(OL8TzfL>!<;qNVZ7FbpSoPq%C zj(+1j#iKC96MVb>qaNO=Pr=~dXuh_AM1T4Ut}dYX>4I1nV?+-du@)w3%1!_m*!+Xe z8-spS!h9sjf>=cS977{~93qu4PZ9kM@5oOfN|M4)T^5L(HuCimW@v3p`+I#Xt!svA zFVMmsS^F3X;6h$uiPCMhtRkrNh|3FnCqce?2n6s9fdHN%5WxJxKLR(z9S~5hI1B&) N002ovPDHLkV1fee1h)VH literal 3151 zcmZ{mdpHwp8^Fh64s$+)>3~eHL+^&Du{Gz@#t4=76~jm)#3ErMH1g)GIVLGlaws}E zjI@wUa*7;YgdEy(Xg0KO-*vs$cU|8f?{i(x^SiJ6zJJg2&-2HfhI6))l2DTX002@B z_SS9!`u=x|i3-kHTJ#A44*T1?VF7?xC;&i71^{@16lDnjAi)5DWnTaQK?4BxhnKu^ zH5U{_{haKq1r#i<)oZc#z)!lvjSz-#gwY8-7z_b}fz;Nk|4#@F3kVLp`tQOOJ4YpfaP)sKLQEyF)*&aZY6!v>bDI~lIuM6(rbsC1{u7Y3haA7ZE_)WG ze;{oC)Fvv7yvJ(+^yi}9-3pW|{X!9i0Wl{n4pS3GKCM^G?O}IXiuY;J#mEvRfa#l> z=tA&AGq^J4_NAgJ+uT1f+!&@T`ydo!Fun@C!n@?9E zPm4=8&!V_{ZTK2i7MsdgP*Ms7s`V$U>ijf>re(IIW$Dr8$u43aqbiP>WsfK4J$MrE zZkp*YiXzY@XH*&6O_l8g{!{@U>7ueUwbxloKugFOkR*x7(c2V|^U^DYry{HJ7`cYi zq9r?=puK|}7}UITlV3!hnwTyjR~3F?IbXGqHN$d0o30_MV8LQS@P|FIcywTSPPMBdnM-XLT$FU z4_X#Eo&y!Fyes)xJq2+QIrowFGx2xSEoA2;wh*aoygsdI6pK`nt8xfQIF>{FP{ta` zWnr!ruQLNcMLkWyujVkVZsa7@4HvrEjLb|^`z7C`2*$Gd7ifvLlWkP5&c|%-abK@pz_+RbTc}d?{RR_UsEp`I7AG5$z12(U)XR`?XZ! zCijjNX07FoSi5u!2&GU~5JDfrJ(C-`W#R$ht?JHt-x$vt0?%XAL173ID1}la~ETMWMErW!-dd9-weFi(eol7{1RSt%GObQsBE;Pf1i3Fh>>D! z_{jUEaAdma{cB0CN0Og(9o1fg#+NVxN9wV|lA0zP5);+EET*6JhM(F5%{xEW-b($V zMEqv)3cC9Z#r?M4+tHPSy)(UZ-4j2l)DPxAIKd$c>#GRvPxeZdu`w=Rr|>a&I~6u)Ka|8to>yCc9DA^0~#w4ORd50(BFADa3wgE$T9Me8ihL70uq3(OHh##tK zoS(g~`$(*GT1EnI3cs7>=-~Y^l{u$4(bbA=)4H)xB6L~ojUieGJkLPtG-B(h8K(>C zzvtKJ)b+EX+le8aU_3QUZAFApDf&0O=a{s3F*+xH^Tlo)g`kPgITIwv_FGJODpcq+0weMs+sZT54&-;2k% zAF%oaS}LgAY_%czYM{&R@l3!`0#78D$3flVOriXO`@H$>mLZf|*~H`Oyu(v?q2@(; z`!0W>^=r>5b*?pEYF8$gu3$N?;rSM` z4rWUmF#Yvawt940T=q5DMvR7M@vVFZpIp32tp1>d|?dr*U{Se zwhxP^-UJrH#4YOEjn`A}DSgaq_~UTnd1iz0oSNyK@a}ZgzNfJctB9TI!#0GV%Rg(M zJ|Lp1OalF%oVG{@aBQZMGuDM3b*b)FO}S~cpnnaUusF-Uw#Y|vm$=az<=-P?7Bq=TAXOos3v5hbn8 zoJ_EhT*TR=N-kx8myAKe>}8a%;(#jCqP5U9q|4Z6j~wE| zZTkTa*M&S#6hZS>pdfq>@jxAh)WiWiP&2(OPp+Vl75w_s}i+xXa?Y_?;-nRYgf6p@*E>9j~TDeg3s3Z|NYhU*XIHsW^sRu96^zFDe`1; zF@u?p-$}d#{o<~-3H@^miPV`fvfUm{9_LcN?M($KW{B7rp|1%eYwh*Wyc4y+c*^J0 zXZ1uQ?wx1l=hJUi_T0FAsxD#NI%&_P!tXm&gKPJtV|^>B%qtJ2{oABn2tNC*c82&x z#FXI;RraG0n&+_ix~EKZjKzB6|VJTlP7@omzt&MYWG}vY9y#hVxMG%H1(u`lthEylmTg2&V4I zECcc#R6+Bo6qcB%{Ih#JL7`PjW%z`XPRu`JE#8t%o*wNT%uu&^Ef}`}4mQr#Pb__t F{sUx&#YX@D diff --git a/src/import/LauncherView.lua b/src/import/LauncherView.lua index 9f001eb2..fba64c35 100644 --- a/src/import/LauncherView.lua +++ b/src/import/LauncherView.lua @@ -233,6 +233,22 @@ function LauncherView.clickAt(imp, x, y) imp._clickPt = { x = x, y = y } end +-- Event-driven click: a macOS trackpad tap delivers press+release inside one +-- frame, so update()'s love.mouse.isDown poll never sees it. Mint the click +-- from the press event under the poll's own suppression rules, and mark the +-- press seen so the poll cannot mint a second one when isDown does catch it. +function LauncherView.mousepressed(imp, x, y) + if not imp._flex then return end + local now = love.timer.getTime() + local touching = imp._touchAt ~= nil and next(imp._touchAt) ~= nil + if touching or now < (imp._suppressMouseUntil or 0) + or now < (imp._suppressClickUntil or 0) then + return + end + imp._clickPt = { x = x, y = y } + imp._prevMouseDown = true +end + -- Keyboard focus ring. Returns true when the key was consumed. Arrows arm -- the ring; Enter only activates a focused control once the user has actually -- used the arrows this session, so the long-standing "Enter plays the visible @@ -580,7 +596,7 @@ local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action) end local halfW, halfH = w / 2, h / 2 - local depth = math.max(8, w * 0.14) + local depth = math.max(6, w * 0.10) local project = function(px, py, pz) return cartProject(cx + pressX, cy + pressY, yaw, pitch, px * pressedScale, py * pressedScale, pz * pressedScale) @@ -623,6 +639,7 @@ local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action) cartPolygon({ mainFront[2], mainFront[3], mainBack[3], mainBack[2] }, side, 1) cartPolygon({ mainFront[3], mainFront[4], mainBack[4], mainBack[3] }, side, 1) cartPolygon({ mainFront[1], mainFront[2], mainBack[2], mainBack[1] }, side, 1) + cartPolygon({ mainFront[4], mainFront[1], mainBack[1], mainBack[4] }, side, 1) cartPolygon({ capFront[2], capFront[3], capBack[3], capBack[2] }, side, 1) cartPolygon({ capFront[1], capFront[2], capBack[2], capBack[1] }, side, 1) cartPolygon({ capFront[4], capFront[1], capBack[1], capBack[4] }, side, 1) @@ -632,6 +649,31 @@ local function cartridgeButton(imp, x, y, w, h, key, version, gameName, action) else cartPolygon(mainBack, side, 1) cartPolygon(capBack, side, 1) + -- The tri-wing security screw: a domed brass head with three teardrop + -- recesses pinwheeled at 120 degrees. + local backZ = -(depth + 0.8) + local sd = math.min(w, h) * 0.11 + cartPill(project, -sd * 0.62, -sd * 0.62, sd * 1.24, sd * 1.24, backZ, + { math.floor(shell[1] * 0.4), math.floor(shell[2] * 0.4), + math.floor(shell[3] * 0.4) }, 0.9) + cartPill(project, -sd / 2, -sd / 2, sd, sd, backZ - 0.4, + { 196, 186, 148 }, 1) + cartPill(project, -sd * 0.32, -sd * 0.32, sd * 0.64, sd * 0.64, + backZ - 0.6, { 220, 212, 178 }, 0.8) + local r = sd / 2 + for k = 0, 2 do + local a = -math.pi / 2 + k * (2 * math.pi / 3) + local ux, uy = math.cos(a), math.sin(a) + local vx, vy = -uy, ux + local r0, r1 = r * 0.16, r * 0.82 + local w0, w1 = r * 0.13, r * 0.3 + cartPolygon({ + { project(ux * r0 + vx * w0, uy * r0 + vy * w0, backZ - 0.8) }, + { project(ux * r0 - vx * w0, uy * r0 - vy * w0, backZ - 0.8) }, + { project(ux * r1 - vx * w1, uy * r1 - vy * w1, backZ - 0.8) }, + { project(ux * r1 + vx * w1, uy * r1 + vy * w1, backZ - 0.8) }, + }, { 112, 104, 76 }, 1) + end end if frontFacing then @@ -1119,7 +1161,7 @@ local function buildHeader(imp, m) end tx = tx + dropW + tabGap - for _, t in ipairs(tabs) do + local function headerTab(t) local w = tabH if tx > tabLeft and tx + w > tabRight then tx = tabLeft @@ -1132,6 +1174,11 @@ local function buildHeader(imp, m) btn(imp, tx, ty, w, tabH, t.key, "", o) tx = tx + w + tabGap end + -- The bug-report chip sits LAST, past the sync chip. + local bugTab + for _, t in ipairs(tabs) do + if t.id == "bug" then bugTab = t else headerTab(t) end + end do local w = tabH @@ -1153,6 +1200,7 @@ local function buildHeader(imp, m) end tx = tx + w + tabGap end + if bugTab then headerTab(bugTab) end -- `ty` has walked down with the wraps, so this stays correct at one row too. y = ty + tabH + math.floor(8 * m.s) @@ -2840,6 +2888,8 @@ local function buildConfirmModal(imp, m) imp:_setAllMods(true, true) elseif c.kind == "importOversize" then imp:_importSave(c.version, c.source, true) + elseif c.kind == "largeImport" then + imp:_importRequiredSource(c.modId, c.importId, c.source, true) else imp:_toggleMod(c.id, true, c.version) end diff --git a/src/import/RomImporter.lua b/src/import/RomImporter.lua index 4cbb2fc4..9ce176ff 100644 --- a/src/import/RomImporter.lua +++ b/src/import/RomImporter.lua @@ -2060,7 +2060,7 @@ function RomImporter:_importRequiredData(modId, importId, data) return nil end -function RomImporter:_importRequiredSource(modId, importId, source) +function RomImporter:_importRequiredSource(modId, importId, source, confirmed) local manifest = requiredManifest(self, modId) local spec = manifest and requiredSpec(manifest, importId) if not spec then @@ -2068,14 +2068,31 @@ function RomImporter:_importRequiredSource(modId, importId, source) self.modNotice = nil return nil end + local RequiredImports = require("src.mods.RequiredImports") local info = love.filesystem.getInfo(source, "file") local size = info and info.size or externalFileSize(source) - local sizeErr = require("src.mods.RequiredImports").sizeError(spec, size, false) + local sizeErr = RequiredImports.sizeError(spec, size, false) if sizeErr then requiredImportNotice(self, modId, importId, sizeErr) self.modNotice = nil return nil end + if not confirmed and type(size) == "number" + and size > RequiredImports.LARGE_WARN_BYTES then + self._modConfirm = { + kind = "largeImport", + modId = modId, importId = importId, source = source, + title = Strings("Large import"), + lines = { + Strings("This is a large import (%s).", + RequiredImports.sizeLabel(size)), + Strings("Please ensure you have enough space on your"), + Strings("device before doing this."), + }, + yesLabel = Strings("I understand"), + } + return nil + end local data = love.filesystem.read(source) if not data then data = readExternalPath(source) end if not data then @@ -2118,8 +2135,13 @@ function RomImporter:chooseRequiredImport(modId, importId) if name:sub(1, 1) ~= "." then local path = inbox .. "/" .. name local info = love.filesystem.getInfo(path, "file") - local sizeErr = info and require("src.mods.RequiredImports") - .sizeError(spec, info.size, false) + local RequiredImports = require("src.mods.RequiredImports") + local sizeErr = info + and RequiredImports.sizeError(spec, info.size, false) + if info and not sizeErr + and info.size > RequiredImports.LARGE_WARN_BYTES then + return self:_importRequiredSource(modId, importId, path) + end local data = not sizeErr and love.filesystem.read(path) or nil if data and self:_importRequiredData(modId, importId, data) then return end if sizeErr then lastError = sizeErr @@ -3098,8 +3120,10 @@ end -- multi-monitor coords). Same contract as PadCursor.yieldToPointer for -- the overlay hosts. Touch move/press/release must still reach -- FlexLove.touch* or scroll containers never drag on phones. -function RomImporter:mousepressed() +function RomImporter:mousepressed(x, y, button) self._padCursorActive = false + if button ~= 1 or not self._flex then return end + require("src.import.LauncherView").mousepressed(self, x, y) end function RomImporter:touchpressed(id, x, y, dx, dy, pressure) diff --git a/src/mods/Manifest.lua b/src/mods/Manifest.lua index 92544589..865b9f03 100644 --- a/src/mods/Manifest.lua +++ b/src/mods/Manifest.lua @@ -195,8 +195,8 @@ local function parseImports(value, field, required) if value == nil then return end assert(type(value) == "number" and value > 0 and value % 1 == 0, field .. " " .. label .. " must be a positive integer") - assert(value <= 128 * 1024 * 1024, - field .. " " .. label .. " exceeds the 128 MiB hard limit") + assert(value <= 2 * 1024 * 1024 * 1024, + field .. " " .. label .. " exceeds the 2 GiB hard limit") end validateSize(size, "size") validateSize(maxSize, "max_size") diff --git a/src/mods/RequiredImports.lua b/src/mods/RequiredImports.lua index 167c263f..806e916e 100644 --- a/src/mods/RequiredImports.lua +++ b/src/mods/RequiredImports.lua @@ -23,11 +23,17 @@ local function isRequired(spec) end RequiredImports.specs = allSpecs -RequiredImports.MAX_BYTES = 128 * 1024 * 1024 +RequiredImports.MAX_BYTES = 2 * 1024 * 1024 * 1024 +-- Past this, the launcher interposes a free-space warning before importing. +RequiredImports.LARGE_WARN_BYTES = 128 * 1024 * 1024 local function sizeLabel(bytes) + if bytes >= 1024 * 1024 * 1024 then + return ("%.1f GiB"):format(bytes / (1024 * 1024 * 1024)) + end return ("%.1f MiB"):format(bytes / (1024 * 1024)) end +RequiredImports.sizeLabel = sizeLabel -- Check size before a caller reads an external or stored file into one large -- Lua string. N64 sources may carry a 512-byte copier header, while stored