plugin = { id = "voidmud", name = "VoidMUD", version = "0.5.0", author = "Void", description = "ASCII map widget + GMCP map feed", settings = { saveState = true } } local mapWidget = nil local mapLines = {} local capturing = false local capturedLines = {} local fallbackFontFamily = "'Fira Mono Nerd Font', 'FiraMono Nerd Font', 'Fira Mono', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace" local fallbackFontSize = 14 local defaultMapFontFamily = fallbackFontFamily local defaultMapFontSize = fallbackFontSize local mapFontFamily = defaultMapFontFamily local mapFontSize = defaultMapFontSize local zoomMinusRect = nil local zoomPlusRect = nil local function split_gmcp_lines(s) local out = {} local i = 1 local n = #s local buf = {} local c, c2, c3, c4 while i <= n do c = s:sub(i, i) c2 = (i + 1 <= n) and s:sub(i + 1, i + 1) or "" c3 = (i + 2 <= n) and s:sub(i + 2, i + 2) or "" c4 = (i + 3 <= n) and s:sub(i + 3, i + 3) or "" if c == "\r" and c2 == "\n" then out[#out + 1] = table.concat(buf) buf = {} i = i + 2 elseif c == "\n" or c == "\r" then out[#out + 1] = table.concat(buf) buf = {} i = i + 1 elseif c == "\\" and c2 == "r" and c3 == "\\" and c4 == "n" then out[#out + 1] = table.concat(buf) buf = {} i = i + 4 elseif c == "\\" and (c2 == "n" or c2 == "r") then out[#out + 1] = table.concat(buf) buf = {} i = i + 2 else buf[#buf + 1] = c i = i + 1 end end out[#out + 1] = table.concat(buf) return out end local function drawMap() local width = (type(_G["getCanvasWidth"]) == "function" and getCanvasWidth()) or 450 local height = (type(_G["getCanvasHeight"]) == "function" and getCanvasHeight()) or 400 if type(_G["setActiveWidget"]) == "function" then setActiveWidget(mapWidget) end if type(_G["clear"]) == "function" then clear("#000000") else drawRect(0, 0, width, height, "#000000") end local fontFamily = mapFontFamily local fontSize = mapFontSize + 2 local fontString = fontSize .. "px " .. fontFamily local charMetrics = measureText("X", fontString) local charWidth = charMetrics.width local lineHeight = charMetrics.height + 2 local startX = 10 local mapY = 10 local i local buttonSize = 22 local buttonGap = 6 local buttonY = 8 local plusX = width - buttonSize - 8 local minusX = plusX - buttonSize - buttonGap zoomMinusRect = { x = minusX, y = buttonY, w = buttonSize, h = buttonSize } zoomPlusRect = { x = plusX, y = buttonY, w = buttonSize, h = buttonSize } drawRect(zoomMinusRect.x, zoomMinusRect.y, zoomMinusRect.w, zoomMinusRect.h, "#1a1a1a", "#555555") drawRect(zoomPlusRect.x, zoomPlusRect.y, zoomPlusRect.w, zoomPlusRect.h, "#1a1a1a", "#555555") drawText("-", zoomMinusRect.x + 8, zoomMinusRect.y + 16, "#dddddd", "18px " .. mapFontFamily) drawText("+", zoomPlusRect.x + 6, zoomPlusRect.y + 16, "#dddddd", "18px " .. mapFontFamily) if #mapLines == 0 then drawText("Waiting for map data...", 10, mapY + lineHeight + 20, "#888888", fontString) return end for i, line in ipairs(mapLines) do local y = mapY + (i - 1) * lineHeight + lineHeight local x = startX local raw = tostring(line or "") local segments if string.find(raw, "\27%[", 1) == nil and string.find(raw, "%[[0-9;:]+m", 1) ~= nil then raw = raw:gsub("%[([0-9;:]+m)", "\27[%1") end if type(_G["parseAnsiText"]) == "function" then segments = parseAnsiText(raw) end if type(segments) == "table" and #segments > 0 then for _, segment in ipairs(segments) do local fontStyle = "" if segment.italic then fontStyle = fontStyle .. "italic " end local font = fontStyle .. fontSize .. "px " .. fontFamily local textLen = segment.charCount or #segment.text local textWidth = textLen * charWidth if segment.backgroundColor then drawRect(x, y - fontSize + 2, textWidth, lineHeight, segment.backgroundColor) end drawText(segment.text, x, y, segment.color, font) if segment.underline then drawLine(x, y + 2, x + textWidth, y + 2, segment.color, 1) end x = x + textWidth end else drawText(raw, x, y, "#d6f6d6", fontString) end end end local function point_in_rect(px, py, r) if type(r) ~= "table" then return false end if px < r.x or py < r.y then return false end if px > (r.x + r.w) or py > (r.y + r.h) then return false end return true end local function set_map_font(size, family) local n = tonumber(size) if n and n >= 8 and n <= 48 then mapFontSize = math.floor(n) if type(_G["setVariable"]) == "function" then pcall(setVariable, "voidmud_map_font_size", tostring(mapFontSize)) end end if type(family) == "string" and family ~= "" then mapFontFamily = family if type(_G["setVariable"]) == "function" then pcall(setVariable, "voidmud_map_font_family", mapFontFamily) end end drawMap() end local function detect_default_map_font() -- Intentionally fixed for map readability consistency. defaultMapFontFamily = fallbackFontFamily defaultMapFontSize = fallbackFontSize end local function init_font_settings() detect_default_map_font() mapFontFamily = defaultMapFontFamily mapFontSize = defaultMapFontSize if type(_G["getVariable"]) ~= "function" then return end local s = getVariable("voidmud_map_font_size") local f = getVariable("voidmud_map_font_family") if s and s ~= "" then local n = tonumber(s) if n then mapFontSize = math.floor(n) end end if f and f ~= "" then mapFontFamily = f end end local function gmcp_map_to_string(v) local function decode_json_string(s) if type(s) ~= "string" then return nil end local first = s:sub(1, 1) if first ~= "{" and first ~= "[" then return nil end if type(_G["json"]) ~= "table" or type(json.decode) ~= "function" then return nil end local ok, decoded = pcall(json.decode, s) if ok and type(decoded) == "table" then return decoded end return nil end if v == nil then return "" end if type(v) == "string" then local decoded = decode_json_string(v) if decoded ~= nil then return gmcp_map_to_string(decoded) end return v end if type(v) == "table" then if type(v.data) == "string" then local decoded = decode_json_string(v.data) if decoded ~= nil then return gmcp_map_to_string(decoded) end return v.data end if type(v.data) == "table" and type(v.data.data) == "string" then return v.data.data end if type(v.package) == "string" and string.lower(v.package) == "map" and v.data ~= nil then return gmcp_map_to_string(v.data) end end return "" end local function gmcp_package_name(v) if type(v) ~= "table" then return nil end if type(v.package) == "string" then return string.lower(v.package) end if type(v.Package) == "string" then return string.lower(v.Package) end return nil end local function on_map_event(v) local pkg = gmcp_package_name(v) if pkg and pkg ~= "map" then return end local payload = "" if type(_G["getGMCPData"]) == "function" then local data = getGMCPData("map") payload = gmcp_map_to_string(data) end if payload == "" then payload = gmcp_map_to_string(v) end if payload == "" then return end mapLines = split_gmcp_lines(payload) drawMap() end local lineCaptureTriggerId lineCaptureTriggerId = addTrigger("*", function(matches, line, wildcards, rawLine) local lineToCapture = rawLine or line or "" if lineToCapture ~= nil and lineToCapture ~= "" then table.insert(capturedLines, lineToCapture) end end, { type = "wildcard", priority = 1, keepEvaluating = false, omitFromOutput = true, enabled = false }) local mapStartTriggerId = addTrigger("{map_start}", function() capturedLines = {} capturing = true enableTrigger(lineCaptureTriggerId) end, { type = "substring", priority = 80, keepEvaluating = false, omitFromOutput = true, enabled = true }) local mapEndTriggerId = addTrigger("{map_end}", function() capturing = false mapLines = capturedLines capturedLines = {} disableTrigger(lineCaptureTriggerId) drawMap() end, { type = "substring", priority = 80, keepEvaluating = false, omitFromOutput = true }) local function init_widgets() mapWidget = createWidget({ type = "canvas", title = "Beyond the Void", position = { x = 50, y = 50 }, size = { width = 450, height = 400 }, resizable = true }) if mapWidget and type(_G["setActiveWidget"]) == "function" then setActiveWidget(mapWidget) end if type(_G["showWidget"]) == "function" then if mapWidget then pcall(showWidget, mapWidget) end end if mapWidget and type(_G["registerWidgetEvent"]) == "function" then registerWidgetEvent(mapWidget, "click", function(e) if type(e) ~= "table" then return end local x = tonumber(e.x) or -1 local y = tonumber(e.y) or -1 if point_in_rect(x, y, zoomMinusRect) then set_map_font((mapFontSize or 14) - 1, nil) return end if point_in_rect(x, y, zoomPlusRect) then set_map_font((mapFontSize or 14) + 1, nil) return end end) registerWidgetEvent(mapWidget, "resize", function() drawMap() end) end end function plugin.init() init_widgets() init_font_settings() if type(_G["onGMCPUpdate"]) == "function" then pcall(_G["onGMCPUpdate"], "map", on_map_event) pcall(_G["onGMCPUpdate"], "Map", on_map_event) end drawMap() return true end plugin.success = true plugin.triggers = { mapStartTriggerId, mapEndTriggerId, lineCaptureTriggerId } return plugin