<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>

<muclient>

<plugin
   name="MIM_GMCP_handler"
   author="Murderous Intentions"
   id="c8de491c3162475692a1d8d4"
   language="Lua"
   purpose="Handle Murderous Intentions GMCP messages and broadcast data"
   save_state="y"
   date_written="2026-10-16 16:46:48"
   requires="4.59"
   version="1.0"
>
<description trim="y">

Murderous Intentions GMCP Handler

Receives the game's GMCP messages, keeps the latest data, and tells other
plugins when new data arrives.  Install it in your Murderous Intentions
world only (it accepts GMCP from whatever game the world connects to).

The game sends, without being asked:

  Char.Vitals   hp, maxhp, mana, maxmana, moves, maxmoves, tnl, perlevel,
                align, enemy, enemypct
                (with the prompt, whenever one of them changes)
  Char.Status   name, level, race, class, practices, trains
                (on entering the game, on levelling, and each tick if changed)
  Room.Info     num, name, zone, area, terrain, details, exits
                (with the prompt when it changes; send "request room" to
                get it again at once)
  Comm.Channel  chan, msg, player
                (each line of talk: the channels, say, mobsay, tell,
                gtell, yell, shout and info; msg has the game's colour
                codes)

This plugin also asks for some MSDP variables, which the game sends over
GMCP in a package called MSDP: ROOM (vnum, name, area, exits), EXPERIENCE,
EXPERIENCE_MAX, MONEY (in silver) and ALIGNMENT.

Package names are stored in lower case: char.vitals, char.status, msdp.

Commands:

  mimgmcp                     this help
  mimgmcp show [what]         show the data, e.g. mimgmcp show char.vitals
  mimgmcp debug 0|1|2         show GMCP messages as they arrive (off,
                              simple, verbose)
  mimgmcp statusbar on|off    show your vitals on MUSHclient's status bar
  mimgmcp send &lt;message&gt;      send a GMCP message to the game

Using the data in your own plugins:

  Each time a message arrives this plugin calls BroadcastPlugin(1, name)
  with its lower case name, e.g. "char.vitals" or "msdp".  Get the data with

    function OnPluginBroadcast (msg, id, name, text)
       if id == "c8de491c3162475692a1d8d4" and text == "char.vitals" then
          local ok, s = CallPlugin("c8de491c3162475692a1d8d4",
                                   "gmcpdata_as_string", "char.vitals")
          local vitals = loadstring("return " .. s)()
          Note("HP: " .. vitals.hp .. "/" .. vitals.maxhp)
       end
    end

  "reload" is broadcast when the data is cleared (on installing the plugin
  and on disconnecting).

</description>

</plugin>

<aliases>
<alias
   match="^mimgmcp$"
   script="show_help"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
></alias>

<alias
   match="^mimgmcp show(?:\s+(\S+))?$"
   script="show_data"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
></alias>

<alias
   match="^mimgmcp debug(?:\s+(\S+))?$"
   script="set_debug"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
></alias>

<alias
   match="^mimgmcp statusbar(?:\s+(\S+))?$"
   script="set_statusbar"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
></alias>

<alias
   match="^mimgmcp send\s+(.+)$"
   script="send_alias"
   enabled="y"
   regexp="y"
   ignore_case="y"
   sequence="100"
></alias>
</aliases>

