Skip to content

feat(mcpforunity): support atlas sprite resolution by guid+spriteName/fileID#873

Merged
Scriptwonder merged 4 commits intoCoplayDev:betafrom
jiajunfeng:feat/atlas-sprite-resolution
Mar 11, 2026
Merged

feat(mcpforunity): support atlas sprite resolution by guid+spriteName/fileID#873
Scriptwonder merged 4 commits intoCoplayDev:betafrom
jiajunfeng:feat/atlas-sprite-resolution

Conversation

@jiajunfeng
Copy link

@jiajunfeng jiajunfeng commented Mar 6, 2026

Adds atlas sprite resolution support for object references using guid + spriteName and fileID forms.

  • resolves sprite references inside atlased assets more reliably
  • improves object reference handling for serialized component properties
  • keeps the change scoped to ComponentOps

Validation:

  • cherry-picked cleanly onto the latest CoplayDev/unity-mcp:beta

Summary by Sourcery

Improve handling of Unity object references in component serialization to better resolve atlased sprites from complex reference forms.

New Features:

  • Support resolving atlas sprites referenced by GUID plus spriteName in serialized properties.
  • Support resolving atlas sprites referenced by GUID plus fileID in serialized properties.

Enhancements:

  • Bypass reflection-based assignment for UnityEngine.Object members backed by JObject values so that SerializedProperty-based resolution can handle complex object references.

Summary by CodeRabbit

  • Bug Fixes
    • Better handling of complex Unity object references by bypassing reflection and deferring to serialized property resolution.
    • Added support for resolving sprites stored inside atlases by name.
    • Added support for resolving sprites by internal identifier when present.
    • Improved diagnostics and clearer error messages for atlas/sprite resolution failures, with safer fallback behavior.

@sourcery-ai
Copy link
Contributor

sourcery-ai bot commented Mar 6, 2026

Reviewer's Guide

Adds atlas sprite resolution support for Unity object references by detecting JObject values and deferring to SerializedProperty for UnityEngine.Object targets, and extends SetObjectReference to resolve sprites within atlases using guid+spriteName or guid+fileID, including a helper for fileID lookup.

Sequence diagram for atlas sprite resolution via guid+spriteName or guid+fileID

sequenceDiagram
    participant Caller
    participant ComponentOps
    participant SerializedProperty
    participant AssetDatabase
    participant Sprite
    participant GlobalObjectId
    participant ParamCoercion

    Caller->>ComponentOps: SetObjectReference(prop, jObj, out_error)
    ComponentOps->>jObj: read guid
    alt guid is valid
        ComponentOps->>AssetDatabase: LoadAssetAtPath~Object~(path from guid)
        AssetDatabase-->>ComponentOps: mainAssetOrNull
        ComponentOps->>jObj: read spriteName
        alt spriteName present
            ComponentOps->>AssetDatabase: LoadAllAssetsAtPath(path)
            AssetDatabase-->>ComponentOps: allAssets
            loop each asset
                ComponentOps->>Sprite: check is Sprite and name == spriteName
            end
            alt matching sprite found
                ComponentOps->>SerializedProperty: set objectReferenceValue = sprite
                ComponentOps-->>Caller: true
            else no matching sprite
                ComponentOps-->>Caller: false with error "Sprite not found in atlas"
            end
        else spriteName missing
            ComponentOps->>jObj: read fileID
            alt fileID present
                ComponentOps->>ParamCoercion: CoerceLong(fileID, 0)
                ParamCoercion-->>ComponentOps: targetFileId
                ComponentOps->>AssetDatabase: LoadAllAssetsAtPath(path)
                AssetDatabase-->>ComponentOps: allAssets
                loop each asset
                    ComponentOps->>Sprite: check is Sprite
                    alt is Sprite
                        ComponentOps->>GlobalObjectId: GetGlobalObjectIdSlow(sprite)
                        GlobalObjectId-->>ComponentOps: globalId
                        ComponentOps->>ComponentOps: compare globalId.targetObjectId with targetFileId
                    end
                end
                alt matching fileID sprite found
                    ComponentOps->>SerializedProperty: set objectReferenceValue = sprite
                    ComponentOps-->>Caller: true
                else no matching fileID
                    ComponentOps->>SerializedProperty: set objectReferenceValue = LoadAssetAtPath(path)
                    ComponentOps-->>Caller: true
                end
            else no fileID
                ComponentOps->>SerializedProperty: set objectReferenceValue = LoadAssetAtPath(path)
                ComponentOps-->>Caller: true
            end
        end
    else guid invalid
        ComponentOps-->>Caller: false with error
    end
