Search FAQ

Gammon Forum

Notice: Any messages purporting to come from this site telling you that your password has expired, or that you need to verify your details, confirm your email, resolve issues, making threats, or asking for money, are spam. We do not email users with any such messages. If you have lost your password you can obtain a new one by using the password reset link.
 Entire forum ➜ MUSHclient ➜ Plugins ➜ How to use the MUSHclient mapper on your own MUD

How to use the MUSHclient mapper on your own MUD

Posting of new messages is disabled at present.

Refresh page


Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Wed 22 Oct 2014 03:17 AM (UTC)

Amended on Fri 25 Jun 2021 11:08 PM (UTC) by Nick Gammon

Message
Introduction


This post will take you through the steps to use the MUSHclient room mapper on any MUD/MUSH or similar game.

It uses the MUSHclient "mapper.lua" module which is supplied with recent versions of MUSHclient (in the lua sub-folder).

Details about its features here: http://www.gammon.com.au/forum/?id=10138


Tip:
The "mapper.lua" module is MUD-agnostic. That is, it is just designed to render rooms in the mapper window, starting at the current one, and then following known exits.

It does not concern itself with where the room info comes from (eg. triggers / MXP / Telnet sequences) nor does it concern itself with how the room data is stored (eg. database, files, plugin "state" files, variables). That is the job of the plugin, described below.

It also does not concern itself with whether rooms are shops, trainers, guilds, etc. That also is the job of the plugin to find that information out, and pass it to the mapper in the form of suggested room colours, or "mouse-over" text.

See http://www.gammon.com.au/forum/?id=14607 for a description of a "learning mapper" which uses the mapper module described here, but tries to learn where room descriptions and exits are.


What does the mapper need?


To work properly, the mapper has to know, somehow, a few simple things:


  • A unique way of identifying each room (unique ID or "uid"). Without a unique ID, it cannot tell the difference between one room and another one.

    Some MUDs provide a "room number" (sometimes known as a "vnum" or "database number") which makes things easy. Otherwise we have to work out a way of uniquely identifying each room, for example by using the room name, or hashing up the room description.

  • Which room you are currently in. It draws the map with your current room in the centre, thus it needs to know what that room is.

    If we are lucky, the MUD will tell us which room we are in. Otherwise, we can probably work it out from which room showed up when we last changed rooms or did a "look".

  • What exits the current room has.

    This is often shown by the MUD, eg.

    
    Exits: north east.
    


  • Where the exits from each room lead

    If we are lucky, the MUD will tell us that. Otherwise we have to deduce it by knowing we used to be in room A, we walked east, and now we are in room B, thus we deduce that the east exit from A leads to B. ie.

    
    A  --> east --> B
    



Minimal mapper


A minimal mapper will just draw the mapper window, but not draw any rooms.

That is:

Template:saveplugin=Example_Mapper To save and install the Example_Mapper plugin do this:
  1. Copy the code below (in the code box) to the Clipboard
  2. Open a text editor (such as Notepad) and paste the plugin code into it
  3. Save to disk on your PC, preferably in your plugins directory, as Example_Mapper.xml
    • The "plugins" directory is usually under the "worlds" directory inside where you installed MUSHclient.
  4. Go to the MUSHclient File menu -> Plugins
  5. Click "Add"
  6. Choose the file Example_Mapper.xml (which you just saved in step 3) as a plugin
  7. Click "Close"
  8. Save your world file, so that the plugin loads next time you open it.



<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

<description trim="y">
<![CDATA[
 Description here.
]]>
</description>

</plugin>

<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { }    

-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config = config,      -- ie. colours, sizes
            get_room = get_room,  -- info about room (uid)
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

]]>
</script>
</muclient>


The above plugin just initializes the mapper, using default colours. The two things we have to supply are a configuration table (missing entries take default values in the mapper.lua module) and a "get_room" function which the mapper uses to get information about each room.

Installing the above, we see:



Adding help


Let's start by adding another configuration option "show_help". Changes are shown in bold below.



<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

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

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
]]>
</description>

</plugin>

<aliases>
  <alias
   script="OnHelp"
   match="mapper help"
   enabled="y"
  >
</alias>

</aliases>
  
<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { }    

-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config     = config,   -- ie. colours, sizes
            get_room   = get_room, -- info about room (uid)
            show_help  = OnHelp,   -- to show help
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

