Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

5 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

il2cpp-to-csharp-skill

中文

A skill for restoring readable C# from Unity IL2CPP binaries. It ships two independent backends that share the same goal and the same Il2CppDumper artifacts:

Backend Environment Docs
IDA (original) IDA Pro + IDA Pro MCP ida/SKILL.md
Ghidra (custom) Ghidra GUI or analyzeHeadless ghidra/SKILL.md

This is not a one-click decompiler. It helps an agent analyze user-provided VAs or function names and reconstruct C# while preserving strings, switch branches, lambdas, LINQ, async/coroutine state machines, and IL2CPP-specific quirks.

Backend selection: the root SKILL.md is a thin dispatcher. It picks exactly one backend — IDA Pro MCP when IDA MCP tools are available, Ghidra when the Ghidra setup is available — and then follows that backend's own SKILL.md. The two backends never mix instructions, so neither workflow is diluted.

Output quality depends on the AI model and the available context. Restored code is for reference only and should be manually reviewed against the decompiler output, DummyDll stubs, and runtime behavior.

Requirements

  • IDA backend: IDA Pro with IDA Pro MCP enabled.
  • Ghidra backend: Ghidra (GUI or analyzeHeadless).
  • Both: Il2CppDumper output — script.json, stringliteral.json, DummyDll/ or dump.cs — and Python 3 for the bundled helper scripts.

Installation

Install the skill in its own directory under the client's skills root. SKILL.md must be directly inside that directory, not one level deeper.

Mode Copy into <skill-directory>/
Both backends Root SKILL.md, ida/, and ghidra/
IDA only The contents of ida/
Ghidra only The contents of ghidra/

The two single-backend installations use the original backend files unchanged. The combined installation uses the root dispatcher to select one backend at runtime.

Installing through an agent

Give your agent this prompt; no local clone is required:

Install the skill from https://github.com/MetaMikuAI/il2cpp-to-csharp-skill. Before making changes, ask me to choose IDA only, Ghidra only, or both backends. Then detect this client's skills root and install the selected version as one skill in its own directory. For both backends, use the repository root dispatcher (`SKILL.md`, `ida/`, and `ghidra/`). For a single backend, use the contents of that backend directory as the skill root. Do not add an extra repository or backend directory level: `SKILL.md` must be directly inside the installed skill directory. If the destination already exists, inspect it and ask before replacing or merging anything. Verify the final layout and frontmatter, safely remove only the temporary checkout, and tell me whether a restart or new session is needed.

Preparation

  1. Open the IL2CPP binary, usually GameAssembly.dll, in IDA Pro and wait for initial analysis to finish.
  2. Run Il2CppDumper's IDA processing script, usually ida_with_struct_py3.py, inside IDA on the current database.
  3. Open DummyDll/Assembly-CSharp.dll in dnSpy and export it as a C# project. This gives the agent readable class, method, field, enum, generic, and [FieldOffset] stubs.
  4. Keep stringliteral.json available and provide its local path when string resolution is needed.

Usage

Install or copy this folder as a skill named il2cpp-to-csharp, then ask the agent to restore one function at a time:

Use $il2cpp-to-csharp to restore 0x180000000.
IDA Pro MCP is ready. The dnSpy-exported stub project from DummyDll/Assembly-CSharp.dll is at C:\path\to\DummyDllExport, and stringliteral.json is at C:\path\to\stringliteral.json.

Ghidra backend prompt example:

Use $il2cpp-to-csharp to restore rva:0x123456.
Ghidra is ready. Project at /path/to/ghidra-projects/game, program UnityFramework, stringliteral.json at /path/to/stringliteral.json.

Example

example 1

IDA

