Generic Mapping Script

All and any discussion and development of the Mudlet Mapper.

Generic Mapping Script

Postby Jor'Mox » Thu Sep 01, 2016 6:10 pm

I made a mapping script for a MUD (DIKU style) I was playing, and I saw a request for one for a different MUD (New Worlds Ateraan), so I modified what I had to make a generic script that could be adapted for just about any MUD with a similar basic setup (room names and exits can be captured reasonably reliably, and movement uses basic commands with one move per command). As indicated in the help section at the top of the script, triggers need to be made to capture the relevant pieces for your specific MUD, and then raise the specified event. I hope some people find this useful. If there are any questions or problems, I'll see what I can do to help address them.
Syntax: [ Download ] [ Hide ]
Using lua Syntax Highlighting
-- Jor'Mox's Generic Map Script
-- 2/06/2017
-- v1.06
--

map = map or {}

map.help = [[
    Jor'Mox's Generic Map Script

    This script allows for semi-automatic mapping using just room names and exits. It is
    a good idea to know the general layout of the area you are trying to map before
    turning on mapping with this script, so as to minimize how much you need to move
    things around to make it look how you want it to. The script will automatically
    stretch out a map to make space for a room if it would overlap with another one, but
    it is important to make sure that things line up properly, or you will have inaccurate
    maps with duplicate sections in them.

    It is up to YOU to create triggers that appropriately raise the following events, and
    gather the necessary information. The way in which such things are displayed varies
    dramatically from game to game, so any set of triggers for one game will likely not
    work for the next.

    Important Events for Proper Mapping
        onMoveFail - raise this event to indicate that you attempted to move, but no move
            was made
        onVisionFail - raise this event to indicate that you moved successfully, but are
            unable to gather some or all of the necessary info about the room
        onRandomMove - raise this event to indicate that you moved, but do not know what
            direction you moved in
        onNewRoom - raise this event to indicate that a room has been detected, typically
            after moving or looking to see the room you are currently in
        onPrompt - raise this event to indicate a prompt has been detected, room name and
            exits must be stored in map.prompt.room and map.prompt.exits before raising
            this event
        onForcedMove - raise this event to indicate that you have been moved without
            entering a command, but you know the direction you went. pass the relevant
            direction as the first argument. if this movement needs to preempt other
            movement commands in the queue (often the case), then pass "true" as a string
            as the second argument

    Important Commands (Aliases) for Proper Mapping

        Fundamental Aliases
        start mapping <optional area name> - use this command to start adding new content
            to the map, area name is required if there are no rooms in the map
        stop mapping - use this command to stop mapping
        save map - saves the map to a file (map.dat, located in the profile folder), this
            is generally only needed to share the map with someone else, or to act as a
            backup in case something happens to the map that Mudlet maintains for your
            profile
        load map <optional 'local'> - loads the map from the location specified in the
            download_path, or from the local copy
        export area <area name> - exports a file to the profile folder with data for the
            named area
        import area <area name> - imports area data from a file created with export area,
            must be located in profile folder


        Mapping Aliases
        map mode <simple, normal, or complex> - sets the mapping mode, determining what
            exits are set automatically as you move around
        set area <area name> - move the current room into the named area, area will be
            created if it does not currently exist
        shift <direction> - use this command to move the room you are currently in around
            on the map
        merge rooms - use this command to combine the room you are in with any other rooms
            in the same location and with the same name
        clear moves - use this command to clear the move queue after you attempt a move
            that doesn't succeed, but for which there is no trigger indicating this with
            the onMoveFail event
        add door <direction> <optional none, open, closed, locked> <optional yes, no> -
            adds a door in the given direction, defaulting to closed (use none to remove
            a door), and defaulting '
no' for one-way status
        add portal <entry command> - adds a portal that uses the given command for entry
        set exit <direction> <roomID> - sets the given direction to connect, one way,
            to the room with the specified roomID, used for very complex areas

        Normal User Aliases
        find me - use this command to search the entire map to try to locate you based on
            room name and exits, typically not necessary, as this will be done anyway if
            a person moves and their location is unknown
        find path <room name> OR <room name> ; <area name> - used to find a walking path
            to a room with the given name, in the given area if specified
        set character <name> - sets the current character name (stored as map.character)
        set recall - sets the current room as the recall room for the current character

    Important Information Regarding Speedwalking

        The existing doSpeedWalk function (which is located at the bottom of the script,
        just above the event handler function) currently only displays a list of moves
        to make to get to the target room. This function should be modified to implement
        speedwalking in your game. For this purpose, a table of roomIDs is generated and
        and stored in speedWalkPath, and a matching table of directions is stored in
        speedWalkDir. You can check for doors with a roomID and a direction using the
        check_doors function in the following manner: check_doors(roomID,dir) or
        check_doors(roomID,tbl_of_dirs). It will return false if a door is not present,
        or a table of directions and door status codes, like what you would get using the
        getDoors function, but only for the exit(s) specified.
]]

map.recall = map.recall or {}
map.character = map.character or ""
map.prompt = map.prompt or {}

map.configs = {
    mode = "normal", -- can be simple, normal, or complex
    x = 16,
    y = 0,
    w = "30%",
    h = "40%",
    origin = "topright",
    download_path = "",
    stretch_map = true,
}