Loading

Class diagram for updated ComponentOps object reference handling

classDiagram
    class ComponentOps {
        +bool TrySetViaReflection(component type propertyName normalizedName value out_error)
        +bool SetObjectReference(prop value out_error)
        -long GetSpriteFileId(sprite)
    }

    class SerializedProperty {
        +object objectReferenceValue
    }

    class AssetDatabase {
        +Object[] LoadAllAssetsAtPath(path)
        +Object LoadAssetAtPath~Object~(path)
    }

    class Sprite {
        +string name
    }

    class GlobalObjectId {
        +static GlobalObjectId GetGlobalObjectIdSlow(obj)
        +long targetObjectId
    }

    class ParamCoercion {
        +static long CoerceLong(token defaultValue)
    }

    class PropertyConversion {
        +static object ConvertToType(value targetType)
    }

    ComponentOps --> SerializedProperty : uses
    ComponentOps --> AssetDatabase : uses
    ComponentOps --> Sprite : resolves
    ComponentOps --> GlobalObjectId : uses
    ComponentOps --> ParamCoercion : uses
    ComponentOps --> PropertyConversion : uses

    note for ComponentOps "TrySetViaReflection now skips UnityEngine.Object targets when value is a JObject, deferring to SerializedProperty-based resolution. SetObjectReference can now resolve atlas sprites via guid+spriteName or guid+fileID using GetSpriteFileId."
Loading

File-Level Changes

Change Details Files
Bypass reflection-based assignment for UnityEngine.Object targets when the incoming value is a JObject, forcing the SerializedProperty path to handle complex object references.
  • Introduce an isJObjectValue flag based on JTokenType.Object.
  • Before writing to properties, early-return false when the target type is assignable to UnityEngine.Object and the value is a JObject.
  • Apply the same early-return logic for direct field writes and for serialized backing fields located via FindSerializedFieldInHierarchy.
MCPForUnity/Editor/Helpers/ComponentOps.cs
Enhance SerializedProperty object reference setting to resolve sprites inside atlases using guid+spriteName or guid+fileID, with a helper to map Sprite instances to file IDs.
  • After resolving a GUID to an asset path, inspect spriteName on the JObject and, if present, LoadAllAssetsAtPath and pick the Sprite whose name matches spriteName, returning an error if not found.
  • If spriteName is absent but fileID is present, coerce it to a long, iterate all sprites at the asset path, and select the sprite whose fileID matches via a new GetSpriteFileId helper.
  • Fallback to the previous behavior of loading the main asset at the path if neither spriteName nor a matching fileID yields a sprite.
  • Add GetSpriteFileId(Sprite) using GlobalObjectId.GetGlobalObjectIdSlow to obtain targetObjectId, with try/catch returning 0 on failure.
MCPForUnity/Editor/Helpers/ComponentOps.cs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Contributor

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues, and left some high level feedback:

  • In the SetObjectReference path for guid + spriteName/fileID, you call AssetDatabase.LoadAllAssetsAtPath(path) separately for the spriteName and fileID branches; consider loading once and reusing the result to avoid redundant asset database calls.
  • When a non-zero fileID is provided but no matching sprite is found, the code silently falls back to LoadAssetAtPath instead of surfacing an error like the spriteName branch; consider returning a specific error in this case to make reference resolution failures easier to diagnose.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In the `SetObjectReference` path for `guid + spriteName/fileID`, you call `AssetDatabase.LoadAllAssetsAtPath(path)` separately for the `spriteName` and `fileID` branches; consider loading once and reusing the result to avoid redundant asset database calls.
