Skip to content

Changelog

All notable changes to the OxHeart package will be documented in this file.

The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.

[1.2.0] - 2026-09-07

Release Summary

  • New capabilities: adds an opt-in analytics abstraction, a Localization Health scanner, the [OxButton] inspector attribute, and Release Compliance tooling for producing and validating third-party compliance artifacts.
  • Service lifecycle: adds IServiceLocator.RegisterManaged<T>() for custom services that should participate in OxHeart's LateInitialize() and Shutdown() lifecycle.
  • Performance and reliability: reduces allocation and repeated-work overhead across save/load, resources, notifications, audio, events, diagnostics, and logging, while improving notification teardown and presentation behaviour.
  • Editor and sample fixes: makes compliance output reproducible across machines, improves localization report accuracy, fixes first-run notice generation, and removes a false registrar conflict from the Core Script Examples sample.
  • Unity compatibility and setup guidance: removes Unity 6000.6 deprecation warnings and corrects the documented dependency, import-order, and link.xml guidance for Asset Store installations.
  • Upgrade impact: there are no breaking changes to existing APIs, save data, serialized data, dependencies, or default behaviour, and existing projects require no migration. If you opt into RegisterManaged<T>(), remove any separate Application.quitting teardown for that service so Shutdown() is not called twice.

Added

  • Analytics abstraction (IAnalyticsService). A new optional, opt-in service (off by default — enable "Analytics Service" in ServiceConfiguration, under Feature Services) providing a single fire-and-forget TrackEvent(name, parameters) call site for telemetry. When enabled without a backend, the engine registers NullAnalyticsService — a silent no-op (ADR-018 null-object pattern). Buyers provide their own implementation via IServiceRegistrar; the registrar overwrites the default at startup with no engine code changes. The interface is intentionally minimal: TrackEvent, IsEnabled, SetEnabled — no batching, session tracking, or config SO, since those belong in the backend. See the Analytics documentation, Samples~/Scripts/13_AnalyticsSample.cs for the call site, and the new interactive Samples~/Analytics/ folder sample (config, scene, README) for a buffering backend registered with RegisterManaged and a Simulate Quit button that shows Shutdown() flushing the batch — matching the standard Scene Management and Game Settings established in 1.1. The sample's registrar activates only when the AnalyticsSample component is present in the loaded scene, so importing the sample never installs its console backend over your own in ordinary game scenes. Migration note: none — additive and off by default.
  • IServiceLocator.RegisterManaged<T>() — opt-in engine lifecycle for services you register yourself. A service created inside an IServiceRegistrar and handed to Register has always been yours to initialize and tear down: the engine calls LateInitialize() and Shutdown() only on services it created (from a ServiceConfiguration toggle or a ServiceRegistrationInfo entry). That left the two documented extension points behaving differently, and made the ADR-007 guidance to "use LateInitialize() for cross-dependencies" a no-op for registrars. RegisterManaged registers exactly like Register and additionally enrols the instance in the engine's lifecycle, so it receives LateInitialize() and Shutdown(); if it supersedes a service the engine created, that instance is retired rather than left enrolled and unreachable. It never calls Initialize() — registrars run after service creation, so initialize before registering, as the samples do. Migration note: none. Register is unchanged, so existing registrars behave exactly as before; the member ships as a default interface member that falls through to Register, so a custom IServiceLocator keeps compiling untouched (same approach as ISaveSystem.RequestSave). If you adopt RegisterManaged, drop any Application.quitting teardown you added for that service, or its Shutdown() will run twice.
  • Localization Health scanner (editor tooling) — diagnostic scanner for tables, configs, and asset references. New interactive window at Tools > OxHeart > Localization > Localization Health Scanner and reorganized menu. Scans localization CSV tables and LocalizationConfig assets to detect duplicate keys (LOC001), malformed rows (LOC002), missing language cells (LOC003), missing language headers (LOC004), empty tables (LOC005), named and indexed placeholder mismatches (LOC010, LOC011, LOC012), plural category gaps (LOC020) and unused plural forms (LOC021) via direct PluralRules probing, cyclic language fallbacks (LOC030), unsupported fallback targets (LOC031), table/config language mismatches (LOC032, LOC033), and unresolvable localized asset references (LOC040). Reports can be exported to deterministic JSON and Markdown. Absorbs and updates LocValidator, preserving existing CI entry points (LocValidator.ValidateAll() and ValidateFile()). Editor tooling only — no runtime service, no ServiceConfiguration toggle. See the Localization Health documentation. Migration note: none — additive.
  • [OxButton] inspector attribute. Place [OxButton] on any parameterless method in a MonoBehaviour or ScriptableObject to render a clickable button in the Inspector — no custom editor required. Supports a custom label ([OxButton("Do The Thing")]), works on private methods, records Undo, supports multi-object editing, and respects inheritance. The fallback editor uses isFallback = true, so it never interferes with types that already have a CustomEditor. See Samples~/Scripts/14_OxButtonSample.cs. Migration note: none — purely additive.
  • Release Compliance (editor tooling) — extends the Third-Party Notices generator. New menu items under Tools > OxHeart > Legal/: Generate Compliance Artifacts writes a deterministic compliance-manifest.json (per-component identity, version, source, direct/transitive from packages-lock.json, resolution method, confidence, evidence) and a concise CREDITS.md (one line per package, no license bodies) to Assets/Legal/, alongside the existing THIRD_PARTY_NOTICES.txt; Preview Compliance Diff logs added/removed/changed components without writing. Persistent decisions (manual entries, per-field overrides, orphan keep/remove/map) live in ProjectSettings/OxHeartComplianceOverrides.json — outside the package, so a package upgrade cannot overwrite them. License ids resolve from each package's declared package.json license field, and — since most Unity packages declare none — otherwise from distinctive markers in the LICENSE file text (an explicit SPDX-License-Identifier: tag, or a canonical license phrase: MIT / Apache-2.0 / BSD / ISC / MPL-2.0 / the Unity Companion and Unity Package Distribution licenses → LicenseRef-Unity-*). Anything without a confident match stays UNKNOWN — never guessed — and can be filled with a field override. Manifest and credits are deterministic (sorted, no timestamps) so they diff cleanly in review/CI. An opt-in build hook can warn or fail a player build when a component's license is UNKNOWN or when compliance changed since the last accepted manifest (both off by default); the artifact output folder, overrides path, and policy levels are configured on the Third-Party Notices manifest asset, and the same actions are available from its window. Editor tooling only (like the Setup Wizard) — no runtime service, no ServiceConfiguration toggle. Convenience tool, not legal advice — it covers the third-party UPM-package surface only, not trademark, marketing/AI disclosures, or loose-Assets/ licenses. See the Release Compliance documentation. Migration note: none — additive.

Performance

  • Completed the N4 GC-polish batch across the affected runtime paths. Audio and notification enum lookups now use explicit comparers rather than relying on EqualityComparer<T>.Default. This is hardening, not a measured saving on 6000.0: measured in an IL2CPP player on Unity 6000.0.82f1, the default comparer allocates 0 bytes over 2,000,000 lookups, so it no longer boxes there — the explicit comparers protect against the AOT runtimes where it did, and against the specialization changing again. recurring audio cleanup reuses its delay and scratch collections; notification-history reads reuse an immutable view until history changes; and dictionary-based game events copy into their pooled parameter storage instead of replacing it.
  • Reduced save/load allocation and repeated-work overhead without changing the save contract. SaveSystem builds a multi-domain container with one final checksum calculation instead of hashing after every domain mutation, GameSaveData maintains exact runtime byte accounting, and compression/encryption avoid avoidable growing-stream and ToArray() copies. Public GameSaveData mutation methods still update integrity immediately, and the serialized payload/checksum scheme is unchanged.
  • Bound resource-cache bookkeeping and made health diagnostics cheaper. Resource memory totals are maintained on cache mutation, stats use one cache pass, eviction-order entries are removed with their cache entries, release-by-object uses reverse indexes, and repeated runtime health checks reuse service-interface discovery instead of rescanning and reflection-invoking the locator.
  • Reduced logging work on disabled and emitted paths. Dynamic game-state transition messages are gated with OxLogger.IsEnabled, the Unity console sink reuses a thread-local formatter, and the file sink uses constant names for normal log levels/channels while preserving fallback formatting for flags and unknown values.

No public API, save format, serialized contract, dependency, or default behavior changed. Migration note: none.

