<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient [
  <!ENTITY show_vnums "true" > 
  <!ENTITY show_timing "false" > 
  <!ENTITY show_completed "true" > 
  <!ENTITY show_database_mods "true" > 
  <!ENTITY show_other_areas "false" > 
  <!ENTITY show_area_exits "false" > 
  <!ENTITY show_up_down "true" > 
  <!ENTITY speedwalk_prefix "" > 
  <!ENTITY mud_name "Realm of Magic">
]>

<muclient>
<plugin
   name="Realm_Of_Magic_Mapper"
   author="Nick Gammon"
   id="b79c1e219a6246eb8f62faf4"
   language="Lua"
   purpose="Mapper for Realm of Magic"
   save_state="y"
   date_written="2014-10-17 07:00"
   requires="4.61"
   version="1.0"
   >

   <description trim="y">
<![CDATA[
AUTOMATIC MAPPER ...  by Nick Gammon

The window can be dragged to a new location by dragging the room name.

Your current room is always in the center with a bolder border.

LH-click on a room to speed-walk to it. RH-click on a room for options. 

LH-click on the "*" button on the bottom-left corner to configure it.

** WHY DOES THE MAP CHANGE? **

The mapper draws from your room outwards - that is, it draws your room's exits
first, then the rooms leading from those rooms, and so on.

Eventually it finds an overlap, and draws a short "stub" line to indicate there
is a room there which there isn't space to draw. If you get closer to that 
room the stub will disappear and the room(s) in question will be drawn.

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
mapper zoom out     --> zoom out
mapper zoom in      --> zoom in
mapper hide         --> hide map
mapper show         --> show map

FINDING THINGS

mapper bookmarks    --> show nearby rooms that you bookmarked
mapper find <text>  --> full-text search  (eg. shop OR baker)
mapper shop         --> show nearby shops/banks etc.
mapper train        --> show nearby trainers
mapper where <room> --> show directions to a room

MOVING  

mapper goto <room>  --> walk to a room by its room number (partial)
mapper stop         --> cancel any current speedwalk
mapper resume       --> resume last speedwalk or hyperlinked speedwalk

]]>
</description>


</plugin>


<!--  Triggers  -->

<triggers>

      
  <trigger
   enabled="y"
   match="Alas, you cannot go that way..."
   name="Cannot_Go"
   script="Cannot_Go"
   sequence="100"
   >
   </trigger>
   
   
  <trigger
   enabled="y"
   match="Nah... You feel too relaxed to do that.."
   name="Too_Relaxed"
   script="Cannot_Go"
   sequence="100"
   >
   </trigger>
   
  <trigger
   enabled="y"
   match="^The (door|gate) is closed\.$"
   regexp="y"
   name="Door_Closed"
   script="Door_Closed"
   sequence="100"
  >
  </trigger>
  
 <!--  various messages that cancel speedwalks -->
  
  <trigger
   enabled="y"
   match="You are too exhausted to go any farther."
   script="Cannot_Go"
   sequence="100"
  > 
  </trigger>
    
</triggers>


<aliases>
 
  <!--  zooming aliases -->

 <alias
   match="mapper zoom out"
   enabled="y"
   sequence="100"
   omit_from_command_history="y"
   omit_from_output="y"
   script="mapper.zoom_out"
  >
  </alias>
  
<alias
   match="mapper zoom in"
   enabled="y"
   sequence="100"
   omit_from_command_history="y"
   omit_from_output="y"
   script="mapper.zoom_in"
  >
  </alias>  
    
    
 <alias
   match="mapper goto *"
   enabled="y"
   sequence="100"
   script="map_goto"
  >
  
  </alias>  
    
  <!--  finding aliases -->

  <alias
   match="^mapper find ([\w* %d/&quot;]+)$"
   enabled="y"
   sequence="100"
   script="map_find"
   regexp="y"
  >
  
  </alias>  
  
  <alias
   match="^mapper book\w*$"
   regexp="y"
   enabled="y"
   sequence="100"
   script="map_bookmarks"
  >
  
  </alias>  
  
 <alias
   match="mapper where *"
   enabled="y"
   sequence="100"
   script="map_where"
  >
  
  </alias>  
    
 <alias
   match="^mapper shops?$"
   regexp="y"
   enabled="y"
   sequence="100"
   script="map_shops"
  >
  
  </alias>  

 <alias
   match="^mapper train\w*$"
   regexp="y"
   enabled="y"
   sequence="100"
   script="map_trainers"
  >
  
  </alias>  
  
 <alias
   match="mapper resume"
   enabled="y"
   sequence="100"
   script="map_resume"
  >
  
  </alias>  
    
  <alias
   script="OnHelp"
   match="mapper help"
   enabled="y"
  >
  </alias>
    
  <!--  cancel speedwalking -->
  
 <alias
   match="mapper stop"
   enabled="y"
   sequence="100"
   script="mapper.cancel_speedwalk"
  >
  </alias>  

  <!--  show/hide mapper -->
      
  <alias
   match="mapper hide"
   enabled="y"
   sequence="100"
   script="mapper.hide"
  >
  </alias>  
  
   <alias
   match="mapper show"
   enabled="y"
   sequence="100"
   script="mapper.show"
  >
  </alias>  
  