-- -----------------------------------------------------------------
-- Plugin Help
-- -----------------------------------------------------------------
function OnHelp ()
  mapper.mapprint (string.format ("[MUSHclient mapper, version %0.1f]", mapper.VERSION))
  mapper.mapprint (world.GetPluginInfo (world.GetPluginID (), 3))
end
]]>
</script>
</muclient>


If you reinstall the plugin the map won't look any different (yet) because we haven't told it to draw a room. However we can now type:


mapper help


Which shows the help above:


[MUSHclient mapper, version 2.5]
AUTOMATIC MAPPER ...  by Nick Gammon

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)


Draw a room


Let's draw a room now. For testing purposes we'll just draw starting at room 1.


  mapper.draw (1)  -- draw room 1


To do that we'll add an alias "mapper test" which, when invoked, does the above function call. So far we don't know anything about room 1, like its name, description, exits, etc., but we'll see how far this takes us:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

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

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
]]>
</description>

</plugin>

<aliases>
  <alias
   match="mapper help"
   script="OnHelp"
   enabled="y"
  >
  </alias>

  <alias
   match="mapper test"
   script="test"
   enabled="y"
  >
  </alias>

</aliases>
  
<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { }    

-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config     = config,   -- ie. colours, sizes
            get_room   = get_room, -- info about room (uid)
            show_help  = OnHelp,   -- to show help
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

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

-- -----------------------------------------------------------------
-- test
-- -----------------------------------------------------------------
function test (name, line, wildcards)
  mapper.draw (1)  -- draw room 1
end -- test

]]>
</script>
</muclient>


Typing in "mapper test" we see this:



(Room a bit hard to see, circled by me to highlight).

And now we can see the "help" button (the "?" on the right) and the "configuration" button (the "*" on the left). Clicking the help button shows the same help as before, and clicking the configuration button opens the configuration sub-window:



Supplying room information


To draw a room we need to tell the mapper, when asked, information about a given room.

Some comments near the start of mapper.lua tell us what we need to supply to the mapper, for each room.


Room info should include:

  name          (what to show as room name)
  exits         (table keyed by direction, value is exit uid)
  area          (area name)
  hovermessage  (what to show when you mouse-over the room)
  bordercolour  (colour of room border)     - RGB colour
  borderpen     (pen style of room border)  - see WindowCircleOp (values 0 to 6)
  borderpenwidth(pen width of room border)  - eg. 1 for normal, 2 for current room
  fillcolour    (colour to fill room)       - RGB colour, nil for default
  fillbrush     (brush to fill room)        - see WindowCircleOp (values 0 to 12)


Most of that has reasonable defaults. What we really need is some sort of room name, and hopefully a list of exits.

Let's make up a tiny MUD using a mud map (little joke, there):



Using that map we can express our little MUD as a small Lua table:


rooms = {

  [1] = {
        name = "Town Square",
        exits = { n = 2, s = 3, e = 4}
        },

  [2] = {
        name = "The Inn",
        exits = { s = 1 }
        },

  [3] = {
        name = "Butcher",
        exits = { n = 1  }
        },
                
  [4] = {
        name = "Main Street",
        exits = { w = 1, e = 5  }
        },

  [5] = {
        name = "West Main Street",
        exits = {  w = 4  }
        },
                
        }  -- example rooms table


We can see in each case the room number, its name, and where the exits from each room goes. Now this is the basic information you have to extract from your own MUD, to make the mapper work.

The complete plugin looks like this:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

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

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
]]>
</description>

</plugin>

<aliases>
  <alias
   match="mapper help"
   script="OnHelp"
   enabled="y"
  >
  </alias>

  <alias
   match="mapper test"
   script="test"
   enabled="y"
  >
  </alias>

</aliases>
  
<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { }    

rooms = {

  [1] = {
        name = "Town Square",
        exits = { n = 2, s = 3, e = 4}
        },

  [2] = {
        name = "The Inn",
        exits = { s = 1 }
        },

  [3] = {
        name = "Butcher",
        exits = { n = 1  }
        },
                
  [4] = {
        name = "Main Street",
        exits = { w = 1, e = 5  }
        },

  [5] = {
        name = "West Main Street",
        exits = {  w = 4  }
        },
                
        }  -- example rooms table
        