- When a non-zero `fileID` is provided but no matching sprite is found, the code silently falls back to `LoadAssetAtPath` instead of surfacing an error like the `spriteName` branch; consider returning a specific error in this case to make reference resolution failures easier to diagnose.

## Individual Comments

### Comment 1
<location path="MCPForUnity/Editor/Helpers/ComponentOps.cs" line_range="625-634" />
<code_context>
                         return false;
                     }
+
+                    var spriteNameToken = jObj["spriteName"];
+                    if (spriteNameToken != null)
+                    {
+                        string spriteName = spriteNameToken.ToString();
+                        var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
+                        foreach (var asset in allAssets)
+                        {
+                            if (asset is Sprite sprite && sprite.name == spriteName)
+                            {
+                                prop.objectReferenceValue = sprite;
+                                return true;
+                            }
+                        }
+
+                        error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
+                        return false;
+                    }
</code_context>
<issue_to_address>
**issue:** Sprite lookup via `spriteName` prevents `fileID`-based fallback when both are present.

When both `spriteName` and `fileID` exist, the `spriteName` path fails fast and prevents the `fileID` lookup from running. If `fileID` is intended to be more stable or authoritative, consider either preferring `fileID` when both are present, or only returning the `spriteName`-not-found error when no `fileID` is available so the `fileID` branch can still execute.
</issue_to_address>

### Comment 2
<location path="MCPForUnity/Editor/Helpers/ComponentOps.cs" line_range="629" />
<code_context>
+                        var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
</code_context>
<issue_to_address>
**suggestion (performance):** Avoid repeated `AssetDatabase.LoadAllAssetsAtPath` calls for the same path.

Suggested implementation:

```csharp
                        string spriteName = spriteNameToken.ToString();
                        if (!_allAssetsByPath.TryGetValue(path, out var allAssets))
                        {
                            allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
                            _allAssetsByPath[path] = allAssets;
                        }

                        foreach (var asset in allAssets)

```

To fully support this change, you also need to:

1. Add (or reuse, if already present) a cache field on the containing class, for example:
   `private static readonly System.Collections.Generic.Dictionary<string, UnityEngine.Object[]> _allAssetsByPath = new System.Collections.Generic.Dictionary<string, UnityEngine.Object[]>();`

2. If `System.Collections.Generic` is not already imported at the top of the file, add:
   `using System.Collections.Generic;`
   and then you can simplify the field to:
   `private static readonly Dictionary<string, UnityEngine.Object[]> _allAssetsByPath = new Dictionary<string, UnityEngine.Object[]>();`
</issue_to_address>

### Comment 3
<location path="MCPForUnity/Editor/Helpers/ComponentOps.cs" line_range="835" />
<code_context>
         }
+
+
+        private static long GetSpriteFileId(Sprite sprite)
+        {
+            if (sprite == null)
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Swallowing all exceptions in `GetSpriteFileId` may make failures hard to diagnose.

The `catch` in `GetSpriteFileId` returns `0` for any exception without logging, and `0` is also the sentinel for “no fileID,” which can mask real issues (e.g., misuse of the API or bad editor state). Please either log the exception (e.g., `Debug.LogWarning`/`LogError`) or narrow the `catch` to specific, expected exception types so unexpected failures remain visible.

Suggested implementation:

```csharp
        private static long GetSpriteFileId(Sprite sprite)
        {
            if (sprite == null)
                return 0;

            try
            {
                var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
                return (long)globalId.targetObjectId;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"GetSpriteFileId: failed to get GlobalObjectId for sprite '{(sprite != null ? sprite.name : \"<null>\")}'. Returning 0. Exception: {ex}");
                return 0;

```

If `System` or `UnityEngine` are not already imported at the top of this file, you will also need:
1. `using System;`
2. `using UnityEngine;`
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +625 to +634
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue: Sprite lookup via spriteName prevents fileID-based fallback when both are present.

When both spriteName and fileID exist, the spriteName path fails fast and prevents the fileID lookup from running. If fileID is intended to be more stable or authoritative, consider either preferring fileID when both are present, or only returning the spriteName-not-found error when no fileID is available so the fileID branch can still execute.

if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (performance): Avoid repeated AssetDatabase.LoadAllAssetsAtPath calls for the same path.

Suggested implementation:

                        string spriteName = spriteNameToken.ToString();
                        if (!_allAssetsByPath.TryGetValue(path, out var allAssets))
                        {
                            allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
                            _allAssetsByPath[path] = allAssets;
                        }

                        foreach (var asset in allAssets)

To fully support this change, you also need to:

  1. Add (or reuse, if already present) a cache field on the containing class, for example:
    private static readonly System.Collections.Generic.Dictionary<string, UnityEngine.Object[]> _allAssetsByPath = new System.Collections.Generic.Dictionary<string, UnityEngine.Object[]>();

  2. If System.Collections.Generic is not already imported at the top of the file, add:
    using System.Collections.Generic;
    and then you can simplify the field to:
    private static readonly Dictionary<string, UnityEngine.Object[]> _allAssetsByPath = new Dictionary<string, UnityEngine.Object[]>();

}