</aliases>

<!--  Script  -->


<script>

local show_vnums = &show_vnums;
local show_timing = &show_timing;
local show_completed = &show_completed;
local show_database_mods = &show_database_mods;
local show_other_areas = &show_other_areas;
local show_up_down = &show_up_down;
local show_area_exits = &show_area_exits;
local speedwalk_prefix = "&speedwalk_prefix;"
local mud_name = "&mud_name;"

<![CDATA[

require "mapper"
require "serialize"
require "copytable"
  
rooms = {}
areas = {}
moves = {}

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

function Cannot_Go ()
  mapper.cancel_speedwalk ()
  moves = {}
end -- Cannot_Go
-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------

function get_room (uid)

  -- check we got room at all
  if not uid then
   -- return nil
  end -- if
  
  -- look it up
  local ourroom = rooms [uid]
  
  -- not cached - see if in database
  if not ourroom then
    ourroom = load_room_from_database (uid)
    rooms [uid] = ourroom -- cache for later
  end -- not in cache
  
  if not ourroom then
     return nil
  end -- if

  local room = copytable.deep (ourroom)
  
  room.area = mud_name
  
  if uid == current_room then
    current_area = room.area
  end -- if
  
  -- build hover message
  
  
  local shop = ""
  if room.shop then
    shop = "\nRoom is shop"
  end -- if shop

  local train = ""
  if room.train then
    train = "\nRoom has trainer"
  end -- if train
    
  local notes = ""
  if room.notes then
    notes = "\nBookmark: " .. room.notes
  end -- if notes
  
  local texits = {}
  for dir in pairs (room.exits) do
    table.insert (texits, dir)
  end -- for
  table.sort (texits)
   
  room.hovermessage = string.format (
      "%s\tExits: %s\nRoom: %s%s%s%s",
      room.name, 
      table.concat (texits, ", "),
      uid,
      shop,
      train,
      notes
      -- depth,
      -- table.concat (path, ",")
      )
      
  room.bordercolour = config.ROOM_COLOUR.colour
  room.borderpen = 0 -- solid
  room.borderpenwidth = 1
  room.fillcolour = 0xff0000
  room.fillbrush = 1 -- no fill
              
  -- special room fill colours
  
  if room.shop then
    room.fillcolour = config.SHOP_FILL_COLOUR.colour
    room.fillbrush = 8
  end -- if 

  if room.train then
    room.fillcolour = config.TRAINER_FILL_COLOUR.colour
    room.fillbrush = 8
  end -- if 
         
  if uid == current_room then
    room.bordercolour = config.OUR_ROOM_COLOUR.colour
    room.borderpenwidth = 2
  elseif room.area ~= current_area then
    room.bordercolour = config.DIFFERENT_AREA_COLOUR.colour
  end -- not in this area
  
  return room      
      
end -- get_room

-- -----------------------------------------------------------------
-- We have changed rooms - work out where the previous room led to 
-- -----------------------------------------------------------------

function fix_up_exit ()

  -- discard old moves (maybe we logged out or something)
  while #moves > 0 and (moves [1].time + 20) < os.time () do
    table.remote (moves, 1)
  end -- for
  
  if #moves == 0 then
--    print "No outstanding moves."
    return
  end -- if
  
--  print "Fixing up exits"
--  tprint (moves)
  
  -- get the earliest move we made
  last_direction_moved = moves [1].dir
  from_room = moves [1].from
  table.remove (moves, 1)
  
  -- where we were before
  local room = rooms [from_room]
  
--   print ("Moved from", from_room, "to", current_room, "in direction", last_direction_moved)
  
  -- leads to here
  if from_room ~= current_room then
  
    local current_dest = nil
    
    for exitrow in db:nrows(string.format ("SELECT * FROM exits WHERE fromuid = %s AND dir = %s", 
          fixsql (from_room),
          fixsql  (last_direction_moved))) do
       current_dest = exitrow.touid
    end -- for each exit
 
 --   print ("Current destination = ", current_dest)
    
    if current_room ~= current_dest then
      dbcheck (db:execute (string.format ([[
          UPDATE exits SET touid = %s WHERE fromuid = %s AND dir = %s;
        ]], 
            fixsql  (current_room),     -- destination room
            fixsql  (from_room),       -- from previous room
            fixsql  (last_direction_moved)  -- direction (eg. "n")
            )))
      
      if show_database_mods then
        mapper.mapprint ("Fixed exit", last_direction_moved, "from room", from_room, "to be to", current_room)
      end -- if
    end -- if destination different
      
    room.exits [last_direction_moved] = current_room
  end -- if
    
  -- clear for next time
  last_direction_moved = nil
  
end -- fix_up_exit

-- -----------------------------------------------------------------
-- try to detect when we send a movement command
-- -----------------------------------------------------------------

function OnPluginSent (sText)
  last_direction_moved = valid_direction [sText]
  if last_direction_moved then
--    print ("Just moved", last_direction_moved)
    if current_room and rooms [current_room] then
      expected_exit = rooms [current_room].exits [last_direction_moved]
     -- print ("expected exit for this direction is to room", expected_exit)
    end -- if have a current room
    t = { dir = last_direction_moved, time = os.time () }
    if #moves == 0 then
      t.from = current_room
    end -- if first stacked move
    table.insert (moves, t)
--    tprint (t)
    from_room = moves [1].from
  end -- if  valid direction
end -- function


default_config = {
  -- assorted colours
  BACKGROUND_COLOUR       = { name = "Background",        colour =  ColourNameToRGB "lightseagreen", },
  ROOM_COLOUR             = { name = "Room",              colour =  ColourNameToRGB "cyan", },
  EXIT_COLOUR             = { name = "Exit",              colour =  ColourNameToRGB "darkgreen", },
  EXIT_COLOUR_UP_DOWN     = { name = "Exit up/down",      colour =  ColourNameToRGB "darkmagenta", },
  EXIT_COLOUR_IN_OUT      = { name = "Exit in/out",       colour =  ColourNameToRGB "#3775E8", },
  OUR_ROOM_COLOUR         = { name = "Our room",          colour =  ColourNameToRGB "black", },
  UNKNOWN_ROOM_COLOUR     = { name = "Unknown room",      colour =  ColourNameToRGB "#00CACA", },
  DIFFERENT_AREA_COLOUR   = { name = "Another area",      colour =  ColourNameToRGB "#009393", },
  SHOP_FILL_COLOUR        = { name = "Shop",              colour =  ColourNameToRGB "darkolivegreen", },
  POSTOFFICE_FILL_COLOUR  = { name = "Post Office",       colour =  ColourNameToRGB "yellowgreen", },
  BANK_FILL_COLOUR        = { name = "Bank",              colour =  ColourNameToRGB "gold", },
  NEWSROOM_FILL_COLOUR    = { name = "Newsroom",          colour =  ColourNameToRGB "lightblue", },
  TRAINER_FILL_COLOUR     = { name = "Trainer",           colour =  ColourNameToRGB "lightgreen" },
  
  ROOM_NAME_TEXT          = { name = "Room name text",    colour = ColourNameToRGB "#BEF3F1", },
  ROOM_NAME_FILL          = { name = "Room name fill",    colour = ColourNameToRGB "#105653", },
  ROOM_NAME_BORDER        = { name = "Room name box",     colour = ColourNameToRGB "black", },
  
  AREA_NAME_TEXT          = { name = "Area name text",    colour = ColourNameToRGB "#BEF3F1",},
  AREA_NAME_FILL          = { name = "Area name fill",    colour = ColourNameToRGB "#105653", },   
  AREA_NAME_BORDER        = { name = "Area name box",     colour = ColourNameToRGB "black", },
               
  FONT = { name =  get_preferred_font {"Dina",  "Lucida Console",  "Fixedsys", "Courier", "Sylfaen",} ,
           size = 8
         } ,
         
  -- size of map window
  WINDOW = { width = 400, height = 400 },
  
  -- how far from where we are standing to draw (rooms)
  SCAN = { depth = 30 },
  
  -- speedwalk delay
  DELAY = { time = 0.3 },
  
  -- how many seconds to show "recent visit" lines (default 3 minutes)
  LAST_VISIT_TIME = { time = 60 * 3 },  
  
  }

  
-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------

function OnPluginInstall ()
  
  config = {}  -- in case not found

  -- get saved configuration
  assert (loadstring (GetVariable ("config") or "")) ()

  -- allow for additions to config
  for k, v in pairs (default_config) do
    config [k] = config [k] or v
  end -- for
  
  -- initialize mapper
  
  mapper.init { config = config, 
                get_room = get_room,
                show_help = OnHelp,         -- to show help
                room_click = room_click,    -- called on RH click on room square
                timing = show_timing,       -- want to see timing
                show_completed = show_completed,  -- want to see "Speedwalk completed." message
                show_other_areas = show_other_areas,  -- want to see areas other than the current one?
                show_up_down = show_up_down,          -- want to follow up/down exits?
                show_area_exits = show_area_exits,    -- want to see area exits?
                speedwalk_prefix = speedwalk_prefix,  -- how to speedwalk
  } 
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))

  -- open databases on disk 
  db    = assert (sqlite3.open(GetInfo (66) .. Trim (WorldAddress ()) .. "_" .. WorldPort () .. ".db"))
  
  create_tables ()    -- create database structure if necessary
  
