<?xml version="1.0" encoding="iso-8859-1"?>
<!DOCTYPE muclient>
<!--
A mostly-generic GMCP handler & subscription interface for other plugins.
=========================================================================

The "Core.Supports.Set" GMCP packet names are set to all the ones the
Discworld MUD uses. (This is the only part of the plugin that is
actually Discworld-specific.) If you want to use it for something else,
just modify the `supportSet` list.

* Other plugins can call `subscribe(pluginID, callbackName, ...)` with any
number of packet names, and when they are received, the named callback will
be called, with the packet name and packet data as arguments.

* Call `unsubscribe(pluginID, ...)` with any number of packet names to stop
receiving the callback for those packets. Disabled or uninstalled plugins
are automatically unsubscribed, so no worries.

* Use the "gmcpdebug <mode> <packet>" alias to change debug settings.
	Modes:
		0 = off
		1 = brief - Only prints the packet name
		2 = verbose - Prints the full packet
	Packet:
		If you specify a packet name (like "char.vitals", or "room.info"),
		then it will only print debug info for that packet. (brief or verbose)

	Example: "gmcpdebug 1 char.vitals" ==> prints "char.vitals" whenever the
	client recieves a packet with that name.

	If you have debug on at all you will also get some other messages, about
	subscription changes and so on.

-->
<muclient>

<plugin
	name="GMCP_Interface"
	author="Ross Grams"
	purpose="GMCP Subscription Interface."
	id="c190b5fc9e83b05d8de996c3"
	language="Lua"
	save_state="y"
	date_written="2019-02-19"
	requires="5.05"
	version="1.0"
	>
</plugin>

<aliases>
	<alias
		script="setDebugMode"
		match="^gmcpdebug (\d)\s*(.*)?$"
		enabled="y"
		regexp="y"
		sequence="100"
	></alias>
</aliases>

