Changelog
[2.11.0] - 2026-09-26
Section titled “[2.11.0] - 2026-09-26”2.11.0 (minor) — Mods can now tell which spec nearby players are playing, the moment they come near you, and know the buffs players already have when they arrive. Adds API for plugins (Abstractions 2.11.0).
- Mods can now see which spec a nearby player is playing as soon as they come near you, even in town before any fight. Combat Meter uses this to show specs straight away.
- Mods now know the buffs a player already has the moment they come near you, instead of only after those buffs change.
Developer notes
Section titled “Developer notes”- Spec from talent root buffs: each of the 18 specs has exactly one root talent buff (“<Spec> Spec”); the map is derived at runtime from TalentStage → TalentTree → TalentTable effects (18 pinned fallback constants). Buff-derived spec is authoritative over cast inference; the swap gap holds the previous spec (expires after 10 s); a snapshot with no root clears it; Battle Imagine transform ids are not class changes. Evidence: devkit
docs/recon/spec-from-public-wire-data.md(2,111/2,111 snapshots; owner-confirmed live). - New API (additive):
ICombatSpec.TryGetTalentSpec(EntityId, out int);CombatEvent.SpecChanged(TimestampMs, TargetId, OldSubProfessionId, NewSubProfessionId, FromTalent)(once per real change, never for the swap gap);CombatEvent.EntityBuffsSeeded(TimestampMs, TargetId, Buffs)—SyncNearEntities/ EnterScene now decodeEntity.buff_infosand replace the entity’s buff set silently, one seed event per entity (live deltas unchanged; a later delta for a seeded buff arrives as Refreshed, its expiry as Removed).BuffsFor/LocalBuffsreturn cached read-only snapshots. STELLAR_WIRECAPdebug capture is now lossless for large frames (node budget 4096 → 262144, hex for short opaque byte fields, raw base64 when a frame still truncates).
[2.10.0] - 2026-09-26
Section titled “[2.10.0] - 2026-09-26”2.10.0 (minor) — Lets plugins save the loadout you’re wearing into another loadout slot through the game’s own Save, and tell whether you have unsaved loadout changes. Powers Loadout Switcher’s new copy button. Adds API for plugins (Abstractions 2.10.0).
- Plugins can now copy the loadout you’re wearing into another of your loadout slots, using the game’s own Save, so the game confirms it with its usual “Loadout saved successfully!” message.
Developer notes
Section titled “Developer notes”- New
ILoadoutSaveonIPluginServices(LoadoutSave):SaveCurrentToAsync(index)runsweapon.AsyncSaveRolePlan(index)on the main-thread drain (refuses without dispatch when the target is the worn plan, unknown, or a switch/save is in flight; 8 s timeout; re-arms the SyncProjectList refresh on success).HasUnsavedChanges= the game’sCheckRolePlanIsChange(), carried as anUNSAVEDrow on the existing LiveStateChunk/RefreshChunk (event-driven, no timer). Measured on the owner’s client:SaveProjectaccepts a non-current plan id and copies the LIVE setup (class, gear, modules, skills, talents, Battle Imagines); the plan name is not copied.
[2.9.0] - 2026-09-25
Section titled “[2.9.0] - 2026-09-25”2.9.0 (minor) — The party meter gains shields, buffs & debuffs, and voice controls; plus a dungeon-teleport archive fix and a band-instrument tone relay. Adds render/data API for plugins (Abstractions 2.9.0).
- The party meter can now show each member’s shield (a band over their HP bar) and their active buffs and debuffs as icons.
- You can click the voice icon on your own meter row to cycle your team-voice mode, and open a party-voice menu from the meter.
- A dungeon fight is no longer split into two separate records when you get teleported within the same instance mid-fight.
Developer notes
Section titled “Developer notes”- MeterRowData status-effect channel (DebuffSlot + IsBuff, Debuff0..11, DebuffColumns, DebuffOverflow, ShowDebuffs, DebuffCellSize, BarShieldFraction, OnDebuffClick); WindowBuilder renders the 2-row colour-coded block (variable columns, per-cell click). IGameDataCombat.AllBuffs() enumerates the buff table; GameTextureElement.Fill stretches an icon to its box. Shield via AttrShieldList → EntityVitals → HP-spine band; InstrumentToneRelayPatch relays band tone listener-side.
[2.8.5] - 2026-09-23
Section titled “[2.8.5] - 2026-09-23”2.8.5 (patch) — Settings → Hotkeys now lists numbered actions in natural order, so “Apply Loadout 10” sits after 9 instead of between 1 and 2. Cosmetic; binary-compatible, no plugin rebuild.
- In Settings → Hotkeys, an action numbered 10 or higher now appears in the right place in the list (after 9), instead of being sorted as if it came right after 1.
Developer notes
Section titled “Developer notes”FrameworkVersion.Value→ 2.8.5.HotkeysPanelsorts within a plugin group byNaturalOrder.Compare(numeric-aware) instead of an ordinalstring.Compareon the action id — digit runs compare as integers, soloadout.apply.10sorts afterloadout.apply.9. Bindings are unaffected (keyed by id, unchanged). +8NaturalOrderTests.
[2.8.4] - 2026-09-22
Section titled “[2.8.4] - 2026-09-22”2.8.4 (patch) — Loadout Switcher Deep-Slumber fixes: switching builds now rebuilds the whole Psychoscope tree reliably, and shared factors are no longer taken from your other build unless there’s truly no other way. Application-only; binary-compatible with every existing plugin, no plugin rebuild.
- Switching between builds now rebuilds the whole Psychoscope tree properly instead of sometimes leaving part of it locked.
- When you switch to a build, a Phantom Factor it shares with another build is no longer pulled out of that other build if you already have a spare, or if the build you’re switching to already has it in place.
Developer notes
Section titled “Developer notes”FrameworkVersion.Value→ 2.8.4. Application + Infrastructure only; no public API change, so no plugin rebuild.DeepSlumberReconciler.FreeForeignSharedFactorsnow frees a cross-loadout copy only up to a real deficit (demand − target-area reset/replace refunds − free bag copies); a new Application-internalIFactorBagProbe(PandaInventoryProbe.ReadFactorBagCounts, over the existingCharSerialize.ItemPackagewalk) supplies the free-inventory counts. Elaina 2026-09-22.- Tree rebuild:
DeepSlumberService.ActivatePhaseAsyncconverges anchor activation on the prerequisite refusal5126(ErrTalentPreTalentNodeNotActivated) — each pass requeues the blocked anchors until the tree self-orders parent-before-child; a class round-trip no longer half-builds and locks the tree. Validated in-game (18 recovered 5126 across 9 rebuilds). - Reactive backstop:
DeepSlumberService.SocketPhaseAsyncfrees a foreign copy and retries when — and only when — a socket is refused7561(ErrSeasonTalentIntermediateNodeClassNumExceeded; a copy in an inactive tree counts toward the per-type limit, which a bag spare cannot clear). So a foreign copy is freed only when the bag lacks it (predictive) or the game itself refuses (reactive).DeepSlumberWriteCode.PreTalentNodeNotActivated(5126) /ItemClassNumExceeded(7561).
[2.8.3] - 2026-09-22
Section titled “[2.8.3] - 2026-09-22”2.8.3 (patch) — new character-portrait controls for mods (hide the weapon, custom portrait lighting) and a per-bar label-colour option for mod HUDs. Additive API; binary-compatible with all existing plugins (EntityInspector is rebuilt alongside).
- Mods that show a 3D character portrait (such as Entity Inspector) can now hide the character’s weapon and light the portrait with a custom, fixed look that doesn’t change with wherever your character happens to be standing. The portrait keeps its lighting entirely to itself, so characters in the world around you look exactly the same as before.
- Mods can now set the colour of the small number or label drawn inside a HUD bar, so it stays readable over any bar fill colour.
Developer notes
Section titled “Developer notes”Stellar.Abstractions:IEntityPortraitgainsShowWeapon(bool; default hidden — matches the prior always-hide behaviour) andLightPreset(int;-1= Scene/dynamic,0..4= custom profiles rebased on the real HDR scene scale). The camera controls (Orbit/Zoom/Pan/SetViewport) are factored onto a new baseIEntityPortraitView(IEntityPortrait : IEntityPortraitView) to keep each interface under the STELLAR0005 8-member cap. This is additive for consumers of the aggregate interface, but moving members to a base type changes their declaring interface — a plugin that CALLS the camera methods and is not recompiled would hitMissingMethodException; the only consumer is EntityInspector, released in tandem.- World isolation: creature lighting is a global shader register shared with in-world characters. Infrastructure (
PortraitModelHost.Lighting/PortraitCmdRenderer) captures the 5 creature-light globals (Shader.GetGlobalVector/MatrixofZModel2RTLight’s prop-id handles) before the portrait’s CommandBuffer draw and restores those exact values (cmd.SetGlobalVector/Matrix) after, so the global state ends byte-identical to what the world left it regardless of frame timing. Weapon-hide excludes the subject’s loosech_wp_*MountComp GameObjects by name from the custom draw and re-asserts on a throttle (the weapon streams in after the body). Stellar.Abstractions:BarElement.LabelColor(ColorRgba?) — a per-bar override for the inside-label colour that wins over the theme’s muted reskin registration;WindowBuilder.Previewhonours it. Defaultnullkeeps every existing bar byte-identical.FrameworkVersion.Value2.8.3. Consumed by StellarEntityInspectorPlugin (separate PR).
[2.8.2] - 2026-09-21
Section titled “[2.8.2] - 2026-09-21”2.8.2 (patch) — fixes stat readouts stuck on “—” for a whole play session after a fast login. Infrastructure/Application only; no API change, binary-compatible with all existing plugins.
- Fixed tracked stats showing “—” instead of a number for the rest of the session. If you got into the world quickly, the mod could read your character before the game had filled in its stats, decide those stats did not exist, and never look again — Stat Inspector’s mini HUD then showed a dash on every row until you restarted the game. It now waits until your stats are actually there before giving up on one, and re-ticking a stat in the picker (or logging out and back in) re-checks it straight away. A stat the game only starts reporting a little later is picked up on its own within about a minute; a stat your character genuinely does not have — one the game never reports at all — keeps showing a dash, which is correct.
Developer notes
Section titled “Developer notes”- Root cause:
PandaPlayerStatsProbe._attrUnreadablewas a process-lifetime negative cache with no invalidation. The local-player entity exists for several ticks before its FightAttr sheet is populated, so a sampling pass taken inside the login window missedTryGetAttr<long>,<int>AND<float>for every subscribed id, andProbeAndMemoizeAttrTypelatched them all.PlayerStatsService.ClearSessiondid not touch probe state andSubscribehad no reset hook, so only a client relaunch cleared it. All-or-nothing by construction — which is why the owner saw eight of eight rows dash (2026-09-21) and why the 2026-09-09 boots, which won the same race, latched only the genuinely absent 11760/11980. - Fix: the decision is now the pure, BCL-only
AttrReadabilityMemoinStellar.Application(unit-pinned inStellar.Application.Tests), one instance shared byPlayerStatsServiceandPandaPlayerStatsProbefrom the composition root. The probe reports each read as anAttrReadOutcomeinto a reused per-tick buffer andRecord(pass, sheetReady)commits the misses only when the attribute sheet is known to be populated — either because an id in the same pass read live, or because the caller’s EXPLICIT readiness signal says so. An all-miss pass before readiness means “the sheet is not ready”, never “these ids are absent”. Readiness is deliberately not inferred from the pass: the pass covers only the plugin-subscribed union, and a CombatMeter-only client subscribes exactly 11760 + 11980 — both genuinely absent from the wire sheet — so every pass there is all-miss forever and those ids would never latch, re-probing three reflective invokes each per tick for the process lifetime.PandaPlayerStateProbe.IsAttrSheetPopulated(entity)answers it from an always-present attribute (max HP, then level — the same evidenceIsUsefuluses) on the SAME entity the pass sampled, so it cannot disagree with the pass and goes false again during a mounted blackout;AttrReadabilityMemo.NeedsReadinessSignal(pass)keeps that read to the ambiguous window only (all-miss AND holding a first-read probe), so steady state pays nothing. A miss through an already-locked storage-type memo (mounted blackout) is readiness evidence only and can never latch.SubscribecallsForget(id);ClearSessioncallsClear(). OneSTELLAR_DIAGNOSTICS-gated line inPandaPlayerStatsProbe.Diagnostics.csreports each memo decision (the previous silence is what made this a full investigation). Reads stay lock-free (copy-on-write) so the per-tick sampler is unaffected.FrameworkVersion.Value2.8.2. Dossier: devkit.superpowers/sdd/statinspector-dashes-investigation.md. - An “unreadable” verdict is a BOUNDED negative cache, not a process-lifetime one:
AttrReadabilityMemo.RetryAfterTicks= 60 s._unreadableis now a copy-on-writeDictionary<int, long>(id → the clock reading that latched it);IsUnreadable(id, nowTicks)is true only inside that window, andRecord(pass, sheetReady, nowTicks)re-stamps a miss that misses again (the window restarts) or drops the verdict outright when the id finally reads — the probe’s storage-type memo (_attrPrefersLong/_attrFloat) is untouched either way.PandaPlayerStatsProbe.TrySamplereads the clock ONCE per pass (Environment.TickCount64 × TimeSpan.TicksPerMillisecond— monotonic, so a wall-clock adjustment can neither freeze nor shorten a window) and hands it down;Stellar.Applicationstays pure. Measured trigger: the 2026-09-22 TEST-client run latched11951(versatility %, a SELECTED id on that client) alongside the genuinely absent 11760/11980 — a hit pass at login condemned an attribute the game had simply not published yet, and with a permanent verdict the row dashed until relaunch. Cost of the retry: ~3 reflective invokes per absent id per MINUTE (pinned: no re-probe inside the window across 3 599 consecutive passes), versus 3 per tick without any memo. Two logging rules follow from the expiry, both required — (1) the UNGATEDattr {id} unreadable …line is now first-sighting-only per id (_unreadableLogged), else every user would collect one line per absent id per minute for the whole session; (2) theSTELLAR_DIAGNOSTICS-gatedmemo: latched=… via=…line’s expected cadence is once per id set per window, not once per process — more often means verdicts are being dropped early, and an id that stops appearing has started reading. Four new pins inStellar.Application.Tests:a_latched_id_is_re_probed_after_the_ttl_and_not_before,a_re_probe_that_still_misses_re_latches_and_restarts_the_window,the_anti_spam_bound_holds_inside_the_window,a_re_probe_that_hits_clears_the_id(PR #88 review, fix 3). - A pass taken while the attribute sheet is DARK (all-miss and
sheetReadyfalse) no longer discards its evidence wholesale:AttrReadabilityMemo.RefreshExistingrestarts the window of every fully-probed miss that ALREADY holds an entry, while an id with no entry still latches nothing (the F1 rule is untouched, andLatchedstays empty — no verdict is new). Without it the expiry introduced the mirror of the bug it fixed: an id whose window lapsed during a blackout kept its already-expired stamp, soIsUnreadableread false and the probe re-probed it — three reflective invokes plus up to three Unityarr type errlines — on EVERY tick until the sheet answered, instead of once a window. The gatedmemo: skipped-all-miss-pass …line now endslatching nothing; existing verdicts held; the refreshed ids are deliberately not listed (the line is deduped per streak, the refreshes recur per window). Three new pins:a_not_ready_pass_refreshes_an_existing_verdict_but_never_creates_one(RED before the fix),the_retry_window_ends_at_exactly_sixty_seconds(age < RetryAfterTicks, so 60 000 ms is already expired) anda_clock_reading_behind_the_stamp_reads_as_expired(PR #88 review, fix 4).
[2.8.1] - 2026-09-13
Section titled “[2.8.1] - 2026-09-13”2.8.1 (patch) — fixes a startup crash on the Steam version of the game when a mod that draws its own overlay is left enabled. Infrastructure/boot-order only; no API change, binary-compatible with all existing plugins.
- Fixed the game crashing on startup on the Steam version of Star Resonance when a mod that draws its own overlay (such as Minimal Nameplate) was left enabled. Turning it on while already in-game had always worked; only having it enabled before launch could crash. (The standalone launcher version was not affected.)
Developer notes
Section titled “Developer notes”- The 2.7.4 Steam Class::Init injection guard (
Il2CppClassInitFix.EnsureSeeded) was armed only insideUnityTickHost.Install(), whichBootstrapPlugin.OnHotUpdateReady()calls AFTERLoadUserPlugins(). That protected the framework’s own first IL2CPP type injection (StellarTicker) but not a user plugin that injects a type in its constructor. Minimal Nameplate, when enabled, callsClassInjector.RegisterTypeInIl2Cpp<LateUpdater>()in its ctor duringLoadUserPlugins— the real first injection in the process — before the seed was armed, hitting the unguardedFindClassInitand fatally crashing theStarSEA_STEAMbuild (0x80131506). Fix: callEnsureSeeded(log)immediately beforeLoadUserPlugins(log); it is_seeded-guarded/idempotent, never throws, and needs onlyGameAssembly.dllexports (available before plugins load). The existingUnityTickHost.Installcall becomes a harmless backstop.FrameworkVersion.Value2.8.1; no-op on the standalone build. Root-cause dossier: devkitdocs/recon/steam-client-classinit-injection.md.
[2.8.0] - 2026-09-09
Section titled “[2.8.0] - 2026-09-09”2.8.0 (minor) — damage-meter bar numbers can now be drawn plain, with an outline, or on a soft shadow; the meter plugin exposes the choice. Additive API; binary-compatible with all existing plugins.
- Mods can now choose how the numbers drawn on a damage-meter bar stay readable over the bar colour: plain text, the same thin dark outline the stat HUD uses, or a soft dark shadow under the numbers. CombatMeter 2.10.0 offers this as an Appearance option, useful with its new class-coloured bars (yellow, green and orange fills made plain white numbers hard to read). Nothing changes until a mod asks for it.
Developer notes
Section titled “Developer notes”Stellar.Abstractions:MeterRowData.LabelStyle(MeterLabelStylePlain = 0 / Outline / Shadow). Default Plain keeps every pre-2.8 plugin’s rows byte-identical.WindowBuilder.MeterRowbuilds all three treatments per row and the binding toggles them from the row data: Outline = the framework’s existing readability outline (TextElement(Shadow: true)’s recipe, StatInspector’s stat HUD: uGUIOutline, black @ 0.85,effectDistance (1.1, -1.1); factored intoUGuiPrimitives.AddReadabilityOutlineand shared by both call sites — the 1.1 px sub-pixel offset is what keeps a small bold glyph’s halo smooth; exactly 1.0 px stair-steps); Shadow = twoRawImagefades under the values (64 px × font scale, plateau + SmoothStep ramp, mirrored for the right end) whose alpha lerps 0.12 → 0.52 with the fill’s relative luminance (LabelFadeTint, retinted whenever the fill colour changes). Measured trigger: with CombatMeter 2.10.0’s class palette the fill can be#f2dc3a/#5ccf36/#f5761c, where white text has a WCAG contrast of 1.4–2.0. Owner 2026-09-09 after four in-game rounds: “it should be option for players: normal text, outline, smooth shadow”.FrameworkVersion.Value2.8.0. Design record: devkitdocs/superpowers/specs/2026-09-09-game-class-palette-and-gauge-color-design.md.
[2.7.4] - 2026-09-09
Section titled “[2.7.4] - 2026-09-09”2.7.4 (patch) — fixes a mod-caused startup crash on the Steam version of the game. Infrastructure-only; no API change, binary-compatible with all existing plugins.
- Fixed the mod crashing the game on startup on the Steam version of Star Resonance. (The standalone launcher version was not affected.)
Developer notes
Section titled “Developer notes”- Il2CppInterop’s IL2CPP type injector (
InjectorHelpers.FindClassInit) finds nativeClass::Initby scanningGameAssembly.dllfor a hardcoded x86-64 call-site byte-signature. The SteamStarSEA_STEAMbuild — a separate SKU compile, identical Unity 2022.3.59f1 / IL2CPP metadata v31.1 — has codegen that scan mis-resolves to a bad pointer, hard-crashing the CLR (0x80131506, an uncatchableExecutionEngineException) at the framework’s FIRSTClassInjector.RegisterTypeInIl2Cpp(StellarTicker). Reproduced identically on Il2CppInterop 1.5.1 and 1.5.3, so a BepInEx bump does not fix it. - Fix: new
Il2CppClassInitFix.EnsureSeeded(Infrastructure), called at the top ofUnityTickHost.Installbefore the first injection, reflectively pre-seeds the process-globalInjectorHelpers.ClassInitwith Il2CppInterop’s own documented substitute export (il2cpp_class_has_references) so theClassInit ??= FindClassInit()guard skips the broken scan. Always-on (all builds): harmless where the scan already worked — the substitute is the library’s own fallback for injected classes — verified in-world on the standalone with all injections (StellarTicker,WindowInteractionTicker,ChartGraphic) succeeding. Idempotent, never throws.FrameworkVersion.Value2.7.4; no API change. Root-cause dossier: devkitdocs/recon/steam-client-classinit-injection.md.
[2.7.3] - 2026-09-09
Section titled “[2.7.3] - 2026-09-09”2.7.3 (patch) — stable release of the 2.7 line. Identical code to the 2.7.2 testing build; the number moves only because 2.7.2 was already published to the testing channel and a taken version is never re-minted. No API change; binary-compatible with plugins built against ≤2.7.2.
- Stable release of the 2.7 line. Same code as the 2.7.2 testing build — the buff and stat recording the logs site’s experimental raid-contribution view needs (Own / Given, and the Buffs tab), plus everything from 2.6.4. Nothing else changes for players coming from 2.6.4 beyond that recording.
Developer notes
Section titled “Developer notes”- Promotion only — no source change. The merged tree is byte-identical to
feat/attr-changed-event@73f349e(the build on the testing channel since 2026-09-09);origin/main(2.6.4,d6a51db) contributed no content because the branch had already merged thev2.6.4tag lineage.FrameworkVersion.Value2.7.3. - 2.7.2 cannot be re-used for the stable publish:
Stellar-2.7.2.zipis already on the CDN under the bundle-immutability guard, and a GitHub pre-release taggedv2.7.2already exists — so the stable promotion rides a patch bump. What the 2.7 line actually adds is under the 2.7.2 / 2.7.1 / 2.7.0 entries below.
[2.7.2] - 2026-09-09
Section titled “[2.7.2] - 2026-09-09”2.7.2 (patch) — the testing-channel framework now carries the 2.6.3 and 2.6.4 fixes. Additive; binary-compatible with plugins built against ≤2.7.1.
- Mods can now tell which of your characters you are currently playing, so features that save things per character keep each character’s data separate.
- Fixed moved HUD elements (quest tracker, boss health bar, and others) jumping to the wrong position after you change your screen resolution.
- Fixed the boss health bar drifting out of place after you repeatedly hide and show it in Settings.
- HUD elements you have repositioned now fall back to their normal in-game position at a resolution you have not customized yet, instead of getting stuck off to one side.
Developer notes
Section titled “Developer notes”- Merge of
v2.6.4(d554236, built onv2.6.3) into the 2.7.x line (feat/attr-changed-event, on top of 2.7.1 = 2.7.0 + v2.6.2);FrameworkVersion.Value2.7.2. Bringslong IPlayerIdentity.CharIdand the native game-UI repositioning fixes; the 2.7.0CombatEvent.EntityAttributesChangedgroundwork is unchanged. Details under the 2.6.4 / 2.6.3 entries below.
[2.7.1] - 2026-09-08
Section titled “[2.7.1] - 2026-09-08”2.7.1 (patch) — the testing-channel framework now carries the 2.6.2 loadout fix. No new API; binary-compatible with plugins built against ≤2.7.0.
- Switching between two loadouts that share a Deep-Slumber (Psychoscope) factor no longer stops partway with “Deep-Slumber partly applied” — the 2.6.2 fix, now included in the testing channel.
Developer notes
Section titled “Developer notes”- Merge of the
v2.6.2hotfix (hotfix/deepslumber-cross-loadout-2.6.2, off v2.6.1) into the 2.7.0 line (feat/attr-changed-event);FrameworkVersion.Value2.7.1; no source change beyond the merge and the version constant.
[2.7.0] - 2026-09-05
Section titled “[2.7.0] - 2026-09-05”2.7.0 (minor) — no player-visible change yet; internal groundwork for an upcoming raid-contribution DPS (rDPS) feature. Additive, binary-compatible with plugins built against ≤2.6.1.
Developer notes
Section titled “Developer notes”- CombatEvent.EntityAttributesChanged: a player’s numeric attributes changed (one event per wire packet, stamped like the packet’s buff events) — enables the CombatMeter stat-sheet track for rDPS.
- Entity attribute capture now stores a genuine zero value (single 0x00 varint) instead of dropping it; non-varint payloads are still skipped.
[2.6.4] - 2026-09-09
Section titled “[2.6.4] - 2026-09-09”2.6.4 (patch) — new plugin API so mods can tell which of your characters is logged in. Additive interface member; binary-compatible with plugins built against ≤2.6.3. Requires updating LoadoutSwitcher + Wardrobe to their per-character builds to take effect.
- Mods can now tell which of your characters you are currently playing, so features that save things per character keep each character’s data separate. This makes the LoadoutSwitcher and Wardrobe updates possible: your Deep-Slumber loadout bindings and your saved outfits are now remembered per character instead of one shared set that switching character would overwrite.
Developer notes
Section titled “Developer notes”- Adds
long IPlayerIdentity.CharId(Stellar.Abstractions/Services/IPlayerState.cs): the local character’s stable id, char-record-backed so it is known before the world entity syncs (likeName/Level/Profession),0when not yet resolved; NOT gated byIsAvailable.PlayerStateServiceexposes the already-tracked_identityCharId(fed fromIPlayerStateProbe.CharId, cleared to 0 on logout / character change) — no probe change.MockPlayerStatereturns a fixed id for tests/sandbox. - Additive (a new interface member + getter): binary-compatible with plugins built against ≤2.6.3, but a plugin that calls
CharIdrequires this framework — so LoadoutSwitcher and Wardrobe bump theirminframework to 2.6.4. Abstractions/Application surface only; built on v2.6.3 (native-UI layout fixes), which also carries the 2.6.2 Deep-Slumber cross-loadout factor-move fix. - Pinned by
PlayerStateServiceidentity testCharIdReflectsIngestedIdentityAndResetsOnLogout(CharIdreflects the ingested identity, resets to 0 on logout / character change).
[2.6.3] - 2026-09-08
Section titled “[2.6.3] - 2026-09-08”2.6.3 (patch) — moved HUD elements stay where you put them across resolution changes and Settings toggles. Infrastructure/Application-only, binary-compatible with plugins built against ≤2.6.2.
- Fixed moved HUD elements (quest tracker, boss health bar, and others) jumping to the wrong position after you change your screen resolution.
- Fixed the boss health bar drifting out of place after you repeatedly hide and show it in Settings.
- HUD elements you have repositioned now fall back to their normal in-game position at a resolution you have not customized yet, instead of getting stuck off to one side.
Developer notes
Section titled “Developer notes”- All in the native game-UI repositioning path (
PandaHudAdapter/NativeUiService/INativeUiAdapter). TheSetRectidempotent guard was reworked to key on(target, anchoredPosition, curatedSize)equality, fixing three defects plus a new position-only fallback: (1) reflow-freeze — a moved HUD element dropped to the bottom after a resolution round-trip (translate applied against a transient/collapsed size, then froze when the element reflowed); (2) toggle-ratchet — the boss HP bar crept left on repeated hide/show in Settings; (3) off-edge churn — elements whose curated rect sits past a screen edge re-applied every tick (harmless, now skips); (4) unconfigured-resolution fallback — newINativeUiAdapter.RestoreOriginalPosereturns a moved element to the game’s default position at a resolution with no saved layout (previously it stayed stranded at the prior resolution’s offset). Scope:src/Stellar.Infrastructure/Game/PandaHudAdapter.cs,src/Stellar.Application/Services/NativeUiService.cs,src/Stellar.Application/Abstractions/INativeUiAdapter.cs,tests/Stellar.Application.Tests/NativeUi/NativeUiServiceTests.cs.INativeUiAdapteris an Application-layer outbound interface, not the public plugin surface — no plugin API or behavior change outside native-UI positioning. Compile clean; 1394/1394 tests pass (incl. a new unconfigured-resolution fallback test); all four cases in-game validated by the owner. Temporary round-trip diagnostics were added during development and stripped before release (repo grep clean).
[2.6.2] - 2026-09-08
Section titled “[2.6.2] - 2026-09-08”2.6.2 (patch) — switching loadouts now moves a shared Deep-Slumber factor instead of half-applying. Application-only, binary-compatible with plugins built against ≤2.6.1.
- Switching between two loadouts that share a Deep-Slumber (Psychoscope) factor no longer stops partway with “Deep-Slumber partly applied”. Because a factor exists only once, if the loadout you are leaving is still holding one the new loadout needs, the switch now takes it out of the old loadout first and puts it into the new one — so the switch completes. Nothing is reset and it costs no season points; the factor simply moves, the same as if you had moved it by hand.
Developer notes
Section titled “Developer notes”DeepSlumberReconcilergains a foreign-free pass (FreeForeignSharedFactors): after the per-target-area reconcile, any factor the target wants that is still socketed in a CURRENT-season area the target does not bind is unsocketed (into the existing enable→reset→unsocket→activate→socket phase, ahead of every socket), so the target socket finds it in the bag instead of failing game code 7561 (“still socketed elsewhere”). Scoped to the current season (currentLine), to areas OUTSIDE the target (AreaIdnot bound — the per-area logic already reconciles the target’s own areas, so re-touching them would double-emit), and to items the target actually wants (never disturbs the other loadout’s non-shared factors); deduped by (AreaId, node) so a node id repeated across different foreign areas never drops a wanted factor. Unsocket is free + non-consuming, so this never spends reset currency. Confirmed in-game on the owner’s MAIN (2026-09-07):worldProxy.UnInstallItemToMiddleNode({nodeId})returns code 0 against a NON-active line’s node — unsocket needs no active line, superseding the 2026-08-24 “area-relative” inference — and a real Smite↔Tank switch (12 factors) reads[LoadoutSwitcher] DS apply -> Success, with every DS op code 0 and no 7561/7555. Pinned byDeepSlumberReconcilerTests(cross-loadout free, two-foreign-areas-same-nodeid, replace+foreign coexistence, prior-season-not-freed, not-wanted-left-alone). Toir report via the owner, 2026-09-07.
[2.6.1] - 2026-09-05
Section titled “[2.6.1] - 2026-09-05”2.6.1 (patch) — “no weapon skin” is saved as no weapon skin again. Infrastructure-only, binary-compatible with plugins built against ≤2.6.0.
- Saving an outfit while your weapon wears no skin now records “no skin”. It used to record whichever skin the wardrobe was showing you at the time, so applying that outfit later put that skin back — even after you had changed to a different weapon, and even though you had never chosen a skin. Now an outfit saved with no weapon skin always shows your current weapon’s own look, whatever weapon you are holding. Outfits you already saved keep the skin they recorded until you use Update on them.
Developer notes
Section titled “Developer notes”- Capture-side half of the
SkinId 0⇄ origin contract. The game has no “skin 0”: the Weapon Skin tab builds its tiles withisEmpty = value.Original == 1and marks worn the tile whoseId == weaponSkillSkinVm:GetWeaponOriginSkinId(curProfessionId)(fashion_weapon_skin_select_view.lua:116-120), so picking ⊘ sendsUseProfessionSkin(pid, <that origin row id>)and the server stores a CONCRETEWeaponSkinTablerow whose look happens to be the weapon’s native one. Measured on the owner’s report: stored outfit 9 heldWeaponProfessionId=5, WeaponSkinId=7350002= “Mirrorlight Ring”,ProfessionId 5,Original: 1— and profession 5 has fourOriginal==1rows (7350002, 7350006, 7351061, 7351062), one per weapon family, so the raw id pins the outfit to the weapon it was saved with.PandaFashionProbe.WeaponCaptureLuanow normalises a worn skin equal toGetWeaponOriginSkinId(cur)back to0, wherecurisCharSerialize.professionList.curProfessionId— exactly the idprofession_vm.GetContainerProfessionreturns, which that VM method requires to match or it returns nil. Both consumers already resolved0→ origin at USE time (BuildWeaponSkinChunkfor apply, mirroringAsyncUseProfessionSkin:199;PandaWardrobePreviewProbe.WeaponSkinSourceLuafor the 3D preview), so only the producer was wrong;ParseWeaponSkinand the"<pid>:<skin>"global format are unchanged, and theIWardrobe.GetWornWeaponSkinXML doc now states why0is the honest value. The origin lookup sits in its OWNpcallnested inside the weapon block’s, so a nil VM, a throwingGetVM, a throwingGetWeaponOriginSkinIdor an origin that resolves0all keep the RAW id rather than losing the capture. Verified against real Lua 5.3 with stubbedZ.ContainerMgr.CharSerialize+Z.VMMgr.GetVM('weapon_skill_skin'), 9/9 cases: worn 7350002 / origin 7350002 →"5:0"; worn 7350003 / origin 7350002 →"5:7350003"; origin call throws →"5:7350002";UseSkinIdnil →"5:0"(no VM call at all); plus VM-nil, GetVM-throws, origin-0, float-typedUseSkinIdand no-current-class. Pinned byWardrobeWeaponSkinTests.CaptureChunk_ReportsTheCurrentWeaponsOriginSkin_AsNoSkin. Owner report 2026-09-05, after the 2.6.0 release.
[2.6.0] - 2026-09-05
Section titled “[2.6.0] - 2026-09-05”2.6.0 (minor) — saved outfits can now carry your weapon skin, and the outfit preview shows both head accessories. Additive, binary-compatible with plugins built against ≤2.5.0.
- Plugins can now read the weapon skin your current class is wearing and switch it for you, through the same game action as the Wardrobe’s Weapon Skin tab. The Wardrobe plugin uses this to save and re-apply your weapon skin together with an outfit.
- The outfit 3D preview now shows the weapon skin a saved outfit carries, so you see the whole look before you switch. Weapon skins belong to one class, so the preview shows it when the outfit’s weapon skin is for the class you are currently playing — otherwise the weapon is left as it looks now.
- The outfit 3D preview no longer shows two weapons at once. When a saved outfit carried a weapon skin, the preview drew that skin and the weapon you are currently holding; it now shows only the saved one, the way the game’s own wardrobe and shop previews do. Outfits saved before weapon skins existed still preview with the weapon you are wearing.
- The outfit 3D preview now shows both head accessories at once. An outfit with two head pieces used to preview only the second one — the first appeared for a moment and was then replaced.
- No more freeze when a plugin saves its settings. Saving an outfit in the Wardrobe used to lock the game up for about a third of a second and then stutter for a few seconds afterwards — the more you had saved, the worse it got. Saving is now instant, however many outfits you keep.
- Switching to the loadout you are already wearing no longer re-equips everything. Pressing it a couple of times in a row used to freeze the game for seconds and then stutter; it now does nothing but tell you that loadout is already on.
- On-screen notices no longer hitch the game when several arrive at once. Saving a few outfits in a row, or tapping a loadout hotkey repeatedly, used to drop frames for every notice it put on screen; notices now reuse the banner that is already up, and a notice repeating word-for-word while its own copy is still visible is left alone instead of stacking.
- Loadout switches now respect the game’s own three-second switch cooldown, exactly like the in-game dropdown does: a second press right after the first is refused — with the game’s own “you switched too recently” message — instead of piling another full re-equip on top of the one still running.
Developer notes
Section titled “Developer notes”IWardrobegainsGetWornWeaponSkin()→WardrobeWeaponSkin(ProfessionId, SkinId)(read fromCharSerialize.professionList.professionList[curProfessionId].UseSkinIdin the same capture chunk as the outfit) andApplyWeaponSkinAsync(professionId, skinId)(drivesWorldProxy.UseProfessionSkinexactly asweapon_skill_skin_vm.AsyncUseProfessionSkindoes: skin 0 → the class’s origin skin,OnWeaponSkinChangedispatch on ok; shares the single in-flight slot withApplyAsync, so await the outfit before sending the skin). Weapon skins stay OUT of the outfit region map (WardrobeRegions.Allunchanged) — they are a per-class game system. Pinned byWardrobeWeaponSkinTests+WardrobeServiceTests.PandaWardrobePreviewProbenow stamps eachSingleWearData.SlotIDwith the piece’sFashionRegion— the shape of the game’s ownfashion_vm.GetFashionWearList(data.SlotId = region). SlotID routes head pieces to their mount (713 → HeadWear, 718 → HeadWear2);SlotID=0put both on one mount so the second overwrote the first. Pinned byWardrobePreviewChunkTests. Discord report 2026-09-03.- Weapon-skin preview:
WardrobeRegions.WeaponSkinPreview(731) is a preview-only key onIWardrobePreview.Show’s outfit map —IWardrobe.ApplyAsyncignores it and it stays out ofWardrobeRegions.All(still 14). A weapon skin can never travel inEWearFashion(the game’s ownfashion_vm.GetFashionWearListskips 731), soPandaWardrobePreviewProbekeeps 731 out of theSingleWearDatalist and instead re-skins the social data the model is built from:socialData.professionData.weaponSkin = skin, written beforeZ.ModelManager:GenModelByLuaSocialData.SocialData.professionDatais the{profession_id, weapon_skin}block the server fills forSocialDataType.SocialDataTypeWeapon(const_value.lua:117), andsocialDatais a plain Lua table (world_proxy.GetSocialData:21831returnspb.decodeoutput with camelCase keys), so the field is directly writable. The display-override attr (m:SetLuaIntAttr((Z.ModelAttr).EModelDisplayWeaponSkinId, skin)) is still emitted last as a secondary, but it is NOT the mechanism: all three game call sites dress the cached PLAYER model (GetCachePlayerModel—fashion_system_view:831,shop_fashion_sub_view:300,competency_rating_main_view:253), never a social-data model, and the owner’s in-game test showed the preview keeping the live weapon for every outfit. The injection alone renders TWO weapons (owner, 2026-09-05: “it show with currently weapon using … previewer show player have 2 weapons skin rendered”, withsocial=set(prof=2,was=nil)in the log): a social-data model carries two independent weapon renderers — the CMount attachment (EModelCMountWeaponL/R, string model paths that ride in with the social data;Panda.ZGame.WeaponOriginDatabundlesModelCMountWeaponL/RbesideWeaponSkinId/MainModelId) andPanda.ZGame.WeaponModelComp, which owns its own weapon GameObjects (mainWeaponModel_/subWeaponModel_,getWeaponMount/clearModel) and is driven by the profession+skin data. So the chunk now does what every game view that shows a weapon skin on a preview model does — clear the CMount first, then ask for the skin:fashion_system_view.initPlayerModel:844-845(the screen hosting the weapon-skin tab whoseSelectStyle:26sets the override) andshop_fashion_sub_view.initPlayerModel:319-320(ShowPlayerWeaponModel:275sets it), the sameSetLuaAttr(EModelCMountWeaponL/R, "")pair every weaponless social view uses (investigation_clue_window_view:942/944,rank_main_view:503/637). The clear is emitted only when the outfit carries a weapon skin and is skipped at run time when the resolved skin is0, so no outfit can leave the model unarmed. Skin0= the class’s default look and resolves throughGetWeaponOriginSkinIdexactly asAsyncUseProfessionSkin:199does. Class-match is the caller’s rule (everyWeaponSkinTablerow isProfessionId-scoped; the game’s own tab refuses other classes atfashion_weapon_skin_select_view.lua:100-106). All three weaponpcalls CAPTURE their result and every preview writes a one-line outcome the host logs unconditionally:[WardrobePreview] weapon none/… weapon skin=<id> social=set(prof=2,was=nil) mount=cleared(L:<path>,R:<path>,skinModels:<WeaponSkinTable.WeaponModelId list>) disp=ok/… mount=skip(skin0)/… social=err:|mount=err:|disp=err:. The mount values are read BEFORE the clear, in their own nestedpcallso a read failure can never block the clear, and the skin’s own weapon model ids sit beside them — one owner log now says which renderer held which weapon. The 2.6.0 silentpcallmade the failure undiagnosable from the owner’s log. Pinned byWardrobePreviewChunkTests. Owner reports 2026-09-05. - Plugin-config save path taken off the large-object heap. Owner report 2026-09-05: every click of Wardrobe’s save button (53 outfits, 57 KB config) froze a frame for 287-366 ms and then stuttered for seconds. Measured on the owner’s real config, 53 saves × 3 watcher events, running the actual
PluginConfigService+FileConfigStore: 1,169 KB/save and 7 full gen2 GCs → 130 KB/save and 0 GCs of any generation (CPU 1.67 → 0.64 ms/save; gen2 pauses on this client are 100-475 ms, per the 2026-07-25 jitter investigation). Three causes, all fixed: (1)FileConfigStore.HandleFileTouchdidFile.ReadAllText+Encoding.UTF8.GetBytes+ SHA256 for EVERY FileSystemWatcher event (Wine delivers 2-4 per write) before the self-write check — a 106 KB LOH string + 54 KB array just to recognise our own write; a newSelfWriteLedgernow answers that from oneFileInfo((Length, LastWriteTimeUtc)) with the content hash kept as the fallback for events that raceFile.WriteAllText. (2)PluginConfigService.SaveSectiondeep-cloned the whole root (ToJsonString+Parse) only to hand it to a store that serializes it again — it now passes the live root under the existing lock, andIConfigStore.Savedocuments the serialize-synchronously / retain-nothing contract. (3) Config files are now written compact, not indented — the owner’s nested config inflated 20 K → 55 K chars when indented, putting every save’s string over the 85 KB LOH threshold; readability of the on-disk file is the deliberate trade, and reading still accepts indented files (rollback-safe, process rules § 6). Pinned bySelfWriteLedgerTests(11),FileConfigStoreEchoTests(9, incl. zero-read echo suppression and a same-length external edit that must still be detected) andPluginConfigServiceTests.Save_HandsTheStoreTheLiveRoot_NotAFreshClonePerSave. WindowBuilder.LoadIconnow dedups Texture2D decodes through the token’s byte[]-keyedAtlasCache— previously onlySpriteElementand the live tile-icon binding did, so every other icon leaf (button chips, images, brand logo, pin stars) re-decoded per widget: the Wardrobe’s 20-row × 9-chip pool built ~180 textures from 8 distinct PNGs on each window open. Dedup now lives in exactly one place (BuildSpriteandIconBindingdropped their own copies). Safe to share because tinting is per-graphic (.color) and sub-rect selection per-RawImage(.uvRect) — nothing is baked into the texture;IconTexturesremains the single owner, so disposal is still oneDestroyper texture.- Loadout switch pre-dispatch gates (owner report 2026-09-05: three same-loadout presses = a 3,261 ms frametime spike, then stutter; log showed eight consecutive
[LoadoutSwitcher] Switched to Beam). The removed fast-path comment inPandaLoadoutProbe.CallApplyAsyncclaimed the game “cheaply no-ops a switch to the already-active loadout” — REFUTED byweapon_vm.lua:509-514, which builds{oldProjectId=CurPlanId, newProjectId=planId}and firesWorldProxy.SwitchProjectunconditionally, then (:536-542) stampsSwitchRolePlanTime, savescurrentProjectSyncDataand dispatchesOnRolePlanChange— a full server re-equip. The game’s own dropdown never allows it:role_plan_loop_item.lua:66ignores the click whenCurPlanId == PlanId,:118-121refuses insideGlobal.lua:1703 CombatStrategySswitchCd = 3(tip 150208),:122-125refuses in battle (tip 150206). New purePandaLoadoutProbe.DecideSwitchmirrors the first two: same plan →LoadoutResult.Successwith nothing dispatched ([Stellar][Loadout] switch to <id> skipped: already active), within 3 s of our last dispatch →Rejected(… refused: within the game's 3s switch cooldown). Combat is unchanged — the server + the game’s own wrapper still own that refusal.PendingSwitchnow records whether the live plan already matched at dispatch and skips the vacuous_currentId == TargetIdcompletion fallback in that case, so a switch can no longer “complete” on the first ~33 ms poll and let the caller’s single-flight guard start an overlapping burst. Pinned byPandaLoadoutProbeSwitchGateTests. A cooldown refusal also shows the game’s OWN tip rather than only a log line (the LoadoutSwitcher plugin’sReporttoasts only onSuccess):BuildShowTipsChunkemitspcall(function() Z.TipsVM.ShowTips(150208) end)— the exact dot callrole_plan_loop_item.lua:119makes, localized by the game — queued on the probe’s existing main-thread dispatch queue and drained by the sameDrainPendingDispatchespass (no new thread hop). One tip per refused press, capped atMaxQueuedTipsand dropped onClearSession. - The per-class gear/module resolve is now debounced on the TRAILING edge of the delta burst. The walk is whole-item-container (uuid index over every package + a gear read per slot per plan) and a re-equip burst re-arms it on every
CharSerializedelta, so ungated it ran at the full ~30 Hz drain rate through each burst — the ~1.8 MB/s allocation class behind the 2026-07-25 A/B’s 100-475 ms GC frames. A leading-edge cooldown (walk now, then wait a window) bounded the rate but still walked THROUGH the burst, twice for a ~1 s burst, both times against half-applied state.TryResolvePerClassDetailsIfDuenow consumes_resolvePendingonce per tick into a sticky_resolveArmed(the arming sites live in sibling partials and a bool cannot carry “again”), restarts a quiet timer on every new arm, and walks only afterResolveQuietTicks(15 ≈ 0.5 s) of silence — one loadout switch = ONE walk, against the burst’s final state. DEFER, NEVER DROP: the arm survives in_resolveArmeduntil the walk runs, andResolveMaxDeferTicks(60 ≈ 2 s, counted on EVERY armed tick so a saturating stream cannot starve it) forces a walk during an endless stream, so “late, never stale” keeps a hard ceiling. The purePandaLoadoutProbe.DecideResolvenow takes(resolveArmed, resolverAttached, hasInputs, quietTicks, deferTicks); the gate still wrapsTryResolvePerClassDetailsrather than living inside it, so every existing live-state / Deep-Slumber resolve pin drives the unchanged inner method. New always-on, self-limiting line[PerClassLoadout] walk {ms}ms plans={n}, emitted only above 33 ms (zero volume in normal play, no diagnostics restart needed to attribute a reported spike). Pinned byPandaLoadoutProbeResolveGateTests(10, incl. a burst replay that scores 1 walk where the leading-edge form scored 2, and a saturating-stream starvation pin). - Notice tips no longer re-open
noticetip_popper tip. Owner reports 2026-09-05: (1) holding a loadout hotkey on the already-active loadout gave 4-5 frames of 100-185 ms over 3-5 s even though the framework SKIPPED every switch (no RPC) — the toast was the only game-engine touch left on that path; (2) repeated “Save current outfit” still stuttered (max 428 ms), and those toasts carry DISTINCT text so a dedupe alone could never have fixed it. Root cause:Z.UIMgr:OpenView('noticetip_pop')once per tip, run synchronously on the main thread inside the service tick. On an ALREADY-OPEN view that is not cheap (ui_manager.lua:112-161): the open list is re-ordered (:137-145),Z.UICameraHelper:OpenUICameraruns (:151), andui:Active(:152→ui_base.lua:45-59) reachesSetAsLastSibling(:53→ui_view_base.lua:80-85= a transform re-parent PLUSZ.UIMgr:UpdateDepthover the layer, i.e. a canvas rebuild) before it ever gets toOnRefresh(:55), thenViewStatusSwitchMgr:TrySetStateActive(:158) and a globalEventMgr:Dispatch(UIOpen)(:160) fire. The view already drains its own queue —OnRefreshdequeues one item when it has noviewData(noticetip_pop_view.lua:80-86),showPopTipholds three at a time and re-enqueues the overflow (:130-133), each item’sOnEndpulls the next (:210-214) — soBuildPopChunk/BuildPopTipChunknow enqueue and then refresh the LIVE view (GetView+SetViewData(nil)+CallLifeCycleFunc(OnRefresh), the same tailActivewould have reached), keepingOpenViewonly as theelsefallback for when there is no active/loaded/visible view — so no tip can be stranded in the queue. Two service-side bounds on top: the drain takes ONE tip per tick (a lone tip still shows on the very next tick), and a chunk byte-identical to one still inside its own delay+duration window is dropped by the pureNoticeTipService.DecideShow— the same call the game’s owncheckConfigRepeatmakes (noticetip_data.lua:8-22), which never fires for us because our tips carryId=0and so have no MessageTable row. DIFFERENT content is never dropped. New always-on, self-limiting line[NoticeTips] slow show {ms}ms, emitted only above 33 ms. Pinned byNoticeTipSpamTests(10).
[2.5.0] - 2026-09-01
Section titled “[2.5.0] - 2026-09-01”2.5.0 (minor) — the Loadout Switcher can switch your whole Deep-Slumber (tree and all), plugins get accurate raid boss health, and raid runs record as one run. Additive, binary-compatible with plugins built against ≤2.4.1.
- Plugins can now read a boss’s exact health straight from the game, and can tell when a raid boss has actually gone down even when the game never shows its health hitting zero. The Combat Meter uses this for accurate boss health in raid replays.
Changed
Section titled “Changed”- The Loadout Switcher now switches your entire Deep-Slumber when you change loadouts — its tree, not just the phantom factors. Switching to a loadout whose Deep-Slumber uses a different tree now rebuilds the tree for you instead of stopping half-done, and it no longer stutters while it applies.
- Raid runs are recorded as one continuous run again, instead of being split into pieces when the game’s own timers reset partway through the fight.
Developer notes
Section titled “Developer notes”DeepSlumberAreaBindinggains a non-positionalNormalNodesinit member (the tree / Anchor allocation — kept off the primary constructor so it stays binary-compatible with ≤2.4.0).DeepSlumberReconcilernow diffs the tree and, when it differs, drivesResetAllNodes+ActiveNormalNode(whole-area reset — the game has no per-node anchor removal, owner ruling 2026-09-01) before re-socketing factors; legacy bindings with a null tree stay factor-only. Both new worldProxy RPCs were validated in-game (code=0,zoneId=areaId). New Kind-phase order: enable → reset → unsocket → activate → socket.- The loadout live-state refresh now defers its full-container walk while a plugin Deep-Slumber apply is in flight (
PandaLoadoutProbe.DecideRefreshgated onPandaSeasonTalentProbe.HasPendingWrites), collapsing the per-opCharSerializeburst into one refresh after the apply settles — removes the 3-5 frame hitches a tree rebuild otherwise caused. Mirrors the existing combat-defer; a manual in-game switch is unaffected. - Native boss-HP tap:
IBossVitals+EntityVitalsServiceread boss HP from the game’s own entity, withEDisappearType-aware AOI eviction and anAttrMaxHpTotal(11321)fallback; a raid boss whose HP never reads 0 is detected as HP≈1%-then-vanish.IRunTimerlatch-epoch gives upgrade-proof run identity so an evidence-less belt resolution cuts a segment without resetting the run’s identity (raid run-split fix; spec 2026-08-26).
[2.4.1] - 2026-08-29
Section titled “[2.4.1] - 2026-08-29”2.4.1 (patch) — stops a repeated combat popup during long fights. No API change; binary-compatible with all existing plugins.
- No more repeated “Cannot perform this action during combat” popup during long fights. It could appear every few seconds in sustained combat, even with no plugins installed.
Developer notes
Section titled “Developer notes”PandaLoadoutProbenow defers only the combat-gatedSyncProjectListRPC (AsyncGetRolePlanData) while the local player is in combat, using the game’s own in-combat check (GetLuaLocalAttrInBattleShow()/GetLuaIsInCombat(), fail-safe to not-in-combat), and fires exactly one refresh at combat end; the RPC-free live-state re-read keeps running in combat. Root cause: every CharSerialize merge re-armed_refreshPending, and the server rejects the RPC in combat (ErrStateIllegal 3202), which the game’s own wrapper toasts (~every 5s as deltas drip in). Infrastructure-only (PandaLoadoutProbe,PandaLoadoutProbe.Resolution); no API change — binary-compatible with all existing plugins. +7PandaLoadoutProbeRefreshGateTests. (#72)
[2.4.0] - 2026-08-25
Section titled “[2.4.0] - 2026-08-25”2.4.0 (minor) — plugins can now save your outfits and switch between them, with a live 3D preview. Additive, binary-compatible with plugins built against ≤2.3.0.
- Plugins can now save the outfit you’re wearing and switch you back to it later, and show a live 3D preview of a saved outfit on your own character. The new Wardrobe plugin uses this for instant hotkey outfit switching.
Developer notes
Section titled “Developer notes”- New plugin surface:
IWardrobe(capture the worn outfit as a region→fashionId map; apply through the game’s ownWorldProxy.FashionWear, keeping every server-side check) andIWardrobePreview(dress a fresh self-model with an arbitrary saved outfit viaGenModelByLuaSocialData+SetLuaAttr(EWearFashion, …), rendered through a secondPortraitModelHost; orbit / zoom / pan).FashionEntrygainsDyeAreas— parallel toDyes, carrying each dye’sEFashionColorAreaTypeso multi-area pieces preview on their real areas;AttrFashionDataReadernow reads both the base (field 2) and attachment/socks (field 3) colour maps. RE:docs/recon/wardrobe-fashion-preview.md.
[2.3.0] - 2026-08-25
Section titled “[2.3.0] - 2026-08-25”2.3.0 (minor) — plugins can now apply a Deep-Slumber setup for you, and applying one is fast and self-healing. Additive, binary-compatible with plugins built against ≤2.2.0.
- Plugins can now change your Deep-Slumber Psychoscope for you — its cultivate line and its phantom factors. The Loadout Switcher uses this to re-apply the Deep-Slumber you bound to a loadout the moment you switch to it.
Changed
Section titled “Changed”- Applying a Deep-Slumber setup is much faster. A switch that moves a lot of factors used to take a few seconds; now it finishes in a fraction of that, and if the game drops one of the changes it quietly retries instead of stopping half-done.
Developer notes
Section titled “Developer notes”- New plugin surface:
IDeepSlumber.ApplySetupAsync(DeepSlumberSetup, CancellationToken)returningDeepSlumberApplyResult, plusDeepSlumberSetup/DeepSlumberAreaBinding. The live→target diff (DeepSlumberReconciler) lives in Application; the write path drives the game’sseason_talentworldProxy RPCs (Approach A — raw RPC returns the bareEErrorCodeinline) viaPandaSeasonTalentProbe. Docs:docs/driving-game-actions.md§ Deep-Slumber. - Apply overlaps its server round-trips: a bounded in-flight window (5) with one-dispatch-per-tick pacing, run in Kind-phases (enable → unsocket → socket) with a barrier that preserves the scarce single-copy unsocket-before-socket invariant. Only transient (did-not-land) ops retry (initial + 2, 250 ms backoff); a positive game refusal (7555/7561/combat) is never retried.
LauncherEntry.TitleProvider(Func<string>?): an optional live-localized tile title. The launcher rendersDisplayTitle(TitleProvider?.Invoke() ?? Title) each frame, so a tile whose plugin setsTitleProvider = () => Localization.T(key)re-localizes immediately on a language change;Titleremains the stable pinned-state identity. Plugins pass a captured string today, which is why their tiles were stuck in the registration-time language.
[2.2.0] - 2026-08-24
Section titled “[2.2.0] - 2026-08-24”2.2.0 (minor) — the live-build release. Stellar now notices the moment your setup changes and reads it live, and exposes the Deep-Slumber Psychoscope to plugins. Additive, binary-compatible with plugins built against ≤2.1.0.
- Stellar now notices the moment you change your build — swapping a piece of gear (including with the Replace button), changing modules, respeccing talents, switching a Battle Imagine, or editing your Psychoscope. Plugins that record your setup, such as the Combat Meter, can now save the exact build you fought each boss with instead of whatever you happened to have on when the run started.
- Your Psychoscope (Deep Slumber) is now available to plugins — season level, lines, socketed cards and node levels, read live from the game rather than from a saved profile.
- Plugins can now show a second value on the same on-screen bar — for example a boss’s shield alongside its health, either as a see-through band laid over the health fill or as extra length past the end of it. Whether a plugin uses this is up to each plugin.
Changed
Section titled “Changed”- Less background work while you play. Stellar used to re-check your equipment, talents and dungeon death count on a timer every moment you were in the world; it now waits for the game to tell it something changed. Same information, noticeably less work per frame.
- Swapping a Battle Imagine now takes effect straight away. Before, Stellar kept showing the pair you had equipped when you logged in.
- Changing a piece of gear with the Replace button is picked up again — it used to go completely unnoticed.
- The dungeon death counter no longer gets stuck. If a later run reached the same number of deaths as an earlier one, it used to report none at all.
- Logging out and back in on another character no longer shows the previous character’s Psychoscope.
- Fixed a case where the game sent several changes at once and everything after the first one was thrown away.
Developer notes
Section titled “Developer notes”ILoadout.LiveState(LiveLoadoutState): the local player’s LIVE class + talents, parsed from the live line — never a saved plan. Refreshed with the loadout data; a respec re-fires the refresh through the new dirty-delta trigger.ILoadout.LiveStateChanged: ONE game-tick event for the whole build — equipped gear/module slots, class, talent stage/nodes, the equipped Battle Imagine pair, andIDeepSlumber.GetState(). Raised only after a re-read actually CHANGED what the service serves (structural, order-insensitive compares; a not-yet-read surface is no-signal). Published from the RESOLVE step, not the raw slot read, so it can never fire whileGetSlots()still describes the previous setup; an unresolvable change is held and delivered on the tick the data lands (LATE, never STALE). Raised on the game Update thread, unlikeIInventory.SelfGearChanged. Deep-Slumber joined this event in8e5a7b2(owner staging runsea/dXkw1PSyOG: a psychoscope factor unequipped between two archives and re-equipped after — the framework re-read it correctly but told nobody, so the consumer kept one stale snapshot across two materially different builds).IPluginServices.DeepSlumber(IDeepSlumber) +DeepSlumberState/DeepSlumberLine/DeepSlumberArea: live reflection reader overCharSerialize.SeasonCultivateLineData/SeasonRoleLevelData, seeded through the tolua# bridge because the C# mirror populates lazily and is a stale latch on a fresh session.zcontainermaps are iterated the game’s way —__pairsyields nil VALUES, so the reader indexes per key (owner runO1jJepsgKC). Session state clears on logout.IInventory.SelfGearChangedwidened to the self BUILD-state signal: genericContainerDirtyDeltaReader.TouchesField+ semantic wrappers fire it on talent (professionList, field 61) and Deep-Slumber (seasonCultivateLineData, field 101) method-22 deltas. Still network-thread — flag there, read on the tick, or subscribe toLiveStateChangedinstead.IResonanceState.Installedkeeps its shape but changes SOURCE and id space: equipped Battle Imagines are read from the skill hotbar’s aoyi slots 7/8 as aoyi SKILL ids (IGameDataResonance.GetImagineForSkillresolves them).CharSerialize.resonance(wire field 28) is never re-serialized on an in-session swap, so the old field-28 source could not see a swap at all (owner runsea/pNhmVQvVmV).- Wire fix: the top-level delta scan died after the first SKIPPED field because it never consumed that field’s trailing END tag — every change after the first in one packet was dropped.
- The framework’s last two per-tick game reads are gone. Live-state capture is driven by the container-merge event (replacing a field allowlist + a 1 s poll), and
IDungeonState.LastDefeatedCountnow ridesWorldNtf 3 EnterScene(EnterSceneInfo.SceneAttrs, the seed) +WorldNtf 7 SyncSceneAttrsinstead of a four-layer IL2CPP reflection read ofZWorld.GetWorldLuaAttr(348)on every main-thread beat — the same carrier the game’s own dungeon HUD watches. It also fixes a latent probe-side memo that never reset across runs (a second run reaching the same count reported 0 forever).StubRouter.Registeris now MULTICAST with registration order preserved (WorldNtf 3 has two subscribers and the order is load-bearing). BarElement.Overlay01/OverlayColor/OverlayInFront(BarStyle.Modernonly, #66): an optional SECOND fill fraction 0..1 on the SAME track asFraction01, re-pulled each refresh through its ownBarBinding.OverlayInFront: truedraws it over the main fill as a translucent band (the label overlay texts still sit on top);false(default)SetAsFirstSiblings it behind the opaque main fill so only the excess shows as an extension cap (e.g.Overlay01 = (hp+shield)/max). A nullOverlay01renders the bar exactly as before, andBarStyle.Defaultignores all three. Init-props only — additive, binary-compatible.tools/install-stellar.shgainsSTELLAR_FRAMEWORK_ONLY=1(deploy the framework set, plugin slots untouched) andSTELLAR_ONLY_PLUGINS=<slots>(deploy only the named plugin slots, framework slot untouched).
[2.1.0] - 2026-08-18
Section titled “[2.1.0] - 2026-08-18”2.1.0 (minor) — Stellar’s own menus now speak your language. Adds a plugin localization API; additive, binary-compatible with all existing plugins.
- Stellar’s settings menus now display in English, 日本語 (Japanese), ไทย (Thai), and Bahasa Indonesia. Pick your language in Settings → Themes → Language, or leave it on “Follow game client” to match your game.
Developer notes
Section titled “Developer notes”IPluginServices.Localization(ILocalization): a plugin-scoped UI-text localizer. Ship four embeddedLang/{en,ja,th,id}.jsoncatalogs (<EmbeddedResource Include="Lang/*.json" LogicalName="Lang.%(Filename)%(Extension)" />) and callLocalization.T("key")/TFormat("key", args); resolution is active-language → English → the key literal (a missing key renders visibly as the key). The framework auto-discovers each plugin’s catalogs at plugin-load (namespaced by plugin GUID, matched by theLang.<code>.jsonsuffix) — no registration code.ILocalizationControl(Settings-facing, NOT onIPluginServices) drives the settinglocalization.language(defaultfollow);ClientLanguageProbemaps the game client’sLanguageType(en=1,ja=2,th=5,id=6, elseen) to a supported code, and the setting live-switches (labels re-poll, baked renderers flush viaLanguageChanged). Catalog completeness is validated bytools/i18n-catalog.py <repo>(used/undefined/incomplete/orphan,--seed). Seedocs/plugin-development.md§ Localizing your plugin. New service only — additive, binary-compatible with plugins built against ≤2.0.3.- Section headers now stand out by accent colour (the theme’s
MenuAccent) instead of bold weight — crisp and clearly a header in every language. Why: under Proton/IL2CPP the overlay can’t render a readable bold for Thai/CJK — a real bold FACE will NOT load (Font.CreateDynamicFontFromOSFontverified across a host bold family name and a distinct normal-weight family installed both in the Wine prefix and as a hostfc-listfont — none render), and Unity’s synthetic bold blurs their tight loops/counters. So emphasis is carried by colour (WindowBuildersets emphasis text toMenuAccent;RegisterTextReskinre-applies it on a theme change), with weight kept CRISP — Latin still gets realFontStyle.Bold(it survives synthetic bold cleanly), complex scripts (CJK/Thai) stay regular. Titles take the crisp weight but no accent (already prominent by position).GlyphScriptpicks Latin-bold vs complex-regular per string, so it tracks a live language switch.
[2.0.3] - 2026-08-16
Section titled “[2.0.3] - 2026-08-16”2.0.3 (minor) — readable text inputs, plus internal groundwork for the CombatMeter Discord run-card and a reconnect fix that keeps a mid-dungeon disconnect from splitting your run in two.
- Text boxes in Stellar windows are readable again — they show the dark themed background instead of white-on-white.
Developer notes
Section titled “Developer notes”IGameAssets.LoadByPath— load an arbitrary game asset by its ZResLoader address (#61); backs the in-game image rendering for the CombatMeter Discord run-card.- Mid-dungeon reconnect party recovery:
PandaTeamInfoRefreshProbeinvokes the game’s ownWorldProxy.GetTeamInfo({})via the tolua# Lua bridge when in a dungeon withIPartySnapshot.PartyId == 0(a reconnect drops the party id until the game lazily re-delivers it — measured arriving only after a whole run). Bounded (≤3/run, 2.5 s throttle, world-gated main-thread); the reply is decoded by the existingPandaPartyStubProbe→PartyId. Enables CombatMeter’s reconnect run-split fix by making the party id available during the run instead of after it.
[2.0.2] - 2026-08-15
Section titled “[2.0.2] - 2026-08-15”- Plugins can now show more of your character’s stats — this powers the new Illusion-Breaking Strength readout in CombatMeter.
[2.0.1] - 2026-08-14
Section titled “[2.0.1] - 2026-08-14”2.0.1 (patch) — summoned-companion (imagine) damage now resolves to the right creature in combat data. Data-resolution only; no API change, binary-compatible with all existing plugins.
- Damage from your summoned companions now shows up correctly in the combat meter and on the logs website. Some newer companion abilities weren’t being recognised, so their damage went uncredited.
Developer notes
Section titled “Developer notes”GetImagineForSkillgainsImagineAoyiRule. Newer battle imagines flag their damage skills as SlotPositionId [0]/[6] (not the aoyi 7/8 slots) with noSkillFightLevelTablerow, so resolution rejected them and player-attributed summon damage never mapped back to the imagine (measured on jp/RXALtMH6J3: Celestial Flier 1008440, Rorola 2900840, Venobzzar 1007741, Kartgriff 111069 — all invisible while their damage sat in the run’s own perActorSkills). The new rung is gated to ids with no fight-level row of their own (baseId == 0): it decomposesMonsterId*100+NNover a lazySkillAoyiTableMonsterId→aoyi index (GameDataResonance.Aoyi.cs) plus a curated companion-arcane map (Boyce/Rorola/Fafala, table-evidenced). The gate is load-bearing — leveled player ids share the numeric namespace (140116 = Windborne Grace lv16 AND an Igoreus monster skill), and an own fight-level row wins. Negative memoisation stays pre-load-safe. Documented residuals: Igoreus/Denvel bands are ambiguous by construction; Dorothy/Lucy/Natsu have no evidenced rows and are never guessed. Also documents theNN=00composite probe as a sanctioned CombatMeter consumer contract (summon-entity appear-sourced imagine capture). Pinned byImagineAoyiRuleTests(23 cases incl. the collision band).
[2.0.0] - 2026-08-10
Section titled “[2.0.0] - 2026-08-10”2.0.0 (major) — the interface overhaul: resize the whole mod UI, a redesigned move-and-resize editing mode, a hotkey to hide the on-screen displays, and the Stellar menu on the login screen. One rendering engine now draws every window and overlay. Breaking for plugins that draw their own on-screen display (HUD) — the old HUD API is removed and those plugins must be rebuilt against the 2.0 SDK; all other plugins stay binary-compatible. Also bundles the 1.17.0–1.18.1 player fixes for anyone updating from an older build.
- A UI Scale slider in Settings → Themes that resizes the entire mod interface from 75% up to 150% (5% steps, default 100%). Every window and on-screen display scales together, and it previews live as you drag. Useful on very high-resolution screens where the panels felt small, or just to make everything bigger or more compact to taste.
- The mod interface now sizes itself to your screen resolution, so it stays proportional on any display instead of being tied to raw pixels.
- A redesigned layout-editing mode for moving and resizing the mod’s windows and on-screen displays. Turn it on with Alt+E, or with the new “Enter layout editing” button in Settings → Game UI; a bar across the top with an Exit button shows you’re editing. While editing: every on-screen display is shown — even ones you normally keep hidden — so you can position them; any window you can resize shows a small square in its bottom-right corner you can drag; and dragging stays smooth at any UI scale. When you leave edit mode, windows lock in place again.
- A hotkey to hide or show all of the mod’s on-screen displays at once, set to Alt+H by default (this resets each time you restart the game). There’s also an optional hold-to-hide key, unbound by default, that hides them only while you hold it — handy for a quick clean screenshot.
- The mod’s on-screen displays now automatically get out of the way while a game confirmation or OK pop-up is open, so they never sit on top of it.
- You can now open the Stellar menu right from the game’s login screen — a Stellar button sits in the login side bar, matching the in-world one — and the mod’s own windows now work at the title and login screens too, not only once you’re in the world.
- Settings now show each plugin’s proper name instead of its internal file name, and the Hotkeys screen groups shortcuts under the plugin that owns them, each with a short description of what it does.
Changed
Section titled “Changed”- This is a major update to how the mod draws its interface. Most plugins are unaffected, but a plugin that adds its own on-screen display may need an updated version from its author to work with 2.0.
- What this means for your existing setup: your saved window and on-screen-display positions are kept, and are remembered separately for each screen resolution. Because the interface now scales to your resolution, the windows may look a little smaller or larger than they did in the old version — most noticeable if you don’t play at 2560×1440 (smaller below that, larger above). Nothing is lost; use the new UI Scale slider and the layout-editing mode to set the size and positions you want.
- You now move and resize the mod’s windows only after entering layout-editing mode (Alt+E). Previously you could drag them at any time; locking them outside edit mode stops windows getting nudged by accident during play. Your positions are unchanged.
- The option that stops the game from also reacting to your mod shortcuts is now turned on by default. If you never changed this setting, the game will no longer also respond to a key that one of your mod hotkeys uses. If you had already set it yourself, your choice is kept exactly as it was.
- The default shortcut for layout-editing mode is now Alt+E (it used to be Shift+backtick). This only changes things if you never rebound it — a custom binding is kept. The block-hotkeys option is also now clearly worded: “Stop the game from also reacting to these keys.”
- Plugins can now build taller, wider, or full-width bars, put a larger label centred on top of the bar, and use a flatter “meter” look with an optional soft moving shine — so a plugin can present a prominent bar like a target’s HP bar or match the Combat Meter’s style. Whether a plugin uses this is up to each plugin.
- Your own class icon (the profession crest) now shows correctly.
- Switching game accounts no longer briefly shows the previous account’s character data — it’s cleared when you log out.
- Your window and on-screen-display positions now survive entering a dungeon or changing zones, instead of jumping back to their defaults.
- Class, skill, buff, and item icons that occasionally loaded blank now retry and appear reliably.
- An on-screen display you’ve hidden now stays hidden after you restart the game.
Developer notes
Section titled “Developer notes”- Breaking — the separate HUD toolkit is removed; there is now ONE UI path.
IHudHost/IHudHandle/HudSpec/HudAnchor,HudService(+.Layout),IHudRenderer/HudRenderer,HudElementBuilder(+.Meter), andHudBarAnimatorare deleted. On-screen overlays register through the window API:Windows.Register(new WindowRegistration(new WindowSpec(..., WindowCategory.HUD, WindowPanelStyle.Borderless){ Surface = SurfaceStyle.HudOverlay, Draggable = true, EditModeDragOnly = true, ShouldRender = ... }, root)). The sharedHudElementrecord tree is unchanged. Newenum SurfaceStyle { Menu, HudOverlay }(defaultMenu→ every existing window byte-identical);HudOverlayreproduces the old HUD look (always-shadowed twin text honouringFontSize/DynamicFontSize/ShadowDistance, the rounded 9-slice smoothed-fill bar, the transparent pill) by reusingHudThemeAssets, now owned byWindowRenderer.IThemeHudColorsretained (Toast + the HudOverlay bake still use it). First-party PlayerHUD / RaidManager countdown / DebugInfo migrated in lockstep, pixel-identical. Only plugins that calledIPluginServices.Hud.Register(HudSpec)are affected; all others are source- and binary-compatible. - Source-breaking rename
BarStyle.Meter→BarStyle.Modern(enum value unchanged,Default=0/Modern=1 → binary-compatible for compiled plugins).BarElementgainsinit-only geometry (Height/Width/FillWidth/LabelFontSize/LabelInside) plus the opt-inModernmeter style with optionalSheen/SecondaryLabel; the original positional constructor is untouched (source- and binary-compatible, unset fields default to the old render byte-for-byte). Both the HUD-overlay path and the window path (WindowBuilder.BuildBar/WindowBuilder.PreviewBuildBarModernWindow) honour every new field. - UI scaling. Window overlay canvas gets a
CanvasScaler(ScaleWithScreenSize, reference 2560×1440, match 0.5). At the reference resolution scaleFactor = 1.0 (byte-identical to pre-2.0); other resolutions convert drag/resize/on-screen-clamp/dropdown-placement/click-rect between screen px and canvas units. The UI Scale slider setsreferenceResolution = (2560/u, 1440/u), persisted astheme.uiscale, quantised to 5% so the font atlas repacks only at boundaries;canvas.pixelPerfecttoggles off at fractional scaleFactor to kill dragged-window jitter.LayoutStorage.Getreads saved rects as canvas units (scaleFactor-corrected) while the KEY lookup stays raw, so pre-2.0 per-resolution layouts are still found. - Game-phase model.
GamePhase { Startup, TitleScreen, CharSelect, World }and[Flags] GameUIState(+GameHudHidden/AnyMenu/Blocking/Popuppresets) onIClientState(Phase/PhaseChanged/IsWorldActive/UiState). The framework tick now runs UI/input/hotkeys every phase (so plugin windows work at title/login); game-state work self-gates per-unit onIsWorldActive, enforced by new STELLAR0006 analyzer (a[WorldGated]method must early-return on!IsWorldActive). Window visibility is plugin-owned via requiredWindowSpec.ShouldRender(IRenderGated), replacingAutoHideBehindGameMenus/HideUntilInWorld; the gate fails safe (null/throwing predicate → hidden + warned) so a stale pre-2.0 plugin can’t NRE the tick.Loading/Popupare driven by dedicated un-gated probes (reliable across loading screens / confirm dialogstips_common_popup/tips_sys_dialog). - Login-screen integration.
NativeUiAnchor.LoginSidebarinjects the Stellar rail icon intologin_main’s sidebar; uGUI injection is un-gated with per-anchor phase-relevance (LoginSidebar=TitleScreen, MainMenuRail/HudTopRight=World) so there is no out-of-phase probing cost.LauncherEntry.ShouldShowgates individual launcher tiles by phase; framework chrome hides over the loading screen. - New members (all additive/binary-compatible):
GameTextureElement.CornerRadius,CooldownTileElement.FallbackLabel,IWindowHost.IsLayoutEditing. - Plugin identity/hotkeys. The framework adopts each plugin’s declared
IStellarPlugin.Nameas its display name (Plugins panel, per-plugin Performance rows, Hotkeys group header); hotkeys group by owning plugin via an internal owner-tagged declaration sink, andIHotkeyActiongainsPluginId+Description. HUD-visibility adds internalIInputGateway.IsKeyHeld+HotkeyService.IsActionHeld. - Interop foundation (toward v2.1), additive, framework-only-implemented:
StellarInterop(cached reflection floor),ILua(IPluginServices.Lua),IHarmonyHost(IPluginServices.Harmony, auto-unpatch on plugin dispose), andIFramework/IFrameworkTiming(Post/Every/TimeNow, skew-correctedICombatSnapshot.ServerNow). - Icon-load resilience. Failed icon slots retry with bounded backoff (~0.5–2s, up to 4×) instead of memoizing
Failedfor the session; a first request that beats the HybridCLR loader registration self-heals. Fixes blank profession crest / skill / buff / item icons. - Other fixes: profession-crest projection falls back to
ProfessionSystemTable.Iconand guards against table-not-ready; account/character data clears on logout (account-switch leak); mod-window and native-game-HUD positions survive scene change; a persisted layout-editor hide survives relaunch. Throwaway phase-diag overlay removed. Merged from main: Steam client region detection (1.18.1) and loadout talent-spec correctness (1.18.0). - Test suite: 1012 passing.
[1.18.1] - 2026-08-07
Section titled “[1.18.1] - 2026-08-07”1.18.1 (patch) — Steam client region detection fix so runs from the Steam build upload again. Detection-logic only; no API change, binary-compatible with all existing plugins.
- If you play the game through Steam, your dungeon runs now upload to the logs website again. The mod couldn’t recognise the Steam version of the game and was holding those runs back.
Developer notes
Section titled “Developer notes”- GameEnvironmentService now matches the running executable by name PREFIX (StarSEA → SEA, StarASIA → JP) instead of exact filename, so the Steam build’s StarSEA_STEAM.exe resolves to SEA instead of Unknown — Unknown made CombatMeter’s RegionKnownOrWarn withhold every upload. One row per region covers StarLauncher, Steam, and future store builds. Regression test SteamSeaExecutable_DetectsSea; the existing UnknownExecutable_DetectsUnknown still pins non-matches. The Steam install dir is flat (no release_<ver> segment) so GameVersion reads “unknown” there — cosmetic, not gated on for uploads. Commit 5f976ea.
[1.18.0] - 2026-08-06
Section titled “[1.18.0] - 2026-08-06”1.18.0 (minor) — one additive loadout fix on top of 1.17.0: the talent specialization shown for a saved build is now read consistently with that build’s own talent nodes. Binary-compatible with plugins built against ≤1.16.1.
- Your saved talent build now shows the correct specialization on the logs website, instead of sometimes showing a different one than the talents you actually picked.
Developer notes
Section titled “Developer notes”- PandaLoadoutProbe.Resolution now sources a saved plan’s talent stage from the live per-profession container (talentList[prof].talentStageCfgId) so the stage id agrees with the plan’s own talent nodes — fixes a stale talentStageCfgId that mislabeled the spec (e.g. run sea/ZEEJjddKHN). Single-file change to PandaLoadoutProbe.Resolution.cs; cherry-picked from e73b99b. The exploratory NotifyShowTips (m42) win-tip tap from the same session was dropped as inert — vaults do send a real settlement message, so the actual vault fix lives plugin-side (CombatMeter 1.6.1). The SquareHandle slider knob referenced in FrameworkVersion already shipped in the 1.17.0 tree and is unchanged here.
[1.17.0] - 2026-08-05
Section titled “[1.17.0] - 2026-08-05”1.17.0 (minor) — kill detection from the game’s own logic, a run-id fix for instanced dungeons, per-class equipped-loadout capture, and a mounted-state stats-survival fix. Additive; binary-compatible with plugins built against ≤1.16.1.
- The Combat Meter can now tell exactly when an enemy dies — including bosses finished off by a cutscene or scripted move, not just a normal killing blow — so fights are timed and scored correctly.
- The logs website can now show the gear, modules, and talents you currently have equipped, for each class you play.
- Some dungeon runs weren’t being saved to the logs website — they’re recorded correctly now.
- Your character’s stats and info no longer disappear while you’re mounted.
Developer notes
Section titled “Developer notes”- Run-id: 3.7 instanced/Mistveil scenes whose scene uuid is below 2^53 were misclassified by magnitude; now classified by SceneType and early at the wire via AttrSceneBasicId (341). Fixes “No run id” (ranked runs not uploading). Commits 4683d8d, 9cb72c8.
- LIVE per-class loadout: parse the refresh chunk’s LIVE row (
_StellarLiveProbe) so a played class exposes its currently-equipped gear/modules/talents, and it is the sole source when a class has no saved plan. Commit ba74dd3 (+ the per-class gear/modules/talents series a513b29..c1a9d65). - Player-state: rescue the local player entity when it goes dark while mounted so PlayerStats/identity survive mounting; identity served from the char record, not the world entity. Commits 1107646, 615042d, 7a40e28.
- New
Stellar.Abstractions.Domain.ActorStateenum (Dead=9,Breaking=23,Unknown=0 for any other/future wire value) andCombatEvent.EntityStateChanged(TimestampMs, TargetId, ActorState), riding the existingICombatEventsstream — no service interface gains a member, so STELLAR0005’s 8-member ceiling is untouched. Field-proven after three recon rounds — seerecon/entity-state-death-signal-notes.md. Round 1’s spec’d leaf,Panda.ZGame.EntityCtrlDead.OnEnter, installed cleanly but stayed silent across all ten deaths in the owner’s confirming run: disproven. Round 2 tried the widerPanda.ZGame.ZStateMachine.onStateChanged/EnterStatehooks on top;onStateChangednever fired (a real negative result), butEnterStatefired correctly for every one of the ten deaths and resolvedDeadcorrectly — it was not broken, just costlier (it fires on every actor’s every transition, not just the one we care about), so round 3 drops it in favour of the cheaper leaf and keeps it on record as a field-proven fallback ifZStateDeadis ever removed/renamed. The shipped design patches exactly two sites:Panda.ZGame.ZStateDead.OnEnter(PROVEN — all ten deaths) andPanda.ZGame.ZStateBreaking.OnEnter(untested, not disproven; kept as the direct sibling of the proven hook, and as a timestamp source for an open frametime-spike investigation). Each site resolves/installs independently and degrades to “signal off” (logged) rather than throwing if a type/accessor is missing after a future game patch — seePandaEntityStateProbe. The site-attributed, per-scene-budgeted ungated observation line survives the cut-down unchanged. 2026-07-28 entity-state-death-signal spec; retires the HP-inference gone-timeout forArchiveReason.BossKillon the plugin side (follow-up work, not in this release).
[1.16.1] - 2026-07-25
Section titled “[1.16.1] - 2026-07-25”1.16.1 — same code as 1.16.0, re-cut under a fresh bundle filename because a CDN cache mismatch left some 1.16.0 downloads stuck at 100%. Carries the full 1.16.0 patch notes so players updating straight from 1.15.0 see what changed.
- Update download no longer gets stuck at 100% for some players (a caching problem on our download server with the 1.16.0 file — this version uses a fresh file).
- Much higher FPS in dungeons and busy areas. The framework was quietly doing heavy background work several times per second — on our test machine that alone cost up to 50 FPS in a dungeon (86 to 144 after the fix). This work is now nearly free.
- The regular micro-stutter is gone. If your frametime graph showed small spikes about 5 times every second — even with no plugins installed — that was us. Fixed.
- No more short freezes every few seconds. The framework re-checked your whole inventory once per second and threw the result away, which piled up memory and caused brief frozen frames (worst on lower-end PCs). It now only does that work when your inventory actually changes.
- Smoother big fights. Network and combat data is now processed with far less memory churn, so crowded areas and boss fights cause fewer hitches.
- Zero input overhead unless you use key-blocking. The hotkey “block from game” feature used to sit on the game’s keyboard input all the time; now it activates only while you actually have a blocked hotkey or are recording a new one.
Developer notes
Section titled “Developer notes”- Identical framework code to 1.16.0 (only FrameworkVersion + this changelog differ). The 1.16.0 manifest republish (player-facing patch notes, PR #46) rotated the bundle sha256 while CDN edges still held the previous zip (max-age 14400); launchers on those edges failed the hash check and hung at 100%. A fresh
Stellar-1.16.1.zipfilename has no cached copies anywhere, so every edge serves consistent manifest+bundle. Lesson (now in docs/release-process.md): a same-version republish requires an immediate CDN purge; prefer a patch bump instead. - Patch-note bullets are now plain text (the launcher renders no markdown — 1.16.0’s
**showed literally).
[1.16.0] - 2026-07-25
Section titled “[1.16.0] - 2026-07-25”1.16.0 (minor) — the frametime-jitter release: eliminates the framework’s in-dungeon FPS loss and frame-spike comb, root-caused by matched A/B on a live client (PR #44). Additive; binary-compatible with plugins built against ≤1.15.0.
- Much higher FPS in dungeons and busy areas. The framework was quietly doing heavy background work several times per second — on our test machine that alone cost up to 50 FPS in a dungeon (86 → 144 after the fix). This work is now nearly free.
- The regular micro-stutter is gone. If your frametime graph showed small spikes about 5 times every second — even with no plugins installed — that was us. Fixed.
- No more short freezes every few seconds. The framework re-checked your whole inventory once per second and threw the result away, which piled up memory and caused brief frozen frames (worst on lower-end PCs). It now only does that work when your inventory actually changes.
- Smoother big fights. Network and combat data is now processed with far less memory churn, so crowded areas and boss fights cause fewer hitches.
- Zero input overhead unless you use key-blocking. The hotkey “block from game” feature used to sit on the game’s keyboard input all the time; now it activates only while you actually have a blocked hotkey or are recording a new one.
Developer notes
Section titled “Developer notes”These details never appear in the launcher — full technical background lives here and in PR #44.
- Anchor probe:
PandaUGuiAdapterran a path-formGameObject.Find(full scene walk, ~30 ms/hit in dense scenes) every 200 ms while the game menu was closed; now cachedzuiroot+ relativeTransform.Find, active-only contract preserved. A check-standards blocker now bans path-formGameObject.Findliterals (exemptions: devkit tech-debt D-26). - Inventory: the 1 Hz poll rebuilt the full module inventory via reflection (~1.8 MB/s garbage → 100–475 ms stop-the-world GC frames on low-spec); now generation-guarded on actual inventory syncs. Measured avgUpdAlloc 55 → 9.8 KB/tick.
- Wire tap: handler snapshot resolved before payload materialization (unsubscribed AOI/world traffic no longer copied), zero-copy reassembly drain, cached per-thread zstd decompressor with exact-size output, quiet-connection eviction + drained-buffer shrink.
- Input:
Rewired.Keyboard.GetKey*prefixes (~8–25k calls/s at render rate) install on first block/capture and uninstall when cleared; primary-key pre-filter before modifier interop reads. - Combat parse: AOI attr payloads slice the per-packet array (−45% alloc per combat packet), lazy cooldown list, positions cache uses an Interlocked counter instead of per-update
ConcurrentDictionary.Count, compiled delegate replacesMethodInfo.Invokefor the IL2CPP span extractor. - New PERFHUD diagnostics:
PerfProbe.HookEndRewired+hook:rewired=ms/callsin the[Perf]line, andBeginSegcoverage for the previously unsegmented tick items.
[1.15.0] - 2026-07-21
Section titled “[1.15.0] - 2026-07-21”1.15.0 (minor) — adds per-plugin binary file storage, the substrate for CombatMeter’s byte-for-byte re-upload. Additive; binary-compatible with plugins built against ≤1.14.0.
IPluginServices.Data(IPluginDataStore) — per-plugin binary file storage (Write/Read/Delete/List) for data too large or opaque forIConfigSection. Never-throws; names are path-traversal-safe (rejects.., rooted paths, backslashes,>1separator). Each plugin’s store is rooted at<gameRoot>/stellar/plugindata/<guid>.data/— a sibling of, and deliberately OUTSIDE, the recursivestellar/plugins/DLL scan path (FrameworkPaths, with a pinned non-nesting test), so a plugin’s stored files can never be shadow-loaded as assemblies.
[1.14.0] - 2026-07-18
Section titled “[1.14.0] - 2026-07-18”1.14.0 (minor) — CombatMeter sync-fix surface: honest death inference, live party status transport, and dungeon flow-state. Additive; binary-compatible with plugins built against ≤1.13.0.
EntityVitals.HasHpObservation— true only once a real current-HP value has been observed; a MaxHp-only attr delta now reads “alive, HP unknown” instead of dead. AOI appear packets now seed vitals directly (previously the first delta after appear defined them).PartyMember.FastSyncState— rawTeamMemberFastSyncData.state(field 6), previously parsed and dropped; the game client itself ignores this field, so semantics are calibrated empirically and consumers must treat unmapped values as “no signal”.IDungeonState.CurrentFlowState+FlowStateVersion— the dungeon flow state machine (EDungeonState: Active/Ready/Playing/End/Settlement/Vote) surfaced from both the method-23 full sync and the method-24 dirty delta, with a monotonic transition counter as the poll-friendly change notification.
- Party fast-sync deliveries that change ONLY the member’s status field now fire
MemberUpdated.
[1.13.0] - 2026-07-16
Section titled “[1.13.0] - 2026-07-16”1.13.0 (minor) — adds live Trading-Center membership so exchange plugins categorize new items without a rebuild. Additive; binary-compatible with plugins built against ≤1.12.0.
IExchange.GetStallSubcategoryMap()— item config id → Trading-Center subcategory leaf (101-104 Growth / 201-209 Life Skills / 301 Modules / 401-405 Appearance), read live at runtime from the game’sStallDetailTable. Empty when the table isn’t loaded yet; consumers fall back to their own data. Lets the Exchange Buyer plugin surface Season-3 (and future) items with no rebuild.
- Game config tables keyed by id in the
ZTable<int, row>key (rather than a value column) — e.g.StallDetailTable— are now read via a key-aware path; the previous value-only table loader silently returned 0 rows for them.
[1.12.0] - 2026-07-11
Section titled “[1.12.0] - 2026-07-11”1.12.0 (minor) — adds the game-environment identity service for SEA/JP region-aware uploads (spec: server-region partitioning). Additive; binary-compatible with plugins built against ≤1.11.0.
IGameEnvironment— region (GameRegionSEA/JP/Unknown, lowercaseRegionCodewire form) + installedGameVersion, detected once at boot from install markers (StarSEA.exe→ SEA); framework configenvironment.regionoverrides. Boot log prints[Stellar] region=… version=… source=….
[1.11.0] - 2026-07-10
Section titled “[1.11.0] - 2026-07-10”1.11.0 (minor) — consolidates the portraits/replay feature line onto main (merge ad173a0): every addition is a new service, init-only property, or defaulted parameter under the plugins-consume-never-implement contract, binary-compatible with plugins built against ≤1.10.0. Note: the IEntityTransforms / IGameDataWorld.GetMonsterByEntity entries listed under the 1.10.0 sections below were documented there but did not ship in the 1.10.0 bundle; they ship in this release.
IDungeonState— dungeon lifecycle + settlement service: run outcome (DungeonOutcome),DungeonSettlementInfo (PassTimeSeconds, MasterModeScore, TotalScore)with the achieved total-score carried end-to-end, defeated count, and run-identity gating for fail-outs.IEntityTransforms— live world transform (position + facing) reads of arbitrary entities by id (main-thread), for replay/position capture.IEntityDetail.RefreshSocialSnapshot— self social-data refresh (drives the game’sAsyncGetSocialDatavia Lua) so plugins can re-read the local player’s social snapshot (e.g. master score) after a settlement without waiting for cache expiry.SocialSnapshot.MasterScoreand related identity fields.IGameDataWorld.GetMonsterByEntity(EntityId)+MonsterInfo.MonsterType/IsBoss— entity→monster-table resolution through the shared combat entity tracker.- Skill-phase
CombatEventcases (cast phases 101–105) for forensic capture. GearInstance.BreakThroughTime;PartyMemberadditions;IPluginServicesexposes the new toolkit services.
[1.10.0] — 2026-07-03
Section titled “[1.10.0] — 2026-07-03”1.10.0 (minor) — pure interface additions under the plugins-consume-never-implement
contract (IEntityTransforms, IGameDataWorld.GetMonsterByEntity); MonsterInfo’s new MonsterType/
IsBoss are init-only properties, not primary-ctor params, so the type stays binary-compatible with
plugins built against ≤1.9.1 (see the SliderElement/1.7.1 precedent below).
IEntityTransforms— toolkit service reading the live world transform (position + facing) of an arbitrary entity by id, for replay/position capture.TryGetTransformreturnsfalse(leaving the out-params at their defaults) when the entity isn’t resolvable this frame; reads must happen on the main thread.IGameDataWorld.GetMonsterByEntity(EntityId)— resolves a live entity to itsMonsterInfotable row via the entity’s cached attr-10 (config-id) attribute, routed through a sharedCombatEntityTrackerso combat and game-data probes read the same cached attribute set instead of each maintaining their own.MonsterInfo.MonsterType/MonsterInfo.IsBoss— numeric monster classification (EMonsterType-mirrored; 0=Monster, 1=Elite, 2=Boss) and a derived boss flag, loaded from theMonsterTablerow. Confirmed by recon on the Ancient Purifier run: entity attr 10 →MonsterTable[33301].MonsterType == 2.
- Run-identity leak across non-instanced scenes.
IDungeonState.CurrentRunId(and the internalDungeonRunIdGate) now clears to 0 on entering a non-instanced (town/field) scene, instead of letting a previous dungeon run’s id linger and get attributed to unrelated open-world activity.
Removed
Section titled “Removed”- Recon scaffolding for boss identification (the entity → monster-config-id spike diagnostic and the
short-lived
IMonsterCatalog/MonsterCatalogServiceprobe path) — superseded by theGetMonsterByEntityMonsterInfo.MonsterType/IsBossresolution above and no longer needed.
[1.10.0] - 2026-07-08
Section titled “[1.10.0] - 2026-07-08”- Window-framework support behind the CooldownBar overlay (all additive + binary-compatible). (#32)
WindowSpec.BackgroundOpacity(Func<float>?) — poll-diffed black backdrop on the borderless root’s click-blocker Image; fills the full window rect and expands on height resize.ColumnElement.Padding(int) — uniform inner padding on all four sides.RowElement.Justify+RowJustifyenum (Left/Center/Right) — ships a 2-arg compat overload for plugins built against the old signature.BackdropElement— stretch-fillignoreLayoutbackdrop (place first in a column so siblings draw on top).VirtualListElement.ResetScroll(Func<bool>?) — polled each refresh; true snaps the list to the top.CooldownTileElement.OnClick(Action?) — whole-tile click action.
Changed
Section titled “Changed”- Atomic game-asset icons —
GameTextureElementmoved toWindowBuilder.GameTexture.cs; icon rebind is now aWindowTokenbinding applied in the sameApply()pass as the virtual list, removing the one-frame wrong-icon blink while scrolling. Buff icons resolve as atlasSprites instead ofTexture2D. ConditionalElementflex clamp — aCondnode inside a row no longer setsflexibleWidth=1(it was stealing all slack, centring fixed content in a borderless bar); column parents still force-expand.- VirtualList viewport inset 9px on the right so the scrollbar no longer overlaps content.
- Framework deploy target moved out of the csproj into
Local.props.
[1.9.1] - 2026-07-01
Section titled “[1.9.1] - 2026-07-01”AutoHideBehindGameMenuswindows now hide behind more full-screen game screens.PandaMenuStateProbewas extended to detect NPC dialogue, loading screens, the dungeon-enter confirm popup, the line-selector panel, and story cutscenes as full-screen menu states. (#29)- The dungeon-enter confirm view instantiates as
team_copy_popuponUILayerFuncPopup(notcommon_matchingonUILayerTop), so detection now scansUILayerFuncPopup(AnyChildActive) and checks themain_line_windowprefix onUILayerMain. - Removed the probe’s diagnostic-logging infrastructure (
Log,DumpZuiroot,DumpActiveCanvases); the layer map is documented inGameMenuState.mdinstead.
- The dungeon-enter confirm view instantiates as
[1.9.0] - 2026-06-30
Section titled “[1.9.0] - 2026-06-30”IWindowControl.BringToFront()— explicitly raises a window above others via aZFrontcounter that sorts above category (ZCat), so cross-category fronting works (e.g. a HUD window surfaces above a Tools window). ABringToFrontPendingflag covers the still-hidden case, soSetVisible(true)+BringToFront()in the same line always surfaces on top. (#27)
- Interaction pass-through to covered windows. A
FrontWindowBlocksguard now wraps every hit-test inWindowInteractionTicker(hovers, resize grip, drag areas, scrollbar suppression, chart pan/zoom, chart navigator, render-host drag/zoom), so back-window elements (chart, 3D portrait, scrollbar) no longer receive interaction when a front window covers the pointer. (#27) - Dropdown item styling now matches
SelectableElement: roundedSwatchBgchip, transparent rest / [email protected] hover / [email protected] active selection, VLG 6/4 padding,ContentSizeFitter. Hover is ticker-driven (Unity EventSystemColorTintdoesn’t fire reliably alongside the ticker). (#27)
[1.8.0] - 2026-06-28
Section titled “[1.8.0] - 2026-06-28”DropdownElement— a reusable compact dropdown for a small, fixed set of mutually-exclusive choices. The trigger shows the current option (caption + ▾); clicking it opens a themed floating option list that floats above the window’s scroll clip (parented to the canvas root, so a dropdown inside aScrollElementis not clipped by itsRectMask2D). Picking an option calls back with its index; the list dismisses on pick, outside-click (a full-screen invisible blocker), or Escape. Drop it into any window element tree like the other widgets.
Changed
Section titled “Changed”- The Settings → Performance per-plugin Self-rate control (Off / Boost / Self-managed) is now a
DropdownElementinstead of a click-to-cycle button.
[1.7.1] - 2026-06-28
Section titled “[1.7.1] - 2026-06-28”- Binary-compatibility regression from 1.7.0.
SliderElement’s newWidth/HandleSizewere added to the record’s primary constructor, which changed the generated constructor signature and broke the old positional ctor. Plugins compiled against framework ≤1.6.0 that construct aSliderElement(e.g.Stellar.AutoFishing) failed to load on 1.7.0 with aTargetInvocationExceptionwrappingMissingMethodException.Width/HandleSizeare now init-only properties (the original primary ctor is restored), so those plugins load again.
[1.7.0] - 2026-06-28
Section titled “[1.7.0] - 2026-06-28”- Per-plugin & dynamic update rate. Each plugin’s
IFramework.Updatenow ticks at its own rate instead of being welded to the single global rate. In Settings → Performance a new “Per-plugin update rate” section lets the user set each plugin’s rate (default Follow global) and grant a per-plugin Self-rate permission (Off / Boost / Self-managed). IFramework.RequestUpdateRate(int hz)→IUpdateRateScope— a permitted plugin can temporarily ramp its own tick rate (e.g. a market snipe’s fast-retry window) and revert by disposing the scope. Returns an inert no-op scope unless the user granted the plugin permission. “Self-managed” exempts the ramp from the 10 s leak-guard for plugins that hold an elevated rate indefinitely.SliderElement.Width/SliderElement.HandleSizefor compact, fixed-size sliders;ToggleElement.Enabledis now honored by the renderer; fixed-widthNoWraptext now clips to its column.
Changed
Section titled “Changed”- The framework tick is now a single variable-speed clock at
max(global, all active plugin rates)(clamped[10,240], realized ≤ frame rate). The Lua-bridge probe drains ride the master tick so a ramped plugin’s RPC round-trips complete proportionally faster; expensive draw/refresh work stays gated to the global rate. Idle behaviour (nothing ramped) is unchanged from 1.6.0.
- Perf overlay no longer freezes its per-window timings when the master rate exceeds the global rate.
[1.6.0] - 2026-06-25
Section titled “[1.6.0] - 2026-06-25”IExchange— player Trading-Center market access, exposed asIPluginServices.Market. Plugins can query the live market and buy through the game’s own trade flow — no packet construction; every purchase is built and validated server-side. Driven via the game’s WorldProxy exchange RPCs (“Approach A”, headless — the trade page never needs to be open).- Reads:
QueryListingsAsync(itemId)(live listings, cheapest-first),QueryCatalogAsync(category)(category browse — the request’stypeis theStallCategoryfamily, derived from the leaf id),QueryCareListAsync(kind)(watch list + availability),QueryNoticeAsync(itemId)(scheduled/pre-order listings). - Buy:
BuyAsync(itemId, quantity, price)→ExchangeBuyOutcome(Success/NoItemAvailable/InsufficientFunds/Rejected/Timeout). - DTOs:
ExchangeListing,ExchangeCatalogItem,ExchangeCareItem,ExchangeNoticeListing,ExchangeItemKind.
- Reads:
- Extensible toast notifications.
INotifications.Create()fluent builder (WithMessage/WithKind/WithDuration/WithIcon) with custom-icon support — render a supplied texture (e.g. a game item icon) in the toast’s icon slot;nullfalls back to the baked per-kind glyph. The existingNotify(string, kind, seconds)shortcut is unchanged.
[1.5.0] - 2026-06-24
Section titled “[1.5.0] - 2026-06-24”- Team voice & dungeon ready-check, surfaced as typed party events. The framework decodes the
game’s voice and ready-check wire traffic and exposes it through
IPartyEvents/IPartyRoster, so plugins consume clean events instead of touching IL2CPP or Lua. (#21)- Ready-check:
WorldNtfLuaStubDispatcher(HarmonyX postfix onZLuaStub.OnCallStub) catches methods 70/71, which flow through the Lua stub rather thanWorldNtfStub;NotifyReadyCheckReaderdecodesNotifyAllMemberReady(70) /NotifyCaptainReady(71) intoIPartyEvents.ReadyCheckResponded/ReadyCheckPhaseChanged. - Team voice:
GrpcTeamNtfmethods 25/26 (mic mode / speaking) decoded inNotifyTeamVoiceReaders;voice_is_open(TeamMemDataf7) +mem_real_time_voice_infos(GetTeamInfoReplyf4) parsed for correct state on join/relogin (incl. theOpenSpeakeredge case the bool can’t express). NewMicrophoneStatusenum +IPartyRoster.GetMicStatus/IsSpeaking— additive;PartyMemberstays binary-compatible. - Meter row (
MeterRowData):NameColor(ready-check vote tint),VoiceIcon/VoiceIconTint/ShowVoiceIcon(mic icon),RowBorder(green while talking),CrestTint.
- Ready-check:
- UI primitives for click-away popups.
WindowSpec.DismissOnOutsideClick— Escape or press-outside invokesOnCloseand hides the window;IsShownstays in sync.PanelElement— themed popup container (2 px border + lifted background + padded content host).- Deterministic z-ordering (
ReorderWindows): draw order follows(ZOrder, Category, Id)regardless of plugin mount order, so click-away popups always render on top.
- Meter row border now draws all four edges (previously only the top edge was visible).
WindowRenderer.SetRectclamps programmatic resizes toMinWidth/MaxWidth. Previously only drag-resize was clamped, so mode switches,RefreshPartyFocusHeight, and prefs restore could silently push a window below its registered minimum.
[1.4.2] - 2026-06-22
Section titled “[1.4.2] - 2026-06-22”- Non-ASCII text input (e.g. Thai) truncated in uGUI fields. Switched from
onValidateInput(per-char ASCII gate) toonValueChanged, so multi-byte / IME input is preserved. (#19)
[1.4.1] - 2026-06-22
Section titled “[1.4.1] - 2026-06-22”- Periodic ~200 ms in-game freeze from the rail-button template lookup. The lookup walked the entire UI tree on a timer; it’s now scoped to the menu-panel subtree. (#17)
[1.4.0] - 2026-06-22
Section titled “[1.4.0] - 2026-06-22”IWindowControl.SetVisiblePersist(bool)— show/hide a window AND persist the choice to the active layout slot (per resolution), so it survives relaunch. This is the single source of truth the framework reapplies on launch (the layout-editor eye-toggle writes the same slot). Plugins should use it for user-driven visibility toggles (hotkeys, close buttons) instead ofSetVisibleplus a private config key, which desyncs from the slot and loses to it on relaunch.SetVisibleis now documented as session-only (non-persisting).
- Native-UI edit-mode grab-boxes dropped to the bottom-left corner during loading / cutscenes. The game collapses its HUD to ~1px stubs during those transitions and the edit outline followed them down. It now holds each element’s last real-size rect (carried across the scene-change re-resolve) and never caches a collapsed stub.
- Repositioned game-UI elements flung off-screen / left at the game default after a cutscene.
SetRectno longer runs on an inactive element (its world-corners are garbage mid-cutscene), caps any bogus translate, and re-applies the saved position once the game resets the element; the 1 Hz re-assert no longer force-shows elements the game is hiding for a cutscene.
[1.3.0] - 2026-06-21
Section titled “[1.3.0] - 2026-06-21”- Native notice banners (
INoticeTipsonIPluginServices) — trigger the game’s own notice system (dungeon bars, win/fail banners, pop tips) with full control over content and audio via a fluent builder (Create/WithContent/WithAudio/WithDuration/Show).NoticeTipService.Show()is thread-safe: it enqueues the pre-built Lua chunk to aConcurrentQueuedrained on the main-thread tick, so plugins may call it from async continuations or any thread without marshalling boilerplate. (#15) - Consume hotkey keypresses (Settings → Hotkeys) — a global toggle that blocks bound keys from
reaching the game via Rewired while the framework still receives them through
UnityEngine.Input. Blocking is modifier-aware (Ctrl+F1 bound does not also block bare F1), and all keys are suppressed while a rebind cell is open. Backed byIHotkeyBlockDirectory+HotkeyKeyBlockPatch. (#15)
[1.2.0] - 2026-06-19
Section titled “[1.2.0] - 2026-06-19”- Loadout API (
ILoadoutonIPluginServices) — read the player’s saved in-game loadouts (the game’s “Role Plan” system: class + gear + spec + modules) and switch to one. Drives the game’s ownAsyncSwitchRolePlan(the path the in-game dropdown uses), so all server-side validation (combat lock, etc.) is respected, never bypassed. Backed byPandaLoadoutProbeover the tolua# Lua bridge +WorldProxy. - Notification toasts (
INotificationsonIPluginServices) — transient on-screen toasts any plugin can raise (Notify(message, kind, seconds)), rendered top-centre with a Pop+Scale animation, per-kind colour, and a countdown bar. First consumer: the LoadoutSwitcher plugin.
[1.1.2] - 2026-06-18
Section titled “[1.1.2] - 2026-06-18”- Launcher tile icons are now live-bound: a plugin icon that loads after the launcher is built (plugins register asynchronously, and the Full / Minimal layouts materialise their tiles at different times) no longer stays baked to the generic puzzle-piece fallback — it now appears in every launcher mode. Fixes a plugin icon showing in expanded mode but the puzzle-piece in minimal mode. (#11)
- Game-data name resolution: locale-gate the empty-
Name→NameDesignfallback so the design-name fallback only kicks in where it should, instead of leaking into clients that do have a localized name. (#12)
[1.1.1] - 2026-06-18
Section titled “[1.1.1] - 2026-06-18”- Plugin SDK on NuGet.org —
Stellar.Abstractions,Stellar.PluginContracts, andStellar.Plugin.InteropRefs(the Unity/Il2Cpp/BepInEx compile-time reference stubs) are published via Trusted Publishing, so plugins build with just<PackageReference>s — no framework checkout and no game install. First step toward per-plugin repos (see the DevKit’s DIP17 migration plan).
Changed
Section titled “Changed”- Framework runtime is unchanged from 1.1.0 (this release adds the SDK + contributor docs only).
[1.1.0] - 2026-06-18
Section titled “[1.1.0] - 2026-06-18”LineChartElement— multi-series time-series line chart with labelled X/Y axes, axis titles, a legend, auto-scaled Y, and interactive zoom (scroll/drag + −/+/Reset buttons + range scrollbar). Rendered via an injectedMaskableGraphicmesh (ChartGraphic). First consumer: CombatMeter history charts.- Generic inter-plugin exchange:
IPluginExchange(Provide<T>/Consume<T>) onIPluginServices, brokered purely byType, plus the newStellar.PluginContractsassembly that cooperating plugins reference (alongsideStellar.Abstractions) for shared contracts —FrozenEntityandIFrozenEntityViewer. (#5 Phase 2) - HUD text enhancements on
TextElement:FontSize,DynamicFontSize,ShadowDistance, andNoWrap, plusTextAlign(Left/Center/Right). Existing 6-argument call sites stay source- and binary-compatible via explicit secondary constructors. - HUD centering anchors
HudAnchor.ScreenCenterX/ScreenCenterY/ScreenCenter(resolution- independent via the RectTransform anchor system) andHudSpec.DynamicDefaultRect. IFramework.ScreenWidth/ScreenHeight, refreshed once per frame, for resolution-aware layout.ICombatSpeccombat-spec lookup;MeterRowData.SelfAccent.- Buff/skill
NameDesignfallback; leveledskill_level_idsnow resolve to their base skill names.
Changed
Section titled “Changed”- Icon-only overlay buttons are centered and equal-size.
- Hotkey handling, layout-editor input leak, combat-spec resolution, and theme-name sync (in-game hotfix batch, #2).
- Programmatic
SetRectis clamped on-screen so windows can’t be placed unreachable. BarElementfill width now tracksFraction01.
[1.0.1] - 2026-06-17
Section titled “[1.0.1] - 2026-06-17”- Per-frame
GameObject.Findleak that decayed FPS across dungeon re-entries.
[1.0.0] - 2026-06-08
Section titled “[1.0.0] - 2026-06-08”- In-game plugin launcher overlay
- Party roster for raid groups
[0.9.0] - 2026-05-29
Section titled “[0.9.0] - 2026-05-29”- Native HUD element editor (move / hide)
- PlayerHUD demonstrator