end -- OnPluginInstall

-- -----------------------------------------------------------------
-- Plugin Save State
-- -----------------------------------------------------------------

function OnPluginSaveState ()
  mapper.save_state ()
  SetVariable ("config", "config = " .. serialize.save_simple (config))
end -- OnPluginSaveState

function map_resume (name, line, wildcards)

  local wanted = mapper.last_hyperlink_uid or mapper.last_speedwalk_uid
  
  if not wanted then
    mapper.print "No outstanding speedwalks or hyperlinks."
    return
  end -- if nothing to do
  
  -- find desired room
  mapper.find (
    function (uid) 
      return uid == wanted, uid == wanted  
    end,  -- function
    show_vnums,  -- show vnum?
    1,      -- how many to expect
    true    -- just walk there
    )
        
end -- map_resume

function map_goto (name, line, wildcards)

  local wanted = wildcards [1]
  
  -- check valid string
  if string.match (wanted, "%X") then
    mapper.maperror ("Room number must be hex string (0-9, A-F), you entered: " .. wanted)
    return
  end -- if
  
  -- internally rooms are upper-case hex
  wanted = wanted:upper ()
  
  -- see if already there
  if current_room and string.match (current_room, "^" .. wanted) then
    mapper.mapprint ("You are already in that room.")
    return
  end -- if
  
  -- find desired room
  mapper.find (
    function (uid) 
      local found = string.match (uid, "^" .. wanted)
      return found, found  
    end,  -- function
    show_vnums,  -- show vnum?
    1,          -- how many to expect
    true        -- just walk there
    )
        
