plugin = { id = "generic-mapper", name = "Generic ASCII Map", version = "0.6.0", author = "Void", description = "Generic map widget (GMCP or map_start/map_end trigger feed)", settings = { saveState = true } } local mapWidget = nil local mapLines = {} local capturing = false local capturedLines = {} -- ========================= -- USER CONFIG (edit these) -- ========================= local USER_CONFIG = { title = "Generic Mapper", position = { x = 50, y = 50 }, size = { width = 450, height = 400 }, showZoomButtons = true, -- show clickable +/- font controls mapSourceMode = "auto", -- auto | gmcp | trigger gmcpPackage = "map", -- GMCP package to read (example: "map") mapStartToken = "{map_start}", -- ASCII capture start marker mapEndToken = "{map_end}", -- ASCII capture end marker commandName = "mapmode", variableFontSize = "generic_mapper_font_size", variableFontFamily = "generic_mapper_font_family", variableSourceMode = "generic_mapper_source_mode", logTag = "[GenericMapper]", fallbackFontFamily = "'Fira Mono Nerd Font', 'FiraMono Nerd Font', 'Fira Mono', 'Bitstream Vera Sans Mono', 'DejaVu Sans Mono', 'Courier New', 'Courier', monospace", fallbackFontSize = 14 } local mapSourceMode = USER_CONFIG.mapSourceMode local lastUpdateSource = "none" local fallbackFontFamily = USER_CONFIG.fallbackFontFamily local fallbackFontSize = USER_CONFIG.fallbackFontSize local defaultMapFontFamily = fallbackFontFamily local defaultMapFontSize = fallbackFontSize local mapFontFamily = defaultMapFontFamily local mapFontSize = defaultMapFontSize local zoomMinusRect = nil local zoomPlusRect = nil local function lower(s) if type(s) ~= "string" then return "" end return string.lower(s) end local function mode_allows_gmcp() return mapSourceMode == "auto" or mapSourceMode == "gmcp" end local function mode_allows_trigger() return mapSourceMode == "auto" or mapSourceMode == "trigger" end 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 if USER_CONFIG.showZoomButtons then 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) else zoomMinusRect = nil zoomPlusRect = nil end if #mapLines == 0 then drawText("Waiting for map data...", 10, mapY + lineHeight + 20, "#888888", fontString) drawText("Mode: " .. mapSourceMode .. " | Last: " .. lastUpdateSource, 10, mapY + lineHeight + 40, "#666666", (fontSize - 2) .. "px " .. mapFontFamily) 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, USER_CONFIG.variableFontSize, tostring(mapFontSize)) end end if type(family) == "string" and family ~= "" then mapFontFamily = family if type(_G["setVariable"]) == "function" then pcall(setVariable, USER_CONFIG.variableFontFamily, 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(USER_CONFIG.variableFontSize) local f = getVariable(USER_CONFIG.variableFontFamily) 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) if not mode_allows_gmcp() then return end local pkg = gmcp_package_name(v) if pkg and pkg ~= lower(USER_CONFIG.gmcpPackage) then return end local payload = "" if type(_G["getGMCPData"]) == "function" then local data = getGMCPData(USER_CONFIG.gmcpPackage) 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) lastUpdateSource = "gmcp" drawMap() end local lineCaptureTriggerId lineCaptureTriggerId = addTrigger("*", function(matches, line, wildcards, rawLine) if not capturing or not mode_allows_trigger() then return end 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(USER_CONFIG.mapStartToken, function() if not mode_allows_trigger() then return end capturedLines = {} capturing = true enableTrigger(lineCaptureTriggerId) end, { type = "substring", priority = 80, keepEvaluating = false, omitFromOutput = true, enabled = true }) local mapEndTriggerId = addTrigger(USER_CONFIG.mapEndToken, function() if not mode_allows_trigger() then return end capturing = false mapLines = capturedLines capturedLines = {} disableTrigger(lineCaptureTriggerId) lastUpdateSource = "trigger" drawMap() end, { type = "substring", priority = 80, keepEvaluating = false, omitFromOutput = true }) local function apply_source_mode() if type(_G["disableTrigger"]) == "function" then pcall(disableTrigger, lineCaptureTriggerId) end capturing = false capturedLines = {} if mode_allows_trigger() then if type(_G["enableTrigger"]) == "function" then pcall(enableTrigger, mapStartTriggerId) pcall(enableTrigger, mapEndTriggerId) end else if type(_G["disableTrigger"]) == "function" then pcall(disableTrigger, mapStartTriggerId) pcall(disableTrigger, mapEndTriggerId) end end drawMap() end local function set_source_mode(mode) local m = lower(mode) if m ~= "auto" and m ~= "gmcp" and m ~= "trigger" then return false end mapSourceMode = m if type(_G["setVariable"]) == "function" then pcall(setVariable, USER_CONFIG.variableSourceMode, mapSourceMode) end apply_source_mode() print(USER_CONFIG.logTag .. " map mode set to: " .. mapSourceMode) return true end local function init_source_mode() if type(_G["getVariable"]) == "function" then local saved = getVariable(USER_CONFIG.variableSourceMode) if type(saved) == "string" and saved ~= "" then local m = lower(saved) if m == "auto" or m == "gmcp" or m == "trigger" then mapSourceMode = m end end end apply_source_mode() end local function init_widgets() mapWidget = createWidget({ type = "canvas", title = USER_CONFIG.title, position = USER_CONFIG.position, size = USER_CONFIG.size, 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 USER_CONFIG.showZoomButtons and point_in_rect(x, y, zoomMinusRect) then set_map_font((mapFontSize or 14) - 1, nil) return end if USER_CONFIG.showZoomButtons and 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() init_source_mode() if type(_G["onGMCPUpdate"]) == "function" then pcall(_G["onGMCPUpdate"], USER_CONFIG.gmcpPackage, on_map_event) pcall(_G["onGMCPUpdate"], string.upper(USER_CONFIG.gmcpPackage:sub(1, 1)) .. USER_CONFIG.gmcpPackage:sub(2), on_map_event) end if type(_G["registerCommand"]) == "function" then pcall(registerCommand, USER_CONFIG.commandName, function(args) local raw = tostring(args or ""):gsub("^%s+", ""):gsub("%s+$", "") if raw == "" then print(USER_CONFIG.logTag .. " map mode: " .. mapSourceMode .. " (use: " .. USER_CONFIG.commandName .. " auto|gmcp|trigger)") return end if not set_source_mode(raw) then print(USER_CONFIG.logTag .. " invalid mode: " .. raw .. " (use auto|gmcp|trigger)") end end, "Set map input mode: auto|gmcp|trigger") end drawMap() return true end plugin.success = true plugin.triggers = { mapStartTriggerId, mapEndTriggerId, lineCaptureTriggerId } return plugin