private static long GetSpriteFileId(Sprite sprite)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): Swallowing all exceptions in GetSpriteFileId may make failures hard to diagnose.

The catch in GetSpriteFileId returns 0 for any exception without logging, and 0 is also the sentinel for “no fileID,” which can mask real issues (e.g., misuse of the API or bad editor state). Please either log the exception (e.g., Debug.LogWarning/LogError) or narrow the catch to specific, expected exception types so unexpected failures remain visible.

Suggested implementation:

        private static long GetSpriteFileId(Sprite sprite)
        {
            if (sprite == null)
                return 0;

            try
            {
                var globalId = GlobalObjectId.GetGlobalObjectIdSlow(sprite);
                return (long)globalId.targetObjectId;
            }
            catch (Exception ex)
            {
                Debug.LogWarning($"GetSpriteFileId: failed to get GlobalObjectId for sprite '{(sprite != null ? sprite.name : \"<null>\")}'. Returning 0. Exception: {ex}");
                return 0;

If System or UnityEngine are not already imported at the top of this file, you will also need:

  1. using System;
  2. using UnityEngine;

… resolution

- Replace non-existent ParamCoercion.CoerceLong with fileIdToken.Value<long>()
- Add error return when fileID matches no sprite (consistent with spriteName branch)
- Remove extra blank line before GetSpriteFileId

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@coderabbitai
Copy link
Contributor

coderabbitai bot commented Mar 11, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3438a2c6-c798-432f-a7e5-b3c4abaebec3

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1bc51 and b61abdc.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Helpers/ComponentOps.cs

📝 Walkthrough

Walkthrough

Added JObject early-exit in reflection paths to defer UnityEngine.Object assignments to SerializedProperty; extended SerializedProperty GUID-based resolution to locate atlas Sprites by spriteName or internal fileID using a new GetSpriteFileId helper and improved error messages.

Changes

Cohort / File(s) Summary
Sprite & Object Reference Resolution
MCPForUnity/Editor/Helpers/ComponentOps.cs
Added early exit when incoming value is a JObject for reflection-based UnityEngine.Object members to delegate to SerializedProperty; extended SetObjectReference GUID handling to resolve atlas sprites by spriteName or internal fileID (new GetSpriteFileId() via GlobalObjectId with fallback and logging); expanded descriptive error messages for atlas/sprite resolution failures.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Poem

🐰 I hop through atlases, nose held high,

sniffing spriteNames that flitter by.
FileIDs hum where pixels hide,
Global IDs guide the path inside.
A little rabbit cheers: "Refs, resolved—goodbye!"

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and specifically describes the main feature: adding atlas sprite resolution support by guid+spriteName/fileID, which aligns with the core change in ComponentOps.cs.
Description check ✅ Passed The description covers the key aspects of the change with type of change indicated and detailed notes, though it lacks some template sections like Testing/Screenshots and explicit Documentation Updates checklist completion.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
MCPForUnity/Editor/Helpers/ComponentOps.cs (2)

624-641: Consider a more accurate error message.

The error at line 639 assumes the asset is an "atlas," but the GUID could point to any asset type (e.g., a single texture with sub-sprites, a texture that's actually not sliced, etc.). A more generic message would be clearer:

-                        error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
+                        error = $"Sprite '{spriteName}' not found in asset '{path}'.";

Also, the sprite name comparison is case-sensitive (sprite.name == spriteName). This matches Unity's behavior, so it's correct—just noting for clarity.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ComponentOps.cs` around lines 624 - 641, The error
message produced when a matching Sprite is not found in the loaded assets
assumes the asset is an "atlas"; update the message in the block handling
spriteNameToken (symbols: spriteNameToken, spriteName, allAssets,
prop.objectReferenceValue, path, jObj) to use a generic wording like "Sprite
'<name>' not found in asset '<path>'." Keep the sprite name comparison as-is
(case-sensitive) and ensure the new message includes spriteName and path so
callers know which lookup failed.

643-666: Clarify behavior when fileID is 0.

When targetFileId == 0 (line 647), the code skips the sprite search and falls through to line 668, loading the main asset. This might be intentional (fileID=0 often means "main asset" in Unity), but it creates an asymmetry:

  • fileID: 0 → falls through silently, loads main asset
  • fileID: 12345 (not found) → returns error

If fileID: 0 should explicitly mean "load main asset," consider adding a comment. If it's unintentional, you may want to handle it explicitly.

Also, same suggestion as above for the error message:

-                        error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
+                        error = $"Sprite with fileID '{targetFileId}' not found in asset '{path}'.";
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ComponentOps.cs` around lines 643 - 666, The code
silently falls through when targetFileId == 0; make this explicit by handling
the zero case for fileID (either add a clear comment that fileID==0 means "main
asset" or explicitly load the main asset and assign it to
prop.objectReferenceValue when targetFileId == 0), and ensure the logic around
GetSpriteFileId and the sprite search only runs for non-zero targetFileId; also
improve the failure path (the error returned when a non-zero targetFileId isn’t
found) to include the targetFileId and atlas path contextual details so the
error is unambiguous (refer to fileIdToken, targetFileId, GetSpriteFileId,
prop.objectReferenceValue).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Helpers/ComponentOps.cs`:
- Around line 837-851: In GetSpriteFileId(Sprite sprite) change the
ulong-to-long conversion to an explicit unchecked cast — use
unchecked((long)globalId.targetObjectId) when returning — and replace the silent
catch with a minimal debug log (e.g., using
UnityEngine.Debug.LogWarning/LogException or existing logger) that records the
exception and context (sprite name/instance) before returning 0 so failures
aren’t swallowed; update references in GetSpriteFileId and
GlobalObjectId.targetObjectId accordingly.

---

Nitpick comments:
In `@MCPForUnity/Editor/Helpers/ComponentOps.cs`:
- Around line 624-641: The error message produced when a matching Sprite is not
found in the loaded assets assumes the asset is an "atlas"; update the message
in the block handling spriteNameToken (symbols: spriteNameToken, spriteName,
allAssets, prop.objectReferenceValue, path, jObj) to use a generic wording like
"Sprite '<name>' not found in asset '<path>'." Keep the sprite name comparison
as-is (case-sensitive) and ensure the new message includes spriteName and path
so callers know which lookup failed.
- Around line 643-666: The code silently falls through when targetFileId == 0;
make this explicit by handling the zero case for fileID (either add a clear
comment that fileID==0 means "main asset" or explicitly load the main asset and
assign it to prop.objectReferenceValue when targetFileId == 0), and ensure the
logic around GetSpriteFileId and the sprite search only runs for non-zero
targetFileId; also improve the failure path (the error returned when a non-zero
targetFileId isn’t found) to include the targetFileId and atlas path contextual
details so the error is unambiguous (refer to fileIdToken, targetFileId,
GetSpriteFileId, prop.objectReferenceValue).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: d2eeda4b-3925-4b21-9c8b-4ca56f084d59

📥 Commits

Reviewing files that changed from the base of the PR and between 7615dd3 and 7fd9af2.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Helpers/ComponentOps.cs

Copy link
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@MCPForUnity/Editor/Helpers/ComponentOps.cs`:
- Around line 205-213: The current early return in ComponentOps (when propInfo
!= null, propInfo.CanWrite, isJObjectValue and the property type is a
UnityEngine.Object) skips invoking the property's setter and prevents
SetViaSerializedProperty from running; instead, check whether a
SerializedProperty exists before bailing and, if not found, use reflection to
set the property via the PropertyInfo setter. Concretely: in the branch that
checks propInfo, isJObjectValue and
typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType), call
SerializedObject.FindProperty(...) /
SerializedProperty.FindPropertyRelative(...) (the same lookup used by
SetViaSerializedProperty) and only return false if that lookup succeeds;
otherwise keep reflection enabled and invoke propInfo.SetValue(...) (or call the
existing codepath that uses PropertyInfo) to set the value so writable
UnityEngine.Object properties like sprite/material still get their setters
invoked.
- Around line 625-665: The method assigns a Sprite to prop.objectReferenceValue
and returns true immediately, which can hide failed assignments; update both
branches (the spriteName branch and the fileID branch in
ComponentOps.ResolveAtlasSprite) to verify the assignment stuck by checking
prop.objectReferenceValue != null (or equals the assigned sprite) after setting
it, and only return true if the reference is non-null; otherwise set the error
message (as done currently) and return false—follow the same verification
pattern used in ResolveSceneObjectByName and continue using GetSpriteFileId for
the fileID branch.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 622da504-f584-49dd-a1fc-86111010a5b0

📥 Commits

Reviewing files that changed from the base of the PR and between 7fd9af2 and 3e1bc51.

📒 Files selected for processing (1)
  • MCPForUnity/Editor/Helpers/ComponentOps.cs

Comment on lines 205 to +213
PropertyInfo propInfo = type.GetProperty(propertyName, flags)
?? type.GetProperty(normalizedName, flags);
if (propInfo != null && propInfo.CanWrite)
{
if (isJObjectValue && typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType))
{
// Let SerializedProperty path handle complex object references.
return false;
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

Does UnityEditor.SerializedObject.FindProperty resolve public C# properties like SpriteRenderer.sprite, or only serialized field/property paths?

💡 Result:

SerializedObject.FindProperty(string propertyPath) looks up Unity-serialized data by serialized “propertyPath”, not arbitrary runtime C# properties.

  • It does not resolve C# properties like SpriteRenderer.sprite by name; if you call FindProperty("sprite") you should expect null unless there’s actually a serialized field with that exact path/name. Unity’s serialization/Inspector pipeline works on serialized fields and does not invoke C# property getters/setters. [1], [2]
  • If the data is serialized via a field (public field or [SerializeField]), FindProperty can find it by its serialized field path. For auto-properties serialized via [field: SerializeField], the serialized name is the compiler-generated backing field (e.g. "<MyProp>k__BackingField"), not MyProp. [3], [4]
  • For built-in components, you typically must use Unity’s internal serialized field names (often m_...) rather than the public API property name; e.g. many UI components expose sprites via a serialized m_Sprite field (so FindProperty("m_Sprite") works there, not "sprite"). [5]

So: FindProperty is for serialized field/property paths, not for resolving public C# properties like SpriteRenderer.sprite by their API name.

Sources: [1] Unity Manual: Script Serialization, [2] Unity Scripting API: SerializedObject (notes about setters/validation), [3] Unity serialization note about backing fields for auto-properties, [4] Stack Overflow discussion of FindProperty returning null for C# properties, [5] example using FindProperty("m_Sprite") in editor code.


Don't bypass writable UnityEngine.Object properties unconditionally.

SerializedProperty.FindProperty() resolves only serialized field/property paths, not arbitrary C# properties. When this early return is hit, a writable property like sprite or material never reaches its setter, and the fallback to SetViaSerializedProperty(...) fails with "SerializedProperty ... not found" because public C# properties have no serialized path. Gate this fallback on FindProperty(...) actually succeeding, or keep reflection enabled for the PropertyInfo case.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ComponentOps.cs` around lines 205 - 213, The
current early return in ComponentOps (when propInfo != null, propInfo.CanWrite,
isJObjectValue and the property type is a UnityEngine.Object) skips invoking the
property's setter and prevents SetViaSerializedProperty from running; instead,
check whether a SerializedProperty exists before bailing and, if not found, use
reflection to set the property via the PropertyInfo setter. Concretely: in the
branch that checks propInfo, isJObjectValue and
typeof(UnityEngine.Object).IsAssignableFrom(propInfo.PropertyType), call
SerializedObject.FindProperty(...) /
SerializedProperty.FindPropertyRelative(...) (the same lookup used by
SetViaSerializedProperty) and only return false if that lookup succeeds;
otherwise keep reflection enabled and invoke propInfo.SetValue(...) (or call the
existing codepath that uses PropertyInfo) to set the value so writable
UnityEngine.Object properties like sprite/material still get their setters
invoked.

Comment on lines +625 to +665
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
return true;
}
}

error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}

