Imported from bradjrenshaw/say-the-spire2 (
AGENTS.md). Install upstream withnpx skills add bradjrenshaw/say-the-spire2. Copyright stays with the author.
AI Agent Guidelines for SayTheSpire2
This file is an actionable review checklist for AI agents. It intentionally mirrors invariants from CLAUDE.md in imperative, checkbox form. When the two files conflict, CLAUDE.md is authoritative — read it for rationale and full architectural context.
Before Making Changes
Understand the scope
- Read CLAUDE.md's "Architectural Invariants" section before touching focus, events, speech, or Harmony patches.
- If the change touches multiple subsystems, consider whether side effects could affect unrelated screens (the shop regression from the focus refactor is a cautionary example).
Check if a plan is needed
- Simple bug fixes (one file, clear cause): proceed directly.
- New features, refactors, or changes to core systems (UIManager, FocusHooks, EventDispatcher, InputManager): plan first, get user approval.
Code Review Checklist
Error Handling & Null Safety
- Every
catchblock logs the exception — never use emptycatch { }. UseLog.Errorfor broken functionality,Log.Infofor expected fallbacks. - All multiplayer hooks gate on
IsMultiplayer()to avoid firing in singleplayer. - Build produces 0 warnings. Do not suppress warnings with
#pragmaor attributes. - Nullable references handled with
?.,is, or null checks — not!(except intentional reflection crash points). - Node/control lookups (
GetNodeOrNull,GetChild, reflectionGetValue) use null checks before access.
Focus System
-
SetFocusedControl/SetFocusedElementonly store state. No speech output, no buffer updates. -
UIManager.Update()is the only place focus announcements happen. - Pre-resolved elements passed to
SetFocusedControlare not overwritten byProxyFactory.Createfallback. OnlyScreenManager.ResolveElement(screen registry) can upgrade them. - New controls that don't extend
NClickableControlneed explicitFocusEnteredsignal connections (seeNSelectedHandCardHolder,NMerchantSlot,NCrystalSphereCellpatterns). - Disabled controls: verify
SetEnabledPostfixkeeps them focusable andRefreshFocusPostfix'sHasFocus()fallback announces them.
Speech & Messages
- No
SpeechManager.Outputcall usesinterrupt: true. - Focus-related announcements go through
UIManager.Update(), not directSpeechManager.Outputcalls. - All user-facing text uses
Message.Localized("ui", "KEY", new { ... })with keys ineng/ui.json. Never useMessage.Raw()with hardcoded English. -
Message.Raw()is only for game-provided text (card names, creature names,LocString.GetFormattedText()results,Title.GetFormattedText(), etc.). - Never wrap a
LocalizationManager.GetOrDefault()result inMessage.Raw()— useMessage.Localized()directly instead. - UIElement methods (
GetLabel,GetStatusString,GetTooltip,GetExtrasString) returnMessage?, notstring?. Call.Resolve()only at final output boundaries (bufferAdd, speech output, visual labels). - Event
GetMessage()returnsMessage?. UseMessage.Localized()for the format template,Message.Raw()only for game-provided names within it. - New localization keys follow the naming convention:
SECTION.KEY_NAME(e.g.,EVENT.CARD_PLAYED,RESOURCE.HP,LABELS.LOCKED).
Harmony Patches
- Use
HarmonyHelper.PatchIfFound()directly — do NOT create local wrapper methods. Do NOT usePatchAll. - Target method is non-virtual and declared on the target type (not inherited from a base class).
- Patch has error logging (try/catch in the postfix/prefix).
HarmonyHelper.PatchIfFoundhandles method-not-found and patch-failure logging automatically. - New patches are registered in the appropriate
Initialize(Harmony)method, not scattered. -
NCardHoldersubclass hooks: only patch those that overrideOnFocus. Check the decompiled source first. - All reflection lookups use
AccessTools.Field/Property/Method, nottypeof().GetFieldwithBindingFlags.
Events
- New event classes have
[EventSettings]attribute. They are auto-discovered — do NOT add manualRegister()calls toEventRegistry.RegisterDefaults(). - Events with creature sources set
Source = creaturein constructor and usehasSourceFilter: true. -
AllowCurrentPlayer/AllowOtherPlayers/AllowEnemiesflags match what the game visually shows to other players. Don't announce information the game doesn't display. - Power events: Decreased handler skips when
power.Amount <= 0(Removed event handles it). - Non-stacking powers (
StackType != Counter): don't show numeric amounts.
Multiplayer
- All multiplayer-specific hooks gate on
IsMultiplayer(). - Player identification uses
PlatformUtil.GetPlayerName(platform, netId)with try/catch fallback. - Local player checks use
LocalContext.IsMe(creature)orplayer.NetId == LocalContext.NetId. - Orb detection, card pile subscriptions, and similar per-player logic use
LocalContext.IsMeto filter to the local player.
Buffers
- Stable container/proxy references for screens with
OnUpdate()rebuilds (don't create newListContainer/ProxyCardobjects each frame). -
FollowLatestbuffers jump to last item on switch. -
AlwaysEnabledBuffersoverride on screens that need buffers to persist across focus changes (e.g., lobby buffer on character select).
Grid/Position
- Grid coordinates use
(x, y)cartesian order (column first, row second). - Verify focus neighbor wiring matches the visual/navigation order. Check if the game's card order is reversed.
Proxy Elements
-
ProxyFactory.Createhandles the new control type, or the caller provides a pre-resolved proxy. -
GetLabel(),GetStatusString(),GetTooltip()returnMessage?and handle null gracefully (the control or its children may not exist). - Game-provided text (titles, descriptions) wrapped in
Message.Raw(). Mod-generated labels/formats useMessage.Localized(). - Star costs use
GetStarCostWithModifiers()notCurrentStarCost(to reflect Void Form and similar modifiers). - Resource formats (HP, gold, energy, block, stars) use the
RESOURCE.*localization keys, not inline format strings.
Help System
- New screens implement
GetHelpMessages()with relevant text tips and control help. - Help text messages use
LocalizationManager.GetOrDefault()with keys fromui.json, not hardcoded English. - Control descriptions in
ControlHelpMessageuse localized strings. - Screen-specific messages are marked
exclusive: trueso they don't leak into child screens. - Multi-action controls (e.g., "Select Combatant 1-12") use the
params string[]constructor with all action keys. - Modal screens (
ModalScreen) provide appropriate help — generic confirm/cancel for dialogs, page navigation for tutorials.
Screens & Modals
- New game screens that use
NOverlayStackare registered inOverlayHookspush/remove. - Screens with dynamic content use state token polling in
OnUpdate()to rebuild when content changes. -
ModalScreenauto-removes when the modal node is freed (safety check inOnUpdate). -
ClaimAllActions()is used for overlay screens that should block all input (help, modals).
Installer (Rust)
- Version comparison uses
semvercrate, handles bothvandVprefixes. - New installation config fields are added to
InstallationConfigstruct,read_installation_config, andsave_installation_config. - Settings file retry dialog:
enable_mods_with_retryhandles missing settings.save. -
MOD_FILESinpaths.rsis updated when new files are added to the mod distribution.
Known Fragile Areas
These areas have historically broken due to unintended side effects. Test them after changes to core systems:
- Shop (merchant slots) — ProxyMerchantSlot relies on pre-resolved elements. Any change to UIManager.Update re-resolve logic can break it.
- Settings navigation — Entering/exiting subcategories depends on FocusContext path diffing and NavigableContainer.FocusFirst.
- Hand select screen — Selected card row uses FocusEntered signals, stable proxy cache, and per-frame OnUpdate rebuild.
- Crystal sphere grid — Custom FocusEntered signals, grid coordinates, and per-frame focus neighbor wiring.
- Disabled buttons — SetEnabled postfix + HasFocus fallback in RefreshFocusPostfix. Breaking either silently hides locked content.
- Card reward screens — Card holder order may be reversed from scene tree order. Check position announcements.
- Multiplayer lobby — LobbyBuffer binding, player list, embark/unready state transitions.
- Mouse hover during controller mode — CheckForMouseInput must be suppressed to prevent focus stealing.
Commit Guidelines
- Commit messages should describe the "why" not just the "what".
- If a fix addresses a regression, mention what caused the regression.
- Include
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>on AI-assisted commits. - Don't amend previous commits — create new ones.
- Don't push to remote unless the user explicitly asks.