The row of icons on top of the map, the options and the other full-screen menus is built from one shared list, Helper.topLevelMenus. An extension adds its own menu to the row by inserting an entry there. The row has a hard limit, and when several extensions each add an icon, the limit is easy to pass without any single one of them doing anything wrong.
This article covers the limit, what happens past it, and a pattern that keeps the menus working: insert the entry, and switch the row to the game's built-in scrolling view whenever it no longer fits.
Helper.createTopLevelTab draws the row as one table with a column per shown entry plus two more. A table has at most Helper.maxTableCols columns, which is 13, so at most 11 entries may be shown at once. An entry is shown when Helper.checkTopLevelConditions(entry) returns true; some vanilla entries come and go with the game state (docked or cockpit, terraforming once it is unlocked, the multiverse map while ventures are online).
Vanilla itself shows 9 or 10 entries, which leaves room for one or two extension entries.
A 12th shown entry does not degrade gracefully. The game log gets
Too many columns. Table contains 14 columns while the max number of columns allowed is 13
from Helper.createTopLevelTab, the view fails, the UI reloads, and the next attempt fails the same way. Every menu with the row fails: the map, the options menu (so Escape too), player information. The player is left without a way to switch anything off.
The row has a second layout, off by default: a carousel of five icons around the current one, with the others one click away on either side. It always uses the same number of columns, so any number of entries fits.
It is a plain field:
Helper.topLevelConfig.scrolling = true
Helper.createTopLevelConfig() builds Helper.topLevelConfig once, when the helper loads, and nothing in vanilla changes it later. Setting the field takes effect the next time a menu draws the row; no patch is needed. A UI reload builds the config again with scrolling off, and the extension's own init runs again after it, so the state does not outlive the Lua session. The field exists in 8.00 and 9.00 alike.
The carousel is fixed at five icons. numDisplayedIcons in the config looks adjustable, but createTopLevelTab fills exactly five slots, so a larger value only adds empty columns.
Step 3 needs a mark that says "an extension switched this on". Store it on the config itself, under a name every extension using this pattern shares: Helper.topLevelConfig.chemodunOverflowScrolling. With one shared name, any of them can switch scrolling off when its own icon goes away, whichever one switched it on, and none of them switches off a scrolling view that another source asked for.
The count limit uses Helper.maxTableCols - 2 rather than a literal 11, so it follows the helper if the table limit ever changes.
A complete Lua side for a fictional extension whose overview menu is FleetBoardMenu, with its icon right after the map. fleetBoard is the extension's own table, and fleetBoard.iconOn() stands for however it reads its own option.
local TOP_LEVEL_ID = "fleetboard"
local SCROLLING_MARK = "chemodunOverflowScrolling"
local CHECK_INTERVAL = 5
local checkQueued = false
local function topLevelRoom()
local shown = 0
for _, entry in ipairs(Helper.topLevelMenus) do
if Helper.checkTopLevelConditions(entry) then
shown = shown + 1
end
end
local max = (Helper.maxTableCols or 13) - 2
return max - shown, shown, max
end
local function hasEntry()
for _, entry in ipairs(Helper.topLevelMenus) do
if entry.id == TOP_LEVEL_ID then
return true
end
end
return false
end
local function enableScrolling(shown, max)
local cfg = Helper.topLevelConfig
if (not cfg) or cfg.scrolling then
return
end
cfg.scrolling = true
cfg[SCROLLING_MARK] = true
DebugError(string.format("Fleet Board: the top menu shows %d entries, at most %d fit in a row: switched it to scrolling. Switch the Fleet Board top menu icon off in Extension Options to get the row back.", shown, max))
end
local function releaseScrolling()
local cfg = Helper.topLevelConfig
if not (cfg and cfg.scrolling and cfg[SCROLLING_MARK]) then
return
end
local room = topLevelRoom()
if room >= 0 then
cfg.scrolling = false
cfg[SCROLLING_MARK] = nil
end
end
local scheduleCheck
local function checkRow()
checkQueued = false
if not hasEntry() then
return
end
local room, shown, max = topLevelRoom()
if room < 0 then
enableScrolling(shown, max)
end
scheduleCheck()
end
scheduleCheck = function()
if checkQueued then
return
end
checkQueued = true
Helper.addDelayedOneTimeCallbackOnUpdate(checkRow, false, getElapsedTime() + CHECK_INTERVAL)
end
local function addEntry()
local list = Helper.topLevelMenus
local pos = #list + 1
for i, entry in ipairs(list) do
if entry.id == TOP_LEVEL_ID then
return
end
if entry.id == "map" then
pos = i + 1
end
end
table.insert(list, pos, {
id = TOP_LEVEL_ID, name = ReadText(77000, 1), icon = "tlt_fleetboard", shortcut = "",
menu = "FleetBoardMenu", helpOverlayID = "toplevel_fleetboard", helpOverlayText = ReadText(77000, 2), param = { 0, 0 },
})
local room, shown, max = topLevelRoom()
if room < 0 then
enableScrolling(shown, max)
end
scheduleCheck()
end
local function removeEntry()
local list = Helper.topLevelMenus
for i, entry in ipairs(list) do
if entry.id == TOP_LEVEL_ID then
table.remove(list, i)
break
end
end
releaseScrolling()
end
-- Called from init and whenever the option changes.
function fleetBoard.syncTopLevelEntry()
if fleetBoard.iconOn() then
addEntry()
else
removeEntry()
end
end
The menu draws the row itself as usual, with Helper.createTopLevelTab(menu, "fleetboard", frame, ...) in its display function; the id there is the entry's id. Nothing in the menu changes for the scrolling view.