var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = fileIdToken.Value<long>();
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
return true;
}
}
}
}

error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
return false;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🌐 Web query:

When assigning UnityEditor.SerializedProperty.objectReferenceValue to an incompatible UnityEngine.Object type, does Unity leave the property null?

💡 Result:

Yes—Unity generally rejects incompatible assignments to SerializedProperty.objectReferenceValue, and the serialized reference ends up cleared (null) (often with the Inspector showing “Type mismatch”). This behavior is commonly observed when code assigns a UnityEngine.Object that doesn’t match what the field/ObjectField expects. [1]

Be aware that “Type mismatch” doesn’t always mean the underlying reference is null: Unity has reported cases where the Inspector displays “Type mismatch” even though the reference is still present/usable (a visual/serialization-state issue, especially around Play Mode). [2]

Sources: [1] [2]


🏁 Script executed:

# Find the ResolveSceneObjectByName method in ComponentOps.cs and check for null-check pattern
fd -e cs -x grep -l "ResolveSceneObjectByName" {} \;

Repository: CoplayDev/unity-mcp

Length of output: 107


🏁 Script executed:

# Also search for SetObjectReference to understand the full context
fd ComponentOps.cs -x wc -l {} \;

Repository: CoplayDev/unity-mcp

Length of output: 164


