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.
- 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.
Minimal mapper
A minimal mapper will just draw the mapper window, but not draw any rooms.
That is:
|
To save and install the Example_Mapper plugin do this:
- Copy the code below (in the code box) to the Clipboard
- Open a text editor (such as Notepad) and paste the plugin code into it
- 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.
- Go to the MUSHclient File menu -> Plugins
- Click "Add"
- Choose the file Example_Mapper.xml (which you just saved in step 3) as a plugin
- Click "Close"
- 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:
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="<*hp *m *mv> <#*>*"
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="<*hp *m *mv> <#*>*"
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:
|