@needle-tools/engine 5.0.0-next.28ba01d → 5.0.0-next.3fe5866
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/SKILL.md +243 -24
- package/dist/{needle-engine.bundle-Clf2sl1k.min.js → needle-engine.bundle-CBQKykGj.min.js} +123 -123
- package/dist/{needle-engine.bundle-D72t1JZc.js → needle-engine.bundle-CFeyadSw.js} +4422 -4383
- package/dist/{needle-engine.bundle-yvkZGrEH.umd.cjs → needle-engine.bundle-CKg_ctfb.umd.cjs} +124 -124
- package/dist/needle-engine.d.ts +59 -23
- package/dist/needle-engine.js +82 -81
- package/dist/needle-engine.min.js +1 -1
- package/dist/needle-engine.umd.cjs +1 -1
- package/lib/engine/engine_input.js +4 -1
- package/lib/engine/engine_input.js.map +1 -1
- package/lib/engine/engine_networking_instantiate.d.ts +16 -0
- package/lib/engine/engine_networking_instantiate.js +32 -1
- package/lib/engine/engine_networking_instantiate.js.map +1 -1
- package/lib/engine-components-experimental/networking/PlayerSync.d.ts +19 -1
- package/lib/engine-components-experimental/networking/PlayerSync.js +33 -7
- package/lib/engine-components-experimental/networking/PlayerSync.js.map +1 -1
- package/package.json +5 -4
- package/plugins/vite/ai.d.ts +11 -10
- package/plugins/vite/ai.js +305 -31
- package/src/engine/engine_input.ts +2 -1
- package/src/engine/engine_networking_instantiate.ts +31 -0
- package/src/engine-components-experimental/networking/PlayerSync.ts +33 -6
package/plugins/vite/ai.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
2
|
-
import { dirname, join } from "path";
|
|
1
|
+
import { copyFileSync, existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, symlinkSync, writeFileSync } from "fs";
|
|
2
|
+
import { dirname, join, relative } from "path";
|
|
3
3
|
import { fileURLToPath } from "url";
|
|
4
4
|
import { needleLog } from "./logging.js";
|
|
5
5
|
|
|
@@ -8,20 +8,49 @@ const __dirname = dirname(__filename);
|
|
|
8
8
|
|
|
9
9
|
const pluginName = "needle-ai";
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Supported AI coding agents.
|
|
13
|
+
* Each entry defines how to detect the agent and how to write its skill file.
|
|
14
|
+
* `detect` is the directory that must exist in the project root.
|
|
15
|
+
* `write(cwd, canonicalDir, content)` installs the skill in the agent's native format.
|
|
16
|
+
*
|
|
17
|
+
* The `.agents/` entry is special: it is the canonical location where SKILL.md
|
|
18
|
+
* and downloaded reference files are written directly. All other agents symlink
|
|
19
|
+
* their skill directory to `.agents/skills/needle-engine/`.
|
|
20
|
+
*/
|
|
21
|
+
const agents = [
|
|
22
|
+
{
|
|
23
|
+
name: "Claude Code",
|
|
24
|
+
detect: ".claude",
|
|
25
|
+
write: (cwd, canonicalDir, content) => symlinkSkillDir(join(cwd, ".claude"), canonicalDir),
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
name: "GitHub Copilot",
|
|
29
|
+
detect: ".github",
|
|
30
|
+
write: (cwd, canonicalDir, content) => symlinkSkillDir(join(cwd, ".github"), canonicalDir),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
name: "Cursor",
|
|
34
|
+
detect: ".cursor",
|
|
35
|
+
write: (cwd, canonicalDir, content) => writeCursorRule(cwd, canonicalDir, content),
|
|
36
|
+
},
|
|
37
|
+
];
|
|
38
|
+
|
|
11
39
|
/**
|
|
12
40
|
* Needle Engine AI skill installer.
|
|
13
41
|
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
42
|
+
* Auto-detects AI coding agents by checking for their config directories
|
|
43
|
+
* in the project root, then writes the Needle Engine skill in each agent's
|
|
44
|
+
* native format.
|
|
45
|
+
*
|
|
46
|
+
* Supported agents: Claude Code, GitHub Copilot, Cursor, Codex / universal.
|
|
19
47
|
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
48
|
+
* `.agents/skills/needle-engine/` is always created as the canonical skill
|
|
49
|
+
* location. SKILL.md and downloaded reference files live there. Other agents
|
|
50
|
+
* symlink to it to avoid duplication.
|
|
51
|
+
*
|
|
52
|
+
* Remote reference files (API docs, templates, etc.) linked from SKILL.md are
|
|
53
|
+
* downloaded at install time and stored locally with relative paths.
|
|
25
54
|
*
|
|
26
55
|
* @param {"build" | "serve"} command
|
|
27
56
|
* @param {{} | undefined | null} config
|
|
@@ -32,34 +61,279 @@ export function needleAI(command, config, userSettings) {
|
|
|
32
61
|
return {
|
|
33
62
|
name: pluginName,
|
|
34
63
|
enforce: "pre",
|
|
35
|
-
buildStart() {
|
|
36
|
-
|
|
64
|
+
async buildStart() {
|
|
65
|
+
await installSkills();
|
|
37
66
|
},
|
|
38
|
-
configureServer() {
|
|
39
|
-
|
|
67
|
+
async configureServer() {
|
|
68
|
+
await installSkills();
|
|
40
69
|
},
|
|
41
70
|
};
|
|
42
71
|
}
|
|
43
72
|
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!existsSync(skillDir)) {
|
|
47
|
-
mkdirSync(skillDir, { recursive: true });
|
|
48
|
-
}
|
|
49
|
-
const skillPath = join(skillDir, "SKILL.md");
|
|
73
|
+
/** Read the SKILL.md template shipped with the engine. */
|
|
74
|
+
function getSkillContent() {
|
|
50
75
|
const templatePath = join(__dirname, "../../SKILL.md");
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
76
|
+
return readFileSync(templatePath, "utf8");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Extract the body of a SKILL.md file (everything after the YAML frontmatter).
|
|
81
|
+
* Returns the full content if no frontmatter is found.
|
|
82
|
+
*/
|
|
83
|
+
function stripFrontmatter(content) {
|
|
84
|
+
const match = content.match(/^---\r?\n[\s\S]*?\r?\n---\r?\n([\s\S]*)$/);
|
|
85
|
+
return match ? match[1].trimStart() : content;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Remote reference downloading
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Extract markdown links to raw.githubusercontent.com files from content.
|
|
94
|
+
* Returns an array of { fullMatch, linkText, url, subdir, filename, localRelPath }.
|
|
95
|
+
*/
|
|
96
|
+
function extractRemoteRefs(content) {
|
|
97
|
+
const pattern = /\[([^\]]*)\]\((https:\/\/raw\.githubusercontent\.com\/[^)]+)\)/g;
|
|
98
|
+
const refs = [];
|
|
99
|
+
let match;
|
|
100
|
+
while ((match = pattern.exec(content)) !== null) {
|
|
101
|
+
const url = match[2];
|
|
102
|
+
const urlPath = new URL(url).pathname;
|
|
103
|
+
const segments = urlPath.split("/").filter(Boolean);
|
|
104
|
+
const filename = segments[segments.length - 1];
|
|
105
|
+
const subdir = segments[segments.length - 2] || "";
|
|
106
|
+
refs.push({
|
|
107
|
+
fullMatch: match[0],
|
|
108
|
+
linkText: match[1],
|
|
109
|
+
url,
|
|
110
|
+
subdir,
|
|
111
|
+
filename,
|
|
112
|
+
localRelPath: `./${subdir}/${filename}`,
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
return refs;
|
|
54
116
|
}
|
|
55
117
|
|
|
56
|
-
|
|
118
|
+
/**
|
|
119
|
+
* Download a single remote file. Returns text on success, null on failure.
|
|
120
|
+
* @param {{ fullMatch: string, linkText: string, url: string, subdir: string, filename: string, localRelPath: string }} ref
|
|
121
|
+
* @param {number} timeoutMs
|
|
122
|
+
* @returns {Promise<string | null>}
|
|
123
|
+
*/
|
|
124
|
+
async function downloadRef(ref, timeoutMs = 5000) {
|
|
125
|
+
try {
|
|
126
|
+
const controller = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
128
|
+
const response = await fetch(ref.url, { signal: controller.signal });
|
|
129
|
+
clearTimeout(timer);
|
|
130
|
+
if (!response.ok) return null;
|
|
131
|
+
return await response.text();
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Download all remote references found in content. Returns rewritten content
|
|
140
|
+
* (absolute URLs → relative paths) and the downloaded file data.
|
|
141
|
+
* Failed downloads keep their original absolute URL.
|
|
142
|
+
*/
|
|
143
|
+
async function downloadAndRewriteRefs(content) {
|
|
144
|
+
const refs = extractRemoteRefs(content);
|
|
145
|
+
if (refs.length === 0) return { rewrittenContent: content, downloadedFiles: [] };
|
|
146
|
+
|
|
147
|
+
if (typeof globalThis.fetch !== "function") {
|
|
148
|
+
return { rewrittenContent: content, downloadedFiles: [] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const results = await Promise.allSettled(
|
|
152
|
+
refs.map(async ref => {
|
|
153
|
+
const data = await downloadRef(ref);
|
|
154
|
+
return { ref, data };
|
|
155
|
+
})
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
let rewrittenContent = content;
|
|
159
|
+
const downloadedFiles = [];
|
|
160
|
+
|
|
161
|
+
for (const result of results) {
|
|
162
|
+
if (result.status === "fulfilled" && result.value.data !== null) {
|
|
163
|
+
const { ref, data } = result.value;
|
|
164
|
+
const newLink = `[${ref.linkText}](${ref.localRelPath})`;
|
|
165
|
+
rewrittenContent = rewrittenContent.replace(ref.fullMatch, newLink);
|
|
166
|
+
downloadedFiles.push({
|
|
167
|
+
subdir: ref.subdir,
|
|
168
|
+
filename: ref.filename,
|
|
169
|
+
data,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
return { rewrittenContent, downloadedFiles };
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Canonical skill directory (.agents/)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Write SKILL.md and downloaded reference files into the canonical location
|
|
183
|
+
* at `.agents/skills/needle-engine/`. This directory is always created and
|
|
184
|
+
* serves as the symlink target for all other agents.
|
|
185
|
+
* @returns {string} The absolute path to the canonical skill directory.
|
|
186
|
+
*/
|
|
187
|
+
function writeCanonicalSkillDir(cwd, content, downloadedFiles) {
|
|
188
|
+
const canonicalDir = join(cwd, ".agents", "skills", "needle-engine");
|
|
189
|
+
if (!existsSync(canonicalDir)) {
|
|
190
|
+
mkdirSync(canonicalDir, { recursive: true });
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
writeFileSync(join(canonicalDir, "SKILL.md"), content, "utf8");
|
|
194
|
+
|
|
195
|
+
for (const file of downloadedFiles) {
|
|
196
|
+
const fileDir = join(canonicalDir, file.subdir);
|
|
197
|
+
if (!existsSync(fileDir)) {
|
|
198
|
+
mkdirSync(fileDir, { recursive: true });
|
|
199
|
+
}
|
|
200
|
+
writeFileSync(join(fileDir, file.filename), file.data, "utf8");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return canonicalDir;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
// Agent writers
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Create a symlink at `<agentDir>/skills/needle-engine` → canonical skill dir.
|
|
212
|
+
* If the target already exists (file, dir, or stale symlink), it is replaced.
|
|
213
|
+
*/
|
|
214
|
+
function symlinkSkillDir(agentDir, canonicalDir) {
|
|
215
|
+
const skillsDir = join(agentDir, "skills");
|
|
216
|
+
const linkPath = join(skillsDir, "needle-engine");
|
|
217
|
+
|
|
218
|
+
if (!existsSync(skillsDir)) {
|
|
219
|
+
mkdirSync(skillsDir, { recursive: true });
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const target = relative(skillsDir, canonicalDir);
|
|
223
|
+
|
|
224
|
+
// Remove existing entry (file, dir, or stale symlink)
|
|
225
|
+
try {
|
|
226
|
+
if (existsSync(linkPath) || lstatSync(linkPath).isSymbolicLink()) {
|
|
227
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
catch { /* nothing to remove */ }
|
|
231
|
+
|
|
232
|
+
try {
|
|
233
|
+
symlinkSync(target, linkPath, "junction");
|
|
234
|
+
return linkPath;
|
|
235
|
+
}
|
|
236
|
+
catch {
|
|
237
|
+
// Fallback: copy if symlink fails (e.g. Windows without privileges)
|
|
238
|
+
if (!existsSync(linkPath)) {
|
|
239
|
+
mkdirSync(linkPath, { recursive: true });
|
|
240
|
+
}
|
|
241
|
+
copyDirSync(canonicalDir, linkPath);
|
|
242
|
+
return linkPath;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* Write a Cursor rule file at `.cursor/rules/needle-engine.mdc` and symlink
|
|
248
|
+
* the reference files directory at `.cursor/rules/needle-engine/`.
|
|
249
|
+
* Cursor uses its own frontmatter format (description, globs, alwaysApply).
|
|
250
|
+
*/
|
|
251
|
+
function writeCursorRule(cwd, canonicalDir, content) {
|
|
252
|
+
const rulesDir = join(cwd, ".cursor", "rules");
|
|
253
|
+
if (!existsSync(rulesDir)) {
|
|
254
|
+
mkdirSync(rulesDir, { recursive: true });
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
// Symlink .cursor/rules/needle-engine/ → canonical dir (for reference files)
|
|
258
|
+
const linkPath = join(rulesDir, "needle-engine");
|
|
259
|
+
const target = relative(rulesDir, canonicalDir);
|
|
260
|
+
try {
|
|
261
|
+
if (existsSync(linkPath) || lstatSync(linkPath).isSymbolicLink()) {
|
|
262
|
+
rmSync(linkPath, { recursive: true, force: true });
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
catch { /* nothing to remove */ }
|
|
266
|
+
|
|
267
|
+
try {
|
|
268
|
+
symlinkSync(target, linkPath, "junction");
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
// Fallback: copy directly
|
|
272
|
+
if (!existsSync(linkPath)) {
|
|
273
|
+
mkdirSync(linkPath, { recursive: true });
|
|
274
|
+
}
|
|
275
|
+
copyDirSync(canonicalDir, linkPath);
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
// Write the .mdc file with adjusted relative paths
|
|
279
|
+
// SKILL.md uses ./references/api.md but the .mdc sits at .cursor/rules/needle-engine.mdc
|
|
280
|
+
// so we need ./needle-engine/references/api.md
|
|
281
|
+
let body = stripFrontmatter(content);
|
|
282
|
+
body = body.replace(/\]\(\.\/(references|templates)\//g, "](./needle-engine/$1/");
|
|
283
|
+
|
|
284
|
+
const rulePath = join(rulesDir, "needle-engine.mdc");
|
|
285
|
+
const cursorContent = `---
|
|
286
|
+
description: Needle Engine context — use when editing TypeScript components, Vite config, GLB assets, or anything related to @needle-tools/engine.
|
|
287
|
+
globs:
|
|
288
|
+
alwaysApply: false
|
|
289
|
+
---
|
|
290
|
+
${body}`;
|
|
291
|
+
writeFileSync(rulePath, cursorContent, "utf8");
|
|
292
|
+
return rulePath;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// Helpers
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
|
|
299
|
+
/** Recursively copy a directory's contents. */
|
|
300
|
+
function copyDirSync(src, dest) {
|
|
301
|
+
for (const entry of readdirSync(src)) {
|
|
302
|
+
const srcPath = join(src, entry);
|
|
303
|
+
const destPath = join(dest, entry);
|
|
304
|
+
if (statSync(srcPath).isDirectory()) {
|
|
305
|
+
if (!existsSync(destPath)) mkdirSync(destPath, { recursive: true });
|
|
306
|
+
copyDirSync(srcPath, destPath);
|
|
307
|
+
}
|
|
308
|
+
else {
|
|
309
|
+
copyFileSync(srcPath, destPath);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// ---------------------------------------------------------------------------
|
|
315
|
+
// Main
|
|
316
|
+
// ---------------------------------------------------------------------------
|
|
317
|
+
|
|
318
|
+
/** Detect agents and install the Needle Engine skill for each. */
|
|
319
|
+
async function installSkills() {
|
|
57
320
|
const cwd = process.cwd();
|
|
58
|
-
const dirs = [".claude", ".github", ".agents"].map(d => join(cwd, d)).filter(d => existsSync(d));
|
|
59
|
-
if (dirs.length === 0) return; // only install if developer uses an AI coding agent
|
|
60
321
|
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
322
|
+
const rawContent = getSkillContent();
|
|
323
|
+
const { rewrittenContent, downloadedFiles } = await downloadAndRewriteRefs(rawContent);
|
|
324
|
+
|
|
325
|
+
// Always write to .agents/ as the canonical location
|
|
326
|
+
const canonicalDir = writeCanonicalSkillDir(cwd, rewrittenContent, downloadedFiles);
|
|
327
|
+
|
|
328
|
+
// Symlink other detected agents to the canonical dir
|
|
329
|
+
const detected = agents.filter(a => existsSync(join(cwd, a.detect)));
|
|
330
|
+
const names = ["Codex / Universal"];
|
|
331
|
+
for (const agent of detected) {
|
|
332
|
+
const path = agent.write(cwd, canonicalDir, rewrittenContent);
|
|
333
|
+
if (path) names.push(agent.name);
|
|
64
334
|
}
|
|
335
|
+
|
|
336
|
+
const refCount = downloadedFiles.length;
|
|
337
|
+
const refMsg = refCount > 0 ? ` (${refCount} reference file${refCount === 1 ? "" : "s"} downloaded)` : "";
|
|
338
|
+
needleLog(pluginName, `Installed for ${names.length} agent${names.length === 1 ? "" : "s"}: ${names.join(", ")}${refMsg}`);
|
|
65
339
|
}
|
|
@@ -1088,7 +1088,8 @@ export class Input implements IInput {
|
|
|
1088
1088
|
if (this.context.isInAR) return;
|
|
1089
1089
|
if (this.canReceiveInput(evt) === false) return;
|
|
1090
1090
|
if (evt.target instanceof HTMLElement) {
|
|
1091
|
-
evt.target.setPointerCapture(evt.pointerId);
|
|
1091
|
+
try { evt.target.setPointerCapture(evt.pointerId); }
|
|
1092
|
+
catch { /* may fail during pointer lock */ }
|
|
1092
1093
|
}
|
|
1093
1094
|
const id = this.getPointerId(evt);
|
|
1094
1095
|
if (debug) showBalloonMessage(`pointer down #${id}, identifier:${evt.pointerId}`);
|
|
@@ -219,6 +219,32 @@ export class NewInstanceModel implements IModel {
|
|
|
219
219
|
export type SyncInstantiateOptions = IInstantiateOptions & Pick<IModel, "deleteOnDisconnect">;
|
|
220
220
|
|
|
221
221
|
// #region Sync Instantiate
|
|
222
|
+
|
|
223
|
+
declare type SyncInstantiateCallback = (instance: GameObject, model: NewInstanceModel) => void;
|
|
224
|
+
const _onSyncInstantiateCallbacks: SyncInstantiateCallback[] = [];
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Register a callback that fires when a remote `syncInstantiate` object is created on this client.
|
|
228
|
+
* Use this to get references to objects spawned by other users.
|
|
229
|
+
* @param callback Called with the instantiated Object3D and the network model data
|
|
230
|
+
* @returns An unsubscribe function
|
|
231
|
+
* @category Networking
|
|
232
|
+
* @example
|
|
233
|
+
* ```ts
|
|
234
|
+
* const unsub = onSyncInstantiate((instance, model) => {
|
|
235
|
+
* console.log("Remote object created:", instance.name, model.originalGuid);
|
|
236
|
+
* });
|
|
237
|
+
* // later: unsub();
|
|
238
|
+
* ```
|
|
239
|
+
*/
|
|
240
|
+
export function onSyncInstantiate(callback: SyncInstantiateCallback): () => void {
|
|
241
|
+
_onSyncInstantiateCallbacks.push(callback);
|
|
242
|
+
return () => {
|
|
243
|
+
const idx = _onSyncInstantiateCallbacks.indexOf(callback);
|
|
244
|
+
if (idx >= 0) _onSyncInstantiateCallbacks.splice(idx, 1);
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
|
|
222
248
|
/**
|
|
223
249
|
* Instantiate an object across the network. See also {@link syncDestroy}.
|
|
224
250
|
* @category Networking
|
|
@@ -350,6 +376,11 @@ export function beginListenInstantiate(context: Context) {
|
|
|
350
376
|
context.scene.add(inst);
|
|
351
377
|
if (debug)
|
|
352
378
|
console.log("[Remote] new instance", "gameobject:", inst?.guid, obj);
|
|
379
|
+
// Notify listeners about the remote instantiation
|
|
380
|
+
for (const cb of _onSyncInstantiateCallbacks) {
|
|
381
|
+
try { cb(inst, model); }
|
|
382
|
+
catch (err) { console.error("Error in onSyncInstantiate callback", err); }
|
|
383
|
+
}
|
|
353
384
|
}
|
|
354
385
|
});
|
|
355
386
|
const cb2 = context.connection.beginListen("left-room", () => {
|
|
@@ -38,17 +38,44 @@ export class PlayerSync extends Behaviour {
|
|
|
38
38
|
* scene.add(res.gameObject);
|
|
39
39
|
* ```
|
|
40
40
|
*/
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
41
|
+
/**
|
|
42
|
+
* This API is experimental and may change or be removed in the future.
|
|
43
|
+
* Creates a PlayerSync instance at runtime from a given URL or Object3D and sets it up for networking.
|
|
44
|
+
* @param urlOrObject Path to the asset that should be instantiated for each player, or a pre-created Object3D to use as the player prefab
|
|
45
|
+
* @param init Optional initialization parameters for the PlayerSync component
|
|
46
|
+
* @returns Promise resolving to a PlayerSync instance with a guaranteed asset property
|
|
47
|
+
* @example
|
|
48
|
+
* ```typescript
|
|
49
|
+
* // From a GLB URL:
|
|
50
|
+
* const res = await PlayerSync.setupFrom("/assets/demo.glb");
|
|
51
|
+
*
|
|
52
|
+
* // From a runtime-created Object3D:
|
|
53
|
+
* const avatar = ObjectUtils.createPrimitive("Sphere");
|
|
54
|
+
* const res = await PlayerSync.setupFrom(avatar);
|
|
55
|
+
* ```
|
|
56
|
+
*/
|
|
57
|
+
static async setupFrom(urlOrObject: string | Object3D, init?: Omit<ComponentInit<PlayerSync>, "asset"> & { guid?: string }): Promise<PlayerSyncWithAsset> {
|
|
58
|
+
let assetReference: AssetReference;
|
|
59
|
+
|
|
60
|
+
if (typeof urlOrObject === "string") {
|
|
61
|
+
assetReference = AssetReference.getOrCreateFromUrl(urlOrObject);
|
|
62
|
+
if (!assetReference.asset) {
|
|
63
|
+
const i = await assetReference.loadAssetAsync();
|
|
64
|
+
if(i) GameObject.getOrAddComponent(i, PlayerState);
|
|
65
|
+
}
|
|
66
|
+
} else {
|
|
67
|
+
// Runtime Object3D passed directly — wrap in AssetReference without loading
|
|
68
|
+
assetReference = new AssetReference({ asset: urlOrObject });
|
|
69
|
+
GameObject.getOrAddComponent(urlOrObject, PlayerState);
|
|
46
70
|
}
|
|
71
|
+
|
|
47
72
|
const ps = new PlayerSync();
|
|
48
73
|
ps._internalInit(init);
|
|
49
74
|
ps.asset = assetReference;
|
|
50
75
|
const obj = new Object3D();
|
|
51
|
-
|
|
76
|
+
// The guid is used to identify this PlayerSync instance across the network.
|
|
77
|
+
// For URLs it uses the URL itself; for runtime objects it tries the init guid, object guid, name, or uuid.
|
|
78
|
+
obj["guid"] = init?.guid ?? (typeof urlOrObject === "string" ? urlOrObject : (urlOrObject["guid"] || urlOrObject.name || urlOrObject.uuid));
|
|
52
79
|
GameObject.addComponent(obj, ps);
|
|
53
80
|
return ps as PlayerSyncWithAsset;
|
|
54
81
|
}
|