<?xml version="1.0" encoding="UTF-8"?>
<MudletPackage version="1.001">
<ScriptPackage>
<Script isActive="yes" isFolder="no"><name>Cairnholm</name><packageName>cairnholm</packageName><script>-- Cairnholm: the transcript is the game. This package only presents facts.
-- No combat aliases, prose triggers, remote code evaluation or analytics.
if Cairnholm and Cairnholm.shutdown then Cairnholm.shutdown() end
-- The generic mapper competes for Room.Info and doSpeedWalk (Mudlet review,
-- 2026-09-19). This profile uses Cairnholm's deliberately bounded mapper.
local removedGeneric=uninstallPackage("generic_mapper")
Cairnholm = {handlers={}, aliases={}, rooms={}, ids={}, areas={}, walk=nil,
             language="en", enabled=true, serial=0}
local C = Cairnholm
local layoutFile=getMudletHomeDir().."/cairnholm-layout.lua"
C.layout={}
if io.exists(layoutFile) then pcall(table.load,layoutFile,C.layout) end
function C.saveLayout()
  if not C.panel and not C.chatPanel then return end
  local ok,result=pcall(saveWindowLayout)
  if ok and result~=false then pcall(table.save,layoutFile,C.layout) end
end
local function dimensions(chat)
  local w,h=getMainWindowSize()
  w=math.max(320,w); h=math.max(300,h)
  if chat then
    return 10,math.floor(h*0.6),math.min(720,math.floor(w*0.65)),math.min(300,math.floor(h*0.35))
  end
  local width=math.min(420,math.max(280,math.floor(w*0.30)))
  return math.max(0,w-width-10),10,width,math.max(280,math.min(760,h-40))
end
local function window(name,title,chat)
  local x,y,w,h=dimensions(chat)
  local panel=Geyser.UserWindow:new({name=name,titleText=title,
    restoreLayout=C.layout[name]==true,x=x,y=y,width=w,height=h})
  -- Migrate the old floating chat once; later player adjustments are retained.
  if chat and C.layout.chatDockVersion~=1 then
    panel:setDockPosition("bottom")
    C.layout.chatDockVersion=1
  end
  C.layout[name]=true
  return panel
end
local starter = yajl.to_value([==[{"threshold": ["The Threshold", "Memory Palace", 0, 0, {"east": "antechamber"}], "antechamber": ["The Antechamber", "Memory Palace", 1, 0, {"west": "threshold", "east": "first_colonnade", "north": "garden_of_figures"}], "garden_of_figures": ["The Garden of Figures", "Memory Palace", 1, 1, {"south": "antechamber"}], "first_colonnade": ["The First Colonnade", "Memory Palace", 2, 0, {"west": "antechamber", "east": "hall_of_doors"}], "hall_of_doors": ["The Hall of Doors", "Memory Palace", 3, 0, {"west": "first_colonnade", "east": "scriptorium"}], "scriptorium": ["The Scriptorium", "Memory Palace", 4, 0, {"west": "hall_of_doors", "east": "observatory", "north": "sunken_chapel"}], "sunken_chapel": ["The Sunken Chapel", "Memory Palace", 4, 1, {"south": "scriptorium"}], "observatory": ["The Observatory", "Memory Palace", 5, 0, {"west": "scriptorium", "east": "locus_corridor"}], "locus_corridor": ["The Locus Corridor", "Memory Palace", 6, 0, {}], "cairnholm_arrival": ["The Approach", "Grassmarket", 1, 1, {"south": "grassmarket_west"}], "grassmarket_outside": ["Outside The Hanged Crown", "Grassmarket", 0, 0, {"north": "hanged_crown_main", "east": "grassmarket_west"}], "hanged_crown_main": ["The Hanged Crown", "Grassmarket", 0, 1, {"south": "grassmarket_outside"}], "grassmarket_west": ["The Grassmarket (West)", "Grassmarket", 1, 0, {"west": "grassmarket_outside", "east": "grassmarket_central"}], "grassmarket_central": ["The Grassmarket", "Grassmarket", 2, 0, {"west": "grassmarket_west", "east": "grassmarket_east", "north": "candlemaker_row_bottom"}], "grassmarket_east": ["The Grassmarket (East)", "Grassmarket", 3, 0, {"west": "grassmarket_central"}], "candlemaker_row_bottom": ["Candlemaker Row", "Grassmarket", 2, 1, {"south": "grassmarket_central"}]}]==])
local prefix = "[Cairnholm] "
local chatKeys={"ooc","party","faction","newbie","event","tells"}
local chatNames={en={"OOC","Party","Faction","Newbie","Events","Tells"},
  de={"OOC","Gruppe","Fraktion","Neulinge","Ereignisse","Privat"}}