<script>
	<![CDATA[

-- Telnet Command Codes:
local IAC = 255 -- Interpret As Command
local WILL = 251
local WONT = 252
local DO = 253
local DONT = 254
local SB = 250 -- Subnegotiation Begin
local SE = 240 -- Subnegotiation End

local GMCP = 201

local GMCPPacketBegin = string.char(IAC, SB, GMCP)
local GMCPPacketEnd = string.char(IAC, SE)

local supportSet = {
	"char.login", "char.info", "char.vitals",
	"room.info", "room.map", "room.writtenmap"
}
local validPacketNames = {}
for i,v in ipairs(supportSet) do  validPacketNames[v] = true  end

local DEBUG_COL = "darkorange"
local DEBUG_BG_COL = ""
local DEBUG_ERR_COL = "white"
local DEBUG_ERR_BG_COL = "darkred"
local DEBUG_MODES = { [0] = "off", [1] = "brief", [2] = "verbose" }

-- Utility Functions
-- ==================================================
local function isEmpty(t) -- Check if a table is empty.
	assert(type(t) == "table", "isEmpty: `t` is not a table. " .. tostring(t))
	for k,v in pairs(t) do
		return false
	end
	return true
end

-- Debugging Stuff
-- ==================================================

local debugMode = tonumber(GetVariable("debugMode")) or 0
local debugPacketName = GetVariable("debugPacketName") or nil

function OnPluginSaveState()
	SetVariable("debugMode", tostring(debugMode))
	SetVariable("debugPacketName", tostring(debugPacketName))
end

local function debugNote(msg, isError)
	local col = isError and DEBUG_ERR_COL or DEBUG_COL
	local bgCol = isError and DEBUG_ERR_BG_COL or DEBUG_BG_COL
	ColourNote(col, bgCol, msg)
end

-- Called from alias.
function setDebugMode(name, line, wildcards)
	local mode = tonumber(wildcards[1])
	local packetName = wildcards[2]
	if not mode or mode > 2 then -- Invalid mode error.
		local errMsg = "GMCP Interface: setDebugMode - Invalid mode '" ..
			wildcards[1] .. "'. Valid modes are: 0, 1, and 2."
		debugNote(errMsg, true)
		return
	end

	if packetName ~= "" and not validPacketNames[packetName] then -- Invalid packet name error.
		debugNote("GMCP Interface: setDebugMode - Invalid packet name '" ..
			packetName .. "'.", true)
		packetName = ""
	end

	debugMode = mode
	debugPacketName = packetName ~= "" and packetName or nil

	local modeStr = DEBUG_MODES[mode]
	local packetStr = ""
	if mode ~= 0 then
		packetStr = packetName == "" and "" or " " .. packetName
	end
	debugNote("GMCP set debug: ".. modeStr .. packetStr)
end

-- Subscription Stuff
-- ==================================================
-- Keeping track of two, keyed lists:
--		- Messages that have subscribers. (for sending out messages)
--		- Plugins that are subscribed to messages. (for removing disabled plugins)

local subscrMessages = {}
local subscrPlugins = {}

-- Subscribe a plugin (with callback name) to one or more GMCP packet names.
function subscribe(pluginID, callbackName, ...)
	if debugMode > 0 then
		debugNote("GMCP Interface: subscribe - " .. tostring(GetPluginInfo(pluginID, 1)))
	end
	local pluginMsgs = subscrPlugins[pluginID] or {}
	local messages = {...}
	for i,msg in ipairs(messages) do
		if debugMode > 0  then  debugNote("\t" .. msg)  end

		if not subscrMessages[msg] then  subscrMessages[msg] = {}  end
		subscrMessages[msg][pluginID] = callbackName
		pluginMsgs[msg] = true
	end
	subscrPlugins[pluginID] = pluginMsgs -- In case it's new.
end

-- Unsubscribe a plugin from one or more GMCP packet names.
function unsubscribe(pluginID, ...)
	if debugMode > 0 then
		debugNote("GMCP Interface: unsubscribe - " .. tostring(GetPluginInfo(pluginID, 1)))
	end
	local pluginMsgs = subscrPlugins[pluginID]
	if not pluginMsgs then  return  end
	local messages = {...}
	for i,msg in ipairs(messages) do
		if debugMode > 0  then  debugNote("\t" .. msg)  end

		if subscrMessages[msg] then  subscrMessages[msg][pluginID] = nil  end
		pluginMsgs[msg] = nil
		if isEmpty(pluginMsgs) then  subscrPlugins[pluginID] = nil  end
	end
end

-- Plugin Tracking & Auto-Unsubscribing
-- ==================================================
-- Track the plugins that are currently enabled and the plugins that have
-- subscribed so we can automatically unsubscribe them when they are
-- uninstalled or disabled.

local oldPluginList = {} -- Last list of currently-enabled plugins.

function OnPluginListChanged()
	local pluginList = GetPluginList()

	-- Remove plugins that are not enabled.
	for i=#pluginList, 1, -1 do
		local ID = pluginList[i]
		if not GetPluginInfo(ID, 17) then  table.remove(pluginList, i)  end
	end

	-- Check for old plugins that are now gone.
	for i,oldID in ipairs(oldPluginList) do
		local noLongerEnabled = true
		for i,ID in ipairs(pluginList) do
			if oldID == ID then
				noLongerEnabled = false
				break
			end
		end
		if noLongerEnabled and subscrPlugins[oldID] then -- Auto-unsubscribe.
			if debugMode > 0 then
				debugNote("Subscribed plugin '" .. GetPluginInfo(oldID, 1) ..
					"' disabled. Auto-unsubscribing it.")
			end
			local msgs = subscrPlugins[oldID]
			local indexed = {} -- Put the messages into an indexed table so we can unpack it.
			for msg,_ in pairs(msgs) do  table.insert(indexed, msg)  end
			unsubscribe(oldID, unpack(indexed))
		end
	end

	oldPluginList = pluginList
end

-- GMCP / Telnet Stuff
-- ==================================================

local function sendGMCPPacket(dataStr)
	local packet = GMCPPacketBegin .. dataStr .. GMCPPacketEnd
	SendPkt(packet)
end

function OnPluginTelnetRequest(code, data)
	if code == GMCP then
		if data == "WILL" then -- GMCP Offered.
			return true
		elseif data == "SENT_DO" then -- We sent acceptance of GMCP.
			-- GMCP negotiated, now send the options that we want.
			local hello = 'core.hello { "client" : "MUSHclient", "version" : "%s" }'
			hello = string.format(hello, Version())
			sendGMCPPacket(hello)

			local supports = 'core.supports.set [ "%s" ]'
			supports = string.format(supports, table.concat(supportSet, '", "'))
			sendGMCPPacket(supports)
			return true
		end
	end
end

-- Recieved telnet data packet.
function OnPluginTelnetSubnegotiation(code, packet)
	if code == GMCP then
		-- Split into packet-name and packet-data.
		local name, data = string.match(packet, "^([%a.]+)%s+(.*)$") -- (letters and periods) spaces (everything else)

		if name then
			if validPacketNames[name] then
				if debugMode == 1 then -- Brief
					if not debugPacketName or debugPacketName == name then
						debugNote(name)
					end
				elseif debugMode == 2 then -- Verbose
					if not debugPacketName or debugPacketName == name then
						debugNote(packet)
					end
				end

				if subscrMessages[name] then -- Notify any subscribers.
					for pluginID,callbackName in pairs(subscrMessages[name]) do
						CallPlugin(pluginID, callbackName, name, data)
					end
				end
			end
		else -- name == nil
			debugNote("GMCP Interface - Couldn't parse GMCP packet!", true)
			if debugMode == 1 then  debugNote(name, true)
			elseif debugMode == 2 then  debugNote(packet, true)  end
		end
	end
end

	]]>
</script>

</muclient>