Need help debugging an error from an old addon

I started using the addon Arbitrary Commands (www. curseforge .com/wow/addons/arbitcomm) during Dragonflight and, though it hadn’t been updated since Aug 2015, it worked perfectly fine. It allowed me to save scripts which I could then access through a minimap button and/or data broker addon (I use Titan Panel). I used it for various things like printing out which taming tomes I had learned on my hunters, which pepe costumes I’ve collected, etc. which helped free up macro slots.

With the recent pre-patch for The War Within, the addon is still mostly working as far as I can tell. I can open the config window and manage my saved scripts. I can even use the “Run now” button to run scripts successfully. But one huge thing stopped working: clicking the data broker/minimap button no longer opens the quick access menu with my scripts. I have a decent amount of experience with making/modifying/fixing addons (and have fixed a fair number of errors from other addons created by the prepatch), but I just can’t seem to figure out how to fix this one, so I’m hoping someone here can help. Here is the error I’m getting:

2x ArbitraryCommands/ac.lua:286: attempt to call global 'EasyMenu' (a nil value)
[string "@ArbitraryCommands/ac.lua"]:286: in function <ArbitraryCommands/ac.lua:282>
[string "@Titan/TitanLDB.lua"]:302: in function <Titan/TitanLDB.lua:300>

Locals:
frame = TitanPanelArbitraryCommandsButton {
 TitanAction = "None"
 titan_tt_func = ""
 plugin_id = "ArbitraryCommands"
 titan_tt_err = ""
 TitanName = "ArbitraryCommands"
 plugin_frame = "TitanPanelArbitraryCommandsButton"
 registry = <table> {
 }
 TitanCreatedBy = "LDB"
}
button = "LeftButton"
f = UIParent {
 Tabs = <table> {
 }
 variablesLoaded = true
 firstTimeLoaded = 1
 Above = Spy_MainWindow {
 }
}
x = 1407.666605
y = -4.777794
(*temporary) = nil
(*temporary) = <table> {
 1 = <table> {
 }
 2 = <table> {
 }
 3 = <table> {
 }
 4 = <table> {
 }
 5 = <table> {
 }
}
(*temporary) = ArbitCommDropDownMenu {
 Text = ArbitCommDropDownMenuText {
 }
 Icon = ArbitCommDropDownMenuIcon {
 }
 Button = ArbitCommDropDownMenuButton {
 }
 Right = ArbitCommDropDownMenuRight {
 }
 Middle = ArbitCommDropDownMenuMiddle {
 }
 Left = ArbitCommDropDownMenuLeft {
 }
}
(*temporary) = UIParent {
 Tabs = <table> {
 }
 variablesLoaded = true
 firstTimeLoaded = 1
 Above = Spy_MainWindow {
 }
}
(*temporary) = 1407.666605
(*temporary) = -4.777794
(*temporary) = "MENU"
(*temporary) = "attempt to call global 'EasyMenu' (a nil value)"
self = <table> {
 script_tree = <table> {
 }
 top_menu = <table> {
 }
 modules = <table> {
 }
 is_slash = <table> {
 }
 defaultModuleLibraries = <table> {
 }
 helptext = <table> {
 }
 added_menu_name_map = <table> {
 }
 options = <table> {
 }
 defaults = <table> {
 }
 dropdownframe = ArbitCommDropDownMenu {
 }
 defaultModuleState = true
 orderedModules = <table> {
 }
 blizOptionsFrame = Frame {
 }
 KEYSEP = "	"
 db = <table> {
 }
 static_script_tree = <table> {
 }
 baseName = "ArbitraryCommands"
 name = "ArbitraryCommands"
 commands = <table> {
 }
 enabledState = true
 macro_editbox = ArbitCommEditBox {
 }
}

I’m obviously not going to include the entire code for the addon, but here is the part of the code that is causing the problem (the bolded line is the specific line throwing the error):

