sindicate 0.13.0 → 0.16.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.
@@ -0,0 +1,145 @@
1
+ // EXPORT KIWI ROAD MATERIALS — drop this file into Kiwi's Assets/Editor/ folder, let
2
+ // Unity compile, then run Tools ▸ Sindicate ▸ Export Road Materials. (~10 seconds.)
3
+ //
4
+ // It does NOT export models (Sindicate already recovered the geometry). It walks every
5
+ // prefab and records each part's UNITY-RESOLVED world transform (the truth — no more
6
+ // hierarchy archaeology) alongside its material stack. It also walks every
7
+ // road prefab (AIGenerateMap/Prefabs/Roads, JunctionPrefabs, RoadPrefabs) and writes ONE
8
+ // truth file mapping every renderer's mesh to its real material stack:
9
+ // Assets/SindicateExport/road-materials.json
10
+ // { entries: [{ prefab, object, mesh, meshGuid,
11
+ // materials: [{ name, shader, mainTex, color, tiling, offset }] }] }
12
+ // …and copies every referenced texture into Assets/SindicateExport/textures/.
13
+ // Copy the SindicateExport folder to Nick's Sites machine and Sindicate's assembler
14
+ // applies exact materials to the already-recovered assemblies.
15
+ using UnityEngine;
16
+ using UnityEditor;
17
+ using System.Collections.Generic;
18
+ using System.IO;
19
+ using System.Text;
20
+
21
+ public static class ExportKiwiRoadMaterials
22
+ {
23
+ static readonly string[] ROOTS = {
24
+ "Assets/AIGenerateMap/Prefabs/Roads",
25
+ "Assets/AIGenerateMap/JunctionPrefabs",
26
+ "Assets/AIGenerateMap/RoadPrefabs",
27
+ };
28
+
29
+ [MenuItem("Tools/Sindicate/Export Road Materials")]
30
+ public static void Export()
31
+ {
32
+ var outDir = "Assets/SindicateExport";
33
+ var texDir = Path.Combine(outDir, "textures");
34
+ Directory.CreateDirectory(texDir);
35
+
36
+ var sb = new StringBuilder();
37
+ sb.Append("{\"entries\":[");
38
+ bool first = true;
39
+ var texturesCopied = new HashSet<string>();
40
+ int prefabCount = 0;
41
+
42
+ foreach (var root in ROOTS)
43
+ {
44
+ foreach (var guid in AssetDatabase.FindAssets("t:Prefab", new[] { root }))
45
+ {
46
+ var path = AssetDatabase.GUIDToAssetPath(guid);
47
+ var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
48
+ if (prefab == null) continue;
49
+ prefabCount++;
50
+
51
+ foreach (var r in prefab.GetComponentsInChildren<MeshRenderer>(true))
52
+ {
53
+ var mf = r.GetComponent<MeshFilter>();
54
+ if (mf == null || mf.sharedMesh == null) continue;
55
+ var meshPath = AssetDatabase.GetAssetPath(mf.sharedMesh);
56
+ var meshGuid = AssetDatabase.AssetPathToGUID(meshPath);
57
+
58
+ if (!first) sb.Append(",");
59
+ first = false;
60
+ sb.Append("{\"prefab\":").Append(J(Path.GetFileNameWithoutExtension(path)));
61
+ sb.Append(",\"object\":").Append(J(r.gameObject.name));
62
+ sb.Append(",\"mesh\":").Append(J(Path.GetFileName(meshPath)));
63
+ sb.Append(",\"meshGuid\":").Append(J(meshGuid));
64
+ sb.Append(",\"meshName\":").Append(J(mf.sharedMesh.name));
65
+ sb.Append(",\"active\":").Append(r.gameObject.activeInHierarchy && r.enabled ? "true" : "false");
66
+ var t = r.transform;
67
+ var wp = t.position; var wq = t.rotation; var ws = t.lossyScale;
68
+ sb.Append(",\"pos\":[").Append(wp.x).Append(",").Append(wp.y).Append(",").Append(wp.z).Append("]");
69
+ sb.Append(",\"quat\":[").Append(wq.x).Append(",").Append(wq.y).Append(",").Append(wq.z).Append(",").Append(wq.w).Append("]");
70
+ sb.Append(",\"scale\":[").Append(ws.x).Append(",").Append(ws.y).Append(",").Append(ws.z).Append("]");
71
+ sb.Append(",\"materials\":[");
72
+ for (int i = 0; i < r.sharedMaterials.Length; i++)
73
+ {
74
+ var m = r.sharedMaterials[i];
75
+ if (i > 0) sb.Append(",");
76
+ if (m == null) { sb.Append("null"); continue; }
77
+ var tex = m.HasProperty("_BaseMap") && m.GetTexture("_BaseMap") != null ? m.GetTexture("_BaseMap")
78
+ : m.HasProperty("_MainTex") ? m.GetTexture("_MainTex") : null;
79
+ string texName = null;
80
+ if (tex != null)
81
+ {
82
+ var tp = AssetDatabase.GetAssetPath(tex);
83
+ texName = Path.GetFileName(tp);
84
+ if (!string.IsNullOrEmpty(tp) && texturesCopied.Add(texName))
85
+ File.Copy(tp, Path.Combine(texDir, texName), true);
86
+ }
87
+ var col = m.HasProperty("_BaseColor") ? m.GetColor("_BaseColor")
88
+ : m.HasProperty("_Color") ? m.GetColor("_Color") : Color.white;
89
+ var st = m.HasProperty("_BaseMap") ? m.GetTextureScale("_BaseMap")
90
+ : m.HasProperty("_MainTex") ? m.GetTextureScale("_MainTex") : Vector2.one;
91
+ var so = m.HasProperty("_BaseMap") ? m.GetTextureOffset("_BaseMap")
92
+ : m.HasProperty("_MainTex") ? m.GetTextureOffset("_MainTex") : Vector2.zero;
93
+ sb.Append("{\"name\":").Append(J(m.name));
94
+ sb.Append(",\"shader\":").Append(J(m.shader != null ? m.shader.name : ""));
95
+ sb.Append(",\"mainTex\":").Append(texName == null ? "null" : J(texName));
96
+ sb.Append(",\"color\":[").Append(col.r).Append(",").Append(col.g).Append(",").Append(col.b).Append(",").Append(col.a).Append("]");
97
+ sb.Append(",\"tiling\":[").Append(st.x).Append(",").Append(st.y).Append("]");
98
+ sb.Append(",\"offset\":[").Append(so.x).Append(",").Append(so.y).Append("]}");
99
+ }
100
+ sb.Append("]}");
101
+ }
102
+ }
103
+ }
104
+ sb.Append("],\"nodes\":[");
105
+
106
+ // MARKERS — every GameObject WITHOUT a MeshRenderer: connectors, sockets, spawn
107
+ // points, group empties. These are what tells Sindicate where a junction's arms
108
+ // are and which way they face, so roads can auto-connect junction to junction.
109
+ // (Renderers are already covered above; this keeps the file small.)
110
+ bool firstNode = true;
111
+ foreach (var root in ROOTS)
112
+ {
113
+ foreach (var guid in AssetDatabase.FindAssets("t:Prefab", new[] { root }))
114
+ {
115
+ var path = AssetDatabase.GUIDToAssetPath(guid);
116
+ var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
117
+ if (prefab == null) continue;
118
+ foreach (var t in prefab.GetComponentsInChildren<Transform>(true))
119
+ {
120
+ if (t.GetComponent<MeshRenderer>() != null) continue;
121
+ if (!firstNode) sb.Append(",");
122
+ firstNode = false;
123
+ // parent path so hierarchy meaning survives (e.g. Sockets/Connector_A)
124
+ var chain = t.name;
125
+ for (var p = t.parent; p != null && p != prefab.transform; p = p.parent) chain = p.name + "/" + chain;
126
+ sb.Append("{\"prefab\":").Append(J(Path.GetFileNameWithoutExtension(path)));
127
+ sb.Append(",\"path\":").Append(J(chain));
128
+ sb.Append(",\"name\":").Append(J(t.name));
129
+ sb.Append(",\"active\":").Append(t.gameObject.activeInHierarchy ? "true" : "false");
130
+ var np = t.position; var nq = t.rotation; var ns = t.lossyScale;
131
+ sb.Append(",\"pos\":[").Append(np.x).Append(",").Append(np.y).Append(",").Append(np.z).Append("]");
132
+ sb.Append(",\"quat\":[").Append(nq.x).Append(",").Append(nq.y).Append(",").Append(nq.z).Append(",").Append(nq.w).Append("]");
133
+ sb.Append(",\"scale\":[").Append(ns.x).Append(",").Append(ns.y).Append(",").Append(ns.z).Append("]}");
134
+ }
135
+ }
136
+ }
137
+ sb.Append("]}");
138
+ File.WriteAllText(Path.Combine(outDir, "road-materials.json"), sb.ToString());
139
+ AssetDatabase.Refresh();
140
+ EditorUtility.DisplayDialog("Sindicate export",
141
+ $"Done: {prefabCount} prefabs walked, {texturesCopied.Count} textures copied.\n\nCopy Assets/SindicateExport/ to Sites and tell Claude.", "OK");
142
+ }
143
+
144
+ static string J(string s) => "\"" + (s ?? "").Replace("\\", "\\\\").Replace("\"", "\\\"") + "\"";
145
+ }
@@ -0,0 +1,94 @@
1
+ // EXPORT ROAD REFS — Unity renders every road prefab to a PNG (the GROUND TRUTH in
2
+ // pixels). Drop next to ExportKiwiRoadMaterials.cs in Assets/Editor/, run
3
+ // Tools ▸ Sindicate ▸ Export Road Reference Images
4
+ // Writes Assets/SindicateExport/refs/<prefab>.png (512px, iso view). Sindicate's compare
5
+ // sheet puts these beside its own renders so every mismatch is visible at a glance.
6
+ using UnityEngine;
7
+ using UnityEditor;
8
+ using System.IO;
9
+
10
+ public static class ExportKiwiRoadRefs
11
+ {
12
+ static readonly string[] ROOTS = {
13
+ "Assets/AIGenerateMap/Prefabs/Roads",
14
+ "Assets/AIGenerateMap/JunctionPrefabs",
15
+ "Assets/AIGenerateMap/RoadPrefabs",
16
+ };
17
+
18
+ [MenuItem("Tools/Sindicate/Export Road Reference Images")]
19
+ public static void Export()
20
+ {
21
+ var outDir = "Assets/SindicateExport/refs";
22
+ Directory.CreateDirectory(outDir);
23
+
24
+ var camGO = new GameObject("__sindicateRefCam");
25
+ var cam = camGO.AddComponent<Camera>();
26
+ cam.clearFlags = CameraClearFlags.SolidColor;
27
+ cam.backgroundColor = new Color(0.09f, 0.09f, 0.14f);
28
+ cam.fieldOfView = 40f;
29
+ cam.farClipPlane = 5000f;
30
+ var lightGO = new GameObject("__sindicateRefLight");
31
+ var light = lightGO.AddComponent<Light>();
32
+ light.type = LightType.Directional;
33
+ light.intensity = 1.4f;
34
+ lightGO.transform.rotation = Quaternion.Euler(50, -30, 0);
35
+
36
+ var rt = new RenderTexture(512, 512, 24);
37
+ var tex = new Texture2D(512, 512, TextureFormat.RGB24, false);
38
+ int done = 0;
39
+
40
+ try
41
+ {
42
+ foreach (var root in ROOTS)
43
+ {
44
+ foreach (var guid in AssetDatabase.FindAssets("t:Prefab", new[] { root }))
45
+ {
46
+ var path = AssetDatabase.GUIDToAssetPath(guid);
47
+ var prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
48
+ if (prefab == null) continue;
49
+ // name refs the same way the materials export names runs: variant dirs
50
+ // (Motorway/DualCarriageway) prefixed to disambiguate duplicates
51
+ var name = Path.GetFileNameWithoutExtension(path);
52
+ if (path.Contains("/Motorway/")) name = "Motorway_" + name;
53
+ else if (path.Contains("/DualCarriageway/")) name = "DualCarriageway_" + name;
54
+
55
+ var inst = (GameObject)Object.Instantiate(prefab, new Vector3(0, -5000, 0), Quaternion.identity);
56
+ try
57
+ {
58
+ var bounds = new Bounds(inst.transform.position, Vector3.one);
59
+ bool first = true;
60
+ foreach (var r in inst.GetComponentsInChildren<Renderer>())
61
+ {
62
+ if (first) { bounds = r.bounds; first = false; }
63
+ else bounds.Encapsulate(r.bounds);
64
+ }
65
+ var size = Mathf.Max(bounds.size.x, bounds.size.z, bounds.size.y, 2f);
66
+ var dist = size * 1.35f;
67
+ cam.transform.position = bounds.center + new Vector3(0.7f, 0.8f, 1f).normalized * dist;
68
+ cam.transform.LookAt(bounds.center);
69
+
70
+ cam.targetTexture = rt;
71
+ cam.Render();
72
+ RenderTexture.active = rt;
73
+ tex.ReadPixels(new Rect(0, 0, 512, 512), 0, 0);
74
+ tex.Apply();
75
+ File.WriteAllBytes(Path.Combine(outDir, name + ".png"), tex.EncodeToPNG());
76
+ done++;
77
+ }
78
+ finally { Object.DestroyImmediate(inst); }
79
+ }
80
+ }
81
+ }
82
+ finally
83
+ {
84
+ cam.targetTexture = null;
85
+ RenderTexture.active = null;
86
+ Object.DestroyImmediate(rt);
87
+ Object.DestroyImmediate(tex);
88
+ Object.DestroyImmediate(camGO);
89
+ Object.DestroyImmediate(lightGO);
90
+ AssetDatabase.Refresh();
91
+ }
92
+ EditorUtility.DisplayDialog("Sindicate refs", $"Rendered {done} reference images.\n\nTell Claude.", "OK");
93
+ }
94
+ }
@@ -0,0 +1,36 @@
1
+ #!/usr/bin/env node
2
+ // BUILD THE KIWIROADS PACK — one command, correct order. The stages are order-dependent
3
+ // (assemblies must exist before patches merge; FBX must be referenced before they're baked),
4
+ // so running them by hand invites half-built packs.
5
+ //
6
+ // node tools/buildKiwiRoadsPack.mjs [--export <SindicateExport>] [--kiwi <KiwiAssets>] [--pack <packDir>]
7
+ //
8
+ // 1. truthToAssemblies Unity truth export → 538 assemblies (+ merges patches/, fbxScale.json)
9
+ // 2. rebuildSimpleRoundabouts regenerate the meshes Kiwi lost upstream → patches/
10
+ // 3. truthToAssemblies again so the fresh patches land in the assemblies
11
+ // 4. fbxToGlb bake Synty FBX props (with their Unity import scale) → GLB
12
+ import { spawnSync } from 'node:child_process';
13
+ import path from 'node:path';
14
+ import { fileURLToPath } from 'node:url';
15
+
16
+ const here = path.dirname(fileURLToPath(import.meta.url));
17
+ const arg = (name, fallback) => {
18
+ const i = process.argv.indexOf(`--${name}`);
19
+ return i > 0 ? process.argv[i + 1] : fallback;
20
+ };
21
+ const exportDir = arg('export', '/Users/nick/Desktop/Kiwi/Assets/SindicateExport');
22
+ const kiwiAssets = arg('kiwi', '/Users/nick/Desktop/Kiwi/Assets');
23
+ const packDir = arg('pack', '/Users/nick/Sites/TakeTheCity/public/assets/packs/KiwiRoads');
24
+
25
+ const run = (script, args, label) => {
26
+ console.log(`\n── ${label}`);
27
+ const r = spawnSync(process.execPath, [path.join(here, script), ...args], { stdio: 'inherit' });
28
+ if (r.status !== 0) { console.error(`FAILED at ${label}`); process.exit(r.status ?? 1); }
29
+ };
30
+
31
+ run('truthToAssemblies.mjs', [exportDir, kiwiAssets, packDir], 'assemblies from Unity truth');
32
+ run('rebuildSimpleRoundabouts.mjs', [packDir], 'reconstruct SimpleRoundabout meshes');
33
+ run('truthToAssemblies.mjs', [exportDir, kiwiAssets, packDir], 'assemblies again (merge fresh patches)');
34
+ run('fbxToGlb.mjs', [packDir], 'bake FBX props → GLB');
35
+ run('roadKitAnalyze.mjs', [packDir], 'measure sockets → roadkit.json');
36
+ console.log('\npack built.');
@@ -0,0 +1,103 @@
1
+ #!/usr/bin/env node
2
+ // THE DESIGNER's MCP SERVER — lets a Claude session drive the World Designer while the
3
+ // human watches the viewport. Zero-dep stdio JSON-RPC (MCP 2024-11-05): every tool call
4
+ // proxies to the game dev server's designer bus (/__designer-bus) or endpoints, and the
5
+ // open /designer.html page executes it live.
6
+ //
7
+ // Register once per game:
8
+ // claude mcp add dustwater-designer -- node /Users/nick/Sites/Sindicate/tools/designer-mcp.mjs --server http://localhost:5317
9
+ //
10
+ // The designer page must be open in a browser tab (it polls the bus).
11
+ const SERVER = (() => {
12
+ const i = process.argv.indexOf('--server');
13
+ return (i >= 0 && process.argv[i + 1]) || process.env.SINDICATE_SERVER || 'http://localhost:5317';
14
+ })();
15
+
16
+ async function bus(op, args = {}, timeoutMs = 20000) {
17
+ const { id } = await fetch(`${SERVER}/__designer-bus/cmd`, {
18
+ method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ op, args }),
19
+ }).then((r) => r.json());
20
+ const t0 = Date.now();
21
+ while (Date.now() - t0 < timeoutMs) {
22
+ const r = await fetch(`${SERVER}/__designer-bus/take?id=${id}`);
23
+ if (r.status === 200) {
24
+ const j = await r.json();
25
+ if (!j.ok) throw new Error(j.err ?? 'designer op failed');
26
+ return j.value;
27
+ }
28
+ await new Promise((res) => setTimeout(res, 300));
29
+ }
30
+ throw new Error(`designer page did not answer in ${timeoutMs / 1000}s — is /designer.html open in a browser tab on ${SERVER}?`);
31
+ }
32
+
33
+ const num = (d) => ({ type: 'number', description: d });
34
+ const str = (d) => ({ type: 'string', description: d });
35
+ const TOOLS = [
36
+ { name: 'designer_status', description: 'Current designer state: mode, prefab/road counts, selection, layout name.', inputSchema: { type: 'object', properties: {} },
37
+ run: () => bus('status') },
38
+ { name: 'designer_packs', description: 'List imported asset packs available to the designer.', inputSchema: { type: 'object', properties: {} },
39
+ run: async () => (await fetch(`${SERVER}/__pack/list`).then((r) => r.json())).imported },
40
+ { name: 'designer_models', description: 'List a pack’s placeable models plus its default texture atlas URL.', inputSchema: { type: 'object', properties: { pack: str('imported pack name') }, required: ['pack'] },
41
+ run: ({ pack }) => fetch(`${SERVER}/__designer/models?pack=${encodeURIComponent(pack)}`).then((r) => r.json()) },
42
+ { name: 'designer_palette', description: 'Search the loaded palette (pack/model labels) by substring.', inputSchema: { type: 'object', properties: { filter: str('substring filter, empty for first 200') } },
43
+ run: ({ filter = '' }) => bus('palette', filter) },
44
+ { name: 'designer_place', description: 'Place a prefab at world x,z (y defaults to terrain height). Returns its index.', inputSchema: { type: 'object', properties: { file: str('model name without extension'), dir: str('URL dir, e.g. /assets/packs/<pack>/models'), x: num('world x'), z: num('world z'), y: num('optional y override'), ry: num('yaw radians'), tex: str('optional texture URL') }, required: ['file', 'dir', 'x', 'z'] },
45
+ run: (a) => bus('place', a) },
46
+ { name: 'designer_move', description: 'Move/rotate placed prefab #i (omitted fields keep their value; y re-snaps to terrain unless given).', inputSchema: { type: 'object', properties: { i: num('prefab index'), x: num(''), z: num(''), y: num(''), ry: num('yaw radians') }, required: ['i'] },
47
+ run: (a) => bus('move', a) },
48
+ { name: 'designer_remove', description: 'Delete placed prefab #i.', inputSchema: { type: 'object', properties: { i: num('prefab index') }, required: ['i'] },
49
+ run: (a) => bus('remove', a) },
50
+ { name: 'designer_road', description: 'Add a road polyline: pts = [{x,z}, ...] in world metres.', inputSchema: { type: 'object', properties: { pts: { type: 'array', items: { type: 'object', properties: { x: num(''), z: num('') }, required: ['x', 'z'] }, description: 'polyline points' } }, required: ['pts'] },
51
+ run: (a) => bus('road', a) },
52
+ { name: 'designer_camera', description: 'Move the viewport camera to x,y,z looking at tx,ty,tz.', inputSchema: { type: 'object', properties: { x: num(''), y: num(''), z: num(''), tx: num('look-at x'), ty: num('look-at y'), tz: num('look-at z') }, required: ['x', 'y', 'z'] },
53
+ run: (a) => bus('camera', a) },
54
+ { name: 'designer_save', description: 'Save the layout to designs/<name>.json in the game repo.', inputSchema: { type: 'object', properties: { name: str('design name') }, required: ['name'] },
55
+ run: (a) => bus('save', a) },
56
+ { name: 'designer_load', description: 'Load designs/<name>.json into the viewport (replaces current layout).', inputSchema: { type: 'object', properties: { name: str('design name') }, required: ['name'] },
57
+ run: (a) => bus('load', a, 120000) },
58
+ { name: 'designer_clear', description: 'Remove every placed prefab and road (does not touch saved files).', inputSchema: { type: 'object', properties: {} },
59
+ run: () => bus('clear') },
60
+ { name: 'designer_designs', description: 'List saved design names.', inputSchema: { type: 'object', properties: {} },
61
+ run: async () => (await fetch(`${SERVER}/__designer/list`).then((r) => r.json())).designs },
62
+ ];
63
+
64
+ // ---- stdio JSON-RPC plumbing (newline-delimited, per the MCP stdio transport) ----
65
+ const reply = (id, result) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, result }) + '\n');
66
+ const fail = (id, message) => process.stdout.write(JSON.stringify({ jsonrpc: '2.0', id, error: { code: -32603, message } }) + '\n');
67
+
68
+ let buf = '';
69
+ process.stdin.on('data', (chunk) => {
70
+ buf += chunk;
71
+ let nl;
72
+ while ((nl = buf.indexOf('\n')) >= 0) {
73
+ const line = buf.slice(0, nl).trim();
74
+ buf = buf.slice(nl + 1);
75
+ if (line) handle(line);
76
+ }
77
+ });
78
+
79
+ async function handle(line) {
80
+ let msg;
81
+ try { msg = JSON.parse(line); } catch { return; }
82
+ const { id, method, params } = msg;
83
+ if (method === 'initialize') {
84
+ return reply(id, {
85
+ protocolVersion: params?.protocolVersion ?? '2024-11-05',
86
+ capabilities: { tools: {} },
87
+ serverInfo: { name: 'sindicate-designer', version: '1.0.0' },
88
+ });
89
+ }
90
+ if (method === 'tools/list') return reply(id, { tools: TOOLS.map(({ run, ...t }) => t) });
91
+ if (method === 'tools/call') {
92
+ const tool = TOOLS.find((t) => t.name === params?.name);
93
+ if (!tool) return fail(id, `unknown tool ${params?.name}`);
94
+ try {
95
+ const value = await tool.run(params?.arguments ?? {});
96
+ return reply(id, { content: [{ type: 'text', text: JSON.stringify(value ?? { ok: true }) }] });
97
+ } catch (e) {
98
+ return reply(id, { content: [{ type: 'text', text: String(e?.message ?? e) }], isError: true });
99
+ }
100
+ }
101
+ if (method === 'ping') return reply(id, {});
102
+ if (id !== undefined) return fail(id, `unknown method ${method}`); // notifications are ignored
103
+ }