reverseport 1.5.0 → 1.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (141) hide show
  1. package/README.md +71 -26
  2. package/THIRD_PARTY_NOTICES.md +1 -0
  3. package/bin/reverseport.mjs +13 -1
  4. package/dist/browser/CdpScreenshot.js +133 -5
  5. package/dist/cli/browserAdvancedCommands.js +6 -1
  6. package/dist/cli-runtime/{AnalysisProvider-CeyX2u-b.js → AnalysisProvider-BOjhIeCw.js} +1 -1
  7. package/dist/cli-runtime/{AnalysisSnapshotFiles-BuK_ymw5.js → AnalysisSnapshotFiles-CpXtlUrr.js} +3 -3
  8. package/dist/cli-runtime/{BinaryTargetResolver-CKRfFhLj.js → BinaryTargetResolver-uNu5jLfD.js} +4 -4
  9. package/dist/cli-runtime/{ClientConfigurationDocument-D0ZqgTCv.js → ClientConfigurationDocument-BvmXMdrN.js} +2 -2
  10. package/dist/cli-runtime/{ClientRegistrationStatus-DGpwJ4ry.js → ClientRegistrationStatus-BiBrkFVM.js} +3 -3
  11. package/dist/cli-runtime/{DirectAnalysis-DW3MvxW0.js → DirectAnalysis-DeVU6XXl.js} +8 -8
  12. package/dist/cli-runtime/{Doctor-CzwYS9nB.js → Doctor-DusrhiVR.js} +6 -6
  13. package/dist/cli-runtime/{ElectronFileScope-C1hUEp36.js → ElectronFileScope-k7Al_zxF.js} +1 -1
  14. package/dist/cli-runtime/{IdaConfiguration-rEjpuRQ1.js → IdaConfiguration-QsToVI_I.js} +4 -4
  15. package/dist/cli-runtime/{InventoryProjectionEvidence-Dt4Og45O.js → InventoryProjectionEvidence-bVXXybtM.js} +2 -2
  16. package/dist/cli-runtime/{InvestigationProviders-D-QCy-4c.js → InvestigationProviders-DhwicuUg.js} +1 -1
  17. package/dist/cli-runtime/{JavaScriptApplicationEvidenceGraph-CODAjVru.js → JavaScriptApplicationEvidenceGraph-CORocMEb.js} +5 -5
  18. package/dist/cli-runtime/{JavaScriptApplicationWorkflowEvidence-CCtwSg8r.js → JavaScriptApplicationWorkflowEvidence-9-Z4VWm1.js} +2 -2
  19. package/dist/cli-runtime/{JsonFiles-Ce_O5NcH.js → JsonFiles-v2nnQzBz.js} +1 -1
  20. package/dist/cli-runtime/{ManagedSnapshot-DthWfmy9.js → ManagedSnapshot-C0cIFxKC.js} +3 -3
  21. package/dist/cli-runtime/{OwnedCommand-CnK8-h3N.js → OwnedCommand-B0mAqpOm.js} +2 -2
  22. package/dist/cli-runtime/{PrivateRuntimeRoot-KNg6qLwo.js → PrivateRuntimeRoot-DMpVdoz-.js} +1 -1
  23. package/dist/cli-runtime/{ProcessOwnership-AOJaszxt.js → ProcessOwnership-Ddlg82OS.js} +1 -1
  24. package/dist/cli-runtime/{SafeOutputTree-D8z2Zrnu.js → SafeOutputTree-BYXf6HYg.js} +1 -1
  25. package/dist/cli-runtime/{SetupHost-CzzRF89z.js → SetupHost-BC1WGGjw.js} +5 -5
  26. package/dist/cli-runtime/{SupportedClients-BrZ2qZVg.js → SupportedClients-Cag4oXyu.js} +1 -1
  27. package/dist/cli-runtime/{Update-DF55X_N2.js → Update-DYBlczEy.js} +1 -1
  28. package/dist/cli-runtime/{WindowsBatchCommand-5gT-d7O4.js → WindowsBatchCommand-Chfe9gzr.js} +1 -1
  29. package/dist/cli-runtime/{ZipArtifactReader-Db65WuM_.js → ZipArtifactReader-DziTscTm.js} +1 -1
  30. package/dist/cli-runtime/{application__DirectAnalysisStatus-OZFIFjZr.js → application__DirectAnalysisStatus-CKDY-9pN.js} +2 -2
  31. package/dist/cli-runtime/{application__EnhancedTools-C__HAdqG.js → application__EnhancedTools-BRAWz8_S.js} +3 -3
  32. package/dist/cli-runtime/{application__FunctionAnalysisEvidence-fWSgvV74.js → application__FunctionAnalysisEvidence-DWYRXeqm.js} +2 -2
  33. package/dist/cli-runtime/{application__ReconstructionCoverageService-DlNt-4TY.js → application__ReconstructionCoverageService-BD8T9JLy.js} +2 -2
  34. package/dist/cli-runtime/{application__ReconstructionObligationLedgerService-CEpbUv-7.js → application__ReconstructionObligationLedgerService-DORXlBCK.js} +4 -4
  35. package/dist/cli-runtime/{application__Setup-BExVGpqG.js → application__Setup-DAqu2s8O.js} +4 -4
  36. package/dist/cli-runtime/{application__Uninstall-BVgWDCPN.js → application__Uninstall-CEp-pG4Y.js} +3 -3
  37. package/dist/cli-runtime/{application__UpdateRuntime-Dj6UIhHQ.js → application__UpdateRuntime-BFjLzGV0.js} +5 -5
  38. package/dist/cli-runtime/{application__android__AndroidApplicationService-CyynwW6Z.js → application__android__AndroidApplicationService-B_3ijho2.js} +3 -3
  39. package/dist/cli-runtime/{application__apple__AppleApplicationService-BNPcWeL8.js → application__apple__AppleApplicationService-D5tIo-c3.js} +3 -3
  40. package/dist/cli-runtime/{application__harmony__HarmonyApplicationService-W66PwMvx.js → application__harmony__HarmonyApplicationService-CRA3tGvH.js} +3 -3
  41. package/dist/cli-runtime/{application__javascript__ElectronActiveObservationService-D8VbeCxz.js → application__javascript__ElectronActiveObservationService--W_nJQk_.js} +1 -1
  42. package/dist/cli-runtime/{application__javascript__ElectronObservationService-FmXFG4f2.js → application__javascript__ElectronObservationService-tfAD3Ice.js} +1 -1
  43. package/dist/cli-runtime/{application__javascript__JavaScriptApplicationService-D-dVrmAY.js → application__javascript__JavaScriptApplicationService-CEZLGA_3.js} +11 -11
  44. package/dist/cli-runtime/{application__javascript__JavaScriptApplicationWorkflowService-Cb0jmh8J.js → application__javascript__JavaScriptApplicationWorkflowService-D3TC2eRI.js} +5 -5
  45. package/dist/cli-runtime/{application__javascript__JavaScriptRuntimeReconciliationService-D55gO7Gq.js → application__javascript__JavaScriptRuntimeReconciliationService-CBhOf3VW.js} +4 -4
  46. package/dist/cli-runtime/{application__javascript__JavaScriptSemanticTraceService-DivM-Ym-.js → application__javascript__JavaScriptSemanticTraceService-BcLmz5aS.js} +4 -4
  47. package/dist/cli-runtime/{artifacts__ArtifactProvider-B_4h3RAw.js → artifacts__ArtifactProvider-Vi0e4diS.js} +10 -10
  48. package/dist/cli-runtime/{binary-9Gt3vJ1P.js → binary-BS-WxQAw.js} +20 -20
  49. package/dist/cli-runtime/{browserScenario-CTNr5ISp.js → browserScenario-CkFQ5Rmd.js} +3 -3
  50. package/dist/cli-runtime/{catalogIdentity-EYVhBUYJ.js → catalogIdentity-D843Oy_i.js} +2 -2
  51. package/dist/cli-runtime/cli.js +177 -63
  52. package/dist/cli-runtime/{composition__browserScenario-DNr3A_BM.js → composition__browserScenario-Ct2YyVW4.js} +1 -1
  53. package/dist/cli-runtime/{composition__electronObservation-DNw1f38M.js → composition__electronObservation-DoZILMUH.js} +2 -2
  54. package/dist/cli-runtime/{composition__electronScenario-Cu7PX270.js → composition__electronScenario-DBBedpL1.js} +2 -2
  55. package/dist/cli-runtime/{doctorRuntime-D5y43lEp.js → doctorRuntime-c5GxpwJd.js} +3 -3
  56. package/dist/cli-runtime/{dosMz-pwwUuxyC.js → dosMz-BZKOsUf9.js} +1 -1
  57. package/dist/cli-runtime/{dotnet__ManagedStaticProvider-BSnWggeb.js → dotnet__ManagedStaticProvider-DiSbEgZQ.js} +1 -1
  58. package/dist/cli-runtime/{enhancedInputs-K8C_o3b4.js → enhancedInputs-ItbV9oua.js} +1 -1
  59. package/dist/cli-runtime/{javascriptApplicationGraph-DUWeec4O.js → javascriptApplicationGraph-DaHZ_j74.js} +1 -1
  60. package/dist/cli-runtime/{javascriptExportReturnShapeProjection-BVbnAdOd.js → javascriptExportReturnShapeProjection-5R4b1b65.js} +2 -2
  61. package/dist/cli-runtime/{javascriptExportShapeComparisonSchemas-BzSil7ft.js → javascriptExportShapeComparisonSchemas-CabiAG0g.js} +2 -2
  62. package/dist/cli-runtime/{javascriptRuntimeObservation-9xTNoKY2.js → javascriptRuntimeObservation-Kk7etMNj.js} +2 -2
  63. package/dist/cli-runtime/{javascriptRuntimeReconciliation-CneBtMvx.js → javascriptRuntimeReconciliation-DuZmwI3R.js} +4 -4
  64. package/dist/cli-runtime/{javascriptRuntimeReconciliationSchemas-D1Mrb_Y0.js → javascriptRuntimeReconciliationSchemas-BoiwFBmk.js} +3 -3
  65. package/dist/cli-runtime/{javascriptSemanticTraceSchemas-BHItrqIA.js → javascriptSemanticTraceSchemas-BlqTEtah.js} +2 -2
  66. package/dist/cli-runtime/{lipo-BCA3wVyN.js → lipo-BA5qA_xF.js} +3 -3
  67. package/dist/cli-runtime/{managedApplicationGraph-BqOr01E6.js → managedApplicationGraph-bNZH8_q9.js} +2 -2
  68. package/dist/cli-runtime/{managedToolContracts-BSy4TRaK.js → managedToolContracts-_YDzyE2p.js} +2 -2
  69. package/dist/cli-runtime/mcpDoctor.js +3 -3
  70. package/dist/cli-runtime/{native__NativeMacOSProvider-BIG9iCcj.js → native__NativeMacOSProvider-C5fgPPf0.js} +10 -10
  71. package/dist/cli-runtime/{reconstructionObligationLedgerSchemas-dvp6PEnZ.js → reconstructionObligationLedgerSchemas-CBxjNJ5m.js} +27 -3
  72. package/dist/cli-runtime/{scanCanonical-Bi6si9nb.js → scanCanonical-Dt1Hf-XQ.js} +4 -4
  73. package/dist/cli-runtime/{snapshotEnvironment-B5Ai0mha.js → snapshotEnvironment-CF3KJGdj.js} +1 -1
  74. package/dist/cli-runtime/{toolContracts-C7pvYKxF.js → toolContracts-DrZdzzVJ.js} +20 -13
  75. package/dist/cli-runtime/{toolEffects-DW31yTSx.js → toolEffects-JovaVppR.js} +2 -2
  76. package/dist/cli-runtime/{workflowInputError-26EGiDpx.js → workflowInputError-uPF2Przv.js} +1 -1
  77. package/dist/contracts/browserToolContracts.js +9 -1
  78. package/dist/domain/webScreenshot.js +32 -0
  79. package/dist/generatedPackageMetadata.js +1 -1
  80. package/dist/mcp/{AnalysisSnapshotFiles-DbgHi36O.js → AnalysisSnapshotFiles-DRJXahG3.js} +2 -2
  81. package/dist/mcp/{ArtifactProvider-BNxKlaKX.js → ArtifactProvider-D8Sojfsy.js} +4 -4
  82. package/dist/mcp/{ArtifactProviderMetadata--hy-wyJK.js → ArtifactProviderMetadata-DDXT41IA.js} +1 -1
  83. package/dist/mcp/{BinaryTargetResolver-DbiUzRLh.js → BinaryTargetResolver-w52tdHfN.js} +2 -2
  84. package/dist/mcp/{DirectAnalysis-BWAu8Ro5.js → DirectAnalysis-CaeqptL4.js} +6 -6
  85. package/dist/mcp/{DirectAnalysisStatus-DofNPhKd.js → DirectAnalysisStatus-DTwOtxgh.js} +1 -1
  86. package/dist/mcp/{ManagedStaticProvider-CXE93Bow.js → ManagedStaticProvider-01USDXJF.js} +3 -3
  87. package/dist/mcp/{NativeMacOSProvider-BbITFLHW.js → NativeMacOSProvider-C_Mjc0mG.js} +4 -4
  88. package/dist/mcp/{NativeMacOSProviderMetadata-mxDhaNJf.js → NativeMacOSProviderMetadata-C6Ov3ewq.js} +1 -1
  89. package/dist/mcp/{artifactToolContracts-TXegBBr1.js → artifactToolContracts-h0iSzOnt.js} +1 -1
  90. package/dist/mcp/{browserObservation-BCgaDRyQ.js → browserObservation-Dxo7O6PX.js} +116 -6
  91. package/dist/mcp/{browserScenario-z7jveI9P.js → browserScenario-CFz4w9YN.js} +1 -1
  92. package/dist/mcp/{browserScenarioCapture-BvnKTmtZ.js → browserScenarioCapture-Cn6A6diD.js} +1 -1
  93. package/dist/mcp/{emptyArraySchema-DEEefG2V.js → emptyArraySchema-CJFbNjT7.js} +27 -3
  94. package/dist/mcp/{main-WQSCuTNt.js → main-DH7dl4Gx.js} +22 -22
  95. package/dist/mcp/main.js +1 -1
  96. package/dist/mcp/{managedToolContracts-o5LHFwHc.js → managedToolContracts-BVLgbsNv.js} +1 -1
  97. package/dist/mcp/{scanCanonical-2GN0h-BE.js → scanCanonical-B6OUfBVK.js} +2 -2
  98. package/dist/mcp/{serverIdentity-rOY69sxc.js → serverIdentity-CwMs2Ey4.js} +1 -1
  99. package/dist/mcp/{toolContracts-CrfYDTUz.js → toolContracts-BMgXB36_.js} +14 -7
  100. package/dist/mcp/{toolOutputSchemaGroups-ChxVLGoy.js → toolOutputSchemaGroups-CwPpULLy.js} +1 -1
  101. package/dist/mcp/{webSourceLocation-Dnkpf0R2.js → webSourceLocation-BkuEbXcI.js} +2 -2
  102. package/licenses/README.md +1 -0
  103. package/licenses/godot-engine-MIT.txt +20 -0
  104. package/native/windows/build/manifest.json +1 -1
  105. package/package.json +1 -1
  106. package/port/cli.mjs +6 -4
  107. package/port/knowledge/apis/godot-3-to-4-renames.json +1 -0
  108. package/port/knowledge/apis/godot-3.json +1 -0
  109. package/port/knowledge/apis/godot-4.3.json +1 -0
  110. package/port/knowledge/apis/godot-4.7.json +1 -0
  111. package/port/src/agent.mjs +1 -1
  112. package/port/src/agentdisk.mjs +83 -0
  113. package/port/src/apicheck.mjs +280 -0
  114. package/port/src/archive.mjs +34 -4
  115. package/port/src/convert.mjs +3 -1
  116. package/port/src/dormant.mjs +159 -0
  117. package/port/src/formats.mjs +1 -0
  118. package/port/src/gamearchives.mjs +476 -0
  119. package/port/src/godotbin.mjs +544 -0
  120. package/port/src/il2cpp.mjs +148 -0
  121. package/port/src/jsworkspace.mjs +2 -2
  122. package/port/src/knowledge.mjs +3 -1
  123. package/port/src/physics.mjs +135 -0
  124. package/port/src/plan.mjs +94 -11
  125. package/port/src/porters.mjs +87 -5
  126. package/port/src/refs.mjs +120 -0
  127. package/port/src/rpgmaker.mjs +416 -0
  128. package/port/src/save.mjs +326 -0
  129. package/port/src/scene.mjs +37 -3
  130. package/port/src/shader.mjs +385 -0
  131. package/port/src/strings.mjs +327 -0
  132. package/port/src/tools.mjs +48 -8
  133. package/port/src/trace.mjs +302 -0
  134. package/port/src/unity-anim.mjs +229 -0
  135. package/port/src/upgrade.mjs +219 -0
  136. package/skills/build-inspector/SKILL.md +8 -0
  137. package/skills/godot-port/SKILL.md +2 -0
  138. package/skills/port-verify/SKILL.md +20 -9
  139. package/skills/reverse-engineer-anything/SKILL.md +18 -2
  140. package/skills/reverse-engineer-anything/references/porting-and-rebuilds.md +35 -4
  141. package/skills/unity-to-godot/SKILL.md +14 -2
@@ -67,16 +67,81 @@ function unityRoles(node) {
67
67
  const cam = has('Camera'); if (cam) roles.camera = { fov: cam.fieldOfView ?? 60 };
68
68
  const light = has('Light'); if (light) roles.light = { Directional: 'directional', Point: 'point', Spot: 'spot' }[light.lightType] ?? 'point';
69
69
  const audio = has('AudioSource'); if (audio) roles.audio = { spatial: true, clip: audio.clip };
70
- roles.scripts = (node.components ?? []).filter(c => c.type === 'MonoBehaviour' && c.script).map(c => c.script);
70
+ roles.scripts = (node.components ?? []).filter(c => c.type === 'MonoBehaviour' && c.script && !c.ui).map(c => c.script);
71
+ const anim = has('Animator'); if (anim) roles.animator = anim.controller ?? null;
72
+ const widgets = (node.components ?? []).filter(c => c.ui).map(c => c.ui);
73
+ if (node.ui || widgets.length || has('Canvas')) roles.ui = { canvas: !!has('Canvas'), widgets, group: has('CanvasGroup') ?? null };
71
74
  return roles;
72
75
  }
73
76
 
77
+ // ---------------------------------------------------------------- uGUI → Control
78
+ const UI_ALIGN_H = { 0: 0, 1: 1, 2: 2, 3: 0, 4: 1, 5: 2, 6: 0, 7: 1, 8: 2 }; // Unity TextAnchor → HORIZONTAL_ALIGNMENT
79
+ const UI_ALIGN_V = { 0: 0, 1: 0, 2: 0, 3: 1, 4: 1, 5: 1, 6: 2, 7: 2, 8: 2 };
80
+ const TMP_ALIGN = a => ({ h: [1, 2, 4, 8].findIndex(b => a & b) === -1 ? 0 : { 1: 0, 2: 1, 4: 2, 8: 3 }[[1, 2, 4, 8].find(b => a & b)], v: { 256: 0, 512: 1, 1024: 2 }[[256, 512, 1024].find(b => a & b)] ?? 0 });
81
+ const color4 = c => `Color(${c.map(f).join(', ')})`;
82
+ /** Godot anchors/offsets from a Unity RectTransform (Y flipped), plus widget properties. */
83
+ function uiProps(node, roles, todo, addExt, resolve) {
84
+ const out = [];
85
+ const r = node.rect;
86
+ const canvas = roles.ui.canvas;
87
+ if (canvas) {
88
+ out.push('anchors_preset = 15', 'anchor_right = 1.0', 'anchor_bottom = 1.0', 'grow_horizontal = 2', 'grow_vertical = 2', 'mouse_filter = 2');
89
+ const scaler = roles.ui.widgets.find(w => w.widget === 'CanvasScaler');
90
+ if (scaler?.referenceResolution?.x) todo.push(`Canvas scaler reference ${scaler.referenceResolution.x}x${scaler.referenceResolution.y}: set display/window/size to it and stretch mode canvas_items in Project Settings`);
91
+ } else if (r) {
92
+ const aMin = r.anchorMin, aMax = r.anchorMax, ap = r.anchoredPosition, sd = r.sizeDelta, pv = r.pivot;
93
+ const offMin = { x: (ap.x ?? 0) - (sd.x ?? 0) * (pv.x ?? 0.5), y: (ap.y ?? 0) - (sd.y ?? 0) * (pv.y ?? 0.5) };
94
+ const offMax = { x: (ap.x ?? 0) + (sd.x ?? 0) * (1 - (pv.x ?? 0.5)), y: (ap.y ?? 0) + (sd.y ?? 0) * (1 - (pv.y ?? 0.5)) };
95
+ const vals = { anchor_left: aMin.x ?? 0, anchor_top: 1 - (aMax.y ?? 0), anchor_right: aMax.x ?? 0, anchor_bottom: 1 - (aMin.y ?? 0), offset_left: offMin.x, offset_top: -offMax.y, offset_right: offMax.x, offset_bottom: -offMin.y };
96
+ for (const [k, v] of Object.entries(vals)) if (Math.abs(v) > 1e-9) out.push(`${k} = ${f(v)}`);
97
+ if (Math.abs((aMin.x ?? 0) - 0) < 1e-9 && Math.abs((aMax.x ?? 0) - 1) < 1e-9) out.push('grow_horizontal = 2');
98
+ if (Math.abs((aMin.y ?? 0) - 0) < 1e-9 && Math.abs((aMax.y ?? 0) - 1) < 1e-9) out.push('grow_vertical = 2');
99
+ if (pv.x !== undefined && (Math.abs(pv.x - 0.5) > 1e-6 || Math.abs((pv.y ?? 0.5) - 0.5) > 1e-6)) out.push(`pivot_offset = Vector2(${f((pv.x ?? 0.5) * (sd.x ?? 0))}, ${f((1 - (pv.y ?? 0.5)) * (sd.y ?? 0))})`);
100
+ const t = node.transform;
101
+ if (t?.scale && (Math.abs(t.scale[0] - 1) > 1e-6 || Math.abs(t.scale[1] - 1) > 1e-6)) out.push(`scale = Vector2(${f(t.scale[0])}, ${f(t.scale[1])})`);
102
+ if (t?.rotation) { const [x, y, z, w] = t.rotation; const ang = Math.atan2(2 * (w * z + x * y), 1 - 2 * (y * y + z * z)); if (Math.abs(ang) > 1e-6) out.push(`rotation = ${f(-ang)}`); }
103
+ }
104
+ for (const w of roles.ui.widgets) {
105
+ if (w.widget === 'Image' || w.widget === 'RawImage') {
106
+ const src = w.sprite ?? w.texture; const res = src ? resolve(src) : null;
107
+ if (res) out.push(`texture = ExtResource("${addExt('Texture2D', res)}")`); else if (src) todo.push(`texture ${src} not ported yet`);
108
+ out.push('expand_mode = 1', `stretch_mode = ${w.preserveAspect ? 5 : w.imageType === 'tiled' ? 1 : 1}`);
109
+ if (w.color && w.color.some((c, i) => Math.abs(c - 1) > 1e-6)) out.push(`modulate = ${color4(w.color)}`);
110
+ if (w.imageType === 'filled') todo.push('Unity filled Image: use a TextureProgressBar (fill_mode) or a shader for the fill amount');
111
+ if (w.imageType === 'sliced') todo.push('Unity sliced Image: use NinePatchRect with the sprite borders');
112
+ }
113
+ if (w.widget === 'Text' || w.widget === 'TextMeshPro') {
114
+ out.push(`text = ${q(w.text ?? '')}`);
115
+ if (w.fontSize) out.push(`theme_override_font_sizes/font_size = ${Math.round(w.fontSize)}`);
116
+ if (w.color && w.color.some((c, i) => Math.abs(c - 1) > 1e-6)) out.push(`theme_override_colors/font_color = ${color4(w.color)}`);
117
+ const al = w.widget === 'Text' ? { h: UI_ALIGN_H[w.alignment] ?? 0, v: UI_ALIGN_V[w.alignment] ?? 0 } : TMP_ALIGN(w.alignment ?? 257);
118
+ if (al.h) out.push(`horizontal_alignment = ${al.h}`);
119
+ if (al.v) out.push(`vertical_alignment = ${al.v}`);
120
+ out.push('autowrap_mode = 3');
121
+ }
122
+ if (w.widget === 'Button') {
123
+ const label = (node.children ?? []).flatMap(c => (c.components ?? []).map(x => x.ui).filter(Boolean)).find(u => u.widget === 'Text' || u.widget === 'TextMeshPro');
124
+ if (label?.text) out.push(`text = ${q(label.text)}`);
125
+ if (w.onClick?.length) todo.push(`OnClick → ${w.onClick.map(c => c.method).join(', ')}: connect the pressed signal to the ported method`);
126
+ }
127
+ if (w.widget === 'Slider') { out.push(`min_value = ${f(w.min ?? 0)}`, `max_value = ${f(w.max ?? 1)}`, `value = ${f(w.value ?? 0)}`, `step = ${w.wholeNumbers ? 1 : 0.001}`); }
128
+ if (w.widget === 'Toggle' && w.isOn) out.push('button_pressed = true');
129
+ if (w.widget === 'InputField') out.push(`text = ${q(w.text ?? '')}`);
130
+ if (w.widget === 'GridLayoutGroup') { out.push(`columns = ${Math.max(1, w.constraintCount ?? 2)}`); if (w.spacing) out.push(`theme_override_constants/h_separation = ${f(w.spacing)}`, `theme_override_constants/v_separation = ${f(w.spacing)}`); }
131
+ if (w.widget === 'VerticalLayoutGroup' || w.widget === 'LayoutGroup') { if (w.spacing) out.push(`theme_override_constants/separation = ${f(w.spacing)}`); }
132
+ if (w.widget === 'Mask') out.push('clip_contents = true');
133
+ }
134
+ if (roles.ui.group && roles.ui.group.alpha !== undefined && Math.abs(roles.ui.group.alpha - 1) > 1e-6) out.push(`modulate = Color(1, 1, 1, ${f(roles.ui.group.alpha)})`);
135
+ if (node.active === false) out.push('visible = false');
136
+ return out;
137
+ }
138
+
74
139
  // ---------------------------------------------------------------- → Godot .tscn