// local variable allocation has failed, the output may be wrong!
Cysharp_Threading_Tasks_UniTaskVoid_o ResultScene_UI_ResultSceneSavePannel__OverrideSave(
        ResultScene_UI_ResultSceneSavePannel_o *this,
        int32_t index,
        const MethodInfo *method)
{
  ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16_o stateMachine; // [rsp+20h] [rbp-38h] BYREF

  if ( !byte_18440B475 )
  {
    sub_18033D260(
      &Method_Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_Start_ResultSceneSavePannel__OverrideSave_d__16___,
      *(_QWORD *)&index);
    byte_18440B475 = 1;
  }
  memset(&stateMachine.fields.__1__state + 1, 0, 44);
  sub_18033CD60(&stateMachine.fields.__t__builder, 0LL);
  stateMachine.fields.__4__this = this;
  sub_18033CD60(&stateMachine.fields.__4__this, this);
  stateMachine.fields.index = index;
  stateMachine.fields.__1__state = -1;
  Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder__Start_ResultSceneSavePannel__OverrideSave_d__16_(
    (Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_o)&stateMachine.fields.__t__builder,
    &stateMachine,
    Method_Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_Start_ResultSceneSavePannel__OverrideSave_d__16___);
  return 0;
}


void ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16__MoveNext(
        ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16_o *this,
        const MethodInfo *method)
{
  struct ResultScene_UI_ResultSceneSavePannel_o *_4__this; // rdi
  __int64 v4; // rdx
  __int64 v5; // rcx
  __int64 v6; // rdx
  struct Cysharp_Threading_Tasks_UniTask_Awaiter_o v7; // xmm6
  __int64 v8; // rdx
  __int64 v9; // rcx
  __int64 v10; // rsi
  __int64 v11; // rax
  struct Cysharp_Threading_Tasks_CompilerServices_IStateMachineRunner_o *runner; // r8
  __int64 v13; // rax
  Cysharp_Threading_Tasks_UniTask_o u__1; // [rsp+30h] [rbp-28h] BYREF

  if ( !byte_18440BEC7 )
  {
    sub_18033D260(
      &Method_Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_AwaitUnsafeOnCompleted_UniTask_Awaiter__ResultSceneSavePannel__OverrideSave_d__16___,
      method);
    byte_18440BEC7 = 1;
  }
  _4__this = this->fields.__4__this;
  if ( !this->fields.__1__state )
  {
    u__1 = (Cysharp_Threading_Tasks_UniTask_o)this->fields.__u__1;
    this->fields.__u__1 = 0LL;
    this->fields.__1__state = -1;
LABEL_5:
    sub_180406670(&u__1, 0LL);
    if ( !_4__this )
      sub_18033D420(v5, v4);
    DEYU_AdpUISystem_PanelCollection_UIPanelBaseImpl__ClosePanel(
      (DEYU_AdpUISystem_PanelCollection_UIPanelBaseImpl_o *)_4__this,
      0LL);
    this->fields.__1__state = -2;
    sub_1804065D0(&this->fields.__t__builder, 0LL);
    return;
  }
  if ( !_4__this )
    sub_18033D420(this, method);
  _4__this->fields.m_BlockSubPanelCommand = 1;
  DEYU_AdpUISystem_PanelCollection_UIPanelBaseImpl__HidePanel(
    (DEYU_AdpUISystem_PanelCollection_UIPanelBaseImpl_o *)_4__this,
    0LL);
  u__1 = *ResultScene_UI_ResultSceneSavePannel__SavePlayerDataCoreAsync(&u__1, _4__this, this->fields.index, 0LL);
  sub_18033CD60(&u__1, 0LL);
  v7 = (struct Cysharp_Threading_Tasks_UniTask_Awaiter_o)u__1;
  if ( !byte_18440B3D8 )
  {
    sub_18033D260(&Cysharp_Threading_Tasks_IUniTaskSource_TypeInfo, v6);
    byte_18440B3D8 = 1;
  }
  if ( !v7.fields.task.fields.source
    || (unsigned int)sub_180004110(
                       0LL,
                       Cysharp_Threading_Tasks_IUniTaskSource_TypeInfo,
                       v7.fields.task.fields.source,
                       (unsigned int)_mm_extract_epi16((__m128i)v7, 4)) )
  {
    goto LABEL_5;
  }
  this->fields.__1__state = 0;
  this->fields.__u__1 = v7;
  sub_18033CD60(&this->fields.__u__1, 0LL);
  v10 = Method_Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_AwaitUnsafeOnCompleted_UniTask_Awaiter__ResultSceneSavePannel__OverrideSave_d__16___;
  if ( !byte_18440BF77 )
  {
    sub_18033D260(&Cysharp_Threading_Tasks_CompilerServices_IStateMachineRunner_TypeInfo, v8);
    byte_18440BF77 = 1;
  }
  if ( !this->fields.__t__builder.fields.runner )
  {
    v11 = *(_QWORD *)(*(_QWORD *)(v10 + 56) + 8LL);
    if ( (*(_BYTE *)(v11 + 306) & 1) == 0 )
      v11 = sub_180365730(*(_QWORD *)(*(_QWORD *)(v10 + 56) + 8LL));
    if ( !*(_DWORD *)(v11 + 224) )
      il2cpp_runtime_class_init(v11);
    (*(void (__fastcall **)(ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16_o *, struct Cysharp_Threading_Tasks_CompilerServices_AsyncUniTaskVoidMethodBuilder_o *, _QWORD))(**(_QWORD **)(v10 + 56) + 8LL))(
      this,
      &this->fields.__t__builder,
      **(_QWORD **)(v10 + 56));
  }
  runner = this->fields.__t__builder.fields.runner;
  if ( !runner )
    sub_18033D420(v9, v8);
  v13 = sub_180002C50(0LL, Cysharp_Threading_Tasks_CompilerServices_IStateMachineRunner_TypeInfo, runner);
  sub_180406800(&u__1, v13, *(_QWORD *)(*(_QWORD *)(v10 + 56) + 24LL));
}