<script>
<![CDATA[

require "json"
require "serialize"

local GMCP = 201
local IAC, SB, SE = 255, 250, 240
local PLUGIN_ID = "c8de491c3162475692a1d8d4"

-- MSDP variables to ask for (sent to us over GMCP in the "MSDP" package)
local MSDP_REPORT = { "ROOM", "EXPERIENCE", "EXPERIENCE_MAX", "MONEY", "ALIGNMENT" }

-- packages the game only sends the changed parts of: merge, don't replace
local MERGED = { msdp = true }

local GMCPDebug = tonumber(GetVariable("GMCPDebug")) or 0
local StatusBar = GetVariable("StatusBar") == "on"

gmcpdata = {}

local function info(msg)
   ColourNote("#FFAF00", "", msg)
end

-- Send a GMCP message to the game.
function Send_GMCP_Packet(what)
   SendPkt(string.char(IAC, SB, GMCP) ..
           string.gsub(what, "\255", "\255\255") ..  -- IAC becomes IAC IAC
           string.char(IAC, SE))
end

-- Find the table at "char.vitals" (or a value, "char.vitals.hp").
function gmcpdata_at_level(what)
   local node = gmcpdata
   for level in string.gmatch(string.lower(what or ""), "[%w_]+") do
      if type(node) ~= "table" or node[level] == nil then
         return nil
      end
      node = node[level]
   end
   return node
end

-- For other plugins, through CallPlugin: the data as a Lua expression.
function gmcpdata_as_string(what)
   return serialize.save_simple(gmcpdata_at_level(what) or "")
end

-- Clear everything, and tell other plugins to.
local function reset()
   gmcpdata = {}
   BroadcastPlugin(1, "reload")
end

-- Say hello, and ask for the MSDP variables we want.
local function hello()
   Send_GMCP_Packet(string.format('Core.Hello { "client": "MUSHclient", "version": "%s" }', Version()))
   Send_GMCP_Packet('Core.Supports.Set [ "Char 1" ]')
   Send_GMCP_Packet('MSDP { "REPORT": [ "' .. table.concat(MSDP_REPORT, '", "') .. '" ] }')
end

local function update_statusbar()
   if not StatusBar then
      return
   end
   local v = gmcpdata.char and gmcpdata.char.vitals
   local s = gmcpdata.char and gmcpdata.char.status
   if not v then
      return
   end
   local text = string.format("HP %s/%s   Mana %s/%s   Moves %s/%s",
      tostring(v.hp), tostring(v.maxhp), tostring(v.mana), tostring(v.maxmana),
      tostring(v.moves), tostring(v.maxmoves))
   if s then
      text = text .. string.format("   %s, level %s %s %s   Practices %s   Trains %s",
         tostring(s.name), tostring(s.level), tostring(s.race), tostring(s.class),
         tostring(s.practices), tostring(s.trains))
   end
   SetStatus(text)
end

-- Accept GMCP when the game offers it.
function OnPluginTelnetRequest(msg_type, data)
   if msg_type ~= GMCP then
      return false
   end
   if data == "WILL" then
      return true
   end
   if data == "SENT_DO" then
      hello()
      return true
   end
   return false
end

-- A GMCP message: "Package.Name {json}".
function OnPluginTelnetSubnegotiation(msg_type, data)
   if msg_type ~= GMCP then
      return
   end

   if GMCPDebug > 0 then
      info("GMCP: " .. data)
   end

   local message, params = string.match(data, "^%s*([%w_%.]+)%s*(.*)$")
   if not message then
      return
   end
   message = string.lower(message)

   local value = nil
   if params ~= "" then
      local ok, t = pcall(json.decode, params)
      if not ok then
         ColourNote("white", "red", "GMCP: could not decode " .. message .. ": " .. tostring(t))
         return
      end
      value = t
   end

   -- walk down to where this message goes, making tables as needed
   local node, last = gmcpdata, nil
   local parts = {}
   for part in string.gmatch(message, "[%w_]+") do
      table.insert(parts, part)
   end
   for i = 1, #parts - 1 do
      if type(node[parts[i]]) ~= "table" then
         node[parts[i]] = {}
      end
      node = node[parts[i]]
   end
   last = parts[#parts]

   if MERGED[message] and type(value) == "table" then
      node[last] = type(node[last]) == "table" and node[last] or {}
      for k, v in pairs(value) do
         node[last][k] = v
      end
   else
      node[last] = value
   end

   if GMCPDebug > 1 then
      info("gmcpdata: " .. serialize.save_simple(gmcpdata))
   end

   if message == "char.vitals" or message == "char.status" then
      update_statusbar()
   end

   BroadcastPlugin(1, message)
end

function show_help(name, line, wildcards)
   info("Murderous Intentions GMCP handler")
   info("  mimgmcp show [what]       show the data, e.g. mimgmcp show char.vitals")
   info("  mimgmcp debug 0|1|2       show GMCP messages as they arrive")
   info("  mimgmcp statusbar on|off  your vitals on the status bar (" .. (StatusBar and "on" or "off") .. ")")
   info("  mimgmcp send <message>    send a GMCP message to the game")
end

function show_data(name, line, wildcards)
   local what = wildcards[1] or ""
   local node = (what == "") and gmcpdata or gmcpdata_at_level(what)
   if node == nil then
      info("No GMCP data for '" .. what .. "' yet.")
      return
   end
   info((what == "" and "gmcpdata" or what) .. " = " .. serialize.save_simple(node))
end

function set_debug(name, line, wildcards)
   local n = tonumber(wildcards[1])
   if n == nil or n < 0 or n > 2 then
      info("GMCP debug is " .. GMCPDebug .. ".  Use: mimgmcp debug 0 (off), 1 (simple) or 2 (verbose)")
      return
   end
   GMCPDebug = n
   info("GMCP debug: " .. ({ [0] = "off", "simple", "verbose" })[n])
end

function set_statusbar(name, line, wildcards)
   local w = string.lower(wildcards[1] or "")
   if w == "on" then
      StatusBar = true
      update_statusbar()
   elseif w == "off" then
      StatusBar = false
      SetStatus("")
   else
      info("The status bar is " .. (StatusBar and "on" or "off") .. ".  Use: mimgmcp statusbar on|off")
      return
   end
   info("Status bar " .. (StatusBar and "on" or "off") .. ".")
end

function send_alias(name, line, wildcards)
   Send_GMCP_Packet(wildcards[1])
end

function OnPluginInstall()
   reset()
   if IsConnected() then
      info("MIM GMCP handler installed.  Reconnect to the game so it can turn GMCP on.")
   end
end

function OnPluginDisconnect()
   reset()
end

function OnPluginSaveState()
   SetVariable("GMCPDebug", GMCPDebug)
   SetVariable("StatusBar", StatusBar and "on" or "off")
end

]]>
</script>
</muclient>
