unity-agentic-tools 0.4.0 → 0.4.2

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.
package/dist/index.js CHANGED
@@ -205,7 +205,7 @@ var require_package = __commonJS((exports2, module2) => {
205
205
  module2.exports = {
206
206
  name: "unity-agentic-tools",
207
207
  packageManager: "bun@latest",
208
- version: "0.4.0",
208
+ version: "0.4.2",
209
209
  description: "Fast, token-efficient Unity YAML parser for AI agents",
210
210
  exports: {
211
211
  ".": {
@@ -1517,6 +1517,7 @@ function search_project(options) {
1517
1517
  const files = walk_project_files(project_path, extensions);
1518
1518
  const scanner = new UnityScanner;
1519
1519
  const matches = [];
1520
+ let files_with_errors = 0;
1520
1521
  for (const file of files) {
1521
1522
  try {
1522
1523
  let gameObjects;
@@ -1603,6 +1604,7 @@ function search_project(options) {
1603
1604
  if (max_matches !== undefined && matches.length >= max_matches)
1604
1605
  break;
1605
1606
  } catch {
1607
+ files_with_errors++;
1606
1608
  continue;
1607
1609
  }
1608
1610
  }
@@ -1611,6 +1613,7 @@ function search_project(options) {
1611
1613
  project_path,
1612
1614
  total_files_scanned: files.length,
1613
1615
  total_matches: matches.length,
1616
+ files_with_errors: files_with_errors > 0 ? files_with_errors : undefined,
1614
1617
  cursor: 0,
1615
1618
  truncated: max_matches !== undefined && matches.length >= max_matches,
1616
1619
  matches
@@ -2333,9 +2336,75 @@ function resolveScriptGuid(script, projectPath) {
2333
2336
  }
2334
2337
  } catch {}
2335
2338
  }
2339
+ try {
2340
+ const assetsDir = path3.join(projectPath, "Assets");
2341
+ if (import_fs8.existsSync(assetsDir)) {
2342
+ const entries = import_fs8.readdirSync(assetsDir, { recursive: true, withFileTypes: false });
2343
+ for (const entry of entries) {
2344
+ if (!entry.endsWith(".cs"))
2345
+ continue;
2346
+ const fileName = path3.basename(entry, ".cs").toLowerCase();
2347
+ if (fileName === scriptNameLower || classNameOnly && fileName === classNameOnly) {
2348
+ const fullPath = path3.join(assetsDir, entry);
2349
+ const guid = extractGuidFromMeta(fullPath + ".meta");
2350
+ if (guid) {
2351
+ return { guid, path: path3.join("Assets", entry) };
2352
+ }
2353
+ }
2354
+ }
2355
+ }
2356
+ } catch {}
2336
2357
  }
2337
2358
  return null;
2338
2359
  }
2360
+ var ASSET_PPTR_MAP = {
2361
+ ".inputactions": { fileID: 11400000, type: 2 },
2362
+ ".asset": { fileID: 11400000, type: 2 },
2363
+ ".mat": { fileID: 2100000, type: 2 },
2364
+ ".prefab": { fileID: 100100000, type: 2 },
2365
+ ".controller": { fileID: 9100000, type: 2 },
2366
+ ".anim": { fileID: 7400000, type: 2 }
2367
+ };
2368
+ var ASSET_EXTENSIONS = new Set(Object.keys(ASSET_PPTR_MAP));
2369
+ function resolveAssetPathToPPtr(value, file_path, project_path) {
2370
+ if (value.startsWith("{fileID:") || value.startsWith("{"))
2371
+ return;
2372
+ const ext = path3.extname(value).toLowerCase();
2373
+ if (!ASSET_EXTENSIONS.has(ext))
2374
+ return;
2375
+ const resolved_project = project_path || find_unity_project_root(path3.dirname(file_path));
2376
+ if (!resolved_project)
2377
+ return null;
2378
+ const mapping = ASSET_PPTR_MAP[ext];
2379
+ let guid = null;
2380
+ const basename_val = path3.basename(value, ext);
2381
+ if (value.includes("/") || value.includes("\\")) {
2382
+ const abs = path3.isAbsolute(value) ? value : path3.join(resolved_project, value);
2383
+ guid = extractGuidFromMeta(abs + ".meta");
2384
+ }
2385
+ if (!guid) {
2386
+ const cache = load_guid_cache_for_file(file_path, resolved_project);
2387
+ if (cache) {
2388
+ const found = cache.find_by_name(basename_val, ext);
2389
+ if (found)
2390
+ guid = found.guid;
2391
+ }
2392
+ }
2393
+ if (!guid) {
2394
+ const candidates = [
2395
+ path3.join(resolved_project, "Assets", value),
2396
+ path3.join(resolved_project, value)
2397
+ ];
2398
+ for (const candidate of candidates) {
2399
+ guid = extractGuidFromMeta(candidate + ".meta");
2400
+ if (guid)
2401
+ break;
2402
+ }
2403
+ }
2404
+ if (!guid)
2405
+ return null;
2406
+ return `{fileID: ${mapping.fileID}, guid: ${guid}, type: ${mapping.type}}`;
2407
+ }
2339
2408
  function resolve_script_with_fields(script, project_path) {
2340
2409
  const resolved = resolveScriptGuid(script, project_path);
2341
2410
  if (!resolved)
@@ -2439,8 +2508,8 @@ function build_type_lookup(project_path) {
2439
2508
  const assetsDir = path3.join(project_path, "Assets");
2440
2509
  const csIndex = new Map;
2441
2510
  try {
2442
- const { readdirSync: readdirSync4 } = require("fs");
2443
- const entries = readdirSync4(assetsDir, { recursive: true, withFileTypes: false });
2511
+ const { readdirSync: readdirSync5 } = require("fs");
2512
+ const entries = readdirSync5(assetsDir, { recursive: true, withFileTypes: false });
2444
2513
  for (const entry of entries) {
2445
2514
  if (typeof entry === "string" && entry.endsWith(".cs")) {
2446
2515
  const basename4 = entry.substring(entry.lastIndexOf("/") + 1).replace(/\.cs$/, "");
@@ -2892,7 +2961,9 @@ ${element_indent2}- ${value}`);
2892
2961
  if (!header) {
2893
2962
  throw new Error(`Invalid Unity YAML block header in replacement text: "${new_text.slice(0, 80)}"`);
2894
2963
  }
2895
- this._raw = new_text;
2964
+ this._raw = new_text.endsWith(`
2965
+ `) ? new_text : new_text + `
2966
+ `;
2896
2967
  this._header = header;
2897
2968
  this._dirty = true;
2898
2969
  this._format_cache.clear();
@@ -2902,6 +2973,11 @@ ${element_indent2}- ${value}`);
2902
2973
  }
2903
2974
  _check_dirty(old_raw) {
2904
2975
  if (this._raw !== old_raw) {
2976
+ if (!this._raw.endsWith(`
2977
+ `)) {
2978
+ this._raw += `
2979
+ `;
2980
+ }
2905
2981
  this._dirty = true;
2906
2982
  return true;
2907
2983
  }
@@ -3963,14 +4039,82 @@ function read_project_version(projectPath) {
3963
4039
  }
3964
4040
 
3965
4041
  // src/editor/create.ts
4042
+ function md4(data) {
4043
+ function F(x, y, z) {
4044
+ return x & y | ~x & z;
4045
+ }
4046
+ function G(x, y, z) {
4047
+ return x & y | x & z | y & z;
4048
+ }
4049
+ function H(x, y, z) {
4050
+ return x ^ y ^ z;
4051
+ }
4052
+ function rotl(x, n) {
4053
+ return (x << n | x >>> 32 - n) >>> 0;
4054
+ }
4055
+ const len = data.length;
4056
+ const bitLen = len * 8;
4057
+ const padded_len = len + 9 + 63 & ~63;
4058
+ const buf = Buffer.alloc(padded_len);
4059
+ data.copy(buf);
4060
+ buf[len] = 128;
4061
+ buf.writeUInt32LE(bitLen >>> 0, padded_len - 8);
4062
+ buf.writeUInt32LE(Math.floor(bitLen / 4294967296) >>> 0, padded_len - 4);
4063
+ let a0 = 1732584193;
4064
+ let b0 = 4023233417;
4065
+ let c0 = 2562383102;
4066
+ let d0 = 271733878;
4067
+ const R1_K = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15];
4068
+ const R1_S = [3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19, 3, 7, 11, 19];
4069
+ const R2_K = [0, 4, 8, 12, 1, 5, 9, 13, 2, 6, 10, 14, 3, 7, 11, 15];
4070
+ const R2_S = [3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13, 3, 5, 9, 13];
4071
+ const R3_K = [0, 8, 4, 12, 2, 10, 6, 14, 1, 9, 5, 13, 3, 11, 7, 15];
4072
+ const R3_S = [3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15, 3, 9, 11, 15];
4073
+ for (let i = 0;i < padded_len; i += 64) {
4074
+ const X = [];
4075
+ for (let j = 0;j < 16; j++)
4076
+ X[j] = buf.readUInt32LE(i + j * 4);
4077
+ let a = a0, b = b0, c = c0, d = d0;
4078
+ for (let j = 0;j < 16; j++) {
4079
+ const t = a + F(b, c, d) + X[R1_K[j]] >>> 0;
4080
+ a = d;
4081
+ d = c;
4082
+ c = b;
4083
+ b = rotl(t, R1_S[j]);
4084
+ }
4085
+ for (let j = 0;j < 16; j++) {
4086
+ const t = a + G(b, c, d) + X[R2_K[j]] + 1518500249 >>> 0;
4087
+ a = d;
4088
+ d = c;
4089
+ c = b;
4090
+ b = rotl(t, R2_S[j]);
4091
+ }
4092
+ for (let j = 0;j < 16; j++) {
4093
+ const t = a + H(b, c, d) + X[R3_K[j]] + 1859775393 >>> 0;
4094
+ a = d;
4095
+ d = c;
4096
+ c = b;
4097
+ b = rotl(t, R3_S[j]);
4098
+ }
4099
+ a0 = a0 + a >>> 0;
4100
+ b0 = b0 + b >>> 0;
4101
+ c0 = c0 + c >>> 0;
4102
+ d0 = d0 + d >>> 0;
4103
+ }
4104
+ const result = Buffer.alloc(16);
4105
+ result.writeUInt32LE(a0, 0);
4106
+ result.writeUInt32LE(b0, 4);
4107
+ result.writeUInt32LE(c0, 8);
4108
+ result.writeUInt32LE(d0, 12);
4109
+ return result;
4110
+ }
3966
4111
  function compute_dll_script_file_id(namespace, class_name) {
3967
- const { createHash } = require("crypto");
3968
4112
  const full_name = namespace ? `${namespace}.${class_name}` : class_name;
3969
4113
  const name_bytes = Buffer.from(full_name, "utf-8");
3970
4114
  const input = Buffer.alloc(4 + name_bytes.length);
3971
4115
  input.writeInt32LE(114, 0);
3972
4116
  name_bytes.copy(input, 4);
3973
- const hash = createHash("md4").update(input).digest();
4117
+ const hash = md4(input);
3974
4118
  const a = hash.readInt32LE(0);
3975
4119
  const b = hash.readInt32LE(4);
3976
4120
  const c = hash.readInt32LE(8);
@@ -3983,13 +4127,13 @@ function get_layer_from_parent(doc, parentTransformId) {
3983
4127
  const parentTransform = doc.find_by_file_id(parentTransformId);
3984
4128
  if (!parentTransform)
3985
4129
  return 0;
3986
- const goMatch = parentTransform.raw.match(/m_GameObject:\s*\{fileID:\s*(\d+)\}/);
4130
+ const goMatch = parentTransform.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(\d+)\}/);
3987
4131
  if (!goMatch)
3988
4132
  return 0;
3989
4133
  const parentGo = doc.find_by_file_id(goMatch[1]);
3990
4134
  if (!parentGo)
3991
4135
  return 0;
3992
- const layerMatch = parentGo.raw.match(/m_Layer:\s*(\d+)/);
4136
+ const layerMatch = parentGo.raw.match(/m_Layer:[ \t]*(\d+)/);
3993
4137
  return layerMatch ? parseInt(layerMatch[1], 10) : 0;
3994
4138
  }
3995
4139
  function createGameObjectYAML(gameObjectId, transformId, name, parentTransformId = "0", rootOrder = 0, layer = 0) {
@@ -4126,48 +4270,320 @@ function find_game_object_in_variant(doc, name, file_path, project_path) {
4126
4270
  }
4127
4271
  return null;
4128
4272
  }
4129
- function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
4130
- const piBlock = doc.find_by_file_id(piId);
4131
- if (!piBlock)
4132
- return;
4133
- const entry = `
4134
- - targetCorrespondingSourceObject: ${targetSourceRef}
4135
- insertIndex: -1
4136
- addedObject: {fileID: ${newGoId}}`;
4137
- let raw = piBlock.raw;
4138
- const emptyPattern = /m_AddedGameObjects:[ \t]*\[\]/;
4139
- if (emptyPattern.test(raw)) {
4140
- raw = raw.replace(emptyPattern, `m_AddedGameObjects:${entry}`);
4141
- piBlock.replace_raw(raw);
4142
- return;
4273
+ function parsePrefabRef(refText) {
4274
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
4275
+ if (!fileIdMatch)
4276
+ return null;
4277
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
4278
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
4279
+ return {
4280
+ file_id: fileIdMatch[1],
4281
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
4282
+ type: typeMatch ? typeMatch[1] : undefined
4283
+ };
4284
+ }
4285
+ function extractPrefabSourceGuid(prefabInstanceRaw) {
4286
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
4287
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
4288
+ }
4289
+ function normalizePrefabRef(refText, sourceGuid) {
4290
+ const parsed = parsePrefabRef(refText);
4291
+ if (!parsed) {
4292
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
4293
+ }
4294
+ const guid = parsed.guid ?? sourceGuid;
4295
+ if (!guid) {
4296
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
4143
4297
  }
4144
- const existingPattern = /(m_AddedGameObjects:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
4145
- if (existingPattern.test(raw)) {
4146
- raw = raw.replace(existingPattern, `$1${entry}`);
4147
- piBlock.replace_raw(raw);
4298
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
4299
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
4148
4300
  }
4301
+ const type = parsed.type ?? "3";
4302
+ if (!/^-?\d+$/.test(type)) {
4303
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
4304
+ }
4305
+ return {
4306
+ ref: {
4307
+ file_id: parsed.file_id,
4308
+ guid,
4309
+ type
4310
+ }
4311
+ };
4149
4312
  }
4150
- function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
4151
- const piBlock = doc.find_by_file_id(piId);
4152
- if (!piBlock)
4153
- return;
4154
- const entry = `
4155
- - targetCorrespondingSourceObject: ${targetSourceRef}
4156
- insertIndex: -1
4157
- addedObject: {fileID: ${newComponentId}}`;
4158
- let raw = piBlock.raw;
4159
- const emptyPattern = /m_AddedComponents:[ \t]*\[\]/;
4160
- if (emptyPattern.test(raw)) {
4161
- raw = raw.replace(emptyPattern, `m_AddedComponents:${entry}`);
4162
- piBlock.replace_raw(raw);
4163
- return;
4313
+ function serializePrefabRef(ref) {
4314
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
4315
+ }
4316
+ function ensurePrefabModificationSerializedVersion(prefabInstanceBlock) {
4317
+ const raw = prefabInstanceBlock.raw;
4318
+ const updated = raw.replace(/(^([ \t]*)m_Modification:[ \t]*\n)(?!\2[ \t]+serializedVersion:[ \t]*\d+)/m, (_match, prefix, indent) => `${prefix}${indent} serializedVersion: 3
4319
+ `);
4320
+ if (updated === raw) {
4321
+ return false;
4322
+ }
4323
+ prefabInstanceBlock.replace_raw(updated);
4324
+ return true;
4325
+ }
4326
+ function addedOverrideEntryKey(entry) {
4327
+ return `${entry.target.file_id}|${entry.target.guid}|${entry.target.type}|${entry.insert_index}|${entry.added_object_file_id}`;
4328
+ }
4329
+ function splitPrefabArraySection(rawText, key) {
4330
+ const lines = rawText.split(`
4331
+ `);
4332
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
4333
+ let keyIndex = -1;
4334
+ let indent = "";
4335
+ for (let i = 0;i < lines.length; i++) {
4336
+ const match = lines[i].match(keyPattern);
4337
+ if (match) {
4338
+ keyIndex = i;
4339
+ indent = match[1];
4340
+ break;
4341
+ }
4342
+ }
4343
+ if (keyIndex === -1) {
4344
+ return null;
4345
+ }
4346
+ const keyIndentLen = indent.length;
4347
+ let sectionEnd = keyIndex + 1;
4348
+ for (let i = keyIndex + 1;i < lines.length; i++) {
4349
+ const line = lines[i];
4350
+ const trimmed = line.trim();
4351
+ if (trimmed.length === 0) {
4352
+ sectionEnd = i + 1;
4353
+ continue;
4354
+ }
4355
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
4356
+ if (indentLen < keyIndentLen) {
4357
+ break;
4358
+ }
4359
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
4360
+ break;
4361
+ }
4362
+ sectionEnd = i + 1;
4363
+ }
4364
+ return {
4365
+ lines,
4366
+ key_index: keyIndex,
4367
+ section_end: sectionEnd,
4368
+ indent
4369
+ };
4370
+ }
4371
+ function collectAddedOverrideEntries(lines, key_index, section_end, sourceGuid) {
4372
+ const sectionLines = lines.slice(key_index + 1, section_end);
4373
+ const dedup = new Map;
4374
+ let i = 0;
4375
+ while (i < sectionLines.length) {
4376
+ const line = sectionLines[i];
4377
+ const trimmed = line.trimStart();
4378
+ if (!trimmed.startsWith("-")) {
4379
+ i++;
4380
+ continue;
4381
+ }
4382
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
4383
+ const entryLines = [line];
4384
+ i++;
4385
+ while (i < sectionLines.length) {
4386
+ const nextLine = sectionLines[i];
4387
+ const nextTrimmed = nextLine.trim();
4388
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
4389
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
4390
+ break;
4391
+ }
4392
+ entryLines.push(nextLine);
4393
+ i++;
4394
+ }
4395
+ const entryText = entryLines.join(`
4396
+ `);
4397
+ const targetMatch = entryText.match(/targetCorrespondingSourceObject:[ \t]*(\{[^}]+\})/);
4398
+ const insertMatch = entryText.match(/insertIndex:[ \t]*(-?\d+)/);
4399
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
4400
+ if (!targetMatch || !addedObjectMatch) {
4401
+ continue;
4402
+ }
4403
+ const normalizedTarget = normalizePrefabRef(targetMatch[1], sourceGuid).ref;
4404
+ if (!normalizedTarget) {
4405
+ continue;
4406
+ }
4407
+ const insertIndex = insertMatch ? Number.parseInt(insertMatch[1], 10) : -1;
4408
+ const entry = {
4409
+ target: normalizedTarget,
4410
+ insert_index: Number.isNaN(insertIndex) ? -1 : insertIndex,
4411
+ added_object_file_id: addedObjectMatch[1]
4412
+ };
4413
+ const dedupKey = addedOverrideEntryKey(entry);
4414
+ if (!dedup.has(dedupKey)) {
4415
+ dedup.set(dedupKey, entry);
4416
+ }
4164
4417
  }
4165
- const existingPattern = /(m_AddedComponents:[ \t]*\n(?:[ \t]+-[\s\S]*?(?=\n[ \t]+m_|$))*)/;
4166
- if (existingPattern.test(raw)) {
4167
- raw = raw.replace(existingPattern, `$1${entry}`);
4168
- piBlock.replace_raw(raw);
4418
+ return [...dedup.values()];
4419
+ }
4420
+ function appendToAddedOverrideArray(doc, piId, key, targetSourceRef, newObjectId) {
4421
+ const piBlock = doc.find_by_file_id(piId);
4422
+ if (!piBlock) {
4423
+ return { success: false, error: `PrefabInstance with fileID ${piId} not found` };
4424
+ }
4425
+ ensurePrefabModificationSerializedVersion(piBlock);
4426
+ const split = splitPrefabArraySection(piBlock.raw, key);
4427
+ if (!split) {
4428
+ return { success: false, error: `${key} property not found in PrefabInstance ${piId}` };
4429
+ }
4430
+ const sourceGuid = extractPrefabSourceGuid(piBlock.raw);
4431
+ const normalizedTarget = normalizePrefabRef(targetSourceRef, sourceGuid);
4432
+ if (!normalizedTarget.ref) {
4433
+ return { success: false, error: normalizedTarget.error };
4434
+ }
4435
+ const candidate = {
4436
+ target: normalizedTarget.ref,
4437
+ insert_index: -1,
4438
+ added_object_file_id: newObjectId
4439
+ };
4440
+ const candidateKey = addedOverrideEntryKey(candidate);
4441
+ const existing = collectAddedOverrideEntries(split.lines, split.key_index, split.section_end, sourceGuid);
4442
+ if (existing.some((entry) => addedOverrideEntryKey(entry) === candidateKey)) {
4443
+ return { success: true, changed: false };
4444
+ }
4445
+ const entryLines = [
4446
+ `${split.indent}- targetCorrespondingSourceObject: ${serializePrefabRef(candidate.target)}`,
4447
+ `${split.indent} insertIndex: ${candidate.insert_index}`,
4448
+ `${split.indent} addedObject: {fileID: ${candidate.added_object_file_id}}`
4449
+ ];
4450
+ const keyLine = split.lines[split.key_index].trim();
4451
+ let insertIndex = split.section_end;
4452
+ while (insertIndex > split.key_index + 1 && split.lines[insertIndex - 1].trim().length === 0) {
4453
+ insertIndex--;
4454
+ }
4455
+ let updatedLines;
4456
+ if (keyLine === `${key}: []`) {
4457
+ const replacement = [`${split.indent}${key}:`, ...entryLines];
4458
+ updatedLines = [
4459
+ ...split.lines.slice(0, split.key_index),
4460
+ ...replacement,
4461
+ ...split.lines.slice(split.section_end)
4462
+ ];
4463
+ } else {
4464
+ updatedLines = [
4465
+ ...split.lines.slice(0, insertIndex),
4466
+ ...entryLines,
4467
+ ...split.lines.slice(insertIndex)
4468
+ ];
4169
4469
  }
4470
+ piBlock.replace_raw(updatedLines.join(`
4471
+ `));
4472
+ return { success: true, changed: true };
4473
+ }
4474
+ function appendToAddedGameObjects(doc, piId, targetSourceRef, newGoId) {
4475
+ return appendToAddedOverrideArray(doc, piId, "m_AddedGameObjects", targetSourceRef, newGoId);
4170
4476
  }
4477
+ function appendToAddedComponents(doc, piId, targetSourceRef, newComponentId) {
4478
+ return appendToAddedOverrideArray(doc, piId, "m_AddedComponents", targetSourceRef, newComponentId);
4479
+ }
4480
+ var COMPONENT_FULL_TEMPLATES = {
4481
+ 65: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
4482
+ m_Material: {fileID: 0}
4483
+ m_IncludeLayers:
4484
+ serializedVersion: 2
4485
+ m_Bits: 0
4486
+ m_ExcludeLayers:
4487
+ serializedVersion: 2
4488
+ m_Bits: 0
4489
+ m_LayerOverridePriority: 0
4490
+ m_IsTrigger: 0
4491
+ m_ProvidesContacts: 0
4492
+ m_Enabled: 1
4493
+ serializedVersion: 3
4494
+ m_Size: {x: 1, y: 1, z: 1}
4495
+ m_Center: {x: 0, y: 0, z: 0}`,
4496
+ 82: (gameObjectId) => ` m_GameObject: {fileID: ${gameObjectId}}
4497
+ m_Enabled: 1
4498
+ serializedVersion: 4
4499
+ OutputAudioMixerGroup: {fileID: 0}
4500
+ m_audioClip: {fileID: 0}
4501
+ m_PlayOnAwake: 1
4502
+ m_Volume: 1
4503
+ m_Pitch: 1
4504
+ Loop: 0
4505
+ Mute: 0
4506
+ Spatialize: 0
4507
+ SpatializePostEffects: 0
4508
+ Priority: 128
4509
+ DopplerLevel: 1
4510
+ MinDistance: 1
4511
+ MaxDistance: 500
4512
+ Pan2D: 0
4513
+ rolloffMode: 0
4514
+ BypassEffects: 0
4515
+ BypassListenerEffects: 0
4516
+ BypassReverbZones: 0
4517
+ rolloffCustomCurve:
4518
+ serializedVersion: 2
4519
+ m_Curve:
4520
+ - serializedVersion: 3
4521
+ time: 0
4522
+ value: 1
4523
+ inSlope: 0
4524
+ outSlope: 0
4525
+ tangentMode: 0
4526
+ weightedMode: 0
4527
+ inWeight: 0.33333334
4528
+ outWeight: 0.33333334
4529
+ - serializedVersion: 3
4530
+ time: 1
4531
+ value: 0
4532
+ inSlope: 0
4533
+ outSlope: 0
4534
+ tangentMode: 0
4535
+ weightedMode: 0
4536
+ inWeight: 0.33333334
4537
+ outWeight: 0.33333334
4538
+ m_PreInfinity: 2
4539
+ m_PostInfinity: 2
4540
+ m_RotationOrder: 4
4541
+ panLevelCustomCurve:
4542
+ serializedVersion: 2
4543
+ m_Curve:
4544
+ - serializedVersion: 3
4545
+ time: 0
4546
+ value: 0
4547
+ inSlope: 0
4548
+ outSlope: 0
4549
+ tangentMode: 0
4550
+ weightedMode: 0
4551
+ inWeight: 0.33333334
4552
+ outWeight: 0.33333334
4553
+ m_PreInfinity: 2
4554
+ m_PostInfinity: 2
4555
+ m_RotationOrder: 4
4556
+ spreadCustomCurve:
4557
+ serializedVersion: 2
4558
+ m_Curve:
4559
+ - serializedVersion: 3
4560
+ time: 0
4561
+ value: 0
4562
+ inSlope: 0
4563
+ outSlope: 0
4564
+ tangentMode: 0
4565
+ weightedMode: 0
4566
+ inWeight: 0.33333334
4567
+ outWeight: 0.33333334
4568
+ m_PreInfinity: 2
4569
+ m_PostInfinity: 2
4570
+ m_RotationOrder: 4
4571
+ reverbZoneMixCustomCurve:
4572
+ serializedVersion: 2
4573
+ m_Curve:
4574
+ - serializedVersion: 3
4575
+ time: 0
4576
+ value: 1
4577
+ inSlope: 0
4578
+ outSlope: 0
4579
+ tangentMode: 0
4580
+ weightedMode: 0
4581
+ inWeight: 0.33333334
4582
+ outWeight: 0.33333334
4583
+ m_PreInfinity: 2
4584
+ m_PostInfinity: 2
4585
+ m_RotationOrder: 4`
4586
+ };
4171
4587
  var COMPONENT_DEFAULTS = {
4172
4588
  20: ` serializedVersion: 2
4173
4589
  m_ClearFlags: 1
@@ -4294,6 +4710,17 @@ var COMPONENT_DEFAULTS = {
4294
4710
  m_IgnoreParentGroups: 0`
4295
4711
  };
4296
4712
  function createGenericComponentYAML(componentName, classId, componentId, gameObjectId) {
4713
+ const fullTemplate = COMPONENT_FULL_TEMPLATES[classId];
4714
+ if (fullTemplate) {
4715
+ return `--- !u!${classId} &${componentId}
4716
+ ${componentName}:
4717
+ m_ObjectHideFlags: 0
4718
+ m_CorrespondingSourceObject: {fileID: 0}
4719
+ m_PrefabInstance: {fileID: 0}
4720
+ m_PrefabAsset: {fileID: 0}
4721
+ ${fullTemplate(gameObjectId)}
4722
+ `;
4723
+ }
4297
4724
  const defaults = COMPONENT_DEFAULTS[classId] ? `
4298
4725
  ` + COMPONENT_DEFAULTS[classId] + `
4299
4726
  ` : "";
@@ -4310,11 +4737,14 @@ ${defaults}`;
4310
4737
  function addComponentToGameObject(doc, gameObjectId, componentId) {
4311
4738
  const goBlock = doc.find_by_file_id(gameObjectId);
4312
4739
  if (!goBlock)
4313
- return;
4314
- let raw = goBlock.raw;
4315
- raw = raw.replace(/(m_Component:\s*\n(?:\s*-\s*component:\s*\{fileID:\s*\d+\}\s*\n)*)/, `$1 - component: {fileID: ${componentId}}
4740
+ return false;
4741
+ const raw = goBlock.raw;
4742
+ const updated = raw.replace(/(m_Component:[ \t]*\n(?:[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*\d+\}[ \t]*\n)*)/, `$1 - component: {fileID: ${componentId}}
4316
4743
  `);
4317
- goBlock.replace_raw(raw);
4744
+ if (updated === raw)
4745
+ return false;
4746
+ goBlock.replace_raw(updated);
4747
+ return true;
4318
4748
  }
4319
4749
  function createMonoBehaviourYAML(componentId, gameObjectId, scriptGuid, fields, version, scriptFileId = 11500000, type_lookup) {
4320
4750
  const field_yaml = fields && fields.length > 0 ? generate_field_yaml(fields, version, " ", type_lookup) : `
@@ -4386,7 +4816,7 @@ function createGameObject(options) {
4386
4816
  if (parentBlock.class_id === 4) {
4387
4817
  parentTransformIdStr = parentIdStr;
4388
4818
  } else if (parentBlock.class_id === 1) {
4389
- const compMatch = parentBlock.raw.match(/m_Component:\s*\n\s*-\s*component:\s*\{fileID:\s*(\d+)\}/);
4819
+ const compMatch = parentBlock.raw.match(/m_Component:[ \t]*\n[ \t]*-[ \t]*component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
4390
4820
  if (compMatch) {
4391
4821
  parentTransformIdStr = compMatch[1];
4392
4822
  } else {
@@ -4443,7 +4873,14 @@ function createGameObject(options) {
4443
4873
  doc.add_child_to_parent(parentTransformIdStr, transformIdStr);
4444
4874
  }
4445
4875
  if (variantPiId && strippedSourceRef) {
4446
- appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
4876
+ const appendResult = appendToAddedGameObjects(doc, variantPiId, strippedSourceRef, gameObjectIdStr);
4877
+ if (!appendResult.success) {
4878
+ return {
4879
+ success: false,
4880
+ file_path,
4881
+ error: appendResult.error || `Failed to update m_AddedGameObjects for PrefabInstance ${variantPiId}`
4882
+ };
4883
+ }
4447
4884
  }
4448
4885
  const saveResult = doc.save();
4449
4886
  if (!saveResult.success) {
@@ -4926,6 +5363,7 @@ PrefabInstance:
4926
5363
  m_ObjectHideFlags: 0
4927
5364
  serializedVersion: 2
4928
5365
  m_Modification:
5366
+ serializedVersion: 3
4929
5367
  m_TransformParent: {fileID: 0}
4930
5368
  m_Modifications:
4931
5369
  - target: {fileID: ${rootInfo.game_object.file_id}, guid: ${sourceGuid}, type: 3}
@@ -5212,6 +5650,8 @@ function is_valid_component_base(base_class, project_path, depth = 0) {
5212
5650
  }
5213
5651
  if (!fullPath.endsWith(".cs") || !import_fs10.existsSync(fullPath)) {
5214
5652
  if (match.kind === "class") {
5653
+ if (fullPath.includes("Assembly-CSharp.dll"))
5654
+ return true;
5215
5655
  return is_likely_component_base_name(base_class);
5216
5656
  }
5217
5657
  }
@@ -5296,7 +5736,7 @@ function addComponent(options) {
5296
5736
  let duplicateWarning;
5297
5737
  const goBlock = doc.find_by_file_id(gameObjectIdStr);
5298
5738
  if (goBlock && classId !== null) {
5299
- const compRefs = [...goBlock.raw.matchAll(/component:\s*\{fileID:\s*(\d+)\}/g)].map((m) => m[1]);
5739
+ const compRefs = [...goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/g)].map((m) => m[1]);
5300
5740
  for (const refId of compRefs) {
5301
5741
  const compBlock = doc.find_by_file_id(refId);
5302
5742
  if (compBlock && compBlock.class_id === classId) {
@@ -5356,11 +5796,11 @@ function addComponent(options) {
5356
5796
  };
5357
5797
  }
5358
5798
  if (resolved.base_class && !COMPONENT_BASE_CLASSES.has(resolved.base_class) && !is_valid_component_base(resolved.base_class, project_path)) {
5359
- return {
5360
- success: false,
5361
- file_path,
5362
- error: `"${component_type}" extends ${resolved.base_class}, which does not inherit from MonoBehaviour. Cannot add as a component. If this is incorrect, ensure the type registry is up to date ("setup" command).`
5363
- };
5799
+ const warn = `"${component_type}" extends ${resolved.base_class}, which does not inherit from MonoBehaviour. Adding anyway, but ensure the script is a valid component. (Re-run "setup" if the inheritance graph is stale).`;
5800
+ if (!extractionError)
5801
+ extractionError = warn;
5802
+ else
5803
+ extractionError += " " + warn;
5364
5804
  }
5365
5805
  let version;
5366
5806
  if (project_path) {
@@ -5376,14 +5816,50 @@ function addComponent(options) {
5376
5816
  componentYAML = createMonoBehaviourYAML(componentId, gameObjectId, resolved.guid, resolved.fields, version, scriptFileId, type_lookup_comp);
5377
5817
  scriptGuid = resolved.guid;
5378
5818
  scriptPath = resolved.path || undefined;
5379
- extractionError = resolved.extraction_error;
5819
+ if (resolved.extraction_error) {
5820
+ if (!extractionError)
5821
+ extractionError = resolved.extraction_error;
5822
+ else
5823
+ extractionError += " " + resolved.extraction_error;
5824
+ }
5380
5825
  }
5381
5826
  if (variantInfo) {
5382
- appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
5827
+ const appendResult = appendToAddedComponents(doc, variantInfo.prefab_instance_id, variantInfo.source_ref, componentIdStr);
5828
+ if (!appendResult.success) {
5829
+ return {
5830
+ success: false,
5831
+ file_path,
5832
+ error: appendResult.error || `Failed to update m_AddedComponents for PrefabInstance ${variantInfo.prefab_instance_id}`
5833
+ };
5834
+ }
5383
5835
  } else {
5384
- addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
5836
+ const added = addComponentToGameObject(doc, gameObjectIdStr, componentIdStr);
5837
+ if (!added) {
5838
+ return {
5839
+ success: false,
5840
+ file_path,
5841
+ error: `Failed to add component reference to GameObject ${gameObjectIdStr}: m_Component array not found or malformed`
5842
+ };
5843
+ }
5385
5844
  }
5386
5845
  doc.append_raw(componentYAML);
5846
+ if (!doc.validate()) {
5847
+ return {
5848
+ success: false,
5849
+ file_path,
5850
+ error: "Validation failed after adding component. The generated YAML may be malformed."
5851
+ };
5852
+ }
5853
+ if (!variantInfo) {
5854
+ const goBlockAfter = doc.find_by_file_id(gameObjectIdStr);
5855
+ if (!goBlockAfter || !goBlockAfter.raw.includes(`component: {fileID: ${componentIdStr}}`)) {
5856
+ return {
5857
+ success: false,
5858
+ file_path,
5859
+ error: `Component block appended but reference not found in GameObject ${gameObjectIdStr}'s m_Component list`
5860
+ };
5861
+ }
5862
+ }
5387
5863
  const saveResult = doc.save();
5388
5864
  if (!saveResult.success) {
5389
5865
  return {
@@ -5439,8 +5915,11 @@ function copyComponent(options) {
5439
5915
  const newIdStr = doc.generate_file_id();
5440
5916
  const newId = parseInt(newIdStr, 10);
5441
5917
  let clonedBlock = sourceBlock.raw.replace(new RegExp(`^(--- !u!${sourceBlock.class_id} &)${source_file_id}`), `$1${newId}`);
5442
- clonedBlock = clonedBlock.replace(/m_GameObject:\s*\{fileID:\s*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
5443
- addComponentToGameObject(doc, targetGoIdStr, newIdStr);
5918
+ clonedBlock = clonedBlock.replace(/m_GameObject:[ \t]*\{fileID:[ \t]*\d+\}/, `m_GameObject: {fileID: ${targetGoId}}`);
5919
+ const added = addComponentToGameObject(doc, targetGoIdStr, newIdStr);
5920
+ if (!added) {
5921
+ return { success: false, file_path, error: `Failed to wire component to target GameObject ${targetGoIdStr}: m_Component array not found or malformed` };
5922
+ }
5444
5923
  doc.append_raw(clonedBlock);
5445
5924
  if (!doc.validate()) {
5446
5925
  return { success: false, file_path, error: "Validation failed after copying component" };
@@ -5490,7 +5969,17 @@ function validate_value_type(current_value, new_value) {
5490
5969
  const incoming = new_value.trim();
5491
5970
  if (/^\{fileID:/.test(current)) {
5492
5971
  if (/^\{fileID:[ \t]*0[ \t]*\}$/.test(current)) {
5493
- return null;
5972
+ if (/^\{fileID:/.test(incoming))
5973
+ return null;
5974
+ if (/^-?\d+(\.\d+)?([eE][+-]?\d+)?$/.test(incoming))
5975
+ return null;
5976
+ if (/^(true|false|yes|no|on|off)$/i.test(incoming))
5977
+ return null;
5978
+ if (/^'[^']*'$/.test(incoming) || /^"[^"]*"$/.test(incoming))
5979
+ return null;
5980
+ if (/^\{.*:.*\}$/.test(incoming))
5981
+ return null;
5982
+ return `Current value is a null reference ({fileID: 0}). New value "${incoming}" is not a valid reference, number, boolean, quoted string, or compound value.`;
5494
5983
  }
5495
5984
  if (!/^\{fileID:/.test(incoming)) {
5496
5985
  return `Expected a reference value ({fileID: ...}), got "${incoming}"`;
@@ -5700,7 +6189,19 @@ function editProperty(options) {
5700
6189
  return result;
5701
6190
  }
5702
6191
  function editComponentByFileId(options) {
5703
- const { file_path, file_id, property, new_value } = options;
6192
+ const { file_path, file_id, property, project_path } = options;
6193
+ let { new_value } = options;
6194
+ const resolved = resolveAssetPathToPPtr(new_value, file_path, project_path);
6195
+ if (resolved === null) {
6196
+ return {
6197
+ success: false,
6198
+ file_path,
6199
+ error: `Could not resolve "${new_value}" to asset reference. Ensure GUID cache exists (run "setup" first, or "setup -p <path>").`
6200
+ };
6201
+ }
6202
+ if (resolved !== undefined) {
6203
+ new_value = resolved;
6204
+ }
5704
6205
  const pathError = validate_file_path(file_path, "write");
5705
6206
  if (pathError) {
5706
6207
  return { success: false, file_path, error: pathError };
@@ -6160,6 +6661,193 @@ function findPrefabInstanceBlock(doc, identifier) {
6160
6661
  }
6161
6662
  return null;
6162
6663
  }
6664
+ function parsePrefabRef2(refText) {
6665
+ const fileIdMatch = refText.match(/fileID:[ \t]*(-?\d+)/i);
6666
+ if (!fileIdMatch)
6667
+ return null;
6668
+ const guidMatch = refText.match(/guid:[ \t]*([a-f0-9]{32})/i);
6669
+ const typeMatch = refText.match(/type:[ \t]*(-?\d+)/i);
6670
+ return {
6671
+ file_id: fileIdMatch[1],
6672
+ guid: guidMatch ? guidMatch[1].toLowerCase() : undefined,
6673
+ type: typeMatch ? typeMatch[1] : undefined
6674
+ };
6675
+ }
6676
+ function extractPrefabSourceGuid2(prefabInstanceRaw) {
6677
+ const sourceMatch = prefabInstanceRaw.match(/m_SourcePrefab:[ \t]*\{[^}]*guid:[ \t]*([a-f0-9]{32})/i);
6678
+ return sourceMatch ? sourceMatch[1].toLowerCase() : null;
6679
+ }
6680
+ function normalizePrefabRef2(refText, sourceGuid) {
6681
+ const parsed = parsePrefabRef2(refText);
6682
+ if (!parsed) {
6683
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
6684
+ }
6685
+ const guid = parsed.guid ?? sourceGuid;
6686
+ if (!guid) {
6687
+ return { error: `Reference "${refText}" is missing guid and source prefab guid could not be inferred.` };
6688
+ }
6689
+ if (!/^[a-f0-9]{32}$/i.test(guid)) {
6690
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
6691
+ }
6692
+ const type = parsed.type ?? "3";
6693
+ if (!/^-?\d+$/.test(type)) {
6694
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
6695
+ }
6696
+ return {
6697
+ ref: {
6698
+ file_id: parsed.file_id,
6699
+ guid,
6700
+ type
6701
+ }
6702
+ };
6703
+ }
6704
+ function parsePrefabRefMatcher(refText) {
6705
+ const parsed = parsePrefabRef2(refText);
6706
+ if (!parsed) {
6707
+ return { error: `Invalid reference "${refText}". Expected format: {fileID: N, guid: ..., type: T}` };
6708
+ }
6709
+ if (parsed.guid && !/^[a-f0-9]{32}$/i.test(parsed.guid)) {
6710
+ return { error: `Invalid guid in reference "${refText}". Expected 32-character hex GUID.` };
6711
+ }
6712
+ if (parsed.type && !/^-?\d+$/.test(parsed.type)) {
6713
+ return { error: `Invalid type in reference "${refText}". Expected an integer type value.` };
6714
+ }
6715
+ return { matcher: parsed };
6716
+ }
6717
+ function prefabRefKey(ref) {
6718
+ return `${ref.file_id}|${ref.guid}|${ref.type}`;
6719
+ }
6720
+ function serializePrefabRef2(ref) {
6721
+ return `{fileID: ${ref.file_id}, guid: ${ref.guid}, type: ${ref.type}}`;
6722
+ }
6723
+ function splitPrefabArraySection2(rawText, key) {
6724
+ const lines = rawText.split(`
6725
+ `);
6726
+ const keyPattern = new RegExp(`^([ ]*)${key}:[ ]*(.*)$`);
6727
+ let keyIndex = -1;
6728
+ let indent = "";
6729
+ for (let i = 0;i < lines.length; i++) {
6730
+ const match = lines[i].match(keyPattern);
6731
+ if (match) {
6732
+ keyIndex = i;
6733
+ indent = match[1];
6734
+ break;
6735
+ }
6736
+ }
6737
+ if (keyIndex === -1) {
6738
+ return null;
6739
+ }
6740
+ const keyIndentLen = indent.length;
6741
+ let sectionEnd = keyIndex + 1;
6742
+ for (let i = keyIndex + 1;i < lines.length; i++) {
6743
+ const line = lines[i];
6744
+ const trimmed = line.trim();
6745
+ if (trimmed.length === 0) {
6746
+ sectionEnd = i + 1;
6747
+ continue;
6748
+ }
6749
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
6750
+ if (indentLen < keyIndentLen) {
6751
+ break;
6752
+ }
6753
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
6754
+ break;
6755
+ }
6756
+ sectionEnd = i + 1;
6757
+ }
6758
+ return {
6759
+ lines,
6760
+ key_index: keyIndex,
6761
+ section_end: sectionEnd,
6762
+ indent
6763
+ };
6764
+ }
6765
+ function collectNormalizedRefsFromSection(lines, key_index, section_end, sourceGuid) {
6766
+ const sectionText = lines.slice(key_index, section_end).join(`
6767
+ `);
6768
+ const refMatches = sectionText.match(/\{[^}]*\}/g) || [];
6769
+ const dedup = new Map;
6770
+ for (const token of refMatches) {
6771
+ const parsed = parsePrefabRef2(token);
6772
+ if (!parsed)
6773
+ continue;
6774
+ const guid = parsed.guid ?? sourceGuid;
6775
+ if (!guid || !/^[a-f0-9]{32}$/i.test(guid))
6776
+ continue;
6777
+ const type = parsed.type ?? "3";
6778
+ if (!/^-?\d+$/.test(type))
6779
+ continue;
6780
+ const normalized = {
6781
+ file_id: parsed.file_id,
6782
+ guid: guid.toLowerCase(),
6783
+ type
6784
+ };
6785
+ const key = prefabRefKey(normalized);
6786
+ if (!dedup.has(key)) {
6787
+ dedup.set(key, normalized);
6788
+ }
6789
+ }
6790
+ return [...dedup.values()];
6791
+ }
6792
+ function mutatePrefabReferenceList(options) {
6793
+ const { target_block, key, ref_text, action, item_label } = options;
6794
+ const split = splitPrefabArraySection2(target_block.raw, key);
6795
+ if (!split) {
6796
+ return { success: false, error: `${key} property not found in PrefabInstance` };
6797
+ }
6798
+ const sourceGuid = extractPrefabSourceGuid2(target_block.raw);
6799
+ const existing = collectNormalizedRefsFromSection(split.lines, split.key_index, split.section_end, sourceGuid);
6800
+ let next = existing;
6801
+ if (action === "add") {
6802
+ const normalized = normalizePrefabRef2(ref_text, sourceGuid);
6803
+ if (!normalized.ref) {
6804
+ return { success: false, error: normalized.error };
6805
+ }
6806
+ const keyText = prefabRefKey(normalized.ref);
6807
+ if (existing.some((entry) => prefabRefKey(entry) === keyText)) {
6808
+ return { success: true, changed: false };
6809
+ }
6810
+ next = [...existing, normalized.ref];
6811
+ } else {
6812
+ const matcherResult = parsePrefabRefMatcher(ref_text);
6813
+ const matcher = matcherResult.matcher;
6814
+ if (!matcher) {
6815
+ return { success: false, error: matcherResult.error };
6816
+ }
6817
+ next = existing.filter((entry) => {
6818
+ if (entry.file_id !== matcher.file_id)
6819
+ return true;
6820
+ if (matcher.guid && entry.guid !== matcher.guid.toLowerCase())
6821
+ return true;
6822
+ if (matcher.type && entry.type !== matcher.type)
6823
+ return true;
6824
+ return false;
6825
+ });
6826
+ if (next.length === existing.length) {
6827
+ return {
6828
+ success: false,
6829
+ error: `${item_label} reference "${ref_text}" not found in ${key}`
6830
+ };
6831
+ }
6832
+ }
6833
+ const replacement = [];
6834
+ if (next.length === 0) {
6835
+ replacement.push(`${split.indent}${key}: []`);
6836
+ } else {
6837
+ replacement.push(`${split.indent}${key}:`);
6838
+ for (const entry of next) {
6839
+ replacement.push(`${split.indent}- ${serializePrefabRef2(entry)}`);
6840
+ }
6841
+ }
6842
+ const updatedLines = [
6843
+ ...split.lines.slice(0, split.key_index),
6844
+ ...replacement,
6845
+ ...split.lines.slice(split.section_end)
6846
+ ];
6847
+ target_block.replace_raw(updatedLines.join(`
6848
+ `));
6849
+ return { success: true, changed: true };
6850
+ }
6163
6851
  function editArray(options) {
6164
6852
  const { file_path, file_id, array_property, action, value, index } = options;
6165
6853
  if (!import_fs11.existsSync(file_path)) {
@@ -6370,9 +7058,6 @@ function addRemovedComponent(options) {
6370
7058
  if (!import_fs11.existsSync(file_path)) {
6371
7059
  return { success: false, file_path, error: `File not found: ${file_path}` };
6372
7060
  }
6373
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
6374
- return { success: false, file_path, error: `Invalid component reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
6375
- }
6376
7061
  let doc;
6377
7062
  try {
6378
7063
  doc = UnityDocument.from_file(file_path);
@@ -6383,19 +7068,16 @@ function addRemovedComponent(options) {
6383
7068
  if (!targetBlock) {
6384
7069
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
6385
7070
  }
6386
- let rawText = targetBlock.raw;
6387
- if (/m_RemovedComponents:\s*\[\]/.test(rawText)) {
6388
- rawText = rawText.replace(/m_RemovedComponents:\s*\[\]/, "m_RemovedComponents:");
6389
- }
6390
- const removedPattern = /m_RemovedComponents:\s*\n/;
6391
- if (removedPattern.test(rawText)) {
6392
- rawText = rawText.replace(removedPattern, `m_RemovedComponents:
6393
- - ${component_ref}
6394
- `);
6395
- } else {
6396
- return { success: false, file_path, error: "m_RemovedComponents property not found in PrefabInstance" };
7071
+ const mutation = mutatePrefabReferenceList({
7072
+ target_block: targetBlock,
7073
+ key: "m_RemovedComponents",
7074
+ ref_text: component_ref,
7075
+ action: "add",
7076
+ item_label: "Component"
7077
+ });
7078
+ if (!mutation.success) {
7079
+ return { success: false, file_path, error: mutation.error };
6397
7080
  }
6398
- targetBlock.replace_raw(rawText);
6399
7081
  if (!doc.validate()) {
6400
7082
  return { success: false, file_path, error: "Validation failed after adding removed component" };
6401
7083
  }
@@ -6424,13 +7106,16 @@ function removeRemovedComponent(options) {
6424
7106
  if (!targetBlock) {
6425
7107
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
6426
7108
  }
6427
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6428
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
6429
- if (!refPattern.test(targetBlock.raw)) {
6430
- return { success: false, file_path, error: `Component reference "${component_ref}" not found in m_RemovedComponents` };
7109
+ const mutation = mutatePrefabReferenceList({
7110
+ target_block: targetBlock,
7111
+ key: "m_RemovedComponents",
7112
+ ref_text: component_ref,
7113
+ action: "remove",
7114
+ item_label: "Component"
7115
+ });
7116
+ if (!mutation.success) {
7117
+ return { success: false, file_path, error: mutation.error };
6431
7118
  }
6432
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
6433
- targetBlock.replace_raw(updatedRaw);
6434
7119
  if (!doc.validate()) {
6435
7120
  return { success: false, file_path, error: "Validation failed after removing component" };
6436
7121
  }
@@ -6449,9 +7134,6 @@ function addRemovedGameObject(options) {
6449
7134
  if (!import_fs11.existsSync(file_path)) {
6450
7135
  return { success: false, file_path, error: `File not found: ${file_path}` };
6451
7136
  }
6452
- if (!/^\{fileID:\s*-?\d+/.test(component_ref)) {
6453
- return { success: false, file_path, error: `Invalid GameObject reference "${component_ref}". Expected format: {fileID: N, guid: ..., type: T}` };
6454
- }
6455
7137
  let doc;
6456
7138
  try {
6457
7139
  doc = UnityDocument.from_file(file_path);
@@ -6462,19 +7144,16 @@ function addRemovedGameObject(options) {
6462
7144
  if (!targetBlock) {
6463
7145
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
6464
7146
  }
6465
- let rawText = targetBlock.raw;
6466
- if (/m_RemovedGameObjects:\s*\[\]/.test(rawText)) {
6467
- rawText = rawText.replace(/m_RemovedGameObjects:\s*\[\]/, "m_RemovedGameObjects:");
6468
- }
6469
- const removedPattern = /m_RemovedGameObjects:\s*\n/;
6470
- if (removedPattern.test(rawText)) {
6471
- rawText = rawText.replace(removedPattern, `m_RemovedGameObjects:
6472
- - ${component_ref}
6473
- `);
6474
- } else {
6475
- return { success: false, file_path, error: "m_RemovedGameObjects property not found in PrefabInstance" };
7147
+ const mutation = mutatePrefabReferenceList({
7148
+ target_block: targetBlock,
7149
+ key: "m_RemovedGameObjects",
7150
+ ref_text: component_ref,
7151
+ action: "add",
7152
+ item_label: "GameObject"
7153
+ });
7154
+ if (!mutation.success) {
7155
+ return { success: false, file_path, error: mutation.error };
6476
7156
  }
6477
- targetBlock.replace_raw(rawText);
6478
7157
  if (!doc.validate()) {
6479
7158
  return { success: false, file_path, error: "Validation failed after adding removed GameObject" };
6480
7159
  }
@@ -6503,13 +7182,16 @@ function removeRemovedGameObject(options) {
6503
7182
  if (!targetBlock) {
6504
7183
  return { success: false, file_path, error: `PrefabInstance "${prefab_instance}" not found` };
6505
7184
  }
6506
- const escapedRef = component_ref.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6507
- const refPattern = new RegExp(`\\s*- ${escapedRef}\\n?`, "m");
6508
- if (!refPattern.test(targetBlock.raw)) {
6509
- return { success: false, file_path, error: `GameObject reference "${component_ref}" not found in m_RemovedGameObjects` };
7185
+ const mutation = mutatePrefabReferenceList({
7186
+ target_block: targetBlock,
7187
+ key: "m_RemovedGameObjects",
7188
+ ref_text: component_ref,
7189
+ action: "remove",
7190
+ item_label: "GameObject"
7191
+ });
7192
+ if (!mutation.success) {
7193
+ return { success: false, file_path, error: mutation.error };
6510
7194
  }
6511
- const updatedRaw = targetBlock.raw.replace(refPattern, "");
6512
- targetBlock.replace_raw(updatedRaw);
6513
7195
  if (!doc.validate()) {
6514
7196
  return { success: false, file_path, error: "Validation failed after removing GameObject" };
6515
7197
  }
@@ -6608,7 +7290,8 @@ function removeComponent(options) {
6608
7290
  return { success: false, file_path, error: "Cannot remove a Transform with remove-component. Use delete to remove the entire GameObject." };
6609
7291
  }
6610
7292
  const resolved_file_id = found.file_id;
6611
- const goMatch = found.raw.match(/m_GameObject:\s*\{fileID:\s*(-?\d+)\}/);
7293
+ let removed_added_component_overrides = 0;
7294
+ const goMatch = found.raw.match(/m_GameObject:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6612
7295
  if (goMatch) {
6613
7296
  const parentGoId = goMatch[1];
6614
7297
  const goBlock = doc.find_by_file_id(parentGoId);
@@ -6616,7 +7299,29 @@ function removeComponent(options) {
6616
7299
  const escaped = resolved_file_id.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
6617
7300
  const compLinePattern = new RegExp(`^[ \\t]*- component: \\{fileID: ${escaped}\\}[ \\t]*\\n`, "m");
6618
7301
  const modifiedRaw = goBlock.raw.replace(compLinePattern, "");
7302
+ if (modifiedRaw === goBlock.raw) {}
6619
7303
  goBlock.replace_raw(modifiedRaw);
7304
+ if (goBlock.is_stripped) {
7305
+ const piMatch = goBlock.raw.match(/m_PrefabInstance:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
7306
+ if (piMatch) {
7307
+ const piBlock = doc.find_by_file_id(piMatch[1]);
7308
+ if (piBlock && piBlock.class_id === 1001) {
7309
+ const updated = removeAddedComponentOverrideEntries(piBlock.raw, resolved_file_id);
7310
+ if (updated.removed_count > 0) {
7311
+ piBlock.replace_raw(updated.updated_raw);
7312
+ removed_added_component_overrides += updated.removed_count;
7313
+ }
7314
+ }
7315
+ }
7316
+ }
7317
+ }
7318
+ }
7319
+ const dangling = [];
7320
+ for (const block of doc.blocks) {
7321
+ if (block.file_id === resolved_file_id)
7322
+ continue;
7323
+ if (block.raw.includes(`{fileID: ${resolved_file_id}}`)) {
7324
+ dangling.push(block.file_id);
6620
7325
  }
6621
7326
  }
6622
7327
  doc.remove_block(resolved_file_id);
@@ -6631,7 +7336,8 @@ function removeComponent(options) {
6631
7336
  success: true,
6632
7337
  file_path,
6633
7338
  removed_file_id: resolved_file_id,
6634
- removed_class_id: found.class_id
7339
+ removed_class_id: found.class_id,
7340
+ warning: dangling.length > 0 ? `Dangling references to deleted component found in blocks: ${dangling.join(", ")}${removed_added_component_overrides > 0 ? ` (removed ${removed_added_component_overrides} m_AddedComponents override entr${removed_added_component_overrides === 1 ? "y" : "ies"})` : ""}` : undefined
6635
7341
  };
6636
7342
  }
6637
7343
  function removeComponentBatch(options) {
@@ -6692,7 +7398,7 @@ function deleteGameObject(options) {
6692
7398
  const goBlock = goResult;
6693
7399
  const goId = goBlock.file_id;
6694
7400
  const componentIds = new Set;
6695
- const compMatches = goBlock.raw.matchAll(/component:\s*\{fileID:\s*(-?\d+)\}/g);
7401
+ const compMatches = goBlock.raw.matchAll(/component:[ \t]*\{fileID:[ \t]*(-?\d+)\}/g);
6696
7402
  for (const cm of compMatches) {
6697
7403
  componentIds.add(cm[1]);
6698
7404
  }
@@ -6702,7 +7408,7 @@ function deleteGameObject(options) {
6702
7408
  const block = doc.find_by_file_id(compId);
6703
7409
  if (block && block.class_id === 4) {
6704
7410
  transformId = compId;
6705
- const fatherMatch = block.raw.match(/m_Father:\s*\{fileID:\s*(-?\d+)\}/);
7411
+ const fatherMatch = block.raw.match(/m_Father:[ \t]*\{fileID:[ \t]*(-?\d+)\}/);
6706
7412
  if (fatherMatch) {
6707
7413
  fatherId = fatherMatch[1];
6708
7414
  }
@@ -6749,6 +7455,124 @@ function findPrefabInstanceBlock2(doc, identifier) {
6749
7455
  }
6750
7456
  return null;
6751
7457
  }
7458
+ function splitPrefabArraySection3(rawText, key) {
7459
+ const lines = rawText.split(`
7460
+ `);
7461
+ const keyPattern = new RegExp(`^[ ]*${key}:[ ]*(.*)$`);
7462
+ let keyIndex = -1;
7463
+ let keyIndentLen = 0;
7464
+ for (let i = 0;i < lines.length; i++) {
7465
+ const match = lines[i].match(keyPattern);
7466
+ if (match) {
7467
+ keyIndex = i;
7468
+ keyIndentLen = (lines[i].match(/^[ \t]*/) || [""])[0].length;
7469
+ break;
7470
+ }
7471
+ }
7472
+ if (keyIndex === -1) {
7473
+ return null;
7474
+ }
7475
+ let sectionEnd = keyIndex + 1;
7476
+ for (let i = keyIndex + 1;i < lines.length; i++) {
7477
+ const line = lines[i];
7478
+ const trimmed = line.trim();
7479
+ if (trimmed.length === 0) {
7480
+ sectionEnd = i + 1;
7481
+ continue;
7482
+ }
7483
+ const indentLen = (line.match(/^[ \t]*/) || [""])[0].length;
7484
+ if (indentLen < keyIndentLen) {
7485
+ break;
7486
+ }
7487
+ if (indentLen === keyIndentLen && !trimmed.startsWith("-") && /^[A-Za-z_][A-Za-z0-9_]*:/.test(trimmed)) {
7488
+ break;
7489
+ }
7490
+ sectionEnd = i + 1;
7491
+ }
7492
+ return {
7493
+ lines,
7494
+ key_index: keyIndex,
7495
+ section_end: sectionEnd
7496
+ };
7497
+ }
7498
+ function collectAddedObjectFileIdsFromArray(rawText, key) {
7499
+ const section = splitPrefabArraySection3(rawText, key);
7500
+ if (!section)
7501
+ return [];
7502
+ const keyLine = section.lines[section.key_index].trim();
7503
+ if (new RegExp(`^${key}:[ ]*[]$`).test(keyLine)) {
7504
+ return [];
7505
+ }
7506
+ const sectionText = section.lines.slice(section.key_index, section.section_end).join(`
7507
+ `);
7508
+ const fileIds = new Set;
7509
+ const addedObjectPattern = /addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/g;
7510
+ let match;
7511
+ while ((match = addedObjectPattern.exec(sectionText)) !== null) {
7512
+ fileIds.add(match[1]);
7513
+ }
7514
+ if (fileIds.size === 0) {
7515
+ const inlineEntryPattern = /^[ \t]*-[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/gm;
7516
+ while ((match = inlineEntryPattern.exec(sectionText)) !== null) {
7517
+ fileIds.add(match[1]);
7518
+ }
7519
+ }
7520
+ return [...fileIds];
7521
+ }
7522
+ function removeAddedComponentOverrideEntries(rawText, componentId) {
7523
+ const section = splitPrefabArraySection3(rawText, "m_AddedComponents");
7524
+ if (!section)
7525
+ return { updated_raw: rawText, removed_count: 0 };
7526
+ const keyLine = section.lines[section.key_index].trim();
7527
+ if (/^m_AddedComponents:[ \t]*\[\]$/.test(keyLine)) {
7528
+ return { updated_raw: rawText, removed_count: 0 };
7529
+ }
7530
+ const bodyLines = section.lines.slice(section.key_index + 1, section.section_end);
7531
+ const keptEntries = [];
7532
+ let removed = 0;
7533
+ let i = 0;
7534
+ while (i < bodyLines.length) {
7535
+ const line = bodyLines[i];
7536
+ const trimmed = line.trimStart();
7537
+ if (!trimmed.startsWith("-")) {
7538
+ i++;
7539
+ continue;
7540
+ }
7541
+ const entryIndent = (line.match(/^[ \t]*/) || [""])[0].length;
7542
+ const entryLines = [line];
7543
+ i++;
7544
+ while (i < bodyLines.length) {
7545
+ const nextLine = bodyLines[i];
7546
+ const nextTrimmed = nextLine.trim();
7547
+ const nextIndent = (nextLine.match(/^[ \t]*/) || [""])[0].length;
7548
+ if (nextTrimmed.length > 0 && nextIndent === entryIndent && nextTrimmed.startsWith("-")) {
7549
+ break;
7550
+ }
7551
+ entryLines.push(nextLine);
7552
+ i++;
7553
+ }
7554
+ const entryText = entryLines.join(`
7555
+ `);
7556
+ const addedObjectMatch = entryText.match(/addedObject:[ \t]*\{[^}]*fileID:[ \t]*(-?\d+)/);
7557
+ if (addedObjectMatch && addedObjectMatch[1] === componentId) {
7558
+ removed++;
7559
+ continue;
7560
+ }
7561
+ keptEntries.push(entryLines);
7562
+ }
7563
+ if (removed === 0) {
7564
+ return { updated_raw: rawText, removed_count: 0 };
7565
+ }
7566
+ const keyIndent = (section.lines[section.key_index].match(/^[ \t]*/) || [""])[0];
7567
+ const replacement = keptEntries.length === 0 ? [`${keyIndent}m_AddedComponents: []`] : [`${keyIndent}m_AddedComponents:`, ...keptEntries.flat()];
7568
+ const updatedLines = [
7569
+ ...section.lines.slice(0, section.key_index),
7570
+ ...replacement,
7571
+ ...section.lines.slice(section.section_end)
7572
+ ];
7573
+ return { updated_raw: updatedLines.join(`
7574
+ `), removed_count: removed };
7575
+ }
6752
7576
  function deletePrefabInstance(options) {
6753
7577
  const { file_path, prefab_instance } = options;
6754
7578
  const pathError = validate_file_path(file_path, "write");
@@ -6770,44 +7594,31 @@ function deletePrefabInstance(options) {
6770
7594
  }
6771
7595
  const piId = piBlock.file_id;
6772
7596
  const allToRemove = new Set([piId]);
6773
- const piRefPattern = new RegExp(`m_PrefabInstance:\\s*\\{fileID:\\s*${piId}\\}`);
7597
+ const piRefPattern = new RegExp(`m_PrefabInstance:[ \\t]*\\{fileID:[ \\t]*${piId}\\}`);
6774
7598
  for (const block of doc.blocks) {
6775
7599
  if (block.is_stripped && piRefPattern.test(block.raw)) {
6776
7600
  allToRemove.add(block.file_id);
6777
7601
  }
6778
7602
  }
6779
- const addedGOPattern = /m_AddedGameObjects:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
6780
- const addedGOMatch = piBlock.raw.match(addedGOPattern);
6781
- if (addedGOMatch) {
6782
- const addedGOSection = addedGOMatch[1];
6783
- const goMatches = addedGOSection.matchAll(/fileID:\s*(\d+)/g);
6784
- for (const match of goMatches) {
6785
- const goId = match[1];
6786
- allToRemove.add(goId);
6787
- const goBlock = doc.find_by_file_id(goId);
6788
- if (goBlock) {
6789
- const compMatch = goBlock.raw.match(/component:\s*\{fileID:\s*(\d+)\}/);
6790
- if (compMatch) {
6791
- const transformId = compMatch[1];
6792
- allToRemove.add(transformId);
6793
- const descendants = doc.collect_hierarchy(transformId);
6794
- for (const id of descendants) {
6795
- allToRemove.add(id);
6796
- }
7603
+ for (const goId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedGameObjects")) {
7604
+ allToRemove.add(goId);
7605
+ const goBlock = doc.find_by_file_id(goId);
7606
+ if (goBlock) {
7607
+ const compMatch = goBlock.raw.match(/component:[ \t]*\{fileID:[ \t]*(\d+)\}/);
7608
+ if (compMatch) {
7609
+ const transformId = compMatch[1];
7610
+ allToRemove.add(transformId);
7611
+ const descendants = doc.collect_hierarchy(transformId);
7612
+ for (const id of descendants) {
7613
+ allToRemove.add(id);
6797
7614
  }
6798
7615
  }
6799
7616
  }
6800
7617
  }
6801
- const addedCompPattern = /m_AddedComponents:\s*\n((?:\s*- \{fileID: \d+\}\n)*)/m;
6802
- const addedCompMatch = piBlock.raw.match(addedCompPattern);
6803
- if (addedCompMatch) {
6804
- const addedCompSection = addedCompMatch[1];
6805
- const compMatches = addedCompSection.matchAll(/fileID:\s*(\d+)/g);
6806
- for (const match of compMatches) {
6807
- allToRemove.add(match[1]);
6808
- }
7618
+ for (const componentId of collectAddedObjectFileIdsFromArray(piBlock.raw, "m_AddedComponents")) {
7619
+ allToRemove.add(componentId);
6809
7620
  }
6810
- const parentMatch = piBlock.raw.match(/m_TransformParent:\s*\{fileID:\s*(\d+)\}/);
7621
+ const parentMatch = piBlock.raw.match(/m_TransformParent:[ \t]*\{fileID:[ \t]*(\d+)\}/);
6811
7622
  const parentTransformId = parentMatch ? parentMatch[1] : "0";
6812
7623
  let strippedRootTransformId = null;
6813
7624
  for (const id of allToRemove) {