feat: let a player answer for itself and hand back its files (v2.3.0) - #17
Conversation
A remote player could barely be reached. Of 61 commands only five ran outside the editor, and every command that produces an artifact wrote it to local disk and returned a path -- which names a file on someone else's machine when the service is on a phone or a colleague's editor. There was no outbound byte channel at all. Two changes fix that. A download route, GET with Range support, returns any file from the machine running the service. Both the editor and a player serve it: a colleague's editor is just as unreachable as their player. The path is unrestricted deliberately -- a session can already read any file by executing C#, so restricting the route would only push callers back to that without making anything safer. One response is capped at 32MB so a mistaken call cannot pull gigabytes into memory; larger files come back through Range. Then, everything that already worked at runtime stops claiming it needs the editor. Several handlers touched no editor API whatsoever and were gated only because their whole file was; others needed nothing more than their undo bookkeeping compiled out. That takes the player-capable set from 5 commands to 23, including the live scene hierarchy, GameObject and transform mutation, Game View capture, and profiler recording -- which, with the download route, finally makes "record on device, open the capture at home" possible. Two bounded exceptions. Undo does not exist in a player, so mutations made there cannot be reversed. And component/get|modify covers only components the project declares: the editor reports serialized names via SerializedObject, while a player must reflect over fields, and those two agree only for project types. Built-in components keep their state in native-backed properties whose getters can have side effects -- reading Renderer.material instantiates a copy -- so they are refused rather than reported in a second, incompatible shape. New runtime/info reports what the responding process is and where it keeps files, which is how a caller learns the absolute path to hand the download route. On Android and iOS consoleLogPath is empty because those platforms write no log file; a project's own logging decides what there is to retrieve. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Asking to modify fields and writing none came back ok with an empty modifiedFields list, so a caller whose field name was wrong -- or whose value did not parse as that field's type -- was told the write had landed while the value never changed. Return an error naming the fields that were requested instead. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 52ed89da15
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| return f.ToString("R"); | ||
| case double d: | ||
| return d.ToString("R"); | ||
| case Vector2 v2: | ||
| return $"{v2.x},{v2.y}"; |
There was a problem hiding this comment.
Format reflected numeric values with invariant culture
On players whose current culture uses a comma decimal separator, these formatters produce values such as 1,5, while TrySetFieldValue parses scalar and vector values using InvariantCulture and also treats commas as vector delimiters. Consequently, feeding component/get output back into component/modify either fails for scalar floats or silently changes a vector such as (1.5, 2.5) to (1, 5). Format every numeric component using invariant culture so the runtime representation round-trips.
Useful? React with 👍 / 👎.
| var optedIn = Attribute.IsDefined(field, typeof(SerializeField)); | ||
| if (field.IsPublic || optedIn) | ||
| { | ||
| fields.Add(field); |
There was a problem hiding this comment.
Exclude readonly fields from runtime component reflection
For a custom MonoBehaviour with a public readonly field, this predicate includes that field even though Unity does not serialize readonly fields. Runtime component/get therefore exposes a property that the editor implementation does not, and component/modify may either mutate an invariant through reflection or reject a field the preceding get advertised. Filter IsInitOnly fields, along with the other Unity serialization exclusions, before returning them.
Useful? React with 👍 / 👎.
What
A player already served
/command,/batchand/health, but almost everyaction was marked editor-only and there was no way to get a file off it. This
opens up what a player can genuinely answer, and adds a byte channel back.
runtime/inforeports the responding process's device facts and itswell-known paths (
consoleLogPath,persistentDataPath,temporaryCachePath,dataPath). Control partition grows 5 → 6; the registry totals 62./downloadserves any absolute path the target can read, capped at 32MBper response with
Rangefor anything larger. The editor serves it too — ateammate's editor is just as remote as their player.
editorOnlydropped from actions that already work at runtime:scene/hierarchy,gameobject/*,transform/set,screenshot/game_view,profiler/start|stop|status.Undocalls compile out;profiler/savekeepsthe flag because
ProfilerDriveris editor-only.component/get|modifyat runtime reflects over public and[SerializeField]private instance fields — what those components actuallyserialize. Built-in components are refused there rather than reported in a
second, incompatible shape, because the editor reports serialized names
(
m_LocalPosition) while reflection reports C# members.component/modifythat wrote nothing now fails instead of returning okwith an empty list. A wrong field name or an unparseable value used to be
reported as success while the value never changed.
Verified against a live IL2CPP development player
runtime/infodistinguishes the two processes (isEditor=false,WindowsPlayer, install-directorydataPath) where the editor reports its own.public,
[SerializeField]private, and inherited public — and excludes[NonSerialized], non-public, and static. Managed stripping did not interfere.component/modifyapplied 9 of 12 requested fields (int, float, string, bool,Vector3, Color, enum, private serialized, inherited), refusing the three that
are not serializable; values read back correct.
them through
SerializedObject.were both retrieved byte-identical over
/download, as was a 40MB file acrosssuccessive ranges.
🤖 Generated with Claude Code