🏁 Script executed:

# Look at the GameObjectComponentHelpers.cs caller mentioned in the review
fd GameObjectComponentHelpers.cs

Repository: CoplayDev/unity-mcp

Length of output: 201


🏁 Script executed:

# Search for ResolveSceneObjectByName in ComponentOps.cs
grep -n "ResolveSceneObjectByName" MCPForUnity/Editor/Helpers/ComponentOps.cs

Repository: CoplayDev/unity-mcp

Length of output: 349


🏁 Script executed:

# Look at the GameObjectComponentHelpers.cs file around lines 180-203 and search for calls to SetProperty/SetObjectReference
sed -n '170,210p' MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs | cat -n

Repository: CoplayDev/unity-mcp

Length of output: 2503


🏁 Script executed:

# Get more context from GameObjectComponentHelpers.cs to understand what SetProperty is
grep -n "SetProperty" MCPForUnity/Editor/Tools/GameObjects/GameObjectComponentHelpers.cs | head -20

Repository: CoplayDev/unity-mcp

Length of output: 180


🏁 Script executed:

# View the ResolveSceneObjectByName method implementation
sed -n '725,760p' MCPForUnity/Editor/Helpers/ComponentOps.cs | cat -n

Repository: CoplayDev/unity-mcp

Length of output: 1716