75
140
  /**
76
141
  * Write a Godot 4 text scene from a neutral tree that is already in Godot coordinates.
77
142
  * resolve(scriptOrPrefabSourcePath) → res:// path of the ported file (or null).
78
143
  */
79
- export function writeTscn(root, { engine = 'unity', resolve = () => null, name } = {}) {
144
+ export function writeTscn(root, { engine = 'unity', resolve = () => null, animator = () => null, name } = {}) {
80
145
  const ext = [], subs = [], nodes = [];
81
146
  const extId = new Map();
82
147
  const addExt = (type, p) => { const k = `${type}:${p}`; if (!extId.has(k)) { const id = `${ext.length + 1}_rp`; extId.set(k, id); ext.push(`[ext_resource type="${type}" path=${q(p)} id="${id}"]`); } return extId.get(k); };
@@ -100,8 +165,14 @@ export function writeTscn(root, { engine = 'unity', resolve = () => null, name }
100
165
  if (c.shape === 'capsule') return addSub('CapsuleShape3D', { radius: f(c.radius ?? 0.5), height: f(c.height ?? 2) });
101
166
  return null;
102
167
  };
168
+ const UI_TYPES = { Image: 'TextureRect', RawImage: 'TextureRect', Text: 'Label', TextMeshPro: 'Label', Button: 'Button', Slider: 'HSlider', Toggle: 'CheckBox', ScrollRect: 'ScrollContainer', InputField: 'LineEdit', GridLayoutGroup: 'GridContainer', VerticalLayoutGroup: 'VBoxContainer', LayoutGroup: 'HBoxContainer', Mask: 'Control', CanvasScaler: null };
103
169
  const godotType = (node, roles) => {
104
170
  if (node.type === 'PrefabInstance') return null;
171
+ if (roles.ui) {
172
+ const w = roles.ui.widgets.find(x => UI_TYPES[x.widget]);
173
+ if (w?.widget === 'Slider' && w.vertical) return 'VSlider';
174
+ return (w && UI_TYPES[w.widget]) || 'Control';
175
+ }
105
176
  if (roles.body === 'character') return 'CharacterBody3D';
106
177
  if (roles.body === 'rigid') return 'RigidBody3D';
107
178
  if (roles.body === 'kinematic') return 'AnimatableBody3D';
@@ -127,20 +198,31 @@ export function writeTscn(root, { engine = 'unity', resolve = () => null, name }
127
198
  }
128
199
  if (node.tag) attrs.push(`groups=[${q(node.tag)}]`);
129
200
  const lines = [`[node ${attrs.join(' ')}]`];
130
- if (!isIdentity(node.transform)) lines.push(`transform = ${tf(node.transform)}`);
201
+ const todo = [];
202
+ if (roles.ui) lines.push(...uiProps(node, roles, todo, addExt, resolve));
203
+ else if (!isIdentity(node.transform)) lines.push(`transform = ${tf(node.transform)}`);
131
204
  const scripts = (roles.scripts ?? []).map(s => ({ src: s, res: resolve(s) }));
132
205
  if (scripts[0]?.res) lines.push(`script = ExtResource("${addExt('Script', scripts[0].res)}")`);
133
- const todo = [];
134
206
  if (node.type === 'PrefabInstance' && node.prefab && !resolve(node.prefab)) todo.push(`instance of ${node.prefab} (not ported yet)`);
207
+ for (const ov of node.overrides ?? []) {
208
+ if (ov.property === 'm_IsActive') { if (ov.value === '0') lines.push('visible = false'); continue; }
209
+ todo.push(`override ${ov.property} = ${ov.reference ?? ov.value} (set it on the ported node)`);
210
+ }
135
211
  if (scripts.length > 1) todo.push(`more scripts on this GameObject: ${scripts.slice(1).map(s => s.src).join(', ')} — add them as child nodes`);
136
212
  if (scripts[0] && !scripts[0].res) todo.push(`script ${scripts[0].src} not ported yet`);
137
213
  if (roles.camera) lines.push(`fov = ${f(roles.camera.fov)}`);
138
- if (node.active === false) lines.push('visible = false');
214
+ if (node.active === false && !roles.ui) lines.push('visible = false');
139
215
  if (roles.mesh?.asset) todo.push(`mesh ${roles.mesh.asset}: import the model and assign it`);
140
216
  if (roles.audio?.clip) todo.push(`audio clip ${roles.audio.clip}`);
141
217
  if (todo.length) lines.push(`metadata/reverseport_todo = ${q(todo.join('; '))}`);
142
218
  nodes.push(lines.join('\n'));
143
219
  const myPath = isRoot ? '.' : parentPath === '.' ? nodeName : `${parentPath}/${nodeName}`;
220
+ if (roles.animator !== undefined) {
221
+ const info = animator(roles.animator) ?? { clips: [], script: null };
222
+ const lib = info.clips.length ? addSub('AnimationLibrary', { _data: `{\n${info.clips.map(c => `${q(c.name)}: ExtResource("${addExt('Animation', c.res)}")`).join(',\n')}\n}` }) : null;
223
+ nodes.push(`[node name="AnimationPlayer" type="AnimationPlayer" parent=${q(myPath)}]${lib ? `\nlibraries = {\n"": SubResource("${lib}")\n}` : ''}${info.missing?.length ? `\nmetadata/reverseport_todo = ${q(`clips not ported yet: ${info.missing.join(', ')}`)}` : ''}`);
224
+ nodes.push(`[node name="Animator" type="Node" parent=${q(myPath)}]${info.script ? `\nscript = ExtResource("${addExt('Script', info.script)}")\nplayer = NodePath("../AnimationPlayer")` : `\nmetadata/reverseport_todo = ${q(`animator controller ${roles.animator ?? '(none)'} not ported yet: port it with port_scene, then set script here`)}`}`);
225
+ }
144
226
  if (roles.mesh?.primitive && roles.body) {
145
227
  const id = meshSub(roles.mesh);
146
228
  nodes.push(`[node name="Mesh" type="MeshInstance3D" parent=${q(myPath)}]\nmesh = SubResource("${id}")`);
@@ -0,0 +1,120 @@
1
+ // re_refs: audit Unity GUID references. Finds references to GUIDs that no .meta file declares (dangling), the
2
+ // same GUID declared by two .meta files (duplicates after a copy), assets without a .meta file, and .meta files
3
+ // whose asset is gone. Suggests remaps for dangling script references by matching the MonoBehaviour class name
4
+ // to a script with the same name. Reports only: nothing is written.
5
+ import fs from 'node:fs/promises';
6
+ import path from 'node:path';
7
+ import { RPError, requireDir, walk, limit } from './util.mjs';
8
+ import { findUnityRoot } from './scene.mjs';
9
+
10
+ const BUILTIN = new Map([
11
+ ['0000000000000000e000000000000000', 'Unity built-in resources'],
12
+ ['0000000000000000f000000000000000', 'Unity built-in extra resources'],
13
+ ]);
14
+ const REF_EXT = new Set(['.unity', '.prefab', '.asset', '.mat', '.controller', '.anim', '.playable', '.overridecontroller', '.mask', '.physicmaterial', '.physicsmaterial2d', '.flare', '.renderTexture', '.cubemap', '.spriteatlas', '.preset', '.shadervariants', '.signal', '.guiskin', '.fontsettings', '.terrainlayer', '.brush', '.mixer', '.lighting', '.giparams']);
15
+
16
+ async function readText(file, max = 32 * 1024 * 1024) {
17
+ const st = await fs.stat(file);
18
+ if (st.size > max) return null;
19
+ return fs.readFile(file, 'utf8');
20
+ }
21
+
22
+ /** Audit the GUID references of a Unity project. `path` may be the project root or any file/folder inside it. */
23
+ export async function refsTool({ path: target, limit: max = 200 }) {
24
+ const abs = await requireDir(target).catch(async () => path.dirname(path.resolve(target)));
25
+ const root = await findUnityRoot(path.join(abs, 'x')) ?? (await isUnityRoot(abs) ? abs : null);
26
+ if (!root) throw new RPError('NOT_UNITY', `No Unity project found at or above ${abs} (needs Assets/ and ProjectSettings/).`);
27
+
28
+ const declared = new Map(); // guid → [asset paths]
29
+ const metaFor = new Set(); // asset paths that have a .meta
30
+ const assets = []; // asset paths (not .meta)
31
+ const metaMissingAsset = [];
32
+ const scriptsByClass = new Map(); // class name → [script paths]
33
+ const refFiles = [];
34
+
35
+ for (const sub of ['Assets', 'Packages']) {
36
+ const base = path.join(root, sub);
37
+ const { files } = await walk(base, { maxFiles: 300000 }).catch(() => ({ files: [] }));
38
+ for (const f of files) {
39
+ const rel = `${sub}/${f.rel}`;
40
+ if (f.ext === '.meta') {
41
+ const head = (await readText(f.abs, 64 * 1024)) ?? '';
42
+ const m = head.match(/guid:\s*([0-9a-f]{32})/);
43
+ const asset = rel.replace(/\.meta$/, '');
44
+ metaFor.add(asset);
45
+ if (m) (declared.get(m[1]) ?? declared.set(m[1], []).get(m[1])).push(asset);
46
+ const assetAbs = f.abs.replace(/\.meta$/, '');
47
+ const st = await fs.stat(assetAbs).catch(() => null);
48
+ if (!st) metaMissingAsset.push({ meta: rel, expected: asset });
49
+ continue;
50
+ }
51
+ assets.push(rel);
52
+ if (f.ext === '.cs') scriptsByClass.set(path.basename(f.rel, '.cs'), [...(scriptsByClass.get(path.basename(f.rel, '.cs')) ?? []), rel]);
53
+ if (REF_EXT.has(f.ext)) refFiles.push({ rel, abs: f.abs, ext: f.ext });
54
+ }
55
+ }
56
+
57
+ // Assets without a .meta (Unity ignores them, or re-creates the .meta with a NEW guid → references break).
58
+ const missingMeta = assets.filter(a => !metaFor.has(a) && !/(^|\/)\.[^/]+$/.test(a) && !/^Packages\/(manifest|packages-lock)\.json$/.test(a));
59
+
60
+ // References.
61
+ const dangling = new Map(); // guid → { files: Set, scriptClass? }
62
+ const used = new Set();
63
+ let refCount = 0;
64
+ for (const f of refFiles) {
65
+ const text = await readText(f.abs);
66
+ if (text === null || !text.startsWith('%YAML')) continue;
67
+ // Pair each guid reference with the nearest preceding "m_Name:"/class hint so script remaps can be suggested.
68
+ for (const m of text.matchAll(/fileID:\s*(-?\d+),\s*guid:\s*([0-9a-f]{32})/g)) {
69
+ const guid = m[2];
70
+ refCount++;
71
+ used.add(guid);
72
+ if (declared.has(guid) || BUILTIN.has(guid)) continue;
73
+ const entry = dangling.get(guid) ?? dangling.set(guid, { guid, files: new Set(), fileIDs: new Set(), kinds: new Set() }).get(guid);
74
+ entry.files.add(f.rel);
75
+ entry.fileIDs.add(m[1]);
76
+ const before = text.slice(Math.max(0, m.index - 400), m.index);
77
+ const key = before.match(/(\w+):\s*\{?\s*$/)?.[1] ?? '';
78
+ if (key === 'm_Script') entry.kinds.add('script');
79
+ else if (key === 'm_Shader') entry.kinds.add('shader');
80
+ else if (key === 'm_Mesh') entry.kinds.add('mesh');
81
+ else if (key === 'm_Texture') entry.kinds.add('texture');
82
+ else if (key === 'm_SourcePrefab') entry.kinds.add('prefab');
83
+ else if (/m_Materials:\s*\n\s*-\s*\{?\s*$/.test(before)) entry.kinds.add('material');
84
+ else entry.kinds.add('other');
85
+ }
86
+ }
87
+
88
+ // Suggest remaps for dangling script references: a MonoBehaviour's class name usually equals the file name, and
89
+ // Unity writes the class name next to the component in many files; when absent we list all unreferenced scripts.
90
+ const referencedScripts = new Set([...used].flatMap(g => declared.get(g) ?? []).filter(p => p.endsWith('.cs')));
91
+ const unreferencedScripts = assets.filter(a => a.endsWith('.cs') && !referencedScripts.has(a) && !/\/Editor\//.test(a));
92
+ const danglingList = [...dangling.values()].map(d => {
93
+ const suggestions = [];
94
+ if (d.kinds.has('script') && unreferencedScripts.length && unreferencedScripts.length <= 20) suggestions.push(...unreferencedScripts.map(s => ({ asset: s, guid: guidOf(declared, s), why: 'script never referenced by any other asset' })));
95
+ return { guid: d.guid, kinds: [...d.kinds], files: [...d.files].sort(), fileIDs: [...d.fileIDs].slice(0, 10), suggestions: suggestions.slice(0, 5) };
96
+ }).sort((a, b) => b.files.length - a.files.length);
97
+
98
+ const duplicates = [...declared.entries()].filter(([, paths]) => paths.length > 1).map(([guid, paths]) => ({ guid, assets: paths.sort(), referenced: used.has(guid), note: 'Two .meta files declare the same guid (copied outside Unity); references resolve to whichever Unity imports first.' }));
99
+ const orphans = [...declared.entries()].filter(([guid, paths]) => !used.has(guid) && paths.every(p => !/\.(cs|unity|asmdef|dll)$/.test(p) && !/^Packages\//.test(p))).map(([guid, paths]) => ({ guid, asset: paths[0] }));
100
+
101
+ const problems = danglingList.length + duplicates.length + missingMeta.length + metaMissingAsset.length;
102
+ return {
103
+ unityProject: root,
104
+ summary: { metaFiles: metaFor.size, guidsDeclared: declared.size, references: refCount, dangling: danglingList.length, duplicateGuids: duplicates.length, assetsWithoutMeta: missingMeta.length, metaWithoutAsset: metaMissingAsset.length, unreferencedAssets: orphans.length },
105
+ dangling: limit(danglingList, max),
106
+ duplicateGuids: limit(duplicates, max),
107
+ assetsWithoutMeta: limit(missingMeta.sort(), max),
108
+ metaWithoutAsset: limit(metaMissingAsset, max),
109
+ unreferencedAssets: limit(orphans, max),
110
+ builtin: [...BUILTIN].map(([guid, what]) => ({ guid, what })),
111
+ passed: problems === 0,
112
+ next: problems ? 'Fix in Unity (reimport or re-link in the Inspector) or write the suggested guid into the .meta of the right asset; keep the asset path, never edit references in scenes by hand.' : 'All GUID references resolve.',
113
+ };
114
+ }
115
+
116
+ const guidOf = (declared, asset) => { for (const [g, paths] of declared) if (paths.includes(asset)) return g; return null; };
117
+
118
+ async function isUnityRoot(dir) {
119
+ try { await fs.access(path.join(dir, 'Assets')); await fs.access(path.join(dir, 'ProjectSettings')); return true; } catch { return false; }
120
+ }