Mudlet Mapper Script

Auto-populate your Mudlet map as you explore Mystic

Mystic supports GMCP (Generic MUD Communication Protocol), which sends structured room data to your client as you move. This script reads that data and builds a live map inside Mudlet's built-in mapper. Rooms are color-coded by terrain, exits are linked automatically, and clicking any room on the map speedwalks you there.

Requirements

Installation

  1. Copy the script below (use the Copy button).
  2. In Mudlet, open the Script Editor (the <> button in the toolbar).
  3. Click Add Item and choose Script (not Trigger or Alias).
  4. Give it a name like Mystic Mapper, paste the script, then click Save Item and Save Profile.
  5. If you have Mudlet's built-in generic-mapper package installed, uninstall it first:
    Package Manager → generic-mapper → Uninstall
  6. Disconnect and reconnect to Mystic.

Usage

To reset the map and start fresh:
In the Mudlet Lua console, run: lua deleteMap(); roomCount = 0
Then reconnect.

Script

-- ==========================================================================

-- GMCP Mapper Script for Mystic MUD (Mudlet)
--

-- Setup:
--   1. Open Mudlet Script Editor (the <> button)
--   2. Create a new Script (not Trigger, not Alias)
--   3. Paste this entire file
--   4. Save and activate
--   5. Disable Mudlet's built-in "generic-mapper" package if installed
--      (Package Manager > generic-mapper > Uninstall)

--   6. Disconnect and reconnect
--

-- Usage:
--   After connecting, the mapper auto-populates as you walk.
--   Open the map window: View > Map

--   Click any room on the map to speedwalk there.
--

-- To reset the map (start fresh):
--   lua deleteMap(); roomCount = 0
--   Then reconnect.
-- ==========================================================================

mudlet = mudlet or {}; mudlet.mapper_script = true

-- -------------------------------------------------------------------------

-- Terrain colors (custom env IDs > 272 to avoid Mudlet built-in conflicts)

-- -------------------------------------------------------------------------

local terrain_envs = {
  town = 300, path = 301, forest = 302, mountain = 303,
  water = 304, swamp = 305, desert = 306, underground = 307,
  inside = 308, plain = 309, hills = 310, jungle = 311,
  shore = 312, shallows = 313, underwater = 314, canyon = 315,
  valley = 316, ice = 317, mud = 318, rocky = 319,
  scrub = 320, air = 321, unknown = 322,
}

setCustomEnvColor(300, 200, 200, 200, 255)  -- town: light grey
setCustomEnvColor(301, 169, 169, 169, 255)  -- path: grey
setCustomEnvColor(302,   0, 128,   0, 255)  -- forest: green
setCustomEnvColor(303, 139,  69,  19, 255)  -- mountain: brown
setCustomEnvColor(304,   0,   0, 255, 255)  -- water: blue
setCustomEnvColor(305, 107, 142,  35, 255)  -- swamp: olive
setCustomEnvColor(306, 210, 180, 140, 255)  -- desert: tan
setCustomEnvColor(307, 105, 105, 105, 255)  -- underground: dim grey
setCustomEnvColor(308, 255, 255, 224, 255)  -- inside: light yellow
setCustomEnvColor(309, 144, 238, 144, 255)  -- plain: light green
setCustomEnvColor(310, 154, 205,  50, 255)  -- hills: yellow green
setCustomEnvColor(311,   0, 100,   0, 255)  -- jungle: dark green
setCustomEnvColor(312, 238, 214, 175, 255)  -- shore: sandy
setCustomEnvColor(313, 135, 206, 235, 255)  -- shallows: sky blue
setCustomEnvColor(314,   0,   0, 139, 255)  -- underwater: dark blue
setCustomEnvColor(315, 160,  82,  45, 255)  -- canyon: sienna
setCustomEnvColor(316, 124, 252,   0, 255)  -- valley: lawn green
setCustomEnvColor(317, 176, 224, 230, 255)  -- ice: powder blue
setCustomEnvColor(318, 101,  67,  33, 255)  -- mud: dark brown
setCustomEnvColor(319, 128, 128, 128, 255)  -- rocky: grey
setCustomEnvColor(320, 189, 183, 107, 255)  -- scrub: khaki
setCustomEnvColor(321, 230, 230, 250, 255)  -- air: lavender
setCustomEnvColor(322, 220, 220, 220, 255)  -- unknown: light grey

-- -------------------------------------------------------------------------

-- Direction helpers

-- -------------------------------------------------------------------------

local dir_offsets = {
  north     = { 0,  1,  0},
  south     = { 0, -1,  0},
  east      = { 1,  0,  0},
  west      = {-1,  0,  0},
  northeast = { 1,  1,  0},
  northwest = {-1,  1,  0},
  southeast = { 1, -1,  0},
  southwest = {-1, -1,  0},
  up        = { 0,  0,  1},
  down      = { 0,  0, -1},
}