🏁 Script executed:

# Get the full SetObjectReference method to see the exact lines 625-665
sed -n '625,665p' MCPForUnity/Editor/Helpers/ComponentOps.cs | cat -n

Repository: CoplayDev/unity-mcp

Length of output: 2272


🏁 Script executed:

# Verify the full context of ResolveSceneObjectByName to see if it continues with component fallback
sed -n '725,770p' MCPForUnity/Editor/Helpers/ComponentOps.cs | cat -n

Repository: CoplayDev/unity-mcp

Length of output: 2095


Verify the sprite assignment actually sticks before returning success.

Both atlas sprite resolution branches return true immediately after assigning prop.objectReferenceValue = sprite without verifying the assignment succeeded. If the target field type does not accept Sprite, Unity rejects the assignment and leaves the reference null, yet this method still reports success. The caller (GameObjectComponentHelpers.cs:187) treats true as final success and does not retry, making this a silent no-op. The same file already uses the safer pattern in ResolveSceneObjectByName (lines 725-770): verify the reference is non-null before returning success.

Proposed fix
                     if (spriteNameToken != null)
                     {
                         string spriteName = spriteNameToken.ToString();
                         var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
                         foreach (var asset in allAssets)
                         {
                             if (asset is Sprite sprite && sprite.name == spriteName)
                             {
                                 prop.objectReferenceValue = sprite;
-                                return true;
+                                if (prop.objectReferenceValue != null)
+                                    return true;
+
+                                error = $"Resolved sprite '{spriteName}' is not compatible with '{prop.propertyPath}'.";
+                                return false;
                             }
                         }
@@
                             foreach (var asset in allAssets)
                             {
                                 if (asset is Sprite sprite)
                                 {
                                     long spriteFileId = GetSpriteFileId(sprite);
                                     if (spriteFileId == targetFileId)
                                     {
                                         prop.objectReferenceValue = sprite;
-                                        return true;
+                                        if (prop.objectReferenceValue != null)
+                                            return true;
+
+                                        error = $"Resolved sprite with fileID '{targetFileId}' is not compatible with '{prop.propertyPath}'.";
+                                        return false;
                                     }
                                 }
                             }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
return true;
}
}
error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}
var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = fileIdToken.Value<long>();
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
return true;
}
}
}
}
error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
return false;
var spriteNameToken = jObj["spriteName"];
if (spriteNameToken != null)
{
string spriteName = spriteNameToken.ToString();
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite && sprite.name == spriteName)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
error = $"Resolved sprite '{spriteName}' is not compatible with '{prop.propertyPath}'.";
return false;
}
}
error = $"Sprite '{spriteName}' not found in atlas '{path}'.";
return false;
}
var fileIdToken = jObj["fileID"];
if (fileIdToken != null)
{
long targetFileId = fileIdToken.Value<long>();
if (targetFileId != 0)
{
var allAssets = AssetDatabase.LoadAllAssetsAtPath(path);
foreach (var asset in allAssets)
{
if (asset is Sprite sprite)
{
long spriteFileId = GetSpriteFileId(sprite);
if (spriteFileId == targetFileId)
{
prop.objectReferenceValue = sprite;
if (prop.objectReferenceValue != null)
return true;
error = $"Resolved sprite with fileID '{targetFileId}' is not compatible with '{prop.propertyPath}'.";
return false;
}
}
}
}
error = $"Sprite with fileID '{targetFileId}' not found in atlas '{path}'.";
return false;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@MCPForUnity/Editor/Helpers/ComponentOps.cs` around lines 625 - 665, The
method assigns a Sprite to prop.objectReferenceValue and returns true
immediately, which can hide failed assignments; update both branches (the
spriteName branch and the fileID branch in ComponentOps.ResolveAtlasSprite) to
verify the assignment stuck by checking prop.objectReferenceValue != null (or
equals the assigned sprite) after setting it, and only return true if the
reference is non-null; otherwise set the error message (as done currently) and
return false—follow the same verification pattern used in
ResolveSceneObjectByName and continue using GetSpriteFileId for the fileID
branch.

- unchecked((long)targetObjectId) prevents OverflowException for large ulong fileIDs
- Replace silent catch with McpLog.Warn including sprite name and instanceID

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@Scriptwonder Scriptwonder merged commit ae13255 into CoplayDev:beta Mar 11, 2026
1 check passed
@jiajunfeng jiajunfeng deleted the feat/atlas-sprite-resolution branch March 17, 2026 07:32
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.

2 participants