function addon:OnEnable()
    if not self.db.profile.enable then
        return self:Disable()
    end

    self.dropdownframe = self.dropdownframe
        or CreateFrame(“Frame”, “ArbitCommDropDownMenu”, nil, “UIDropDownMenuTemplate”)

    if LDB then
        local launcher = LDB:GetDataObjectByName(nametag) or LDB:NewDataObject(nametag, {
            type = “launcher”,
            label = “Arbitrary Commands”,
            icon = self.options.args.options.args.version.image,
            –[=[
            OnTooltipShow = function (tooltip)
                if not tooltip or not tooltip.AddLine then return end
                tooltip:AddLine(…)
            end, ]=]
            OnClick = function (frame, button)
                if button == “LeftButton” then
                    –if InCombatLockdown() then return end
                    local f,x,y = self:GetMenuLocations()
                    EasyMenu (self.top_menu, self.dropdownframe, f, x, y, “MENU”)
                else
                    self:minimap_icon_right_click()
                end
            end,
        })
        if LDBIcon and not LDBIcon:IsRegistered(nametag) then
            LDBIcon:Register (nametag, launcher, self.db.profile.minimap)
            if not self.db.profile.minimap.hide then
                ldbicon_do_minimap_lock (self.db.profile.minimap.locked)
            end
        end
    end

    self.commands = self.db.profile.commands
    if #self.commands == 0 then
        self:register_default_commands()
    else
        self:BackRegister (self.commands)
    end
    self.register_default_commands = nil

    self:RefreshConfig()
end

A new menu system was implented and EasyMenu has gone. The addons menus will need to be rewritten.

Is there a guide somewhere on how to rewrite it? Or documentation of the changes?

https://warcraft.wiki.gg/wiki/Patch_11.0.0/API_changes

So I have been spending the better part of the afternoon trying to figure out how to rework the code to work with the new menu system, but haven’t made much progress. Would someone be able to look over the addon’s code and help me out?

Here is the entirety of the main file. I don’t believe any of the other files are relevant, but if there is anything missing that is necessary to reference, let me know.

--[[
----- Constants and initial data
----- Other locals
----- Ace3 framework
----- Misc
----- Data bridges (to easymenu, to treegroup)
----- Script execution
----- Initial scripts
----- Added Lua routines
]]
local nametag, addon = ...
local L = LibStub("AceLocale-3.0"):GetLocale(nametag)
local LDB = LibStub("LibDataBroker-1.1", true)
local LDBIcon = LibStub("LibDBIcon-1.0", true)
local AceCD = LibStub("AceConfigDialog-3.0", true)
local flib = LibStub("LibFarmbuyer")


----- Constants and initial data
addon.defaults = {
	profile = {
		enable = true,
		minimap = {    -- LibDBIcon looks inside here for 'minimapPos' and 'hide'
			--minimapPos = 225,
			locked = false,
		},
		verbose = true,
		commands = {},
	}
}

addon.static_script_tree = {
	{
		value = 'intro',
		text = L["Instructions"],
		children = {
			{
				value = 'editor',
				text = L["Editing scripts"],
			},
			{
				value = 'slash',
				text = L["Macro Commands"],
			},
			{
				value = 'lua',
				text = "Lua",
			},
			{
				value = 'morefuncs',
				text = L["Added Lua functions"],
			},
			{
				value = 'registercomm',
				text = "From other addons",  -- XXX add localization once a better label is chosen
			},
		},
	},
	{
		value = 'userscripts',
		text = L["Player Scripts"],
	},
}
if flib.author_debug then
	_G.AC = addon
	addon.static_script_tree[3] = {
		value = 'reloadui',
		text = "ReloadUI",
	}
end

