local prefix = 'room.writtenmap "' local suffix = '\\n"' local mangleUnmangle = { {"(%w+) and white", "%1 andandwhite", " andandwhite", " and white"}, {"(%w+) and yellow", "%1 andandyellow", " andandyellow", " and yellow"}, } local function mangle (txt) for i,v in ipairs(mangleUnmangle) do txt = txt:gsub(v[1], v[2]) end return txt end local function unmangle (txt) for i,v in ipairs(mangleUnmangle) do txt = txt:gsub(v[3], v[4]) end return txt end -- Up-scoped values for regex replacer and other functions to grab. local _debugLevel -- Set for each call to parse(). local _playerPrefix local _colorOption local function hexToAnsi(hex) local rgb = ColourNameToRGB(hex) -- How to split a single-int-rgb value into its components if you have absolutely no clue what you're doing. local r = rgb % 256 local g = (rgb - r) % 65536 / 256 local b = bit.shr(rgb, 16) return ANSI(38, 2, r, g, b) -- 38;2 is the 24-bit foreground color ANSI sequence. end local CASE_INSENSITIVE = rex.flags().CASELESS -- MXP color can be a hex color: "C #d7d7d7" or a named color: "White". local playerColorRegex = rex.new([[\\u001b\[4zMXP<(?:C )?(#[0-9a-f]{6}|[A-Z]\w+)MXP>]]) local THING_COUNT = "(?:(one|two|three|four|five|six|seven|eight|nine|ten|eleven|twelve|thirteen|fourteen) )" local MOVE_COUNT = "(one|two|three|four|five|six|seven)" local DIRECTION = "(northeast|northwest|southeast|southwest|north|south|east|west|here)\\b" local moveRegex = rex.new("(?:"..MOVE_COUNT.." )?"..DIRECTION, CASE_INSENSITIVE) -- Match a space ONLY if there's a number, but don't capture it. local numStrToNum = { [false] = 1, 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 } local longDirToShort = { here = "here", northeast = "ne", northwest = "nw", southeast = "se", southwest = "sw", north = "n", south = "s", east = "e", west = "w" } local MOVE = "<\\d \\w{1,2}>" -- Ex: "<1 nw>" -- NOTE: No captures. local moveSequenceRegex = rex.new("((?:(?:"..MOVE.."), )*)("..MOVE.." and )?("..MOVE..")") local splitMoveSeqRegex = rex.new("(\\d) ([nsew]{1,2})(?=, |$)") local BOUNDARY_JUNK = [[(?: and |, |.)]] -- Chunks will end with an [[ and ]], [[, ]], or [[.]] that we don't need but don't want to leave behind. local EXIT_CHUNK = [[(?:a |an )?(exit|door)s? <(.+?)> of <(.+?)>]] .. BOUNDARY_JUNK local VISION_CHUNK = [[the limit of your vision is (?:<(.+?)> from )?<0 n>]] .. BOUNDARY_JUNK -- Must match: "The limit of your vision is here." local ENTITY_CHUNK = [[(.+?) (?:is|are) <(.+?)>]] .. BOUNDARY_JUNK -- Once other chunks are filtered out, this can be very broad. local exitChunkRegex = rex.new(EXIT_CHUNK, CASE_INSENSITIVE) local visionChunkRegex = rex.new(VISION_CHUNK, CASE_INSENSITIVE) local entityChunkRegex = rex.new(ENTITY_CHUNK, CASE_INSENSITIVE) local splitEntitySeqRegex = rex.new("(?:a |an |)"..THING_COUNT.."?(.+?)(?:, |$)", CASE_INSENSITIVE) -- Chunk capture indices: local THING, THING_POS = 1, 2 -- For entity chunks. local function regexReplace(str, regex, matchFn) local overloadLimit = 1000 local startI = 1 local lastCharI = #str local iter = 0 repeat iter = iter + 1 if iter >= overloadLimit then print("regexReplace - HIT OVERLOAD LIMIT") break end local startCharI, endCharI, captures = regex:match(str, startI) if startCharI then local fullMatch = str:sub(startCharI, endCharI) local repl = matchFn(fullMatch, captures) if repl then local pre = str:sub(1, startCharI - 1) local post = str:sub(endCharI + 1, -1) str = pre .. repl .. post endCharI = startCharI + #repl lastCharI = #str end end startI = (endCharI or lastCharI) until startI >= lastCharI return str end local function dirStrToVec(dir) if dir == "n" then return 0, 1 elseif dir == "s" then return 0, -1 elseif dir == "e" then return 1, 0 elseif dir == "w" then return -1, 0 elseif dir == "ne" then return 1, 1 elseif dir == "nw" then return -1, 1 elseif dir == "se" then return 1, -1 elseif dir == "sw" then return -1, -1 end end -- Replacer for MXP hex colors before player names. local function playerColorReplacer(match, captures) local replacement = "" if _playerPrefix then replacement = _playerPrefix end if _colorOption == "ansi" then local hexColor = captures[1] local ansiColor = hexToAnsi(hexColor) replacement = ansiColor .. replacement elseif _colorOption == "unmodified" then replacement = match .. replacement end return replacement end -- Replace words with abbreviations, inside < >. Example: "two northwest" --> "<2 nw>" local function moveReplacer(match, captures) local num = numStrToNum[captures[1]] or 1 local dir = longDirToShort[captures[2]] if dir == "here" then num, dir = 0, "n" end return "<"..num.." "..dir..">" end -- Merge a sequence of moves into a single list inside < >. -- Example <1 n>, <2 nw>, <1 n> and <1 w> --> <1 n, 2 nw, 1 n, 1 w> local function moveSequenceReplacer(match, captures) local list, moveAnd, last = captures[1], captures[2], captures[3] list = list or "" moveAnd = moveAnd and moveAnd:gsub(" and", ",") or "" local all = list..moveAnd..last all = all:gsub("[<>]", "") return "<"..all..">" end -- Temporary variables so regex match functions can be static. local _rooms local _entities local _moves local _dx, _dy local function sumMove(match, captures) -- Full match is the abbreviation: "1 nw", etc. local dist, dir = tonumber(captures[1]), captures[2] local dx, dy = dirStrToVec(dir) _dx, _dy = _dx + dx*dist, _dy + dy*dist table.insert(_moves, match) end local function getMoveSequenceFromString(moveSeqStr) _dx, _dy = 0, 0 _moves = {} splitMoveSeqRegex:gmatch(moveSeqStr, sumMove) return _moves, _dx, _dy end local function replaceExitChunk(match, captures) return "" end local function replaceVisionChunk(match, captures) return "" end local function addEntities(match, captures) if captures[1] then captures[1] = string.lower(captures[1]) end local num = numStrToNum[captures[1]] if type(num) ~= "number" then print("WARNING: MDT-Parser.addEntities - Number capture seems to have failed. Invalid number: '"..tostring(num).."' for match: '"..match.."'") num = 1 end local entStr = unmangle(captures[2]) if num > 1 then entStr = num .. " " .. entStr end _entities = _entities or {} table.insert(_entities, entStr) end local function parseEntityChunk(match, captures) local thingStr = captures[THING] local posStr = captures[THING_POS] getMoveSequenceFromString(posStr) thingStr = thingStr:gsub(" and ", ", ") _entities = nil -- Clear data from last use. splitEntitySeqRegex:gmatch(thingStr, addEntities) if _debugLevel then local str = string.format("%s: %s", table.concat(_moves, ", "), table.concat(_entities, ", ")) print(str) end if _entities and #_entities > 0 then local roomData = { entities = _entities, moves = _moves, dx = _dx, dy = _dy } table.insert(_rooms, roomData) end end local isValidColorOption = { strip = false, ansi = true, unmodified = true } local function parse(str, debugLevel, playerPrefix, colorOption) _debugLevel = debugLevel -- any-truthy-value --> print each room, 2 --> print whole text and each room. if playerPrefix == "" then playerPrefix = nil end _playerPrefix = playerPrefix colorOption = isValidColorOption[colorOption] and colorOption or nil _colorOption = colorOption str = str:gsub(prefix, "") str = str:gsub(suffix, "") str = str:gsub('"', "") -- Handle player colors and prefix. if not playerPrefix and not colorOption then -- Default: just strip out player colors. str = str:gsub("\\u001b%[%dz", "") -- NOTE: \u001b == Unicode Escape Sequence. str = str:gsub("MXP<.-MXP>", "") else str = regexReplace(str, playerColorRegex, playerColorReplacer) if colorOption == "ansi" then -- If converting color codes to ANSI, make sure it's reset first or it looks like we messed up if a color is already set (like the standard Note blue). str = ANSI(0) .. str str = str:gsub("\\u001b%[3z", ANSI(0)) elseif not colorOption then str = str:gsub("\\u001b%[3z", "") end end str = mangle(str) if _debugLevel and _debugLevel >= 2 then AnsiNote(str) end _rooms = {} -- List of room entries: { entities, moves, dx, dy } -- (Store at higher scope so parseChunk() can access it.) -- First replace directional stuff with sequences that are easy to deal with. str = regexReplace(str, moveRegex, moveReplacer) -- "two northwest" --> "<2 nw>" str = regexReplace(str, moveSequenceRegex, moveSequenceReplacer) -- "<2 nw>, <1 n> and <1 w>" --> "<2 nw, 1 n, 1 w>" if _debugLevel == 3 then AnsiNote("\n"..str) end -- Now we can detect the different chunks more specifically without extra junk in the way. str = regexReplace(str, exitChunkRegex, replaceExitChunk) -- Replace door/exit chunks. str = regexReplace(str, visionChunkRegex, replaceVisionChunk) -- Replace vision limit chunks. entityChunkRegex:gmatch(str, parseEntityChunk) -- Parse what's left: entity chunks. return _rooms end return parse