C.chatEnabled=true; C.chatSelected="ooc"; C.chatTabs={}; C.unread={}
local dir = {north="n", south="s", east="e", west="w", northeast="ne",
  northwest="nw", southeast="se", southwest="sw", up="up", down="down",
  ["in"]="in", out="out"}
local vectors = {north={0,1,0}, south={0,-1,0}, east={1,0,0}, west={-1,0,0},
  northeast={1,1,0}, northwest={-1,1,0}, southeast={1,-1,0}, southwest={-1,-1,0},
  up={0,0,1}, down={0,0,-1}, ["in"]={0,0,1}, out={0,0,-1}}
local words = {n="north",s="south",e="east",w="west",ne="northeast",nw="northwest",
  se="southeast",sw="southwest",u="up",d="down",norden="north",sueden="south",
  ["süden"]="south",osten="east",westen="west",oben="up",unten="down"}
local function say(en,de)
  echo("\n" .. (C.language == "de" and de or en) .. "\n")
end
local function html(s)
  return tostring(s):gsub("&amp;","&amp;amp;"):gsub("&lt;","&amp;lt;"):gsub("&gt;","&amp;gt;"):gsub('"',"&amp;quot;")
end
local function listen(event, fn)
  table.insert(C.handlers, registerAnonymousEventHandler(event, fn))
end
local function file()
  return getMudletHomeDir() .. "/cairnholm-" .. C.owner .. ".lua"
end
function C.save()
  C.saveLayout()
  if C.owner then
    local ok=pcall(table.save,file(),{rooms=C.rooms,enabled=C.enabled,chatEnabled=C.chatEnabled})
    if not ok and not C.saveWarning then
      C.saveWarning=true
      say("The local map could not be saved; text play is unaffected.",
        "Die örtliche Karte konnte nicht gespeichert werden; das Textspiel bleibt möglich.")
    end
  end
end
function C.stop()
  C.walk=nil
  if C.timer then killTimer(C.timer); C.timer=nil end