end -- map_goto

function map_where (name, line, wildcards)

  if not mapper.check_we_can_find () then
    return
  end -- if

  local wanted = wildcards [1]
  
  if current_room and wanted == current_room then
    mapper.mapprint ("You are already in that room.")
    return
  end -- if
  
  local paths = mapper.find_paths (current_room, 
           function (uid) 
             return uid == wanted,  -- wanted room?
                    uid == wanted   -- stop searching?
            end)

  local uid, item = next (paths, nil) -- extract first (only) path
  
  -- nothing? room not found
  if not item then
    mapper.mapprint (string.format ("Room %s not found", wanted))
    return
  end -- if
  
  -- turn into speedwalk
  local path = mapper.build_speedwalk (item.path)
  
  -- display it
  mapper.mapprint (string.format ("Path to %s is: %s", wanted, path))
  
end -- map_where

function OnHelp ()
  mapper.mapprint (string.format ("[MUSHclient mapper, version %0.1f]", mapper.VERSION))
  mapper.mapprint (world.GetPluginInfo (world.GetPluginID (), 3))
end



room_not_in_database = {}
room_in_database = {}

function dbcheck (code)

 if code ~= sqlite3.OK and    -- no error
    code ~= sqlite3.ROW and   -- completed OK with another row of data
    code ~= sqlite3.DONE then -- completed OK, no more rows
    local err = db:errmsg ()  -- the rollback will change the error message
    db:exec ("ROLLBACK")      -- rollback any transaction to unlock the database
    error (err, 2)            -- show error in caller's context
  end -- if

end -- dbcheck 

function fixsql (s)
  
  if s then
    return "'" .. (string.gsub (s, "'", "''")) .. "'" -- replace single quotes with two lots of single quotes
  else
    return "NULL"
  end -- if
end -- fixsql

function fixbool (b)
  if b then
    return 1
  else
    return 0
  end -- if
end -- fixbool

function load_room_from_database (uid)

  local room
 
  assert (uid, "No UID supplied to load_room_from_database")
  
  -- if not in database, don't look again
  if room_not_in_database [uid] then
    return nil
  end -- no point looking
  
  for row in db:nrows(string.format ("SELECT * FROM rooms WHERE uid = %s", fixsql (uid))) do
     room = {
       name = row.name,
       area = row.area,
       description = row.description,
       shop = row.shop,
       train = row.train,
       notes = row.notes,
  
       exits = {} }
      
    for exitrow in db:nrows(string.format ("SELECT * FROM exits WHERE fromuid = %s", fixsql (uid))) do
       room.exits [exitrow.dir] = tostring (exitrow.touid)
    end -- for each exit
    
  end   -- finding room

  if room then
    rooms [uid] = room
    return room
  end -- if found
  
  room_not_in_database [uid] = true
  return nil
    
end -- load_room_from_database


