-- mapper.lua --[[ Author: Nick Gammon Date: 11th March 2010 Amended: 15th August 2010 Amended: 2nd October 2010 Amended: 18th October 2010 to added find callback Amended: 16th November 2010 to add symbolic constants (miniwin.xxxx) Amended: 18th November 2010 to add more timing and count of times called Also added zooming with the mouse wheel. Amended: 26th November 2010 to check timers are enabled when speedwalking. Amended: 11th November 2014 to allow for detecting mouse-overs of rooms Generic MUD mapper. Exposed functions: init (t) -- call once, supply: t.config -- ie. colours, sizes t.get_room -- info about room (uid) t.show_help -- function that displays some help t.room_click -- function that handles RH click on room (uid, flags) t.room_mouseover -- function that handles mouse-over a room (uid, flags) t.room_cancelmouseover -- function that handles cancelled mouse-over of a room (uid, flags) t.timing -- true to show timing t.show_completed -- true to show "Speedwalk completed." t.show_other_areas -- true to show non-current areas t.show_up_down -- follow up/down exits t.show_area_exits -- true to draw a circle around rooms leading to other areas t.speedwalk_prefix -- if not nil, speedwalk by prefixing with this zoom_in () -- zoom in map view zoom_out () -- zoom out map view mapprint (message) -- like print, but uses mapper colour maperror (message) -- like print, but prints in red hide () -- hides map window (eg. if plugin disabled) show () -- show map window (eg. if plugin enabled) save_state () -- call to save plugin state (ie. in OnPluginSaveState) draw (uid) -- draw map - starting at room 'uid' start_speedwalk (path) -- starts speedwalking. path is a table of directions/uids build_speedwalk (path) -- builds a client speedwalk string from path cancel_speedwalk () -- cancel current speedwalk, if any check_we_can_find () -- returns true if doing a find is OK right now find (f, show_uid, count, walk) -- generic room finder find_paths (uid, f) -- lower-level room finder (for getting back a path) Exposed variables: win -- the window (in case you want to put up menus) VERSION -- mapper version last_hyperlink_uid -- room uid of last hyperlink click (destination) last_speedwalk_uid -- room uid of last speedwalk attempted (destination) -- functions required to be global by the client (eg. for mouseup) 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) --]] module (..., package.seeall) VERSION = 2.6 -- for querying by plugins require "movewindow" require "copytable" require "gauge" require "pairsbykeys" require "mw" local FONT_ID = "fn" -- internal font identifier local FONT_ID_UL = "fnu" -- internal font identifier - underlined -- size of room box local ROOM_SIZE = 10 -- how far away to draw rooms from each other local DISTANCE_TO_NEXT_ROOM = 15 -- supplied in init local config -- configuration table local supplied_get_room local room_click local room_mouseover local room_cancelmouseover local timing -- true to show timing and other info local show_completed -- true to show "Speedwalk completed." local show_other_areas -- true to draw other areas local show_area_exits -- true to show area exits local show_up_down -- true to show up/down exits -- current room number local current_room -- our copy of rooms info local rooms = {} local last_visited = {} -- other locals local HALF_ROOM, connectors, half_connectors, arrows local plan_to_draw, speedwalks, drawn, drawn_coords local last_drawn, depth, windowinfo, font_height local walk_to_room_name local total_times_drawn = 0 local total_time_taken = 0 local function build_room_info () HALF_ROOM = ROOM_SIZE / 2 local THIRD_WAY = DISTANCE_TO_NEXT_ROOM / 3 local DISTANCE_LESS1 = DISTANCE_TO_NEXT_ROOM - 1 -- how to draw a line from this room to the next one (relative to the center of the room) connectors = { n = { x1 = 0, y1 = - HALF_ROOM, x2 = 0, y2 = - HALF_ROOM - DISTANCE_LESS1, at = { 0, -1 } }, s = { x1 = 0, y1 = HALF_ROOM, x2 = 0, y2 = HALF_ROOM + DISTANCE_LESS1, at = { 0, 1 } }, e = { x1 = HALF_ROOM, y1 = 0, x2 = HALF_ROOM + DISTANCE_LESS1, y2 = 0, at = { 1, 0 }}, w = { x1 = - HALF_ROOM, y1 = 0, x2 = - HALF_ROOM - DISTANCE_LESS1, y2 = 0, at = { -1, 0 }}, ne = { x1 = HALF_ROOM, y1 = - HALF_ROOM, x2 = HALF_ROOM + DISTANCE_LESS1 , y2 = - HALF_ROOM - DISTANCE_LESS1, at = { 1, -1 } }, se = { x1 = HALF_ROOM, y1 = HALF_ROOM, x2 = HALF_ROOM + DISTANCE_LESS1 , y2 = HALF_ROOM + DISTANCE_LESS1, at = { 1, 1 } }, nw = { x1 = - HALF_ROOM, y1 = - HALF_ROOM, x2 = - HALF_ROOM - DISTANCE_LESS1 , y2 = - HALF_ROOM - DISTANCE_LESS1, at = {-1, -1 } }, sw = { x1 = - HALF_ROOM, y1 = HALF_ROOM, x2 = - HALF_ROOM - DISTANCE_LESS1 , y2 = HALF_ROOM + DISTANCE_LESS1, at = {-1, 1 } }, } -- end connectors -- how to draw a stub line half_connectors = { n = { x1 = 0, y1 = - HALF_ROOM, x2 = 0, y2 = - HALF_ROOM - THIRD_WAY, at = { 0, -1 } }, s = { x1 = 0, y1 = HALF_ROOM, x2 = 0, y2 = HALF_ROOM + THIRD_WAY, at = { 0, 1 } }, e = { x1 = HALF_ROOM, y1 = 0, x2 = HALF_ROOM + THIRD_WAY, y2 = 0, at = { 1, 0 }}, w = { x1 = - HALF_ROOM, y1 = 0, x2 = - HALF_ROOM - THIRD_WAY, y2 = 0, at = { -1, 0 }}, ne = { x1 = HALF_ROOM, y1 = - HALF_ROOM, x2 = HALF_ROOM + THIRD_WAY , y2 = - HALF_ROOM - THIRD_WAY, at = { 1, -1 } }, se = { x1 = HALF_ROOM, y1 = HALF_ROOM, x2 = HALF_ROOM + THIRD_WAY , y2 = HALF_ROOM + THIRD_WAY, at = { 1, 1 } }, nw = { x1 = - HALF_ROOM, y1 = - HALF_ROOM, x2 = - HALF_ROOM - THIRD_WAY , y2 = - HALF_ROOM - THIRD_WAY, at = {-1, -1 } }, sw = { x1 = - HALF_ROOM, y1 = HALF_ROOM, x2 = - HALF_ROOM - THIRD_WAY , y2 = HALF_ROOM + THIRD_WAY, at = {-1, 1 } }, } -- end half_connectors -- how to draw one-way arrows (relative to the center of the room) arrows = { n = { - 2, - HALF_ROOM - 2, 2, - HALF_ROOM - 2, 0, - HALF_ROOM - 6 }, s = { - 2, HALF_ROOM + 2, 2, HALF_ROOM + 2, 0, HALF_ROOM + 6 }, e = { HALF_ROOM + 2, -2, HALF_ROOM + 2, 2, HALF_ROOM + 6, 0 }, w = { - HALF_ROOM - 2, -2, - HALF_ROOM - 2, 2, - HALF_ROOM - 6, 0 }, ne = { HALF_ROOM + 3, - HALF_ROOM, HALF_ROOM + 3, - HALF_ROOM - 3, HALF_ROOM, - HALF_ROOM - 3 }, se = { HALF_ROOM + 3, HALF_ROOM, HALF_ROOM + 3, HALF_ROOM + 3, HALF_ROOM, HALF_ROOM + 3 }, nw = { - HALF_ROOM - 3, - HALF_ROOM, - HALF_ROOM - 3, - HALF_ROOM - 3, - HALF_ROOM, - HALF_ROOM - 3 }, sw = { - HALF_ROOM - 3, HALF_ROOM, - HALF_ROOM - 3, HALF_ROOM + 3, - HALF_ROOM, HALF_ROOM + 3}, } -- end of arrows end -- build_room_info local 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", }, UNKNOWN_ROOM_COLOUR = { name = "Unknown room", colour = ColourNameToRGB "#00CACA", }, MAPPER_NOTE_COLOUR = { name = "Messages", 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 }, } local expand_direction = { 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 expand_direction local function get_room (uid) local room = supplied_get_room (uid) room = room or { unknown = true } -- defaults in case they didn't supply them ... room.name = room.name or string.format ("Room %s", uid or "") room.name = mw.strip_colours (room.name) -- no colour codes for now room.exits = room.exits or {} room.area = room.area or "" room.hovermessage = room.hovermessage or "" room.bordercolour = room.bordercolour or config.ROOM_COLOUR.colour room.borderpen = room.borderpen or 0 -- solid room.borderpenwidth = room.borderpenwidth or 1 room.fillcolour = room.fillcolour or 0x000000 room.fillbrush = room.fillbrush or 1 -- no fill return room end -- get_room local function check_connected () if not IsConnected() then mapprint ("You are not connected to", WorldName()) return false end -- if not connected return true end -- check_connected local function make_number_checker (title, min, max, decimals) return function (s) local n = tonumber (s) if not n then utils.msgbox (title .. " must be a number", "Incorrect input", "ok", "!", 1) return false -- bad input end -- if if n < min or n > max then utils.msgbox (title .. " must be in range " .. min .. " to " .. max, "Incorrect input", "ok", "!", 1) return false -- bad input end -- if if not decimals then if string.match (s, "%.") then utils.msgbox (title .. " cannot have decimal places", "Incorrect input", "ok", "!", 1) return false -- bad input end -- if end -- no decimals return true -- good input end -- generated function end -- make_number_checker local function get_number_from_user (msg, title, current, min, max, decimals) local max_length = math.ceil (math.log10 (max) + 1) -- if decimals allowed, allow room for them if decimals then max_length = max_length + 2 -- allow for 0.x end -- if -- if can be negative, allow for minus sign if min < 0 then max_length = max_length + 1 end -- if can be negative return tonumber (utils.inputbox (msg, title, current, nil, nil, { validate = make_number_checker (title, min, max, decimals), prompt_height = 14, box_height = 130, box_width = 300, reply_width = 150, max_length = max_length, } -- end extra stuff )) end -- get_number_from_user local function draw_configuration () local width = max_text_width (win, FONT_ID, {"Configuration", "Font", "Width", "Height", "Depth"}, true) local lines = 6 -- "Configuration", font, width, height, depth, delay local GAP = 5 local suppress_colours = false for k, v in pairs (config) do if v.colour then width = math.max (width, WindowTextWidth (win, FONT_ID, v.name, true)) lines = lines + 1 end -- a colour item end -- for each config item if (config.WINDOW.height - 13 - font_height * lines) < 10 then suppress_colours = true lines = 6 -- forget all the colours end -- if local x = 3 local y = config.WINDOW.height - 13 - font_height * lines local box_size = font_height - 2 local rh_size = math.max (box_size, max_text_width (win, FONT_ID, {config.FONT.name .. " " .. config.FONT.size, tostring (config.WINDOW.width), tostring (config.WINDOW.height), tostring (config.SCAN.depth)}, true)) local frame_width = GAP + width + GAP + rh_size + GAP -- gap / text / gap / box / gap -- fill entire box with grey WindowRectOp (win, miniwin.rect_fill, x, y, x + frame_width, y + font_height * lines + 10, 0xDCDCDC) -- frame it draw_3d_box (win, x, y, frame_width, font_height * lines + 10) y = y + GAP x = x + GAP -- title WindowText (win, FONT_ID, "Configuration", x, y, 0, 0, 0x808080, true) -- close box WindowRectOp (win, miniwin.rect_frame, x + frame_width - box_size - GAP * 2, y + 1, x + frame_width - GAP * 2, y + 1 + box_size, 0x808080) WindowLine (win, x + frame_width - box_size - GAP * 2 + 3, y + 4, x + frame_width - GAP * 2 - 3, y - 2 + box_size, 0x808080, miniwin.pen_solid, 1) WindowLine (win, x - 4 + frame_width - GAP * 2, y + 4, x - 1 + frame_width - box_size - GAP * 2 + 3, y - 2 + box_size, 0x808080, miniwin.pen_solid, 1) -- close configuration hotspot WindowAddHotspot(win, "$", x + frame_width - box_size - GAP * 2, y + 1, x + frame_width - GAP * 2, y + 1 + box_size, -- rectangle "", "", "", "", "mapper.mouseup_close_configure", -- mouseup "Click to close", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height if not suppress_colours then for k, v in pairsByKeys (config) do if v.colour then WindowText (win, FONT_ID, v.name, x, y, 0, 0, 0x000000, true) WindowRectOp (win, miniwin.rect_fill, x + width + rh_size / 2, y + 1, x + width + rh_size / 2 + box_size, y + 1 + box_size, v.colour) WindowRectOp (win, miniwin.rect_frame, x + width + rh_size / 2, y + 1, x + width + rh_size / 2 + box_size, y + 1 + box_size, 0x000000) -- colour change hotspot WindowAddHotspot(win, "$colour:" .. k, x + GAP, y + 1, x + width + rh_size / 2 + box_size, y + 1 + box_size, -- rectangle "", "", "", "", "mapper.mouseup_change_colour", -- mouseup "Click to change colour", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height end -- a colour item end -- for each config item end -- if -- depth WindowText (win, FONT_ID, "Depth", x, y, 0, 0, 0x000000, true) WindowText (win, FONT_ID_UL, tostring (config.SCAN.depth), x + width + GAP, y, 0, 0, 0x808080, true) -- depth hotspot WindowAddHotspot(win, "$", x + GAP, y, x + frame_width, y + font_height, -- rectangle "", "", "", "", "mapper.mouseup_change_depth", -- mouseup "Click to change scan depth", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height -- font WindowText (win, FONT_ID, "Font", x, y, 0, 0, 0x000000, true) WindowText (win, FONT_ID_UL, config.FONT.name .. " " .. config.FONT.size, x + width + GAP, y, 0, 0, 0x808080, true) -- colour font hotspot WindowAddHotspot(win, "$", x + GAP, y, x + frame_width, y + font_height, -- rectangle "", "", "", "", "mapper.mouseup_change_font", -- mouseup "Click to change font", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height -- width WindowText (win, FONT_ID, "Width", x, y, 0, 0, 0x000000, true) WindowText (win, FONT_ID_UL, tostring (config.WINDOW.width), x + width + GAP, y, 0, 0, 0x808080, true) -- width hotspot WindowAddHotspot(win, "$", x + GAP, y, x + frame_width, y + font_height, -- rectangle "", "", "", "", "mapper.mouseup_change_width", -- mouseup "Click to change window width", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height -- height WindowText (win, FONT_ID, "Height", x, y, 0, 0, 0x000000, true) WindowText (win, FONT_ID_UL, tostring (config.WINDOW.height), x + width + GAP, y, 0, 0, 0x808080, true) -- height hotspot WindowAddHotspot(win, "$", x + GAP, y, x + frame_width, y + font_height, -- rectangle "", "", "", "", "mapper.mouseup_change_height", -- mouseup "Click to change window height", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height -- delay WindowText (win, FONT_ID, "Walk delay", x, y, 0, 0, 0x000000, true) WindowText (win, FONT_ID_UL, tostring (config.DELAY.time), x + width + GAP, y, 0, 0, 0x808080, true) -- height hotspot WindowAddHotspot(win, "$", x + GAP, y, x + frame_width, y + font_height, -- rectangle "", "", "", "", "mapper.mouseup_change_delay", -- mouseup "Click to change speedwalk delay", miniwin.cursor_hand, 0) -- hand cursor y = y + font_height end -- draw_configuration -- for calculating one-way paths 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 local function add_another_room (uid, path, x, y) local path = path or {} return {uid=uid, path=path, x = x, y = y} end -- add_another_room local function draw_room (uid, path, x, y) local coords = string.format ("%i,%i", math.floor (x), math.floor (y)) -- need this for the *current* room !!! drawn_coords [coords] = uid -- print ("drawing", uid, "at", coords) if drawn [uid] then return end -- done this one -- don't draw the same room more than once drawn [uid] = { coords = coords, path = path } local room = rooms [uid] -- not cached - get from caller if not room then room = get_room (uid) rooms [uid] = room end -- not in cache local left, top, right, bottom = x - HALF_ROOM, y - HALF_ROOM, x + HALF_ROOM, y + HALF_ROOM -- forget it if off screen if x < HALF_ROOM or y < HALF_ROOM or x > config.WINDOW.width - HALF_ROOM or y > config.WINDOW.height - HALF_ROOM then return end -- if -- exits local texits = {} for dir, exit_uid in pairs (room.exits) do table.insert (texits, dir) local exit_info = connectors [dir] local stub_exit_info = half_connectors [dir] local exit_line_colour = config.EXIT_COLOUR.colour local arrow = arrows [dir] -- draw up in the ne/nw position if not already an exit there at this level if dir == "u" then if not room.exits.nw then exit_info = connectors.nw stub_exit_info = half_connectors.nw arrow = arrows.nw exit_line_colour = config.EXIT_COLOUR_UP_DOWN.colour end -- if available elseif dir == "in" then if not room.exits.ne then exit_info = connectors.ne stub_exit_info = half_connectors.ne arrow = arrows.ne exit_line_colour = config.EXIT_COLOUR_IN_OUT.colour end -- if elseif dir == "d" then if not room.exits.se then exit_info = connectors.se stub_exit_info = half_connectors.se arrow = arrows.se exit_line_colour = config.EXIT_COLOUR_UP_DOWN.colour end -- if available elseif dir == "out" then if not room.exits.sw then exit_info = connectors.sw stub_exit_info = half_connectors.sw arrow = arrows.sw exit_line_colour = config.EXIT_COLOUR_IN_OUT.colour end -- if end -- if down if exit_info then local linetype = miniwin.pen_solid -- unbroken local linewidth = 1 -- not recent -- try to cache room if not rooms [exit_uid] then rooms [exit_uid] = get_room (exit_uid) end -- if if rooms [exit_uid].unknown then linetype = miniwin.pen_dot -- dots end -- if local next_x = x + exit_info.at [1] * (ROOM_SIZE + DISTANCE_TO_NEXT_ROOM) local next_y = y + exit_info.at [2] * (ROOM_SIZE + DISTANCE_TO_NEXT_ROOM) local next_coords = string.format ("%i,%i", math.floor (next_x), math.floor (next_y)) -- remember if a zone exit (first one only) if show_area_exits and room.area ~= rooms [exit_uid].area then area_exits [ rooms [exit_uid].area ] = area_exits [ rooms [exit_uid].area ] or {x = x, y = y} end -- if -- if another room (not where this one leads to) is already there, only draw "stub" lines if drawn_coords [next_coords] and drawn_coords [next_coords] ~= exit_uid then exit_info = stub_exit_info elseif exit_uid == uid then -- here if room leads back to itself exit_info = stub_exit_info linetype = miniwin.pen_dash -- dash else if (not show_other_areas and rooms [exit_uid].area ~= current_area) or (not show_up_down and (dir == "u" or dir == "d")) then exit_info = stub_exit_info -- don't show other areas else -- if we are scheduled to draw the room already, only draw a stub this time if plan_to_draw [exit_uid] and plan_to_draw [exit_uid] ~= next_coords then -- here if room already going to be drawn exit_info = stub_exit_info linetype = miniwin.pen_dash -- dash else -- remember to draw room next iteration local new_path = copytable.deep (path) table.insert (new_path, { dir = dir, uid = exit_uid }) table.insert (rooms_to_be_drawn, add_another_room (exit_uid, new_path, next_x, next_y)) drawn_coords [next_coords] = exit_uid plan_to_draw [exit_uid] = next_coords -- if exit room known if not rooms [exit_uid].unknown then local exit_time = last_visited [exit_uid] or 0 local this_time = last_visited [uid] or 0 local now = os.time () if exit_time > (now - config.LAST_VISIT_TIME.time) and this_time > (now - config.LAST_VISIT_TIME.time) then linewidth = 2 end -- if end -- if end -- if end -- if end -- if drawn on this spot WindowLine (win, x + exit_info.x1, y + exit_info.y1, x + exit_info.x2, y + exit_info.y2, exit_line_colour, linetype, linewidth) -- one-way exit? if not rooms [exit_uid].unknown then local dest = rooms [exit_uid] -- if inverse direction doesn't point back to us, this is one-way if dest.exits [inverse_direction [dir]] ~= uid then -- turn points into string, relative to where the room is local points = string.format ("%i,%i,%i,%i,%i,%i", x + arrow [1], y + arrow [2], x + arrow [3], y + arrow [4], x + arrow [5], y + arrow [6]) -- draw arrow WindowPolygon(win, points, exit_line_colour, miniwin.pen_solid, 1, exit_line_colour, miniwin.brush_solid, true, true) end -- one way end -- if we know of the room where it does end -- if we know what to do with this direction end -- for each exit if room.unknown then WindowCircleOp (win, miniwin.circle_rectangle, left, top, right, bottom, config.UNKNOWN_ROOM_COLOUR.colour, miniwin.pen_dot, 1, -- dotted single pixel pen -1, miniwin.brush_null) -- opaque, no brush else WindowCircleOp (win, miniwin.circle_rectangle, left, top, right, bottom, 0, miniwin.pen_null, 0, -- no pen room.fillcolour, room.fillbrush) -- brush WindowCircleOp (win, miniwin.circle_rectangle, left, top, right, bottom, room.bordercolour, room.borderpen, room.borderpenwidth, -- pen -1, miniwin.brush_null) -- opaque, no brush end -- if -- show up and down in case we can't get a line in if room.exits.u then -- line at top WindowLine (win, left, top, left + ROOM_SIZE, top, config.EXIT_COLOUR_UP_DOWN.colour, miniwin.pen_solid, 1) end -- if if room.exits.d then -- line at bottom WindowLine (win, left, bottom, left + ROOM_SIZE, bottom, config.EXIT_COLOUR_UP_DOWN.colour, miniwin.pen_solid, 1) end -- if if room.exits ['in'] then -- line at right WindowLine (win, left + ROOM_SIZE, top, left + ROOM_SIZE, bottom, config.EXIT_COLOUR_IN_OUT.colour, miniwin.pen_solid, 1) end -- if if room.exits.out then -- line at left WindowLine (win, left, top, left, bottom, config.EXIT_COLOUR_IN_OUT.colour, miniwin.pen_solid , 1) end -- if speedwalks [uid] = path -- so we know how to get here WindowAddHotspot(win, uid, left, top, right, bottom, -- rectangle "mapper.mouseover_room", -- mouseover "mapper.cancelmouseover_room", -- cancelmouseover "", -- mousedown "", -- cancelmousedown "mapper.mouseup_room", -- mouseup room.hovermessage, miniwin.cursor_hand, 0) -- hand cursor WindowScrollwheelHandler (win, uid, "mapper.zoom_map") end -- draw_room local function changed_room (uid) hyperlink_paths = nil -- those hyperlinks are meaningless now speedwalks = {} -- old speedwalks are irrelevant if current_speedwalk then if uid ~= expected_room then local exp = rooms [expected_room] if not exp then exp = get_room (expected_room) or { name = expected_room } end -- if local here = rooms [uid] if not here then here = get_room (uid) or { name = uid } end -- if exp = expected_room here = uid maperror (string.format ("Speedwalk failed! Expected to be in '%s' but ended up in '%s'.", exp or "", here)) cancel_speedwalk () else if #current_speedwalk > 0 then local dir = table.remove (current_speedwalk, 1) SetStatus ("Walking " .. (expand_direction [dir.dir] or dir.dir) .. " to " .. walk_to_room_name .. ". Speedwalks to go: " .. #current_speedwalk + 1) expected_room = dir.uid if config.DELAY.time > 0 then if GetOption ("enable_timers") ~= 1 then maperror ("WARNING! Timers not enabled. Speedwalking will not work properly.") end -- if timers disabled DoAfter (config.DELAY.time, dir.dir) else Send (dir.dir) end -- if else last_hyperlink_uid = nil last_speedwalk_uid = nil if show_completed then mapprint ("Speedwalk completed.") end -- if wanted cancel_speedwalk () end -- if any left end -- if expected room or not end -- if have a current speedwalk end -- changed_room local function draw_zone_exit (exit) local x, y = exit.x, exit.y local offset = ROOM_SIZE -- draw circle around us WindowCircleOp (win, miniwin.circle_ellipse, x - offset, y - offset, x + offset, y + offset, ColourNameToRGB "cornflowerblue", -- pen colour miniwin.pen_solid, -- solid pen 3, -- pen width 0, -- brush colour miniwin.brush_null) -- null brush WindowCircleOp (win, miniwin.circle_ellipse, x - offset, y - offset, x + offset, y + offset, ColourNameToRGB "cyan", -- pen colour miniwin.pen_solid, -- solid pen 1, -- pen width 0, -- brush colour miniwin.brush_null) -- null brush end -- draw_zone_exit ---------------------------------------------------------------------------------- -- EXPOSED FUNCTIONS ---------------------------------------------------------------------------------- -- can we find another room right now? function check_we_can_find () if not check_connected () then return end -- if if not current_room then mapprint ("I don't know where you are right now - try: LOOK") return false end -- if if current_speedwalk then mapprint ("No point doing this while you are speedwalking.") return false end -- if return true end -- check_we_can_find -- see: http://www.gammon.com.au/forum/?id=7306&page=2 -- Thanks to Ked. -- uid is starting room -- f returns true (or a "reason" string) if we want to store this one, and true,true if -- we have done searching (ie. all wanted rooms found) function find_paths (uid, f) local function make_particle (curr_loc, prev_path) local prev_path = prev_path or {} return {current_room=curr_loc, path=prev_path} end local depth = 0 local count = 0 local done = false local found, reason local explored_rooms, particles = {}, {} -- this is where we collect found paths -- the table is keyed by destination, with paths as values local paths = {} -- create particle for the initial room table.insert (particles, make_particle (uid)) while (not done) and #particles > 0 and depth < config.SCAN.depth do -- create a new generation of particles new_generation = {} depth = depth + 1 SetStatus (string.format ("Scanning: %i/%i depth (%i rooms)", depth, config.SCAN.depth, count)) -- process each active particle for i, part in ipairs (particles) do count = count + 1 if not rooms [part.current_room] then rooms [part.current_room] = get_room (part.current_room) end -- if not in memory yet -- if room doesn't exist, forget it if rooms [part.current_room] then -- get a list of exits from the current room exits = rooms [part.current_room].exits -- create one new particle for each exit for dir, dest in pairs(exits) do -- if we've been in this room before, drop it if not explored_rooms[dest] then explored_rooms[dest] = true rooms [dest] = supplied_get_room (dest) -- make sure this room in table if rooms [dest] then new_path = copytable.deep (part.path) table.insert(new_path, { dir = dir, uid = dest } ) -- if this room is in the list of destinations then save its path found, done = f (dest) if found then paths[dest] = { path = new_path, reason = found } end -- found one! -- make a new particle in the new room table.insert(new_generation, make_particle(dest, new_path)) end -- if room exists end -- not explored this room if done then break end end -- for each exit end -- if room exists if done then break end end -- for each particle particles = new_generation end -- while more particles SetStatus "Ready" return paths, count, depth end -- function find_paths -- draw our map starting at room: uid function draw (uid) if not uid then maperror "Cannot draw map right now, I don't know where you are - try: LOOK" return end -- if if current_room and current_room ~= uid then changed_room (uid) end -- if current_room = uid -- remember where we are -- timing local start_time = utils.timer () -- start with initial room rooms = { [uid] = get_room (uid) } -- lookup current room local room = rooms [uid] room = room or { name = "", area = "" } last_visited [uid] = os.time () current_area = room.area -- we are recreating the window so any mouse-over is not valid any more if WindowInfo (win, 19) and WindowInfo (win, 19) ~= "" then if type (room_cancelmouseover) == "function" then room_cancelmouseover (WindowInfo (win, 19), 0) -- cancelled mouse over end -- if end -- have a hotspot WindowDeleteAllHotspots (win) WindowCreate (win, windowinfo.window_left, windowinfo.window_top, config.WINDOW.width, config.WINDOW.height, windowinfo.window_mode, -- top right windowinfo.window_flags, config.BACKGROUND_COLOUR.colour) -- let them move it around movewindow.add_drag_handler (win, 0, 0, 0, font_height + 4) -- for zooming WindowAddHotspot(win, "zzz_zoom", 0, 0, 0, 0, "", "", "", "", "", "", -- hint miniwin.cursor_arrow, 0) WindowScrollwheelHandler (win, "zzz_zoom", "mapper.zoom_map") -- set up for initial room, in middle drawn, drawn_coords, rooms_to_be_drawn, speedwalks, plan_to_draw, area_exits = {}, {}, {}, {}, {}, {} depth = 0 -- insert initial room table.insert (rooms_to_be_drawn, add_another_room (uid, {}, config.WINDOW.width / 2, config.WINDOW.height / 2)) while #rooms_to_be_drawn > 0 and depth < config.SCAN.depth do local old_generation = rooms_to_be_drawn rooms_to_be_drawn = {} -- new generation for i, part in ipairs (old_generation) do draw_room (part.uid, part.path, part.x, part.y) end -- for each existing room depth = depth + 1 end -- while all rooms_to_be_drawn for area, zone_exit in pairs (area_exits) do draw_zone_exit (zone_exit) end -- for local room_name = room.name local name_width = WindowTextWidth (win, FONT_ID, room_name, true) local add_dots = false -- truncate name if too long while name_width > (config.WINDOW.width - 10) do -- get rid of last word local s = string.match (" " .. room_name .. "...", "(%s%S*)$") if not s or #s == 0 then break end room_name = room_name:sub (1, - (#s - 2)) -- except the last 3 dots but add the space name_width = WindowTextWidth (win, FONT_ID, room_name .. " ...", true) add_dots = true end -- while if add_dots then room_name = room_name .. " ..." end -- if -- room name draw_text_box (win, FONT_ID, (config.WINDOW.width - WindowTextWidth (win, FONT_ID, room_name, true)) / 2, -- left 3, -- top room_name, true, -- what to draw, utf8 config.ROOM_NAME_TEXT.colour, -- text colour config.ROOM_NAME_FILL.colour, -- fill colour config.ROOM_NAME_BORDER.colour) -- border colour -- area name local areaname = room.area if areaname then draw_text_box (win, FONT_ID, (config.WINDOW.width - WindowTextWidth (win, FONT_ID, areaname, true)) / 2, -- left config.WINDOW.height - 6 - font_height, -- top areaname, true, -- what to draw, utf8 config.AREA_NAME_TEXT.colour, -- text colour config.AREA_NAME_FILL.colour, -- fill colour config.AREA_NAME_BORDER.colour) -- border colour end -- if area known -- configure? if draw_configure_box then draw_configuration () else local x = 5 local y = config.WINDOW.height - 6 - font_height local width = draw_text_box (win, FONT_ID, x, -- left y, -- top (ie. at bottom) "*", true, -- what to draw, utf8 config.AREA_NAME_TEXT.colour, -- text colour config.AREA_NAME_FILL.colour, -- fill colour config.AREA_NAME_BORDER.colour) -- border colour WindowAddHotspot(win, "", x, y, x + width, y + font_height, -- rectangle "", -- mouseover "", -- cancelmouseover "", -- mousedown "", -- cancelmousedown "mapper.mouseup_configure", -- mouseup "Click to configure map", miniwin.cursor_hand, 0) -- hand cursor end -- if if type (show_help) == "function" then local x = config.WINDOW.width - WindowTextWidth (win, FONT_ID, "?", true) - 5 local y = config.WINDOW.height - 6 - font_height local width = draw_text_box (win, FONT_ID, x, -- left y, -- top (ie. at bottom) "?", true, -- what to draw, utf8 config.AREA_NAME_TEXT.colour, -- text colour config.AREA_NAME_FILL.colour, -- fill colour config.AREA_NAME_BORDER.colour) -- border colour WindowAddHotspot(win, "", x, y, x + width, y + font_height, -- rectangle "", -- mouseover "", -- cancelmouseover "", -- mousedown "", -- cancelmousedown "mapper.show_help", -- mouseup "Click for help", miniwin.cursor_hand, 0) -- hand cursor end -- if -- 3D box around whole thing draw_3d_box (win, 0, 0, config.WINDOW.width, config.WINDOW.height) -- make sure window visible WindowShow (win, not hidden) last_drawn = uid -- last room number we drew (for zooming) local end_time = utils.timer () -- timing stuff if timing then local count= 0 for k in pairs (drawn) do count = count + 1 end print (string.format ("Time to draw %i rooms = %0.3f seconds, search depth = %i", count, end_time - start_time, depth)) total_times_drawn = total_times_drawn + 1 total_time_taken = total_time_taken + end_time - start_time print (string.format ("Total times map drawn = %i, average time to draw = %0.3f seconds", total_times_drawn, total_time_taken / total_times_drawn)) end -- if end -- draw local credits = { "MUSHclient mapper", string.format ("Version %0.1f", VERSION), "Written by Nick Gammon", WorldName (), GetInfo (3), } -- call once to initialize the mapper function init (t) -- make copy of colours, sizes etc. config = t.config assert (type (config) == "table", "No 'config' table supplied to mapper.") supplied_get_room = t.get_room assert (type (supplied_get_room) == "function", "No 'get_room' function supplied to mapper.") show_help = t.show_help -- "help" function room_click = t.room_click -- RH mouse-click function room_mouseover = t.room_mouseover -- mouse-over function room_cancelmouseover = t.room_cancelmouseover -- cancel mouse-over function timing = t.timing -- true for timing info show_completed = t.show_completed -- true to show "Speedwalk completed." message show_other_areas = t.show_other_areas -- true to show other areas show_up_down = t.show_up_down -- true to show up or down show_area_exits = t.show_area_exits -- true to show area exits speedwalk_prefix = t.speedwalk_prefix -- how to speedwalk (prefix) -- force some config defaults if not supplied for k, v in pairs (default_config) do config [k] = config [k] or v end -- for win = GetPluginID () .. "_mapper" WindowCreate (win, 0, 0, 0, 0, 0, 0, 0) -- add the fonts WindowFont (win, FONT_ID, config.FONT.name, config.FONT.size) WindowFont (win, FONT_ID_UL, config.FONT.name, config.FONT.size, false, false, true) -- see how high it is font_height = WindowFontInfo (win, FONT_ID, 1) -- height -- find where window was last time windowinfo = movewindow.install (win, miniwin.pos_center_right) -- calculate box sizes, arrows, connecting lines etc. build_room_info () WindowCreate (win, windowinfo.window_left, windowinfo.window_top, config.WINDOW.width, config.WINDOW.height, windowinfo.window_mode, -- top right windowinfo.window_flags, config.BACKGROUND_COLOUR.colour) -- let them move it around movewindow.add_drag_handler (win, 0, 0, 0, font_height + 4) local top = (config.WINDOW.height - #credits * font_height) /2 for _, v in ipairs (credits) do local width = WindowTextWidth (win, FONT_ID, v, true) local left = (config.WINDOW.width - width) / 2 WindowText (win, FONT_ID, v, left, top, 0, 0, config.ROOM_COLOUR.colour, true) top = top + font_height end -- for draw_3d_box (win, 0, 0, config.WINDOW.width, config.WINDOW.height) WindowShow (win, true) end -- init function zoom_in () if last_drawn and ROOM_SIZE < 40 then ROOM_SIZE = ROOM_SIZE + 2 DISTANCE_TO_NEXT_ROOM = DISTANCE_TO_NEXT_ROOM + 2 build_room_info () draw (last_drawn) end -- if end -- zoom_in function zoom_out () if last_drawn and ROOM_SIZE > 4 then ROOM_SIZE = ROOM_SIZE - 2 DISTANCE_TO_NEXT_ROOM = DISTANCE_TO_NEXT_ROOM - 2 build_room_info () draw (last_drawn) end -- if end -- zoom_out function mapprint (...) local old_note_colour = GetNoteColourFore () SetNoteColourFore(config.MAPPER_NOTE_COLOUR.colour) print (...) SetNoteColourFore (old_note_colour) end -- mapprint function maperror (...) local old_note_colour = GetNoteColourFore () SetNoteColourFore(ColourNameToRGB "red") print (...) SetNoteColourFore (old_note_colour) end -- maperror function show () WindowShow (win, true) hidden = false end -- show function hide () WindowShow (win, false) hidden = true end -- hide function save_state () movewindow.save_state (win) end -- save_state -- generic room finder -- f (uid) is a function which returns: found, done -- found is not nil if uid is a wanted room - if it is a string it is the reason it matched (eg. shop) -- done is true if we know there is nothing else to search for (eg. all rooms found) -- show_uid is true if you want the room uid to be displayed -- expected_count is the number we expect to find (eg. the number found on a database) -- if 'walk' is true, we walk to the first match rather than displaying hyperlinks -- if fcb is a function, it is called back after displaying each line function find (f, show_uid, expected_count, walk, fcb) if not check_we_can_find () then return end -- if if fcb then assert (type (fcb) == "function") end -- if local start_time = utils.timer () local paths, count, depth = find_paths (current_room, f) local end_time = utils.timer () local t = {} local found_count = 0 for k in pairs (paths) do table.insert (t, k) found_count = found_count + 1 end -- for -- timing stuff if timing then print (string.format ("Time to search %i rooms = %0.3f seconds, search depth = %i", count, end_time - start_time, depth)) end -- if if found_count == 0 then mapprint ("No matches.") return end -- if if found_count == 1 and walk then uid, item = next (paths, nil) mapprint ("Walking to:", rooms [uid].name) start_speedwalk (item.path) return end -- if walking wanted -- sort so closest ones are first table.sort (t, function (a, b) return #paths [a].path < #paths [b].path end ) hyperlink_paths = {} for _, uid in ipairs (t) do local room = rooms [uid] -- ought to exist or wouldn't be in table assert (room, "Room " .. uid .. " is not in rooms table.") if current_room == uid then mapprint (room.name, "is the room you are in") else local distance = #paths [uid].path .. " room" if #paths [uid].path > 1 then distance = distance .. "s" end -- if distance = distance .. " away" local room_name = room.name if show_uid then room_name = room_name .. " (" .. uid .. ")" end -- if -- in case the same UID shows up later, it is only valid from the same room local hash = utils.tohex (utils.md5 (tostring (current_room) .. "<-->" .. tostring (uid))) Hyperlink ("!!" .. GetPluginID () .. ":mapper.do_hyperlink(" .. hash .. ")", room_name, "Click to speedwalk there (" .. distance .. ")", "", "", false) local info = "" if type (paths [uid].reason) == "string" and paths [uid].reason ~= "" then info = " [" .. paths [uid].reason .. "]" end -- if mapprint (" - " .. distance .. info) -- new line -- callback to display extra stuff (like find context, room description) if fcb then fcb (uid) end -- if callback hyperlink_paths [hash] = paths [uid].path end -- if end -- for each room if expected_count and found_count < expected_count then local diff = expected_count - found_count local were, matches = "were", "matches" if diff == 1 then were, matches = "was", "match" end -- if mapprint ("There", were, diff, matches, "which I could not find a path to within", config.SCAN.depth, "rooms.") end -- if end -- map_find_things -- executed when the mapper draws a hyperlink to a room function do_hyperlink (hash) if not check_connected () then return end -- if if not hyperlink_paths or not hyperlink_paths [hash] then mapprint ("Hyperlink is no longer valid, as you have moved.") return end -- if local path = hyperlink_paths [hash] if #path > 0 then last_hyperlink_uid = path [#path].uid end -- if start_speedwalk (path) end -- do_hyperlink -- build a speedwalk from a path into a string function build_speedwalk (path) -- build speedwalk string (collect identical directions) local tspeed = {} for _, dir in ipairs (path) do local n = #tspeed if n == 0 then table.insert (tspeed, { dir = dir.dir, count = 1 }) else if tspeed [n].dir == dir.dir then tspeed [n].count = tspeed [n].count + 1 else table.insert (tspeed, { dir = dir.dir, count = 1 }) end -- if different direction end -- if end -- for if #tspeed == 0 then return end -- nowhere to go (current room?) -- now build string like: 2n3e4(sw) local s = "" for _, dir in ipairs (tspeed) do if dir.count > 1 then s = s .. dir.count end -- if if #dir.dir == 1 then s = s .. dir.dir else s = s .. "(" .. dir.dir .. ")" end -- if s = s .. " " end -- if return s end -- build_speedwalk -- start a speedwalk to a path function start_speedwalk (path) if not check_connected () then return end -- if if current_speedwalk and #current_speedwalk > 0 then mapprint ("You are already speedwalking! (Ctrl + LH-click on any room to cancel)") return end -- if current_speedwalk = path if current_speedwalk then if #current_speedwalk > 0 then last_speedwalk_uid = current_speedwalk [#current_speedwalk].uid -- fast speedwalk: just send # 4s 3e etc. if type (speedwalk_prefix) == "string" and speedwalk_prefix ~= "" then local s = speedwalk_prefix .. " " .. build_speedwalk (path) Execute (s) current_speedwalk = nil return end -- if local dir = table.remove (current_speedwalk, 1) local room = get_room (dir.uid) walk_to_room_name = room.name SetStatus ("Walking " .. (expand_direction [dir.dir] or dir.dir) .. " to " .. walk_to_room_name .. ". Speedwalks to go: " .. #current_speedwalk + 1) Send (dir.dir) expected_room = dir.uid else cancel_speedwalk () end -- if any left end -- if end -- start_speedwalk -- cancel the current speedwalk function cancel_speedwalk () if current_speedwalk and #current_speedwalk > 0 then mapprint "Speedwalk cancelled." end -- if current_speedwalk = nil expected_room = nil hyperlink_paths = nil SetStatus ("Ready") end -- cancel_speedwalk -- ------------------------------------------------------------------ -- mouse-up handlers (need to be exposed) -- these are for clicking on the map, or the configuration box -- ------------------------------------------------------------------ function mouseup_room (flags, hotspot_id) local uid = hotspot_id if bit.band (flags, miniwin.hotspot_got_rh_mouse) ~= 0 then -- RH click if type (room_click) == "function" then room_click (uid, flags) end -- if return end -- if RH click -- here for LH click -- Control key down? if bit.band (flags, miniwin.hotspot_got_control) ~= 0 then cancel_speedwalk () return end -- if ctrl-LH click start_speedwalk (speedwalks [uid]) end -- mouseup_room -- ------------------------------------------------------------------ -- mouse-over handlers (need to be exposed) -- these are for mousing over a room -- ------------------------------------------------------------------ function mouseover_room (flags, hotspot_id) if type (room_mouseover) == "function" then room_mouseover (hotspot_id, flags) -- moused over end -- if end -- mouseover_room function cancelmouseover_room (flags, hotspot_id) if type (room_cancelmouseover) == "function" then room_cancelmouseover (hotspot_id, flags) -- cancled mouse over end -- if end -- cancelmouseover_room function mouseup_configure (flags, hotspot_id) draw_configure_box = true draw (current_room) end -- mouseup_configure function mouseup_close_configure (flags, hotspot_id) draw_configure_box = false draw (current_room) end -- mouseup_player function mouseup_change_colour (flags, hotspot_id) local which = string.match (hotspot_id, "^$colour:([%a%d_]+)$") if not which then return -- strange ... end -- not found local newcolour = PickColour (config [which].colour) if newcolour == -1 then return end -- if dismissed config [which].colour = newcolour draw (current_room) end -- mouseup_change_colour function mouseup_change_font (flags, hotspot_id) local newfont = utils.fontpicker (config.FONT.name, config.FONT.size, config.ROOM_NAME_TEXT.colour) if not newfont then return end -- if dismissed config.FONT.name = newfont.name if newfont.size > 12 then utils.msgbox ("Maximum allowed font size is 12 points.", "Font too large", "ok", "!", 1) else config.FONT.size = newfont.size end -- if config.ROOM_NAME_TEXT.colour = newfont.colour -- reload new font WindowFont (win, FONT_ID, config.FONT.name, config.FONT.size) WindowFont (win, FONT_ID_UL, config.FONT.name, config.FONT.size, false, false, true) -- see how high it is font_height = WindowFontInfo (win, FONT_ID, 1) -- height draw (current_room) end -- mouseup_change_font function mouseup_change_width (flags, hotspot_id) local width = get_number_from_user ("Choose window width (200 to 1000 pixels)", "Width", config.WINDOW.width, 200, 1000) if not width then return end -- if dismissed config.WINDOW.width = width draw (current_room) end -- mouseup_change_width function mouseup_change_height (flags, hotspot_id) local height = get_number_from_user ("Choose window height (200 to 1000 pixels)", "Width", config.WINDOW.height, 200, 1000) if not height then return end -- if dismissed config.WINDOW.height = height draw (current_room) end -- mouseup_change_height function mouseup_change_depth (flags, hotspot_id) local depth = get_number_from_user ("Choose scan depth (3 to 100 rooms)", "Depth", config.SCAN.depth, 3, 100) if not depth then return end -- if dismissed config.SCAN.depth = depth draw (current_room) end -- mouseup_change_depth function mouseup_change_delay (flags, hotspot_id) local delay = get_number_from_user ("Choose speedwalk delay time (0 to 10 seconds)", "Delay in seconds", config.DELAY.time, 0, 10, true) if not delay then return end -- if dismissed config.DELAY.time = delay draw (current_room) end -- mouseup_change_delay function zoom_map (flags, hotspot_id) if bit.band (flags, 0x100) ~= 0 then zoom_out () else zoom_in () end -- if end -- zoom_map