local dir_to_num = {
  north = 1, northeast = 2, northwest = 3,
  east = 4, west = 5,
  south = 6, southeast = 7, southwest = 8,
  up = 9, down = 10,
}

-- -------------------------------------------------------------------------

-- State

-- -------------------------------------------------------------------------

local prevRoomID = nil
local prevRoomHash = nil
prevExitsGMCP = prevExitsGMCP or {}
roomCount = roomCount or 0

-- -------------------------------------------------------------------------

-- GMCP negotiation: request packages when Core.Hello arrives

-- -------------------------------------------------------------------------

function onCoreHello()
  sendGMCP([[Core.Supports.Set ["Char 1", "Room 1", "Comm 1", "Group 1"] ]])
end

-- -------------------------------------------------------------------------

-- Room mapper: creates rooms, sets coordinates, links exits

-- -------------------------------------------------------------------------

function onGMCPRoomInfo()
  if not gmcp.Room or not gmcp.Room.Info then return end
  local info = gmcp.Room.Info
  local hash = info.num

  -- Strip exit list from name: "Town Square (n, s)" -> "Town Square"
  local cleanName = string.gsub(info.name, "%s*%b()$", "")

  -- Look up or create the room
  local roomID = getRoomIDbyHash(hash)
  local isNew = false

  if not roomID or roomID == -1 then
    isNew = true
    roomCount = roomCount + 1
    roomID = roomCount
    while roomExists(roomID) do
      roomCount = roomCount + 1
      roomID = roomCount
    end
    addRoom(roomID)
    setRoomIDbyHash(roomID, hash)
  end

  setRoomName(roomID, cleanName)

  -- Area management
  if info.area then
    local areaTable = getAreaTable() or {}
    local areaID = areaTable[info.area]
    if not areaID then
      areaID = addAreaName(info.area)
    end
    setRoomArea(roomID, areaID)
  end

  -- Coordinates: only set for NEW rooms, based on direction from previous
  if isNew and prevRoomID then
    local moved_dir = nil

    -- Check which direction from the previous room leads here
    if prevRoomHash then
      local prevExits = getRoomExits(prevRoomID) or {}
      for dir, destID in pairs(prevExits) do
        if destID == roomID then
          moved_dir = dir
          break
        end
      end
      -- If not linked yet, check GMCP exits of the previous room
      if not moved_dir then
        for dir, destPath in pairs(prevExitsGMCP or {}) do
          local cleanPath = destPath:gsub("^/", "")
          if cleanPath == hash then
            moved_dir = dir
            break
          end
        end
      end
    end

    local px, py, pz = getRoomCoordinates(prevRoomID)
    local offset = moved_dir and dir_offsets[moved_dir]
    if offset then
      setRoomCoordinates(roomID, px + offset[1], py + offset[2], px and pz + offset[3] or offset[3])
    else
      -- Unknown direction: place slightly offset to avoid overlap
      setRoomCoordinates(roomID, px + 1, py, pz)
    end

  elseif isNew then
    setRoomCoordinates(roomID, 0, 0, 0)
  end

  -- Terrain color
  local envID = terrain_envs[info.terrain] or 322
  setRoomEnv(roomID, envID)

  -- Set exits: link to known rooms, stub for unknown
  local exits = info.exits or {}
  for dir, destPath in pairs(exits) do
    local dirNum = dir_to_num[dir]
    if dirNum then
      local cleanPath = destPath:gsub("^/", "")
      local destID = getRoomIDbyHash(cleanPath)
      if destID and destID ~= -1 then
        setExit(roomID, destID, dirNum)
      else
        setExitStub(roomID, dirNum, true)
      end
    end
  end

  -- Save state for next move
  prevExitsGMCP = exits
  prevRoomID = roomID
  prevRoomHash = hash

  centerview(roomID)
end

-- -------------------------------------------------------------------------

-- Speedwalk: clicking a room on the map walks there

-- -------------------------------------------------------------------------

function doSpeedWalk()
  for _, dir in ipairs(speedWalkDir) do
    send(dir)
  end
end

-- -------------------------------------------------------------------------

-- Placeholder handlers for other GMCP packages
-- (These keep Mudlet's gmcp table populated)

-- -------------------------------------------------------------------------

function onGMCPReceived() end

-- -------------------------------------------------------------------------

-- Register all event handlers

-- -------------------------------------------------------------------------

registerAnonymousEventHandler("gmcp.Core.Hello", "onCoreHello")
registerAnonymousEventHandler("gmcp.Room.Info", "onGMCPRoomInfo")
registerAnonymousEventHandler("gmcp.Char.Name", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Char.Vitals", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Char.Status", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Char.Afflictions", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Char.Items", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Comm.Channel", "onGMCPReceived")
registerAnonymousEventHandler("gmcp.Group.Info", "onGMCPReceived")

Questions? Ask in-game or on Discord.