end
local function clearMap()
  C.stop()
  for name,id in pairs(getAreaTable()) do
    if name:sub(1,#prefix) == prefix then deleteArea(id) end
  end
  C.ids, C.areas, C.current, C.last = {}, {}, nil, nil
end
local function room(key, r)
  if not C.ids[key] then
    local area = prefix .. r.area
    if not C.areas[area] then C.areas[area] = addAreaName(area) end
    local id = createRoomID()
    addRoom(id, C.areas[area]); C.ids[key] = id
    setRoomCoordinates(id,r.x,r.y,r.z or 0)
  end
  setRoomName(C.ids[key],r.name)
  return C.ids[key]
end
local function mapAll()
  for key,r in pairs(C.rooms) do room(key,r) end
  for key,r in pairs(C.rooms) do
    for direction,target in pairs(r.exits or {}) do
      if dir[direction] and C.ids[target] then
        setExit(C.ids[key],C.ids[target],dir[direction])
      end
    end
  end
end
function C.ui()
  if not C.enabled then
    if C.panel then C.panel:hide() end
    if C.chatPanel then C.chatPanel:hide() end
    return
  end
  if not C.panel then
    C.panel = window("Cairnholm","Cairnholm",false)
    C.hp = Geyser.Gauge:new({name="CairnholmHP",x="3%",y=5,width="94%",height=24},C.panel)
    C.mp = Geyser.Gauge:new({name="CairnholmMP",x="3%",y=34,width="94%",height=24},C.panel)
    C.hp:setColor(130,60,65); C.mp:setColor(60,85,130)
    C.effectsView=Geyser.MiniConsole:new({name="CairnholmEffects",x="3%",y=64,
      width="94%",height=108,autoWrap=true,scrollBar=true,fontSize=10},C.panel)
    C.effectsView:setBufferSize(128,16)
    C.caption = Geyser.Label:new({name="CairnholmCaption",x="3%",y=178,width="94%",height=48},C.panel)
    C.map = Geyser.Mapper:new({name="CairnholmMap",x=0,y=230,width="100%",height="-6px"},C.panel)
  end
  C.panel:show()
  C.chatUI()
  C.saveLayout()
end
function C.effects()
  if not C.effectsView then return end
  local position=0
  if getScroll then position=getScroll("CairnholmEffects") or 0 end
  C.effectsView:clear()
  if not C.owner or not C.enabled then return end
  local p=gmcp.Cairnholm and gmcp.Cairnholm.Effects
  local de=C.language=="de"
  C.effectsView:echo((de and "Zustände" or "Status effects").."\n")
  if not p or p.owner~=C.owner then return end
  local labels=de and {buff="Stärkung",debuff="Schwächung",cooldown="Wartezeit",condition="Zustand"}
    or {buff="Buff",debuff="Debuff",cooldown="Cooldown",condition="Condition"}
  local signature={C.owner,C.language}
  for i,a in ipairs(p.items or {}) do
    if i&gt;64 then break end
    table.insert(signature,tostring(a.kind)..":"..tostring(a.name))
    C.effectsView:echo((labels[a.kind] or labels.condition)..": "..tostring(a.name):sub(1,480)
      .." - "..tostring(a.detail):sub(1,80).."\n")
  end
  if #(p.items or {})==0 then
    C.effectsView:echo((de and "Keine aktiven Zustände." or "No active effects.").."\n")
  end
  if (p.overflow or 0)&gt;0 then
    C.effectsView:echo(de and "Weitere: AFFECTS eingeben.\n" or "More: type AFFECTS.\n")
  end
  signature=table.concat(signature,"\n")
  -- scrollTo(name, 0) opens Mudlet's second, tail pane even for a short list,
  -- repeating the whole block (owner report, 2026-09-13). clear() closes that
  -- split. Only reopen scrollback when the rendered buffer exceeds the view;
  -- use native counts so wrapped lines and the actual font/height count too.
  -- Countdown-only refreshes preserve position for lists that need scrolling.
  if scrollTo and getLineCount and getRowCount
      and getLineCount("CairnholmEffects")&gt;getRowCount("CairnholmEffects") then
    scrollTo("CairnholmEffects",C.effectSignature==signature and position or 0)
  end
  C.effectSignature=signature
end
function C.chatLabels()
  for i,key in ipairs(chatKeys) do
    local tab=C.chatTabs[key]
    if tab then
      local count=C.unread[key] or 0
      tab.button:echo((C.chatSelected==key and "&gt; " or "")..chatNames[C.language][i]
        ..(count&gt;0 and " ("..count..")" or ""))
    end
  end
end
function C.selectChat(key)
  if not C.chatTabs[key] then return end
  C.chatSelected=key; C.unread[key]=0
  for k,tab in pairs(C.chatTabs) do
    if k==key then tab.console:show() else tab.console:hide() end
  end
  C.chatLabels()
end
function C.clearChat()
  for _,tab in pairs(C.chatTabs) do tab.console:clear() end
  C.unread={}; C.chatSelected="ooc"
  if gmcp.Cairnholm then gmcp.Cairnholm.Chat=nil end
  C.selectChat("ooc")
end
function C.chatUI()
  if not C.owner or not C.enabled or not C.chatEnabled then
    if C.chatPanel then C.chatPanel:hide() end
    return
  end
  if not C.chatPanel then
    C.chatPanel=window("CairnholmChat","Cairnholm Chat",true)
    for i,key in ipairs(chatKeys) do
      local button=Geyser.Label:new({name="CairnholmTab_"..key,
        x=((i-1)%3*33).."%",y=math.floor((i-1)/3)*28,width="33%",height=26},C.chatPanel)
      button:setClickCallback(function() C.selectChat(key) end)
      local console=Geyser.MiniConsole:new({name="CairnholmChat_"..key,
        x=0,y=58,width="100%",height="-4px",autoWrap=true,scrollBar=true,
        fontSize=10},C.chatPanel)
      console:setBufferSize(500,50)
      C.chatTabs[key]={button=button,console=console}
    end
  end
  C.chatPanel:show(); C.selectChat(C.chatSelected)
end
function C.chat()
  local packet=gmcp.Cairnholm.Chat
  gmcp.Cairnholm.Chat=nil -- never replay the last private line on installation
  if not packet or packet.owner~=C.owner or not C.owner or not C.enabled
      or not C.chatEnabled or type(packet.text)~="string" then return end
  if not C.chatTabs[packet.channel] then return end
  local tab=C.chatTabs[packet.channel]
  -- Plain echo, not HTML/cecho/links: player text cannot become executable UI.
  tab.console:echo(packet.text:sub(1,16384).."\n")
  if packet.channel~=C.chatSelected then
    C.unread[packet.channel]=math.min(99,(C.unread[packet.channel] or 0)+1)
  end
  C.chatLabels()
end
function C.chatControl(action)
  action=(action or ""):lower()
  local aliases={gruppe="party",fraktion="faction",neulinge="newbie",
    ereignisse="event",events="event",privat="tells"}
  action=aliases[action] or action
  if action=="off" or action=="aus" then
    C.chatEnabled=false; C.clearChat(); C.chatUI(); C.save()
  elseif action=="on" or action=="an" then
    C.chatEnabled=true; C.chatUI(); C.save()
  elseif action=="clear" or action=="leeren" then C.clearChat()
  elseif C.chatTabs[action] then C.selectChat(action)
  else
    say("chchat ooc/party/faction/newbie/event/tells selects a tab. chchat on/off/clear. Send with the usual ooc, pt, fc, newbie, event, tell or reply commands; selecting a tab does not change where you send. The transcript keeps every line. Tabs keep at most 500 lines each, never saved, and clear on disconnect or character change.",
      "chchat ooc/gruppe/fraktion/neulinge/ereignisse/privat wählt einen Reiter. chchat an/aus/leeren. Sende mit ooc, pt, fc, newbie, event, tell oder reply und ihren deutschen Entsprechungen; der Reiter ändert das Sendeziel nicht. Der Haupttext bleibt vollständig. Reiter halten höchstens 500 Zeilen, werden nie gespeichert und beim Trennen oder Figurenwechsel geleert.")
  end
end
function C.status()
  local s = gmcp.Char.Status
  if not s or not tostring(s.id):match("^[0-9a-f]+$") then return end
  C.language = s.language == "de" and "de" or "en"
  C.combat = s.combat
  if C.combat then C.stop() end
  if C.owner ~= s.id then
    C.effectSignature=nil
    C.save(); clearMap(); C.clearChat(); C.owner=s.id; C.rooms={}; C.enabled=true; C.chatEnabled=true
    local saved={}
    if io.exists(file()) then pcall(table.load,file(),saved) end
    if type(saved.rooms)=="table" then C.rooms=saved.rooms end
    if saved.enabled==false then C.enabled=false end
    if saved.chatEnabled==false then C.chatEnabled=false end
    for key,r in pairs(starter) do
      local exits={}
      local remembered=C.rooms[key]
      if remembered then for d,to in pairs(remembered.exits or {}) do exits[d]=to end end
      for d,to in pairs(r[5]) do exits[d]=to end
      C.rooms[key]={name=r[1],area=r[2],x=r[3],y=r[4],z=0,exits=exits}
    end
    mapAll(); C.ui(); C.effects()
  end
  C.chatLabels(); C.effects()
  -- Reconnect can deliver Room.Info before the character identity. Consume
  -- that pending packet once, not on every later status/combat update.
  if C.pendingLocation then C.location() end
end
function C.vitals()
  if not C.owner or not C.enabled then return end
  if not C.panel then C.ui() end
  local v=gmcp.Char.Vitals
  C.hp:setValue(v.hp,math.max(1,v.maxhp),(C.language=="de" and "LP " or "HP ")..v.hp.."/"..v.maxhp)
  C.mp:setValue(v.mp,math.max(1,v.maxmp),"MP "..v.mp.."/"..v.maxmp)
end
function C.location()
  local r=gmcp.Room and gmcp.Room.Info
  if not r then return end
  if not C.owner then C.pendingLocation=true; return end
  C.pendingLocation=nil
  local key=tostring(r.num or "")
  if key=="" then C.current=nil; C.stop(); return end
  if not C.rooms[key] then
    C.serial=C.serial+1
    local pos={x=C.serial*2,y=-4,z=0}
    if C.last and C.rooms[C.last.from] and vectors[C.last.direction] then
      local old=C.rooms[C.last.from]; local v=vectors[C.last.direction]
      pos={x=old.x+v[1],y=old.y+v[2],z=(old.z or 0)+v[3]}
    end
    C.rooms[key]={name=r.name,area=r.area or "Cairnholm",x=pos.x,y=pos.y,z=pos.z,exits={}}
  end
  C.rooms[key].name=r.name
  if C.last and C.last.from~=key and C.rooms[C.last.from] then
    C.rooms[C.last.from].exits[C.last.direction]=key
    local from=C.ids[C.last.from]
    local to=room(key,C.rooms[key])
    if from then setExit(from,to,dir[C.last.direction]) end
  end
  C.last=nil
  C.current=key; C.visible={}
  for _,d in ipairs(r.directions or {}) do C.visible[d]=true end
  room(key,C.rooms[key]); updateMap()
  if C.enabled then
    if not C.panel then C.ui() end
    centerview(C.ids[key])
    C.caption:echo(html(r.name).."&lt;br&gt;"..(C.language=="de" and "Hilfe: chui hilfe" or "Help: chui help"))
  end
  if C.walk then
    if C.walk.expected~=key then
      C.stop(); say("Walk stopped: check LOOK.","Weg beendet: Bitte SCHAU prüfen.")
    end
  end
end
function C.ready()
  local r=gmcp.Cairnholm.Ready
  if not C.walk then return end
  if r.combat or r.room~=C.walk.expected or r.room~=C.current then C.stop(); return end
  if C.timer then killTimer(C.timer); C.timer=nil end
  C.timer=tempTimer(0.4,function() C.nextStep() end)
end
function C.nextStep()
  if not C.walk then return end
  local step=table.remove(C.walk.steps,1)
  if not step or C.combat or not C.visible[step.direction] or not C.enabled then C.stop(); return end
  C.walk.expected=step.target
  C.sending=true; send(step.direction); C.sending=false
  C.timer=tempTimer(4,function()
    C.stop(); say("Walk stopped; no confirmed arrival. Use LOOK.","Weg beendet; keine bestätigte Ankunft. Bitte SCHAU.")
  end)
end
function C.beginWalk(target)
  C.stop()
  local destination
  for key,id in pairs(C.ids) do if id==tonumber(target) then destination=key end end
  if not destination or not starter[destination] or not starter[C.current] or C.combat then
    say("Choose a mapped starter room; other journeys are manual.","Wähle einen Anfangsraum; andere Wege gehst du selbst."); return
  end
  local queue={{key=C.current,steps={}}}; local seen={[C.current]=true}
  local i=1
  while queue[i] do
    local entry=queue[i]; i=i+1
    if entry.key==destination then C.walk={steps=entry.steps}; C.nextStep(); return end
    for d,to in pairs(starter[entry.key][5]) do
      if starter[to] and not seen[to] then
        seen[to]=true; local steps={}
        for _,step in ipairs(entry.steps) do table.insert(steps,step) end
        table.insert(steps,{direction=d,target=to}); table.insert(queue,{key=to,steps=steps})
      end
    end
  end
  say("There is no ordinary mapped route. Travel manually.","Kein gewöhnlicher Kartenweg. Gehe selbst weiter.")
end
function C.control(action)
  action=(action or ""):lower()
  if action=="on" or action=="an" then
    C.enabled=true; C.ui(); C.effects(); if gmcp.Char and gmcp.Char.Vitals then C.vitals() end; C.save()
  elseif action=="layout" or action=="anordnung" then
    C.enabled=true; C.ui()
    for _,entry in ipairs({{C.panel,false},{C.chatPanel,true}}) do
      local panel,chat=entry[1],entry[2]
      if panel then
        local x,y,w,h=dimensions(chat)
        panel:move(x,y); panel:resize(w,h)
        if chat then panel:setDockPosition("bottom") end
      end
    end
    C.saveLayout()
    C.effects(); if gmcp.Char and gmcp.Char.Vitals then C.vitals() end; C.save()
    say("Default window sizes restored; chat docked below the main text. Your layout is remembered.",
      "Standardgrößen wiederhergestellt; Gespräche unter dem Haupttext angedockt. Die Anordnung wird gespeichert.")
  elseif action=="off" or action=="aus" then C.saveLayout(); C.enabled=false; C.stop(); C.clearChat(); C.effects(); C.ui(); C.save()
  else
    say("Cairnholm: chui on/off. Map + HP/MP + current effects + chat tabs (chchat help). chui layout restores window sizes and docks chat below the main text. Later layout changes are remembered. chwalk &lt;map room number&gt;; chwalk stop. Double-clicking a starter room uses the same walker. Walking stops on combat, diversion or missing confirmation. GUIDE gives your next step; LOOK and HELP always work. New places are recorded only as you visit. Card choice and unusual passages stay manual.",
      "Cairnholm: chui an/aus. Karte + LP/MP + aktuelle Zustände + Gesprächsreiter (chchat hilfe). chui anordnung stellt Fenstergrößen wieder her und dockt Gespräche unter dem Haupttext an. Spätere Änderungen bleiben erhalten. chwalk &lt;Kartennummer&gt;; chwalk stopp. Ein Doppelklick auf einen Anfangsraum folgt demselben Weg. Kampf, Umweg oder fehlende Bestätigung stoppen den Weg. WEGWEISER zeigt den nächsten Schritt; SCHAU und HILFE bleiben verfügbar. Neue Orte werden erst beim Besuch erfasst. Kartenwahl und ungewöhnliche Wege bleiben manuell.")
  end
end
function C.shutdown()
  C.save(); C.stop(); C.clearChat()
  C.effectSignature=nil
  if C.effectsView then C.effectsView:clear() end
  for _,id in ipairs(C.handlers) do killAnonymousEventHandler(id) end
  for _,id in ipairs(C.aliases) do killAlias(id) end
  if C.panel then C.panel:hide() end
  if C.chatPanel then C.chatPanel:hide() end
  if doSpeedWalk==C.mapperWalk then
    doSpeedWalk=C.previousWalk
    if mudlet then mudlet.mapper_script=C.previousMapper end
  end
  clearMap()
end
listen("gmcp.Char.Status",C.status)
listen("gmcp.Char.Vitals",C.vitals)
listen("gmcp.Cairnholm.Effects",C.effects)
listen("gmcp.Room.Info",C.location)
listen("gmcp.Cairnholm.Ready",C.ready)
listen("gmcp.Cairnholm.Chat",C.chat)
function C.options()
  if gmcp.Cairnholm.Options.gui==false then C.control("off") end
  if gmcp.Cairnholm.Options.gui==true and (gmcp.Cairnholm.Options.explicit or 0)&gt;0 then C.control("on") end
end
listen("gmcp.Cairnholm.Options",C.options)
listen("sysDataSendRequest",function(_,command)
  if C.walk and not C.sending then C.stop() end
  local d=tostring(command):lower():gsub("%s+$","")
  d=words[d] or d
  C.last=nil
  if C.current and C.visible and C.visible[d] and dir[d] then C.last={from=C.current,direction=d} end
end)
listen("sysDisconnectionEvent",function()
  C.save(); clearMap(); C.clearChat(); C.owner=nil
  C.pendingLocation=nil
  if gmcp.Room then gmcp.Room.Info=nil end
  C.effectSignature=nil
  if gmcp.Cairnholm then gmcp.Cairnholm.Effects=nil end
  if C.effectsView then C.effectsView:clear() end
  if C.panel then C.panel:hide() end
  if C.chatPanel then C.chatPanel:hide() end
end)
listen("sysExitEvent",C.save)
listen("sysUninstallPackage",function(_,name) if name=="cairnholm" then C.shutdown() end end)
table.insert(C.aliases,tempAlias("^chui(?: (.*))?$",function() C.control(matches[2]) end))
table.insert(C.aliases,tempAlias("^chchat(?: (.*))?$",function() C.chatControl(matches[2]) end))
table.insert(C.aliases,tempAlias("^chwalk (.+)$",function()
  if matches[2]=="stop" or matches[2]=="stopp" then C.stop() else C.beginWalk(matches[2]) end
end))
mudlet=mudlet or {}
C.previousMapper=not removedGeneric and mudlet.mapper_script or nil
C.previousWalk=not removedGeneric and doSpeedWalk or nil
C.mapperWalk=function()
  C.beginWalk(speedWalkPath and speedWalkPath[#speedWalkPath])
end
doSpeedWalk=C.mapperWalk; mudlet.mapper_script=true
-- Late installation must consume the current snapshot, not await a stat change.
if gmcp.Char and gmcp.Char.Status then C.status() end
if gmcp.Cairnholm and gmcp.Cairnholm.Options then C.options() end
if gmcp.Char and gmcp.Char.Vitals then C.vitals() end
if gmcp.Cairnholm and gmcp.Cairnholm.Effects then C.effects() end
if gmcp.Room and gmcp.Room.Info then C.location() end
</script><eventHandlerList /></Script>
</ScriptPackage>
</MudletPackage>