GPT 5.5 (Codex)

private async UniTaskVoid OverrideSave(int index)
{
    m_BlockSubPanelCommand = true;
    HidePanel();

    await SavePlayerDataCoreAsync(index);

    ClosePanel();
}

Opus 4.6 (Copilot)

private async UniTaskVoid OverrideSave(int index) // By Opus 4.7
{
    this.m_BlockSubPanelCommand = true;
    HidePanel();
    await SavePlayerDataCoreAsync(index);
    ClosePanel();
}

mimo-v2.5-pro (Claude Code)

// ResultScene.UI.ResultSceneSavePannel
public async UniTaskVoid OverrideSave(int index)
{
    m_BlockSubPanelCommand = true;
    HidePanel();

    await SavePlayerDataCoreAsync(index);

    ClosePanel();
}

DeepSeek v4 pro (Claude Code) 1 retry

async UniTaskVoid OverrideSave(int index)
{
    m_BlockSubPanelCommand = true;
    HidePanel();
    await SavePlayerDataCoreAsync(index);
    ClosePanel();
}

Gemini 3.1 Pro (Copilot)

private async UniTaskVoid OverrideSave(int index)
{
    this.m_BlockSubPanelCommand = true;
    base.HidePanel();
    await this.SavePlayerDataCoreAsync(index);
    base.ClosePanel();
}

example 2

GPT 5.5 (Codex)