local function make_aliases()
    map.aliases = map.aliases or {}
    local id
    local tbl = {
        ["Start Mapping Alias"] = {[[^start mapping(?: (.*))?$]], [[map.start_mapping(matches[2])]]},
        ["Stop Mapping Alias"] = {[[^stop mapping$]], [[map.stop_mapping()]]},
        ["Save Map Alias"] = {[[^save map$]], [[saveMap(getMudletHomeDir() .. "/map.dat")]]},
        ["Load Map Alias"] = {[[^load map(?: (local))?$]], [[map.load_map(matches[2])]]},
        ["Export Map Area Alias"] = {[[^export area (.*)]],[[map.export_area(matches[2])]]},
        ["Import Map Area Alias"] = {[[^import area (.*)]],[[map.import_area(matches[2])]]},

        ["Set Room Area Alias"] = {[[^set area (.*)$]], [[map.set_area(matches[2])]]},
        ["Set Map Mode Alias"] = {[[^map mode (\w+)$]],[[map.set_mode(matches[2])]]},
        ["Shift Room Alias"] = {[[^shift (.*)$]], [[map.shift_room(matches[2])]]},
        ["Merge Rooms Alias"] = {[[^merge rooms$]], [[map.merge_rooms()]]},
        ["Add Door Alias"] = {[[^add door (\w+)(?: (none|open|closed|locked)(?: (yes|no))?)?$]],[[map.set_door(matches[2],matches[3],matches[4])]]},
        ["Add Portal Alias"] = {[[^add portal (.*)$]],[[map.set_portal(matches[2])]]},
        ["Set Room Exit Alias"] = {[[^set exit (\w+) (\d+)]],[[map.set_exit(matches[2],matches[3])]]},
        ["Clear Moves Alias"] = {[[^clear moves$]], [[map.clear_moves()]]},

        ["Find Me Alias"] = {[[^find me$]], [[map.find_me()]]},
        ["Find Path Alias"] = {[[find path ([^;]+)(?:\s*;\s*(.+))?]],[[map.find_path(matches[2],matches[3])]]},
        ["Set Recall Alias"] = {[[^set recall$]],[[map.set_recall()]]},
        ["Set Character Alias"] = {[[^set character (.*)$]],[[map.character = matches[2]]},
    }
    for k,v in pairs(tbl) do
        if map.aliases[k] and exists(map.aliases[k],"alias") ~= 0 then
            killAlias(map.aliases[k])
        end
        id = tempAlias(v[1],v[2])
        map.aliases[k] = id
    end
end

local function config()
    local configs = map.configs
    map_mode = configs.mode
    --print(configs.x,configs.y,configs.w,configs.h,configs.origin)
    windowManager.add("mini_map","mapper",configs.x,configs.y,configs.w,configs.h,configs.origin)
    windowManager.show("mini_map")

    make_aliases()
end

local move_queue = {}
local prevRoom, prevName, prevExits
local currentRoom, currentName, currentExits, currentArea
local mapping, map_mode, downloading
local find_portal, vision_fail, room_detected, random_move

local exitmap = {
    n = '
north',    ne = 'northeast',   nw = 'northwest',   e = 'east',
    w = '
west',     s = 'south',        se = 'southeast',   sw = 'southwest',
    u = '
up',       d = 'down',         ["in"] = 'in',      out = 'out',
}

local short = {}
for k,v in pairs(exitmap) do
    short[v] = k
end

local stubmap = {
    north = 1,      northeast = 2,      northwest = 3,      east = 4,
    west = 5,       south = 6,          southeast = 7,      southwest = 8,
    up = 9,         down = 10,          ["in"] = 11,        out = 12,
    [1] = "north",  [2] = "northeast",  [3] = "northwest",  [4] = "east",
    [5] = "west",   [6] = "south",      [7] = "southeast",  [8] = "southwest",
    [9] = "up",     [10] = "down",      [11] = "in",        [12] = "out",
}

local coordmap = {
    [1] = {0,1,0},      [2] = {1,1,0},      [3] = {-1,1,0},     [4] = {1,0,0},
    [5] = {-1,0,0},     [6] = {0,-1,0},     [7] = {1,-1,0},     [8] = {-1,-1,0},
    [9] = {0,0,1},      [10] = {0,0,-1},    [11] = {0,0,0},     [12] = {0,0,0},
}

local reverse_dirs = {
    north = "south", south = "north", west = "east", east = "west", up = "down",
    down = "up", northwest = "southeast", northeast = "southwest", southwest = "northeast",
    southeast = "northwest", ["in"] = "out", out = "in",
}

local function set_room(roomID)
    -- moves the map to the new room
    if currentRoom ~= roomID then
        prevRoom = currentRoom
        currentRoom = roomID
    end
    if getRoomName(currentRoom) ~= currentName then
        prevName = currentName
        prevExits = currentExits
        currentName = getRoomName(currentRoom)
        currentExits = getRoomExits(currentRoom)
    end
    currentArea = getRoomArea(currentRoom)
    centerview(currentRoom)
end

local function add_door(roomID, dir, status)
    -- create or remove a door in the designated direction
    -- consider options for adding pickable and passable information
    dir = exitmap[dir] or dir
    if not table.contains(exitmap,dir) then error("Add Door: invalid direction.",2) end
    status = assert(table.index_of({"none","open","closed","locked"},status),"Add Door: Invald status, must be none, open, closed, or locked") - 1
    local exits = getRoomExits(roomID)
    if not exits[dir] then
        setExitStub(roomID,stubmap[dir],true)
    end
    if table.contains({"up","down","in","out"},dir) then
        setRoomUserData(roomID,"door " .. dir, tostring(status))
    else
        setDoor(roomID,short[dir],status)
    end
end

local function check_doors(roomID,exits)
    -- looks to see if there are doors in designated directions
    -- used for room comparison, can also be used for pathing purposes
    if type(exits) == "string" then exits = {exits} end
    local statuses = {}
    local doors = getDoors(roomID)
    for k,v in ipairs({"up","down","in","out"}) do
        doors[short[v]] = tonumber(getRoomUserData(roomID,"door " .. v) or "")
    end
    local dir
    for k,v in pairs(exits) do
        dir = short[k] or short[v]
        if not doors[dir] or doors[dir] == 0 then
            return false
        else
            statuses[dir] = doors[dir]
        end
    end
    return statuses
end

local function find_room(name, area)
    -- looks for rooms with a particular name, and if given, in a specific area
    local rooms = searchRoom(name)
    if type(area) == "string" then
        local areas = getAreaTable() or {}
        for k,v in pairs(areas) do
            if string.lower(k) == string.lower(area) then
                area = v
                break
            end
        end
        area = areas[area] or nil
    end
    for k,v in pairs(rooms) do
        if string.lower(v) ~= string.lower(name) then
            rooms[k] = nil
        elseif area and getRoomArea(k) ~= area then
            rooms[k] = nil
        end
    end
    return rooms
end

local function getRoomStubs(roomID)
    -- turns stub info into table similar to exit table
    local stubs = getExitStubs(roomID)
    if type(stubs) ~= "table" then stubs = {} end
    local instub = getRoomUserData(roomID,"stubin")
    if instub ~= "" then
        table.insert(stubs,tonumber(instub))
    end
    local outstub = getRoomUserData(roomID,"stubout")
    if outstub ~= "" then
        table.insert(stubs,tonumber(outstub))
    end
    local exits = {}
    for k,v in pairs(stubs) do
        exits[stubmap[v]] = 0
    end
    return exits
end

local function connect_rooms(ID1, ID2, dir1, dir2, no_check)
    -- makes a connection between rooms
    -- can make backwards connection without a check
    local match = false
    if not ID1 and ID2 and dir1 then error("Connect Rooms: Missing Required Arguments.",2) end
    dir2 = dir2 or reverse_dirs[dir1]
    setExit(ID1,ID2,stubmap[dir1])
    if stubmap[dir1] > 10 then
        setRoomUserData(ID1,"stub"..dir1,"")
    end
    if map_mode ~= "complex" then
        local stubs = getRoomStubs(ID2)
        if stubs[dir2] then match = true end
        if (match or no_check) then
            setExit(ID2,ID1,stubmap[dir2])
            if stubmap[dir2] > 10 then
                setRoomUserData(ID2,"stub"..dir2,"")
            end
        end
    end
end

local function check_room(roomID, name, exits)
    -- check to see if room name and exits match expecations
    if not roomID then error("Check Room Error: No ID",2) end
    if name ~= getRoomName(roomID) then return false end
    local t_exits = table.union(getRoomExits(roomID),getRoomStubs(roomID))
    for k,v in ipairs(exits) do
        if short[v] and not table.contains(t_exits,v) then return false end
        t_exits[v] = nil
    end
    return table.is_empty(t_exits) or check_doors(roomID,t_exits)
end

local function stretch_map(dir,x,y,z)
    -- stretches a map to make room for just added room that would overlap with existing room
    local dx,dy,dz
    for k,v in pairs(getAreaRooms(currentArea)) do
        if v ~= currentRoom then
            dx,dy,dz = getRoomCoordinates(v)
            if dx >= x and string.find(dir,"east") then
                dx = dx + 1
            elseif dx <= x and string.find(dir,"west") then
                dx = dx - 1
            end
            if dy >= y and string.find(dir,"north") then
                dy = dy + 1
            elseif dy <= y and string.find(dir,"south") then
                dy = dy - 1
            end
            if dz >= z and string.find(dir,"up") then
                dz = dz + 1
            elseif dz <= z and string.find(dir,"down") then
                dz = dz - 1
            end
            setRoomCoordinates(v,dx,dy,dz)
        end
    end
end

local function create_room(name, exits, dir, coords)
    -- makes a new room with captured name and exits
    -- links with other rooms as appropriate
    -- links to adjacent rooms in direction of exits if in simple mode
    if mapping then
        print("New Room: " .. name .. "\n")
        local newID = createRoomID()
        addRoom(newID)
        setRoomArea(newID, currentArea)
        setRoomName(newID, name)
        for k,v in ipairs(exits) do
            if stubmap[v] then
                if stubmap[v] <= 10 then
                    setExitStub(newID, stubmap[v], true)
                else
                    setRoomUserData(newID, "stub"..v,stubmap[v])
                end
            end
        end
        if dir then
            connect_rooms(currentRoom, newID, dir)
        elseif find_portal then
            addSpecialExit(currentRoom, newID, find_portal)
            setRoomUserData(newID,"portals",tostring(currentRoom)..":"..find_portal)
        end
        setRoomCoordinates(newID,unpack(coords))
        if map.configs.stretch_map and table.size(getRoomsByPosition(currentArea,unpack(coords))) > 1 then
            set_room(newID)
            stretch_map(dir,unpack(coords))
        end
        if map_mode == "simple" then
            local x,y,z = unpack(coords)
            local dx,dy,dz,rooms
            for k,v in ipairs(exits) do
                if v ~= dir then
                    dx,dy,dz = unpack(coordmap[stubmap[v]])
                    rooms = getRoomsByPosition(currentArea,x+dx,y+dy,z+dz)
                    if table.size(rooms) == 1 then
                        connect_rooms(newID,rooms[0],v)
                    end
                end
            end
        end
        set_room(newID)
    end
end

local function find_area_limits(areaID)
    -- used to find min and max coordinate limits for an area
    if not areaID then error("Find Limits: Missing area ID",2) end
    local rooms = getAreaRooms(areaID)
    local minx, miny, minz = getRoomCoordinates(rooms[0])
    local maxx, maxy, maxz = minx, miny, minz
    local x,y,z
    for k,v in pairs(rooms) do
        x,y,z = getRoomCoordinates(v)
        minx = math.min(x,minx)
        maxx = math.max(x,maxx)
        miny = math.min(y,miny)
        maxy = math.max(y,maxy)
        minz = math.min(z,minz)
        maxz = math.max(z,maxz)
    end
    return minx, maxx, miny, maxy, minz, maxz
end

local function find_link(name, exits, dir, max_distance)
    -- search for matching room in desired direction
    local x,y,z = getRoomCoordinates(currentRoom)
    if mapping and x then
        local dx,dy,dz = unpack(coordmap[stubmap[dir]])
        local minx, maxx, miny, maxy, minz, maxz = find_area_limits(currentArea)
        local rooms, match, stubs
        if max_distance then
            minx = x - max_distance
            maxx = x + max_distance
            miny = y - max_distance
            maxy = y + max_distance
            minz = z - max_distance
            maxz = z + max_distance
        end
        repeat
            x = x + dx
            y = y + dy
            z = z + dz
            rooms = getRoomsByPosition(currentArea,x,y,z)
        until (x > maxx or x < minx or y > maxy or y < miny or z > maxz or z < minz or not table.is_empty(rooms))
        for k,v in pairs(rooms) do
            if check_room(v,name,exits) then
                match = v
                break
            end
        end
        if match then
            connect_rooms(currentRoom, match, dir)
            set_room(match)
        else
            x,y,z = getRoomCoordinates(currentRoom)
            create_room(name, exits, dir,{x+dx,y+dy,z+dz})
        end
    end
end

local function get_recall()
    table.load(getMudletHomeDir() .. "/map_recalls.dat",map.recall)
    return map.recall[map.character]
end

local function move_map()
    -- tries to move the map to the next room
    local move = table.remove(move_queue,1)
    if move or random_move then
        local exits = (currentRoom and getRoomExits(currentRoom)) or {}
        local special = (currentRoom and getSpecialExitsSwap(currentRoom)) or {}
        if move and not exits[move] and not special[move] then
            for k,v in pairs(special) do
                if string.starts(k,move) then
                    move = k
                    break
                end
            end
        end

        if find_portal then
            map.find_me(currentName,currentExits,move)
            find_portal = false
        elseif move == "recall" then
            set_room(get_recall())
        else
            if exits[move] and (vision_fail or check_room(exits[move], currentName, currentExits)) then
                set_room(exits[move])
            elseif special[move] and (vision_fail or check_room(special[move], currentName, currentExits)) then
                set_room(special[move])
            elseif not vision_fail then
                if mapping and move then
                    find_link(currentName, currentExits, move)
                else
                    map.find_me(currentName,currentExits, move)
                end
            end
        end
        vision_fail = false
    end
end

local function capture_move_cmd(dir,priority)
    -- captures valid movement commands
    dir = string.lower(dir)
    if dir == "/" then dir = "recall" end
    if table.contains(exitmap,dir) or string.starts(dir,"enter ") or dir == "recall" then
        if priority then
            table.insert(move_queue,1,exitmap[dir] or dir)
        else
            table.insert(move_queue,exitmap[dir] or dir)
        end
    elseif currentRoom then
        local special = getSpecialExitsSwap(currentRoom) or {}
        if special[dir] then
            if priority then
                table.insert(move_queue,1,dir)
            else
                table.insert(move_queue,dir)
            end
        end
    end
end

local function capture_room_info(name, exits)
    -- captures room info, and tries to move map to match
    if (not vision_fail) and name and exits then
        prevName = currentName
        prevExits = currentExits
        name = string.trim(name)
        currentName = name
        exits = string.gsub(exits,"and","")
        currentExits = (exits ~= "" and string.split(exits,"[, ]+")) or {}
        move_map()
    elseif vision_fail then
        move_map()
    end
end

local function find_area(name)
    -- searches for the named area, and creates it if necessary
    local areas = getAreaTable()
    local areaID
    for k,v in pairs(areas) do
        if string.lower(name) == string.lower(k) then
            areaID = v
            break
        end
    end
    if not areaID then areaID = addAreaName(name) end
    if not areaID then error("Invalid Area. No such area found, and area could not be added.") end
    currentArea = areaID
end

function map.load_map(use_local)
    local path = getMudletHomeDir() .. "/map.dat"
    if use_local then
        loadMap(path)
        print("Map reloaded from local copy.")
    else
        local address = map.configs.download_path .. "map.dat"
        downloading = true
        downloadFile(path,address)
        print("Downloading Map File.")
    end
end

function map.set_exit(dir,roomID)
    -- used to set unusual exits from the room you are standing in
    if mapping then
        roomID = assert(tonumber(roomID),"Set Exit: Invalid Room ID")
        if not table.contains(exitmap,dir) then error("Set Exit: Invalid Direction") end
        dir = short[exitmap[dir] or dir]
        setExit(currentRoom,roomID,dir)
    end
end

function map.find_path(roomName,areaName,return_tables)
    areaName = (areaName ~= "" and areaName) or nil
    local rooms = find_room(roomName,areaName)
    local found,dirs = false,{}
    local path = {}
    for k,v in pairs(rooms) do
        found = getPath(currentRoom,k)
        if found and (#dirs == 0 or #dirs > #speedWalkDir) then
            dirs = speedWalkDir
            path = speedWalkPath
        end
    end
    if return_tables then
        if table.is_empty(path) then
            path, dirs = nil, nil
        end
        return path, dirs
    else
        if #dirs > 0 then
            print("Path to " .. roomName .. ((areaName and " in " .. areaName) or "") .. ": " .. table.concat(dirs,", "))
        else
            print("No path found to " .. roomName .. ((areaName and " in " .. areaName) or "") .. ".")
        end
    end
end

function map.export_area(name)
    -- used to export a single area to a file
    local areas = getAreaTable()
    name = string.lower(name)
    for k,v in pairs(areas) do
        if name == string.lower(k) then name = k end
    end
    if not areas[name] then error("No such area.") end
    local rooms = getAreaRooms(areas[name])
    local tmp = {}
    for k,v in pairs(rooms) do
        tmp[v] = v
    end
    rooms = tmp
    local tbl = {}
    tbl.name = name
    tbl.rooms = {}
    tbl.exits = {}
    tbl.special = {}
    local rname, exits, stubs, doors, special, portals, door_up, door_down, coords
    for k,v in pairs(rooms) do
        rname = getRoomName(v)
        exits = getRoomExits(v)
        stubs = getExitStubs(v)
        doors = getDoors(v)
        door_up = getRoomUserData(v,"door up") or ""
        door_down = getRoomUserData(v,"door down") or ""
        door_in = getRoomUserData(v,"door in") or ""
        door_out = getRoomUserData(v,"door out") or ""
        special = getSpecialExitsSwap(v)
        portals = getRoomUserData(v,"portals") or ""
        coords = {getRoomCoordinates(v)}
        tbl.rooms[v] = {name = rname, coords = coords, exits = exits, stubs = stubs, doors = doors, door_up = door_up, door_down = door_down, door_in = door_in, door_out = door_out, special = special, portals = portals}
        tmp = {}
        for k1,v1 in pairs(exits) do
            if not table.contains(rooms,v1) then
                tmp[k1] = {v1, getRoomName(v1)}
            end
        end
        if not table.is_empty(tmp) then
            tbl.exits[v] = tmp
        end
        tmp = {}
        for k1,v1 in pairs(special) do
            if not table.contains(rooms,v1) then
                tmp[k1] = {v1, getRoomName(v1)}
            end
        end
        if not table.is_empty(tmp) then
            tbl.special[v] = tmp
        end
    end
    local path = getMudletHomeDir().."/"..string.gsub(string.lower(name),"%s","_")..".dat"
    table.save(path,tbl)
    print("Area " .. name .. " exported to " .. path)
end

function map.import_area(name)
    name = getMudletHomeDir() .. "/" .. string.gsub(string.lower(name),"%s","_") .. ".dat"
    local tbl = {}
    table.load(name,tbl)
    local areas = getAreaTable()
    local areaID = areas[tbl.name] or addAreaName(tbl.name)
    local rooms = {}
    local ID
    for k,v in pairs(tbl.rooms) do
        ID = createRoomID()
        rooms[k] = ID
        addRoom(ID)
        setRoomName(ID,v.name)
        setRoomArea(ID,areaID)
        setRoomCoordinates(ID,unpack(v.coords))
        if type(v.stubs) == "table" then
            for i,j in pairs(v.stubs) do
                setExitStub(ID,j,true)
            end
        end
        for i,j in pairs(v.doors) do
            setDoor(ID,i,j)
        end
        setRoomUserData(ID,"door up",v.door_up)
        setRoomUserData(ID,"door down",v.door_down)
        setRoomUserData(ID,"door in",v.door_in)
        setRoomUserData(ID,"door out",v.door_out)
        setRoomUserData(ID,"portals",v.portals)
    end
    for k,v in pairs(tbl.rooms) do
        for i,j in pairs(v.exits) do
            if rooms[j] then
--                print("Setting Exit " .. rooms[k] .. " " .. rooms[j] .. " " .. i)
                connect_rooms(rooms[k],rooms[j],i)
            end
        end
        for i,j in pairs(v.special) do
            if rooms[j] then
                addSpecialExit(rooms[k],rooms[j],i)
            end
        end
    end
    for k,v in pairs(tbl.exits) do
        for i,j in pairs(v) do
            if getRoomName(j[1]) == j[2] then
                connect_rooms(rooms[k],j[1],i)
            end
        end
    end
    for k,v in pairs(tbl.special) do
        for i,j in pairs(v) do
            addSpecialExit(k,j[1],i)
        end
    end
    map.fix_portals()
    print("Area " .. tbl.name .. " imported from " .. name)
end

function map.set_recall()
    -- assigned the current room to be recall for the current character
    map.recall[map.character] = currentRoom
    table.save(getMudletHomeDir() .. "/map_recalls.dat",map.recall)
    print("Recall room set to: " .. getRoomName(currentRoom) .. ".")
end

function map.set_portal(name)
    -- creates a new portal in the room
    if mapping then
        find_portal = name
        move_queue = {name}
        send(name)
    end
end

function map.set_mode(mode)
    -- switches mapping modes
    if not table.contains({"simple","normal","complex"},mode) then error("Invalid Map Mode, must be '
simple', 'normal', or 'complex'.") end
    map_mode = mode
    print("Current mode set to: " .. mode)
end

function map.start_mapping(area_name)
    -- starts mapping, and sets the current area to the given one, or uses the current one
    if not currentName then error("No room detected!") end
    local rooms
    move_queue = {}
    area_name = area_name ~= "" and area_name or nil
    if currentArea and not area_name then
        local areas = getAreaTableSwap()
        area_name = areas[currentArea]
    end
    if not area_name then print("Start Mapping Error: No area set!") return end
    print("Now mapping in area: " .. area_name)
    mapping = true
    find_area(area_name)
    rooms = find_room(currentName, currentArea)
    if table.is_empty(rooms) then
        if currentRoom then
            map.set_area(area_name)
        else
            create_room(currentName, currentExits, nil, {0,0,0})
        end
    elseif currentRoom and currentArea ~= getRoomArea(currentRoom) then
        map.set_area(area_name)
    end
end

function map.stop_mapping()
    mapping = false
    print("Mapping off.")
end

function map.clear_moves()
    move_queue = {}
    print("Move queue cleared.")
end

function map.set_area(name)
    -- assigns the current room to the area given, creates the area if necessary
    if mapping then
        find_area(name)
        if currentRoom and getRoomArea(currentRoom) ~= currentArea then
            setRoomArea(currentRoom,currentArea)
            set_room(currentRoom)
        end
    end
end

function map.set_door(dir,status,one_way)
    -- adds a door on a given exit
    if mapping then
        if not currentRoom then error("Make Door: No room found.") end
        dir = exitmap[dir] or dir
        if not stubmap[dir] then error("Make Door: Invalid direction.") end
        status = (status ~= "" and status) or "closed"
        one_way = (one_way ~= "" and one_way) or "no"
        if not table.contains({"yes","no"},one_way) then error("Make Door: Invalid one-way status, must be yes or no.") end

        local exits = getRoomExits(currentRoom)
        local target_room = exits[dir]
        if target_room then
            exits = getRoomExits(target_room)
        end
        if one_way == "no" and (target_room and exits[reverse_dirs[dir]] == currentRoom) then
            add_door(target_room,reverse_dirs[dir],status)
        end
        add_door(currentRoom,dir,status)
    end
end

function map.shift_room(dir)
    -- shifts a room around on the map
    if mapping then
        dir = assert(exitmap[dir] or (table.contains(exitmap,dir) and dir),"Exit Not Found")
        local x,y,z = getRoomCoordinates(currentRoom)
        dir = stubmap[dir]
        local coords = coordmap[dir]
        x = x + coords[1]
        y = y + coords[2]
        z = z + coords[3]
        setRoomCoordinates(currentRoom,x,y,z)
        centerview(currentRoom)
    end
end

local function check_link(firstID, secondID, dir)
    -- check to see if two rooms are connected in a given direction
    if not firstID then error("Check Link Error: No first ID",2) end
    if not secondID then error("Check Link Error: No second ID",2) end
    local name = getRoomName(firstID)
    local exits1 = table.union(getRoomExits(firstID),getRoomStubs(firstID))
    local exits2 = table.union(getRoomExits(secondID),getRoomStubs(secondID))
    local checkID = exits2[reverse_dirs[dir]]
    local exits = {}
    for k,v in pairs(exits1) do
        table.insert(exits,k)
    end
    return checkID and check_room(checkID,name,exits)
end

function map.find_me(name, exits, dir)
    -- tries to locate the player using the current room name and exits, and if provided, direction of movement
    -- if direction of movement is given, narrows down possibilities using previous room info
    if move ~= "recall" then move_queue = {} end
    local check = dir and currentRoom and table.contains(exitmap,dir)
    name = name or currentName
    exits = exits or currentExits
    local rooms = find_room(name)
    local match_IDs = {}
    for k,v in pairs(rooms) do
        if check_room(k, name, exits) then
            table.insert(match_IDs,k)
        end
    end
    rooms = match_IDs
    match_IDs = {}
    if table.size(rooms) > 1 and check then
        for k,v in pairs(rooms) do
            if check_link(currentRoom,v,dir) then
                table.insert(match_IDs,v)
            end
        end
    elseif random_move then
        for k,v in pairs(getRoomExits(currentRoom)) do
            if check_room(v,currentName,currentExits) then
                table.insert(match_IDs,v)
            end
        end
    end
    if table.size(match_IDs) == 0 then
        match_IDs = rooms
    end
    if not table.is_empty(match_IDs) and not find_portal then
        set_room(match_IDs[1])
    elseif find_portal then
        if not table.is_empty(match_IDs) then
            print("FOUND PORTAL DESTINATION, LINKING ROOMS")
            addSpecialExit(currentRoom,match_IDs[1],find_portal)
            local portals = getRoomUserData(match_IDs[1],"portals") or ""
            portals = portals .. "," .. tostring(currentRoom)..":"..find_portal
            setRoomUserData(match_IDs[1],"portals",portals)
            set_room(match_IDs[1])
        else
            print("CREATING PORTAL DESTINATION")
            create_room(currentName, currentExits, nil, {getRoomCoordinates(currentRoom)})
        end
        find_portal = false
    else
--       for k,v in pairs(rooms) do
--           display(check_room(k,name,exits))
--           if check then display(check_link(currentRoom,k,dir)) end
--       end
--       error("ROOM NOT FOUND!")
    end
end

function map.fix_portals()
    if mapping then
        -- used to clear and update data for portal back-referencing
        local rooms = getRooms()
        local portals
        for k,v in pairs(rooms) do
            setRoomUserData(k,"portals","")
        end
        for k,v in pairs(rooms) do
            for cmd,room in pairs(getSpecialExitsSwap(k)) do
                portals = getRoomUserData(room,"portals") or ""
                if portals ~= "" then portals = portals .. "," end
                portals = portals .. tostring(k) .. ":" .. cmd
                setRoomUserData(room,"portals",portals)
                --print(room,portals)
            end
        end
    end
end

function map.merge_rooms()
    -- used to combine essentially identical rooms with the same coordinates
    -- typically, these are generated due to mapping errors
    if mapping then
        local x,y,z = getRoomCoordinates(currentRoom)
        local rooms = getRoomsByPosition(currentArea,x,y,z)
        local exits, portals,room,cmd,curportals
        for k,v in pairs(rooms) do
            if v ~= currentRoom then
                if getRoomName(v) == getRoomName(currentRoom) then
                    for k1,v1 in pairs(getRoomExits(v)) do
                        setExit(currentRoom,v1,stubmap[k1])
                        exits = getRoomExits(v1)
                        if exits[reverse_dirs[k1]] == v then
                            setExit(v1,currentRoom,stubmap[reverse_dirs[k1]])
                        end
                    end
                    for k1,v1 in pairs(getDoors(v)) do
                        setDoor(currentRoom,k1,v1)
                    end
                    for k1,v1 in pairs(getSpecialExitsSwap(v)) do
                        addSpecialExit(currentRoom,v1,k1)
                    end
                    portals = getRoomUserData(v,"portals") or ""
                    if portals ~= "" then
                        portals = string.split(portals,",")
                        for k1,v1 in ipairs(portals) do
                            room,cmd = unpack(string.split(v1,":"))
                            addSpecialExit(tonumber(room),currentRoom,cmd)
                            curportals = getRoomUserData(currentRoom,"portals") or ""
                            if not string.find(curportals,room) then
                                curportals = curportals .. "," .. room .. ":" .. cmd
                                setRoomUserData(currentRoom,"portals",curportals)
                            end
                        end
                    end
                    for _,door in ipairs({"door up","door down","door in","door out"}) do
                        local tmp = getRoomUserData(v,door) or ""
                        if tmp ~= "" then
                            setRoomUserData(currentRoom,door,tmp)
                        end
                    end
                    deleteRoom(v)
                end
            end
        end
    end
end

function doSpeedWalk()
    -- we can do a lot here, this fires when a room is double clicked on, and is intended to speedwalk to it
    print("Path to " .. getRoomName(speedWalkPath[#speedWalkPath]) .. ": " .. table.concat(speedWalkDir, ", "))
end

function map.eventHandler(event,...)
    if event == "onPrompt" and room_detected then
        room_detected = false
        capture_room_info(map.prompt.room, map.prompt.exits)
    elseif event == "onMoveFail" then
        table.remove(move_queue,1)
    elseif event == "onVisionFail" then
        vision_fail = true
        room_detected = true
    elseif event == "onRandomMove" then
        random_move = true
        move_queue = {}
    elseif event == "onForcedMove" then
        capture_move_cmd(arg[1],arg[2]=="true")
    elseif event == "onNewRoom" then
        room_detected = true
    elseif event == "sysDataSendRequest" then
        capture_move_cmd(arg[1])
    elseif event == "sysDownloadDone" and downloading then
        loadMap(getMudletHomeDir() .. "/map.dat")
        downloading = false
        print("Map File Loaded.")
    elseif event == "sysConnectionEvent" then
        config()
    end
end

registerAnonymousEventHandler("sysDownloadDone", "map.eventHandler")
registerAnonymousEventHandler("sysConnectionEvent", "map.eventHandler")
registerAnonymousEventHandler("sysDataSendRequest", "map.eventHandler")
registerAnonymousEventHandler("onPrompt", "map.eventHandler")
registerAnonymousEventHandler("onLine"," map.eventHandler")
registerAnonymousEventHandler("onMoveFail"," map.eventHandler")
registerAnonymousEventHandler("onVisionFail"," map.eventHandler")
registerAnonymousEventHandler("onRandomMove"," map.eventHandler")
registerAnonymousEventHandler("onForcedMove"," map.eventHandler")
registerAnonymousEventHandler("onNewRoom"," map.eventHandler")
Also, this script positions its mapper window using the Simple Window Manager Script (found here), but this should be able to be modified to use other GUI management systems as desired, by changing relevant lines in the config function.

As I have been asked this several times, the proper way to store the exits in the map.prompt.exits variable is as a string, with exit directions separated by any number of spaces and commas, and no extra words other than "and" (which is automatically removed). Ex: "north, south, west, up, and northwest" or "north south west up northwest".

Edit: Updated to handle in/out exit stubs properly, bypassing the bug in the Mudlet function setExitStub. Also added a "stretch_map" variable in the configs that lets you determine if the map will automatically reposition rooms if a new room would overlap with an existing one.

Edit: Updated to hopefully handle forced movement in known directions, via the "onForcedMove" event, which takes two arguments, the first being the direction you are being moved in, and the second (optional) telling it to force that movement to the front of the list of directions moved (use "true" as a string if this is needed). Note that this hasn't been tested, but given that it is basically just grabbing the exit it is given and putting it in the movement queue as if you had typed in the direction, I don't anticipate many problems.

Edit: At Vadi's suggestion, I created a proper package out of this, and wrote a single command you could put into Mudlet's command line to install it. There is a very basic set of triggers in place to help get things going, but the odds are low that they will work for your game without significant adjustment.
Syntax: [ Download ] [ Hide ]
Using lua Syntax Highlighting
lua function downloaded_package(a,b) if not b:find("generic_mapper",1,true) then return end installPackage(b) os.remove(b) end registerAnonymousEventHandler("sysDownloadDone","downloaded_package") downloadFile(getMudletHomeDir().."/generic_mapper.mpackage","http://dslpnp.altervista.org/Generic_Mapper.mpackage")
Last edited by Jor'Mox on Sat Mar 04, 2017 5:26 pm, edited 13 times in total.
Jor'Mox
 
Posts: 684
Joined: Wed Apr 03, 2013 2:19 am

Re: Generic Mapping Script

Postby SlySven » Fri Sep 02, 2016 3:23 am

You may get more precise results from the searchRoom(<roomId>) function as it has gained two extra (optional, omission is equivalent to false) booleans in the current code in the GitHub repositories for both the development and the delta preview source code to become searchRoom(<roomId>, [<case-sensitive> [, <exact-match>]]) - the plain command does a sub-string search and is not case sensitive but that can now be modified!

Area names in current code have mandatory names and mandatory uniqueness - they cannot have an empty name and the name must be unique amongst the currently defined areas. They are also case sensitive and case can be something that makes two otherwise identical names different - I spotted in the depths of that script something that squashed the case of the names (converted to lower-case for all area names) for a search (I think) that will fall over in the corner case where there are, for instance, "AnArea" and "anArea" as two different areas...

The current codebase does not clearly show the direction for which those exits where it shows a door marking (in the subset of normal exits on the XY-plain without a custom exit line) for 2 way exits - the coloured squared for the opposite directions are drawn on top of each other so if they are different it is unclear which direction is the one shown.

As it happens I am currently redesigning the code and it will be that doors can be indicated for any exit, even a stub or custom line and for the un-drawn (i.e. no custom line cases) of up, down, in or out markings on the room on the 2D map. This has come about as a side effect of refactoring the room internal structure, particularly the handling of exits {the code now allows for special stub exits - for when you just don't know what an unusual exit does yet, complete with markers on the 2D map showing that there are Special Exits WITHOUT a custom lines in that room and if there are none of those if there are any Special Exit stubs in that room!} and there will be a uniform set of lua exit feature commands based on a exit-pair {direction 1=north,2=north-east,...13=special; exit-string=nil for 1-12,name for 13} as part of a getAnyExitXXXX(...), getAllExitXXX(...) and setAnyExitXXXX(...) interface. The reason for using numbers is for I18n work so that scripts can work uniformly no matter what the normal exit directions are called in the current language - I have just put into the prototype code that allows the commands that the speedwalk code generates to be set for each normal exit direction on a global basis and for that to be over-ride-able on an exit by exit case - which opens the possibility of dropping in a script like a special exit can have - and special exits can have a name (identifier) that is not the command that is used (otherwise to identify a complex script you have to specify that script as the identifier) - but some care will need to be taking to make sure that it is at least backwards compatible for existing maps! :geek:
User avatar
SlySven
 
Posts: 719
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK

Re: Generic Mapping Script

Postby Jor'Mox » Fri Sep 02, 2016 11:02 am

Yeah, I definitely did a bit of coding around the way Mudlet currently functions. So room searches are made exact by going back through and comparing the room names. Area names, so long as they are generated using the aliases provided, are forced to be unique independent of capitalization in a similar manner. I even built in doors functionality for up and down exits, specifically to allow me to check to see if visible exits match with known exits for a particular room, since a closed door can make a given exit invisible. However, I'm still using 2.1, and will be for the foreseeable future, so there are limits to what I can take advantage of. Once an official release for 3.0 comes out, then I can see about creating an updated version that uses its improved mapping functions.

By the way, the current code not only doesn't show where a door goes super clearly, if you have a door between areas, and the coordinates of the connected rooms don't line up correctly, it places the door on a spot midway between the two rooms, which can put it pretty much anywhere, including so far out of sight as to be unnoticed, or positioned as if it were a door for a different room entirely.

Anyway, it is my hope that this will let people with relatively simple mapping needs and limited coding ability put together a mapper for whatever game they play in. We shall see how successful that is.
Last edited by Jor'Mox on Sun Jan 15, 2017 11:09 pm, edited 1 time in total.
Jor'Mox
 
Posts: 684
Joined: Wed Apr 03, 2013 2:19 am

Re: Generic Mapping Script

Postby SlySven » Fri Sep 02, 2016 9:38 pm

Jor'Mox wrote:...By the way, the current code not only doesn't show where a door goes super clearly, if you have a door between areas, and the coordinates of the connected rooms don't line up correctly, it places the door on a spot midway between the two rooms, which can put it pretty much anywhere, including so far out of sight as to be unnoticed, or positioned as if it were a door for a different room entirely...
Does the sample below look any better - there is rather a lot of different things on display here but I think you can determine the presence of doors on some of the exits!

When you say "if you have a door between areas" you mean between two rooms in the same "Area" but in two separated "clumps" I take it - so that the mid point where the current square is drawn is not clearly associated with either room? I'm not showing any area exits in the screen shot from my work in progress as I've still got to code for them:
Mudlet_wip2.png
User avatar
SlySven
 
Posts: 719
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK

Re: Generic Mapping Script

Postby Jor'Mox » Sat Sep 03, 2016 1:08 am

No, I mean when the door is between two rooms in two different areas. This could potentially be avoided by including the room on the other side of the door into the area, but areas have specific in-game consequences in the MUD I play (and I wager a lot of MUDs), so I try to draw area borders as exactly as possible.
Jor'Mox
 
Posts: 684
Joined: Wed Apr 03, 2013 2:19 am

Re: Generic Mapping Script

Postby SlySven » Sat Sep 03, 2016 8:27 pm

I think I get you, the coloured arrow (in the destination room's colours) does not have any indication of a door in the current code or rather it does but it is based on the "mid-point" of the coordinates for the two rooms but as different areas do not have a common coordinate system that is a bogus thing to do.

For what it is worth, I have addressed that in the "new" stuff I'm working on since I wrote the last post... 8-)
Mudlet_wip3.png

I must ensure that custom exit lines always get the same massive arrow-head if they are for an area exit as well!
User avatar
SlySven
 
Posts: 719
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK

Re: Generic Mapping Script

Postby Jor'Mox » Sat Sep 03, 2016 9:15 pm

So, since you seem to be working on the map and related things, is there any chance that whatever it is that causes it to crash when you mess with it via the GUI too much has been fixed? Or is that one still lurking?

I ask because I have never seen it have any problems when I work with the map via code, but once I start clicking and dragging, I know it is just a matter of time before the whole thing crashes, which is a real pain.
Jor'Mox
 
Posts: 684
Joined: Wed Apr 03, 2013 2:19 am

Re: Generic Mapping Script

Postby SlySven » Sat Sep 03, 2016 10:50 pm

Jor'Mox wrote:...but once I start clicking and dragging, I know it is just a matter of time before the whole thing crashes, which is a real pain.
Any particular symptoms or odd things happen just before it crashes? On what OS? "Clicking and dragging" what, a room, a group of rooms, a point on a custom exit line? Any error messages? Tell me more!
User avatar
SlySven
 
Posts: 719
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK

Re: Generic Mapping Script

Postby Jor'Mox » Sat Sep 03, 2016 11:22 pm

So, I have experienced this on Mac OS X v.10.11, though I have also had it reported by someone using Windows of some variety (I don't know which one, but not Windows 10). I haven't noticed any specific symptoms that precede a crash, and I haven't noticed an exact pattern for what I'm doing when it happens. In a general sense, I and the other person who has reported it move single and multiple rooms, delete single and multiple rooms, and change exit assignments for individual rooms. If I remember correctly, the crash always seems to happen to me when I'm trying to bring up the menu, and the person who was using windows seemed to think it was related to having deleted multiple rooms somehow, but I haven't noticed such a connection, and specifically I have experienced a crash, restarted Mudlet, and manipulated the mapper without deleting any rooms, and experienced another crash, in one sitting.

When it was first reported to me, the person was using Mudlet 3.0.0 delta, so I recommended trying Mudlet 2.1 (because I had not yet had such a crash), but it obviously didn't fix it. Pretty much any time that I am using the GUI a significant amount, I can be certain that it will crash, but it can vary in how long it takes. Sometimes only 15 minutes is enough, and other times it can take an hour or more, though how much I'm using it is probably a factor there.

Oh, and I don't know if there are error messages, because it crashes Mudlet completely.
Jor'Mox
 
Posts: 684
Joined: Wed Apr 03, 2013 2:19 am

Re: Generic Mapping Script

Postby SlySven » Sat Sep 03, 2016 11:35 pm

There WAS some nasty bugs in older 3.0 previews (inherited from 2.1 IIRC) in relation to deletion of rooms or movement of them between areas that could be causing the sort of things you describe - basically the areas' idea of rooms that they had was not being correctly maintained in the map data structures and these issues could be preserved in the saved form of the map. The current development and 3.0 preview source code (post-"delta") has additional stuff in place that should repair/fix/log (and report right in the main console if told to, check for that option on the "Map" tab of the "Options"/"Preferences" dialog) any such problems but I cannot remember whether the downloadable binaries have been recompiled to bring them up to that state...
User avatar
SlySven
 
Posts: 719
Joined: Mon Mar 04, 2013 3:40 pm
Location: Deepest Wiltshire, UK

Next

Return to Mudlet Mapper

Who is online

Users browsing this forum: No registered users and 1 guest