Remote Control
Change history for jp.lilium.livestudio.remotecontrol.
[0.25.3] - 2026-07-22
Added
RemoteConfirmSystemraises a confirmation on every surface at once — the OS dialog on the machine running the app and a modal in each connected remote app — and resolves it from whichever answers first, dismissing the rest. An operator working from a phone could not answer a prompt that only existed on the PC, and someone at the PC could not answer one that only existed in the remote app. Prompts travel as localization keys with the app's translation attached as a fallback, so each remote app renders them in ITS language. Answers come back overPOST /api/confirm(a default route, since the framework itself raises prompts).NativeConfirmDialogshows the OS dialog on its own thread and can be closed from another one. The blockingConfirmDialogcannot back a mirrored prompt: while it is up the app cannot serve the REST call carrying the remote answer, let alone act on it.ConfirmDialogstays for terminal, single-surface paths.RemoteControlBehaviour.TrySaveOrPromptsaves to the current path or opens the platform's "Save As" picker, picking the Editor or player dialog itself.QuitApiHandler.onQuitRequestingvetoes a quit asked for over REST. A player already hasApplication.wantsToQuitfor this, but in the Editor a remote quit forces play mode off and the resultingExitingPlayModecannot be aborted — so without a veto here the Editor tore down before an asynchronous unsaved-changes prompt could be answered, and the prompt never reached the remote apps at all. Pressing Stop in the Editor still uses the local, un-mirrored dialog: nothing can hold that path open.LiveSceneSaveSystem.ResetAllToDefaultreverts every contained object to its captured defaults regardless of dirty state.RevertAllToDefaultonly touches objects reporting dirty properties, and a load re-baselines everything as clean, so right after one it reverts nothing — a caller that has to discard the current state (switching to another project) needs the unconditional form.LoadCurrentData/LoadCurrentDataFromtakeforceBaseSceneReload, which reloads the base scene even when the file already targets the active one. Without it a load into the same base scene deserializes on top of the live state, so every value the (delta) file omits keeps whatever was there before.
Changed
- The unsaved-changes prompt shown when quitting now goes through
RemoteConfirmSystem, so it appears in the remote apps as well as on the machine. It no longer needs the deferring coroutine either: the prompt never blockswantsToQuit, which simply declines the quit and lets the answer start a new one.
Fixed
- The package compiles on Unity 6.5 (6000.5), where
Object.GetInstanceID/Resources.InstanceIDToObject— and theEntityId⇔intimplicit conversions that had kept them working since Unity 6.3 — were promoted to Obsolete errors. Every call site goes throughExposedObjectUtility, which is now the single place holding the version split, and the ID is carried aslongso it still fits onceEntityIdoutgrows 32 bits. The@instanceIDfield and the numeric-id fallback inExposedObjectHandlerare unchanged for callers; the values remain session-scoped and must not be persisted.
[0.25.2] - 2026-07-21
Added
ExposedObjectHandle.MarkClean/ExposedObjectDefaultRegistry.MarkCleanadopt an object's current state as the user-change baseline, so nothing on it counts as an unsaved edit until the next write. Only the user-change baseline moves; the serialization baseline keeps the captured defaults, so a delta save that follows still writes the full diff. Call it wherever state arrives from disk.
Fixed
- Unsaved-change detection no longer reports a scene as dirty on every launch, which blocked quitting a Studio that had opened an existing project. The check serialized the whole scene and string-compared it against the file, so it could not tell "the user edited something" apart from "this build serializes the scene differently than the build that wrote the file" — any project whose
live.jsoncame from an older version failed it forever. It now asks the per-object dirty tracking, re-baselined wherever state arrives from disk (scene load, save, deferred pending entries, asset load tails). IsDirtyandGetDirtyPropertiescompare in persistence shape (forPersistence: true), matchingCaptureDefaultsand the per-property baseline. Dirty must mean "a save would write something different", so read-only and non-persistable members — exactly what async loading churns after a restore: the crawl-builtExternalAssetManager.assetscatalog, rebuiltStageManager.sets,TransformRef.availableOwnerNames— no longer count.
[0.25.0] - 2026-07-17
Added
- Invoking exposed functions with arguments and through property paths:
ExposedPropertySerializer.BuildInvokeArgumentsbuilds a typed positional argument array from a JSON array (shared by the REST invoke path and callers that replay a stored call), andExposedObjectHandle.ResolveFunctionresolves a function reached through a property path (a nested function on a member), mirroring how the REST layer resolves nested members. ExposedProperty.TryGetValue<T>/TrySetValue<T>: read and write value-type members without boxing, through typed delegates (Func<object,T>/Action<object,T>) emitted by the Source Generator.ShowIf/HideIfaccept multiple conditions and can be placed on methods. Conditions serialize as avisibilityConditionsarray and are AND-evaluated on the client, so a member — or a function button — can be gated by several conditions at once. A single condition still emits the legacyvisibilityfield, keeping the type output byte-compatible.ImagePreviewAttribute: a read-only control that polls a server-relative image URL and renders it (backs the Capture camera preview).ExposedObjectRegistry.keyedCollectionVersionandNotifyKeyedCollectionChanged(), so consumers can invalidate cached property resolutions when a keyed collection's elements are rebuilt and stop writes landing on stale keys.
Changed
- Cut GC from the exposed property pipeline.
bool/enumgetters return canonical boxed instances viaBoxedValues;oldValuereads and property-change events are skipped when nothing is subscribed;FindProperty/GetPropertyare span-based (noname.ToString()); array-element descriptors are cached (ExposedPropertyType.GetArrayElement) and element lookup reads the collection once, building only the matched element;PropertyPathslash conversion andAppendIndexare allocation-free;ExposedUnityObject.componentsreplaces its LINQ filter with a two-pass hand roll. REST responses andscene.jsonare unchanged.
[0.24.3] - 2026-07-09
Added
AssetSelectorcontrol: asset-reference fields serialize as asset GUIDs (AssetRegistry, baked inOnValidateand registered at runtime).GET /api/asset?guid=resolves a GUID to the asset's name and type so clients can label the choices. Sub-assets use aguid:localIdcomposite key, and an optionalrefPropertycarries scheme-prefixed external keys (e.g.file:<path>#<sub>) whose target may not be loaded yet;AssetRegistrygains name-fallback and display-name resolution so those unloaded keys can still be labelled.GET /exposed/orphansandPOST /exposed/orphans/remove: root references a saved scene carries but whose object is absent this session are surfaced as removable "missing" items (path-carrying child overrides are excluded — they still bind once their asset loads asynchronously). Removing one drops every queued entry rooted at that id, so the next scene save stops re-emitting it.RemoteErrorNotifierforwards Unity errors (Error / Exception / Assert) to every connected remote app as an error notification. The broadcast is marshalled to the main thread (the server registry is mutated only there), identical messages are de-duplicated and bursts are capped so a per-frame exception cannot flood the app. Enabled by default (RemoteErrorNotifier.enabled).
Changed
- The live-scene serializer no longer logs an ambiguous-restore warning for persisted-file entries. They carry no
@nameand cannot be disambiguated by type, so they are left unresolved for the remote app to present and delete instead of emitting a warning the user cannot act on.
[0.24.2] - 2026-07-05
Changed
- Breaking:
GET /exposed/objectsandGET /exposed/object/{id}now return objects at depth 1 by default. A nested inline (unregistered) composite child is emitted as a truncation stub{ "@type": ..., "@truncated": true }instead of being fully expanded, keeping the payload small and scalable as the object graph grows. Arrays do not consume depth, so element count and per-element type stay visible, and registered children keep their@refform. Pass?nested(or?nested=true) to restore the previous unbounded expansion. Property reads (GET /exposed/object/{id}/{path}), SSE broadcasts, PUT responses and persistence (scene / project / preset) are unchanged — always fully expanded, so saved files stay byte-identical. Clients that walked nested values from the object list must either pass?nestedor lazily fetch each truncated child via the property GET.
Added
POST /exposed/batchendpoint applies multiple object / property / function operations in a single request with per-item continue-on-error (each item's status and body are echoed back in order). The exposed REST API is now documented inDocumentation~/openapi.yml.- Per-member persist scope:
[ExposedField]/[ExposedProperty]gain apersistScope(Scene default / Project), so the serializer can split live-scene state from per-class project settings. - Exposed key-path addressing: a property path can target an array element by a stable
[ExposedKey]value (e.g.expressions[Joy].weight), which backs the genericSetPropertyAction"bind to key" flow. [Collapsed]attribute: a hint that makes the remote app render an array or nested struct collapsed by default (the expand toggle remains). Emitted as a standalonecollapsedflag, independent of the property controller.elementTypeOptionsis emitted for polymorphic array properties so the remote app can offer "add an element of type …".- A dedicated
GET /api/performanceendpoint reports the app's current FPS and process memory (sampled from the player loop viaPerformanceMonitor; memory is read through the nativeProcessMemoryReaderon Windows), so the remote app can poll it only while an overlay is showing instead of piggybacking on other requests.
Fixed
ExposedPropertyUtility.CreateDefaultElementfalls back to the first concrete[ExposedClass]subtype for abstract / interface element types, so adding an element to a polymorphic[SerializeReference]array no longer throwsMissingMethodException.- Constructing an
ExposedObjectHandleno longer bakes an already-applied override value into its dirty-detection baseline. The constructor now usesEnsureDefaultsCaptured(capture only when unset, preserve an existing baseline) instead of an unconditionalSetDefault, so an override captured by live-scene restore before the handle was registered is no longer overwritten with the current (applied) value and then lost on save.
[0.23.6] - 2026-06-23
Changed
[StringSelector]can now annotate method parameters in addition to properties and fields. AnExposedFunctionargument so marked is rendered as a dropdown whose choices come from the owning object'ssourcePropertyNameproperty, letting a function take a constrained string argument (e.g. selecting a named stage mark).
[0.23.4] - 2026-06-22
Fixed
- A
?type=Xquery no longer drops a first-class[ExposedClass]component (e.g.AvatarControllerexposed as "Avatar") when its GameObject is also surfaced through a generic wrapper handle. The component and the wrapper are distinct exposed identities; the previous GameObject-identity de-duplication made RemoteApp pages report "No avatars available" once the avatar was exposed as anExposedGameObjectWithTransform. Genuine same-target duplicates are still collapsed viaFindByTarget. - Live scene serialization now skips only a real-but-destroyed (fake-null) Unity reference during a base-scene reload, while still serializing a pure proxy that legitimately never had a backing reference.
[0.23.2] - 2026-06-21
Fixed
FrameRate.AsFrameNumbernow computes indoubleinstead of casting throughfloat. Past 2^24 (~3.2 days at 60fps)floatlost integer precision, collapsing consecutive frame numbers onto the same value and stuttering the Studio playback buffer routing;doubleis exact for frame integers up to 2^53.
[0.23.0] - 2026-06-21
Added
[ExposedExternalEnum(typeof(T))]assembly attribute registers external or built-in enums (e.g.HumanBodyBones) as remote-app dropdowns without modifying the type; supportsexcludeNamesand the registration survivesReset().ExposedObjectSnapshotcan now capture an exposed object's default state and compute the delta against it (CaptureDefaults/CaptureDelta), the basis for prop presets.- The remote app is now notified when a live scene is saved.
Fixed
- Saved live-scene entries that reference an unresolved
@typenow deserialize away cleanly instead of leaving a null hole. - Live-scene restore now reapplies scene visibility and active state correctly on startup.
[0.22.1] - 2026-06-14
Fixed
- Live scene saves now record only a build-settings scene as
baseSceneName. Saving while an additive.scene.lsbworld (buildIndex -1) was the active scene previously wrote the world's name, so loading that file in a build without the world dirtied the active scene and blocked graceful quit with an unsaved-changes dialog. The name is now resolved to the active scene only when it is a build scene, otherwise the first loaded build scene (worlds are calibrated on top of it), applied to both the save and theHasUnsavedChangesbaseline.
[0.21.2] - 2026-06-11
Fixed
ExposedObjectUtility.InstanceIDToObjectnow calls the publicResources.InstanceIDToObjectinstead of reflecting the internalUnityEngine.Object.FindObjectFromInstanceID, which broke on Unity 6.1 (6000.3) where its argument changed fromintto anEntityIdstruct. The public API keeps anintargument across Unity 2021.3–6000.3.
[0.21.0] - 2026-06-08
Changed
- Member access is now backed by source-generated accessors, removing the runtime dependency on
Unity.Propertiesand reducing reflection overhead. ExposedObjectis now a readonly struct and was renamed toExposedObjectHandle, eliminating transient view allocations during property access.
Fixed
- Static
[ExposedClass]types no longer disappear from the exposed set when their static constructor runs before all game assemblies are loaded; attributes are re-scanned after assembly load. - Inherited
[ExposedClass]members are now registered in declaration order.
[0.20.12] - 2026-06-04
Added
switchSceneOnLoadoption for LiveScene loading, controlling whether loading a scene also switches to it.
[0.20.11] - 2026-06-03
Added
- Log the live scene file path when a scene is saved.
[0.20.10] - 2026-06-02
- No functional changes (version synchronized with the monorepo release).
[0.20.9] - 2026-06-02
Added
allowExternalConnectionsoption to bind the server to all network interfaces.
Changed
- Handlers are now registered per-instance with their routes declared in the constructor.