protected override IEnumerator OnPositiveBuffExecute(SpellExecutionContext spellExecutionContext)
{
    List<int> targetSeats = EventManager.Instance
        .TargetTablesAny(1000)
        .Except(EventManager.Instance.TargetTablesGuests(1000))
        .ToList();

    if (targetSeats.Count <= 0)
        yield break;

    List<NormalGuest> allNGuests = DataBaseCharacter.GetAllNormalGuests()
        .Where(x => /* __c.<OnPositiveBuffExecute>b__15_0, predicate not resolved by IDA MCP */ true)
        .ToList();

    GuestsManager guestManager = GuestsManager.Instance;

    Vector3 guestPosition = spellExecutionContext.GuestPosition.Value;
    Vector3 tablePosition = spellExecutionContext.GuestsController.DeskCode == -1
        ? default
        : GetGuestTable(spellExecutionContext.GuestsController.DeskCode);

    Vector3 aliceHand = guestPosition + new Vector3(
        tablePosition.x > guestPosition.x ? handOffset.x : -handOffset.x,
        handOffset.y,
        0f);

    bool dollEnd = false;

    PlayAudio(rewardSFX);

    foreach (int seat in targetSeats)
    {
        GameObject doll = Instantiate(rewardDoll);
        doll.transform.position = aliceHand;

        UIElementCluster uiElementCluster = doll.GetComponent<UIElementCluster>();

        Vector3 targetPosition = GetGuestTable(seat) + new Vector3(0f, -0.75f, 0f);
        uiElementCluster.GetObject<SpriteRenderer>(0).flipX = targetPosition.x > aliceHand.x;

        EventManager.Instance.StartCoroutine(SetLinenear(doll, uiElementCluster, seat));
        EventManager.Instance.StartCoroutine(
            LerpPosition(doll.transform, () => targetPosition, 1f));
    }

    yield return UnityCoroutineExtension.OneSecondDelay;

    dollEnd = true;

    yield return UnityCoroutineExtension.OneSecondDelay;

    IEnumerator SetLinenear(GameObject doll, UIElementCluster uiElementCluster, int seat)
    {
        LineRenderer lineRenderer = uiElementCluster.GetObject<LineRenderer>(0);
        lineRenderer.SetPosition(1, aliceHand);

        while (!dollEnd)
        {
            Vector3 position = doll.transform.position;
            lineRenderer.SetPosition(0, position + new Vector3(0f, 0.5f, 0f));
            yield return null;
        }

        Animator animator = doll.GetComponent<Animator>();

        if (EventManager.Instance.ShouldSkip)
        {
            animator.Play("AliceRewardDoll2");
            yield return UnityCoroutineExtension.HalfSecondDelay;
        }
        else
        {
            animator.Play("AliceRewardDoll1");

            SpriteMask mask1 = uiElementCluster.GetObject<SpriteMask>(0);
            SpriteMask mask2 = uiElementCluster.GetObject<SpriteMask>(1);

            NormalGuest guest1 = allNGuests.RandomSelectOne();
            NormalGuest guest2 = allNGuests.RandomSelectOne();
            SpriteRenderer dollSprite = uiElementCluster.GetObject<SpriteRenderer>(0);

            mask1.sprite = guest1.CharacterPixel.m_AnimatedSprites[1];
            mask2.sprite = guest2.CharacterPixel.m_AnimatedSprites[1];

            yield return UnityCoroutineExtension.QuarterSecondDelay;

            dollSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;

            yield return UnityCoroutineExtension.HalfSecondDelay;

            guestManager.SpawnNormalGuestGroup(
                new[] { guest1, guest2 },
                doll.transform.position,
                GuestGroupController.LeaveType.Fading,
                seat,
                false);

            yield return UnityCoroutineExtension.QuarterSecondDelay;
        }

        Destroy(doll);
    }
}

Opus 4.7 (Copilot)

