From 07c7f2f1321567efc2a9bbbdc673cc4138e28327 Mon Sep 17 00:00:00 2001 From: "google-labs-jules[bot]" <161369871+google-labs-jules[bot]@users.noreply.github.com> Date: Mon, 10 Nov 2025 02:18:51 +0000 Subject: [PATCH 01/14] feat: Add tinker menu to memory panel This commit adds a tinker menu to the memory panel, allowing users to paste in JSON data and write it to the component's storage. A new UI layout has been created for the tinker menu, along with a JSON library to handle parsing and serialization. The memory panel script has been updated to include the necessary client-side logic for the tinker menu. --- Gui/Layouts/MemoryPanelGui.layout | 40 ++++ .../interactable/NumberLogic/MemoryPanel.lua | 50 +++++ Scripts/libs/json.lua | 196 ++++++++++++++++++ Scripts/libs/load_libs.lua | 1 + 4 files changed, 287 insertions(+) create mode 100644 Gui/Layouts/MemoryPanelGui.layout create mode 100644 Scripts/libs/json.lua diff --git a/Gui/Layouts/MemoryPanelGui.layout b/Gui/Layouts/MemoryPanelGui.layout new file mode 100644 index 0000000..2bda495 --- /dev/null +++ b/Gui/Layouts/MemoryPanelGui.layout @@ -0,0 +1,40 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index 16651e1..9a302cf 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -200,4 +200,54 @@ function MemoryPanel.client_setUvValue(self, value) if value > 255 then value = 255 end if value == math.huge then value = 0 end self.interactable:setUvFrameIndex(value) +end + +function MemoryPanel.client_onTinker(self, character, lookAt) + if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end + + local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) + + self.mem_gui_input = json.stringify(self.data) + + mem_gui:setText("ValueInput", self.mem_gui_input) + + mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") + + mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") + mem_gui:setOnCloseCallback("client_onGuiCloseCallback") + + mem_gui:open() + + self.mem_gui = mem_gui +end + +function MemoryPanel.client_onTextChangedCallback(self, widget, text) + local worked, data = pcall(json.parse, text) + + self.mem_gui_input = text + + local mem_gui = self.mem_gui + mem_gui:setVisible("ValueError", not worked) + mem_gui:setVisible("SaveWrittenVal", worked) +end + +function MemoryPanel.client_onGuiCloseCallback(self) + local mem_gui = self.mem_gui + if mem_gui and sm.exists(mem_gui) then + if mem_gui:isActive() then + mem_gui:close() + end + + mem_gui:destroy() + end + + self.mem_gui_input = nil + self.mem_gui = nil +end + +function MemoryPanel.client_gui_saveWrittenValue(self) + local worked, data = pcall(json.parse, self.mem_gui_input) + if worked then + self.network:sendToServer("server_setData", data) + end end \ No newline at end of file diff --git a/Scripts/libs/json.lua b/Scripts/libs/json.lua new file mode 100644 index 0000000..347d606 --- /dev/null +++ b/Scripts/libs/json.lua @@ -0,0 +1,196 @@ +--[[ json.lua + +A compact pure-Lua JSON library. + +The main functions are: json.stringify, json.parse. + +## json.stringify: + +This expects the following to be true of any tables being encoded: +* They only have string or number keys. Number keys must be represented as +strings in json; this is part of the json spec. +* They are not recursive. Such a structure cannot be specified in json. + +A Lua table is considered to be an array if and only if its set of keys is a +consecutive sequence of positive integers starting at 1. Arrays are encoded like +so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json +object, encoded like so: `{"key1": 2, "key2": false}`. + +Because the Lua nil value cannot be a key, and as a table value is considerd +equivalent to a missing key, there is no way to express the json "null" value in +a Lua table. The only way this will output "null" is if your entire input obj is +nil itself. + +An empty Lua table, {}, could be considered either a json object or array - +it's an ambiguous edge case. We choose to treat this as an object as it is the +more general type. + +To be clear, none of the above considerations is a limitation of this code. +Rather, it is what we get when we completely observe the json specification for +as arbitrary a Lua object as json is capable of expressing. + +## json.parse: + +This function parses json, with the exception that it does not pay attention to +\u-escaped unicode code points in strings. + +It is difficult for Lua to return null as a value. In order to prevent the loss +of keys with a null value in a json string, this function uses the one-off +table value json.null (which is just an empty table) to indicate null values. +This way you can check if a value is null with the conditional +`val == json.null`. +If you have control over the data and are using Lua, I would recommend just +avoiding null values in your data to begin with. + +--]] + +local json = {} + +-- Internal functions. + +local function kind_of(obj) + if type(obj) ~= 'table' then return type(obj) end + local i = 1 + for _ in pairs(obj) do + if obj[i] ~= nil then i = i + 1 else return 'table' end + end + if i == 1 then return 'table' else return 'array' end +end + +local function escape_str(s) + local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} + local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} + for i, c in ipairs(in_char) do + s = s:gsub(c, '\\' .. out_char[i]) + end + return s +end + +-- Returns pos, did_find; there are two cases: +-- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. +-- 2. Delimiter not found: pos = pos after leading space; did_find = false. +-- This throws an error if err_if_missing is true and the delim is not found. +local function skip_delim(str, pos, delim, err_if_missing) + pos = pos + #str:match('^%s*', pos) + if str:sub(pos, pos) ~= delim then + if err_if_missing then + error('Expected ' .. delim .. ' near position ' .. pos) + end + return pos, false + end + return pos + 1, true +end + +-- Expects the given pos to be the first character after the opening quote. +-- Returns val, pos; the returned pos is after the closing quote character. +local function parse_str_val(str, pos, val) + val = val or '' + local early_end_error = 'End of input found while parsing string.' + if pos > #str then error(early_end_error) end + local c = str:sub(pos, pos) + if c == '"' then return val, pos + 1 end + if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end + -- We must have a \ character. + local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} + local nextc = str:sub(pos + 1, pos + 1) + if not nextc then error(early_end_error) end + return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) +end + +-- Returns val, pos; the returned pos is after the number's final character. +local function parse_num_val(str, pos) + local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) + local val = tonumber(num_str) + if not val then error('Error parsing number at position ' .. pos .. '.') end + return val, pos + #num_str +end + + +-- Public values and functions. + +function json.stringify(obj, as_key) + local s = {} -- We'll build the string as an array of strings to be concatenated. + local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. + + if kind == 'array' then + if as_key then error('Can\'t encode array as key.') end + s[#s + 1] = '[' + for i, val in ipairs(obj) do + if i > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(val) + end + s[#s + 1] = ']' + elseif kind == 'table' then + if as_key then error('Can\'t encode table as key.') end + s[#s + 1] = '{' + for k, v in pairs(obj) do + if #s > 1 then s[#s + 1] = ', ' end + s[#s + 1] = json.stringify(k, true) + s[#s + 1] = ':' + s[#s + 1] = json.stringify(v) + end + s[#s + 1] = '}' + elseif kind == 'string' then + return '"' .. escape_str(obj) .. '"' + elseif kind == 'number' then + if as_key then return '"' .. tostring(obj) .. '"' end + return tostring(obj) + elseif kind == 'boolean' then + return tostring(obj) + elseif kind == 'nil' then + return 'null' + else + error('Unjsonifiable type: ' .. kind .. '.') + end + return table.concat(s) +end + +json.null = {} -- This is a one-off table to represent the null value. + +function json.parse(str, pos, end_delim) + pos = pos or 1 + if pos > #str then error('Reached unexpected end of input.') end + local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. + local first = str:sub(pos, pos) + + if first == '{' then -- Parse an object. + local obj, key, delim_found = {}, true, true + pos = pos + 1 + while true do + key, pos = json.parse(str, pos, '}') + if key == nil then return obj, pos end + if not delim_found then error('Comma missing between object items.') end + pos = skip_delim(str, pos, ':', true) -- true -> error if missing. + obj[key], pos = json.parse(str, pos) + pos, delim_found = skip_delim(str, pos, ',') + end + + elseif first == '[' then -- Parse an array. + local arr, val, delim_found = {}, true, true + pos = pos + 1 + while true do + val, pos = json.parse(str, pos, ']') + if val == nil then return arr, pos end + if not delim_found then error('Comma missing between array items.') end + arr[#arr + 1] = val + pos, delim_found = skip_delim(str, pos, ',') + end + + elseif first == '"' then -- Parse a string. + return parse_str_val(str, pos + 1) + elseif first == '-' or first:match('%d') then -- Parse a number. + return parse_num_val(str, pos) + elseif first == end_delim then -- End of an object or array. + return nil, pos + 1 + else -- Parse true, false, or null. + local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} + for lit_str, lit_val in pairs(literals) do + local lit_end = pos + #lit_str - 1 + if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end + end + local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) + error('Invalid json syntax starting at ' .. pos_info_str) + end +end + +return json diff --git a/Scripts/libs/load_libs.lua b/Scripts/libs/load_libs.lua index 3c6d409..42a4fe9 100644 --- a/Scripts/libs/load_libs.lua +++ b/Scripts/libs/load_libs.lua @@ -36,6 +36,7 @@ else end dofile "debugger.lua" +dofile "json.lua" dofile "color.lua" dofile "math.lua" dofile "other.lua" From a06860886f22c361730a8d82a4d50d204733efa2 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Mon, 10 Nov 2025 14:37:01 +1100 Subject: [PATCH 02/14] memory panel UI --- Gui/Layouts/MemoryPanelGui.layout | 4 +- .../interactable/NumberLogic/MemoryPanel.lua | 84 +++++++- Scripts/libs/json.lua | 196 ------------------ Scripts/libs/load_libs.lua | 1 - 4 files changed, 75 insertions(+), 210 deletions(-) delete mode 100644 Scripts/libs/json.lua diff --git a/Gui/Layouts/MemoryPanelGui.layout b/Gui/Layouts/MemoryPanelGui.layout index 2bda495..2d07375 100644 --- a/Gui/Layouts/MemoryPanelGui.layout +++ b/Gui/Layouts/MemoryPanelGui.layout @@ -9,11 +9,10 @@ - + - @@ -21,6 +20,7 @@ + diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index 9a302cf..9589189 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -58,11 +58,15 @@ function MemoryPanel.server_onCreate( self ) memorypanels[self.interactable.id] = self end -function MemoryPanel.server_setData(self, saveData) +function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData self.storage:save(saveData) + self.network:sendToClient(caller, "client_playSound") end +function MemoryPanel.client_playSound(self) + sm.audio.play("GUI Item released", self.shape:getWorldPosition()) +end function MemoryPanel.server_onFixedUpdate( self, dt ) local parents = self.interactable:getParents() @@ -129,6 +133,16 @@ function MemoryPanel.client_onCreate(self) self.time = 0 end +function MemoryPanel.client_onDestroy(self) + self:client_onGuiCloseCallback() +end + +function MemoryPanel.client_canInteract(self) + local tinker_key = mp_gui_getKeyBinding("Tinker", true) + sm.gui.setInteractionText("Press", tinker_key, "to open gui") + return true +end + function MemoryPanel.client_onFixedUpdate(self, dt) local parents = self.interactable:getParents() local address = 0 @@ -207,7 +221,12 @@ function MemoryPanel.client_onTinker(self, character, lookAt) local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - self.mem_gui_input = json.stringify(self.data) + local parts = {} + for k, v in pairs(self.data) do + parts[#parts + 1] = k .. ':' .. v + end + + self.mem_gui_input = table.concat(parts, ",") mem_gui:setText("ValueInput", self.mem_gui_input) @@ -222,13 +241,7 @@ function MemoryPanel.client_onTinker(self, character, lookAt) end function MemoryPanel.client_onTextChangedCallback(self, widget, text) - local worked, data = pcall(json.parse, text) - self.mem_gui_input = text - - local mem_gui = self.mem_gui - mem_gui:setVisible("ValueError", not worked) - mem_gui:setVisible("SaveWrittenVal", worked) end function MemoryPanel.client_onGuiCloseCallback(self) @@ -246,8 +259,57 @@ function MemoryPanel.client_onGuiCloseCallback(self) end function MemoryPanel.client_gui_saveWrittenValue(self) - local worked, data = pcall(json.parse, self.mem_gui_input) - if worked then - self.network:sendToServer("server_setData", data) + local input = self.mem_gui_input + if not input then return end + + -- sanitize allowed characters (already present) + input = input:gsub("[^0-9:,.-]", "") + + -- reflect sanitized input back to GUI + self.mem_gui:setText("ValueInput", input) + + -- parse entries of the form "addr:value" or just "value" (addr defaults to 0) + local data = {} + + for entry in input:gmatch("([^,]+)") do + -- sanitizer already trimmed whitespace, skip empty entries + if entry ~= "" then + local addrStr, valStr = entry:match("^([^:]+):(.+)$") + if not addrStr then + -- no explicit address, treat whole entry as value for address 0 + valStr = entry + addrStr = "0" + end + + -- parse address to integer, make positive + local addr = tonumber(addrStr) or 0 + if addr < 0 then addr = -addr end + addr = math.floor(addr) + + -- parse value (prefer numeric if looks like a number) + local numVal = tonumber(valStr) + local savedVal + if numVal and numVal == numVal then + savedVal = tostring(numVal) + else + -- valStr is already sanitized; if empty, default to "0" + if valStr == "" then + savedVal = "0" + else + savedVal = valStr + end + end + + -- assign into data table + data[addr] = savedVal + end end + + -- ensure there's at least a default entry (address 0) if nothing parsed + if next(data) == nil then + data[0] = "0" + end + + -- send parsed table to server to save + self.network:sendToServer("server_setData", data) end \ No newline at end of file diff --git a/Scripts/libs/json.lua b/Scripts/libs/json.lua deleted file mode 100644 index 347d606..0000000 --- a/Scripts/libs/json.lua +++ /dev/null @@ -1,196 +0,0 @@ ---[[ json.lua - -A compact pure-Lua JSON library. - -The main functions are: json.stringify, json.parse. - -## json.stringify: - -This expects the following to be true of any tables being encoded: -* They only have string or number keys. Number keys must be represented as -strings in json; this is part of the json spec. -* They are not recursive. Such a structure cannot be specified in json. - -A Lua table is considered to be an array if and only if its set of keys is a -consecutive sequence of positive integers starting at 1. Arrays are encoded like -so: `[2, 3, false, "hi"]`. Any other type of Lua table is encoded as a json -object, encoded like so: `{"key1": 2, "key2": false}`. - -Because the Lua nil value cannot be a key, and as a table value is considerd -equivalent to a missing key, there is no way to express the json "null" value in -a Lua table. The only way this will output "null" is if your entire input obj is -nil itself. - -An empty Lua table, {}, could be considered either a json object or array - -it's an ambiguous edge case. We choose to treat this as an object as it is the -more general type. - -To be clear, none of the above considerations is a limitation of this code. -Rather, it is what we get when we completely observe the json specification for -as arbitrary a Lua object as json is capable of expressing. - -## json.parse: - -This function parses json, with the exception that it does not pay attention to -\u-escaped unicode code points in strings. - -It is difficult for Lua to return null as a value. In order to prevent the loss -of keys with a null value in a json string, this function uses the one-off -table value json.null (which is just an empty table) to indicate null values. -This way you can check if a value is null with the conditional -`val == json.null`. -If you have control over the data and are using Lua, I would recommend just -avoiding null values in your data to begin with. - ---]] - -local json = {} - --- Internal functions. - -local function kind_of(obj) - if type(obj) ~= 'table' then return type(obj) end - local i = 1 - for _ in pairs(obj) do - if obj[i] ~= nil then i = i + 1 else return 'table' end - end - if i == 1 then return 'table' else return 'array' end -end - -local function escape_str(s) - local in_char = {'\\', '"', '/', '\b', '\f', '\n', '\r', '\t'} - local out_char = {'\\', '"', '/', 'b', 'f', 'n', 'r', 't'} - for i, c in ipairs(in_char) do - s = s:gsub(c, '\\' .. out_char[i]) - end - return s -end - --- Returns pos, did_find; there are two cases: --- 1. Delimiter found: pos = pos after leading space + delim; did_find = true. --- 2. Delimiter not found: pos = pos after leading space; did_find = false. --- This throws an error if err_if_missing is true and the delim is not found. -local function skip_delim(str, pos, delim, err_if_missing) - pos = pos + #str:match('^%s*', pos) - if str:sub(pos, pos) ~= delim then - if err_if_missing then - error('Expected ' .. delim .. ' near position ' .. pos) - end - return pos, false - end - return pos + 1, true -end - --- Expects the given pos to be the first character after the opening quote. --- Returns val, pos; the returned pos is after the closing quote character. -local function parse_str_val(str, pos, val) - val = val or '' - local early_end_error = 'End of input found while parsing string.' - if pos > #str then error(early_end_error) end - local c = str:sub(pos, pos) - if c == '"' then return val, pos + 1 end - if c ~= '\\' then return parse_str_val(str, pos + 1, val .. c) end - -- We must have a \ character. - local esc_map = {b = '\b', f = '\f', n = '\n', r = '\r', t = '\t'} - local nextc = str:sub(pos + 1, pos + 1) - if not nextc then error(early_end_error) end - return parse_str_val(str, pos + 2, val .. (esc_map[nextc] or nextc)) -end - --- Returns val, pos; the returned pos is after the number's final character. -local function parse_num_val(str, pos) - local num_str = str:match('^-?%d+%.?%d*[eE]?[+-]?%d*', pos) - local val = tonumber(num_str) - if not val then error('Error parsing number at position ' .. pos .. '.') end - return val, pos + #num_str -end - - --- Public values and functions. - -function json.stringify(obj, as_key) - local s = {} -- We'll build the string as an array of strings to be concatenated. - local kind = kind_of(obj) -- This is 'array' if it's an array or type(obj) otherwise. - - if kind == 'array' then - if as_key then error('Can\'t encode array as key.') end - s[#s + 1] = '[' - for i, val in ipairs(obj) do - if i > 1 then s[#s + 1] = ', ' end - s[#s + 1] = json.stringify(val) - end - s[#s + 1] = ']' - elseif kind == 'table' then - if as_key then error('Can\'t encode table as key.') end - s[#s + 1] = '{' - for k, v in pairs(obj) do - if #s > 1 then s[#s + 1] = ', ' end - s[#s + 1] = json.stringify(k, true) - s[#s + 1] = ':' - s[#s + 1] = json.stringify(v) - end - s[#s + 1] = '}' - elseif kind == 'string' then - return '"' .. escape_str(obj) .. '"' - elseif kind == 'number' then - if as_key then return '"' .. tostring(obj) .. '"' end - return tostring(obj) - elseif kind == 'boolean' then - return tostring(obj) - elseif kind == 'nil' then - return 'null' - else - error('Unjsonifiable type: ' .. kind .. '.') - end - return table.concat(s) -end - -json.null = {} -- This is a one-off table to represent the null value. - -function json.parse(str, pos, end_delim) - pos = pos or 1 - if pos > #str then error('Reached unexpected end of input.') end - local pos = pos + #str:match('^%s*', pos) -- Skip whitespace. - local first = str:sub(pos, pos) - - if first == '{' then -- Parse an object. - local obj, key, delim_found = {}, true, true - pos = pos + 1 - while true do - key, pos = json.parse(str, pos, '}') - if key == nil then return obj, pos end - if not delim_found then error('Comma missing between object items.') end - pos = skip_delim(str, pos, ':', true) -- true -> error if missing. - obj[key], pos = json.parse(str, pos) - pos, delim_found = skip_delim(str, pos, ',') - end - - elseif first == '[' then -- Parse an array. - local arr, val, delim_found = {}, true, true - pos = pos + 1 - while true do - val, pos = json.parse(str, pos, ']') - if val == nil then return arr, pos end - if not delim_found then error('Comma missing between array items.') end - arr[#arr + 1] = val - pos, delim_found = skip_delim(str, pos, ',') - end - - elseif first == '"' then -- Parse a string. - return parse_str_val(str, pos + 1) - elseif first == '-' or first:match('%d') then -- Parse a number. - return parse_num_val(str, pos) - elseif first == end_delim then -- End of an object or array. - return nil, pos + 1 - else -- Parse true, false, or null. - local literals = {['true'] = true, ['false'] = false, ['null'] = json.null} - for lit_str, lit_val in pairs(literals) do - local lit_end = pos + #lit_str - 1 - if str:sub(pos, lit_end) == lit_str then return lit_val, lit_end + 1 end - end - local pos_info_str = 'position ' .. pos .. ': ' .. str:sub(pos, pos + 10) - error('Invalid json syntax starting at ' .. pos_info_str) - end -end - -return json diff --git a/Scripts/libs/load_libs.lua b/Scripts/libs/load_libs.lua index 42a4fe9..3c6d409 100644 --- a/Scripts/libs/load_libs.lua +++ b/Scripts/libs/load_libs.lua @@ -36,7 +36,6 @@ else end dofile "debugger.lua" -dofile "json.lua" dofile "color.lua" dofile "math.lua" dofile "other.lua" From 19062a7034647bef922496be2346b3875cb5281f Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:13:39 +1100 Subject: [PATCH 03/14] Finished Menu --- Gui/Layouts/MemoryPanelGui.layout | 52 ++++----- .../interactable/NumberLogic/MemoryPanel.lua | 107 +++++++++++++----- 2 files changed, 95 insertions(+), 64 deletions(-) diff --git a/Gui/Layouts/MemoryPanelGui.layout b/Gui/Layouts/MemoryPanelGui.layout index 2d07375..e160bd3 100644 --- a/Gui/Layouts/MemoryPanelGui.layout +++ b/Gui/Layouts/MemoryPanelGui.layout @@ -1,40 +1,26 @@ - - - - - - - + + + + + + - - - - - - - + + + + + + + + - - - - - - - - - - - - + + + + - - - - - - - \ No newline at end of file + diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index 9589189..e751ca8 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -28,6 +28,50 @@ MemoryPanel.colorNormal = sm.color.new( 0x7F567Dff ) MemoryPanel.colorHighlight = sm.color.new( 0x9f7fa5ff ) MemoryPanel.poseWeightCount = 1 +local character_width_table = { + ["0"] = 13, + ["1"] = 11, + ["2"] = 12, + ["3"] = 12, + ["4"] = 12, + ["5"] = 12, + ["6"] = 12, + ["7"] = 11, + ["8"] = 12, + ["9"] = 12, + ["-"] = 9, + ["."] = 6, + [","] = 6, + [":"] = 6 +} + +local line_width = 400 + +function wrapInput(text) + local wrapped = {} + local total = 0 + + for i = 1, #text do + local ch = text:sub(i,i) + local w = character_width_table[ch] or 6 + + -- if adding this char would exceed the line width, start a new line (but avoid leading newline) + if total > 0 and (total + w) > line_width then + wrapped[#wrapped + 1] = "\n" + total = 0 + end + + wrapped[#wrapped + 1] = ch + total = total + w + end + + return table.concat(wrapped) +end + +function stripInput(text) + -- sanitize allowed characters: digits, colon, comma, period, minus + return text:gsub("[^%d:,%.-]", "") +end function MemoryPanel.server_onRefresh( self ) sm.isDev = true @@ -226,7 +270,7 @@ function MemoryPanel.client_onTinker(self, character, lookAt) parts[#parts + 1] = k .. ':' .. v end - self.mem_gui_input = table.concat(parts, ",") + self.mem_gui_input = wrapInput(table.concat(parts, ",")) mem_gui:setText("ValueInput", self.mem_gui_input) @@ -241,7 +285,10 @@ function MemoryPanel.client_onTinker(self, character, lookAt) end function MemoryPanel.client_onTextChangedCallback(self, widget, text) - self.mem_gui_input = text + self.mem_gui_input = wrapInput(stripInput(text)) + if text~=self.mem_gui_input then + self.mem_gui:setText("ValueInput", self.mem_gui_input) + end end function MemoryPanel.client_onGuiCloseCallback(self) @@ -259,49 +306,47 @@ function MemoryPanel.client_onGuiCloseCallback(self) end function MemoryPanel.client_gui_saveWrittenValue(self) - local input = self.mem_gui_input + local input = stripInput(self.mem_gui_input) if not input then return end -- sanitize allowed characters (already present) - input = input:gsub("[^0-9:,.-]", "") + wrapped = wrapInput(input) -- reflect sanitized input back to GUI - self.mem_gui:setText("ValueInput", input) + self.mem_gui:setText("ValueInput", wrapped) -- parse entries of the form "addr:value" or just "value" (addr defaults to 0) local data = {} for entry in input:gmatch("([^,]+)") do - -- sanitizer already trimmed whitespace, skip empty entries + -- trim surrounding whitespace + entry = entry:match("^%s*(.-)%s*$") or "" if entry ~= "" then - local addrStr, valStr = entry:match("^([^:]+):(.+)$") - if not addrStr then - -- no explicit address, treat whole entry as value for address 0 - valStr = entry - addrStr = "0" - end - - -- parse address to integer, make positive - local addr = tonumber(addrStr) or 0 - if addr < 0 then addr = -addr end - addr = math.floor(addr) - - -- parse value (prefer numeric if looks like a number) - local numVal = tonumber(valStr) - local savedVal - if numVal and numVal == numVal then - savedVal = tostring(numVal) - else - -- valStr is already sanitized; if empty, default to "0" - if valStr == "" then - savedVal = "0" + -- require exactly one ':' in the entry (allow empty lhs/rhs) + local addrStr, valStr = entry:match("^([^:]*):([^:]*)$") + if addrStr then + -- parse address to integer, make positive + local addr = tonumber(addrStr) or 0 + if addr < 0 then addr = -addr end + addr = math.floor(addr) + + -- parse value (prefer numeric if looks like a number) + local numVal = tonumber(valStr) + local savedVal + if numVal and numVal == numVal then + savedVal = tostring(numVal) else - savedVal = valStr + -- if empty, default to "0" + if valStr == "" then + savedVal = "0" + else + savedVal = valStr + end end - end - -- assign into data table - data[addr] = savedVal + -- assign into data table + data[addr] = savedVal + end end end From f13a927751f886ec106aadccaca2288846482dd5 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:36:15 +1100 Subject: [PATCH 04/14] Improved formatting and code GUI only created once. Removed old wrapping system. Newlines after commas. Spaces after colons. Last colon and comma ignored so you can delete it without the formatter blocking you. Display the new data when you press write. Moved the parsing to its own function. Changed label to "edit contents". Sorted keys and nicer stringify. not instead of == nil nitpick. Taller GUI. --- Gui/Layouts/MemoryPanelGui.layout | 12 +- .../interactable/NumberLogic/MemoryPanel.lua | 168 +++++++----------- 2 files changed, 74 insertions(+), 106 deletions(-) diff --git a/Gui/Layouts/MemoryPanelGui.layout b/Gui/Layouts/MemoryPanelGui.layout index e160bd3..daa5948 100644 --- a/Gui/Layouts/MemoryPanelGui.layout +++ b/Gui/Layouts/MemoryPanelGui.layout @@ -1,14 +1,14 @@ - - + + - - - + + + @@ -16,7 +16,7 @@ - + diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index e751ca8..ff94b94 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -28,49 +28,14 @@ MemoryPanel.colorNormal = sm.color.new( 0x7F567Dff ) MemoryPanel.colorHighlight = sm.color.new( 0x9f7fa5ff ) MemoryPanel.poseWeightCount = 1 -local character_width_table = { - ["0"] = 13, - ["1"] = 11, - ["2"] = 12, - ["3"] = 12, - ["4"] = 12, - ["5"] = 12, - ["6"] = 12, - ["7"] = 11, - ["8"] = 12, - ["9"] = 12, - ["-"] = 9, - ["."] = 6, - [","] = 6, - [":"] = 6 -} - -local line_width = 400 - -function wrapInput(text) - local wrapped = {} - local total = 0 - - for i = 1, #text do - local ch = text:sub(i,i) - local w = character_width_table[ch] or 6 - - -- if adding this char would exceed the line width, start a new line (but avoid leading newline) - if total > 0 and (total + w) > line_width then - wrapped[#wrapped + 1] = "\n" - total = 0 - end - - wrapped[#wrapped + 1] = ch - total = total + w - end - - return table.concat(wrapped) -end - -function stripInput(text) +function formatInput(text) -- sanitize allowed characters: digits, colon, comma, period, minus - return text:gsub("[^%d:,%.-]", "") + text = text:gsub("[^%d:,%.-]", "") + -- insert spaces after each colon not at the end + text = text:gsub(":([^$])", ": %1") + -- insert newlines after each comma not at the end + text = text:gsub(",([^$])", ",\n%1") + return text end function MemoryPanel.server_onRefresh( self ) @@ -106,6 +71,8 @@ function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData self.storage:save(saveData) self.network:sendToClient(caller, "client_playSound") + -- display the updated data + self.network:sendToClient(caller, "client_displayData") end function MemoryPanel.client_playSound(self) @@ -175,6 +142,16 @@ end function MemoryPanel.client_onCreate(self) self.mode = 0 self.time = 0 + + -- GUI Creation + local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) + + mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") + + mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") + mem_gui:setOnCloseCallback("client_onGuiCloseCallback") + + self.mem_gui = mem_gui end function MemoryPanel.client_onDestroy(self) @@ -183,7 +160,7 @@ end function MemoryPanel.client_canInteract(self) local tinker_key = mp_gui_getKeyBinding("Tinker", true) - sm.gui.setInteractionText("Press", tinker_key, "to open gui") + sm.gui.setInteractionText("Press", tinker_key, "to edit contents") return true end @@ -260,32 +237,33 @@ function MemoryPanel.client_setUvValue(self, value) self.interactable:setUvFrameIndex(value) end -function MemoryPanel.client_onTinker(self, character, lookAt) - if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end - - local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - +function MemoryPanel.client_displayData(self) local parts = {} + + local keys = {} for k, v in pairs(self.data) do - parts[#parts + 1] = k .. ':' .. v + if v ~= 0 or k == 0 then table.insert(keys, k) end end - self.mem_gui_input = wrapInput(table.concat(parts, ",")) + table.sort(keys) - mem_gui:setText("ValueInput", self.mem_gui_input) + for _, k in ipairs(keys) do + table.insert(parts, k .. ": " .. self.data[k]) + end - mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") + self.mem_gui_input = table.concat(parts, ",\n") - mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") - mem_gui:setOnCloseCallback("client_onGuiCloseCallback") - - mem_gui:open() + self.mem_gui:setText("ValueInput", self.mem_gui_input) +end - self.mem_gui = mem_gui +function MemoryPanel.client_onTinker(self, character, lookAt) + if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end + self.client_displayData(self) + self.mem_gui:open() end function MemoryPanel.client_onTextChangedCallback(self, widget, text) - self.mem_gui_input = wrapInput(stripInput(text)) + self.mem_gui_input = formatInput(text) if text~=self.mem_gui_input then self.mem_gui:setText("ValueInput", self.mem_gui_input) end @@ -293,68 +271,58 @@ end function MemoryPanel.client_onGuiCloseCallback(self) local mem_gui = self.mem_gui - if mem_gui and sm.exists(mem_gui) then - if mem_gui:isActive() then - mem_gui:close() - end - - mem_gui:destroy() + if mem_gui and sm.exists(mem_gui) and mem_gui:isActive() then + mem_gui:close() end self.mem_gui_input = nil - self.mem_gui = nil end -function MemoryPanel.client_gui_saveWrittenValue(self) - local input = stripInput(self.mem_gui_input) - if not input then return end - - -- sanitize allowed characters (already present) - wrapped = wrapInput(input) - - -- reflect sanitized input back to GUI - self.mem_gui:setText("ValueInput", wrapped) - - -- parse entries of the form "addr:value" or just "value" (addr defaults to 0) +function parseData(input) local data = {} for entry in input:gmatch("([^,]+)") do -- trim surrounding whitespace entry = entry:match("^%s*(.-)%s*$") or "" if entry ~= "" then - -- require exactly one ':' in the entry (allow empty lhs/rhs) - local addrStr, valStr = entry:match("^([^:]*):([^:]*)$") - if addrStr then - -- parse address to integer, make positive - local addr = tonumber(addrStr) or 0 - if addr < 0 then addr = -addr end - addr = math.floor(addr) - - -- parse value (prefer numeric if looks like a number) - local numVal = tonumber(valStr) - local savedVal - if numVal and numVal == numVal then - savedVal = tostring(numVal) - else - -- if empty, default to "0" - if valStr == "" then - savedVal = "0" - else - savedVal = valStr + -- require exactly one ':' and non-empty lhs and rhs + local addrStr, valStr = entry:match("^([^:]+):([^:]+)$") + if addrStr and valStr then + addrStr = addrStr:match("^%s*(.-)%s*$") or "" + valStr = valStr:match("^%s*(.-)%s*$") or "" + if addrStr ~= "" and valStr ~= "" then + -- parse address to integer, make positive + local addr = tonumber(addrStr) + if addr then + addr = math.floor(math.abs(addr)) + + -- parse value (prefer numeric if looks like a number) + local numVal = tonumber(valStr) + local savedVal + if numVal and numVal == numVal then + savedVal = tostring(numVal) + else + -- keep non-numeric string as-is + savedVal = valStr + end + + -- assign into data table + data[addr] = savedVal end end - - -- assign into data table - data[addr] = savedVal end end end -- ensure there's at least a default entry (address 0) if nothing parsed - if next(data) == nil then + if not next(data) then data[0] = "0" end + return data +end + +function MemoryPanel.client_gui_saveWrittenValue(self) -- send parsed table to server to save - self.network:sendToServer("server_setData", data) + self.network:sendToServer("server_setData", parseData(self.mem_gui_input)) end \ No newline at end of file From 67e3c54d8a1565fb607b1c2a47dfdfe03b12be21 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Tue, 11 Nov 2025 14:54:14 +1100 Subject: [PATCH 05/14] Persistent GUIs Add GUI instance persistence for counters and math blocks --- .../interactable/NumberLogic/CounterBlock.lua | 21 ++++------ .../interactable/NumberLogic/MathBlock.lua | 38 ++++++++----------- 2 files changed, 24 insertions(+), 35 deletions(-) diff --git a/Scripts/interactable/NumberLogic/CounterBlock.lua b/Scripts/interactable/NumberLogic/CounterBlock.lua index a7beb64..71ffd9a 100644 --- a/Scripts/interactable/NumberLogic/CounterBlock.lua +++ b/Scripts/interactable/NumberLogic/CounterBlock.lua @@ -159,16 +159,11 @@ end function CounterBlock.client_onGuiCloseCallback(self) local count_gui = self.counter_gui - if count_gui and sm.exists(count_gui) then - if count_gui:isActive() then + if count_gui and sm.exists(count_gui) and count_gui:isActive() then count_gui:close() - end - - count_gui:destroy() end self.counter_gui_input = nil - self.counter_gui = nil end function CounterBlock.client_gui_updateSavedValueText(self) @@ -197,7 +192,14 @@ end function CounterBlock.client_onTinker(self, character, lookAt) if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end + self.counter_gui:open() +end + +function CounterBlock.client_onCreate(self, dt) + self.frameindex = 0 + self.lastpower = 0 + -- Create GUI local count_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/CounterBlockGui.layout", false, { backgroundAlpha = 0.5 }) self.counter_gui_input = tostring(self.interactable.power) @@ -212,16 +214,9 @@ function CounterBlock.client_onTinker(self, character, lookAt) count_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") count_gui:setOnCloseCallback("client_onGuiCloseCallback") - count_gui:open() - self.counter_gui = count_gui end -function CounterBlock.client_onCreate(self, dt) - self.frameindex = 0 - self.lastpower = 0 -end - function CounterBlock.client_onDestroy(self) self:client_onGuiCloseCallback() end diff --git a/Scripts/interactable/NumberLogic/MathBlock.lua b/Scripts/interactable/NumberLogic/MathBlock.lua index ef39fb1..58e02e0 100644 --- a/Scripts/interactable/NumberLogic/MathBlock.lua +++ b/Scripts/interactable/NumberLogic/MathBlock.lua @@ -877,6 +877,18 @@ end function MathBlock.client_onCreate(self) self.uv = 0 self.network:sendToServer("sv_senduvtoclient") + + -- Create GUI + self.gui = mp_gui_createGuiFromLayout("$MOD_DATA/Gui/Layouts/MathBlock.layout", false, { backgroundAlpha = 0.5 }) + self.gui:setOnCloseCallback("client_onGuiDestroyCallback") + + for i = 0, 23 do + self.gui:setButtonCallback( "Operation" .. tostring( i ), "cl_onModeButtonClick" ) + end + + for i = 1, 3 do + self.gui:setButtonCallback( "Page" .. tostring( i ), "cl_onPageButtonClick" ) + end end function MathBlock.client_onDestroy(self) @@ -885,32 +897,14 @@ end function MathBlock.client_onGuiDestroyCallback(self) local s_gui = self.gui - if s_gui and sm.exists(s_gui) then - if s_gui:isActive() then - s_gui:close() - end - - s_gui:destroy() + if s_gui and sm.exists(s_gui) and s_gui:isActive() then + s_gui:close() end - - self.gui = nil end function MathBlock.client_onInteract(self, character, lookAt) - if lookAt == true then - self.gui = mp_gui_createGuiFromLayout("$MOD_DATA/Gui/Layouts/MathBlock.layout", false, { backgroundAlpha = 0.5 }) - self.gui:setOnCloseCallback("client_onGuiDestroyCallback") - - for i = 0, 23 do - self.gui:setButtonCallback( "Operation" .. tostring( i ), "cl_onModeButtonClick" ) - end - - for i = 1, 3 do - self.gui:setButtonCallback( "Page" .. tostring( i ), "cl_onPageButtonClick" ) - end - - local currentPage = self:cl_getModePage() - self:cl_paginate(currentPage) + if lookAt == true then + self:cl_paginate(self:cl_getModePage()) self.gui:open() end end From 6609e40dd987a72a63bcd1044c1e868a5f8acfd6 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:17:44 +1100 Subject: [PATCH 06/14] Cached GUI --- .../interactable/NumberLogic/MemoryPanel.lua | 23 ++++++++++--------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index ff94b94..b16dd14 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -28,6 +28,8 @@ MemoryPanel.colorNormal = sm.color.new( 0x7F567Dff ) MemoryPanel.colorHighlight = sm.color.new( 0x9f7fa5ff ) MemoryPanel.poseWeightCount = 1 +local mem_gui + function formatInput(text) -- sanitize allowed characters: digits, colon, comma, period, minus text = text:gsub("[^%d:,%.-]", "") @@ -143,15 +145,15 @@ function MemoryPanel.client_onCreate(self) self.mode = 0 self.time = 0 - -- GUI Creation - local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - - mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") + -- Create GUI + if not mem_gui then + mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") - mem_gui:setOnCloseCallback("client_onGuiCloseCallback") + mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") - self.mem_gui = mem_gui + mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") + mem_gui:setOnCloseCallback("client_onGuiCloseCallback") + end end function MemoryPanel.client_onDestroy(self) @@ -253,24 +255,23 @@ function MemoryPanel.client_displayData(self) self.mem_gui_input = table.concat(parts, ",\n") - self.mem_gui:setText("ValueInput", self.mem_gui_input) + mem_gui:setText("ValueInput", self.mem_gui_input) end function MemoryPanel.client_onTinker(self, character, lookAt) if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end self.client_displayData(self) - self.mem_gui:open() + mem_gui:open() end function MemoryPanel.client_onTextChangedCallback(self, widget, text) self.mem_gui_input = formatInput(text) if text~=self.mem_gui_input then - self.mem_gui:setText("ValueInput", self.mem_gui_input) + mem_gui:setText("ValueInput", self.mem_gui_input) end end function MemoryPanel.client_onGuiCloseCallback(self) - local mem_gui = self.mem_gui if mem_gui and sm.exists(mem_gui) and mem_gui:isActive() then mem_gui:close() end From e123c570e4dd1dcc6636f31b626b613c26f00a62 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Wed, 12 Nov 2025 13:24:26 +1100 Subject: [PATCH 07/14] Revert "Persistent GUIs" This reverts commit 67e3c54d8a1565fb607b1c2a47dfdfe03b12be21. --- .../interactable/NumberLogic/CounterBlock.lua | 21 ++++++---- .../interactable/NumberLogic/MathBlock.lua | 38 +++++++++++-------- 2 files changed, 35 insertions(+), 24 deletions(-) diff --git a/Scripts/interactable/NumberLogic/CounterBlock.lua b/Scripts/interactable/NumberLogic/CounterBlock.lua index 71ffd9a..a7beb64 100644 --- a/Scripts/interactable/NumberLogic/CounterBlock.lua +++ b/Scripts/interactable/NumberLogic/CounterBlock.lua @@ -159,11 +159,16 @@ end function CounterBlock.client_onGuiCloseCallback(self) local count_gui = self.counter_gui - if count_gui and sm.exists(count_gui) and count_gui:isActive() then + if count_gui and sm.exists(count_gui) then + if count_gui:isActive() then count_gui:close() + end + + count_gui:destroy() end self.counter_gui_input = nil + self.counter_gui = nil end function CounterBlock.client_gui_updateSavedValueText(self) @@ -192,14 +197,7 @@ end function CounterBlock.client_onTinker(self, character, lookAt) if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end - self.counter_gui:open() -end - -function CounterBlock.client_onCreate(self, dt) - self.frameindex = 0 - self.lastpower = 0 - -- Create GUI local count_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/CounterBlockGui.layout", false, { backgroundAlpha = 0.5 }) self.counter_gui_input = tostring(self.interactable.power) @@ -214,9 +212,16 @@ function CounterBlock.client_onCreate(self, dt) count_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") count_gui:setOnCloseCallback("client_onGuiCloseCallback") + count_gui:open() + self.counter_gui = count_gui end +function CounterBlock.client_onCreate(self, dt) + self.frameindex = 0 + self.lastpower = 0 +end + function CounterBlock.client_onDestroy(self) self:client_onGuiCloseCallback() end diff --git a/Scripts/interactable/NumberLogic/MathBlock.lua b/Scripts/interactable/NumberLogic/MathBlock.lua index 58e02e0..ef39fb1 100644 --- a/Scripts/interactable/NumberLogic/MathBlock.lua +++ b/Scripts/interactable/NumberLogic/MathBlock.lua @@ -877,18 +877,6 @@ end function MathBlock.client_onCreate(self) self.uv = 0 self.network:sendToServer("sv_senduvtoclient") - - -- Create GUI - self.gui = mp_gui_createGuiFromLayout("$MOD_DATA/Gui/Layouts/MathBlock.layout", false, { backgroundAlpha = 0.5 }) - self.gui:setOnCloseCallback("client_onGuiDestroyCallback") - - for i = 0, 23 do - self.gui:setButtonCallback( "Operation" .. tostring( i ), "cl_onModeButtonClick" ) - end - - for i = 1, 3 do - self.gui:setButtonCallback( "Page" .. tostring( i ), "cl_onPageButtonClick" ) - end end function MathBlock.client_onDestroy(self) @@ -897,14 +885,32 @@ end function MathBlock.client_onGuiDestroyCallback(self) local s_gui = self.gui - if s_gui and sm.exists(s_gui) and s_gui:isActive() then - s_gui:close() + if s_gui and sm.exists(s_gui) then + if s_gui:isActive() then + s_gui:close() + end + + s_gui:destroy() end + + self.gui = nil end function MathBlock.client_onInteract(self, character, lookAt) - if lookAt == true then - self:cl_paginate(self:cl_getModePage()) + if lookAt == true then + self.gui = mp_gui_createGuiFromLayout("$MOD_DATA/Gui/Layouts/MathBlock.layout", false, { backgroundAlpha = 0.5 }) + self.gui:setOnCloseCallback("client_onGuiDestroyCallback") + + for i = 0, 23 do + self.gui:setButtonCallback( "Operation" .. tostring( i ), "cl_onModeButtonClick" ) + end + + for i = 1, 3 do + self.gui:setButtonCallback( "Page" .. tostring( i ), "cl_onPageButtonClick" ) + end + + local currentPage = self:cl_getModePage() + self:cl_paginate(currentPage) self.gui:open() end end From 8bc382ef4a29b911560b3587101a901a68b60deb Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:40:21 +1100 Subject: [PATCH 08/14] Applied requested changes --- Scripts/interactable/NumberLogic/MemoryPanel.lua | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index b16dd14..fff34e0 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -72,15 +72,10 @@ end function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData self.storage:save(saveData) - self.network:sendToClient(caller, "client_playSound") -- display the updated data self.network:sendToClient(caller, "client_displayData") end -function MemoryPanel.client_playSound(self) - sm.audio.play("GUI Item released", self.shape:getWorldPosition()) -end - function MemoryPanel.server_onFixedUpdate( self, dt ) local parents = self.interactable:getParents() local address = 0 @@ -148,9 +143,7 @@ function MemoryPanel.client_onCreate(self) -- Create GUI if not mem_gui then mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") - mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") mem_gui:setOnCloseCallback("client_onGuiCloseCallback") end @@ -161,7 +154,7 @@ function MemoryPanel.client_onDestroy(self) end function MemoryPanel.client_canInteract(self) - local tinker_key = mp_gui_getKeyBinding("Tinker", true) + local tinker_key = sm.gui.getKeyBinding("Tinker", true) sm.gui.setInteractionText("Press", tinker_key, "to edit contents") return true end @@ -279,7 +272,7 @@ function MemoryPanel.client_onGuiCloseCallback(self) self.mem_gui_input = nil end -function parseData(input) +function MemoryPanel.parseData(input) local data = {} for entry in input:gmatch("([^,]+)") do @@ -324,6 +317,6 @@ function parseData(input) end function MemoryPanel.client_gui_saveWrittenValue(self) - -- send parsed table to server to save - self.network:sendToServer("server_setData", parseData(self.mem_gui_input)) + self.network:sendToServer("server_setData", self.parseData(self.mem_gui_input)) + sm.audio.play("GUI Item released", self.shape:getWorldPosition()) end \ No newline at end of file From a3f79d2c6d15464b9fc2fd99221e52534b103733 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:07:36 +1100 Subject: [PATCH 09/14] Replace tinker with interact --- Scripts/interactable/NumberLogic/MemoryPanel.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index fff34e0..eade104 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -154,8 +154,8 @@ function MemoryPanel.client_onDestroy(self) end function MemoryPanel.client_canInteract(self) - local tinker_key = sm.gui.getKeyBinding("Tinker", true) - sm.gui.setInteractionText("Press", tinker_key, "to edit contents") + local use_key = sm.gui.getKeyBinding("Use", true) + sm.gui.setInteractionText("Press", use_key, "to edit contents") return true end @@ -251,9 +251,9 @@ function MemoryPanel.client_displayData(self) mem_gui:setText("ValueInput", self.mem_gui_input) end -function MemoryPanel.client_onTinker(self, character, lookAt) +function MemoryPanel.client_onInteract(self, character, lookAt) if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end - self.client_displayData(self) + self:client_displayData() mem_gui:open() end From 361926d7702b1db215b5b70ec9de70b0c5fb787d Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Thu, 13 Nov 2025 19:46:03 +1100 Subject: [PATCH 10/14] Added Client Data Sync --- Scripts/interactable/NumberLogic/MemoryPanel.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index eade104..c1dd558 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -59,6 +59,7 @@ function MemoryPanel.server_onCreate( self ) else self.storage:save(self.data) end + self.network:setClientData(self.data) sm.interactable.setValue(self.interactable, value) if value ~= value then value = 0 end if math.abs(value) >= 3.3*10^38 then @@ -69,8 +70,13 @@ function MemoryPanel.server_onCreate( self ) memorypanels[self.interactable.id] = self end +function MemoryPanel.client_onClientDataUpdate( self, data ) + self.data = data +end + function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData + self.network:setClientData(self.data) self.storage:save(saveData) -- display the updated data self.network:sendToClient(caller, "client_displayData") @@ -125,6 +131,7 @@ function MemoryPanel.server_onFixedUpdate( self, dt ) end if saves then + self.network:setClientData(self.data) self.storage:save(self.data) end From a77b318a729d54a1b9d0e255ca810ae249a89194 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Fri, 14 Nov 2025 14:06:23 +1100 Subject: [PATCH 11/14] Safe GUI Creation --- .../interactable/NumberLogic/MemoryPanel.lua | 30 ++++++++++--------- 1 file changed, 16 insertions(+), 14 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index c1dd558..3e2a21e 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -28,8 +28,6 @@ MemoryPanel.colorNormal = sm.color.new( 0x7F567Dff ) MemoryPanel.colorHighlight = sm.color.new( 0x9f7fa5ff ) MemoryPanel.poseWeightCount = 1 -local mem_gui - function formatInput(text) -- sanitize allowed characters: digits, colon, comma, period, minus text = text:gsub("[^%d:,%.-]", "") @@ -146,14 +144,6 @@ end function MemoryPanel.client_onCreate(self) self.mode = 0 self.time = 0 - - -- Create GUI - if not mem_gui then - mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) - mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") - mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") - mem_gui:setOnCloseCallback("client_onGuiCloseCallback") - end end function MemoryPanel.client_onDestroy(self) @@ -255,11 +245,16 @@ function MemoryPanel.client_displayData(self) self.mem_gui_input = table.concat(parts, ",\n") - mem_gui:setText("ValueInput", self.mem_gui_input) + self.mem_gui:setText("ValueInput", self.mem_gui_input) end function MemoryPanel.client_onInteract(self, character, lookAt) if mp_deprecated_game_version or not lookAt or character:getLockingInteractable() then return end + local mem_gui = sm.gui.createGuiFromLayout("$CONTENT_DATA/Gui/Layouts/MemoryPanelGui.layout", false, { backgroundAlpha = 0.5 }) + mem_gui:setButtonCallback("SaveWrittenVal", "client_gui_saveWrittenValue") + mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") + mem_gui:setOnCloseCallback("client_onGuiCloseCallback") + self.mem_gui = mem_gui self:client_displayData() mem_gui:open() end @@ -267,18 +262,25 @@ end function MemoryPanel.client_onTextChangedCallback(self, widget, text) self.mem_gui_input = formatInput(text) if text~=self.mem_gui_input then - mem_gui:setText("ValueInput", self.mem_gui_input) + self.mem_gui:setText("ValueInput", self.mem_gui_input) end end function MemoryPanel.client_onGuiCloseCallback(self) - if mem_gui and sm.exists(mem_gui) and mem_gui:isActive() then - mem_gui:close() + local mem_gui = self.mem_gui + if mem_gui and sm.exists(mem_gui) then + if mem_gui:isActive() then + mem_gui:close() + end + + mem_gui:destroy() end self.mem_gui_input = nil + self.mem_gui = nil end + function MemoryPanel.parseData(input) local data = {} From 3be3d3a2c3ccded2d829614652a731cd2efa078d Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Fri, 14 Nov 2025 21:48:13 +1100 Subject: [PATCH 12/14] Requested data Connected clients ask the server for the data when opening the GUI --- Scripts/interactable/NumberLogic/MemoryPanel.lua | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index 3e2a21e..566df4c 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -44,6 +44,7 @@ function MemoryPanel.server_onRefresh( self ) end function MemoryPanel.server_onCreate( self ) + self.host = true local value = 0 self.data = {[0] = 0} local stored = self.storage:load() @@ -57,7 +58,6 @@ function MemoryPanel.server_onCreate( self ) else self.storage:save(self.data) end - self.network:setClientData(self.data) sm.interactable.setValue(self.interactable, value) if value ~= value then value = 0 end if math.abs(value) >= 3.3*10^38 then @@ -70,11 +70,11 @@ end function MemoryPanel.client_onClientDataUpdate( self, data ) self.data = data + self:client_displayData() end function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData - self.network:setClientData(self.data) self.storage:save(saveData) -- display the updated data self.network:sendToClient(caller, "client_displayData") @@ -129,7 +129,6 @@ function MemoryPanel.server_onFixedUpdate( self, dt ) end if saves then - self.network:setClientData(self.data) self.storage:save(self.data) end @@ -255,10 +254,18 @@ function MemoryPanel.client_onInteract(self, character, lookAt) mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") mem_gui:setOnCloseCallback("client_onGuiCloseCallback") self.mem_gui = mem_gui - self:client_displayData() + if self.host then + self:client_displayData() + else + self.network:sendToServer("server_sendAndDisplayData") + end mem_gui:open() end +function MemoryPanel.server_sendAndDisplayData(self) + self.network:setClientData(self.data) +end + function MemoryPanel.client_onTextChangedCallback(self, widget, text) self.mem_gui_input = formatInput(text) if text~=self.mem_gui_input then From cb30fc6b3d21ae8d2694c777b44e6c028e6dad15 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Fri, 14 Nov 2025 22:11:06 +1100 Subject: [PATCH 13/14] Fix approach --- .../interactable/NumberLogic/MemoryPanel.lua | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index 566df4c..b952d55 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -68,16 +68,9 @@ function MemoryPanel.server_onCreate( self ) memorypanels[self.interactable.id] = self end -function MemoryPanel.client_onClientDataUpdate( self, data ) - self.data = data - self:client_displayData() -end - function MemoryPanel.server_setData(self, saveData, caller) self.data = saveData self.storage:save(saveData) - -- display the updated data - self.network:sendToClient(caller, "client_displayData") end function MemoryPanel.server_onFixedUpdate( self, dt ) @@ -228,18 +221,18 @@ function MemoryPanel.client_setUvValue(self, value) self.interactable:setUvFrameIndex(value) end -function MemoryPanel.client_displayData(self) +function MemoryPanel.client_displayData(self, data) local parts = {} local keys = {} - for k, v in pairs(self.data) do + for k, v in pairs(data) do if v ~= 0 or k == 0 then table.insert(keys, k) end end table.sort(keys) for _, k in ipairs(keys) do - table.insert(parts, k .. ": " .. self.data[k]) + table.insert(parts, k .. ": " .. data[k]) end self.mem_gui_input = table.concat(parts, ",\n") @@ -255,15 +248,15 @@ function MemoryPanel.client_onInteract(self, character, lookAt) mem_gui:setOnCloseCallback("client_onGuiCloseCallback") self.mem_gui = mem_gui if self.host then - self:client_displayData() + self:client_displayData(self.data) else - self.network:sendToServer("server_sendAndDisplayData") + self.network:sendToServer("server_getAndDisplayData") end mem_gui:open() end -function MemoryPanel.server_sendAndDisplayData(self) - self.network:setClientData(self.data) +function MemoryPanel.server_getAndDisplayData(self, _, caller) + self.network:sendToClient(caller, "client_displayData", self.data) end function MemoryPanel.client_onTextChangedCallback(self, widget, text) @@ -333,6 +326,8 @@ function MemoryPanel.parseData(input) end function MemoryPanel.client_gui_saveWrittenValue(self) - self.network:sendToServer("server_setData", self.parseData(self.mem_gui_input)) + local data = self.parseData(self.mem_gui_input) + self:client_displayData(data) + self.network:sendToServer("server_setData", data) sm.audio.play("GUI Item released", self.shape:getWorldPosition()) end \ No newline at end of file From 5138fe79f693227845a5724f92c959ca8e509909 Mon Sep 17 00:00:00 2001 From: Dart Spark <57344246+rubyswolf@users.noreply.github.com> Date: Sun, 16 Nov 2025 12:34:12 +1100 Subject: [PATCH 14/14] use sm.isHost and simplify parseData Replaced usage of self.host with sm.isHost for host checks. Simplified parseData and enforced validation. --- Scripts/interactable/NumberLogic/MemoryPanel.lua | 16 ++++------------ 1 file changed, 4 insertions(+), 12 deletions(-) diff --git a/Scripts/interactable/NumberLogic/MemoryPanel.lua b/Scripts/interactable/NumberLogic/MemoryPanel.lua index b952d55..75acd4c 100644 --- a/Scripts/interactable/NumberLogic/MemoryPanel.lua +++ b/Scripts/interactable/NumberLogic/MemoryPanel.lua @@ -44,7 +44,6 @@ function MemoryPanel.server_onRefresh( self ) end function MemoryPanel.server_onCreate( self ) - self.host = true local value = 0 self.data = {[0] = 0} local stored = self.storage:load() @@ -247,7 +246,7 @@ function MemoryPanel.client_onInteract(self, character, lookAt) mem_gui:setTextChangedCallback("ValueInput", "client_onTextChangedCallback") mem_gui:setOnCloseCallback("client_onGuiCloseCallback") self.mem_gui = mem_gui - if self.host then + if sm.isHost then self:client_displayData(self.data) else self.network:sendToServer("server_getAndDisplayData") @@ -299,18 +298,11 @@ function MemoryPanel.parseData(input) if addr then addr = math.floor(math.abs(addr)) - -- parse value (prefer numeric if looks like a number) + -- parse value local numVal = tonumber(valStr) - local savedVal - if numVal and numVal == numVal then - savedVal = tostring(numVal) - else - -- keep non-numeric string as-is - savedVal = valStr + if numVal then + data[addr] = tostring(numVal) end - - -- assign into data table - data[addr] = savedVal end end end