-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
 
  room = rooms [uid]
  if not room then
    return nil
  end -- if not found
  
  room.area = "The swamp"  -- OK, let's assume every room is in this area
  room.hovermessage = room.name

  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config     = config,   -- ie. colours, sizes
            get_room   = get_room, -- info about room (uid)
            show_help  = OnHelp,   -- to show help
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

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

-- -----------------------------------------------------------------
-- test
-- -----------------------------------------------------------------
function test (name, line, wildcards)
  mapper.draw (1)  -- draw room 1
end -- test

]]>
</script>
</muclient>


The results are (if we type "mapper test") as follows:



We can see the rooms drawn exactly as expected, with the first room (room 1) in the centre.

And, if we hover the mouse over a particular room, the "room.hovermessage" message is shown:



Important point:

You only have to tell the mapper to draw one room (the current one). It draws the other ones automatically, based on the exits (if any) from the current room (and subsequent rooms). So in this case, although we only told the mapper to draw room #1, it actually drew 5 rooms, because it found a link from room 1 to room 2, and so on. So the key to getting the whole map drawn is to let the mapper know which rooms connect to which other ones.

Integrate with your own MUD


For simplicity here, I am going to assume your MUD gives you a room number, like this:


Market Street
The cobbled road is buzzing with activity.  Eager shouts from numerous tents
and pavilions encourage potential customers to come inspect a shopkeeper's
wares.  To the north is the Food Market, while the tent to the south is
dedicated to repairing items.  Market Street stretches to the east and west.
Exits: north east south west.
<24hp 145m 107mv> <#21048> 



Tip:
If not, well life is a bit harder, You could take the description and "hash" it. For example:


roomname = "Market Street"
roomdesc = [[
The cobbled road is buzzing with activity.  Eager shouts from numerous tents
and pavilions encourage potential customers to come inspect a shopkeeper's
wares.  To the north is the Food Market, while the tent to the south is
dedicated to repairing items.  Market Street stretches to the east and west.]]


uid = utils.tohex (utils.md5 (roomname .. roomdesc))
uid = uid:sub (1, 10)    -- more manageable length

print (uid)  --> 2780473A26


So in this case "Market Street" would have a UID of "2780473A26". This isn't too hard to do with a couple of suitable triggers.


Detecting room names


In my example case, I might know the room number, but have to work out the room name myself. So I'll make a trigger to do that.


<trigger
   back_colour="8"
   bold="y"
   match_bold="y"
   enabled="y"
   match="*"
   match_back_colour="y"
   match_text_colour="y"
   script="got_room_name"
   text_colour="15"
   keep_evaluating="y"
   sequence="90"
  >
  </trigger>
</triggers>


This matches any text, in bold white, and has "keep evaluating" set so that other triggers can match as well.

We'll make a function "got_room_name" that just remembers the last name we saw, hoping that the next prompt will have us still in the same room.

We also have a trigger to match on the prompt line, and pull out the room number. That puts that room name into the rooms table, keyed by the room number.

I have discarded the "mapper test" alias, as our prompt line should now trigger the map being drawn.

So the amended plugin is now:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

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

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
]]>
</description>

</plugin>

<aliases>
  <alias
   match="mapper help"
   script="OnHelp"
   enabled="y"
  >
  </alias>

</aliases>
  
<triggers>
  <trigger
   enabled="y"
   match="&lt;*hp *m *mv&gt; &lt;#*&gt;*"
   script="got_prompt"
   sequence="100"
  >
  </trigger>

<trigger
   back_colour="8"
   bold="y"
   match_bold="y"
   enabled="y"
   match="*"
   match_back_colour="y"
   match_text_colour="y"
   script="got_room_name"
   text_colour="15"
   keep_evaluating="y"
   sequence="90"
  >
  </trigger>
</triggers>

<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { }    

-- all the rooms we have come across
rooms = { }

-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
 
  room = rooms [uid]
  if not room then
    return nil
  end -- if not found
  
  room.area = "The swamp"  -- OK, let's assume every room is in this area
  room.hovermessage = room.name
  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config     = config,   -- ie. colours, sizes
            get_room   = get_room, -- info about room (uid)
            show_help  = OnHelp,   -- to show help
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

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