protected override IEnumerator OnPositiveBuffExecute(SpellExecutionContext spellExecutionContext) // By Opus 4.7
{
    // NOTE(By Opus 4.7): cached __c.<OnPositiveBuffExecute>b__15_0 - Where predicate body
    // not directly resolvable from IDA (Method$ pointer indirection); preserved as
    // a placeholder lambda. Heuristic: filters unsuitable guests for the reward duo.
    List<NormalGuest> allNGuests = DataBaseCharacter.GetAllNormalGuests()
        .Where(g => /* TODO(By Opus 4.7): unresolved __c.<OnPositiveBuffExecute>b__15_0 */ true)
        .ToList();
    GuestsManager guestManager = MonoSingleton<GuestsManager>.Instance;

    Vector3 guestPos = spellExecutionContext.GuestPosition.Value;
    float xOffset;
    SpecialGuestsController guestsController = spellExecutionContext.GuestsController;
    if (guestsController.DeskCode == -1
        || GetGuestTable(guestsController.DeskCode).x > spellExecutionContext.GuestPosition.Value.x)
    {
        xOffset = this.handOffset.x;
    }
    else
    {
        xOffset = -this.handOffset.x;
    }
    Vector3 aliceHand = new Vector3(
        guestPos.x + xOffset,
        guestPos.y + this.handOffset.y,
        guestPos.z + 0f);
    bool dollEnd = false;

    PlayAudio(this.rewardSFX);

    List<int> tablesToReward = EventManager.Instance.TargetTablesAny(1000)
        .Except(EventManager.Instance.TargetTablesGuests(1000))
        .ToList();
    if (tablesToReward.Count <= 0)
        yield break;

    foreach (int seat in tablesToReward)
    {
        int capturedSeat = seat;
        GameObject doll = Instantiate(this.rewardDoll);
        UIElementCluster uiElementCluster = doll.GetComponent<UIElementCluster>();
        doll.transform.position = aliceHand;

        Vector3 tablePos = GetGuestTable(capturedSeat);
        Vector3 capturedTarget = new Vector3(
            tablePos.x + 0f,
            tablePos.y - 0.75f,
            tablePos.z + 0f);

        uiElementCluster.GetObject<SpriteRenderer>(0).flipX = capturedTarget.x > aliceHand.x;

        EventManager.Instance.StartCoroutine(SetLinenear_1(doll, uiElementCluster, capturedSeat));
        // original: <>c__DisplayClass15_1.<OnPositiveBuffExecute>b__2 returns capturedTarget
        EventManager.Instance.StartCoroutine(
            LerpPosition(doll.transform, () => capturedTarget, 1f));
    }

    yield return UnityCoroutineExtension.OneSecondDelay;
    dollEnd = true;
    yield return UnityCoroutineExtension.OneSecondDelay;
    yield break;

    // original: <OnPositiveBuffExecute>g__SetLinenear_1|15_1 (iterator state machine)
    IEnumerator SetLinenear_1(GameObject doll, UIElementCluster uiElementCluster, int seat)
    {
        LineRenderer lineRenderer = uiElementCluster.GetObject<LineRenderer>(0);
        lineRenderer.SetPosition(1, aliceHand);

        while (!dollEnd)
        {
            Vector3 dollPos = doll.transform.position;
            lineRenderer.SetPosition(0, new Vector3(
                dollPos.x + 0f,
                dollPos.y + 0.5f,
                dollPos.z + 0f));
            yield return null;
        }

        Animator animator = doll.GetComponent<Animator>();
        // NOTE(By Opus 4.7): BYTE4(EventManager.Instance[40].monitor) - an unresolved
        // boolean field on EventManager that picks between the two reward animations.
        if (/* TODO(By Opus 4.7): unresolved EventManager flag */ false)
        {
            animator.Play("AliceRewardDoll2");
            yield return UnityCoroutineExtension.HalfSecondDelay;
        }
        else
        {
            animator.Play("AliceRewardDoll1");
            SpriteMask spriteMask1 = uiElementCluster.GetObject<SpriteMask>(0);
            SpriteMask spriteMask2 = uiElementCluster.GetObject<SpriteMask>(1);
            NormalGuest guest1 = allNGuests.RandomSelectOne();
            NormalGuest guest2 = allNGuests.RandomSelectOne();
            SpriteRenderer dollSprite = uiElementCluster.GetObject<SpriteRenderer>(0);

            // NOTE(By Opus 4.7): NormalGuest vtable slot 6 returning an object whose
            // field at +0x28 (relative to its Fields) is a Sprite. Mapped here as a
            // portrait-style accessor; concrete member name unresolved.
            spriteMask1.sprite = guest1.GetVisual()/* TODO(By Opus 4.7): vtable._6 */.Sprites[0];
            spriteMask2.sprite = guest2.GetVisual()/* TODO(By Opus 4.7): vtable._6 */.Sprites[0];

            yield return UnityCoroutineExtension.QuarterSecondDelay;
            dollSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
            yield return UnityCoroutineExtension.HalfSecondDelay;

            guestManager.SpawnNormalGuestGroup(
                new NormalGuest[] { guest1, guest2 },
                doll.transform.position,
                true,
                seat,
                false);
            yield return UnityCoroutineExtension.QuarterSecondDelay;
        }

        UnityEngine.Object.Destroy(doll);
    }
}

mimo-v2.5-pro (Claude Code)

// GameData.Core.Collections.NightSceneUtility.SkillCollection.Spell_Alice
public override IEnumerator OnPositiveBuffExecute(SpellExecutionContext spellExecutionContext)
{
    // State machine: _OnPositiveBuffExecute_d__15
    // Captures: this, spellExecutionContext
    // (coroutine body below)
    yield return null; // placeholder — see MoveNext
}