function save_room_to_database (uid, title, description)
  
  assert (uid, "No UID supplied to save_room_to_database")
  
  dbcheck (db:execute (string.format (
        "INSERT INTO rooms (uid, name, description, date_added) VALUES (%s, %s, %s, DATETIME('NOW'));",
          fixsql (uid), 
          fixsql (title),
          fixsql (description)
        )))
        
  dbcheck (db:execute (string.format ([[
        INSERT INTO rooms_lookup (uid, name, description) VALUES (%s, %s, %s);
      ]], fixsql  (uid),       
          fixsql  (title),
          fixsql  (description) 
          )))
        
  room_not_in_database [uid] = false
  
  if show_database_mods then
    mapper.mapprint ("Added room", uid, "to database. Name:", title)
  end -- if
  
end -- function save_room_to_database
      
function save_exits_to_database (uid, exits)
  
  for dir in string.gmatch (exits, "%a+") do

    -- fix up in and out
    dir = ({ ['i'] = "in", o = "out", }) [dir] or dir
    
    dbcheck (db:execute (string.format ([[
      INSERT INTO exits (dir, fromuid, touid, date_added) 
          VALUES (%s, %s, %s, DATETIME('NOW'));
    ]], fixsql  (dir),  -- direction (eg. "n")
        fixsql  (uid),         -- from current room
        fixsql  (0)     -- destination room (not known)
        )))
    if show_database_mods then
       mapper.mapprint ("Added unknown exit", dir, "from room", uid, "to database.")
    end -- if
  
  end -- for each exit
  
end -- function save_exits_to_database


function create_tables ()
  -- create rooms table
  dbcheck (db:execute[[
  
  PRAGMA foreign_keys = ON;
  PRAGMA journal_mode = WAL;
  
  CREATE TABLE IF NOT EXISTS rooms (
      roomid        INTEGER PRIMARY KEY AUTOINCREMENT,
      uid           TEXT NOT NULL,   -- vnum or how the MUD identifies the room
      name          TEXT,            -- name of room
      description   TEXT,            -- description
      building      TEXT,            -- which building it is in
      shop          INTEGER,         -- 1 = shop here
      train         INTEGER,         -- 1 = trainer here
      notes         TEXT,            -- player notes
      date_added    DATE,            -- date added to database
      UNIQUE (uid)
    );
  CREATE INDEX IF NOT EXISTS shop_index ON rooms (shop);
  CREATE INDEX IF NOT EXISTS train_index ON rooms (train);

  CREATE TABLE IF NOT EXISTS exits (
      exitid      INTEGER PRIMARY KEY AUTOINCREMENT,
      dir         TEXT    NOT NULL, -- direction, eg. "n", "s"
      fromuid     TEXT    NOT NULL, -- exit from which room (in rooms table)
      touid       TEXT    NOT NULL, -- exit to which room (in rooms table)
      date_added  DATE,             -- date added to database
      FOREIGN KEY(fromuid) REFERENCES rooms(uid)
    );
  CREATE INDEX IF NOT EXISTS fromuid_index ON exits (fromuid);
  CREATE INDEX IF NOT EXISTS touid_index   ON exits (touid);
  
  ]])
  
  -- check if rooms_lookup table exists
  local table_exists
  for a in db:nrows "SELECT * FROM sqlite_master WHERE name = 'rooms_lookup' AND type = 'table'" do
    table_exists = true
  end  -- for
  
  if not table_exists then
    dbcheck (db:execute "CREATE VIRTUAL TABLE rooms_lookup USING FTS3(uid, name, description);")
    -- in case we only deleted the rooms_lookup table to save space in the download
    dbcheck (db:execute "INSERT INTO rooms_lookup (uid, name, description) SELECT uid, name, description FROM rooms;")
  end -- if
   
      
end -- function create_tables

function room_edit_bookmark (room, uid)
  
  local notes = room.notes or ""
  
  if notes ~= "" then  
    newnotes = utils.inputbox ("Modify room comment (clear it to delete from database)", room.name, notes)
  else
    newnotes = utils.inputbox ("Enter room comment (creates a bookmark for this room)", room.name, notes)
  end -- if
  
  if not newnotes then
    return
  end -- if cancelled 
  
  if newnotes == "" then
    if notes == "" then
      mapper.mapprint ("No comment entered, bookmark not saved.")
      return
    else
      dbcheck (db:execute (string.format (
        "UPDATE rooms SET notes = NULL WHERE uid = %s;",
          fixsql (uid)
        )))
      mapper.mapprint ("Bookmark for room", uid, "deleted. Was previously:", notes)
      rooms [uid].notes = nil
      return
    end -- if
  end -- if
  
  if notes == newnotes then
    return -- no change made
  end -- if

  dbcheck (db:execute (string.format (
      "UPDATE rooms SET notes = %s WHERE uid = %s;",
        fixsql (newnotes),
        fixsql (uid)
      )))
    
  if notes ~= "" then
     mapper.mapprint ("Bookmark for room", uid, "changed to:", newnotes)
  else
     mapper.mapprint ("Bookmark added to room", uid, ":", newnotes)
   end -- if    
   
   rooms [uid].notes = newnotes
   
