-- Copyright (c) 2019-2022 David Vogel -- -- This software is released under the MIT License. -- https://opensource.org/licenses/MIT ----------------------- -- Load global stuff -- ----------------------- -- TODO: Wrap Noita utilities and wrap them into a table: https://stackoverflow.com/questions/9540732/loadfile-without-polluting-global-environment require("utilities") -- Loads Noita's utilities from `data/scripts/lib/utilitites.lua`. -------------------------- -- Load library modules -- -------------------------- ---------- -- Code -- ---------- ---Splits the given string to fit inside maxLength. ---@param gui userdata ---@param text string ---@param maxLength number -- In UI pixels. ---@return string[] local function splitString(gui, text, maxLength) local splitted = {} local first, rest = text, "" while first:len() > 0 do local width, height = GuiGetTextDimensions(gui, first, 1, 2) if width <= maxLength then table.insert(splitted, first) first, rest = rest, "" else first, rest = first:sub(1, -2), first:sub(-1, -1) .. rest end end return splitted end ---Returns unique IDs for the widgets. ---`_ResetID` has to be called every time before the UI is rebuilt. ---@return integer function UI:_GenID() self.CurrentID = (self.CurrentID or 0) + 1 return self.CurrentID end function UI:_ResetID() self.CurrentID = nil end ---Stops the UI from drawing for the next few frames. ---@param frames integer function UI:SuspendDrawing(frames) self.suspendFrames = math.max(self.suspendFrames or 0, frames) end function UI:_DrawToolbar() local gui = self.gui GuiZSet(gui, 0) GuiLayoutBeginHorizontal(gui, 2, 2, true, 2, 2) local captureMode = tostring(ModSettingGet("noita-mapcap.capture-mode")) if Capture.MapCapturingCtx:IsRunning() then local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/stop-16x16.png") GuiTooltip(gui, "Stop capture", "Stop the capturing process.\n \nRight click: Reset any modifications that this mod has done to Noita.") if clicked then Capture:StopCapturing() end if clickedRight then Message:ShowResetNoitaSettings() end else local clicked, clickedRight = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/record-16x16.png") GuiTooltip(gui, string.format("Start %s capture", captureMode), "Go into mod settings to configure the capturing process.\n \nRight click: Reset any modifications that this mod has done to Noita.") if clicked then Capture:StartCapturing() end if clickedRight then Message:ShowResetNoitaSettings() end end local clicked = GuiImageButton(gui, self:_GenID(), 0, 0, "", "mods/noita-mapcap/files/ui-gfx/open-output-16x16.png") GuiTooltip(gui, "Open output directory", "Reveals the output directory in your file browser.") if clicked then os.execute("start .\\mods\\noita-mapcap\\output\\") end GuiLayoutEnd(gui) end function UI:_DrawMessages(messages) local gui = self.gui -- Abort if there is no messages list. if not messages then return end local screenWidth, screenHeight = GuiGetScreenDimensions(gui) GuiZSet(gui, 0) -- Unfortunately you can't stack multiple layout containers with the same direction. -- So keep track of the y position manually. local posY = 60 for key, message in pairs(messages) do GuiZSet(gui, -10) GuiBeginAutoBox(gui) GuiLayoutBeginHorizontal(gui, 27, posY, true, 5, 0) posY = posY + 20 if message.Type == "warning" or message.Type == "error" then GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/warning-16x16.png", 1, 1, 0, 0, 0, "") elseif message.Type == "hint" or message.Type == "info" then GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "") else GuiImage(gui, self:_GenID(), 0, 0, "mods/noita-mapcap/files/ui-gfx/hint-16x16.png", 1, 1, 0, 0, 0, "") end GuiLayoutBeginVertical(gui, 0, 0, false, 0, 0) if type(message.Lines) == "table" then for _, line in ipairs(message.Lines) do for splitLine in tostring(line):gmatch("[^\n]+") do for _, splitLine in ipairs(splitString(gui, splitLine, screenWidth - 80)) do GuiText(gui, 0, 0, splitLine) posY = posY + 11 end end end end if type(message.Actions) == "table" then posY = posY + 11 for _, action in ipairs(message.Actions) do local clicked = GuiButton(gui, self:_GenID(), 0, 11, ">" .. action.Name .. " <") posY = posY + 11 if action.Hint or action.HintDesc then GuiTooltip(gui, action.Hint or "", action.HintDesc or "") end if clicked then local ok, err = pcall(action.Callback) if not ok then Message:ShowRuntimeError("MessageAction", "Message action error:", err) end messages[key] = nil end end end GuiLayoutEnd(gui) if not message.Autoclose then local clicked = GuiImageButton(gui, self:_GenID(), 5, 0, "", "mods/noita-mapcap/files/ui-gfx/dismiss-8x8.png") --GuiTooltip(gui, "Dismiss message", "") if clicked then messages[key] = nil end end GuiLayoutEnd(gui) GuiZSet(gui, -9) GuiEndAutoBoxNinePiece(gui, 5, 0, 0, false, 0, "data/ui_gfx/decorations/9piece0_gray.png", "data/ui_gfx/decorations/9piece0_gray.png") end end function UI:_DrawProgress() local gui = self.gui -- Check if there is progress to show. local state = Capture.MapCapturingCtx:GetState() if not state then return end local factor if state.Current and state.Max > 0 then factor = state.Current / state.Max end local width, height = GuiGetScreenDimensions(gui) local widthHalf, heightHalf = math.floor(width/2), math.floor(height/2) GuiZSet(gui, -20) local barWidth = width - 60 local y = heightHalf if factor then GuiImageNinePiece(gui, self:_GenID(), 30, y, barWidth, 9, 1, "mods/noita-mapcap/files/ui-gfx/progress-a.png", "mods/noita-mapcap/files/ui-gfx/progress-a.png") GuiImageNinePiece(gui, self:_GenID(), 30, y, math.floor(barWidth * factor + 0.5), 9, 1, "mods/noita-mapcap/files/ui-gfx/progress-b.png", "mods/noita-mapcap/files/ui-gfx/progress-b.png") GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter) GuiText(gui, widthHalf, y, string.format("%d of %d (%.1f%%)", state.Current, state.Max, factor*100)) y = y + 11 y = y + 15 end if state.WaitFrames then GuiOptionsAddForNextWidget(gui, GUI_OPTION.Align_HorizontalCenter) GuiText(gui, widthHalf, y, string.format("Waiting for %d frames.", state.WaitFrames)) y = y + 11 end end function UI:Draw() self.gui = self.gui or GuiCreate() local gui = self.gui -- Skip drawing if we are asked to do so. -- TODO: Find a way to susped UI drawing, but still being able to receive events if self.suspendFrames and self.suspendFrames > 0 then self.suspendFrames = self.suspendFrames - 1 return end self.suspendFrames = nil -- Reset ID generator. self:_ResetID() GuiStartFrame(gui) GuiIdPushString(gui, "noita-mapcap") self:_DrawToolbar() self:_DrawMessages(Message.List) self:_DrawProgress() GuiIdPop(gui) end