// Spell_Alice.OnPositiveBuffExecute (coroutine body)
private IEnumerator OnPositiveBuffExecuteCoroutine(SpellExecutionContext spellExecutionContext)
{
    var closure = new DisplayClass15_0();
    closure._this = this;

    // Get empty tables (target tables minus occupied tables)
    var emptyTables = EventManager.Instance.TargetTablesAny(1000, null, null)
        .Except(EventManager.Instance.TargetTablesGuests(1000, null, null))
        .ToList();

    if (emptyTables.Count <= 0)
        yield break;

    // Get all normal guests and filter
    closure.allNGuests = DataBaseCharacter.GetAllNormalGuests()
        .Where(/* b__15_0: predicate — likely guest.IsNormal / guest.IsSeated check */)
        .ToList();

    closure.guestManager = GuestsManager.Instance;

    // Calculate Alice's hand position with directional offset
    var guestPos = spellExecutionContext.GuestPosition.Value;
    var deskCode = spellExecutionContext.GuestsController.DeskCode;

    float xOffset;
    if (deskCode == -1 || GetGuestTable(deskCode).x > guestPos.x)
        xOffset = this.offsetX;  // field at +0x28 (offset 10 from base)
    else
        xOffset = -this.offsetX;

    closure.aliceHand = new Vector3(
        guestPos.x + xOffset,
        guestPos.y + this.offsetY,  // field at +0x2C (offset 11 from base)
        guestPos.z);

    closure.dollEnd = false;

    PlayAudio(this.audioClip);  // field at +0x30 (offset 6 from base)

    foreach (var tableIndex in emptyTables)
    {
        // Spawn doll prefab at aliceHand position
        var doll = Instantiate(this.dollPrefab);  // field at +0x20 (offset 4 from base)
        doll.transform.position = closure.aliceHand;

        var uiCluster = doll.GetComponent<UIElementCluster>();
        var dollSprite = uiCluster.GetObject<SpriteRenderer>(0);
        var guestTablePos = GetGuestTable(tableIndex);
        var targetPos = new Vector3(guestTablePos.x, guestTablePos.y - 0.75f, guestTablePos.z);

        // Flip sprite based on direction
        dollSprite.flipX = targetPos.x > closure.aliceHand.x;

        // Start line-drawing coroutine
        EventManager.Instance.StartCoroutine(
            SetLinenear(closure, doll, uiCluster, tableIndex));

        // Start movement coroutine (LerpPosition)
        EventManager.Instance.StartCoroutine(
            LerpPosition(doll.transform, () => /* b__2: returns targetPos */));
    }

    yield return UnityCoroutineExtension.OneSecondDelay;

    closure.dollEnd = true;
    yield return UnityCoroutineExtension.OneSecondDelay;
}

// Spell_Alice.DisplayClass15_0.SetLinenear (local function / coroutine)
private IEnumerator SetLinenear(GameObject doll, UIElementCluster uiElementCluster, int seat)
{
    // State machine: _SetLinenear_1_d
    var lineRenderer = uiElementCluster.GetObject<LineRenderer>(0);
    lineRenderer.SetPosition(1, this.aliceHand);

    while (!this.dollEnd)
    {
        var dollPos = doll.transform.position;
        lineRenderer.SetPosition(0, new Vector3(dollPos.x, dollPos.y + 0.5f, dollPos.z));
        yield return null;
    }

    // Get animator and play appropriate animation
    var animator = doll.GetComponent<Animator>();

    if (/* game state condition — checks a boolean field on EventManager */)
    {
        animator.Play("w");  // StringLiteral_4133
        yield return UnityCoroutineExtension.HalfSecondDelay;
    }
    else
    {
        animator.Play("u");  // StringLiteral_4126

        // Set up sprite masks with random guest portraits
        var mask1 = uiElementCluster.GetObject<SpriteMask>(0);
        var mask2 = uiElementCluster.GetObject<SpriteMask>(1);

        var guest1 = allNGuests.RandomSelectOne();
        var guest2 = allNGuests.RandomSelectOne();

        var dollSprite = uiElementCluster.GetObject<SpriteRenderer>(0);

        // Access guest sprite via vtable slot 6
        var spriteData1 = guest1./* vtable._6 */();
        if (spriteData1 != null && spriteData1.Length > 1)
            mask1.sprite = spriteData1[1];

        var spriteData2 = guest2./* vtable._6 */();
        if (spriteData2 != null && spriteData2.Length > 1)
            mask2.sprite = spriteData2[1];

        yield return UnityCoroutineExtension.QuarterSecondDelay;

        // Set mask interaction and wait
        dollSprite.maskInteraction = SpriteMaskInteraction.VisibleInsideMask;
        yield return UnityCoroutineExtension.HalfSecondDelay;

        // Spawn guest group at doll position
        var guests = new NormalGuest[] { guest1, guest2 };
        var dollPosition = doll.transform.position;
        guestManager.SpawnNormalGuestGroup(guests, dollPosition, true, seat, false);

        yield return UnityCoroutineExtension.QuarterSecondDelay;
    }

    // Cleanup
    Destroy(doll);
}

