710 lines
23 KiB
Lua

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 <yellow>map help<reset>.")
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 <yellow>map start <current area><reset>.
Once mapping is started, move <red>slowly<reset> 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 <yellow>map stop<reset> to stop recording your movements, and be sure to
<yellow>map save<reset>! Map data will not be saved automatically.
Other commands are available to adjust mapping as you go. <yellow>map shift <direction><reset>, for example,
will move your current room. See <yellow>map help<reset> 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: <green>Mapping<reset> in area <yellow>"..lotj.mapper.mappingArea.."<reset>\n")
else
cecho("Mapper status: <red>Off<reset>\n")
end
end
function lotj.mapper.printHelp()
lotj.mapper.log("Mapper Command List")
cecho([[
<yellow>map start [<area name>]<reset>
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 <yellow>map shift<reset> 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.
<yellow>map stop<reset>
Stop editing the map based on your movements.
<yellow>map save<reset>
Save the map to the map.dat file in your Mudlet profile's directory.
<yellow>map deletearea <area name><reset>
Deletes all data for an area. There's no confirmation and no undo!
<yellow>map shift <direction><reset>
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([[
<yellow>map setroomcoords <area name><reset>
<orange>(Staff Command)<reset> 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 <yellow><area name><reset>")
return
end
end
if lotj.mapper.mappingArea ~= nil then
lotj.mapper.logError("Mapper already running in <yellow>"..lotj.mapper.mappingArea.."<reset>.")
return
end
local areaTable = getAreaTable()
if areaTable[areaName] == nil then
addAreaName(areaName)
lotj.mapper.log("Mapping in new area <yellow>"..areaName.."<reset>.")
else
lotj.mapper.log("Mapping in existing area <yellow>"..areaName.."<reset>.")
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 <red>stopped<reset>. Don't forget to <yellow>map save<reset>!")
end
function lotj.mapper.deleteArea(areaName)
areaName = trim(areaName)
if #areaName == 0 then
lotj.mapper.log("Syntax: map deletearea <yellow><area name><reset>")
return
end
local areaTable = getAreaTable()
if areaTable[areaName] == nil then
lotj.mapper.logError("Area <yellow>"..areaName.."<reset> does not exist.")
return
end
if areaName == lotj.mapper.mappingArea then
lotj.mapper.stopMapping()
end
deleteArea(areaName)
lotj.mapper.log("Area <yellow>"..areaName.."<reset> deleted.")
end
function lotj.mapper.shiftCurrentRoom(direction)
direction = trim(direction)
if #direction == 0 then
lotj.mapper.log("Syntax: map shift <yellow><direction><reset>")
return
end
local dir = dirObj(direction)
if dir == nil then
lotj.mapper.logError("Direction unknown: <yellow>"..direction.."<reset>")
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: <yellow>"..lotj.mapper.current.name.."<reset>")
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 <yellow>"..amenityName.."<reset> on room <yellow>"..addAmenityRoom.name.."<reset>")
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 <yellow>"..lotj.mapper.current.planet.."<reset>. "..
"To begin mapping this area as you explore, type <yellow>map start<reset>.")
echo("\n")
end
end
end
lotj.mapper.processCurrentRoom()
end
------------------------------------------------------------------------------
-- Utility Functions
------------------------------------------------------------------------------
function lotj.mapper.log(text)
cecho("[<cyan>LOTJ Mapper<reset>] "..text.."\n")
end
function lotj.mapper.logDebug(text)
if lotj.mapper.debug then
lotj.mapper.log("<green>Debug:<reset> "..text)
end
end
function lotj.mapper.logError(text)
lotj.mapper.log("<red>Error:<reset> "..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