Add Support for 12 New Mods and Improve the Mod UI#2
Conversation
… support Register banners from Thorium (103), Spirit (117) and Spirit Reforged (15) alongside the existing Calamity/Catalyst integration. These mods ship one sprite per banner item instead of a single packed banner atlas, so they cannot use the existing atlas+Index render path. Add a BannerInfo.UseItemIcon flag that routes a banner to a new render path drawing it from its own item sprite (TextureAssets.Item[ItemId]), requiring no Index and no mod banner atlas texture. Existing Calamity/Catalyst/vanilla banners keep the atlas+Index path untouched. - BannerLoad.cs: add BannerInfo.UseItemIcon flag (default false) - BannerLoadMod.cs: add AddItemIconBanners() + banner name lists per mod - BannerUISystem.cs: register the 3 mods in ModList for the mod filter - BannerUIElements.cs: add item-icon render path (icon + count + hardmode pin) - BannerBuffTile.cs: grant banner buff by resolving NPC from banner item name Banner names extracted from mod assets (Calamity 2.1.2, Thorium 1.7.2.5, Spirit 1.5.0.43, Spirit Reforged 0.2.0.5). Banner collection and save/load are type-keyed and already cover the new banners.
…cons to stop atlas desync
Calamity banners were drawn from the shared Calamity banner atlas (Tiles/MonsterBanner) using hardcoded per-banner Index values.
Calamity's 2.1.2 update added/removed banners, which shifted the atlas frame order, so every hardcoded Index now pointed at the wrong sprite and banners showed jumbled images that no longer matched their slots.
Migrate Calamity to the existing UseItemIcon path (already used by Thorium, Spirit and Spirit Reforged): each banner is drawn from its own item sprite (TextureAssets.Item[ItemId]) instead of an atlas frame, so banner images can never desync again when Calamity changes its banner set.
The Calamity name list is synced to 2.1.2 +6 added, -3 removed and self-syncs going forward via Mod.TryFind.
- BannerLoadMod.cs:
- replace the ~1490-line hand-written Calamity region with a single AddItemIconBanners("CalamityMod", CalamityBanners, CalamityHardModeBanners)
- add CalamityBanners (121 names from Calamity 2.1.2 assets)
- add CalamityHardModeBanners (56 names) to preserve the hardmode/pre-hardmode filter classification from the original hand-maintained data
- extend AddItemIconBanners with an optional hardmode name set
- PlayerAssist.cs:
- on load, restore only BannerCount from the save; always take static metadata (Index, ModName, ItemName, IsHardMode, UseItemIcon) from code. Previously the whole BannerInfo was overwritten from JSON, so an older save would revert UseItemIcon/Index and defeat the fix. This also stops any future Calamity update from desyncing banners loaded from old saves.
Untouched: Calamity buff granting (still routed through the existing ModName=="CalamityMod" branch), Catalyst (still atlas+Index), and the Thorium/Spirit/Spirit Reforged integration.
Banner data verified against Calamity 2.1.2: 121 banners, no duplicates, all 56 hardmode names present in the banner list.
…ir broken buff
Migrate Catalyst to the same UseItemIcon path as Calamity/Thorium/Spirit so banner sprites are drawn from each item's own icon instead of the Catalyst banner atlas (Tiles/EnemyBanners) with hardcoded Index values.
Catalyst's banner set is unchanged in 1.1.5 (same 4 banners), so this is a consistency and future-proofing change rather than a content fix.
Side effect: Catalyst banner buffs now work. Previously BannerBuffTile had no CatalystMod branch, so Catalyst banners fell through and reused a stale bannerIndex from the previous loop iteration, granting a wrong buff.
They now route through the UseItemIcon branch, which resolves the NPC by name (verified: WulfrumSlime, WulfrumMine, AscendedAstralSlime, MetanovaSlime).
- replace the hand-written Catalyst region with AddItemIconBanners("CatalystMod", CatalystBanners, CatalystHardModeBanners)
- add CatalystBanners (4) and CatalystHardModeBanners (2, preserving the hardmode/pre-hardmode filter classification)
- remove the now-unused bannerInfo/modName locals in LoadModBanners
Untouched: Calamity/Thorium/Spirit/Spirit Reforged registration, draw and buff logic.
The Catalyst atlas texture load and dead atlas draw branch are left in place (harmless, never reached for item-icon banners).
The page-navigation dots were laid out on a single horizontal row (Left = center + i*16), so with many enabled mods the row grew wider than the panel and the dots ran off both sides of the window. Extract the layout into LayoutPageButtons() and place the dots in centered rows below the banner grid, wrapping to a new row whenever a row would exceed the panel width. The panel height now grows downward to fit the extra rows (its top-left is fixed) and shrinks back when fewer pages are shown. Used by both layout sites (UI open and filter/sort), removing the duplicated positioning code. - BannerUI.cs: LayoutPageButtons() computes rows, resizes the panel and positions the dots in centered, wrapping rows - BannerUIElements.cs: expose BannerPanel.DesignHeight as the single source for the base panel height
…look Mod banners were drawn from the item inventory sprite (UseItemIcon), which looks smaller and stylistically off compared to vanilla banners. Instead, read each banner's createTile and placeStyle from its item sample at load time and draw the actual banner tile frame (the vanilla 16x48 three-frame look). This keeps the look authentic for every supported mod while staying robust: placeStyle comes from the item itself, so there are no hardcoded atlas indices to desync when a mod adds, removes or reorders banners. The tile is read per banner, so mods with multiple banner atlases (e.g. Thorium's biome sheets) and mods with one tile per banner (Spirit Reforged) both work automatically. - BannerLoad.cs: add BannerInfo.TileType; UseItemIcon now means "mod banner drawn from its tile, falling back to the item icon if it has none" - BannerLoadMod.cs: read ContentSamples createTile/placeStyle into TileType/Index when registering - BannerUIElements.cs: add DrawTileBanner (tile-frame draw) used by the UseItemIcon path, with the item-icon draw kept as fallback Untouched: buffs, banner collection, save/load (count-only), the vanilla banner path and the hardmode classification.
Register Consolaria (21 banners) and Vitality (32 banners) via the existing item-icon/tile-frame path, so they are drawn from their real banner tiles, collected and buffed like the other supported mods. Hardmode classification verified against the terrariamods.fandom.com enemy lists. - BannerUISystem.cs: add Consolaria and VitalityMod to ModList - BannerLoadMod.cs: add banner name lists and hardmode sets for both mods No existing logic changed; other mods are untouched.
… game and reports which are missing, extra, or unresolved Add a diagnostic tool that auto-discovers every real, buff-granting enemy banner of all loaded mods straight from the engine's banner registry (NPC.BannerID -> Item.NPCtoBanner -> Item.BannerToItem) and reconciles it against what the mod has registered. The "/discoverbanners" chat command writes a per-mod report to Documents/My Games/Terraria/tModLoader/BannerDiscovery.txt: - MISSING: banners the game has but our list doesn't (need adding) — for a mod we haven't covered yet this lists every banner with its exact internal item name, so new mods can be added without digging through their assets; - EXTRA: banners in our list the registry doesn't know (sprite-only items that aren't real enemy banners, e.g. colour variants); - SKIPPED: curated names that failed Mod.TryFind (texture-vs-class mismatches that would silently drop a banner). Isolated in BannerDiscovery.cs as a pure diagnostic: it reads game state and writes a report only, and changes nothing about banner loading, rendering, buffs or collection.
The banner discovery walked the NPC banner registry only, so banners whose NPC has no standard banner link (custom drop / no banner index) were missed. Add a second pass: pass 1 resolves each modded NPC's banner (precise, no false positives) and records the tile types those banners place; pass 2 then treats any modded item that places one of those confirmed banner tiles as a banner too. Keying on a confirmed banner tile means only real banners are added, so it finds the missed siblings without the false positives a raw registry-table walk produced.
…-buff issues The buff branch in BannerBuffTile resolved each banner's NPC every tick by stripping the "Banner"/"BannerItem" suffix and calling Mod.TryFind. If a mod's NPC class name didn't match item-name-minus-suffix the lookup failed silently and no buff was granted. Resolve once at load via the game's own NPC-to-banner association (NPC.BannerID -> Item.NPCtoBanner -> Item.BannerToItem), with the name-strip kept only as a fallback. Store the result in BannerInfo.NpcType so the buff tile just reads a precomputed value. Unresolved banners are logged into SkippedBanners with [NPC not resolved], so silent failures are visible via /discoverbanners. Vanilla banner path, banner discovery, rendering, collection and save/load are untouched.
Clicking the mod filter button now opens a vertical, alphabetically sorted dropdown of all loaded mods instead of cycling one mod per click. - sort BannerLoad.ModList alphabetically as the single source of truth for both the dropdown order and the index-based filter mapping - add ButtonModEntry rows framed by an opaque Terraria UIPanel - close the dropdown on selection, on a click outside it, and when the banner collection is hidden
Add a "Terraria" entry right after "All Mods" in the mod filter dropdown that shows vanilla banners only, hiding everything added by mods.
/discoverbanners now appends a MOD HEALTH REPORT that verifies every banner we register (BannerLoad.BannerDict) actually grants a working nearby-banner buff on the correct enemy. It is grounded entirely in engine data: for each banner it takes the exact index the buff tile writes at runtime and validates it against the game's own banner registry (the NPC -> Item.NPCtoBanner -> Item.BannerToItem chain the damage code itself reads): OK proven - BannerToItem(index) is this exact banner item -> provably correct; OK name - the index hits a real enemy whose display name matches the banner; CHECK - the index hits a real but differently-named enemy (listed to verify); FIXABLE - the same enemy has a banner under a different item id (a wrong id); UNRESOLVED - no banner number maps to it and its banner tile is shared by several enemies, so it is ambiguous from engine data (verify it in-game); BROKEN - the index is a number with no enemy behind it (grants nothing). Pure diagnostic: read-only, writes only the report file, changes no game state.
The nearby-banner buff wrote NPCBannerBuff at a proxy of the banner number: vanilla used the tile style minus 21, modded used the NPC type. Both equal the real banner number for most banners but not all - the banner-tile layout breaks the constant offset, and an enemy's NPC type differs from its banner number when several NPCs share one banner - so those banners hit the wrong slot or a dead one. Resolve the authoritative banner number once at load from the game's own banner registry (NPC -> Item.NPCtoBanner -> Item.BannerToItem == this item) and store it in BannerInfo.BannerId (renamed from NpcType). The buff tile writes that single value for every banner, vanilla and modded alike, so it always targets the banner's real enemy and the exact NPCBannerBuff slot the engine reads. Granting a banner number also covers every NPC that shares it, so multi-part enemies are protected as a whole. Two resolver fixes so every banner that works in vanilla also works in the mod: - iterate ContentSamples.NpcsByNetId (every net id), not 0..NPCCount, so the negative-net-id variant enemies (the coloured slimes, Pinky, ...) keep their banners instead of silently losing them; - when an item is not itself a registry banner, fall back to the banner of its tile when that tile is dedicated to a single enemy, so the extra head/variant items a mod ships for one enemy (e.g. the three Hydra heads -> Lernean Hydra) still grant that enemy's buff. General, no hardcoding. Items that resolve to no banner grant no buff (BannerId == -1) and are surfaced by /discoverbanners. BannerId is recomputed every load, so a save can never carry a stale or shifted index. - BannerLoad.cs: BannerInfo.NpcType -> BannerId; build the item->banner-number and tile->banner-numbers maps, then assign vanilla banner numbers, in LoadBanners - BannerLoadMod.cs: BuildBannerItemToBannerId + ResolveBannerNumber (registry, then single-enemy tile); modded resolution prefers it, then the named-NPC fallback - Tiles/BannerBuffTile.cs: one buff path writing NPCBannerBuff[BannerId] - BannerDiscovery.cs: the audit reads BannerId
There was a problem hiding this comment.
Code Review
This pull request refactors the banner registration and rendering system to dynamically discover and load modded banners using their real banner tiles or item icons, significantly reducing boilerplate code and preventing sprite desyncs. It also introduces a diagnostic banner discovery tool and a mod-filter dropdown in the UI. The review feedback highlights a critical missing bounds check on banner.BannerId in BannerBuffTile.cs that could cause an IndexOutOfRangeException, a cross-platform path issue in BannerDiscovery.cs that should be resolved using Main.SavePath, and a potential layout calculation issue in BannerUIElements.cs where the extra height calculation should be clamped to zero.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
…Redemption, Secrets Of The Shadows
Hard-coded Documents path failed on Linux/macOS and ignored custom save paths. Main.SavePath is the same directory the game writes to, so the report lands next to the rest of the tModLoader data on every platform.
121183a to
c47cb7e
Compare
Two long-standing rendering issues with modded banner thumbnails are fixed in DrawTileBanner: 1. Low quality. The frame was hardcoded to a 16px / 18px-stride banner sheet, so banners authored at a higher resolution (e.g. 2x sheets) were cropped to their top-left 16x16 corner and looked tiny/blocky. The cell geometry (frame size, padding, frame count) is now read from the tile's own TileObjectData and the whole banner is scaled to fit the slot, so every banner renders at its real resolution. 2. Coloured grid lines. Banner sheets pack the three frames with padding pixels between them. At any UI scale other than 100% the default linear sampling blended that padding into the frame edges as thin lines (faint on vanilla/Spirit, bright purple on Thorium, Consolaria, Vitality, ...). The frames are now drawn with point sampling, which reads exact texels and never reaches the padding, and the UI's default linear sampling is restored afterwards so nothing else is affected. Frames are also placed on integer rows with a 1px overlap so no seam shows through at a fractional scale. At 100% scale the output is texel-for-texel identical to before; the change only affects fractional scales and high-resolution sheets. The vanilla atlas path, the icon fallback, buffs, collection and filters are untouched.
Vanilla banners were drawn straight from the Tiles_91 atlas with linear sampling, so at any UI scale other than 100% the sheet's inter-frame padding bled into the frame edges as faint grid lines - the same artifact that was already fixed for mod banners. Route vanilla banners through the same renderer: - extract the point-sampled banner draw into a shared DrawBannerFrames method (point sampling so the inter-frame padding is never sampled, integer rows with a 1px overlap so no seam shows at a fractional scale, then the UI's default linear sampling is restored) - the vanilla atlas path and DrawTileBanner both call it now, removing the duplicated draw code At 100% scale the output is identical (same frame positions, slot-fit scale); only fractional scales change, where the grid lines disappear. Texture selection, collected-count text, the hardmode pin, buffs, collection and filters are untouched.
… at some UI scales Banner frames were positioned and rounded in UI coordinates, so after the UI-scale transform a frame boundary could land mid-pixel; at some interface scales point sampling then caught the sheet's inter-frame padding as a thin coloured line on a row of banners. Position and size the frames in whole screen pixels instead: take the slot's position through Main.UIScaleMatrix, draw the frames under an identity matrix, and lay them on contiguous integer rows. Frame edges now fall on exact pixels at any UI scale, so the padding is never sampled. At 100% scale the output is unchanged.
Register a "Toggle Banner Buffs" keybind that flips the EnableBannerBuff config setting - the same flag the config checkbox uses and that BannerBuffTile reads live, so it takes effect immediately. - BannerCollector: register/unregister the ModKeybind - PlayerBuff.ProcessTriggers: toggle the setting on press and report the new state in chat - BannerCollectorConfig.Save(): persist the change to disk so the toggle survives a restart, via tModLoader's ConfigManager.Save resolved by reflection; best-effort and wrapped so a failed save never interrupts play - localization: keybind display name Client-side only; no change to how buffs are granted.
…state The collection window can be dragged from its panel background. Dragging is gated by a config lock and a small move threshold, so a normal click still deposits items and the window can't be moved by accident. Window position (X/Y) and the lock are client-side config fields. The window applies the saved position each time it opens and writes the new position back on drag release, clamped to the screen. "Reset to Defaults" returns the window to its default spot and re-locks it. The X/Y fields use a screen-sized range so they display correctly in the menu, the lock is listed above them, and they use "Mod Window" naming. The collection's open/closed state persists across sessions via a config value that is serialized but hidden from the menu, applied on world load and saved on world exit.
Add two client config settings - BannerColumns and BannerRows - that control how many banners are shown horizontally and vertically. The window grows to fit the grid; the default 10x2 reproduces the original. - BannerGrid: single source of truth for the grid geometry and counts, with every layout value (page size, panel size, slot positions, page-dot band) derived from the two counts. Pixel constants measured from UI_Board.png. - BannerPanel.Draw: the fixed board art is rebuilt as a true 9-slice - corners kept 1:1, frame edges stretched, pill cells tiled NxM, and only the page-dot band stretched. It is composed in whole screen pixels under an identity matrix with point sampling, so neighbouring pieces share an exact edge and there are no seams at any UI scale. - BannerUI: slot array, page size, totalPages, the slot grid layout, the page-dot rows and the close button (which now tracks the right edge) all derive from the configured counts; the grid is rebuilt (and re-paged) when the counts change, with the current page clamped. - Banner buffs, collection, save/load, dragging, filters and the mod dropdown are untouched.
Add an "All Mods" entry that shows only modded banners, and rename the old catch-all to "All" (every banner, vanilla + modded).
Add a fourth progression filter state (Post-Moon Lord) after All / Pre-Hardmode / Hardmode, cycling 0..3 with its own button icon and a pin drawn on matching banners. A new BannerInfo.IsPostMoonLord flag and an optional postMoonLordNames set in AddItemIconBanners mirror the existing hardmode mechanism. The tiers are mutually exclusive: post-Moon-Lord banners are kept out of the hardmode sets, and the Pre-Hardmode filter now excludes them too, so each banner is exactly one of pre-hardmode / hardmode / post-Moon Lord.
…scale Banner icons were positioned on a screen grid that rounded independently from the pill cells, so at some UI scales the icon and its cell drifted apart by up and the right-hand gap shrank. BannerPanel now publishes its draw origin (UI space) and scale. Each banner resolves its own pill cell exactly as the panel renders it (origin pixel + rounded offset; the slot is a fixed inset into its cell, so no row/column index is needed) and maps its box through that rendered rect the same way the pill maps its recess: cellLeft + round(localOffset / CellW * cellWidth). Icon and recess are therefore the identical fraction of the identical rect, so their margins stay equal at any UI scale.
The dropdown was a single vertical column whose height was the full mod count, so a long list ran straight off the bottom of the screen. It now fills a column top-to-bottom and wraps into a new column to the right as soon as the next row would pass the bottom of the screen. The panel is sized to the resulting grid and shifted left if the extra columns would overflow the right edge. The rows-per-column fit is measured against Main.screenHeight - the same coordinate basis the window-drag clamp uses - so it tracks the window wherever it is dragged and behaves consistently at any UI scale. Because the window is kept on-screen, the fit always leaves several rows per column, so a degenerate single-row layout does not occur in practice.
The mod window's toolbar settings - the sort mode and the three filters (owned/unowned, progression, mod) - are now remembered across sessions. They are stored hidden in the client config as [JsonProperty] internal fields: serialized to disk but kept off the config menu (which lists only public members), exactly like CollectionOpen, and likewise untouched by "Reset to Defaults" (which only resets the menu-visible fields). The window mirrors them to disk on every change (a click, never a per frame tick) and reapplies them on world load, whether the window is open or closed. The saved mod filter is clamped to the current dropdown, so a mod uninstalled since the last session falls back to "All" instead of selecting a missing entry. SortFilterList attaches the page dots and slots only while the window is open; the filtered list and page count are computed either way. This stops the dots and banners from being drawn floating before the inventory is opened, which would otherwise happen when the saved settings are reapplied on world load.
While the collection window is open, shift-clicking a banner moves it between the collection and the inventory or any open storage, mirroring vanilla quick-stack behavior: - inventory <-> collection when no storage is open - chest/bank <-> collection, with an open container taking priority over the collection for inventory items - vanilla handling is left untouched for every other slot and item Add Magic Storage as a weak reference: an open Magic Storage counts as an open container, so banners deposit into it from both the inventory and the collection, and shift-withdrawing a banner from its grid routes into the collection. World-chest writes are synced in multiplayer; the Magic Storage integration is fully inert when that mod is not installed.
- Filter buttons now show a distinct "on" icon (white center) whenever a filter other than "All" is active, instead of always showing the inactive icon. Adds a UI_FilterModOn sprite for the mod filter. - Re-sort and re-filter the collection against current banner counts every time the inventory opens, so the sort-by-count order stays correct without manually toggling the sort button.
While the cursor is over the collection window (or its mod dropdown), nothing behind it reacts: vanilla item slots, the loadout / map / accessory HUD, other mods' panels, and world interaction are all inert. Every interactive element hit-tests the cursor, so the cursor is pushed off-screen at the single choke point of each frame phase that reads it, behind one "cursor over window" guard that is free while the window is closed: - World interaction (item use, mining, placing) is blocked by Player.mouseInterface, raised in BannerUI.Update. - Vanilla HUD and every mod interface layer (item slots, loadout, map, accessories, ...) are blocked in the draw phase: a layer inserted first pushes the cursor off-screen for everything drawn below the window and restores it right before the window and the vanilla cursor draw. The offset is applied to PlayerInput's zoom source (_originalMouseX/Y), not just Main.mouseX, so it survives the SetZoom calls the interface draw makes. - Other mods' UIState panels are blocked in the update phase by detouring UserInterface.Update and hiding the cursor from every interface except the mod's own.
When the wrapped columns of the mod-filter dropdown would still run past the screen width, cap the column count to what fits and scroll the columns vertically with the mouse wheel (one row per notch, clamped), so every mod stays reachable at any resolution. Dropdowns that already fit are unchanged.
Opening the window from the toggle now re-sorts and re-filters, so banners gained or spent while it was closed show at once - matching the existing inventory-open auto-show.
The collection was written only on world exit, so an unexpected exit lost everything gained since the world was loaded. A ModPlayer.SaveData hook now writes it on Terraria's periodic player save too, calling the same SavePlayerBannerData as the exit path, so autosave and exit store identical data. SaveData/LoadData are paired as tModLoader requires; loading still happens in OnWorldLoad, so LoadData is intentionally empty. The autosave runs SaveData on a background thread, so the write is now serialised with a lock and wrapped so a failure can never crash the save thread - on error the previous file is kept and the next save retries. The write logic itself is unchanged, moved verbatim into WriteBannerData.


Hello
First of all, I should say that I am not a programmer, and all of these changes were made with the help of AI.
Main advantage of all these changes is that now even a non-programmer can easily add support for any mod.
Main Changes:
Support has been added for the following mods:
Replaced clicking through the mod filter with a drop-down list.
The quality of the displayed banners has been improved.
A new "Terraria" filter has been added, which shows only vanilla banners.
A new "All mods" filter has been added, which shows only banners from mods.
A new filter for post-Moon Lord banners has been added.
A setting for the number of displayed rows and columns of banners has been added.
The ability to drag the mod window has been added.
A keybind to toggle banner buffs has been added.
A command "/discoverbanners" has been added that outputs all banners for any mod. It will also indicate if the banners in BannerCollector do not match the banners in any mod
Minor Changes:
However, to do all of this, I had to remove indexes and atlases and replace them with another method. The AI said that some of the newer mods have a different structure, so the old logic will not work there.
For now, I will keep using my own version. If I find anything, I will mention it. At the moment, I have not noticed any problems.
In general, this can be used as a backup option if you do not have the ability or the desire to update the mod.