end -- room_edit_bookmark

function room_toggle_shop (room, uid)
   
  rooms [uid].shop = not rooms [uid].shop 
  
  dbcheck (db:execute (string.format (
      "UPDATE rooms SET shop = %i WHERE uid = %s;",
        fixbool (rooms [uid].shop),
        fixsql (uid)
      )))

  if rooms [uid].shop then
    mapper.mapprint ("Room", uid, "marked as a shop")
  else
    mapper.mapprint ("Room", uid, "not a shop any more")
  end    
  
  mapper.draw (current_room)
   
end -- room_toggle_shop

function room_toggle_train (room, uid)
   
  rooms [uid].train = not rooms [uid].train 
  
  dbcheck (db:execute (string.format (
      "UPDATE rooms SET train = %i WHERE uid = %s;",
        fixbool (rooms [uid].train),
        fixsql (uid)
      )))

  if rooms [uid].train then
    mapper.mapprint ("Room", uid, "marked as a training room")
  else
    mapper.mapprint ("Room", uid, "not a training room any more")
  end    
  
  mapper.draw (current_room)
   
end -- room_toggle_train



function room_add_exit (room, uid)

local available =  {
  n = "North",
  s = "South",
  e = "East",
  w = "West",
  u = "Up",
  d = "Down",
  ne = "Northeast",
  sw = "Southwest",
  nw = "Northwest",
  se = "Southeast",
  ['in'] = "In",
  out = "Out",
  }  -- end of available

  -- remove existing exits
  for k in pairs (room.exits) do
    available [k] = nil
  end -- for
  
  if next (available) == nil then
    utils.msgbox ("All exits already used.", "No free exits!", "ok", "!", 1)
    return
  end -- not known
  
  local chosen_exit = utils.listbox ("Choose exit to add", "Exits ...", available )
  if not chosen_exit then
    return
  end
  
  exit_destination = utils.inputbox ("Enter destination room identifier (number) for " .. available [chosen_exit], room.name, "")

  if not exit_destination then
    return
  end -- cancelled
  
    -- look it up
  local dest_room = rooms [exit_destination]
  
  -- not cached - see if in database
  if not dest_room then
    dest_room = load_room_from_database (exit_destination)
    rooms [exit_destination] = dest_room -- cache for later
  end -- not in cache
  
  if not dest_room then
    utils.msgbox ("Room " .. exit_destination .. " does not exist.", "Room does not exist!", "ok", "!", 1)
    return
  end -- if still not there

  dbcheck (db:execute (string.format ([[
    INSERT INTO exits (dir, fromuid, touid, date_added) 
        VALUES (%s, %s, %s, DATETIME('NOW'));
  ]], fixsql  (chosen_exit),  -- direction (eg. "n")
      fixsql  (uid),  -- from current room
      fixsql  (exit_destination) -- destination room 
      )))
  if show_database_mods then
    mapper.mapprint ("Added exit", available [chosen_exit], "from room", uid, "to room", exit_destination, "to database.")
  end -- if
  
  -- update in-memory table
  rooms [uid].exits [chosen_exit] = exit_destination
  
  mapper.draw (current_room)
   
end -- room_add_exit


function room_delete_exit (room, uid)

local available =  {
  n = "North",
  s = "South",
  e = "East",
  w = "West",
  u = "Up",
  d = "Down",
  ne = "Northeast",
  sw = "Southwest",
  nw = "Northwest",
  se = "Southeast",
  ['in'] = "In",
  out = "Out",
  }  -- end of available

  -- remove non-existent exits
  for k in pairs (available) do
    if room.exits [k] then
      available [k] = available [k] .. " --> " .. room.exits [k] 
    else
      available [k] = nil
    end -- if not a room exit
  end -- for
  
  if next (available) == nil then
    utils.msgbox ("There are no exits from this room.", "No exits!", "ok", "!", 1)
    return
  end -- not known
  
  local chosen_exit = utils.listbox ("Choose exit to delete", "Exits ...", available )
  if not chosen_exit then
    return
  end

  dbcheck (db:execute (string.format ([[
    DELETE FROM exits WHERE dir = %s AND fromuid = %s;
  ]], fixsql  (chosen_exit),  -- direction (eg. "n")
      fixsql  (uid)  -- from current room
      )))
  if show_database_mods then
    mapper.mapprint ("Deleted exit", available [chosen_exit], "from room", uid, "from database.")
  end -- if
  
  -- update in-memory table
  rooms [uid].exits [chosen_exit] = nil
  
  mapper.draw (current_room)
   
end -- room_delete_exit