local ldbicon_do_minimap_lock   -- forward decl used in options table
addon.options = {
	name = "Arbitrary Commands",
	type = 'group',
	childGroups = 'tab',
	handler = addon,    -- functions listed as strings called as addon:func
	get = "GetOption",
	set = "SetOption",
	args = {
		options = {
			name = GENERAL,
			desc = L["General options"],
			type = 'group',
			order = 100,
			args = {
				version = {
					--name = filled in during OnInit
					type = 'description',
					fontSize = "large",
					image = "Interface\\MacroFrame\\MacroFrame-Icon",
					cmdHidden = true,
					order = 1,
				},
				header1 = {
					name = GENERAL,
					type = 'header',
					cmdHidden = true,
					order = 2,
				},
				enable = {
					name = ENABLE,
					desc = L["Use this addon"],
					type = 'toggle',
					arg  = "ToggleEnable",
					order = 5,
				},
				verbose = {
					name = L["Verbose execution"],
					desc = L["Print the name/description of an entry when running it."],
					type = 'toggle',
					order = 5,
				},
				refresh = {
					name = L["Rebuild Dropdowns"],
					desc = L["Rebuilds the dropdown menu from current config. Shouldn't be needed most of the time."],
					type = 'execute',
					func = "RefreshConfig",
					order = 10,
				},
				header2 = {
					name = L["Specialized"],
					type = 'header',
					cmdHidden = true,
					order = 50,
				},
				minimap = {
					name = L["Show minimap icon"],
					desc = L["Leave this ON unless you have another LDB display and know what you're doing."],
					type = 'toggle',
					order = 55,
					get = function() return not addon.db.profile.minimap.hide end,
					set = function(info,val)
					          addon.db.profile.minimap.hide = not val
					          LDBIcon[val and "Show" or "Hide"](LDBIcon,nametag)
					      end,
					disabled = function() return (not LDBIcon) or (not addon.db.profile.enable) end,
				},
				minimaplock = {
					name = L["Lock minimap icon"],
					desc = L["Click and drag icon into place, then toggle this option."],
					type = 'toggle',
					order = 56,
					get = function() return addon.db.profile.minimap.locked end,
					set = function(info,val)
					          addon.db.profile.minimap.locked = val
					          ldbicon_do_minimap_lock (val)
					      end,
					disabled = function()
					               return addon.options.args.options.args.minimap.disabled()
								          or addon.db.profile.minimap.hide
					           end,
				},
			},
		},
		--profiles =   filled in OnInit
		--scripts =   filled in editor.lua
	}
}


-----------------------------------------------------------------------------
----- Other locals
local is_slash
local added_menu_name_map = {}
local ldbicon_ondragstart
function ldbicon_do_minimap_lock (locked)
	-- LibDBIcon doesn't provide an API for retrieving the button it creates
	ldbicon_ondragstart = ldbicon_ondragstart or LDBIcon.objects[nametag]:GetScript("OnDragStart")
	if locked then
		LDBIcon.objects[nametag]:SetScript("OnDragStart", nil)
	else
		LDBIcon.objects[nametag]:SetScript("OnDragStart", ldbicon_ondragstart)
	end
end

-- Working around this bug:
-- http://forums.wowace.com/showpost.php?p=295202&postcount=31
if false then   -- XXX no longer needed?
	local function FixFrameLevel (level, ...)
		for i = 1, select("#", ...) do
			local button = select(i, ...)
			button:SetFrameLevel(level)
		end
	end

	local function FixMenuFrameLevels()
		local f = DropDownList1
		local i = 1
		while f do
			FixFrameLevel (f:GetFrameLevel() + 2, f:GetChildren())
			i = i + 1
			f = _G["DropDownList"..i]
		end
	end

	-- To fix Blizzard's bug caused by the new "self:SetFrameLevel(2);"
	hooksecurefunc("UIDropDownMenu_CreateFrames", FixMenuFrameLevels)
end


-----------------------------------------------------------------------------
----- Ace3 framework
addon = LibStub("AceAddon-3.0"):NewAddon (addon, nametag,
                "AceConsole-3.0")