-- -----------------------------------------------------------------
-- Here on prompt
-- -----------------------------------------------------------------
function got_prompt (name, line, wildcards)
  uid = wildcards [4]
  
  -- assume we know the room name by now
  -- add to rooms table if not there
  if not rooms [uid] then
    rooms [uid] = { name = room_name, exits = {}  }
  end -- if
  
  -- draw this room
  mapper.draw (uid)
end -- got_prompt

-- -----------------------------------------------------------------
-- Here on room name
-- -----------------------------------------------------------------
function got_room_name (name, line, wildcards)
  local name = wildcards [1]
  
  -- ignore exits lines
  if string.match (name, "^Exits: ") then
    return
  end -- if
  
  -- ignore really long lines
  if #name > 50 then
    return
  end -- if
  
  room_name = name
end -- got_room_name


]]>
</script>
</muclient>



Now as we walk around my test MUD, the mapper updates automatically, drawing the current room, and its name:



Getting the exits


But, there's a problem. Only the current room is drawn, because we haven't deduced where the exits go.

So in the absence of data from the MUD about this, we have to work out which exit goes where. One way of doing that is:


  • Catch movement commands (eg. "n") and remember them.
  • If we have changed rooms, assume that the last thing we typed (eg. "n") means that going north from our previous room leads us to this room.


The code below attempts to do that:


<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<muclient>
<plugin
   name="Example_Mapper"
   author="Nick Gammon"
   id="63e6909083318cf63707c044"
   language="Lua"
   purpose="Example mapper"
   save_state="y"
   date_written="2014-10-22"
   requires="4.61"
   version="1.0"
   >

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

ACTIONS

mapper help         --> this help  (or click the "?" button on the bottom right)
]]>
</description>

</plugin>

<aliases>
  <alias
   match="mapper help"
   script="OnHelp"
   enabled="y"
  >
  </alias>

</aliases>
  
<triggers>
  <trigger
   enabled="y"
   match="&lt;*hp *m *mv&gt; &lt;#*&gt;*"
   script="got_prompt"
   sequence="100"
  >
  </trigger>

<trigger
   back_colour="8"
   bold="y"
   match_bold="y"
   enabled="y"
   match="*"
   match_back_colour="y"
   match_text_colour="y"
   script="got_room_name"
   text_colour="15"
   keep_evaluating="y"
   sequence="90"
  >
  </trigger>
</triggers>

<!--  Script  -->

<script>
<![CDATA[

-- mapper module
require "mapper"

-- configuration table
config = { 
        OUR_ROOM_COLOUR         = { name = "Our room",  colour =  ColourNameToRGB "black", },
         }    

-- all the rooms we have come across
rooms = { }


-- -----------------------------------------------------------------
-- these commands will be considered "room changing" commands
-- -----------------------------------------------------------------
local 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
  
-- for calculating the way back
local inverse_direction = {
  n = "s",
  s = "n",
  e = "w",
  w = "e",
  u = "d",
  d = "u",
  ne = "sw",
  sw = "ne",
  nw = "se",
  se = "nw",
  ['in'] = "out",
  out = "in",
  }  -- end of inverse_direction
  
-- -----------------------------------------------------------------
-- mapper 'get_room' callback - it wants to know about room uid
-- -----------------------------------------------------------------
function get_room (uid)
 
  room = rooms [uid]
  if not room then
    return nil
  end -- if not found


  -- how to draw this particular room
  room.bordercolour = config.ROOM_COLOUR.colour
  room.borderpen = miniwin.pen_solid 
  room.borderpenwidth = 1
  room.fillbrush = miniwin.brush_null  -- no fill

  -- draw current room in bolder colour  
  if uid == current_room then
    room.bordercolour = config.OUR_ROOM_COLOUR.colour
    room.borderpenwidth = 2
  end -- not in this area
  
  room.area = "The swamp"  -- assume every room is in this area
  room.hovermessage = room.name
  return room
end -- get_room

-- -----------------------------------------------------------------
-- Plugin Install
-- -----------------------------------------------------------------
function OnPluginInstall ()
  -- initialize mapper
  mapper.init { 
            config     = config,   -- ie. colours, sizes
            get_room   = get_room, -- info about room (uid)
            show_help  = OnHelp,   -- to show help
              }
               
  mapper.mapprint (string.format ("MUSHclient mapper installed, version %0.1f", mapper.VERSION))
end -- OnPluginInstall

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

-- -----------------------------------------------------------------
-- Here on prompt
-- -----------------------------------------------------------------
function got_prompt (name, line, wildcards)
  uid = wildcards [4]
  
  -- assume we know the room name by now
  -- add to rooms table if not there
  if not rooms [uid] then
    rooms [uid] = { name = room_name, exits = {} }
  end -- if
  
  -- if we changed rooms assume that our last movement sent us here
  if uid ~= current_room 
        and current_room
        and last_direction_moved then
    -- previous room led here
    rooms [current_room].exits [last_direction_moved] = uid 
    -- assume inverse direction leads back
    rooms [uid].exits [inverse_direction [last_direction_moved]] = current_room
  end -- if
  
  -- this is now our current room
  current_room = uid
  
  -- draw this room
  mapper.draw (current_room)

end -- got_prompt

-- -----------------------------------------------------------------
-- Here on room name
-- -----------------------------------------------------------------
function got_room_name (name, line, wildcards)
  local name = wildcards [1]
  
  -- ignore exits lines
  if string.match (name, "^Exits: ") then
    return
  end -- if
  
  -- ignore really long lines
  if #name > 50 then
    return
  end -- if
  
  room_name = name
end -- got_room_name

-- -----------------------------------------------------------------
-- try to detect when we send a movement command
-- -----------------------------------------------------------------
function OnPluginSent (sText)
  last_direction_moved = valid_direction [sText]
end -- OnPluginSent

]]>
</script>
</muclient>