function room_change_exit (room, uid)

local available =  {
  n = "North",
  s = "South",
  e = "East",
  w = "West",
  u = "Up",
  d = "Down",
  ne = "Northeast",
  sw = "Southwest",
  nw = "Northwest",
  se = "Southeast",
  ['in'] = "In",
  out = "Out",
  }  -- end of available

  -- remove non-existent exits
  for k in pairs (available) do
    if room.exits [k] then
      available [k] = available [k] .. " --> " .. room.exits [k] 
    else
      available [k] = nil
    end -- if not a room exit
  end -- for
  
  if next (available) == nil then
    utils.msgbox ("There are no exits from this room.", "No exits!", "ok", "!", 1)
    return
  end -- not known
  
  local chosen_exit = utils.listbox ("Choose exit to change destination of:", "Exits ...", available )
  if not chosen_exit then
    return
  end

  exit_destination = utils.inputbox ("Enter destination room identifier (number) for " .. available [chosen_exit], room.name, "")

  if not exit_destination then
    return
  end -- cancelled
  
    -- look it up
  local dest_room = rooms [exit_destination]
  
  -- not cached - see if in database
  if not dest_room then
    dest_room = load_room_from_database (exit_destination)
    rooms [exit_destination] = dest_room -- cache for later
  end -- not in cache
  
  if not dest_room then
    utils.msgbox ("Room " .. exit_destination .. " does not exist.", "Room does not exist!", "ok", "!", 1)
    return
  end -- if still not there
    
  dbcheck (db:execute (string.format ([[
    UPDATE exits SET touid = %s WHERE dir = %s AND fromuid = %s;
  ]], fixsql  (exit_destination),
      fixsql  (chosen_exit),  -- direction (eg. "n")
      fixsql  (uid)  -- from current room
      )))
      
  if show_database_mods then
    mapper.mapprint ("Modified exit", available [chosen_exit], "from room", uid, "to be to room", exit_destination, "in database.")
  end -- if
  
  -- update in-memory table
  rooms [uid].exits [chosen_exit] = exit_destination
  mapper.draw (current_room)
   
end -- room_change_exit

function room_click (uid, flags)

  -- check we got room at all
  if not uid then
    return nil
  end -- if
  
  -- look it up
  local room = rooms [uid]
  
  -- not cached - see if in database
  if not room then
    room = load_room_from_database (uid)
    rooms [uid] = room -- cache for later
  end -- not in cache
  
  if not room then
    return
  end -- if still not there
  
  local handlers = {
      { name = "Edit bookmark", func = room_edit_bookmark} ,
      { name = "-", } ,
      { name = "Add Exit", func = room_add_exit} ,
      { name = "Change Exit", func = room_change_exit} ,
      { name = "Delete Exit", func = room_delete_exit} ,
      { name = "-", } ,
      { name = "Toggle Shop", func = room_toggle_shop } ,
      { name = "Toggle Trainer", func = room_toggle_train } ,
     } -- handlers
      
  local t, tf = {}, {}
  for _, v in pairs (handlers) do
    table.insert (t, v.name)
    tf [v.name] = v.func
  end -- for
      
  local choice = WindowMenu (mapper.win, 
                            WindowInfo (mapper.win, 14), 
                            WindowInfo (mapper.win, 15), 
                            table.concat (t, "|"))
   
  local f = tf [choice]
  
  if f then
    f (room, uid)
  end -- if handler found
                           
end -- room_click


function map_find (name, line, wildcards)
 
  local rooms = {}
  local count = 0
  local snippets = {}
  local reset = ANSI (0)
  local bold = ANSI (1)
  local unbold = ANSI (22)
  
  function show_snippet (uid)
    AnsiNote (reset .. snippets [uid])
  end -- show_snippet

  
  -- find matching rooms using FTS3
  for row in db:nrows(string.format (
     [[
     SELECT uid, name, snippet(rooms_lookup, '%s', '%s', ' ... ', -1, -10) AS snippet 
        FROM rooms_lookup 
        WHERE rooms_lookup MATCH %s]], 
      bold, unbold,
      fixsql (wildcards [1]))) do
     rooms [row.uid] = true
     snippets [row.uid] = row.snippet
     count = count + 1
  end   -- finding room
  
  -- see if nearby
  mapper.find (
    function (uid) 
      local room = rooms [uid] 
      if room then
        rooms [uid] = nil
      end -- if
      return room, next (rooms) == nil
    end,  -- function
    show_vnums,  -- show vnum?
    count,      -- how many to expect
    false,       -- don't auto-walk
    show_snippet -- show find snippet
    )
  
end -- map_find