function addon:SetOption (info, value)--, ...)
	local option = info[#info]
	self.db.profile[option] = value
	local arg = info.arg
	if arg then self[arg](self) end
	--if arg then self[arg](self,value,...) end
end

function addon:GetOption (info)
	local option = info[#info]
	return self.db.profile[option]
end

function addon:OnInitialize()
	local noob = _G.ArbitCommDB == nil
	self.db = LibStub("AceDB-3.0"):New("ArbitCommDB", self.defaults, --[[Default=]]true)

	local function refreshconfig_and_minimap()
		CloseDropDownMenus()
		table.wipe(added_menu_name_map)
		self:BackRegister (self.db.profile.commands)
		self:RefreshConfig()
		if LDBIcon and LDBIcon:IsRegistered(nametag) then
			LDBIcon:Refresh (nametag, self.db.profile.minimap)
		end
	end
	self.db.RegisterCallback (self, "OnProfileChanged", refreshconfig_and_minimap)   -- textbook configs here
	self.db.RegisterCallback (self, "OnProfileCopied", refreshconfig_and_minimap)
	self.db.RegisterCallback (self, "OnProfileReset", function()
		StaticPopup_Show("ARBITCOMM_RESET_ALL").data = self
	end)

	self.options.args.options.args.version.name =
		"|cff30adffVersion " .. (GetAddOnMetadata(nametag, "Version") or "?") .. "|r"
	local AceDBOptions = LibStub("AceDBOptions-3.0", true)
	if AceDBOptions then
		self.options.args.profiles = AceDBOptions:GetOptionsTable(self.db)
		self.options.args.profiles.order = 200
	end

	LibStub("AceConfig-3.0"):RegisterOptionsTable(nametag, self.options)
	AceCD:SetDefaultSize (nametag, --[[width=]]820, --[[height=]]535)
	self.blizOptionsFrame = AceCD:AddToBlizOptions(nametag, "Arbitrary Commands")
	self:RegisterChatCommand("arbit", "OnChatCommand")

	if noob then
		self:Printf(L["If this is your first time using the addon, you may find it helpful to enable the Blizzard %s option (%s -> %s tab -> %s) during setup."],
			"|cff30adff" .. SHOW_NEWBIE_TIPS_TEXT .. "|r",
			UIOPTIONS_MENU,  -- Interface
			GAME,            -- Game (tab)
			HELP_LABEL       -- Help
			)
	end
	self:SetEnabledState(self.db.profile.enable)
	self.OnInitialize = nil
end

function addon:OnEnable()
	if not self.db.profile.enable then
		return self:Disable()
	end

	self.dropdownframe = self.dropdownframe
		or CreateFrame("Frame", "ArbitCommDropDownMenu", nil, "UIDropDownMenuTemplate")

	if LDB then
		local launcher = LDB:GetDataObjectByName(nametag) or LDB:NewDataObject(nametag, {
			type = "launcher",
			label = "Arbitrary Commands",
			icon = self.options.args.options.args.version.image,
			--[=[
			OnTooltipShow = function (tooltip)
				if not tooltip or not tooltip.AddLine then return end
				tooltip:AddLine(.....)
			end, ]=]
			OnClick = function (frame, button)
				if button == "LeftButton" then
					--if InCombatLockdown() then return end
					local f,x,y = self:GetMenuLocations()
					EasyMenu (self.top_menu, self.dropdownframe, f, x, y, "MENU")
				else
					self:minimap_icon_right_click()
				end
			end,
		})
		if LDBIcon and not LDBIcon:IsRegistered(nametag) then
			LDBIcon:Register (nametag, launcher, self.db.profile.minimap)
			if not self.db.profile.minimap.hide then
				ldbicon_do_minimap_lock (self.db.profile.minimap.locked)
			end
		end
	end

	self.commands = self.db.profile.commands
	if #self.commands == 0 then
		self:register_default_commands()
	else
		self:BackRegister (self.commands)
	end
	self.register_default_commands = nil

	self:RefreshConfig()
end

-- Enabling does not change the minimap setting.  Disabling turns the minimap
-- icon off.  Changing the minimap icon does not change enabled state.  This
-- sounds fragile, but is precisely the behavior we want in the presence of
-- possible other LDB launchers.
function addon:OnDisable()
	self.options.args.options.args.minimap.set(nil,false)   -- permanently hides
	self.commands = nil
	self.top_menu = nil
end

function addon:ToggleEnable()
	if self.db.profile.enable then
		self:Enable()
	else
		self:Disable()
	end
end


-----------------------------------------------------------------------------
----- Misc
-- This can be made more "interesting" in future; for now just pop it up
-- near the mouse cursor.
function addon:GetMenuLocations()
	local x,y = GetCursorPosition()
	local scale = UIParent:GetEffectiveScale()
	x, y = x/scale, y/scale
	return UIParent, x+5, y-15
end

function addon:OnChatCommand (input)
	--if not input or input:trim() == "" then
		AceCD:Open (nametag, self:build_main_window())
	--else
	--	LibStub("AceConfigCmd-3.0").HandleCommand(self, "arbit", nametag, input)
	--end
end

function addon:getparent (p, func_name_for_errors)
	local name, found
	local t = self.commands
	for word,dot in p:gmatch('([^%.]+)(.?)') do
		if dot == '.' then       -- not last segment
			found = false
			-- nested loop, oh well, it's called rarely
			for i,c in ipairs(t) do
				if c.sub and c.menulabel == word then
					t = c.sub
					found = true
					break
				end
			end
			assert(found, "Argument to "..func_name_for_errors.." tries to index a menu that doesn't exist yet")
		else                     -- last segment
			name = word
		end
	end
	return name, t
end

StaticPopupDialogs["ARBITCOMM_RESET_ALL"] = flib.StaticPopup{
	text = L["Are you sure you want to delete ALL your scripts and revert to the examples?  This will force a UI reload and cannot be undone."],
	button1 = ACCEPT,
	button2 = CANCEL,
	showAlert = true,
	OnAccept = function (dialog, addon)
		table.wipe(addon.commands)
		addon.commands = nil
		ReloadUI()
	end,
}


-----------------------------------------------------------------------------
----- Data bridges (to easymenu, to treegroup)
local KEYSEP = flib.author_debug and '<SEP>' or '\009'
addon.KEYSEP = KEYSEP
addon.added_menu_name_map = added_menu_name_map

-- Returns the index number of KEY's entry inside KEY's group menu.  Ex:
-- foo.bar                    -> 0
-- foo.bar.barbaz<keysep>1    -> 1
-- foo.bar.barbaz<keysep>4    -> 4
-- barbaz<keysep>4            -> 4
function addon:get_menu_offset (key)
	local o = key:match(KEYSEP .. "(%d+)")
	return tonumber(o) or 0
end

-- Fetch a subset of the commands array, using a menu path.  If SUB_P is
-- true, descends into sub-table.  Returns the table and its parent in the
-- ".commands" tree.  XXX no, just the table for now, need the parent later.
function addon:get_submenu (path, sub_p)
	local m = self.commands
	if type(path) == "string" and path ~= "" then
		m = added_menu_name_map[path]
		assert(m, ":get_submenu given a submenu name (" .. path .. ") not previously registered")
		if sub_p then
			m = m.sub
		end
	end
	return m
end

-- Given tree path, retrieve the command entry.  Returns P,G,X where P is
-- the path table, suitable for reselecting the current position.  The rest:
-- G(group)  X
-- nil       string: treepath was not to a user script; string is first element of path
-- group     true:  treepath was to the topmost "Player Scripts" label
-- group     table:  treepath was to a submenu or script; table is the entry
-- If G is non-nil, it is the menu pathname for/from RegisterMenu/RegisterCommand.
-- (This may be an empty string!)
do
	local seppat = "([%w_ ]+)" .. KEYSEP .. "(%d+)"
	--local seppat = "([%w_]+)" .. KEYSEP .. "([%w_]+)"
	local cache = {}
	local unpack = _G.unpack
	function addon:find (key)
--flib.safeprint("FIND ON", key)
		if key == cache[1] then return unpack(cache,2) end

		local path
		if type(key) == "table" then
			path = key
		elseif type(key) == "string" then
			path = { ('\001'):split(key) }
		else error"wtf" end
		cache[1], cache[2] = key, path

		if path[1] ~= 'userscripts' then
			cache[3], cache[4] = nil, path[1]
			return unpack(cache,2)
		end
		local last = path[#path]
		if last == 'userscripts' then
			cache[3], cache[4] = "", true  -- special case
			return unpack(cache,2)
		end

		-- KEYSEP not present at all:
		-- 		clicking on menu or submenu label
		-- leading KEYSEP:
		-- 		top-level script
		-- two elements containing KEYSEP:
		-- 		nested script
		local ss,se = last:find(KEYSEP,--[[initial=]]1,--[[plain=]]true)
		local p
		if not ss then
			-- submenu
			p = table.concat(path,".",2)
			cache[3] = p
			p = assert(added_menu_name_map[p], "key lookup(1) on submenu that was never added: " .. (p or 'nil'))
		else
			local ele1, ele2
			if ss == 1 then
				-- top script
				cache[3] = ""
				ele1 = self.commands
			else
				-- nested script
				ele1 = last:sub(1,ss-1)
				p = table.concat(path,".",2,#path-1)
				cache[3] = p
				p = assert(added_menu_name_map[p], "key lookup(2) on submenu that was never added: " .. (p or 'nil'))
				ele1 = p.sub
			end
			ele2 = tonumber(last:sub(se+1))
			p = assert(ele1[ele2], "key lookup on nonexistant entry: " .. (ele2 or 'nil'))
		end
		cache[4] = p
		return unpack(cache,2)
	end
end


-- Recursively traverse COMMNADS to generate (1) easymenu entries into EMT,
-- and (2) treegroup entries into TGT.
--
-- SEP breaks up key elements in auxiliary tables.
function addon:populate_data_tables (prefix, EMT, TGT, commands)
	for i = 1, #commands do
		local cmd = commands[i]
		local EMn, TGn = #EMT + 1, #TGT + 1
		local key = prefix .. i
		if cmd.sub then
			local subEMT, subTGT = {}, {}
			self:populate_data_tables (cmd.menulabel .. KEYSEP, subEMT, subTGT, cmd.sub)
			EMT[EMn], TGT[TGn] = self:gen_submenu (cmd, subEMT, subTGT)
		else
			local s = cmd.script:sub(1,1)
			is_slash[key] = (s == '/' or s == '#') and true or nil
			--print(cmd.tooltip or cmd.menulabel, ":", s, ":", is_slash[key])
			if not cmd.no_show then
				EMT[EMn] = self:gen_menu_entry (key, EMn, cmd)
			end
			TGT[TGn] = self:gen_tree_entry (key, TGn, cmd)
		end
	end
end

do
	local function dropdown_handler (ddbutton, lookup_key, cmd)
		CloseDropDownMenus()
		addon:RunScript (lookup_key, cmd)
	end
	function addon:gen_menu_entry (lookup_key, menu_index, cmd)
		return {
			text = cmd.menulabel,
			func = dropdown_handler,
			--value = commands_index,   -- value of UIDROPDOWNMENU_MENU_VALUE when clicked
			arg1 = lookup_key,
			arg2 = cmd,
			notCheckable = true,
			tooltipTitle = cmd.tooltip and cmd.menulabel or nil,
			tooltipText = cmd.tooltip,
		}
	end
	function addon:gen_tree_entry (lookup_key, menu_index, cmd)
		return {
			value = lookup_key,
			text = cmd.menulabel,
		}
	end
	function addon:gen_submenu (menu, subeasymenu, subtreegroup)
		local easymenu = {
			text = menu.menulabel,
			hasArrow = true,
			menuList = subeasymenu,
			notCheckable = true,
		}
		local treegroup = {
			value = menu.menulabel,
			text = menu.menulabel,
			children = subtreegroup,
		}
		return easymenu, treegroup
	end
end


-- This can and will be called multiple times, so must be (effectively,
-- behaviorally) idempotent.  Still going to generate a crapton of garbage.
-- Maybe rewrite this part.
function addon:RefreshConfig()
	self.commands = self.db.profile.commands
	is_slash = {}
	self.is_slash = is_slash

	-- Set up the dropdown menu
	local top = {}

	-- Set up the treegroup structure for the scripting window
	local script = {}

	self:populate_data_tables (KEYSEP, top, script, self.commands)

	-- Finish the dropdown menu
	top[#top + 1] = {
		text = "|cff30adff"..L["Main Menu"].."|r",
		func = function() AceCD:Open (nametag, self:build_main_window()) end,
		--hasArrow = true,
		notCheckable = true,
		tooltipTitle = "|cff30adffArbitrary Commands|r",
		tooltipText = L["Click to open the config menu.  Right-click the minimap icon to open directly to the script editor."],
		--menuList = submenu,
	}
	self.top_menu = top

	-- Finish the scripting window
	self.script_tree = {}
	for i,v in ipairs(self.static_script_tree) do
		if v.value == 'userscripts' then v.children = script end
		table.insert (self.script_tree, v)
	end
end


----- Script execution
do
	-- Only need an internal frame name here to make ChatEdit_HandleChatType not
	-- spew nil errors.  But once set, we can clean up the global reference.
	-- (ChatEdit_HandleChatType/_UpdateHeader needs to be made more robust...)
	local editbox = CreateFrame("EditBox", "ArbitCommEditBox")
	_G.ArbitCommEditBox = nil
	editbox:SetAttribute("chatType", DEFAULT_CHAT_FRAME.editBox:GetAttribute("chatType"))
	editbox:SetAttribute("tellTarget", DEFAULT_CHAT_FRAME.editBox:GetAttribute("tellTarget"))
	editbox:SetAttribute("channelTarget", DEFAULT_CHAT_FRAME.editBox:GetAttribute("channelTarget"))
	-- We could hooksecurefunc or do other things with AddHistoryLine here.
	editbox.language = DEFAULT_CHAT_FRAME.language
	editbox:Hide()
	addon.macro_editbox = editbox
	-- If the player is using IM-style chatframes, then ChatEdit_SendText will
	-- eventually try to :Show/:Hide these members.  Sink those calls into one frame.
	local p = CreateFrame("Frame")
	editbox.header = p
	editbox.focusLeft = p
	editbox.focusRight = p
	editbox.focusMid = p

	function addon:RunScript (lookup_key, entry)
		local cmd = entry

		local verb = cmd.verbose
		if verb == nil then verb = self.db.profile.verbose end
		if verb then
			self:Printf("%s <<%s>>", L["running"], cmd.tooltip or cmd.menulabel)
		end

		local ok, err, errtag
		if is_slash[lookup_key] then
			errtag = '/'
			-- can't use RunMacroText, as it's protected and secure code is a nightmare
			for line in cmd.script:gmatch("[^\n]+") do while true do
				if not line:match("^/") then break end
				editbox:SetText(line)
				ok, err = flib.safecall (ChatEdit_SendText, editbox, --[[addHistory=]]nil)
				break
			end end
		else
			errtag = 'L'
			-- Raw Lua script.  We could use RunScript here, except that any typos or
			-- other errors on the player's part will (a) unconditionally trigger an
			-- error popup of some kind, even if RunScript is xpcall'd, and (b) that
			-- traceback will start in Blizzard code and then move through here, looking
			-- to the player as if it's a bug in AC.  We want to catch those mistakes
			-- ourselves instead.

			-- Syntax and semantic testing
			ok, err = loadstring (cmd.script)
			if ok then
				-- Now call it
				ok, err = flib.safecall (ok)
			end
		end
		if not ok then
			self:Printf ("(%s)%s  %s:  %s",
			             errtag, ERROR_CAPS, cmd.tooltip or cmd.menulabel, tostring(err))
		end
	end
end


----- Initial player scripts

-- For external callers.
function addon:ClearCommands()
	table.wipe(self.commands)
end

-- These functions are called during OnEnable.  Be careful!
function addon:RegisterMenu (pathname, offset)
	--flib.safeprint("RM path:", pathname, "offset", offset)
	assert(type(pathname)=="string", "Argument to :RegisterMenu must be a string")
	if not self:IsEnabled() then
		return  -- is this error()-worthy?
	end

	pathname = strtrim(pathname)
	local name, t = self:getparent (pathname, ":RegisterMenu")

	local m = {
		menulabel = name,
		sub = {},
	}
	added_menu_name_map[pathname] = m

	offset = offset or #t
	assert(type(offset)=="number","offset to :RegisterMenu was not a number!")
	table.insert (t, offset+1, m)
end

-- PATHNAME may be nil or the empty string to register at the top level.
function addon:RegisterCommand (pathname, a1, a2, offset)
	if not self:IsEnabled() then
		return  -- is this error()-worthy?
	end

	local cmd
	if type(a1) == "table" then
		if type(a2) ~= "nil" then
			error(L["No secondary argument may be passed to :RegisterCommand when using a command table."])
		end
		cmd = a1
		assert(type(cmd.menulabel)=="string", L["Command argument to :RegisterCommand missing 'menulabel' field"])
		assert(type(cmd.script)=="string", L["Command argument to :RegisterCommand missing 'script' field"])
	elseif type(a1) == "string" then
		if type(a2) ~= "string" then
			error(L["Secondary argument to :RegisterCommand must be a string."])
		end
		cmd = {
			menulabel = a1,
			script = a2,
		}
	else
		error(L["Primary argument to :RegisterCommand must be a string or a table."])
	end

	local m = self.commands
	if type(pathname) == "string" and pathname ~= "" then
		m = added_menu_name_map[pathname]
		assert(m, ":RegisterCommand given a submenu name (" .. pathname .. ") not previously registered")
		m = m.sub
	end

	offset = offset or #m
	assert(type(offset)=="number",L["offset to :RegisterCommand was not a number!"])

	cmd.menulabel = strtrim(cmd.menulabel)
	--cmd.script = strtrim(cmd.script)
	if cmd.tooltip ~= nil then
		assert(type(cmd.tooltip)=="string")
		cmd.tooltip = strtrim(cmd.tooltip)
	end

	table.insert (m, offset+1, cmd)
end

-- Given TREE from a SV, build the map table that would have been used to create it.
function addon:BackRegister (tree, path)
	for i,cmd in ipairs(tree) do
		if cmd.sub then
			local prefix = path and (path .. '.') or ''
			--print("backregistering", cmd.menulabel, "into", prefix..cmd.menulabel)
			added_menu_name_map[prefix .. cmd.menulabel] = cmd
			self:BackRegister (cmd.sub, cmd.menulabel)
		end
	end
end


----- Added Lua routines
do
	local chanmap = {
		["s"] = "SAY", ["say"] = "SAY",
		["y"] = "YELL", ["yell"] = "YELL",
		["p"] = "PARTY", ["party"] = "PARTY",
		["i"] = "INSTANCE_CHAT", ["instance"] = "INSTANCE_CHAT",
		["ra"] = "RAID", ["raid"] = "RAID",
		["g"] = "GUILD", ["o"] = "OFFICER",
		["rw"] = "RAID_WARNING",
		["bg"] = "INSTANCE_CHAT",
	}
	for _,v in ipairs{
		"SAY", "YELL", "INSTANCE_CHAT", "PARTY", "RAID", "GUILD", "OFFICER", "RAID_WARNING"
	} do
		local l = _G["SLASH_"..v.."1"]
		l = l and l:sub(2)
		--print("mapping",l,"to",v)
		if l then chanmap[l] = v end
	end
	function _G.AC_CHAT (slash, msg)
		local chantype, channel
		local c = chanmap[slash]
		if c then
			chantype, channel = c, nil
		elseif slash == "r" then
			c = ChatEdit_GetLastTellTarget(DEFAULT_CHAT_FRAME.editBox)
			if c and #c > 0 then
				chantype, channel = "WHISPER", c
			else c = nil end
		else
			c = tonumber(slash)
			if c then
				chantype, channel = "CHANNEL", c
			end
		end
		if c then
			SendChatMessage (msg, chantype, nil, channel)
		else
			addon:Printf("%s  %s:  %s", ERROR_CAPS, CHAT_INVALID_NAME_NOTICE, slash)
		end
	end
	function _G.AC_SLASH (text)
		addon.macro_editbox:SetText(text)
		ChatEdit_SendText (addon.macro_editbox, --[[addHistory=]]nil)
		--DEFAULT_CHAT_FRAME.editBox:SetText(text)
		--ChatEdit_SendText (DEFAULT_CHAT_FRAME.editBox, --[[addHistory=]]nil)
	end
end

-- vim:noet

The quick and dirty fix would be to create local copies of the two EasyMenu functions from a previous version of the UI code. How long that would work would depend on how long Blizz. want to keep the old menu system in place ie. you would still have to convert the code, it would just give you more time.

I can’t believe I didn’t think of that. :woman_facepalming: Assuming it works, I’m just going to use that as a fix for now. If it ends up breaking, I’ll go back to trying to convert it to the new system.