This works moderately well:



We can see rooms being linked. It would be better to detect the "exits" line from the MUD and put in a "dummy" exit as soon as we can (eg. leading to room zero) so the mapper draws a "stub" line to indicate an unexplored exit.

Where to from here?


One fairly obvious omission from the above is any ability to remember the map. Without that, you have to re-map the MUD every time you start up the client.

Doing this is a bit more complex, because you need to save the data somewhere (I prefer to use a SQLite3 database), and then get it back when needed.

For a lengthier example, take a look at this:

https://github.com/nickgammon/plugins/blob/master/Realm_Of_Magic_Mapper.xml

That illustrates opening a database, creating tables if required, saving changes to the map information to the database, and loading it back next time.

You can also add extra features such as:


  • Detecting shops, trainers, inns, and so on, and showing them in a different colour.
  • Searching the database for matches on things (like shops) or just some search text
  • Listing shops, trainers, etc. on request
  • Detecting messages like "this door is locked" and opening it, or cancelling speed walking.
  • Adding bookmarks (free-format comments per room)
  • Amending the automatically-detected exits on request


Example of the mapper running on Realm of Magic:


- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Reply #1 on Sat 09 May 2015 09:17 PM (UTC)

Amended on Sat 09 May 2015 09:58 PM (UTC) by Nick Gammon

Message
Example of adapting for a MUD that doesn't have Vnums


There is a bigger challenge if your MUD doesn't help you with room numbers. Then you have to invent your own. As I suggested in other threads, one way of doing this it so "hash" up the room name / description / exits to give a unique ID, for example:


7A1E2B98153D1D3874A59E626


This ID can then be used to know which room you are in. Taking as an exercise I tried to do this on Lands of Redemption MUD. An example of normal MUD output is this:



We can see a few important lines here:


  • A "prompt" line starting with "<"
  • A "room name" line which is a short line describing the room
  • Multiple room description lines, most of which start by being indented by two spaces
  • An "exits" line which looks like: [Exits: *]


Thus to put all this together we need to look for a room name. Once we have a room name, we tentatively start looking for a room description, being the next thing received, indented by two spaces.

After that we gather more lines (being more description) until the exits line.

If we get all of this we can then hash them together to get our room ID.

Certain things will stop this process. For example, if we get a prompt line, before getting the exits line, or an exits line without any description, we assume that other MUD output misled us into thinking it was a room description.

These lines, for example, generate the room UID (Unique ID):


  -- generate a "room ID" by hashing the room name, description and exits
    
  uid = utils.tohex (utils.md5 (room_name .. room_description .. exits_string))
  uid = uid:sub (1, 25)  