function map_bookmarks (name, line, wildcards)

  local rooms = {}
  local count = 0
  
  -- build table of special places (with info in them)
  for row in db:nrows(string.format ("SELECT uid, notes FROM rooms WHERE notes IS NOT NULL")) do
     rooms [row.uid] = capitalize (row.notes)
     count = count + 1
  end   -- finding room
  
  -- find such places
  mapper.find (
    function (uid) 
      local room = rooms [uid] 
      if room then
        rooms [uid] = nil
      end -- if
      return room, next (rooms) == nil  -- room will be type of info (eg. shop)
    end,  -- function
    show_vnums,  -- show vnum?
    count,       -- how many to expect
    false        -- don't auto-walk
    )
        
end -- map_bookmarks

function map_find_special (which)

  local rooms = {}
  local count = 0
  
  -- build table of special places (with info in them)
  for row in db:nrows(string.format ("SELECT uid, name FROM rooms WHERE %s = 1", which)) do
    rooms [row.uid] = true
    count = count + 1
  end   -- finding room
  
  -- find such places
  mapper.find (
    function (uid) 
      local room = rooms [uid] 
      if room then
        rooms [uid] = nil
      end -- if
      return room, next (rooms) == nil  -- room will be type of info (eg. shop)
    end,  -- function
    show_vnums,  -- show vnum?
    count,      -- how many to expect
    false       -- don't auto-walk
    )
        
end -- map_find_special

function map_shops (name, line, wildcards)
  map_find_special ("shop")
end -- map_shops

function map_trainers (name, line, wildcards)
  map_find_special ("train")
end -- map_trainers


function Door_Closed (name, line, wildcards)

local dirs =  {
  n = "north",
  s = "south",
  e = "east",
  w = "west",
  u = "up",
  d = "down",
  ne = "northeast",
  sw = "southwest",
  nw = "northwest",
  se = "southeast",
  ['in'] = "in",
  out = "out",
  }  -- end of available
  
  Cannot_Go ()
  
  if last_direction_moved then
    Send ("open " .. dirs [last_direction_moved])
    Send (dirs [last_direction_moved])
  end -- if
  
end -- Door_Closed

-- rname
function handleRoomName (rname)
  roomname = Trim (rname)
  roomdesc = ""
  exits = {}
  exits_str = ""
end -- handleRoomName
  
function handleRoomExit (ex)


end -- handleRoomExit

function handleRoomExits (exits)
  exits_str = exits:lower ()
  got_exits = true
end -- handleRoomExits

-- rdesc
function handleRoomDescription (desc)

  roomdesc = desc

  
  -- generate a "room ID" by hashing the room name, description and exits
  uid = utils.tohex (utils.md5 (roomname .. roomdesc .. exits_str))
  uid = uid:sub (1, 10)    -- more manageable length

  --[[
  ColourNote ("dimgray", "", "Room ID: " .. uid)  -- debugging
  ColourNote ("dimgray", "", "Name: " .. roomname)
  ColourNote ("dimgray", "", "Exits: " .. exits_str)
  ColourNote ("dimgray", "", roomdesc)
  --]]
  
  -- save so we know current room later on  
  current_room = uid

  for k, v in ipairs (moves) do
    if v.from == nil then
       v.from = current_room
       break
    end -- if
  end -- for
  
  --ColourNote ("rosybrown", "", roomdesc)
  --ColourNote ("white", "olive", uid)
  
  local room = rooms [current_room]
  
  -- not cached - see if in database
  if not room then
    --print ("Loading room", current_room, "from database")
    room = load_room_from_database (current_room)
  end -- not in cache
  
  if not room then
     --print ("Added room", uid)  -- debugging
     --print ("Name", roomname)
     --print ("Exits", exits_str)
     --ColourNote ("rosybrown", "", roomdesc)
     
    db:exec ("BEGIN TRANSACTION;") 

    save_room_to_database (current_room, roomname, Trim (roomdesc))
    save_exits_to_database (current_room, exits_str)
    
    db:exec ("COMMIT;") 

    -- get it back now
    room = load_room_from_database (current_room)

  end -- if room not there
  
  -- call mapper to draw this rom
  mapper.draw (current_room)    -- redraw room with name

  -- try to work out where previous room's exit led  
--  if expected_exit ~= uid and #moves > 0 then
    fix_up_exit ()
--  end -- exit was wrong
  
end -- handleRoomDescription

function OnPluginMXPcloseTag (name)
  local tagname, text = string.match (name, "(%a-),(.*)")
  AppendToNotepad ("MXP", "Closing tag: " .. tagname .. "\r\n")
  AppendToNotepad ("MXP", "Text: " .. text .. "\r\n")
  
  if tagname == "ex" then
    handleRoomExit (text)
  elseif tagname == "rexits" then
    handleRoomExits (text)
  elseif tagname == "rdesc" then
    handleRoomDescription (text)
  elseif tagname == "rname" then
    handleRoomName (text)
  end -- if
  
end -- function


]]>
</script>

</muclient>