example 3 — Ghidra backend

The same OverrideSave method as example 1, queried through the Ghidra backend instead of IDA:

python3 scripts/ghidra_query.py query \
  --project-location /path/to/ghidra-projects \
  --project-name game \
  --program UnityFramework \
  decompile name:ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16__MoveNext

Ghidra decompiler C (abridged from the complete decompile.c; class-init guards and builder plumbing elided)

void ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16__MoveNext
          (ResultScene_UI_ResultSceneSavePannel__OverrideSave_d__16_o *this, MethodInfo *method)

{
  Cysharp_Threading_Tasks_UniTask_o u__1;

  if (this->fields.__1__state == 0) {
    u__1 = this->fields.__u__1;
    this->fields.__u__1 = 0;
    this->fields.__1__state = -1;
LAB_00123456:
    UniTask_Awaiter__GetResult(&u__1);
    ResultScene_UI_ResultSceneSavePannel__ClosePanel(this->fields.__4__this, 0);
    this->fields.__1__state = -2;
    return;
  }
  this->fields.__4__this->fields.m_BlockSubPanelCommand = 1;
  ResultScene_UI_ResultSceneSavePannel__HidePanel(this->fields.__4__this, 0);
  ResultScene_UI_ResultSceneSavePannel__SavePlayerDataCoreAsync
            (&u__1, this->fields.__4__this, this->fields.index, 0);
  if (UniTask_Awaiter__IsCompleted(&u__1) == 0) {
    this->fields.__1__state = 0;
    this->fields.__u__1 = u__1;
    AsyncUniTaskVoidMethodBuilder__AwaitUnsafeOnCompleted(&this->fields.__t__builder, &u__1, this);
    return;
  }
  goto LAB_00123456;
}

Restored (same method as example 1 — both backends converge on the same source):

private async UniTaskVoid OverrideSave(int index)
{
    m_BlockSubPanelCommand = true;
    HidePanel();
    await SavePlayerDataCoreAsync(index);
    ClosePanel();
}

All names and addresses above are example placeholders, matching the skill's convention; for real work use the current binary's Ghidra output.

Repository Layout

SKILL.md               Dispatcher: backend selection + shared rules (start here)
ida/                   IDA backend (original skill, unmodified)
  SKILL.md             IDA workflow: ida-usage.md, ida-quirks.md, strings.md,
                       helpers.md, compiler-patterns.md, scripts/
ghidra/                Ghidra backend (custom skill, unmodified)
  SKILL.md             Ghidra workflow: ghidra-setup.md, ghidra-query.md,
                       ghidra-quirks.md, strings.md, helpers.md,
                       string-formatting.md, lambdas-closures.md, linq-generics.md,
                       coroutines.md, async.md, runtime-exceptions.md,
                       runtime-memory.md, scripts/, agents/

Each backend's scripts/ tree is self-contained; run its scripts from within that backend's directory so relative references stay valid.

About

A skill for BepInEx il2cpp modders.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages