<?xml version="1.0" encoding="iso-8859-1"?><!DOCTYPE muclient><muclient><script><![CDATA[ -- this is all on the first line (including this comment) so that lua error messages will have correct line numbers require "json" local SELF_ID = GetPluginID() local GMCP_INTERFACE_ID = "c190b5fc9e83b05d8de996c3" local winID = SELF_ID .. "RossAsciiMap" local rossPluginsDir = GetPluginInfo(SELF_ID, 20) local RGBToInt = dofile(rossPluginsDir .. "RGBToInt.lua") local window = dofile(rossPluginsDir .. "window.lua") local parseMDT = dofile(rossPluginsDir .. "MapDoorTextParser.lua") local function copy(t, out) out = out or {} for k,v in pairs(t) do out[k] = v end return out end -- Settings: -------------------------------------------------- local COLORS = { background = 0, border = 12632256, Normal = RGBToInt(192), Yellow = RGBToInt(255, 255, 0), Red = RGBToInt(255, 0, 0), Green = RGBToInt(0, 255, 0), Cyan = RGBToInt(0, 255, 255), Blue = RGBToInt(0, 0, 255), Magenta = RGBToInt(255, 0, 255), White = RGBToInt(255), Desert = 34815, } local DEFAULT_COLORS = {} for k,v in pairs(COLORS) do DEFAULT_COLORS[k] = v end local fontFamily = "fixedsys" local fontSize = 9 -- NOTE: FixedSys only comes in this size. local spacingX, spacingY = 0, 0 -- Other Vars: -------------------------------------------------- local winRect = {x = 800, y = 0, width = 200, height = 200, z = 0} local winLocked = false local fontID = "font" -- Not the font name, just the ID it's registered to for our window. local fontHeight local fontCharWidth local fontMaxCharWidth -- For the rect to draw each character within. local loadMapFont, loadFilterGroupFont -- Upvalue for functions defined below. local playerY, playerX = 1, 1 local mapLines = {} local colorChangesAt = {} local mxpColorRegex = rex.new("MXP<(.+?)MXP>") local hexRegex = rex.new(".*? (#[0-9a-f]{6})") -- MXP<C #ff8700MXP> local lastMapPacket, lastMDTPacket-- Save last raw packet string to disk so we can show something on load. local MDTIndicatorsEnabled = true local MDTPlayerPrefix = "P)" local entityFilters = {} local DEFAULT_FILTER_GROUP = { name = "default", color = RGBToInt(255, 0, 0), fontFamily = "fixedsys", fontSize = 9, ox = 2, oy = 0 } local filterGroups = { default = copy(DEFAULT_FILTER_GROUP) } local disabledGroups = {} local CASE_INSENSITIVE = rex.flags().CASELESS -- Loading & Saving Settings: -------------------------------------------------- local function toboolean(v) -- Convert a value to a boolean. Always returns a boolean. if v == nil then return false elseif v == false or v == "false" then return false end return true -- Anything other than nil, false, or "false" ==> true. end local function loadVar(name) -- Returns nil instead of an empty string. local v = GetVariable(name) if v == "" then return nil end return v end local function loadSettings() for k,v in pairs(winRect) do winRect[k] = tonumber(GetVariable("window_" .. k)) or v end winLocked = toboolean(GetVariable("windowLocked")) lastMapPacket = loadVar("lastMapPacket") lastMDTPacket = loadVar("lastMDTPacket") fontFamily = loadVar("fontFamily") or fontFamily fontSize = loadVar("fontSize") or fontSize spacingX = loadVar("spacingX") or spacingX spacingY = loadVar("spacingY") or spacingY for colorName in pairs(COLORS) do COLORS[colorName] = tonumber(loadVar("color_" .. colorName)) or DEFAULT_COLORS[colorName] end local MDTEnabled = loadVar("MDTIndicatorsEnabled") if MDTEnabled then MDTIndicatorsEnabled = toboolean(MDTEnabled) end MDTPlayerPrefix = loadVar("MDTPlayerPrefix") or MDTPlayerPrefix local filters = loadVar("entityFilters") if filters then entityFilters = json.decode(filters) for i,f in ipairs(entityFilters) do local regexFlags = f.doesIgnoreCase and CASE_INSENSITIVE or nil f.regex = rex.new(f.pattern, regexFlags) end end local groups = loadVar("filterGroups") if groups then filterGroups = json.decode(groups) end local disabled = loadVar("disabledGroups") if disabled then disabledGroups = json.decode(disabled) end end local function saveVar(name, val, nilVal) if val == nil then val = nilVal or false end SetVariable(name, tostring(val)) end local function saveSettings() for k,v in pairs(winRect) do saveVar("window_" .. k, v) end winLocked = window.getLocked(winID) saveVar("windowLocked", winLocked) if lastMapPacket then saveVar("lastMapPacket", lastMapPacket) end if lastMDTPacket then saveVar("lastMDTPacket", lastMDTPacket) end saveVar("fontFamily", fontFamily, "fixedsys") saveVar("fontSize", fontSize, 9) saveVar("spacingX", spacingX, 0) saveVar("spacingY", spacingY, 0) for colorName in pairs(COLORS) do saveVar("color_" .. colorName, COLORS[colorName]) end saveVar("MDTIndicatorsEnabled", MDTIndicatorsEnabled) saveVar("MDTPlayerPrefix", MDTPlayerPrefix, "") -- Save custom entity filters. if next(entityFilters) then -- Can't save regex object, so make a copy of the filters without it. local saveData = {} local filterPropsToSave = {"pattern", "score", "group", "keepEvaluating", "doesIgnoreCase"} for i,f in ipairs(entityFilters) do local v = {} for _,prop in ipairs(filterPropsToSave) do v[prop] = f[prop] end saveData[i] = v end saveVar("entityFilters", json.encode(saveData)) end saveVar("filterGroups", json.encode(filterGroups)) saveVar("disabledGroups", json.encode(disabledGroups)) end function OnPluginSaveState() saveSettings() end local function refresh() if lastMapPacket then onGMCPReceived("room.map", lastMapPacket) end if lastMDTPacket and MDTIndicatorsEnabled then onGMCPReceived("room.writtenmap", lastMDTPacket) else window.draw(winID) end end -- Living Thing Filter Editing: -------------------------------------------------- local curMDTData = nil local getCountRegex = rex.new("^(\\d+)") local function setPlayerPrefix() local msg = [[The text you enter will be inserted before each player name so you can have a custom filter for them. If you put in something weird like " and ", "four ", or "one northwest ", then you'll break things.]] local prefix = utils.inputbox(msg, "Select Player Name Prefix", MDTPlayerPrefix) if prefix then MDTPlayerPrefix = prefix window.setMenuItem(winID, 34, "Set player name prefix...(cur: "..MDTPlayerPrefix..")") refresh() end end local function addFilter(pattern, score, group, keepEvaluating, doesIgnoreCase) local regexFlags = doesIgnoreCase and CASE_INSENSITIVE or nil local filter = { regex = rex.new(pattern, regexFlags), pattern = pattern, score = score, group = group or "default", keepEvaluating = keepEvaluating, doesIgnoreCase = doesIgnoreCase } table.insert(entityFilters, filter) end local function setFilter(i, filter) -- 1. Set the regex pattern. local msg = [[Enter a regular expression to check against the living thing name. If there are multiple identical things in the room, their text will start with a number. i.e. "2 annoying children". Capitalization is preserved, but you can set the regex to be case-insensitive.]] local extras = { box_width = 800, box_height = 250, prompt_height = 100 } local default = filter and filter.pattern local pattern = utils.editbox(msg, "Define RegEx", default, nil, nil, extras) if not pattern then return end -- 2. Set the score. local msg = [[Enter your desired score number.]] local default = filter and filter.score or 1 local score = utils.inputbox(msg, "Select Score", default) if not score then return end score = tonumber(score) if not score then Note("[RossAsciiMap] - Invalid score. Must be a number. Aborting.") return end -- 3. Set the group. local msg = [[Things in each group is counted separately and each group count can be drawn as a separate indicator.]] local default = filter and filter.group or "default" local list = {} for k,v in pairs(filterGroups) do list[k] = k end local group = utils.choose(msg, "Select Group", list, default) if not group then return end -- 4. Set 'keepEvaluating'. local msg = [[Should we keep evaluating if this filter matches? i.e. should subsequent filters be able to override this one?]] local default = filter and (filter.keepEvaluating and 1 or 2) or 2 local keepEvaluating = utils.msgbox(msg, "Keep Evaluating?", "yesno", "?", default) if not keepEvaluating then return end keepEvaluating = keepEvaluating == "yes" and true or nil -- 5. Set 'doesIgnoreCase' local msg = [[Is the regex pattern case-sensitive?]] local default = filter and (filter.doesIgnoreCase and 2 or 1) or 1 local doesIgnoreCase = utils.msgbox(msg, "Case-sensitive?", "yesno", "?", 1) if not doesIgnoreCase then return end doesIgnoreCase = doesIgnoreCase == "no" and true or nil if filter then local regexFlags = doesIgnoreCase and CASE_INSENSITIVE or nil filter.regex = rex.new(pattern, regexFlags) filter.pattern = pattern filter.score = score filter.group = group filter.keepEvaluating = keepEvaluating filter.doesIgnoreCase = doesIgnoreCase else addFilter(pattern, score, group, keepEvaluating, doesIgnoreCase) end refresh() end local function chooseFilter(msg, title, isMulti) local list = {} for i,f in ipairs(entityFilters) do local str = '%s: %s, %s, "%s", %s, %s %s' local isDisabled = disabledGroups[f.group or "default"] and "(group disabled)" or "" str = str:format( i, f.pattern, f.score, f.group or "default", tostring(not not keepEvaluating), tostring(not doesIgnoreCase), isDisabled ) table.insert(list, str) end local message = 'Shown as: #: regex pattern, score, "group", keepEvaluating, caseSensitive' if msg then message = message .. "\n\n" .. msg end local fn = isMulti and utils.multilistbox or utils.listbox return fn(message, title, list), list -- Return the filter index. end local function modifyFilter() local filterIdx = chooseFilter(nil, "Choose a Filter to Modify") if filterIdx then setFilter(nil, entityFilters[filterIdx]) end end local function deleteFilters() local filterIndices, list = chooseFilter("You may select multiple.", "Choose Filters to Delete", true) if filterIndices then for filterIdx in pairs(filterIndices) do local idStr = list[filterIdx] local msg = "Are you sure you want to delete the filter:\n" .. idStr local confirm = utils.msgbox(msg, "Really Delete?", "okcancel") if confirm == "ok" then table.remove(entityFilters, filterIdx) end end refresh() end end local function setFilterOrder() local filterIdx = chooseFilter(nil, "Choose a Filter to Reorder") if filterIdx then local minI, maxI = 1, #entityFilters local list = {} for i=minI,maxI do list[i] = i end local toIdx = utils.choose("", "Choose a New Index", list, filterIdx) if toIdx then local filter = entityFilters[filterIdx] table.remove(entityFilters, filterIdx) table.insert(entityFilters, toIdx, filter) refresh() end end end -- Group Editing: -------------------------------------------------- local function addGroup(name, color, fontFamily, fontSize, ox, oy) local group = { name = name, color = color, fontFamily = fontFamily, fontSize = fontSize, ox = ox, oy = oy } filterGroups[name] = group loadFilterGroupFont(winID, group) end local function setGroup(i, group) -- 1. Set the group name. local msg = [[Enter the group name.]] local default = group and group.name local name = utils.inputbox(msg, "Enter a Group Name", default) if not name then return end if filterGroups[name] and not (group and group.name == name) then Note('[RossAsciiMap] - The group name: "'..name..'" is already used. Aborting.') return end -- 2. Set the group color. local default = group and group.color or DEFAULT_FILTER_GROUP.color local color = PickColour(default) if color == -1 then return end -- 3. Set the group font family and size. local defaultFamily = group and group.fontFamily or DEFAULT_FILTER_GROUP.fontFamily local defaultSize = group and group.fontSize or DEFAULT_FILTER_GROUP.fontSize local fontSpecs = utils.fontpicker(defaultFamily, defaultSize) local fontFamily, fontSize = defaultFamily, defaultSize if fontSpecs then fontFamily, fontSize = fontSpecs.name, fontSpecs.size end -- 4. Set the group X-offset. local default = group and group.ox or DEFAULT_FILTER_GROUP.ox local title = "Choose X Offset" local msg = "Enter the offset on the X-axis where the group score will be shown." local ox = utils.inputbox(msg, title, default) if not ox then return end ox = tonumber(ox) if not ox then Note("[RossAsciiMap] - Invalid offset. Must be a number. Aborting.") return end -- 5. Set the group Y-offset. local default = group and group.oy or DEFAULT_FILTER_GROUP.oy local title = "Choose Y Offset" local msg = "Enter the offset on the Y-axis where the group score will be shown." local oy = utils.inputbox(msg, title, default) if not oy then return end oy = tonumber(oy) if not oy then Note("[RossAsciiMap] - Invalid offset. Must be a number. Aborting.") return end if group then group.name = name group.color = color group.fontFamily = fontFamily group.fontSize = fontSize group.ox = ox group.oy = oy loadFilterGroupFont(winID, group) else addGroup(name, color, fontFamily, fontSize, ox, oy) end refresh() end local function chooseGroup(msg, title, isMulti) local list = {} for groupName,group in pairs(filterGroups) do local str = '"%s", %s, %s, %s, %s, %s %s' local isDisabled = disabledGroups[groupName] and "(disabled)" or "" str = str:format(groupName, group.color, group.fontFamily, group.fontSize, group.ox, group.oy, isDisabled) list[groupName] = str end local message = 'Shown as: "groupName", color, font, fontSize, offsetX, offsetY' if msg then message = message .. "\n\n" .. msg end local fn = isMulti and utils.multilistbox or utils.listbox return fn(message, title, list), list -- Returns the group name. end local function modifyGroup() local groupName = chooseGroup("Select a group to modify it.", "Choose a Group to Modify") if groupName then setGroup(nil, filterGroups[groupName]) end end local function deleteGroups() local groupNames, list = chooseGroup("You may select multiple.", "Choose Groups to Delete", true) if groupNames then for groupName in pairs(groupNames) do local idStr = list[groupName] local msg = "Are you sure you want to delete the group:\n" .. idStr local confirm = utils.msgbox(msg, "Really Delete?", "okcancel") if confirm == "ok" then filterGroups[groupName] = nil end end refresh() end end local function toggleGroups() local msg = "You may select multiple." msg = msg .. "\nAny filters associated with disabled groups will be skipped." local groupNames, list = chooseGroup(msg, "Choose Groups to Toggle On/Off", true) if groupNames then for groupName in pairs(groupNames) do if disabledGroups[groupName] then disabledGroups[groupName] = nil -- Enable again, remove from dict. else disabledGroups[groupName] = true -- Disable. end end refresh() end end function aliasSetGroupsEnabled(name, line, captures) local isDisable = captures[1] == "disable" local groupList = captures[2] groupList:gsub(",", "") local groupNames = {} for groupName in string.gmatch(groupList, "([^%s]+)") do table.insert(groupNames, groupName) end local val = isDisable or nil local msg = "[RossAsciiMap] - " .. (isDisable and "Disabling" or "Enabling") .. ' group: "' for i,groupName in ipairs(groupNames) do if not filterGroups[groupName] then Note('[RossAsciiMap] - Group: "' .. groupName .. '" does not exist.') else disabledGroups[groupName] = val Note(msg .. groupName .. '".') end end refresh() end -- MDT Data Handling: -------------------------------------------------- local function printLastMDTPacket() if lastMDTPacket then print(lastMDTPacket) end end local function toggleMDTIndicators(i) MDTIndicatorsEnabled = not MDTIndicatorsEnabled window.checkMenuItem(winID, i, MDTIndicatorsEnabled) refresh() end -- Loop through each room with things in it and give it a score property. local function scoreMDTRooms(rooms) for i,room in ipairs(rooms) do local scoreList = {} for i,entityStr in ipairs(room.entities) do local _, _, capt = getCountRegex:match(entityStr) local count = capt and capt[1] or 1 local wasCaughtByFilter = false for i,filter in ipairs(entityFilters) do if not disabledGroups[filter.group] then if filter.regex:match(entityStr) then wasCaughtByFilter = true local groupName = filter.group or "default" scoreList[groupName] = scoreList[groupName] or 0 scoreList[groupName] = scoreList[groupName] + count * filter.score if not filter.keepEvaluating then break end end end end if not wasCaughtByFilter then scoreList.default = scoreList.default or 0 scoreList.default = scoreList.default + count end end room.scoreList = scoreList end end -- Make a 2D array of room scores with relative coordinates from player. local function makeMDTArraymap(data) local map = {} for i,room in ipairs(data) do local x, y = room.dx, room.dy map[y] = map[y] or {} map[y][x] = room.scoreList end data.scoreMap = map end -- Right-Click Menu Handling: -------------------------------------------------- local function pickFont() local fnt = utils.fontpicker(fontFamily, fontSize) if fnt then fontFamily, fontSize = fnt.name, fnt.size loadMapFont(winID, fontID, fontFamily, fontSize) window.draw(winID) end end local function pickColor(i, colorName) local result = PickColour(COLORS[colorName]) if result ~= -1 then COLORS[colorName] = result refresh() end end local function resetColorToDefault(i, colorName) COLORS[colorName] = DEFAULT_COLORS[colorName] refresh() end local function setSpacing(i, axis) local curSpacing = axis == "X" and spacingX or spacingY local msg = "Enter your desired " .. axis .. " spacing." local title = "Set " .. axis .. " Spacing" local newSpacing = utils.inputbox(msg, title, curSpacing) if not newSpacing then return end newSpacing = tonumber(newSpacing) if not newSpacing then Note("[RossAsciiMap] - Invalid spacing. Must be a number. Aborting.") return end if axis == "X" then spacingX = newSpacing window.setMenuItem(winID, 2, "Set X spacing...(cur: "..spacingX..")") else spacingY = newSpacing window.setMenuItem(winID, 3, "Set Y spacing...(cur: "..spacingY..")") end refresh() end local menuItems = {} -- List of tables with button text and callback arguments. local menuItemTextList = {} -- List of button texts for adding to window local function menuItem(text, fn, ...) table.insert(menuItems, {text, fn, ...}) end local function setupMenuItems() menuItems, menuItemTextList = {}, {} menuItem("Set font...", pickFont) menuItem("Set X spacing...(cur: "..spacingX..")", setSpacing, "X") menuItem("Set Y spacing...(cur: "..spacingY..")", setSpacing, "Y") local colorNames = {"background", "border", "Normal", "Yellow", "Red", "Green", "Cyan", "Blue", "Desert", "Magenta", "White"} menuItem(">Colors:") menuItem(">Defaults:") for i,colorName in ipairs(colorNames) do menuItem("Reset "..colorName.." to default", resetColorToDefault, colorName) end menuItem("<") menuItem("-") for i,colorName in ipairs(colorNames) do menuItem("Set "..colorName.." color...", pickColor, colorName) end menuItem("<") menuItem(">Living thing indicators:") local checkChar = MDTIndicatorsEnabled and "+" or "" menuItem(checkChar .. "Enabled", toggleMDTIndicators) menuItem("Output last MDT packet (for debug)", printLastMDTPacket) menuItem("Set player name prefix...(cur: "..MDTPlayerPrefix..")", setPlayerPrefix) menuItem("-") menuItem("Show filters...", modifyFilter) menuItem("Add filter...", setFilter) menuItem("Delete filters...", deleteFilters) menuItem("Change filter order...", setFilterOrder) menuItem("-") menuItem("Show groups...", modifyGroup) menuItem("Add group...", setGroup) menuItem("Delete groups...", deleteGroups) menuItem("Toggle groups...", toggleGroups) menuItem("<") for i,v in ipairs(menuItems) do table.insert(menuItemTextList, v[1]) end end local function menuItemClicked(winID, i, prefix, item) local data = menuItems[i] if data then local fn = data[2] fn(i, unpack(data, 3)) end end -- GMCP Parsing: -------------------------------------------------- local function processColorCodes(startPos, line, lineIdx) -- Strip out color codes and record at which character index the color changes. local startI, endI, captures = mxpColorRegex:match(line, startPos) if endI then local colorName = captures[1] if colorName == "Yellow" then -- This is the line the player is on. playerX, playerY = startI, lineIdx end -- Convert color string into an integer RGB value. local colorInt if COLORS[colorName] then colorInt = COLORS[colorName] else local _s, _e, hexCaptures = hexRegex:match(colorName) if hexCaptures then if hexCaptures[1] == "#ff8700" then colorName = "Desert" colorInt = COLORS[colorName] else colorInt = ColourNameToRGB(hexCaptures[1]) -- ColourNameToRGB will convert hex colors. end else Note("[RossAsciiMap] - Unrecognized color code: '", string.sub(line, startI, endI), "'.") colorInt = COLORS.Normal end end colorChangesAt[lineIdx] = colorChangesAt[lineIdx] or {} colorChangesAt[lineIdx][startI] = colorInt -- Cut color code chunk out of map string. local pre = string.sub(line, 1, startI - 1) local post = string.sub(line, endI + 1) line = pre .. post end -- We're removing everything from startI to endI, so our startI should -- also be the start for the next match. return startI, line end --[[ -- Output from MUD: "\u001b[3z\u001b[4zMXP<RedMXP>+\u001b[3z \u001b[3z\u001b[4zMXP<RedMXP>+\u001b[3z \n\u001b[3z\u001b[4zMXP<GreenMXP>&\u001b[3z-\u001b[3z\u001b[4zMXP<CyanMXP>*\u001b[3z-\u001b[3z\u001b[4zMXP<YellowMXP>@\u001b[3z-\u001b[3z\u001b[4zMXP<GreenMXP>$\u001b[3z \n \u001b[3z\\\u001b[3z \n \u001b[3z\u001b[4zMXP<CyanMXP>*\u001b[3z-\u001b[3z\n" -- Desired Result: + + &-*-@-$ \ *- --]] -- example: regexSplit("a,b, c", ", ?") => {"a", "b", "c"} function regexSplit(text, regularExpression) local ret = {} local reg = rex.new(regularExpression) local matchStart, matchEnd = reg:match(text) if matchStart == nil then table.insert(ret, text) else local prevEnd = 0 while matchStart ~= nil do table.insert(ret, string.sub(text, prevEnd + 1, matchStart - 1)) prevEnd = matchEnd matchStart, matchEnd = reg:match(text, prevEnd + 1) end table.insert(ret, string.sub(text, prevEnd + 1, -1)) end return ret end -- cb is passed the MDTData and should return true if the display should be updated function editMDTData(cb) if MDTIndicatorsEnabled and curMDTData ~= nil then local changed = cb(curMDTData) if changed then scoreMDTRooms(curMDTData) makeMDTArraymap(curMDTData) window.draw(winID) -- writtenmap is sent second, so draw here. end end end local numStrToNum = { one = 1, two = 2, three = 3, four = 4, five = 5, six = 6, seven = 7, eight = 8, nine = 9, ten = 10, eleven = 11, twelve = 12, thirteen = 13, fourteen = 14 } function parseNumNpc(str) local count, name local i = string.find(str, ' ') if i then local firstWord = string.sub(str, 1, i - 1) name = string.sub(str, i + 1) count = numStrToNum[string.lower(firstWord)] if not count then count = tonumber(firstWord) if not count then count = 1 name = firstWord .. ' ' .. name end end else count = 1 name = str end return count, name end function formatNumNpc(count, str) if count == 1 then return str else return count .. ' ' .. str end end function invertKV(kv) local ret = {} for k, v in pairs(kv) do ret[v] = k end return ret end local pluralizeCache = { man = "men", fisherman = "fishermen", woman = "women", mercenary = "mercenaries", lady = "ladies", sheep = "sheep", grflx = "grflxen", child = "children", } local singularizeCache = invertKV(pluralizeCache) function autopluralize(str) if str:match('[sxz]$') or str:match('[cs]h$') then return str .. 'es' elseif str:match('quy$') or str:match('[^aeiou]y$') then return str .. 'ies' else return str .. 's' end end function pluralizeWord(word) if not pluralizeCache[word] then local plural = autopluralize(word) pluralizeCache[word] = plural singularizeCache[plural] = word end return pluralizeCache[word] end function pluralize(str) local words = regexSplit(str, ' ') words[#words] = pluralizeWord(words[#words]) return table.concat(words, ' ') end function singularizeWord(word) return singularizeCache[word] or string.sub(word, 1, -2) end -- only called when if there are 3 or more and all but 1 enter at once function singularize(str) local words = regexSplit(str, ' ') words[#words] = singularizeWord(words[#words]) return table.concat(words, ' ') end local exitToMoves = { northeast = "1 ne", northwest = "1 nw", southeast = "1 se", southwest = "1 sw", north = "1 n", south = "1 s", east = "1 e", west = "1 w" } function triggerNPCEntered(name, line, wildcards) local allEntering = wildcards.npcs local moves = exitToMoves[wildcards.exit] editMDTData(function (data) local changed = false for roomI,room in ipairs(data) do if room.entities ~= nil then if #room.moves == 1 and room.moves[1] == moves then for _, numNpc in ipairs(regexSplit(allEntering, "(, | and (?!yellow |white ))(an? )?")) do local enteringCount, enteringName = parseNumNpc(numNpc) local enteringPlural = enteringCount > 1 for distantI, distant in ipairs(room.entities) do local distantCount, distantName = parseNumNpc(distant) local distantPlural = distantCount > 1 local maybePluralEnteringName = enteringName if distantPlural and not enteringPlural then maybePluralEnteringName = pluralize(enteringName) end if maybePluralEnteringName == distantName then local remaining = distantCount - enteringCount if remaining < 1 then table.remove(room.entities, distantI) else if remaining == 1 then if enteringCount == 1 then distantName = enteringName else distantName = singularize(enteringName) end end room.entities[distantI] = formatNumNpc(remaining, distantName) end changed = true break end end end end end end return changed end) end function onGMCPReceived(message, dataStr) if message == "room.writtenmap" then lastMDTPacket = dataStr if MDTIndicatorsEnabled then curMDTData = parseMDT(dataStr, nil, MDTPlayerPrefix) -- Gets a list of rooms with things in them. scoreMDTRooms(curMDTData) makeMDTArraymap(curMDTData) end window.draw(winID) -- writtenmap is sent second, so draw here. elseif message == "room.map" then -- Map is colored with MXP color codes. First strip out the extra junk, then split into lines and process the color codes. lastMapPacket = dataStr local map = dataStr map = string.sub(map, 2, -2) -- remove surrounding quotes (""). map = string.gsub(map, "\\u001b%[4z", "") -- Remove ANSI codes before MXP colors. map = string.gsub(map, "MXP<[\\/]-send.-MXP>", "") -- remove MXP links, if any (not colors). -- Replace ANSI reset code with a fake MXP color tag, which the next step will pick up. map = string.gsub(map, "\\u001b%[3z", "MXP<NormalMXP>") -- Un-escape actual map characters. map = string.gsub(map, "\\\\", "\\") -- change \\ to \. map = string.gsub(map, "\\\/", "\/") -- change \/ to /. for i=#mapLines,1,-1 do -- Clear old lists. mapLines[i] = nil colorChangesAt[i] = nil end for line in string.gmatch(map, "(.-)\\n") do -- Split map up into lines. table.insert(mapLines, line) end for lineIdx,line in ipairs(mapLines) do local startPos = 1 while startPos do startPos, line = processColorCodes(startPos, line, lineIdx) end mapLines[lineIdx] = line -- Update line after stripping out color codes. end end end -- Drawing: -------------------------------------------------- local function drawWindow() winRect.width = WindowInfo(winID, 3) winRect.height = WindowInfo(winID, 4) WindowRectOp(winID, 2, 0, 0, winRect.width, winRect.height, COLORS.background) -- Clear/Fill Background WindowRectOp(winID, 1, 0, 0, winRect.width, winRect.height, COLORS.border) -- Draw Border local gridX, gridY = fontCharWidth + spacingX, fontHeight + spacingY local mapHalfWidth = (playerX - 0.5) * gridX local mapHalfHeight = (playerY - 0.5) * gridY local ox, oy = winRect.width/2 - mapHalfWidth, winRect.height/2 - mapHalfHeight local baseCol = COLORS.Normal local color = baseCol for lineIdx,line in ipairs(mapLines) do local x, y = ox, oy + (lineIdx - 1)*gridY color = baseCol for charIdx=1,string.len(line) do if colorChangesAt[lineIdx] and colorChangesAt[lineIdx][charIdx] then color = colorChangesAt[lineIdx][charIdx] if type(color) ~= "number" then Note("[RossAsciiMap] - Weird color value in colorChangesAt: ", colorChangesAt[lineIdx][charIdx]) color = COLORS.Normal end end local char = string.sub(line, charIdx, charIdx) if char ~= " " then WindowText(winID, fontID, char, x, y, x+fontMaxCharWidth, y+gridY, color) end x = x + gridX end end -- Draw room scores. if MDTIndicatorsEnabled and curMDTData then local ox, oy = winRect.width/2, winRect.height/2 -- Start from player pos, center of window. for dy,row in pairs(curMDTData.scoreMap) do for dx,scoreList in pairs(row) do -- Double dx and dy because rooms are every-other line on the ASCII map. local x, y = ox + dx*2*gridX, oy - dy*2*gridY -- Subtract dy because we draw in +y = down coordinates. for groupName,score in pairs(scoreList) do if not disabledGroups[groupName] then -- Only really affects the default group, other disabled groups are simply not scored. local group = filterGroups[groupName] if not group then Note('[RossAsciiMap] - No filter group defined for name: "' .. groupName .. '"') group = filterGroups.default or DEFAULT_FILTER_GROUP end local _x, _y = x + group.ox, y + group.oy local width = group.fontMaxCharWidth * tostring(score):len() WindowText(winID, group.fontID, score, _x, _y, _x+width, _y+gridY, group.color) end end end end end end -- Tooltip - Show Things Under Cursor: -------------------------------------------------- local floor = math.floor local function round(x, incr) if incr then return floor(x/incr + 0.5)*incr end return floor(x + 0.5) end local function windowMousePosToRoomPos(mx, my) local gridX, gridY = fontCharWidth + spacingX, fontHeight + spacingY local x, y = mx - winRect.width/2, my - winRect.height/2 return round(x/gridX/2), -round(y/gridY/2) end local function getThingsInRoom(rx, ry) if not curMDTData then return end for i,room in ipairs(curMDTData) do if room.dx == rx and room.dy == ry then return room.entities end end end local tooltipRX, tooltipRY, tooltipText local tooltipWinID = SELF_ID .. "tooltip" local tooltipFont, tooltipFontSize = "FixedSys", 9 local tooltipFontHeight local padding = 2 local tooltipBorderColor = RGBToInt(80) local tooltipTextColor = RGBToInt(160) local tooltipIsVisible = false local tooltipW, tooltipH local function drawTooltip() WindowRectOp(tooltipWinID, 2, 0, 0, tooltipW, tooltipH, 0) -- Draw background WindowRectOp(tooltipWinID, 1, 0, 0, tooltipW, tooltipH, tooltipBorderColor) -- Draw border WindowText(tooltipWinID, "font", tooltipText, padding, padding, tooltipW, tooltipH, tooltipTextColor) Redraw() end local function showTooltip(x, y, rx, ry, text) tooltipIsVisible = true tooltipRX, tooltipRY, tooltipText = rx, ry, text tooltipW = WindowTextWidth(tooltipWinID, "font", text) + padding*2 tooltipH = tooltipFontHeight + padding*2 WindowResize(tooltipWinID, tooltipW, tooltipH, 0) local gridX, gridY = fontCharWidth + spacingX, fontHeight + spacingY local winX, winY = WindowInfo(winID, 1), WindowInfo(winID, 2) local x = winX + winRect.width/2 + rx*2 * gridX local y = winY + winRect.height/2 - ry*2 * gridY WindowPosition(tooltipWinID, x, y - tooltipH*1.5, 5, 2) WindowShow(tooltipWinID, true) local mapWindowZ = WindowInfo(winID, 22) WindowSetZOrder(tooltipWinID, mapWindowZ + 1) -- NOTE: winRect.z is only set when settings ore loaded or saved. drawTooltip() end local function hideTooltip() tooltipIsVisible = false WindowShow(tooltipWinID, false) tooltipRX, tooltipRY, tooltipText = nil, nil, nil end function OnPluginMouseMoved(x, y, mw) if mw == winID then local rx, ry = windowMousePosToRoomPos(WindowInfo(winID, 14), WindowInfo(winID, 15)) local thingList = getThingsInRoom(rx, ry) local text if thingList and #thingList > 0 then text = table.concat(thingList, ", ") end if tooltipIsVisible and not text then hideTooltip() elseif text then showTooltip(x, y, rx, ry, text) end else if tooltipIsVisible then hideTooltip() end end end -- Setup & Breakdown: -------------------------------------------------- function loadMapFont(winID, fontID, fontFamily, fontSize) -- Is local--upvalue defined above. WindowFont(winID, fontID, fontFamily, fontSize, nil, nil, nil, nil, 0) fontHeight = WindowFontInfo(winID, fontID, 1) fontCharWidth = WindowFontInfo(winID, fontID, 6) -- Average character width. fontMaxCharWidth = WindowFontInfo(winID, fontID, 7) -- Max character width. end function loadFilterGroupFont(winID, group) -- Is local--upvalue defined above. group.fontID = "font_" .. group.name group.fontFamily = group.fontFamily or "FixedSys" group.fontSize = group.fontSize or 9 WindowFont(winID, group.fontID, group.fontFamily, group.fontSize) group.fontMaxCharWidth = WindowFontInfo(winID, group.fontID, 7) -- Max character width. end local function init() CallPlugin(GMCP_INTERFACE_ID, "subscribe", SELF_ID, "onGMCPReceived", "room.map", "room.writtenmap") loadSettings() window.new( winID, winRect.x, winRect.y, winRect.width, winRect.height, winRect.z, nil, nil, nil, true, winLocked, menuItemClicked, drawWindow ) setupMenuItems() window.addMenuItems(winID, 1, menuItemTextList) loadMapFont(winID, fontID, fontFamily, fontSize) loadFilterGroupFont(winID, DEFAULT_FILTER_GROUP) for groupName,group in pairs(filterGroups) do loadFilterGroupFont(winID, group) end refresh() -- Tooltip. WindowCreate(tooltipWinID, 0, 0, 100, 25, 5, 2, 0) WindowFont(tooltipWinID, "font", tooltipFont, tooltipFontSize) tooltipFontHeight = WindowFontInfo(tooltipWinID, "font", 1) end local function final() winRect.x, winRect.y, winRect.width, winRect.height, winRect.z = window.getRect(winID) winLocked = window.getLocked(winID) WindowDelete(winID) WindowDelete(tooltipWinID) CallPlugin(GMCP_INTERFACE_ID, "unsubscribe", SELF_ID, "room.map", "room.writtenmap") end function OnPluginInstall() init() end function OnPluginEnable() init() end function OnPluginClose() final() end function OnPluginDisable() final() end ]]> </script> <plugin name="RossAsciiMap" author="Ross Grams" purpose="ASCII-map miniwindow" id="4045bd321bb34f7bc51a3ce8" language="Lua" save_state="y" date_written="2019-02-22 13:38:17" requires="5.05" version="2.0" > </plugin> <aliases> <alias match="^asciimap (enable|disable) (.+)$" enabled="y" regexp="y" sequence="100" script="aliasSetGroupsEnabled" > </alias> </aliases> <triggers> <trigger match="^asciimap (enable|disable) (.+)$" enabled="y" regexp="y" sequence="100" script="aliasSetGroupsEnabled" > </trigger> </triggers> <triggers> <trigger script="triggerNPCEntered" enabled="y" ignore_case="n" keep_evaluating="y" match="^(\*thump\* \*click\* \*thump\* \*click\* )?([aA] |[aA]n |[tT]he )?(?P<npcs>.*) (arrives?|climbs?|enters?|hobbles?|rides?|shambles?|shuffles?|squeezes?|swims?|trudges?|trundles?|wades?) (.* )?from (the )+(?P<exit>north|northeast|east|southeast|south|southwest|west|northwest)[.]$" regexp="y" repeat="n" sequence="100" ></trigger> </triggers> </muclient>