Skip to content

fix(prefab): edit prefab assets without prefab-stage save corruption (set_prefab_property)#45

Open
dehuaichendragonplus wants to merge 1 commit into
FunplayAI:mainfrom
dehuaichendragonplus:fix/prefab-asset-safe-property-edit
Open

fix(prefab): edit prefab assets without prefab-stage save corruption (set_prefab_property)#45
dehuaichendragonplus wants to merge 1 commit into
FunplayAI:mainfrom
dehuaichendragonplus:fix/prefab-asset-safe-property-edit

Conversation

@dehuaichendragonplus

Copy link
Copy Markdown
Contributor

Why

Editing a single field on a prefab that contains UGUI layout drivers, auto-sizing TMP text, or Spine components via the open_prefab_stageset_component_propertysave_prefab_stage three-piece silently corrupts unrelated fields in the .prefab asset.

Root cause (reproduced deterministically on Unity 6000.3.13f1, a pure open+save with zero edits already corrupts):

  • open_prefab_stage (PrefabStageUtility.OpenPrefab) instantiates the prefab into a live isolated scene with its own Canvas. [ExecuteAlways] edit-mode drivers then run without Play Mode: LayoutGroup/ContentSizeFitter and TMP auto-size dirty into CanvasUpdateRegistry and rebuild against the isolated canvas's default metrics, mutating RectTransform.m_SizeDelta/m_AnchoredPosition/m_LocalScale and TMP m_fontSize in memory; Spine SkeletonGraphic (which has no IsPartOfPrefabAsset guard) rewrites rects and, when an import/refresh reinitializes it, nulls its skeletonDataAsset.
  • save_prefab_stage / close_prefab_stage then call PrefabUtility.SaveAsPrefabAsset(stage.prefabContentsRoot, …), a wholesale re-serialize with no per-field change tracking, which faithfully writes those transient values back to disk.

Observed on real prefabs (each pure open+save, no intended edit):

  • A layout-heavy prefab: m_AnchoredPosition {155,-27} → {0,-27}, m_LocalScale 0.9885 → 1 frozen.
  • A Spine prefab's skeletonDataAsset {fileID:11400000, guid:…, type:2} → {fileID:0}runtime animation permanently lost (the {fileID:0} is bare with no guid, i.e. the field was genuinely null in memory, not a guid-reassignment churn).

There is no way to make SaveAsPrefabAsset write only the intended field, so a single-field edit through the stage is inherently unsafe on these prefabs.

What changed

New tools — set_prefab_property / set_prefab_properties (in ComponentPropertyFunctions): edit a component field (or fields) inside a .prefab asset in place, without opening a prefab stage:

AssetDatabase.LoadAssetAtPath<GameObject>(path)  →  resolve GameObject by hierarchy path
  →  resolve component by type  →  ComponentSerializer.WriteProperties(...)  →  AssetDatabase.SaveAssetIfDirty(root)

Because the prefab is never instantiated into a live scene, no layout/TMP/Spine rebuild tick runs, so only the field you set changes. Value format, [SerializeField] private support, and Object-reference handling ({"fileID":id} / {"assetPath":"Assets/…"}) are reused verbatim from set_component_property; the response echoes the post-write value (newValue / applied). Structural edits (add/reparent components) still use the stage.

Guard on save_prefab_stage / close_prefab_stage: before the wholesale save, scan the stage root for corruption-prone drivers (UGUI ILayoutController/ILayoutGroup, auto-sizing TMP text, Spine components — detected by interface/type name via reflection, no new UI/TMP/Spine assembly dependency). When present, the tool message carries a ⚠ WARNING naming the risk kinds and steering single-field edits to set_prefab_property. The save still proceeds (structural edits legitimately need it) — the corruption is just no longer silent.

Benefits

  • Fixes a data-loss bug generally — not Spine-specific. Any prefab with layout/auto-size/Spine drivers is safe to single-field-edit via the new tools.
  • API-correct replacement for the common "hand-edit the YAML text + AssetDatabase.ImportAsset(ForceUpdate)" workaround: SerializedObject resolves Object references ({fileID, guid, type}) correctly, which hand-written YAML routinely gets wrong.
  • No silent corruption on the stage path either — the guard makes the risk visible with an actionable next step.
  • Reuses existing ComponentSerializer machinery, so type coercion / private-field / object-ref behavior stays identical to set_component_property.

Compatibility

  • Additive. set_prefab_property / set_prefab_properties are new tools; nothing else changes behavior.
  • save_prefab_stage / close_prefab_stage keep their existing string return and success message verbatim — the warning is appended only when risk drivers are detected, so existing callers and Does.Contain("Prefab stage saved")-style checks are unaffected.
  • Driver detection is reflection-by-name/interface, so the package gains no dependency on com.unity.ugui, TextMeshPro, or Spine.
  • Note (documented in the tool description): any save that round-trips Unity serialization — including set_prefab_property — may upgrade a prefab authored by an older Unity/TMP version to the current serialization format. That is Unity's normal, harmless format migration and is orthogonal to this fix.

Testing & verification

  • Verified on a live Unity 6000.3.13f1 editor via the MCP tools: on the exact layout-heavy Spine prefab that the three-piece corrupts (3 RectTransform fields rewritten on a no-op save), set_prefab_property produces a 2-line diff — only the target field — and save_prefab_stage now emits the warning listing layout / Spine / TMP auto-size.
  • Object-reference and multi-field edits confirmed; error paths return structured codes (PREFAB_NOT_FOUND, PREFAB_GAMEOBJECT_NOT_FOUND, COMPONENT_NOT_FOUND_ON_TARGET with an available component list, PROPERTY_REQUIRED, PROPERTIES_REQUIRED).
  • 10 new EditMode tests (PrefabPropertyFunctionsTests) covering: only-target-field-changes + no-stage-opened, post-write echo, multi-field apply, root-path resolution, all error paths, and the layout-driver save warning. All pass; no regressions in the rest of the EditMode suite.

Changelog

Added under ## Unreleased (Added + Fixed).

…corruption

Add set_prefab_property / set_prefab_properties: edit a component field inside a
.prefab asset in place via SerializedObject, without opening a prefab stage.
Because no live isolated scene/Canvas is created, edit-mode drivers (UGUI layout,
ContentSizeFitter, TMP auto-size, Spine SkeletonGraphic) never recompute and
mutate the graph, so only the field you set changes. This is the safe alternative
to open_prefab_stage -> set_component_property -> save_prefab_stage for single-field
edits; value format and Object-reference handling reuse ComponentSerializer.

Also: save_prefab_stage / close_prefab_stage now warn when the open prefab contains
components that recompute in a stage, since PrefabUtility.SaveAsPrefabAsset
re-serializes the whole in-memory graph and freezes those transient values into the
asset (RectTransform/font freezing; Spine skeletonDataAsset zeroed to {fileID:0}).

Adds 10 EditMode tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant