mudlet = mudlet or {}; mudlet.mapper_script = true lotj = lotj or {} lotj.mapper = lotj.mapper or {} local dirs = {} -- The order of these is important. The indices of the directions must match -- https://github.com/Mudlet/Mudlet/blob/9c13f8f946f5b82c0c2e817dab5f42588cee17e0/src/TRoom.h#L38 table.insert(dirs, {short="n", long="north", rev="s", xyzDiff = { 0, 1, 0}}) table.insert(dirs, {short="ne", long="northeast", rev="sw", xyzDiff = { 1, 1, 0}}) table.insert(dirs, {short="nw", long="northwest", rev="se", xyzDiff = {-1, 1, 0}}) table.insert(dirs, {short="e", long="east", rev="w", xyzDiff = { 1, 0, 0}}) table.insert(dirs, {short="w", long="west", rev="e", xyzDiff = {-1, 0, 0}}) table.insert(dirs, {short="s", long="south", rev="n", xyzDiff = { 0,-1, 0}}) table.insert(dirs, {short="se", long="southeast", rev="nw", xyzDiff = { 1,-1, 0}}) table.insert(dirs, {short="sw", long="southwest", rev="ne", xyzDiff = {-1,-1, 0}}) table.insert(dirs, {short="u", long="up", rev="d", xyzDiff = { 0, 0, 1}}) table.insert(dirs, {short="d", long="down", rev="u", xyzDiff = { 0, 0,-1}}) -- Given a direction short or long name, or a direction number, return an object representing it. local function dirObj(arg) if dirs[arg] ~= nil then return dirs[arg] end for _, dir in ipairs(dirs) do if arg == dir.short or arg == dir.long then return dir end end return nil end -- Given a direction short or long name, or a direction number, return an object representing its opposite local function revDirObj(arg) local dir = dirObj(arg) if dir ~= nil then return dirObj(dir.rev) end return nil end -- Configuration of an amenity name to the environment code to use on rooms with it local amenityEnvCodes = { bacta = { envCode = 269, symbol = "B" }, bank = { envCode = 267, symbol = "B" }, broadcast = { envCode = 270, symbol = "B" }, hotel = { envCode = 265, symbol = "H" }, library = { envCode = 261, symbol = "L" }, locker = { envCode = 263, symbol = "L" }, package = { envCode = 262, symbol = "P" }, workshop = { envCode = 266, symbol = "W" }, ["turbolift landing"] = { envCode = 259, symbol = "L" }, ["turbolift"] = { envCode = 263, symbol = "T" }, } local function trim(s) return (s:gsub("^%s*(.-)%s*$", "%1")) end ------------------------------------------------------------------------------ -- Command Handlers ------------------------------------------------------------------------------ -- Main "map" command handler function lotj.mapper.mapCommand(input) input = trim(input) if #input == 0 then lotj.mapper.printMainMenu() return end _, _, cmd, args = string.find(input, "([^%s]+)%s*(.*)") cmd = string.lower(cmd) if cmd == "help" then lotj.mapper.printHelp() elseif cmd == "start" then lotj.mapper.startMapping(args) elseif cmd == "stop" then lotj.mapper.stopMapping() elseif cmd == "deletearea" then lotj.mapper.deleteArea(args) elseif cmd == "shift" then lotj.mapper.shiftCurrentRoom(args) elseif cmd == "save" then lotj.mapper.saveMap() elseif cmd == "setroomcoords" then lotj.mapper.setRoomCoords(args) else lotj.mapper.logError("Unknown map command. Try map help.") end end function lotj.mapper.printMainMenu() lotj.mapper.log("Mapper Introduction and Status") cecho([[ The LOTJ Mapper plugin tracks movement using GMCP variables. To begin, try map start . Once mapping is started, move slowly between rooms to map them. Moving too quickly will cause the mapper to skip rooms. You should wait for the map to reflect your movements before moving again whenever you are in mapping mode. When you are finished mapping, use map stop to stop recording your movements, and be sure to map save! Map data will not be saved automatically. Other commands are available to adjust mapping as you go. map shift , for example, will move your current room. See map help for a full list of available commands. The map GUI also offers editing functionality and is ideal for moving groups of rooms, deleting or coloring rooms, etc. ]]) if lotj.mapper.mappingArea ~= nil then cecho("Mapper status: Mapping in area "..lotj.mapper.mappingArea.."\n") else cecho("Mapper status: Off\n") end end function lotj.mapper.printHelp() lotj.mapper.log("Mapper Command List") cecho([[ map start [] Begin mapping. Any new rooms you enter while mapping will be added to this area name, so you should be sure to stop mapping before entering a ship or moving to a different planet. No area name argument is required if you're on a planet, as we'll default to the planet name. Some tips to remember: - Use a light while mapping. Entering a dark room where you can't see will not update the map. - Use map shift to adjust room positioning, especially after going through turbolifts or voice-activated doors. It's faster to click-and-drag with the GUI to move large blocks of rooms, though. - Rooms in ships are all unique, even if they are the same model. In practice, mapping ships really isn't supported yet, although platforms or ships you use frequently may be worth it. map stop Stop editing the map based on your movements. map save Save the map to the map.dat file in your Mudlet profile's directory. map deletearea Deletes all data for an area. There's no confirmation and no undo! map shift Moves the current room in whichever direction you enter. Useful for adjusting placement of rooms when you need to space them out. ]]) if gmcp.Char.Info.immLevel and gmcp.Char.Info.immLevel >= 102 then cecho([[ map setroomcoords (Staff Command) Assuming you are happy with your local copy of a map, sends commands to the game to set the x/y/z coordinates of all rooms. (Requires the 'at' command.) ]]) end end function lotj.mapper.startMapping(areaName) areaName = trim(areaName) if #areaName == 0 then if lotj.mapper.current and lotj.mapper.current.planet then areaName = lotj.mapper.current.planet else lotj.mapper.log("Syntax: map start ") return end end if lotj.mapper.mappingArea ~= nil then lotj.mapper.logError("Mapper already running in "..lotj.mapper.mappingArea..".") return end local areaTable = getAreaTable() if areaTable[areaName] == nil then addAreaName(areaName) lotj.mapper.log("Mapping in new area "..areaName..".") else lotj.mapper.log("Mapping in existing area "..areaName..".") end lotj.mapper.mappingArea = areaName lotj.mapper.lastMoveDirs = {} lotj.mapper.processCurrentRoom() end function lotj.mapper.stopMapping() if lotj.mapper.mappingArea == nil then lotj.mapper.logError("Mapper not running.") return end lotj.mapper.mappingArea = nil lotj.mapper.lastMoveDirs = nil lotj.mapper.log("Mapping stopped. Don't forget to map save!") end function lotj.mapper.deleteArea(areaName) areaName = trim(areaName) if #areaName == 0 then lotj.mapper.log("Syntax: map deletearea ") return end local areaTable = getAreaTable() if areaTable[areaName] == nil then lotj.mapper.logError("Area "..areaName.." does not exist.") return end if areaName == lotj.mapper.mappingArea then lotj.mapper.stopMapping() end deleteArea(areaName) lotj.mapper.log("Area "..areaName.." deleted.") end function lotj.mapper.shiftCurrentRoom(direction) direction = trim(direction) if #direction == 0 then lotj.mapper.log("Syntax: map shift ") return end local dir = dirObj(direction) if dir == nil then lotj.mapper.logError("Direction unknown: "..direction.."") return end local vnum = lotj.mapper.current.vnum local room = lotj.mapper.getRoomByVnum(vnum) if room ~= nil then currentX, currentY, currentZ = getRoomCoordinates(vnum) dx, dy, dz = unpack(dir.xyzDiff) setRoomCoordinates(vnum, currentX+dx, currentY+dy, currentZ+dz) updateMap() centerview(vnum) end end function lotj.mapper.saveMap() saveMap(getMudletHomeDir() .. "/map.dat") lotj.mapper.log("Map saved.") end function lotj.mapper.setRoomCoords(areaName) if not gmcp.Char.Info.immLevel or gmcp.Char.Info.immLevel < 102 then lotj.mapper.logError("This command only works for imm characters.") return end local areaId = getAreaTable()[areaName] if not areaId then lotj.mapper.logError("Area not found by name "..areaName) return end for _, roomId in ipairs(getAreaRooms(areaId)) do local x, y, z = getRoomCoordinates(roomId) send("at "..roomId.." redit xyz "..x.." "..y.." "..z) end end ------------------------------------------------------------------------------ -- Event Handlers ------------------------------------------------------------------------------ function lotj.mapper.setup() if not geyserMapper then -- Preserve this as a global. We can only create one mapper in a profile, so if we -- unload and reload this UI, we need to reuse what was created before. geyserMapper = Geyser.Mapper:new({ x = 0, y = 0, width = "100%", height = "100%", }, lotj.layout.upperRightTabData.contents["map"]) else lotj.layout.upperRightTabData.contents["map"]:add(geyserMapper) geyserMapper:raiseAll() end setMapZoom(15) local hasAnyAreas = false for name, id in pairs(getAreaTable()) do if name ~= "Default Area" then hasAnyAreas = true end end if not hasAnyAreas then loadMap(getMudletHomeDir().."/@PKGNAME@/starter-map.dat") end lotj.mapper.loadShipData() lotj.setup.registerEventHandler("sysDataSendRequest", lotj.mapper.handleSentCommand) lotj.setup.registerEventHandler("gmcp.Room.Info", lotj.mapper.onEnterRoom) lotj.setup.registerEventHandler("sysExitEvent", lotj.mapper.saveShipData) end function lotj.mapper.teardown() lotj.layout.upperRightTabData.contents["map"]:remove(geyserMapper) geyserMapper:hide() end function lotj.mapper.loadShipData() local location = getMudletHomeDir() .. "/lotjmapper_ship.lua" lotj.mapper.shipLastRoom = lotj.mapper.shipLastRoom or {} if io.exists(location) then table.load(location, lotj.mapper.shipLastRoom) end end function lotj.mapper.saveShipData() if lotj.mapper.shipLastRoom then local location = getMudletHomeDir() .. "/lotjmapper_ship.lua" table.save(location, lotj.mapper.shipLastRoom) end end -- Track the most recent movement command so we know which direction we moved when automapping function lotj.mapper.handleSentCommand(event, cmd) -- If we're not mapping, store the last direction only for ships if lotj.mapper.mappingArea == nil then if not gmcp.Room.Info.planet then -- only store movement if we're actually on a ship lotj.mapper.shipMovement = lotj.mapper.shipMovement or {} table.insert(lotj.mapper.shipMovement, dirObj(trim(cmd))) end return end local dir = dirObj(trim(cmd)) if dir ~= nil then lotj.mapper.lastMoveDirs = lotj.mapper.lastMoveDirs or {} table.insert(lotj.mapper.lastMoveDirs, dir) lotj.mapper.logDebug("Pushed movement dir: "..dir.long) end end function lotj.mapper.popMoveDir() if not lotj.mapper.lastMoveDirs or #lotj.mapper.lastMoveDirs == 0 then lotj.mapper.logDebug("Popped movement dir: nil") return nil end local result = table.remove(lotj.mapper.lastMoveDirs, 1) lotj.mapper.logDebug("Popped movement dir: "..result.long) return result end -- Function used to handle virtual ship maps. This will process the room -- as a ship and attempt to position a player on an existing map of the same ship. function lotj.mapper.processCurrentRoomAsShip(roomData, movement) local roomVnum = table.keys(roomData)[1] local matchesMany = #table.keys(roomData) if matchesMany == 1 then -- Only one room matched. lotj.mapper.shipLastRoom = {actual = gmcp.Room.Info.vnum, virtual = roomVnum} centerview(roomVnum) elseif matchesMany > 1 then -- Multiple matches if lotj.mapper.shipLastRoom ~= nil and lotj.mapper.shipLastRoom.actual == gmcp.Room.Info.vnum then -- This is likely a reboot. The last room and the current room match. centerview(lotj.mapper.shipLastRoom.virtual) elseif lotj.mapper.shipLastRoom ~= nil and movement then -- position based on movement local nextRoom = getRoomExits(lotj.mapper.shipLastRoom.virtual) if table.contains(nextRoom, movement.long) then lotj.mapper.shipLastRoom = {acutal = gmcp.Room.Info.vnum, virtual = nextRoom[movement.long]} centerview(lotj.mapper.shipLastRoom.virtual) end end end end -- Function used to handle a room that we've moved into. This will use the data on -- lotj.mapper.current, compared with lotj.mapper.last, to potentially create a new room and -- link it with an exit on the previous room. function lotj.mapper.processCurrentRoom() local vnum = lotj.mapper.current.vnum local moveDir = lotj.mapper.popMoveDir() local room = lotj.mapper.getRoomByVnum(vnum) local searchData = searchRoom(gmcp.Room.Info.name, false, true) -- Only virtualize ships when we aren't actively mapping if not gmcp.Room.Info.planet and lotj.mapper.mappingArea == nil then if room == nil then -- Don't virtualize if it's actually mapped if not table.is_empty(searchData) then if lotj.mapper.shipMovement then lotj.mapper.processCurrentRoomAsShip(searchData, table.remove(lotj.mapper.shipMovement,1)) else lotj.mapper.processCurrentRoomAsShip(searchData, nil) end return end end else -- if we're not virtualizing, make sure this is empty lotj.mapper.shipMovement = {} end if lotj.mapper.mappingArea == nil and room == nil then lotj.mapper.logDebug("Room not found, but mapper not running.") return end local lastRoom = nil if lotj.mapper.last ~= nil then lastRoom = lotj.mapper.getRoomByVnum(lotj.mapper.last.vnum) end -- Try to account for moving between visible and non-visible rooms if moveDir ~= nil then if not table.contains(gmcp.Room.Info.exits, revDirObj(moveDir.long).long) then -- There was no return exit in this room matching the movement if not table.is_empty(lotj.mapper.lastMoveDirs) then -- There are additional movements in the table so test those while not table.is_empty(lotj.mapper.lastMoveDirs) do local tempDir = lotj.mapper.popMoveDir() if table.contains(gmcp.Room.Info.exits, revDirObj(tempDir.long).long) then -- This seems to be a match so use this one and empty out the last room as it is incorrect moveDir = tempDir lotj.mapper.last = nil lastRoom = nil end end end end end -- Create the room if we don't have it yet if room == nil then lotj.mapper.log("Added new room: "..lotj.mapper.current.name.."") addRoom(vnum) setRoomArea(vnum, lotj.mapper.mappingArea) setRoomCoordinates(vnum, 0, 0, 0) setRoomName(vnum, lotj.mapper.current.name) room = lotj.mapper.getRoomByVnum(vnum) -- Create stub exits in any known direction we see for dir, state in pairs(lotj.mapper.current.exits) do local exitDir = dirObj(dir) if exitDir ~= nil then setExitStub(vnum, exitDir.short, true) if state == "C" then setDoor(vnum, exitDir.short, 2) end end end local lastRoomAtPresetCoords = false if lastRoom ~= nil then -- Figure out if our last room was positioned by ingame room settings. local lastX, lastY, lastZ = getRoomCoordinates(lotj.mapper.last.vnum) if lotj.mapper.last.x == lastX and lotj.mapper.last.y == lastY and lotj.mapper.last.z == lastZ then lastRoomAtPresetCoords = true end end if lotj.mapper.current.x ~= nil and (not lastRoom or lastRoomAtPresetCoords) then -- This room has x/y/z set ingame and we're not coming from a lastRoom with -- a custom direction, so we should honor what the game said to use. setRoomCoordinates(vnum, lotj.mapper.current.x, lotj.mapper.current.y, lotj.mapper.current.z) elseif lastRoom ~= nil then -- Position the room relative to the room we came from local lastX, lastY, lastZ = getRoomCoordinates(lotj.mapper.last.vnum) -- If we recorded a valid movement command, use that direction to position this room if moveDir ~= nil then local dx, dy, dz = unpack(moveDir.xyzDiff) lotj.mapper.log("Positioning new room "..moveDir.long.." of the previous room based on movement command.") setRoomCoordinates(vnum, lastX+dx, lastY+dy, lastZ+dz) else -- We didn't have a valid movement command but we still changed rooms, so try to guess -- where this room should be relative to the last. -- Find a stub with a door on the last room which matches a stub with a door on this room -- This aims to handle cases where you've used a voice-activated locked door local lastDoors = getDoors(lotj.mapper.last.vnum) local currentDoors = getDoors(vnum) local matchingStubDir = nil for _, lastRoomStubDirNum in ipairs(getExitStubs1(lotj.mapper.last.vnum) or {}) do local lastRoomStubDir = dirObj(lastRoomStubDirNum) for _, currentRoomStubDirNum in ipairs(getExitStubs1(vnum) or {}) do local currentRoomStubDir = dirObj(currentRoomStubDirNum) if lastRoomStubDir.short == currentRoomStubDir.rev and lastDoors[lastRoomStubDir.short] == 2 and currentDoors[currentRoomStubDir.short] == 2 then matchingStubDir = lastRoomStubDir end end end if matchingStubDir ~= nil then local dx, dy, dz = unpack(matchingStubDir.xyzDiff) setRoomCoordinates(vnum, lastX+dx, lastY+dy, lastZ+dz) lotj.mapper.log("Positioning new room "..matchingStubDir.long.." of the previous room based on matching closed doors.") else -- If no matching stubs were found, just find a nearby location which isn't taken by either a stub or a real room. for dir in pairs({"n", "e", "w", "s", "ne", "nw", "se", "sw", "u", "d"}) do local dx, dy, dz = unpack(dirObj(dir).xyzDiff) local overlappingRoomId = lotj.mapper.getRoomByCoords(lotj.mapper.mappingArea, lastX+dx, lastY+dy, lastZ+dz) local hasOverlappingStub = false for _, stubDirNum in ipairs(getExitStubs1(lotj.mapper.last.vnum) or {}) do if dirObj(stubDirNum) == dirObj(dir) then hasOverlappingStub = true end end if overlappingRoomId == nil and not hasOverlappingStub then lotj.mapper.log("Exit unknown. Positioning new room "..dirObj(dir).long.." of the previous room.") setRoomCoordinates(vnum, lastX+dx, lastY+dy, lastZ+dz) break end end end end end end -- Link this room with the previous one if they have a matching set of exit stubs if lastRoom ~= nil and moveDir ~= nil then -- Always set the exit we took even if it wasn't a stub. The direction we just moved is our best -- evidence of how rooms are connected, overriding any reverse-created exits made earlier if they -- are different. setExit(lotj.mapper.last.vnum, vnum, moveDir.short) -- Only set the reverse exit (from current room back to where we came from) if it's a stub. -- In the case of mazes or asymmetrical exits, this may be wrong but will be fixed on moving back -- out through this exit. for _, currentRoomStubDirNum in ipairs(getExitStubs1(vnum) or {}) do local currentRoomStubDir = dirObj(currentRoomStubDirNum) if moveDir.rev == currentRoomStubDir.short then setExit(vnum, lotj.mapper.last.vnum, moveDir.rev) end end end centerview(vnum) end function lotj.mapper.checkAmenityLine(roomName, amenityName) if lotj.mapper.mappingArea == nil then return end amenityData = amenityEnvCodes[string.lower(amenityName)] if amenityData == nil then return end -- Sanity check that the current room matches the name we just saw local addAmenityRoom = nil if lotj.mapper.current.name == roomName then addAmenityRoom = lotj.mapper.current else return end -- This is being invoked on seeing a room name and we don't want it mushed into that line. echo("\n") lotj.mapper.log("Set amenity "..amenityName.." on room "..addAmenityRoom.name.."") setRoomEnv(addAmenityRoom.vnum, amenityData.envCode) setRoomChar(addAmenityRoom.vnum, amenityData.symbol) updateMap() end -- The vnum is always sent after the name and exits, so we can use it as a trigger for -- handling movement to a new room function lotj.mapper.onEnterRoom() lotj.mapper.logDebug("Handling entered room, vnum "..gmcp.Room.Info.vnum) if lotj.mapper.current ~= nil then lotj.mapper.last = lotj.mapper.current end lotj.mapper.current = { vnum = gmcp.Room.Info.vnum, name = gmcp.Room.Info.name:gsub("&.", ""), exits = gmcp.Room.Info.exits or {}, planet = gmcp.Room.Info.planet, } -- This room has coordinates set in the game which we should use. if gmcp.Room.Info.x ~= nil then lotj.mapper.current.x = gmcp.Room.Info.x lotj.mapper.current.y = gmcp.Room.Info.y lotj.mapper.current.z = gmcp.Room.Info.z end -- If the new room has has a planet different than the last one and we don't have -- an area for that planet yet, give a prompt about how to start mapping it. if lotj.mapper.current.planet then if lotj.mapper.last and lotj.mapper.last.planet ~= lotj.mapper.current.planet then if getAreaTable()[lotj.mapper.current.planet] == nil then lotj.mapper.log("Welcome to "..lotj.mapper.current.planet..". ".. "To begin mapping this area as you explore, type map start.") echo("\n") end end end lotj.mapper.processCurrentRoom() end ------------------------------------------------------------------------------ -- Utility Functions ------------------------------------------------------------------------------ function lotj.mapper.log(text) cecho("[LOTJ Mapper] "..text.."\n") end function lotj.mapper.logDebug(text) if lotj.mapper.debug then lotj.mapper.log("Debug: "..text) end end function lotj.mapper.logError(text) lotj.mapper.log("Error: "..text) end function lotj.mapper.getRoomByVnum(vnum) return getRooms()[vnum] end function lotj.mapper.getRoomByCoords(areaName, x, y, z) local areaRooms = getAreaRooms(getAreaTable()[areaName]) or {} for _, roomId in pairs(areaRooms) do local roomX, roomY, roomZ = getRoomCoordinates(roomId) if roomX == x and roomY == y and roomZ == z then return roomId end end return nil end function doSpeedWalk() lotj.mapper.log("Speedwalking using these directions: " .. table.concat(speedWalkDir, ", ") .. "\n") for _, dir in ipairs(speedWalkDir) do send(dir, false) end end