From 691c3adb376b43ca13f640799b53d35d22798ae2 Mon Sep 17 00:00:00 2001 From: Ryan Ward Date: Mon, 15 Jun 2026 23:31:09 -0700 Subject: [PATCH] added zip utils --- gui | 2 +- main.lua | 98 ++++++++++++++++++-- menus/board.lua | 61 +++++++++---- menus/settings.lua | 13 ++- menus/title.lua | 12 ++- templates/zoom.lua | 2 +- utils/zip.lua | 217 +++++++++++++++++++++++++++++++++++++++++++++ 7 files changed, 374 insertions(+), 31 deletions(-) create mode 100644 utils/zip.lua diff --git a/gui b/gui index e33a6f4..27995af 160000 --- a/gui +++ b/gui @@ -1 +1 @@ -Subproject commit e33a6f4e146ea3bc61b1621fd1cdecf48c60e40b +Subproject commit 27995af07f02c80cc99a0283f06d85a1bd2461f7 diff --git a/main.lua b/main.lua index ef211cd..c0a9f2e 100644 --- a/main.lua +++ b/main.lua @@ -1,11 +1,10 @@ local logger = require("utils/logger") local lovehandler = require("utils/love_file_handler") +local zip = require("utils/zip") -multi, thread = require("multi"):init({priority=true}) +local multi, thread = require("multi"):init({priority=true}) multi.setClock(require("socket").gettime) -- When on linux os.clock doesn't reture actual seconds the program has elapsed for -GLOBAL, THREAD = require("multi.integration.loveManager"):init() - - +local GLOBAL, THREAD = require("multi.integration.loveManager"):init() local fileHandler, logCtrl = lovehandler.new("logs/jeopardy.log", { max_size = 512 * 1024, -- rotate at 512 KB @@ -39,6 +38,9 @@ color.indexColor("header_font","#fef6eeda") color.indexColor("correct", "#52b11b") color.indexColor("skip", "#5d5d5d") color.indexColor("wrong", "#bd2626") +color.indexColor("shaderColA","rgb01(0.10, 0.12, 0.38)") +color.indexColor("shaderColB","rgb01(0.20, 0.22, 0.58)") +color.indexColor("shaderColC","rgb01(0.55, 0.58, 0.78)") require("gui.addons") @@ -56,7 +58,19 @@ multi.error = function(self, err) love.quit() end +function readFileData(path) + return io.open(path,"r"):read("*a") +end + function love.load() + -- local z = zip.new() + -- z:add("main.lua",readFileData("main.lua")) + -- z:add("package.bat",readFileData("package.bat")) + -- local data = z:build() + -- local file = io.open("test.zip","wb") + -- file:write(data) + -- file:close() + loadLog:info("Starting Jeopardy Game") loadLog:debug("Setting game identity to jeopardy") for i,v in ipairs(color.listColors(showAllColors)) do @@ -84,11 +98,83 @@ function love.load() end function love.update(dt) - gui.update(dt) - multi:uManager(dt) + gui.update(dt) + multi:uManager(dt) end function love.draw() gui.draw() end +-- custom shaders + +gui.NewShader("background", [[ + extern float time; + extern vec2 size; + extern vec3 colA; + extern vec3 colB; + extern vec3 colC; + + // Hash function (pseudo-random from 2D input) + float hash(vec2 p) { + p = fract(p * vec2(127.1, 311.7)); + p += dot(p, p + 19.19); + return fract(p.x * p.y); + } + + // Smooth value noise + float noise(vec2 p) { + vec2 i = floor(p); + vec2 f = fract(p); + // Quintic smoothstep + vec2 u = f * f * f * (f * (f * 6.0 - 15.0) + 10.0); + + float a = hash(i); + float b = hash(i + vec2(1.0, 0.0)); + float c = hash(i + vec2(0.0, 1.0)); + float d = hash(i + vec2(1.0, 1.0)); + + return mix(mix(a, b, u.x), mix(c, d, u.x), u.y); + } + + // Fractional Brownian Motion – layers of noise at different scales + float fbm(vec2 p) { + float value = 0.0; + float amplitude = 0.5; + float frequency = 1.0; + for (int i = 0; i < 6; i++) { + value += amplitude * noise(p * frequency); + frequency *= 2.0; + amplitude *= 0.5; + } + return value; + } + + vec4 effect(vec4 color, Image tex, vec2 texCoord, vec2 screenCoord) { + // Normalized UV with aspect ratio correction + vec2 uv = screenCoord / size; + + // Slow, asymmetric time offsets per axis for lava-lamp feel + float t1 = time * 0.008; + float t2 = time * 0.005; + + // Domain-warped FBM: feed FBM output back into itself + // This creates the bulging, self-folding blob shapes. + vec2 q = vec2( + fbm(uv + vec2(0.0, 0.0) + t1), + fbm(uv + vec2(5.2, 1.3) + t2) + ); + + vec2 r = vec2( + fbm(uv + 4.0 * q + vec2(1.7, 9.2) + t1 * 0.7), + fbm(uv + 4.0 * q + vec2(8.3, 2.8) + t2 * 0.9) + ); + + float n = fbm(uv + 4.0 * r + t1 * 0.4); + + vec3 col = mix(colA, colB, clamp(n * 2.0, 0.0, 1.0)); + col = mix(col, colC, clamp(n * 2.0 - 0.6, 0.0, 1.0)); + + return vec4(col, 1.0); + } +]]) \ No newline at end of file diff --git a/menus/board.lua b/menus/board.lua index 733cec6..e4e7215 100644 --- a/menus/board.lua +++ b/menus/board.lua @@ -38,7 +38,9 @@ local dd_enabled = false local applied_dd = false local board, question -local function cleanUp() +local function cleanUp(frameRef) + boardLog:debug("Cleaning up state and returning to title screen") + scoreboard:removeAllPlayers() activePlayer = nil handler_conn:Remove() playerList = {} @@ -55,6 +57,7 @@ local function cleanUp() applied_dd = false board = nil question = nil + frameRef:destroy() end local function pickUniqueIndices(t, count) @@ -189,16 +192,23 @@ end local function manageCosmetics(frame, cosmetics) boardLog:debug("Managing cosmetics") - if cosmetics["bg-img"] then - frame:setImage(cosmetics["bg-img"]) - else - frame:setImage("assets/images/background.png") - end if type(cosmetics.colors) == "table" then for i,v in pairs(cosmetics.colors) do color.reindexColor((i:gsub("-","_")),v) end end + if cosmetics["bg-img"] then + frame:setImage(cosmetics["bg-img"]) + frame:setShader() + else + -- frame:setImage("assets/images/background.png") + frame:setImage(nil) + frame:setShader(gui.SHADERS.background,{ + colA = color.shaderColA, + colB = color.shaderColB, + colC = color.shaderColC + }) + end end function gui:cleanup() @@ -210,7 +220,7 @@ function gui:cleanup() end function Menu(frame, path, deb) - local frameRef = frame:newFrame():fullFrame() + local frameRef = frame:newImageLabel():fullFrame() frameRef.visibility = 0 boardLog:info("Building board") if path:match(".zip") then @@ -262,10 +272,7 @@ function Menu(frame, path, deb) refresh.align = gui.ALIGN_CENTER refresh.color = color.green refresh:OnReleased(function() - boardLog:debug("Cleaning up state and returning to title screen") - scoreboard:removeAllPlayers() - cleanUp() - frameRef:destroy() + cleanUp(frameRef) menu.getMenu("title"):visible(true) menu.getMenu("board"):visible(frame, path, deb) end) @@ -284,6 +291,8 @@ function Menu(frame, path, deb) local function displayAnswer(q) dframe.text = q.answer + dframe.textColor = color.white + dframe.visible = true dframe.click = q["display-mode"] == "click" local sleep = 5 @@ -334,6 +343,7 @@ function Menu(frame, path, deb) OnExit = function(self) self:setShader() end, + shaderTime = {true}, OnReleased=function(self) if self.text == "Skip" then handler_conn:Fire() @@ -342,10 +352,11 @@ function Menu(frame, path, deb) return end local val = self.text == "Correct" - print(multiply) handler_conn:Fire(val, multiply) - if val then - reference:Unconnect() + if val then + if reference then + reference:Unconnect() + end handler.visible = false end end, @@ -368,7 +379,16 @@ function Menu(frame, path, deb) index = data.index local cosmetics = index.cosmetics or {} - manageCosmetics(frame, cosmetics) + cosmetics.colors = cosmetics.colors or {} + + for i,v in pairs(color.listColors()) do + if not cosmetics.colors[v] then + cosmetics.colors[v] = color.rgbToHex(color[v]) + end + end + + frameRef:shaderTime() + manageCosmetics(frameRef, cosmetics) board:fullFrame() question:fullFrame() @@ -407,7 +427,7 @@ function Menu(frame, path, deb) reset(function(q) question:cleanup() - manageCosmetics(frame, cosmetics) + manageCosmetics(frameRef, cosmetics) label.text = default_text if q["display-answer"] then displayAnswer(q) @@ -449,6 +469,13 @@ function Menu(frame, path, deb) t.category = v.name t.index = tier t.price = start + inc*(tier-1) + t:OnEnter(function(self) + self:setShader(gui.SHADERS.glow) + end) + t:OnExit(function(self) + self:setShader() + end) + t:shaderTime(true) t:OnReleased(boardUpdater:newFunction(function(self) if self.text == "" or GetActivePlayer() == nil then return end if dd_enabled then @@ -470,7 +497,7 @@ function Menu(frame, path, deb) setmetatable(q, {__index = globals}) if q == nil then boardLog:debug("Question contains no data: File: %s Category: %s Tier: %s",path,index.categories[cat].name,start + inc*(tier-1)) return end if q.cosmetics then - manageCosmetics(frame, q.cosmetics) + manageCosmetics(frameRef, q.cosmetics) end self.textVisibility = 0 local template = LoadTemplate(q.template, path) diff --git a/menus/settings.lua b/menus/settings.lua index ab36e44..461ed7f 100644 --- a/menus/settings.lua +++ b/menus/settings.lua @@ -4,6 +4,13 @@ local menu = require("gui.core.menus") -- local proc = gui:getProcessor():newProcessor("settings") function Menu(frame) - local background = frame:newImageLabel("assets/images/background.jpg") - background:fullFrame() -end \ No newline at end of file + local background = frame:newFrame():fullFrame() + background:setShader(gui.SHADERS.background,{ + colA = color.shaderColA, + colB = color.shaderColB, + colC = color.shaderColC + }) + background:shaderTime() +end + +return menu.registerMenu("settings", Menu) \ No newline at end of file diff --git a/menus/title.lua b/menus/title.lua index 0b29e51..9f59433 100644 --- a/menus/title.lua +++ b/menus/title.lua @@ -5,7 +5,8 @@ local proc = gui:getProcessor():newProcessor("main-title") local APP_VERSION = love.filesystem.read("version.txt") -local board = require("menus.board") +local boardMenu = require("menus.board") +local settingsMenu = require("menus.settings") local titleLog = log:child("title") @@ -59,16 +60,21 @@ function Menu(frame) play:OnReleased(function() titleLog:debug("Play button pressed") if love.filesystem.getInfo("quiz") then - menu.getMenu("board"):visible(true, "quiz", true) + boardMenu:visible(true, "quiz", true) return else gui:newFilePicker("File picker test",nil, {".zip"}, function(file) titleLog:info("File picked: %s", file) - menu.getMenu("board"):visible(true, file:gsub("\\","/"):gsub(love.filesystem.getSaveDirectory(),""):sub(2,-1)) + boardMenu:visible(true, file:gsub("\\","/"):gsub(love.filesystem.getSaveDirectory(),""):sub(2,-1)) end) end end) + settings:OnReleased(function() + titleLog:debug("Settings button pressed") + settingsMenu:visible(true) + end) + quit:OnReleased(function(self, x, y) print("Quit Pressed",x,y,self:getAbsolutes()) titleLog:debug("Quit button pressed") diff --git a/templates/zoom.lua b/templates/zoom.lua index 5d337d0..6418faa 100644 --- a/templates/zoom.lua +++ b/templates/zoom.lua @@ -52,7 +52,7 @@ local function index(window, q, callback) imageHolder:OnExit(function(self) self:setShader() end) - + imageHolder:shaderTime(true) imageHolder:OnReleased(function() diff --git a/utils/zip.lua b/utils/zip.lua new file mode 100644 index 0000000..39a7bdd --- /dev/null +++ b/utils/zip.lua @@ -0,0 +1,217 @@ +--[[ + zip.lua — A zip writer module for LÖVE 2D + ========================================== + Uses love.math.compress (zlib/DEFLATE) and a pure-Lua CRC-32 table. + No external dependencies required. + + Requires: LÖVE 11.0+ + + Usage: + local Zip = require("zip") + + local z = Zip.new() + z:add("level1.lua", love.filesystem.read("level1.lua")) + z:add("tiles/grass.png", love.filesystem.read("tiles/grass.png")) + local data = z:build() + love.filesystem.write("levels.zip", data) + + API: + Zip.new() → creates a new Zip writer + zip:add(path, data) → adds a file entry (path uses forward slashes) + zip:build() → returns the zip file as a string +]] + +local Zip = {} +Zip.__index = Zip + +-- ── helpers ────────────────────────────────────────────────────────────────── + +--- Pack bytes into a little-endian binary string. +local function pack_u16(n) + return string.char(n % 256, math.floor(n / 256) % 256) +end + +local function pack_u32(n) + return string.char( + n % 256, + math.floor(n / 256) % 256, + math.floor(n / 65536) % 256, + math.floor(n / 16777216) % 256 + ) +end + +-- LuaJIT (used by LOVE) provides the 'bit' library for bitwise operations. +local bxor = bit.bxor +local band = bit.band +local rshift = bit.rshift + +--- CRC-32 lookup table (standard IEEE 802.3 polynomial 0xEDB88320, reflected). +local _crc_table = (function() + local t = {} + for i = 0, 255 do + local c = i + for _ = 1, 8 do + if band(c, 1) == 1 then + c = bxor(rshift(c, 1), 0xEDB88320) + else + c = rshift(c, 1) + end + end + t[i] = c + end + return t +end)() + +--- Compute CRC-32 of a string (LuaJIT / LOVE compatible). +local function crc32(data) + local crc = 0xFFFFFFFF + for i = 1, #data do + local idx = band(bxor(crc, data:byte(i)), 0xFF) + crc = bxor(rshift(crc, 8), _crc_table[idx]) + end + return band(bxor(crc, 0xFFFFFFFF), 0xFFFFFFFF) +end + +--- Deflate-compress data using love.math.compress. +--- Returns compressed_string, or nil if compression made it larger. +local function deflate(data) + -- "zlib" wraps DEFLATE with a 2-byte header and 4-byte Adler-32 checksum. + -- For ZIP we need raw DEFLATE, which we get by stripping the zlib envelope. + -- love.math.compress( rawstring, format, level ) + -- love.data.compress( container, format, rawstring, level ) + local zlib_data = love.data.compress("string", "zlib", data, 6) -- level 6 = default + + -- Strip the 2-byte zlib header and 4-byte Adler-32 trailer + local raw_deflate = zlib_data:sub(3, -5) + + if #raw_deflate >= #data then + return nil -- not worth compressing + end + return raw_deflate +end + +--- MS-DOS date/time encoding for the current time. +local function dos_datetime() + local t = os.date("*t") + local date = ((t.year - 1980) * 512) + (t.month * 32) + t.day + local time = (t.hour * 2048) + (t.min * 32) + math.floor(t.sec / 2) + return pack_u16(time), pack_u16(date) +end + +-- ── Zip writer ──────────────────────────────────────────────────────────────── + +--- Create a new Zip writer instance. +function Zip.new() + return setmetatable({ _entries = {}, _offset = 0 }, Zip) +end + +--- Add a file to the zip. +-- @param path string Archive path, e.g. "maps/level1.lua". Use '/' separators. +-- @param data string Raw file contents. +function Zip:add(path, data) + assert(type(path) == "string", "path must be a string") + assert(type(data) == "string", "data must be a string") + assert(not path:match("^/"), "path must not start with '/'") + + local crc = crc32(data) + local original = #data + local compressed, method + + local deflated = deflate(data) + if deflated then + compressed = deflated + method = 8 -- DEFLATE + else + compressed = data + method = 0 -- Stored + end + + table.insert(self._entries, { + path = path, + data = data, -- uncompressed (kept for the central dir) + compressed = compressed, + method = method, + crc = crc, + size_orig = original, + size_comp = #compressed, + offset = self._offset, + }) + + -- Local file header = 30 bytes + filename length + compressed data length + self._offset = self._offset + 30 + #path + #compressed +end + +--- Build and return the full zip file as a binary string. +function Zip:build() + local parts = {} + + -- ── Local file headers + data ──────────────────────────────────────────── + for _, e in ipairs(self._entries) do + local dos_time, dos_date = dos_datetime() + + -- Local file header signature + parts[#parts+1] = "\x50\x4B\x03\x04" -- PK\3\4 + parts[#parts+1] = "\x14\x00" -- version needed: 2.0 + parts[#parts+1] = "\x00\x00" -- general purpose bit flag + parts[#parts+1] = pack_u16(e.method) + parts[#parts+1] = dos_time + parts[#parts+1] = dos_date + parts[#parts+1] = pack_u32(e.crc) + parts[#parts+1] = pack_u32(e.size_comp) + parts[#parts+1] = pack_u32(e.size_orig) + parts[#parts+1] = pack_u16(#e.path) -- filename length + parts[#parts+1] = "\x00\x00" -- extra field length + parts[#parts+1] = e.path + parts[#parts+1] = e.compressed + end + + local central_dir_offset = 0 + for _, p in ipairs(parts) do central_dir_offset = central_dir_offset + #p end + + -- ── Central directory ──────────────────────────────────────────────────── + local central_parts = {} + for _, e in ipairs(self._entries) do + local dos_time, dos_date = dos_datetime() + + central_parts[#central_parts+1] = "\x50\x4B\x01\x02" -- PK\1\2 + central_parts[#central_parts+1] = "\x14\x00" -- version made by: 2.0 + central_parts[#central_parts+1] = "\x14\x00" -- version needed: 2.0 + central_parts[#central_parts+1] = "\x00\x00" -- general purpose bit flag + central_parts[#central_parts+1] = pack_u16(e.method) + central_parts[#central_parts+1] = dos_time + central_parts[#central_parts+1] = dos_date + central_parts[#central_parts+1] = pack_u32(e.crc) + central_parts[#central_parts+1] = pack_u32(e.size_comp) + central_parts[#central_parts+1] = pack_u32(e.size_orig) + central_parts[#central_parts+1] = pack_u16(#e.path) -- filename length + central_parts[#central_parts+1] = "\x00\x00" -- extra field length + central_parts[#central_parts+1] = "\x00\x00" -- file comment length + central_parts[#central_parts+1] = "\x00\x00" -- disk number start + central_parts[#central_parts+1] = "\x00\x00" -- internal attributes + central_parts[#central_parts+1] = "\x00\x00\x00\x00" -- external attributes + central_parts[#central_parts+1] = pack_u32(e.offset) -- offset of local header + central_parts[#central_parts+1] = e.path + end + + local central_dir_size = 0 + for _, p in ipairs(central_parts) do central_dir_size = central_dir_size + #p end + + -- ── End of central directory record ───────────────────────────────────── + local num_entries = #self._entries + local eocd = table.concat({ + "\x50\x4B\x05\x06", -- PK\5\6 (EOCD signature) + "\x00\x00", -- disk number + "\x00\x00", -- disk with central dir start + pack_u16(num_entries), -- entries on this disk + pack_u16(num_entries), -- total entries + pack_u32(central_dir_size), -- central dir size + pack_u32(central_dir_offset), -- central dir offset + "\x00\x00", -- comment length + }) + + return table.concat(parts) + .. table.concat(central_parts) + .. eocd +end + +return Zip