Fixed

  • EventStopListening no longer throws when the engine is already torn down. Unsubscribing ran through a guard that threw InvalidOperationException: EventRegister not initialized whenever the cached ServiceLocator was absent — which happens routinely at teardown, because Unity gives no ordering guarantee between a MonoBehaviour's OnDestroy and the engine's shutdown, and the cache is deliberately cleared on every Play-enter for Fast Enter Play Mode safety. Every unsubscribe therefore had to be wrapped in a guard or a try/catch, and any that was not produced a red exception on stop — for a cleanup call with nothing left to clean up. EventStopListening now returns silently when there is no bus, on the same reasoning that makes removing an absent dictionary key a no-op. EventStartListening still throws, deliberately: a caller that believes it subscribed and silently receives nothing is the harder bug to find. See ADR-027. Migration note: none — code that previously guarded its unsubscribe keeps working unchanged; the guard is simply no longer required.
  • Save config cross-validation at init. ImprovedGameInitializer now cross-checks OxHeartConfig save flags (compressSaveFiles, encryptSaveFiles) against the corresponding ServiceConfiguration service toggles at startup, catching mismatches (e.g. compression flag enabled but Compression Service disabled) at init instead of on the first save I/O mid-session. The check is skipped entirely when EnableDataPersistenceService is false, so a project that never saves is not warned about save settings it does not use. It also surfaces via the editor Validate Setup context menu, Setup Wizard, and Dependency Checker.
  • Obfuscation/encryption precedence hardened. SaveConfig.ToRuntimeSettings() now uses an explicit conditional (UseEncryption = ... && !ObfuscateSaveFiles) instead of relying on property-setter side-effects and assignment order in an object initializer — reordering two lines could previously have silently flipped which protection was applied. A runtime warning is now emitted when both flags reach the runtime as true (covering the OnValidate editor-only gap for script-generated or YAML-edited config assets).

  • Importing the package into a Unity 6000.6 project no longer logs eight deprecation warnings. Unity 6000.6 ships two new compiler analyzers that the engine tripped on every compile — UAC0009 (the DEVELOPMENT_BUILD preprocessor directive is deprecated in favour of the managed-code-variant directives) and UAC0005 (AppDomain.CurrentDomain.GetAssemblies() may return assemblies Unity has already unloaded, risking errors or leaks). Warnings only — the package compiled and ran correctly on 6000.6 throughout — and nothing here changes behaviour on any Unity version. Every DEVELOPMENT_BUILD guard is now DEBUG, including the six [Conditional] attributes on OxLogger's Log/DebugLog/LogEmphasis (and GameLogger's). Unity defines DEBUG in the editor and in development player builds and leaves it undefined in release players — exactly where DEVELOPMENT_BUILD was defined — so the ADR-006 release-stripping contract documented in the Logger guide is unchanged: those call sites, argument evaluation included, still compile to nothing in a release build. The reflection scans that enumerate loaded assemblies (external IServiceRegistrar discovery, runtime service-interface discovery, and the editor's dependency checker, save-type validator and sample-scene builder) now route through a single helper that calls Unity's CurrentAssemblies.GetLoadedAssemblies() on 6000.6+ and keeps the previous AppDomain call, unchanged, on 6000.0–6000.5 where that API does not exist — the declared 6000.0 floor is untouched. Migration note: none — but if your own game code guards debug-only paths with #if DEVELOPMENT_BUILD, you will see the same UAC0009 warning on 6000.6; the fix is the same swap to #if DEBUG, or Debug.isDebugBuild for a runtime check.

  • Third-Party Notices: the Package Manifest window's "Generate Notices" button no longer throws when the curated file doesn't exist yet. It now creates the commented stub curated file first (matching the Tools > OxHeart > Legal > Generate Third-Party Notices menu item, which already did) instead of failing with FileNotFoundException: Curated notices source is configured but missing. The two entry points are now consistent. Migration note: none.

  • Release Compliance: evidence for a package resolved outside the project folder no longer varies by machine. External evidence is now anchored on the package id (<external>/com.example.foo/package.json) rather than on where the package happens to sit on disk, so the same dependency produces byte-identical manifest entries on a developer machine and on CI, and a package checked out directly into a home folder can no longer leak the user name into a committed artifact.
  • Release Compliance: a build no longer fails over an incidental error when no policy is armed. With only Regenerate on build enabled, a failure to write the artifacts (locked file, read-only output folder) logs an error and lets the build continue. When On UNKNOWN license or On changes is set to Warn/Fail, an incidental failure still fails the build — that configuration asked for a gate.
  • Localization Health: LOC040 is no longer reported once per LocalizationConfig. Findings are deduplicated across configs by expected path, key, and type, so two configs sharing a tables path report a missing asset once; configs with genuinely different tables paths still each report.
  • Localization Health: exported reports now list the component sources that were scanned. componentSources was always empty; it is now populated with the distinct prefab and scene paths the localized asset references came from.

  • NotificationPresenter now waits for the event bus instead of giving up. Services initialize asynchronously, so the presenter's OnEnable routinely ran before IEventManager was registered; it checked once, logged a warning, and never subscribed — leaving the presenter permanently deaf on a cold Play. It now retries until the bus is available. The warning it used to print was also wrong: it advised enabling an "Event Service" toggle on ServiceConfiguration, but the event service is a core service that is always registered and has no such toggle, so the advice sent buyers looking for a checkbox that does not exist.

  • NotificationPresenter no longer shows an untracked notification forever. The bundled presenter added a toast on NotificationSent and removed it only on Expired/Dismissed — events that never arrive for a notification sent with Duration <= 0 and IsPersistent false, so any buyer sending one got a permanent toast. It now applies its own display lifetime to exactly that case, configurable via Untracked Toast Seconds (default 3s; set 0 to disable). Persistent notifications are deliberately excluded — staying until dismissed is what they are for. Migration note: none — behaviour changes only for notifications that previously could never be removed.
  • Notification sample: Damage toasts stayed on screen permanently. DamageNotification passed a Duration of 0f commented "Instant", but a duration of 0 does not mean show briefly — NotificationService reads it as do not track: the notification is announced but never added to the active set, so it is never scheduled for auto-dismiss, no NotificationExpired or NotificationDismissed event ever names it, and Clear All cannot reach it either. Every other type in the sample cleared; Damage accumulated until the scene ended. It now uses a real 2-second duration, and the misleading comment is replaced with what the value actually means.
  • Importing the Core Script Examples sample through Package Manager no longer logs a false registrar conflict. That path compiles 09_GameServiceRegistrarSample.cs once in Assembly-CSharp and once in OxHeart-Samples; the duplicate GameServiceRegistrar now stands down when the wallet sample has already been registered. The guard and its cross-assembly marker live only in the bundled sample. Engine-level registrar conflict detection is unchanged: genuine two-registrar collisions still log an error naming both assemblies. Migration note: none — sample-only additive fix.

Documentation

  • Release Compliance: the license-resolution section now describes what the tool actually does — package.json and the LICENSE file are both read and reconciled, and conflicting evidence resolves to UNKNOWN rather than silently preferring one source. Added the reconciliation table, the <external>/… evidence form for packages resolved outside the project folder, a note that the manifest carries the disclaimer and scope statement, and an explanation of why the build gate evaluates policy before writing. Corrected the overrides filename in the artifact table (OxHeartComplianceOverrides.json, not compliance-overrides.json), and rewrote the orphan-decisions section: map rebinds a field override onto a detected replacement id, while keep/remove record review intent and do not add or drop components.
  • Localization Health: documented the three config-sanity findings that were missing from the table (LOC034_INVALID_LANGUAGE_HEADER, LOC035_DEFAULT_LANGUAGE_NOT_SUPPORTED, LOC036_EMPTY_SUPPORTED_LANGUAGES), corrected the LOC001 runtime consequence (the last duplicate silently overwrites earlier rows — the scanner's own message already said so), and added a Scope and Limitations section covering table discovery by convention, per-config LOC040 evaluation, and runtime-composed keys.
  • Analytics: added a Lifecycle section. The engine calls Initialize()/LateInitialize()/Shutdown() only on services it creates from ServiceConfiguration; a backend registered through IServiceRegistrar is outside that list, so a batching backend must flush on quit itself. The same boundary is now stated in the ADR-007 rules in the Architecture guide.
  • Menu Reference: added the two Release Compliance commands (Generate Compliance Artifacts, Preview Compliance Diff), which were missing.
  • Documentation home: the service index now lists Scene Management, Game Settings, and Analytics, adds an Editor Tooling section, and lists the Analytics sample. The Architecture guide's ADR index extends through ADR-024.
  • Third Party Notices.md corrected — it understated the packages OxHeart declares and gave the wrong import order. Addressables was listed as 2.5.0; the declared dependency is 2.9.1. Two declared built-in modules, com.unity.modules.audio and com.unity.modules.physics, were missing from the table entirely. The introduction also stated that Unity installs OxHeart's package dependencies automatically — true for a UPM install, false for the Asset Store .unitypackage, which is how you received OxHeart: Addressables and Newtonsoft JSON must be installed through Package Manager before importing, or the package will not compile. The publisher line also still carried pre-rebrand product naming. Nothing about your licence obligations changed; the file now describes the dependency set accurately.
  • Documentation~/README.md System Requirements corrected — same stale Addressables 2.5.0 claim, no uGUI entry, no mention of the built-in Audio and Physics modules, and no import-order warning. It now matches the package README and the notices file.
  • A hardcoded version was removed from the Deterministic RNG sample's README, which used .../1.1.0/Rng/ as a literal example of the imported-sample path.
  • Corrected the link.xml placement guidance, which promised protection on an install path that would not have had it. The bundled link.xml comment and the IL2CPP guide both stated that Unity honours a link.xml at a UPM package root. It does not — package-root link files are intentionally ignored by the linker, and the supported mechanisms for a UPM package are an assembly-embedded link.xml or IUnityLinkerProcessor.GenerateAdditionalLinkXmlFile. Your project is unaffected in OxHeart's shipped form: the Asset Store .unitypackage restores to Assets/OxHeart/, and Unity honours a link.xml anywhere under Assets/ — which is the mechanism the file has always actually relied on. What was wrong was the promise made for a different install path: the Quick Start also documented an "Add package from disk / git URL" route, and on that route the bundled file would have been silently ignored while the IL2CPP guide said engine types were "safe at any stripping level with no action on your part" — the linker would then have stripped the engine's reflection-created service constructors, surfacing at boot as Cannot register null service of type I… in an IL2CPP player only, with no compile error and no Editor symptom. The Quick Start no longer documents that route (.unitypackage import is the supported install), and both the comment and the guide now state the real rule plus what to do if you relocate the folder into Packages/ yourself: copy the four <assembly> entries into a link.xml under your own Assets/. Documentation and one XML comment only — no code, public API, serialized field, or <assembly> entry changed. Migration note: none.
  • Dropped stale "in 1.0" phrasing from the cheat sheet, config guide, docs README and Time Management page, and removed the 1.0.x sections from this changelog. Those versions were internal pre-release checkpoints that were never published, so they described releases no buyer received.
  • DataPersistence: encryption/obfuscation precedence corrected. DataPersistence.md previously stated that encryption takes precedence over obfuscation at runtime — the code does the opposite (obfuscation wins in all three resolution paths). Corrected to document the actual behaviour: obfuscation and encryption are mutually exclusive, obfuscation takes precedence.
  • DataPersistence: compression one-way-door warning added. DataPersistence.md § Compression and the Puzzle profile line in 02-SERVICES-CHECKLIST.md now warn that changing compressSaveFiles after release invalidates every existing save file (compression state is not self-describing on disk, unlike obfuscation/encryption which are recorded in the file extension).
  • Analytics: added a "What Your Backend Owns" section. Documents the boundaries the minimal interface leaves to the backend: asynchronous SDK start-up (Initialize() is synchronous and the engine has no "ready" concept — buffer events until your SDK accepts them, as the bundled sample backend does), consent (the engine neither gathers nor stores it, and never starts collection for you), event-name and parameter validation (passed through unvalidated; IReadOnlyDictionary<string, object> is deliberately wider than most backends accept, so convert in your wrapper), and batching/retries/offline queueing.

[1.1.0] - 2026-08-14

Release Summary

  • First public release: OxHeart 1.1.0 is the package's debut release on the Unity Asset Store; earlier 1.0.x versions were internal development checkpoints and were never published.
  • New game services: introduces opt-in Scene Management and Game Settings services, including asynchronous scene transitions, progress and fades, persistent display settings, and safe resolution confirmation and rollback.
  • Editor tooling: adds IL2CPP save-type validation and a Third-Party Notices generator to catch stripping-sensitive save fields and help projects assemble dependency attribution.
  • Ready-to-use presentation and samples: adds a default notification toast presenter and expands the bundled samples with scene-management, game-settings, persistence, audio, notification, and legal-notice improvements.
  • Reliability and compatibility: fixes Unity math-structure persistence, notification clearing, Addressables startup and version compatibility, optional Input System handling, audio sample lifecycle, and several misleading or incomplete diagnostics.
  • Product foundation: establishes the OxHeart package identity, removes the nonfunctional pre-release Asset Updater subsystem, and strengthens IL2CPP and versioned-documentation guidance before the public debut.
  • Upgrade impact: no migration is required because this was the first publicly available OxHeart version. The normal Semantic Versioning compatibility contract applies from this release onward.

Added

  • Tools > OxHeart > Validate Save Types — catches save fields IL2CPP stripping would silently drop. Scans every PersistableData subclass in your project and reports public auto-properties, the one save-data shape the managed linker can remove without any runtime signal. If nothing in your own code ever calls such a getter — ordinary for data written during play and read back only by the loader — the linker strips it, the serializer never sees the member, and its key is simply absent from the save file: no exception, no warning, no quarantine entry. The report names each type and property and gives the two fixes that work (a [SerializeField] backing field, or a link.xml entry), including the caveat that changing shape renames the serialized key and so needs a SchemaVersion bump plus a migration. This is deliberately an editor check: after stripping, reflection has no baseline to compare against, so a player build cannot detect what it lost — the shape has to be caught before the linker runs. Types in the engine's own assemblies are skipped, since the package-root link.xml already preserves them. Editor tooling only — no runtime service, no ServiceConfiguration toggle, no runtime cost. Migration note: none — additive.
  • Scene Management service (ISceneService). A new optional, opt-in service (off by default — enable "Scene Service" in ServiceConfiguration, under Game Services) for async scene load/unload with progress. LoadSceneAsync(nameOrKey, options) streams a scene from the Build Settings list or Addressables — one SceneSource.Auto option resolves Addressables-then-build-settings and falls back quietly with no error-log noise when Addressables has no entry for the key (the P1 quiet-fallback contract). Progress is reported as a monotonic 0 → 1 (Unity's allowSceneActivation 0.9 activation quirk is mapped) through both a direct OnLoadProgress event and pooled SceneEvents.LoadStarted/LoadProgress/LoadCompleted bus events. Optional CanvasGroup fade-out/fade-in is driven by the service's own DontDestroyOnLoad overlay canvas — it never touches your camera or game UI — and an optional AutoSaveBeforeLoad requests an immediate save through the AutoSave service before the load (warn-and-skip if AutoSave is disabled). The service is graceful: a missing scene, an empty key, or a load superseded by a newer one returns a failed SceneLoadResult and never throws; a second load cancels the first (single in-flight load, no queue — deliberately no scene-dependency graph). SceneServiceConfig supplies the fade colour, opt-in fade durations, and overlay sort order. Ships with the Scene Management sample (Samples/SceneManagement/): SceneTransitionSample + SceneTransitionA/SceneTransitionB scenes demonstrating Auto / Build Settings / Addressables loads with fades and an on-screen progress bar. See the "Scene Management" documentation and ADR-024. Migration note: none — additive and off by default.
  • Game Settings service (IGameSettingsService). A new optional, opt-in service (off by default — enable "Game Settings Service" in ServiceConfiguration) that owns the standard graphics/display options — quality tier, resolution + fullscreen mode, vsync, target frame rate — persisting them across restarts (PlayerPrefs), applying them on init, and raising SettingsChanged. Resolution changes go through a revert/confirm safety: ApplyResolution(...) applies the new mode immediately and returns an IResolutionChangeHandle; unless you Confirm() it before the timeout it auto-reverts to the last confirmed resolution. The countdown runs on the Time service's unscaled clock, so it still fires while the game is paused (timeScale = 0), and an unconfirmed change is reverted on the next launch — so a bad mode can never strand the player, even across a crash or quit mid-countdown. Per-setting "manage" toggles on the new GameSettingsConfig let a game hand only some settings to the service. Requires the Time service (on by default). Ships with a click-and-run GameSettings.unity scene wrapping the existing runtime-built options panel — the "Screen now" readout reports the actual measured frame rate (a Time.unscaledDeltaTime-based counter, averaged and refreshed twice a second) alongside the Target FPS setting, so stepping the target visibly caps the real rate rather than only showing the value you chose. CurrentResolution tracks the resolution the service itself last applied rather than reading Screen.currentResolution back — in windowed / FullScreenWindow (the default fullscreen mode), that Unity API reports the display's native mode and never reflects a Screen.SetResolution call, which made a confirmed resolution change look like it silently failed to persist even though it had genuinely saved and was correctly re-applied on the next launch. CurrentResolution still falls back to the raw display read when resolution is unmanaged. See the "Game Settings" documentation and ADR-023. Migration note: none — additive and off by default.
  • Default notification toast presenter (NotificationPresenter). A drop-in, sample-adjacent MonoBehaviour that renders INotificationService notifications as stacked, priority-coloured toasts — so buyers see notifications on screen without building UI first. Add it to any GameObject in an initialized scene (Notification + Event services enabled) and it builds its own uGUI canvas at runtime; no prefab or scene wiring required. It is a pure view: the notification service owns expiry/dismissal (and already never auto-dismisses Critical or persistent notifications), so the presenter runs no timers of its own — it adds a toast on NotificationSent and removes it on NotificationExpired/NotificationDismissed, honouring stacking, a configurable MaxVisibleToasts, priority ordering, and persistence. It lives in the OxHeart-Samples assembly; the engine core gains no dependency on it, and the NotificationSystem sample now spawns it automatically. Migration note: none — additive.
  • Third-Party Notices generator (editor tooling) + display sample. New editor tools under Tools > OxHeart > Legal/ that scan resolved UPM packages, read each package's license file (LICENSE.md / LICENSE.txt / LICENSE / Third Party Notices.md), merge them with a buyer-curated text file, and write a consolidated THIRD_PARTY_NOTICES.txt (default output: Assets/StreamingAssets/THIRD_PARTY_NOTICES.txt). A committed ThirdPartyNoticesManifest ScriptableObject holds paths, product name, exclude / force-include lists, and two opt-in build flags (RegenerateOnBuild, ValidateOnBuild — both off by default so the engine is never hostile-by-default). The Package Manifest window exposes one checkbox per package whose state mirrors the generator's include decision. This is editor tooling only (like the Setup Wizard) — no runtime service, no ServiceConfiguration toggle. Ships with the Third-Party Notices uGUI sample (Samples/ThirdPartyNotices/) that loads the generated file from StreamingAssets cross-platform and displays it in a scroll view, plus a click-and-run ThirdPartyNotices.unity scene (camera + component; no engine initializer). Docs carry an explicit "convenience tool, not legal advice — you own your attribution obligations" disclaimer. See the Third-Party Notices documentation. Migration note: none — additive.

Fixed

  • The Data Persistence sample silently saved 0 when a numeric field held non-numeric text. Typing something like abc into Level or Gold fell through a TryParse-or-zero helper, so the save reported success while the value read 0 — indistinguishable from the save system having dropped it. The sample now says so: the status line names the offending field and its rejected text, and a warning goes to the console. 0 is still stored so Save never blocks, and an empty field stays a quiet 0 (that is the ordinary "nothing typed yet" case, not a mistake). No engine change — the save system stored exactly the value it was handed; only the sample's input handling was silent about coercing it.
  • The Scene Management sample gave no hint that an Addressable scene's address must match the key. Ticking Addressable on a scene makes Unity default its address to the full asset path (Assets/…/Scenes/SceneTransitionA.unity), but LoadSceneAsync is called with the key SceneTransitionA — so the Addressables load path failed with nothing explaining why. ISceneService itself now returns an actionable error instead of a bare "not found": a failed SceneSource.Addressables load names the exact address the scene needs and warns that Unity's default address (the full asset path) will never match, and a failed SceneSource.BuildSettings load points at File > Build Settings. The sample's own failure message names the address too, and the Scene Management guide states the rule for your own scenes — including the wrinkle that SceneSource.Auto hides the mismatch by design (its Addressables probe is silent, so a wrongly-addressed scene quietly falls back to Build Settings and looks like it worked). Diagnostic text only — SceneLoadResult.Success, the failure conditions, and every API signature are unchanged.
  • ClearNotifications() left cleared notifications on screen forever. The method emptied the active set directly, so it raised no event at all — unlike DismissNotification() (which raises NotificationDismissed) and the auto-expire path (which raises NotificationExpired). Worse, clearing also suppressed the already-scheduled NotificationExpired: that path is guarded by an internal "was it still active?" check, which now found nothing to remove, so the event never fired either. Any UI driven by the documented notification events — including the bundled NotificationPresenter toast presenter new in this release — was therefore never told the notifications had gone, and kept displaying them permanently, for every notification regardless of duration. Reproducible in the Notification System sample: pressing Clear All emptied the service but left every toast on screen. A bulk clear is a manual removal, so it now raises one NotificationDismissed per cleared notification, exactly as a single dismiss does; the Dismissed-vs-Expired distinction that lets consumers tell a user action from a timeout is unchanged. Routing the clear through the shared removal helper also cancels each notification's pending auto-dismiss task, which the previous implementation left scheduled to fire and quietly no-op. Migration note: none — if you subscribe to NotificationDismissed you will now receive one event per notification when ClearNotifications() is called, which is the behaviour the event contract always implied.
  • The declared Addressables dependency floor (2.5.0) does not compile on Unity 6000.5+. Confirmed by importing the package into a clean project on Unity 6000.5.8f1 with com.unity.addressables at the declared 2.5.0 floor: compilation failed with error CS0619: 'Object.GetInstanceID()' is obsolete: 'Use GetEntityId instead.' inside Addressables' own source (AsyncOperationBase.cs, VirtualAssetBundle.cs). Unity 6000.5 hard-obsoletes Object.GetInstanceID() (an error, not a warning), and Addressables 2.5.0 still calls it — a genuine compile-time incompatibility baked into that package version, not a soft recommendation. 2.5.0 continues to compile fine on Unity 6000.0.81f1; 2.9.1 was confirmed clean on both 6000.0.81f1 and 6000.5.8f1 in the same clean-project test. The floor in package.json is now 2.9.1. Every EditMode/PlayMode/IL2CPP run in this repository has in fact compiled against 2.9.1 all along — the shipped dependency declaration was the only place still advertising the older, now-broken floor. Migration note: none — a strictly higher dependency floor, and no buyers have imported the incorrect one.
  • Every scene logged red Addressables console errors on boot in a fresh buyer project. The shipped DefaultResourceManagementConfig.asset — which every sample and every fresh consuming project resolves ResourceManagementConfig to by convention, since ServiceConfiguration.resourceManagementConfigName is blank everywhere — carried enableAddressables: 1. Any project that had not built Addressables groups (the default out-of-box state for anyone evaluating the free samples) got Addressables.InitializeAsync() called anyway, and Addressables logs its own "Unable to load runtime data" errors via internal Debug.LogError/LogWarningFormat calls as a side effect of running the operation — before the engine's own try/catch around the awaited handle ever gets control back, so no amount of exception handling in ResourceService could suppress it. Confirmed live in a clean buyer-simulation project: a built player was console-clean with the flag off and produced three Addressables errors on every affected scene with it on. enableAddressables now ships false on the default config. All 13 samples load their assets from Resources and were unaffected by the change. ResourceManagementConfig.enableAddressables's C# field default is unchanged (true) — that default is the intentional ADR-001 fallback used only when no config is registered at all — this fix is to the shipped asset's serialized value, not the class default. Flip it on once you have built Addressables groups for your project. Migration note: none — 1.1.0 is the debut, and any project that already built Addressables groups and wants the shipped default to demonstrate them can flip the one checkbox back on.
  • Sample scenes required the optional Input System package, breaking 8 of the 13 samples without it. Every EventSystem-bearing sample scene serialized an InputSystemUIInputModule, because the scene generator attached whichever input module existed on the machine that authored the scene — and that machine had com.unity.inputsystem installed. In a project without the package the component resolved to a missing script, leaving the EventSystem with no input module at all, so the on-screen buttons in Getting Started, Audio, Save/Load, Events, Localization, Notifications, Object Pooling and RNG did nothing. This contradicted the README's promise that with neither input backend available the samples "still compile, run, and work from their on-screen UI buttons" — the one configuration it explicitly guaranteed was the one that broke. The scenes now carry SampleEventSystemInstaller, which adds the right module at runtime: InputSystemUIInputModule when the package is genuinely installed and the new backend is active, StandaloneInputModule otherwise, and a clear warning when neither backend is usable. com.unity.inputsystem remains undeclared in package.json — the engine still never reads input. Migration note: none.
  • A registrar-name conflict between two IServiceRegistrar implementations sharing a type name was undiagnosable. 09_GameServiceRegistrarSample.cs explicitly recommends copying its GameServiceRegistrar pattern into your own game code — if that copy keeps the sample's original class name (an easy copy-first-rename-later slip) and the bundled sample stays compiled too, ConfigurableServiceInitializer's reflection-based registrar scan finds and runs both, and the second one's registrations collide with the first's. The resulting log, Service conflict: ... already registered by 'GameServiceRegistrar', now being overwritten by 'GameServiceRegistrar', named both sides identically, giving no way to tell that two different assemblies — not one buggy registrar calling itself — were involved. The conflict log now includes each registrar's assembly ('GameServiceRegistrar [YourGame]' vs 'GameServiceRegistrar [OxHeart-Samples]'), so the actual cause is visible immediately. Diagnostic-only change — no behavior, timing, or registration outcome is different. Migration note: none.
  • BasicAudio sample: music stopped responding to mute/volume, and couldn't be restarted, after playing for a few seconds. PlayerController.PlayMusicSound() played the bundled Demo_Music clip (8s) without loop: true, so it played once and completed naturally during ordinary testing — well within the time it takes to toggle mute and drag the volume slider. AudioService correctly disposes a completed one-shot's handle per its documented lifecycle (ADR-004), but the sample's _musicHandle field was never cleared, only IsValid flipped to false: ApplyVolumeToMusic() silently no-op'd on the stale handle (mute/volume appeared dead) and PlayMusicSound()'s dedup guard (_musicHandle != null) permanently blocked restarting. Fixed by playing the music bed with loop: true (matching its intended "keeps playing until stopped" role) and changing the guard to _musicHandle != null && _musicHandle.IsValid, so the sample recovers even if a future clip swap makes it non-looping again. Sample-only change — no engine API affected. Migration note: none.
  • Saving a Vector3 (or Quaternion, Color, Rect, Bounds, …) threw instead of saving. Storing any Unity math struct in a PersistableData failed with JsonSerializationException: Self referencing loop detected for property 'normalized'. The cause: Vector3.normalized returns a Vector3, which has its own normalized, so a general-purpose serializer reflecting over properties never terminates. The 08_DataPersistenceSample sample stored a Vector3 position and hit this, and no test covered it. JsonDataSerializer now registers converters for Vector2/Vector3/Vector4, Vector2Int/Vector3Int, Quaternion, Color, Color32, Rect and Bounds, each writing only the type's real components — which terminates the recursion and keeps payloads small (a Vector3 is three numbers, not the dozen derived members reflection would otherwise walk). Deserialization tolerates missing components (they read as zero) and ignores unknown ones, so a save written by an older or newer build still loads. Migration note: none — the previous behaviour was an exception, so no existing save file can contain one of these values. The written shapes ({"x":…,"y":…,"z":…}) are now part of the save format and are frozen under the same rule as ModelId strings and the checksum scheme.

Changed

  • Removed: the Asset Updater / asset-versioning subsystem. AssetUpdaterService, IAssetUpdaterService, VersionCatalog, SemanticVersion, AssetVersionInfo, the resource-service versioning extensions, the enableAssetUpdaterService toggle on ServiceConfiguration, the three Asset Updater fields on ResourceManagementConfig (remoteCatalogUrl, localCatalogFileName, assetDownloadSubPath), and the VersionedAssetsExample sample have all been deleted. The subsystem could not do what it advertised: the three config fields were never read (the initializer hardcoded an empty catalog URL and fixed persistentDataPath locations), and — more fundamentally — wiring them would not have helped, because downloaded files landed in a directory no resource loader ever consults, while the versioning extensions synthesized assetId_version keys and passed them through the ordinary Resources/Addressables/StreamingAssets loaders. There was no path from downloaded bytes to a loadable asset. The bundled sample also resolved a concrete type that was never registered, and the remote catalog's JSON schema was documented nowhere. Rather than ship a service whose core promise cannot be met, it is removed. Teams needing remote content today should use Addressables' own remote catalog support directly. No compatibility stubs or [Obsolete] shims were left behind — they would preserve exactly the dead surface this removal exists to eliminate. Migration note: none — this subsystem never reached a buyer. It was disabled by default, and no release of this package was ever public (see ADR-026); 1.1.0 is the debut. If you enabled the toggle while evaluating a pre-release build, remove your IAssetUpdaterService usages — nothing else in the engine referenced it, and every other ResourceManagementConfig and ServiceConfiguration setting is unchanged.
  • The engine is now OxHeart. The package has been renamed from its previous identity throughout: package id (games.moomoo.oxheart), display name, namespace root (OxHeart.*), assembly definitions (OxHeart-Core, -Runtime, -Helpers, -Utility, -Editor, -Samples), every public type that carried the old product name (OxHeartConfig, IOxHeartService, ...), the compile define symbols (OXHEART_DEV, OXHEART_TESTS_ENABLED, OXHEART_STARTUP_DIAGNOSTICS), the editor menu roots (Tools > OxHeart, Assets → Create → OxHeart, Add Component → OxHeart), the documentation site, and the repository and support URLs in package.json. The logger keeps the short form OxLogger. The brand is spelled OxHeart everywhere — internal capital, never Oxheart.
  • Nothing in the save contract moved. No save file format, ModelId, checksum scheme, or quarantine reason code (JSN / CHK / SCH, including the -BAK variants) changed. Five string literals deliberately keep their pre-rename spelling because they are save-format contract rather than branding: the package-default obfuscation salt and its companion secret in SecureKeyProvider, the AES key-derivation suffix in AesEncryptionService, the fallback XOR key in XorObfuscationService, and the PlayerPrefs key in GameSettingsService. The first four feed SHA256 key derivation — renaming them would produce a valid-looking wrong key and fail silently, so they are frozen, marked in source, and pinned by a regression suite replaying key vectors captured before the rename. See ADR-025.
  • Migration note: none. No version of this package was ever publicly released — 1.0.0 and every 1.0.x tag after it were internal checkpoints. 1.1.0 is the debut release, so there is no upgrade path from an older name and no deprecation shims for it. This is why a rename ships in a minor rather than a major; see ADR-026. From 1.1.0 onward the normal SemVer contract applies in full.
  • Sample save types now use [SerializeField] backing fields instead of auto-properties. DemoProfileData (DataPersistence sample) and PlayerData (08_DataPersistenceSample) changed shape so the samples model the pattern that is safe under IL2CPP managed stripping — see the IL2CPP entry under Documentation. Public API is unchanged: the same PlayerName/Level/Gold/Position/PlayTime properties exist with the same types and accessibility, so sample code and anything copied from it compiles and behaves identically. Only the serialized key names inside the samples' own save files change (Gold → _gold), which affects nothing but a sample save written by a pre-release build. Migration note: none.
  • Sample asset renamed: Samples/TestConfig.asset → Samples/SampleOxHeartConfig.asset. It is an OxHeartConfig sample asset, not a test artifact, and the old name read as one inside a shipped package. The asset GUID is unchanged, so any reference to it keeps resolving. Migration note: none — nothing in the package referenced this asset by GUID or by name, and 1.1.0 is the debut release.
  • Release tooling: releases are now cut from persistent release/vX.Y.x line branches, never from develop. Scripts/create-release.sh creates the line on the first cut of a minor; from then on every rc, final, and patch of that minor is a version-bump commit + annotated tag on the line. main is merged only for a final (non-rc) cut of the newest line, and the line back-merges to develop only when it is the newest line — patches to older lines touch neither (their fixes originate on develop and reach the line via Scripts/backport-commit.sh). New guards refuse malformed versions, existing tags, dirty trees on branch switches, and behind-origin branches. A new interactive Scripts/release-wizard.sh shows the live pipeline state (git, Asset Store review status via _internal/RELEASE_STATE.json, docs site) with a suggested next step, and drives cuts, store-submission prep, verdict recording, and docs publishing. Engine-internal only — no package code, public API, or asset changes. Migration note: none for buyers; maintainers cut from line branches from now on (see _internal/Documentation/Release/RELEASE_PIPELINE.md).
  • Release tooling: backport-commit.sh no longer offers its inline patch release; publish-theme-only.sh speaks the versioned publisher. The backport script's "create patch release?" path bumped only package.json (skipping OxHeartVersion.cs, CHANGELOG.md, and the README badges — a tag that fails the release checklist's own version sweep); it now ends by pointing at create-release.sh for the cut. publish-theme-only.sh derives the docs minor from its frozen snapshot and passes it to the now-versioned publish-docs-local.sh (preserving the site's latest alias), instead of calling it with the removed argument-less signature. Maintainer tooling only — no package code changes.

Documentation

  • IL2CPP guide: corrected the save-type stripping advice, which was insufficient. The guide told you to add [Preserve] to a PersistableData subclass that is only constructed by deserialization. That is true as far as it goes, but it misses the case that actually causes damage: stripping applies to members, not only whole types. A public auto-property whose getter is never called anywhere in your own code — ordinary for save data written during play and read back only by the loader — can be removed by the linker even though its class survives. Newtonsoft then never sees the member and the key is silently absent from the save file: no exception, no warning, no quarantine entry. Verified on Unity 6000.0 / IL2CPP / Managed Stripping High with the data class in a game assembly: [Preserve] on the class does not rescue it — the object still serializes, minus that property. What does work, and is now documented with a comparison table: a [SerializeField] private backing field (the recommended shape — the engine's contract resolver picks these up on purpose, including private fields declared on base classes) or an explicit link.xml entry for the type. Also notes that switching an existing auto-property to a backing field changes the serialized key (Coins → _coins) and therefore needs a SchemaVersion bump and a migration step. No code changed — public fields, [SerializeField] fields, and auto-properties your code does read were already safe, and Newtonsoft itself was never at risk.
  • The hosted docs site is now versioned per minor. The site publishes one folder per minor (/1.0/, /1.1/, ...) with a root versions.json driving Material's header version selector (extra.version.provider: mike in mkdocs.yml), and the root URL redirects to the latest published version. Tools/publish-docs-local.sh now takes the docs version (<X.Y> [--latest]), stages into that version's folder only, and refuses a version that does not match the tree's package.json — so docs builds stay anchored to the released tag. Publishing (commit + push of gh-pages) remains a manual, owner-reviewed step. Pages built before this change (the /1.0/ content) do not show the version selector; new versions do.

1.1.0 is the first public release of OxHeart on the Unity Asset Store. Earlier 1.0.x versions were internal pre-release checkpoints and were never published, so no changelog history precedes 1.1.0.