unity-mcp-server 1.0.0 → 1.1.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.
- package/README.md +22 -6
- package/dist/index.d.ts +1 -3
- package/dist/index.js +134 -58
- package/dist/readers.d.ts +90 -0
- package/dist/readers.js +502 -0
- package/package.json +1 -1
- package/server.json +3 -3
- package/src/index.ts +272 -56
- package/src/readers.ts +518 -0
package/src/readers.ts
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unity project filesystem readers. No Unity Editor required.
|
|
3
|
+
* All paths are relative to project root.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { readFileSync, existsSync, readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { join, extname } from "node:path";
|
|
8
|
+
|
|
9
|
+
const ASSETS = "Assets";
|
|
10
|
+
const PROJECT_SETTINGS = "ProjectSettings";
|
|
11
|
+
const PACKAGES = "Packages";
|
|
12
|
+
|
|
13
|
+
// --- Helpers ---
|
|
14
|
+
|
|
15
|
+
export function readFileSafe(root: string, ...path: string[]): string | null {
|
|
16
|
+
const p = join(root, ...path);
|
|
17
|
+
if (!existsSync(p)) return null;
|
|
18
|
+
try {
|
|
19
|
+
return readFileSync(p, "utf-8");
|
|
20
|
+
} catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function readJsonSafe<T>(root: string, ...path: string[]): T | null {
|
|
26
|
+
const s = readFileSafe(root, ...path);
|
|
27
|
+
if (!s) return null;
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(s) as T;
|
|
30
|
+
} catch {
|
|
31
|
+
return null;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Recursively list files under dir, relative to root. Optional extension filter (e.g. ".cs"). */
|
|
36
|
+
export function listFilesRecursive(
|
|
37
|
+
root: string,
|
|
38
|
+
dir: string,
|
|
39
|
+
opts: { ext?: string; excludeMeta?: boolean } = {}
|
|
40
|
+
): string[] {
|
|
41
|
+
const full = join(root, dir);
|
|
42
|
+
if (!existsSync(full) || !statSync(full).isDirectory()) return [];
|
|
43
|
+
const out: string[] = [];
|
|
44
|
+
const stack: string[] = [dir];
|
|
45
|
+
while (stack.length) {
|
|
46
|
+
const d = stack.pop()!;
|
|
47
|
+
const fullD = join(root, d);
|
|
48
|
+
let entries: string[];
|
|
49
|
+
try {
|
|
50
|
+
entries = readdirSync(fullD);
|
|
51
|
+
} catch {
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
for (const e of entries) {
|
|
55
|
+
const rel = join(d, e);
|
|
56
|
+
const fullPath = join(root, rel);
|
|
57
|
+
if (statSync(fullPath).isDirectory()) {
|
|
58
|
+
if (e !== "node_modules" && e !== ".git" && !e.startsWith(".")) stack.push(rel);
|
|
59
|
+
} else {
|
|
60
|
+
if (opts.excludeMeta && e.endsWith(".meta")) continue;
|
|
61
|
+
if (opts.ext && extname(e).toLowerCase() !== opts.ext) continue;
|
|
62
|
+
out.push(rel);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out.sort();
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Parse Unity YAML-like key-value (m_Key: value) from content. */
|
|
70
|
+
export function parseUnityKeyValue(content: string): Record<string, string> {
|
|
71
|
+
const out: Record<string, string> = {};
|
|
72
|
+
const re = /^(\w+):\s*(.*)$/gm;
|
|
73
|
+
let m: RegExpExecArray | null;
|
|
74
|
+
while ((m = re.exec(content)) !== null) {
|
|
75
|
+
const v = m[2].trim();
|
|
76
|
+
if (v && !v.startsWith("{") && !v.startsWith("[")) out[m[1]] = v;
|
|
77
|
+
}
|
|
78
|
+
return out;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Get GUID from a .meta file path (reads sibling .meta of asset). */
|
|
82
|
+
export function getGuidFromMeta(root: string, assetPath: string): string | null {
|
|
83
|
+
const metaPath = assetPath.endsWith(".meta") ? assetPath : assetPath + ".meta";
|
|
84
|
+
const s = readFileSafe(root, metaPath);
|
|
85
|
+
if (!s) return null;
|
|
86
|
+
const m = s.match(/^guid:\s*([a-f0-9]{32})/m);
|
|
87
|
+
return m ? m[1] : null;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Get asset path by GUID (scan Assets for .meta with this guid). */
|
|
91
|
+
export function getAssetPathByGuid(root: string, guid: string): string | null {
|
|
92
|
+
const assetsDir = join(root, ASSETS);
|
|
93
|
+
if (!existsSync(assetsDir)) return null;
|
|
94
|
+
const stack: string[] = [ASSETS];
|
|
95
|
+
while (stack.length) {
|
|
96
|
+
const d = stack.pop()!;
|
|
97
|
+
const fullD = join(root, d);
|
|
98
|
+
let entries: string[];
|
|
99
|
+
try {
|
|
100
|
+
entries = readdirSync(fullD);
|
|
101
|
+
} catch {
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
for (const e of entries) {
|
|
105
|
+
const rel = join(d, e);
|
|
106
|
+
const fullPath = join(root, rel);
|
|
107
|
+
if (statSync(fullPath).isDirectory()) {
|
|
108
|
+
if (!e.startsWith(".")) stack.push(rel);
|
|
109
|
+
} else if (e.endsWith(".meta")) {
|
|
110
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
111
|
+
const m = content.match(/^guid:\s*([a-f0-9]{32})/m);
|
|
112
|
+
if (m && m[1] === guid) return rel.replace(/\.meta$/, "");
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/** Find all Asset/Scene/Prefab files that reference this GUID (fileRef or guid:). */
|
|
120
|
+
export function findReferencesToGuid(root: string, guid: string): string[] {
|
|
121
|
+
const found: string[] = [];
|
|
122
|
+
const exts = [".unity", ".prefab", ".asset", ".mat", ".controller", ".anim", ".mixer", ".overrideController"];
|
|
123
|
+
const assetsDir = join(root, ASSETS);
|
|
124
|
+
if (!existsSync(assetsDir)) return found;
|
|
125
|
+
const stack: string[] = [ASSETS];
|
|
126
|
+
while (stack.length) {
|
|
127
|
+
const d = stack.pop()!;
|
|
128
|
+
const fullD = join(root, d);
|
|
129
|
+
try {
|
|
130
|
+
for (const e of readdirSync(fullD)) {
|
|
131
|
+
const rel = join(d, e);
|
|
132
|
+
const fullPath = join(root, rel);
|
|
133
|
+
if (statSync(fullPath).isDirectory()) {
|
|
134
|
+
if (!e.startsWith(".")) stack.push(rel);
|
|
135
|
+
} else if (exts.some((x) => rel.endsWith(x))) {
|
|
136
|
+
const content = readFileSync(fullPath, "utf-8");
|
|
137
|
+
if (content.includes(guid)) found.push(rel);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
} catch {
|
|
141
|
+
/* skip */
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return found;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- 1. Project & package info ---
|
|
148
|
+
|
|
149
|
+
export function getUnityVersion(root: string): string {
|
|
150
|
+
const s = readFileSafe(root, PROJECT_SETTINGS, "ProjectVersion.txt");
|
|
151
|
+
if (!s) return "unknown";
|
|
152
|
+
const m = s.match(/m_EditorVersion:\s*(.+)/);
|
|
153
|
+
return m ? m[1].trim() : "unknown";
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export function getBuildScenes(root: string): { index: number; path: string; name: string }[] {
|
|
157
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "EditorBuildSettings.asset");
|
|
158
|
+
if (!content) return [];
|
|
159
|
+
const scenes: { index: number; path: string; name: string }[] = [];
|
|
160
|
+
let index = 0;
|
|
161
|
+
const pathRe = /path: (Assets\/[^\n]+\.unity)/g;
|
|
162
|
+
let m: RegExpExecArray | null;
|
|
163
|
+
while ((m = pathRe.exec(content)) !== null) {
|
|
164
|
+
const fullPath = m[1];
|
|
165
|
+
const name = fullPath.replace(/^.*\//, "").replace(/\.unity$/, "");
|
|
166
|
+
scenes.push({ index, path: fullPath, name });
|
|
167
|
+
index++;
|
|
168
|
+
}
|
|
169
|
+
return scenes;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface PackageInfo {
|
|
173
|
+
name: string;
|
|
174
|
+
version: string;
|
|
175
|
+
type?: string;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export function getPackages(root: string): { dependencies: PackageInfo[]; lock?: Record<string, string> } {
|
|
179
|
+
const manifest = readJsonSafe<{ dependencies?: Record<string, string> }>(root, PACKAGES, "manifest.json");
|
|
180
|
+
if (!manifest?.dependencies) return { dependencies: [] };
|
|
181
|
+
const dependencies: PackageInfo[] = Object.entries(manifest.dependencies).map(([name, version]) => ({
|
|
182
|
+
name,
|
|
183
|
+
version,
|
|
184
|
+
type: version.startsWith("file:") ? "local" : "registry",
|
|
185
|
+
}));
|
|
186
|
+
const lock = readJsonSafe<Record<string, string>>(root, PACKAGES, "packages-lock.json");
|
|
187
|
+
return { dependencies, lock: lock || undefined };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function getPlayerSettings(root: string): Record<string, string> {
|
|
191
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "ProjectSettings.asset");
|
|
192
|
+
if (!content) return {};
|
|
193
|
+
const kv = parseUnityKeyValue(content);
|
|
194
|
+
const out: Record<string, string> = {};
|
|
195
|
+
if (kv.productGUID) out.productGUID = kv.productGUID;
|
|
196
|
+
const ps = content.match(/PlayerSettings:/s) ? parseUnityKeyValue(content) : kv;
|
|
197
|
+
["m_ProductName", "m_CompanyName", "m_ApplicationIdentifier", "bundleVersion", "AndroidBundleVersionCode", "iOSBuildNumber"].forEach((k) => {
|
|
198
|
+
if (ps[k] !== undefined) out[k] = ps[k];
|
|
199
|
+
});
|
|
200
|
+
return Object.keys(out).length ? out : kv;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function getQualitySettings(root: string): Record<string, string>[] {
|
|
204
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "QualitySettings.asset");
|
|
205
|
+
if (!content) return [];
|
|
206
|
+
const levels: Record<string, string>[] = [];
|
|
207
|
+
const blockRe = /m_QualitySettings:\s*\n(\s+-\s+\n(?:\s+[\w:]+\s*\n)+)/g;
|
|
208
|
+
let m: RegExpExecArray | null;
|
|
209
|
+
while ((m = blockRe.exec(content)) !== null) {
|
|
210
|
+
levels.push(parseUnityKeyValue(m[1]));
|
|
211
|
+
}
|
|
212
|
+
if (levels.length === 0) levels.push(parseUnityKeyValue(content));
|
|
213
|
+
return levels;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export function getScriptingDefines(root: string): { global: string[]; perAssembly: Record<string, string[]> } {
|
|
217
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "ProjectSettings.asset");
|
|
218
|
+
const global: string[] = [];
|
|
219
|
+
if (content) {
|
|
220
|
+
const m = content.match(/m_ScriptingDefineSymbols:\s*\n\s+(\w+):\s*([^\n]+)/);
|
|
221
|
+
if (m) global.push(...m[2].split(",").map((s) => s.trim()).filter(Boolean));
|
|
222
|
+
}
|
|
223
|
+
const perAssembly: Record<string, string[]> = {};
|
|
224
|
+
const asmDefs = listFilesRecursive(root, ASSETS, { ext: ".asmdef" });
|
|
225
|
+
for (const rel of asmDefs) {
|
|
226
|
+
const j = readJsonSafe<{ name: string; defineConstraints?: string[]; versionDefines?: Array<{ name: string; define: string }> }>(root, rel);
|
|
227
|
+
if (j?.name) {
|
|
228
|
+
const defs: string[] = [];
|
|
229
|
+
j.defineConstraints?.forEach((c) => defs.push(`constraint:${c}`));
|
|
230
|
+
j.versionDefines?.forEach((v) => defs.push(`${v.name}=${v.define}`));
|
|
231
|
+
if (defs.length) perAssembly[j.name] = defs;
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
return { global, perAssembly };
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
// --- 2. Code & assemblies ---
|
|
238
|
+
|
|
239
|
+
export interface AsmDefInfo {
|
|
240
|
+
path: string;
|
|
241
|
+
name: string;
|
|
242
|
+
references: string[];
|
|
243
|
+
defineConstraints?: string[];
|
|
244
|
+
optionalUnityReferences?: string[];
|
|
245
|
+
includePlatforms?: string[];
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export function getAssemblyDefinitions(root: string): AsmDefInfo[] {
|
|
249
|
+
const files = listFilesRecursive(root, ASSETS, { ext: ".asmdef" });
|
|
250
|
+
const out: AsmDefInfo[] = [];
|
|
251
|
+
for (const rel of files) {
|
|
252
|
+
const j = readJsonSafe<{ name: string; references?: string[]; defineConstraints?: string[]; optionalUnityReferences?: string[]; includePlatforms?: string[] }>(root, rel);
|
|
253
|
+
if (j?.name)
|
|
254
|
+
out.push({
|
|
255
|
+
path: rel,
|
|
256
|
+
name: j.name,
|
|
257
|
+
references: j.references || [],
|
|
258
|
+
defineConstraints: j.defineConstraints,
|
|
259
|
+
optionalUnityReferences: j.optionalUnityReferences,
|
|
260
|
+
includePlatforms: j.includePlatforms,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return out;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export function listScripts(root: string, folderPrefix?: string): string[] {
|
|
267
|
+
let files = listFilesRecursive(root, ASSETS, { ext: ".cs", excludeMeta: true });
|
|
268
|
+
if (folderPrefix) {
|
|
269
|
+
const prefix = folderPrefix.startsWith("Assets/") ? folderPrefix : join(ASSETS, folderPrefix);
|
|
270
|
+
files = files.filter((f) => f.startsWith(prefix + "/") || f.startsWith(prefix + "\\"));
|
|
271
|
+
}
|
|
272
|
+
return files;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
/** Simple grep for type/namespace in .cs file content. */
|
|
276
|
+
export function findScriptsByContent(root: string, pattern: "MonoBehaviour" | "ScriptableObject" | string, namespaceFilter?: string): string[] {
|
|
277
|
+
const files = listScripts(root);
|
|
278
|
+
const re = new RegExp(pattern, "i");
|
|
279
|
+
const nsRe = namespaceFilter ? new RegExp(namespaceFilter.replace(/\*/g, ".*"), "i") : null;
|
|
280
|
+
const out: string[] = [];
|
|
281
|
+
for (const rel of files) {
|
|
282
|
+
const content = readFileSafe(root, rel);
|
|
283
|
+
if (!content || !re.test(content)) continue;
|
|
284
|
+
if (nsRe && !nsRe.test(content)) continue;
|
|
285
|
+
out.push(rel);
|
|
286
|
+
}
|
|
287
|
+
return out;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// --- 3. Scenes & build ---
|
|
291
|
+
|
|
292
|
+
export function getAllScenes(root: string): string[] {
|
|
293
|
+
return listFilesRecursive(root, ASSETS, { ext: ".unity" });
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/** Minimal scene summary: root GameObjects (top-level in hierarchy). */
|
|
297
|
+
export function getSceneSummary(root: string, scenePath: string): { rootObjects: string[]; componentCount: number } {
|
|
298
|
+
const content = readFileSafe(root, scenePath);
|
|
299
|
+
if (!content) return { rootObjects: [], componentCount: 0 };
|
|
300
|
+
const rootObjects: string[] = [];
|
|
301
|
+
const goRe = /GameObject:\s*\n\s+m_Name:\s*([^\n]+)/g;
|
|
302
|
+
let m: RegExpExecArray | null;
|
|
303
|
+
const seen = new Set<number>();
|
|
304
|
+
while ((m = goRe.exec(content)) !== null) {
|
|
305
|
+
const name = m[1].trim();
|
|
306
|
+
if (!seen.has(m.index)) rootObjects.push(name);
|
|
307
|
+
seen.add(m.index);
|
|
308
|
+
}
|
|
309
|
+
const compRe = /MonoBehaviour:|\d+:\s*\d+/g;
|
|
310
|
+
const compCount = (content.match(compRe) || []).length;
|
|
311
|
+
return { rootObjects: [...new Set(rootObjects)].slice(0, 100), componentCount: compCount };
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// --- 4. Prefabs ---
|
|
315
|
+
|
|
316
|
+
export function getPrefabs(root: string, pathPrefix?: string): string[] {
|
|
317
|
+
let files = listFilesRecursive(root, ASSETS, { ext: ".prefab" });
|
|
318
|
+
if (pathPrefix) {
|
|
319
|
+
const prefix = pathPrefix.startsWith("Assets/") ? pathPrefix : join(ASSETS, pathPrefix);
|
|
320
|
+
files = files.filter((f) => f.startsWith(prefix + "/") || f.startsWith(prefix + "\\"));
|
|
321
|
+
}
|
|
322
|
+
return files;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// --- 5. Assets & references ---
|
|
326
|
+
|
|
327
|
+
export function getAssetFolderTree(root: string, maxDepth: number = 4): Record<string, string[]> {
|
|
328
|
+
const assetsDir = join(root, ASSETS);
|
|
329
|
+
if (!existsSync(assetsDir)) return {};
|
|
330
|
+
const result: Record<string, string[]> = {};
|
|
331
|
+
function walk(dir: string, depth: number): string[] {
|
|
332
|
+
if (depth > maxDepth) return [];
|
|
333
|
+
const full = join(root, dir);
|
|
334
|
+
let entries: string[];
|
|
335
|
+
try {
|
|
336
|
+
entries = readdirSync(full);
|
|
337
|
+
} catch {
|
|
338
|
+
return [];
|
|
339
|
+
}
|
|
340
|
+
const children: string[] = [];
|
|
341
|
+
for (const e of entries) {
|
|
342
|
+
if (e.startsWith(".")) continue;
|
|
343
|
+
const rel = join(dir, e);
|
|
344
|
+
const fullPath = join(root, rel);
|
|
345
|
+
if (statSync(fullPath).isDirectory()) {
|
|
346
|
+
children.push(rel + "/");
|
|
347
|
+
if (depth < maxDepth) walk(rel, depth + 1);
|
|
348
|
+
} else {
|
|
349
|
+
children.push(e);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
result[dir] = children.slice(0, 200);
|
|
353
|
+
return children;
|
|
354
|
+
}
|
|
355
|
+
walk(ASSETS, 0);
|
|
356
|
+
return result;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
export function listAssetsByExtension(root: string, ext: string, folder?: string): string[] {
|
|
360
|
+
const dir = folder ? join(ASSETS, folder) : ASSETS;
|
|
361
|
+
return listFilesRecursive(root, dir, { ext: ext.toLowerCase() });
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// --- 6. Materials & shaders ---
|
|
365
|
+
|
|
366
|
+
export function getMaterials(root: string, folder?: string): { path: string; shader?: string }[] {
|
|
367
|
+
let files = listFilesRecursive(root, ASSETS, { ext: ".mat" });
|
|
368
|
+
if (folder) {
|
|
369
|
+
const prefix = folder.startsWith("Assets/") ? folder : join(ASSETS, folder);
|
|
370
|
+
files = files.filter((f) => f.startsWith(prefix + "/") || f.startsWith(prefix + "\\"));
|
|
371
|
+
}
|
|
372
|
+
return files.map((path) => {
|
|
373
|
+
const content = readFileSafe(root, path);
|
|
374
|
+
const shader = content?.match(/m_Shader:\s*\{fileID:\s*\d+,\s*guid:\s*([a-f0-9]+)/);
|
|
375
|
+
return { path, shader: shader ? shader[1] : undefined };
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
export function getShaders(root: string): string[] {
|
|
380
|
+
const inAssets = listFilesRecursive(root, ASSETS, { ext: ".shader" });
|
|
381
|
+
const inPackages = listFilesRecursive(root, PACKAGES, { ext: ".shader" });
|
|
382
|
+
return [...inAssets, ...inPackages.map((p) => p.startsWith("Packages/") ? p : `Packages/${p}`)];
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
// --- 7. Animation ---
|
|
386
|
+
|
|
387
|
+
export function getAnimatorControllers(root: string): string[] {
|
|
388
|
+
return listFilesRecursive(root, ASSETS, { ext: ".controller" });
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function getAnimationClips(root: string): string[] {
|
|
392
|
+
return listFilesRecursive(root, ASSETS, { ext: ".anim" });
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/** High-level state names from .controller (m_StateMachine -> states). */
|
|
396
|
+
export function getAnimatorStates(root: string, controllerPath: string): string[] {
|
|
397
|
+
const content = readFileSafe(root, controllerPath);
|
|
398
|
+
if (!content) return [];
|
|
399
|
+
const names: string[] = [];
|
|
400
|
+
const re = /m_Name:\s*([^\n]+)/g;
|
|
401
|
+
let m: RegExpExecArray | null;
|
|
402
|
+
while ((m = re.exec(content)) !== null) names.push(m[1].trim());
|
|
403
|
+
return [...new Set(names)].filter((n) => n && n !== "Base Layer" && !/^\d+$/.test(n));
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
// --- 8. Audio ---
|
|
407
|
+
|
|
408
|
+
export function getAudioClips(root: string): string[] {
|
|
409
|
+
const exts = [".wav", ".mp3", ".ogg", ".aiff"];
|
|
410
|
+
const out: string[] = [];
|
|
411
|
+
for (const ext of exts) out.push(...listFilesRecursive(root, ASSETS, { ext }));
|
|
412
|
+
return out.sort();
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function getAudioMixers(root: string): string[] {
|
|
416
|
+
return listFilesRecursive(root, ASSETS, { ext: ".mixer" });
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// --- 9. Addressables ---
|
|
420
|
+
|
|
421
|
+
export function getAddressablesInfo(root: string): { groups: string[]; configPath: string | null } {
|
|
422
|
+
const path = join(ASSETS, "AddressableAssetsData");
|
|
423
|
+
const full = join(root, path);
|
|
424
|
+
if (!existsSync(full)) return { groups: [], configPath: null };
|
|
425
|
+
let entries: string[];
|
|
426
|
+
try {
|
|
427
|
+
entries = readdirSync(full);
|
|
428
|
+
} catch {
|
|
429
|
+
return { groups: [], configPath: null };
|
|
430
|
+
}
|
|
431
|
+
const groups: string[] = [];
|
|
432
|
+
let configPath: string | null = null;
|
|
433
|
+
for (const e of entries) {
|
|
434
|
+
if (e.endsWith(".asset")) groups.push(e.replace(/\.asset$/, ""));
|
|
435
|
+
if (e === "AddressableAssetSettings.asset") configPath = join(path, e);
|
|
436
|
+
}
|
|
437
|
+
return { groups, configPath };
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// --- 10. Localization ---
|
|
441
|
+
|
|
442
|
+
export function getLocalizationTables(root: string): string[] {
|
|
443
|
+
const tables: string[] = [];
|
|
444
|
+
const locDir = join(ASSETS, "Localization");
|
|
445
|
+
const full = join(root, locDir);
|
|
446
|
+
if (!existsSync(full)) return [];
|
|
447
|
+
try {
|
|
448
|
+
for (const e of readdirSync(full)) {
|
|
449
|
+
if (e.endsWith(".asset") || e.endsWith(".csv") || e.endsWith(".json")) tables.push(join(locDir, e));
|
|
450
|
+
}
|
|
451
|
+
} catch {
|
|
452
|
+
/* */
|
|
453
|
+
}
|
|
454
|
+
return tables;
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// --- 11. Input ---
|
|
458
|
+
|
|
459
|
+
export function getInputAxes(root: string): { name: string; descriptiveName?: string; positiveButton?: string }[] {
|
|
460
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "InputManager.asset");
|
|
461
|
+
if (!content) return [];
|
|
462
|
+
const axes: { name: string; descriptiveName?: string; positiveButton?: string }[] = [];
|
|
463
|
+
const blockRe = /-\s+serializedVersion:\s*\d+\s+m_Name:\s*([^\n]+)\s+m_DescriptiveName:\s*([^\n]*)\s+m_PositiveButton:\s*([^\n]*)/g;
|
|
464
|
+
let m: RegExpExecArray | null;
|
|
465
|
+
while ((m = blockRe.exec(content)) !== null) axes.push({ name: m[1].trim(), descriptiveName: m[2].trim() || undefined, positiveButton: m[3].trim() || undefined });
|
|
466
|
+
if (axes.length === 0) {
|
|
467
|
+
const kv = parseUnityKeyValue(content);
|
|
468
|
+
if (kv.m_Name) axes.push({ name: kv.m_Name });
|
|
469
|
+
}
|
|
470
|
+
return axes;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
// --- 12. Tags & layers ---
|
|
474
|
+
|
|
475
|
+
export function getTagsAndLayers(root: string): { tags: string[]; layers: string[] } {
|
|
476
|
+
const content = readFileSafe(root, PROJECT_SETTINGS, "TagManager.asset");
|
|
477
|
+
if (!content) return { tags: [], layers: [] };
|
|
478
|
+
const tags: string[] = [];
|
|
479
|
+
const tagBlock = content.match(/m_Tags:\s*\n([\s\S]*?)(?=\n\w|\n---|$)/);
|
|
480
|
+
if (tagBlock) tagBlock[1].replace(/-\s+([^\n]+)/g, (_, name) => { tags.push(name.trim()); return ""; });
|
|
481
|
+
const layers: string[] = [];
|
|
482
|
+
for (let i = 0; i < 32; i++) {
|
|
483
|
+
const m = content.match(new RegExp(`m_Layer${i}:\\s*([^\\s\n]+)`));
|
|
484
|
+
if (m?.[1]) layers.push(`${i}: ${m[1]}`);
|
|
485
|
+
}
|
|
486
|
+
return { tags, layers };
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// --- 13. Testing ---
|
|
490
|
+
|
|
491
|
+
export function getTestAssemblies(root: string): AsmDefInfo[] {
|
|
492
|
+
const all = getAssemblyDefinitions(root);
|
|
493
|
+
return all.filter((a) => a.path.toLowerCase().includes("test") || a.path.toLowerCase().includes("editor") && a.path.toLowerCase().includes("test"));
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
// --- 14. Docs & conventions ---
|
|
497
|
+
|
|
498
|
+
const DOC_FILES = ["README.md", "CONTRIBUTING.md", ".cursorrules", "CODING_STANDARDS.md", "STYLE.md"];
|
|
499
|
+
|
|
500
|
+
export function getRepoDocs(root: string): Record<string, string> {
|
|
501
|
+
const out: Record<string, string> = {};
|
|
502
|
+
for (const name of DOC_FILES) {
|
|
503
|
+
const s = readFileSafe(root, name);
|
|
504
|
+
if (s) out[name] = s;
|
|
505
|
+
}
|
|
506
|
+
return out;
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
// --- 15. CI / versioning ---
|
|
510
|
+
|
|
511
|
+
export function getProjectVersion(root: string): string {
|
|
512
|
+
const ps = getPlayerSettings(root);
|
|
513
|
+
return ps.bundleVersion || ps.m_ApplicationVersion || "unknown";
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
export function getChangelog(root: string): string | null {
|
|
517
|
+
return readFileSafe(root, "CHANGELOG.md") || readFileSafe(root, "CHANGELOG") || readFileSafe(root, "changelog.md");
|
|
518
|
+
}
|