With a room ID we can then add this room to the rooms table (if it isn't there already), keyed by the room ID. We can add things like the name, description, and table of exits.

Changing rooms

Also when we change rooms, as described earlier in this thread, we can "fix up" which room leads to what. If we were in room A, and we go east, and are now in room B, we can logically assume that east from A is B. Thus we amend the east exit from A to point to B, and the west exit of B to point to A.

Detecting no movement

Certain (humourous) messages tell us we couldn't move. These are detected, so that we don't erroneously think we changed rooms when we didn't.

You can see the source code here on GitHub:

Template:saveplugin=Lands_Of_Redemption_Mapper To save and install the Lands_Of_Redemption_Mapper plugin do this:
  1. Go to the GitHub page: Lands_Of_Redemption_Mapper.xml
  2. Select all the page and copy it to the Clipboard
  3. Open a text editor (such as Notepad) and paste the plugin into it
  4. Save to disk on your PC, preferably in your plugins directory, as: Lands_Of_Redemption_Mapper.xml
  5. Go to the MUSHclient File menu -> Plugins
  6. Click "Add"
  7. Choose the file Lands_Of_Redemption_Mapper.xml (which you just saved in step 4) as a plugin
  8. Click "Close"

The main GitHub page for this plugin is at: https://github.com/nickgammon/plugins/blob/master/Lands_Of_Redemption_Mapper.xml.

There you will find the commit history and other information.



Here is an example of it operating with debugging turned on. You can see the decision-making as it processes each line:



A trail through the dense forest
process_room_name: A trail through the dense forest
  You are on a trail leading east and west through the dense forest.  To the
process_description_line:   You are on a trail leading east and west through the dense forest.  To the
east, the forest gradually seems to become lighter.
process_description_line: east, the forest gradually seems to become lighter.

[Exits: north east west]
Exits: [Exits: north east west]
0 changed exits
  You are on a trail leading east and west through the dense forest.  To the
east, the forest gradually seems to become lighter.
last_direction_moved = w
current_room = 9DA2EF3CE5EB4F64393DB2B0C
Added room 1EF0C891AB04FDFDBBE1F40C8 to database. Name: A trail through the dense forest
Added exit e to database.
Added exit w to database.
Added exit n to database.
Fixed exit  w from 9DA2EF3CE5EB4F64393DB2B0C to here in database

<4263/6643hp 648/1003m 1219mv 847 [A trail through the dense forest]> 
got_prompt
No description


Serializing room data


The plugin also illustrates serializing (saving and restoring) the room data, so next time you play, it remembers all the rooms. This is done by saving the rooms table in the OnPluginSaveState function:


-- -----------------------------------------------------------------
-- OnPluginSaveState - save rooms info
-- -----------------------------------------------------------------
function OnPluginSaveState ()

  -- just save the relevant stuff  
  wanted_keys = { "name", "area", "exits", "description" }
  
  saved_rooms = { }
  for k, v in pairs (rooms) do
    saved_rooms [k] = { }
    for i, j in ipairs (wanted_keys) do
      saved_rooms [k] [j] = v [j]
    end -- for
  end -- for
 
  SetVariable ("rooms", "rooms = " .. serialize.save_simple (saved_rooms))
              
end -- function OnPluginSaveState


That function actually makes a smaller copy of the rooms table, discarding all elements except for "wanted_keys". This saves saving stuff that will be recreated next time, like the room line thickness.

Inside OnPluginInstall the saved rooms data is restored back into a Lua table in one line of code:


  -- on plugin install, convert variable into Lua table
  assert (loadstring (GetVariable ("rooms") or "")) ()


Inside the saved "state" file you can see the rooms data (RH-double-click on the plugin in the plugins list to see it). An example room is:


 CAB4A0FDD1E4CC00ADAE29654 = {
    exits = {
      e = "2E21E716C456E749964877C22",
      n = "56880738E08F38CF292147125",
      },
    description = "  You are at the southwest corner of the city wall.  The Concourse leads\
both north and east.",
    name = "On the Concourse",
    area = "Lands of Redemption",
    },


SQL database


It would be better to save the rooms data into a SQLite3 database. That way the rooms can be shared between multiple characters that you might have. Alternatively you might serialize to a disk file that is shared between worlds. The mapper I did recently for Achaea uses the SQLite3 database (http://www.gammon.com.au/forum/?id=12873).

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Tharius   (29 posts)  Bio
Date Reply #2 on Wed 13 May 2015 01:58 PM (UTC)
Message
Nick,

Thanks for the update. I'm not sure if the hashing idea will ultimately work out for Realms of Despair but your post brought my thoughts back to trying to get the mapper working.

Realms has a number of rooms where the name, description and exits are identical (and that's not including intentionally shifting mazes or anything advanced).

Still it's great food for thought, thank you.
Top

Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Reply #3 on Wed 13 May 2015 07:51 PM (UTC)
Message
Even so, most rooms work. So once the mapper finds a non-confusing room it synchronizes again with the existing room layout.

Think of it like a video game where they deliberately put your character in fog for a little while. You eventually get past that spot.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Xavious   (41 posts)  Bio
Date Reply #4 on Tue 07 Jul 2015 04:15 PM (UTC)

Amended on Tue 07 Jul 2015 04:51 PM (UTC) by Xavious

Message
Nick, you are awesome! This tutorial really helped me understand how to use the mapper.lua library effectively. I've spent the last week building and fine tuning this for a mud that I play.

The particular MUD I play supports MSDP, so I chose to have my room generation fire OnPluginBroadcast() from my MSDP handler whenever the VNUM variable is broadcasted. This worked excellent except for in situations where you walk faster than the room broadcasts and your last direction is no longer relevant. Basically when you've stacked walking commands, but the character hasn't actually entered the room yet.

I haven't found an elegant way to deal with this other than walk slow to build the map, then use a toggle to turn mapping off. This works well enough, though I'd be interested in suggestions.

I'm curious what the conventional wisdom is in regards to using a database to save room information compared to relying on serialized save states. At this point in time, my mapper save state is over 43,000 lines. I imagine that this might be a bit overwhelming to the client, my computer, or just have the potential to cause some kind of catastrophe.

Should I be using a database to store this massive amount of information?

Are there risks associated with having such a huge safe state?

Lastly, can I convert what I already have into a database if I needed to?
Top

Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Reply #5 on Tue 07 Jul 2015 09:04 PM (UTC)
Message
I used SQLite3 database for the larger mapping projects I was involved with, like Aardwolf and the IRE MUDs.

That is supposed to be fail-safe in event of a power failure etc. You could always (in addition) just back up the database file daily.

An example is here:

https://github.com/nickgammon/plugins/blob/master/GMCP_Mapper.xml

As for converting, if you haven't mapped all that much it is probably faster to re-map. You could do it, but going through your existing tables, and converting it to the database would take time.

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Xavious   (41 posts)  Bio
Date Reply #6 on Tue 07 Jul 2015 11:18 PM (UTC)
Message
If you save it to a database, is the client just loading the whole table back into the state/memory when you open up the world again?

I'm curious if the bloated state file is a concern for performance. Like I mentioned, mine is already 43,000 lines long. I can't help but wonder if this is an efficiency/optimization concern.

If I'm not worried about losing the room data, is it still worthwhile to save to a database instead?
Top

Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Reply #7 on Wed 08 Jul 2015 01:45 AM (UTC)
Message
The database is much better for large files. The entire state file has to be loaded into memory, so this will get increasingly slow. Plus, the whole lot has to be written out to save it. If you get a disk crash or power failure while it is being written you could lose the lot (it doesn't save to a temporary file and rename it).

With a database, only the requested data is read at any one time (eg. one room).

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

Posted by Nick Gammon   Australia  (23,122 posts)  Bio   Forum Administrator
Date Reply #8 on Wed 08 Jul 2015 01:46 AM (UTC)
Message
If you have a lot of rooms mapped, it probably isn't *too* hard to convert that to a database. Once you have your database format worked out, you could "walk" your state data (ie, your table) and generate "insert into" SQL statements for each room in it (plus the exits).

- Nick Gammon

www.gammon.com.au, www.mushclient.com
Top

The dates and times for posts above are shown in Universal Co-ordinated Time (UTC).

To show them in your local time you can join the forum, and then set the 'time correction' field in your profile to the number of hours difference between your location and UTC time.


43,234 views.

Posting of new messages is disabled at present.

Refresh page

Go to topic:           Search the forum


[Go to top] top

Information and images on this site are licensed under the Creative Commons Attribution 3.0 Australia License unless stated otherwise.