playcanvas
Version:
Open-source WebGL/WebGPU 3D engine for the web
245 lines (244 loc) • 9.01 kB
JavaScript
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
import { Debug } from "../../core/debug.js";
import { EventHandler } from "../../core/event-handler.js";
import { reservedScriptNames } from "./constants.js";
import { getScriptRegistryName } from "./script.js";
class ScriptRegistry extends EventHandler {
/**
* Create a new ScriptRegistry instance.
*
* @param {AppBase} app - Application to attach registry to.
*/
constructor(app) {
super();
/**
* A Map of script names to script classes. A Map is used (rather than a plain object) so that
* script names which collide with `Object.prototype` members - e.g. `hasOwnProperty`,
* `toString`, `__proto__` - are stored and looked up safely.
*
* @type {Map<string, typeof ScriptType>}
* @private
*/
__publicField(this, "_scripts", /* @__PURE__ */ new Map());
/**
* @type {typeof ScriptType[]}
* @private
*/
__publicField(this, "_list", []);
/**
* A Map of script names to attribute schemas.
*
* @type {Map<string, AttributeSchema>}
* @private
*/
__publicField(this, "_scriptSchemas", /* @__PURE__ */ new Map());
this.app = app;
}
destroy() {
this.app = null;
this.off();
}
/**
* Registers a schema against a script instance.
*
* @param {string} id - The key to use to store the schema
* @param {AttributeSchema} schema - An schema definition for the script
*/
addSchema(id, schema) {
if (!schema) return;
this._scriptSchemas.set(id, schema);
}
/**
* Returns a schema for a given script name.
*
* @param {string} id - The key to store the schema under
* @returns {AttributeSchema | undefined} - The schema stored under the key
*/
getSchema(id) {
return this._scriptSchemas.get(id);
}
/**
* Add a script to the registry, keyed by its name. The name is taken from the script's static
* `scriptName` property (for {@link Script} classes), or assigned by {@link createScript} /
* {@link registerScript}. Note: when {@link createScript} or {@link registerScript} is called,
* the script is added to the registry automatically, so calling this method directly is only
* required when registering a {@link Script} class manually (e.g. in an engine-only project).
*
* If a script with the same name already exists in the registry, and the new script has a
* `swap` method defined, it will perform code hot swapping automatically in an async manner.
*
* @param {typeof Script | typeof ScriptType} script - The script class to add. Must have a
* resolvable name (a static `scriptName`, an assigned `__name`, or an inferable class name).
* @returns {boolean} True if the script was added for the first time. False if a script with
* the same name already exists, or if the script has no resolvable name.
* @example
* var PlayerController = pc.createScript('playerController');
* // playerController Script Type will be added to pc.ScriptRegistry automatically
* console.log(app.scripts.has('playerController')); // outputs true
* @example
* // engine-only: register an ESM Script class manually
* class Rotator extends pc.Script {
* static scriptName = 'rotator';
* }
* app.scripts.add(Rotator);
* console.log(app.scripts.has('rotator')); // outputs true
*/
add(script) {
const scriptName = getScriptRegistryName(script);
if (!scriptName) {
Debug.error(`script class '${script?.name ?? script}' has no name and cannot be added to the script registry.`);
return false;
}
if (reservedScriptNames.has(scriptName)) {
Debug.error(`script name '${scriptName}' is reserved and cannot be added to the script registry.`);
return false;
}
script.__name = scriptName;
if (this._scripts.has(scriptName)) {
setTimeout(() => {
if (script.prototype.swap) {
const old = this._scripts.get(scriptName);
const ind = this._list.indexOf(old);
this._list[ind] = script;
this._scripts.set(scriptName, script);
this.fire("swap", scriptName, script);
this.fire(`swap:${scriptName}`, script);
} else {
console.warn(`script registry already has '${scriptName}' script, define 'swap' method for new script type to enable code hot swapping`);
}
});
return false;
}
this._scripts.set(scriptName, script);
this._list.push(script);
this.fire("add", scriptName, script);
this.fire(`add:${scriptName}`, script);
setTimeout(() => {
if (!this._scripts.has(scriptName)) {
return;
}
if (!this.app || !this.app.systems || !this.app.systems.script) {
return;
}
const components = this.app.systems.script._components;
let attributes;
const scriptInstances = [];
const scriptInstancesInitialized = [];
for (components.loopIndex = 0; components.loopIndex < components.length; components.loopIndex++) {
const component = components.items[components.loopIndex];
if (component._scriptsIndex[scriptName] && component._scriptsIndex[scriptName].awaiting) {
if (component._scriptsData && component._scriptsData[scriptName]) {
attributes = component._scriptsData[scriptName].attributes;
}
const scriptInstance = component.create(scriptName, {
preloading: true,
ind: component._scriptsIndex[scriptName].ind,
attributes
});
if (scriptInstance) {
scriptInstances.push(scriptInstance);
}
for (const script2 of component.scripts) {
component.initializeAttributes(script2);
}
}
}
for (let i = 0; i < scriptInstances.length; i++) {
if (scriptInstances[i].enabled) {
scriptInstances[i]._initialized = true;
scriptInstancesInitialized.push(scriptInstances[i]);
if (scriptInstances[i].initialize) {
scriptInstances[i].initialize();
}
}
}
for (let i = 0; i < scriptInstancesInitialized.length; i++) {
if (!scriptInstancesInitialized[i].enabled || scriptInstancesInitialized[i]._postInitialized) {
continue;
}
scriptInstancesInitialized[i]._postInitialized = true;
if (scriptInstancesInitialized[i].postInitialize) {
scriptInstancesInitialized[i].postInitialize();
}
}
});
return true;
}
/**
* Remove {@link ScriptType}.
*
* @param {string|typeof ScriptType} nameOrType - The name or type
* of {@link ScriptType}.
* @returns {boolean} True if removed or False if already not in registry.
* @example
* app.scripts.remove('playerController');
*/
remove(nameOrType) {
let scriptType = nameOrType;
let scriptName = nameOrType;
if (typeof scriptName !== "string") {
scriptName = scriptType.__name;
} else {
scriptType = this.get(scriptName);
}
if (this.get(scriptName) !== scriptType) {
return false;
}
this._scripts.delete(scriptName);
const ind = this._list.indexOf(scriptType);
this._list.splice(ind, 1);
this.fire("remove", scriptName, scriptType);
this.fire(`remove:${scriptName}`, scriptType);
return true;
}
/**
* Get {@link ScriptType} by name.
*
* @param {string} name - Name of a {@link ScriptType}.
* @returns {typeof ScriptType} The Script Type if it exists in the
* registry or null otherwise.
* @example
* var PlayerController = app.scripts.get('playerController');
*/
get(name) {
return this._scripts.get(name) || null;
}
/**
* Check if a {@link ScriptType} with the specified name is in the registry.
*
* @param {string|typeof ScriptType} nameOrType - The name or type
* of {@link ScriptType}.
* @returns {boolean} True if {@link ScriptType} is in registry.
* @example
* if (app.scripts.has('playerController')) {
* // playerController is in pc.ScriptRegistry
* }
*/
has(nameOrType) {
if (typeof nameOrType === "string") {
return this._scripts.has(nameOrType);
}
if (!nameOrType) return false;
const scriptName = nameOrType.__name;
return this._scripts.get(scriptName) === nameOrType;
}
/**
* Get list of all {@link ScriptType}s from registry.
*
* @returns {Array<typeof ScriptType>} list of all {@link ScriptType}s
* in registry.
* @example
* // logs array of all Script Type names available in registry
* console.log(app.scripts.list().map(function (o) {
* return o.name;
* }));
*/
list() {
return this._list;
}
}
export {
ScriptRegistry
};