playcanvas
Version:
PlayCanvas WebGL game engine
893 lines (890 loc) • 38.1 kB
JavaScript
import { Debug } from '../../../core/debug.js';
import { SortedLoopArray } from '../../../core/sorted-loop-array.js';
import { ScriptAttributes, assignAttributesToScript } from '../../script/script-attributes.js';
import { Component } from '../component.js';
import { Entity } from '../../entity.js';
import { SCRIPT_POST_INITIALIZE, SCRIPT_INITIALIZE, SCRIPT_UPDATE, SCRIPT_POST_UPDATE, SCRIPT_SWAP } from '../../script/constants.js';
import { ScriptType } from '../../script/script-type.js';
import { getScriptName } from '../../script/script.js';
/**
* @import { ScriptComponentSystem } from './system.js'
* @import { Script } from '../../script/script.js'
*/ const toLowerCamelCase = (str)=>str[0].toLowerCase() + str.substring(1);
/**
* The ScriptComponent allows you add custom behavior to an {@link Entity} by attaching
* your own scripts written in JavaScript (or TypeScript).
*
* You should never need to use the ScriptComponent constructor directly. To add a
* ScriptComponent to an Entity, use {@link Entity#addComponent}:
*
* ```javascript
* const entity = new pc.Entity();
* entity.addComponent('script');
* ```
*
* Once the ScriptComponent is added to the entity, you can access it via the {@link Entity#script}
* property.
*
* Add scripts to the entity by calling the `create` method:
*
* ```javascript
* // Option 1: Add a script using the name registered in the ScriptRegistry
* entity.script.create('cameraControls');
*
* // Option 2: Add a script using the script class
* entity.script.create(CameraControls);
* ```
*
* For more details on scripting see the [Scripting Section](https://developer.playcanvas.com/user-manual/scripting/)
* of the User Manual.
*
* @hideconstructor
* @category Script
*/ class ScriptComponent extends Component {
static{
/**
* Fired when a {@link ScriptType} instance is created and attached to the script component.
* This event is available in two forms. They are as follows:
*
* 1. `create` - Fired when a script instance is created. The name of the script type and the
* script type instance are passed as arguments.
* 2. `create:[name]` - Fired when a script instance is created that has the specified script
* type name. The script instance is passed as an argument to the handler.
*
* @event
* @example
* entity.script.on('create', (name, scriptInstance) => {
* console.log(`Instance of script '${name}' created`);
* });
* @example
* entity.script.on('create:player', (scriptInstance) => {
* console.log(`Instance of script 'player' created`);
* });
*/ this.EVENT_CREATE = 'create';
}
static{
/**
* Fired when a {@link ScriptType} instance is destroyed and removed from the script component.
* This event is available in two forms. They are as follows:
*
* 1. `destroy` - Fired when a script instance is destroyed. The name of the script type and
* the script type instance are passed as arguments.
* 2. `destroy:[name]` - Fired when a script instance is destroyed that has the specified
* script type name. The script instance is passed as an argument.
*
* @event
* @example
* entity.script.on('destroy', (name, scriptInstance) => {
* console.log(`Instance of script '${name}' destroyed`);
* });
* @example
* entity.script.on('destroy:player', (scriptInstance) => {
* console.log(`Instance of script 'player' destroyed`);
* });
*/ this.EVENT_DESTROY = 'destroy';
}
static{
/**
* Fired when the script component becomes enabled. This event does not take into account the
* enabled state of the entity or any of its ancestors.
*
* @event
* @example
* entity.script.on('enable', () => {
* console.log(`Script component of entity '${entity.name}' has been enabled`);
* });
*/ this.EVENT_ENABLE = 'enable';
}
static{
/**
* Fired when the script component becomes disabled. This event does not take into account the
* enabled state of the entity or any of its ancestors.
*
* @event
* @example
* entity.script.on('disable', () => {
* console.log(`Script component of entity '${entity.name}' has been disabled`);
* });
*/ this.EVENT_DISABLE = 'disable';
}
static{
/**
* Fired when the script component has been removed from its entity.
*
* @event
* @example
* entity.script.on('remove', () => {
* console.log(`Script component removed from entity '${entity.name}'`);
* });
*/ this.EVENT_REMOVE = 'remove';
}
static{
/**
* Fired when the script component changes state to enabled or disabled. The handler is passed
* the new boolean enabled state of the script component. This event does not take into account
* the enabled state of the entity or any of its ancestors.
*
* @event
* @example
* entity.script.on('state', (enabled) => {
* console.log(`Script component of entity '${entity.name}' changed state to '${enabled}'`);
* });
*/ this.EVENT_STATE = 'state';
}
static{
/**
* Fired when the index of a {@link ScriptType} instance is changed in the script component.
* This event is available in two forms. They are as follows:
*
* 1. `move` - Fired when a script instance is moved. The name of the script type, the script
* type instance, the new index and the old index are passed as arguments.
* 2. `move:[name]` - Fired when a specifically named script instance is moved. The script
* instance, the new index and the old index are passed as arguments.
*
* @event
* @example
* entity.script.on('move', (name, scriptInstance, newIndex, oldIndex) => {
* console.log(`Script '${name}' moved from index '${oldIndex}' to '${newIndex}'`);
* });
* @example
* entity.script.on('move:player', (scriptInstance, newIndex, oldIndex) => {
* console.log(`Script 'player' moved from index '${oldIndex}' to '${newIndex}'`);
* });
*/ this.EVENT_MOVE = 'move';
}
static{
/**
* Fired when a {@link ScriptType} instance had an exception. The handler is passed the script
* instance, the exception and the method name that the exception originated from.
*
* @event
* @example
* entity.script.on('error', (scriptInstance, exception, methodName) => {
* console.log(`Script error: ${exception} in method '${methodName}'`);
* });
*/ this.EVENT_ERROR = 'error';
}
/**
* Create a new ScriptComponent instance.
*
* @param {ScriptComponentSystem} system - The ComponentSystem that created this Component.
* @param {Entity} entity - The Entity that this Component is attached to.
*/ constructor(system, entity){
super(system, entity), /**
* A map of script name to initial component data.
*
* @type {Map<string, object>}
* @private
*/ this._attributeDataMap = new Map();
/**
* Holds all script instances for this component.
*
* @type {ScriptType[]}
* @private
*/ this._scripts = [];
// holds all script instances with an update method
this._updateList = new SortedLoopArray({
sortBy: '__executionOrder'
});
// holds all script instances with a postUpdate method
this._postUpdateList = new SortedLoopArray({
sortBy: '__executionOrder'
});
this._scriptsIndex = {};
this._destroyedScripts = [];
this._destroyed = false;
this._scriptsData = null;
this._oldState = true;
// override default 'enabled' property of base pc.Component
// because this is faster
this._enabled = true;
// whether this component is currently being enabled
this._beingEnabled = false;
// if true then we are currently looping through
// script instances. This is used to prevent a scripts array
// from being modified while a loop is being executed
this._isLoopingThroughScripts = false;
// the order that this component will be updated
// by the script system. This is set by the system itself.
this._executionOrder = -1;
this.on('set_enabled', this._onSetEnabled, this);
}
/**
* Sets the array of all script instances attached to an entity. This array is read-only and
* should not be modified by developer.
*
* @type {Script[]}
*/ set scripts(value) {
this._scriptsData = value;
for(const key in value){
if (!value.hasOwnProperty(key)) {
continue;
}
const script = this._scriptsIndex[key];
if (script) {
// existing script
// enabled
if (typeof value[key].enabled === 'boolean') {
// Before a script is initialized, initialize any attributes
script.once('preInitialize', ()=>{
this.initializeAttributes(script);
});
script.enabled = !!value[key].enabled;
}
// attributes
if (typeof value[key].attributes === 'object') {
for(const attr in value[key].attributes){
if (ScriptAttributes.reservedNames.has(attr)) {
continue;
}
if (!script.__attributes.hasOwnProperty(attr)) {
// new attribute
const scriptType = this.system.app.scripts.get(key);
if (scriptType) {
scriptType.attributes.add(attr, {});
}
}
// update attribute
script[attr] = value[key].attributes[attr];
}
}
} else {
// TODO scripts2
// new script
console.log(this.order);
}
}
}
/**
* Gets the array of all script instances attached to an entity.
*
* @type {ScriptType[]}
*/ get scripts() {
return this._scripts;
}
set enabled(value) {
const oldValue = this._enabled;
this._enabled = value;
this.fire('set', 'enabled', oldValue, value);
}
get enabled() {
return this._enabled;
}
onEnable() {
this._beingEnabled = true;
this._checkState();
if (!this.entity._beingEnabled) {
this.onPostStateChange();
}
this._beingEnabled = false;
}
onDisable() {
this._checkState();
}
onPostStateChange() {
const wasLooping = this._beginLooping();
for(let i = 0, len = this.scripts.length; i < len; i++){
const script = this.scripts[i];
if (script._initialized && !script._postInitialized && script.enabled) {
script._postInitialized = true;
if (script.postInitialize) {
this._scriptMethod(script, SCRIPT_POST_INITIALIZE);
}
}
}
this._endLooping(wasLooping);
}
// Sets isLoopingThroughScripts to false and returns
// its previous value
_beginLooping() {
const looping = this._isLoopingThroughScripts;
this._isLoopingThroughScripts = true;
return looping;
}
// Restores isLoopingThroughScripts to the specified parameter
// If all loops are over then remove destroyed scripts form the _scripts array
_endLooping(wasLoopingBefore) {
this._isLoopingThroughScripts = wasLoopingBefore;
if (!this._isLoopingThroughScripts) {
this._removeDestroyedScripts();
}
}
// We also need this handler because it is fired
// when value === old instead of onEnable and onDisable
// which are only fired when value !== old
_onSetEnabled(prop, old, value) {
this._beingEnabled = true;
this._checkState();
this._beingEnabled = false;
}
_checkState() {
const state = this.enabled && this.entity.enabled;
if (state === this._oldState) {
return;
}
this._oldState = state;
this.fire(state ? 'enable' : 'disable');
this.fire('state', state);
if (state) {
this.system._addComponentToEnabled(this);
} else {
this.system._removeComponentFromEnabled(this);
}
const wasLooping = this._beginLooping();
for(let i = 0, len = this.scripts.length; i < len; i++){
const script = this.scripts[i];
script.once('preInitialize', ()=>{
this.initializeAttributes(script);
});
script.enabled = script._enabled;
}
this._endLooping(wasLooping);
}
_onBeforeRemove() {
this.fire('remove');
const wasLooping = this._beginLooping();
// destroy all scripts
for(let i = 0; i < this.scripts.length; i++){
const script = this.scripts[i];
if (!script) continue;
this.destroy(script.__scriptType.__name);
}
this._endLooping(wasLooping);
}
_removeDestroyedScripts() {
const len = this._destroyedScripts.length;
if (!len) return;
for(let i = 0; i < len; i++){
const script = this._destroyedScripts[i];
this._removeScriptInstance(script);
}
this._destroyedScripts.length = 0;
// update execution order for scripts
this._resetExecutionOrder(0, this._scripts.length);
}
_onInitializeAttributes() {
for(let i = 0, len = this.scripts.length; i < len; i++){
const script = this.scripts[i];
this.initializeAttributes(script);
}
}
initializeAttributes(script) {
// if script has __initializeAttributes method assume it has a runtime schema
if (script instanceof ScriptType) {
script.__initializeAttributes();
} else {
// otherwise we need to manually initialize attributes from the schema
const name = script.__scriptType.__name;
const data = this._attributeDataMap.get(name);
// If not data exists return early
if (!data) {
return;
}
// Fetch schema and warn if it doesn't exist
const schema = this.system.app.scripts?.getSchema(name);
if (!schema) {
Debug.warnOnce(`No schema exists for the script '${name}'. A schema must exist for data to be instantiated on the script.`);
}
// Assign the attributes to the script instance based on the attribute schema
assignAttributesToScript(this.system.app, schema.attributes, data, script);
}
}
_scriptMethod(script, method, arg) {
try {
script[method](arg);
} catch (ex) {
// disable script if it fails to call method
script.enabled = false;
if (!script.hasEvent('error')) {
console.warn(`unhandled exception while calling "${method}" for "${script.__scriptType.__name}" script: `, ex);
console.error(ex);
}
script.fire('error', ex, method);
this.fire('error', script, ex, method);
}
}
_onInitialize() {
const scripts = this._scripts;
const wasLooping = this._beginLooping();
for(let i = 0, len = scripts.length; i < len; i++){
const script = scripts[i];
if (!script._initialized && script.enabled) {
script._initialized = true;
if (script.initialize) {
this._scriptMethod(script, SCRIPT_INITIALIZE);
}
}
}
this._endLooping(wasLooping);
}
_onPostInitialize() {
this.onPostStateChange();
}
_onUpdate(dt) {
const list = this._updateList;
if (!list.length) return;
const wasLooping = this._beginLooping();
for(list.loopIndex = 0; list.loopIndex < list.length; list.loopIndex++){
const script = list.items[list.loopIndex];
if (script.enabled) {
this._scriptMethod(script, SCRIPT_UPDATE, dt);
}
}
this._endLooping(wasLooping);
}
_onPostUpdate(dt) {
const list = this._postUpdateList;
if (!list.length) return;
const wasLooping = this._beginLooping();
for(list.loopIndex = 0; list.loopIndex < list.length; list.loopIndex++){
const script = list.items[list.loopIndex];
if (script.enabled) {
this._scriptMethod(script, SCRIPT_POST_UPDATE, dt);
}
}
this._endLooping(wasLooping);
}
/**
* Inserts script instance into the scripts array at the specified index. Also inserts the
* script into the update list if it has an update method and the post update list if it has a
* postUpdate method.
*
* @param {object} scriptInstance - The script instance.
* @param {number} index - The index where to insert the script at. If -1, append it at the end.
* @param {number} scriptsLength - The length of the scripts array.
* @private
*/ _insertScriptInstance(scriptInstance, index, scriptsLength) {
if (index === -1) {
// append script at the end and set execution order
this._scripts.push(scriptInstance);
scriptInstance.__executionOrder = scriptsLength;
// append script to the update list if it has an update method
if (scriptInstance.update) {
this._updateList.append(scriptInstance);
}
// add script to the postUpdate list if it has a postUpdate method
if (scriptInstance.postUpdate) {
this._postUpdateList.append(scriptInstance);
}
} else {
// insert script at index and set execution order
this._scripts.splice(index, 0, scriptInstance);
scriptInstance.__executionOrder = index;
// now we also need to update the execution order of all
// the script instances that come after this script
this._resetExecutionOrder(index + 1, scriptsLength + 1);
// insert script to the update list if it has an update method
// in the right order
if (scriptInstance.update) {
this._updateList.insert(scriptInstance);
}
// insert script to the postUpdate list if it has a postUpdate method
// in the right order
if (scriptInstance.postUpdate) {
this._postUpdateList.insert(scriptInstance);
}
}
}
_removeScriptInstance(scriptInstance) {
const idx = this._scripts.indexOf(scriptInstance);
if (idx === -1) return idx;
this._scripts.splice(idx, 1);
if (scriptInstance.update) {
this._updateList.remove(scriptInstance);
}
if (scriptInstance.postUpdate) {
this._postUpdateList.remove(scriptInstance);
}
return idx;
}
_resetExecutionOrder(startIndex, scriptsLength) {
for(let i = startIndex; i < scriptsLength; i++){
this._scripts[i].__executionOrder = i;
}
}
_resolveEntityScriptAttribute(attribute, attributeName, oldValue, useGuid, newAttributes, duplicatedIdsMap) {
if (attribute.array) {
// handle entity array attribute
const len = oldValue.length;
if (!len) {
return;
}
const newGuidArray = oldValue.slice();
for(let i = 0; i < len; i++){
const guid = newGuidArray[i] instanceof Entity ? newGuidArray[i].getGuid() : newGuidArray[i];
if (duplicatedIdsMap[guid]) {
newGuidArray[i] = useGuid ? duplicatedIdsMap[guid].getGuid() : duplicatedIdsMap[guid];
}
}
newAttributes[attributeName] = newGuidArray;
} else {
// handle regular entity attribute
if (oldValue instanceof Entity) {
oldValue = oldValue.getGuid();
} else if (typeof oldValue !== 'string') {
return;
}
if (duplicatedIdsMap[oldValue]) {
newAttributes[attributeName] = duplicatedIdsMap[oldValue];
}
}
}
/**
* Detect if script is attached to an entity.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {boolean} If script is attached to an entity.
* @example
* if (entity.script.has('playerController')) {
* // entity has script
* }
*/ has(nameOrType) {
if (typeof nameOrType === 'string') {
return !!this._scriptsIndex[nameOrType];
}
if (!nameOrType) return false;
const scriptType = nameOrType;
const scriptName = scriptType.__name;
const scriptData = this._scriptsIndex[scriptName];
const scriptInstance = scriptData && scriptData.instance;
return scriptInstance instanceof scriptType; // will return false if scriptInstance undefined
}
/**
* Get a script instance (if attached).
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {ScriptType|null} If script is attached, the instance is returned. Otherwise null
* is returned.
* @example
* const controller = entity.script.get('playerController');
*/ get(nameOrType) {
if (typeof nameOrType === 'string') {
const data = this._scriptsIndex[nameOrType];
return data ? data.instance : null;
}
if (!nameOrType) return null;
const scriptType = nameOrType;
const scriptName = scriptType.__name;
const scriptData = this._scriptsIndex[scriptName];
const scriptInstance = scriptData && scriptData.instance;
return scriptInstance instanceof scriptType ? scriptInstance : null;
}
/**
* Create a script instance and attach to an entity script component.
*
* @param {string|typeof Script} nameOrType - The name or type of {@link Script}.
* @param {object} [args] - Object with arguments for a script.
* @param {boolean} [args.enabled] - If script instance is enabled after creation. Defaults to
* true.
* @param {object} [args.attributes] - Object with values for attributes (if any), where key is
* name of an attribute.
* @param {object} [args.properties] - Object with values that are **assigned** to the script instance.
* @param {boolean} [args.preloading] - If script instance is created during preload. If true,
* script and attributes must be initialized manually. Defaults to false.
* @param {number} [args.ind] - The index where to insert the script instance at. Defaults to
* -1, which means append it at the end.
* @returns {ScriptType|null} Returns an instance of a {@link ScriptType} if successfully
* attached to an entity, or null if it failed because a script with a same name has already
* been added or if the {@link ScriptType} cannot be found by name in the
* {@link ScriptRegistry}.
* @example
* entity.script.create('playerController', {
* attributes: {
* speed: 4
* }
* });
*/ create(nameOrType, args = {}) {
const self = this;
let scriptType = nameOrType;
let scriptName = nameOrType;
// shorthand using script name
if (typeof scriptType === 'string') {
scriptType = this.system.app.scripts.get(scriptType);
} else if (scriptType) {
const inferredScriptName = getScriptName(scriptType);
const lowerInferredScriptName = toLowerCamelCase(inferredScriptName);
if (!(scriptType.prototype instanceof ScriptType) && !scriptType.scriptName) {
Debug.warnOnce(`The Script class "${inferredScriptName}" must have a static "scriptName" property: \`${inferredScriptName}.scriptName = "${lowerInferredScriptName}";\`. This will be an error in future versions of PlayCanvas.`);
}
scriptType.__name ??= scriptType.scriptName ?? lowerInferredScriptName;
scriptName = scriptType.__name;
}
if (scriptType) {
if (!this._scriptsIndex[scriptName] || !this._scriptsIndex[scriptName].instance) {
// create script instance
const scriptInstance = new scriptType({
app: this.system.app,
entity: this.entity,
enabled: args.hasOwnProperty('enabled') ? args.enabled : true,
attributes: args.attributes || {}
});
if (args.properties && typeof args.properties === 'object') {
Object.assign(scriptInstance, args.properties);
}
// If the script is not a ScriptType then we must store attribute data on the component
if (!(scriptInstance instanceof ScriptType)) {
// Store the Attribute data
this._attributeDataMap.set(scriptName, args.attributes);
}
const len = this._scripts.length;
let ind = -1;
if (typeof args.ind === 'number' && args.ind !== -1 && len > args.ind) {
ind = args.ind;
}
this._insertScriptInstance(scriptInstance, ind, len);
this._scriptsIndex[scriptName] = {
instance: scriptInstance,
onSwap: function() {
self.swap(scriptName);
}
};
this[scriptName] = scriptInstance;
if (!args.preloading) {
this.initializeAttributes(scriptInstance);
}
this.fire('create', scriptName, scriptInstance);
this.fire(`create:${scriptName}`, scriptInstance);
this.system.app.scripts.on(`swap:${scriptName}`, this._scriptsIndex[scriptName].onSwap);
if (!args.preloading) {
if (scriptInstance.enabled && !scriptInstance._initialized) {
scriptInstance._initialized = true;
if (scriptInstance.initialize) {
this._scriptMethod(scriptInstance, SCRIPT_INITIALIZE);
}
}
if (scriptInstance.enabled && !scriptInstance._postInitialized) {
scriptInstance._postInitialized = true;
if (scriptInstance.postInitialize) {
this._scriptMethod(scriptInstance, SCRIPT_POST_INITIALIZE);
}
}
}
return scriptInstance;
}
Debug.warn(`script '${scriptName}' is already added to entity '${this.entity.name}'`);
} else {
this._scriptsIndex[scriptName] = {
awaiting: true,
ind: this._scripts.length
};
Debug.warn(`script '${scriptName}' is not found, awaiting it to be added to registry`);
}
return null;
}
/**
* Destroy the script instance that is attached to an entity.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {boolean} If it was successfully destroyed.
* @example
* entity.script.destroy('playerController');
*/ destroy(nameOrType) {
let scriptName = nameOrType;
let scriptType = nameOrType;
// shorthand using script name
if (typeof scriptType === 'string') {
scriptType = this.system.app.scripts.get(scriptType);
} else if (scriptType) {
scriptName = scriptType.__name;
}
const scriptData = this._scriptsIndex[scriptName];
delete this._scriptsIndex[scriptName];
if (!scriptData) return false;
this._attributeDataMap.delete(scriptName);
const scriptInstance = scriptData.instance;
if (scriptInstance && !scriptInstance._destroyed) {
scriptInstance.enabled = false;
scriptInstance._destroyed = true;
// if we are not currently looping through our scripts
// then it's safe to remove the script
if (!this._isLoopingThroughScripts) {
const ind = this._removeScriptInstance(scriptInstance);
if (ind >= 0) {
this._resetExecutionOrder(ind, this._scripts.length);
}
} else {
// otherwise push the script in _destroyedScripts and
// remove it from _scripts when the loop is over
this._destroyedScripts.push(scriptInstance);
}
}
// remove swap event
this.system.app.scripts.off(`swap:${scriptName}`, scriptData.onSwap);
delete this[scriptName];
this.fire('destroy', scriptName, scriptInstance || null);
this.fire(`destroy:${scriptName}`, scriptInstance || null);
if (scriptInstance) {
scriptInstance.fire('destroy');
}
return true;
}
/**
* Swap the script instance.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @returns {boolean} If it was successfully swapped.
* @private
*/ swap(nameOrType) {
let scriptName = nameOrType;
let scriptType = nameOrType;
// shorthand using script name
if (typeof scriptType === 'string') {
scriptType = this.system.app.scripts.get(scriptType);
} else if (scriptType) {
scriptName = scriptType.__name;
}
const old = this._scriptsIndex[scriptName];
if (!old || !old.instance) return false;
const scriptInstanceOld = old.instance;
const ind = this._scripts.indexOf(scriptInstanceOld);
const scriptInstance = new scriptType({
app: this.system.app,
entity: this.entity,
enabled: scriptInstanceOld.enabled,
attributes: scriptInstanceOld.__attributes
});
if (!scriptInstance.swap) {
return false;
}
this.initializeAttributes(scriptInstance);
// add to component
this._scripts[ind] = scriptInstance;
this._scriptsIndex[scriptName].instance = scriptInstance;
this[scriptName] = scriptInstance;
// set execution order and make sure we update
// our update and postUpdate lists
scriptInstance.__executionOrder = ind;
if (scriptInstanceOld.update) {
this._updateList.remove(scriptInstanceOld);
}
if (scriptInstanceOld.postUpdate) {
this._postUpdateList.remove(scriptInstanceOld);
}
if (scriptInstance.update) {
this._updateList.insert(scriptInstance);
}
if (scriptInstance.postUpdate) {
this._postUpdateList.insert(scriptInstance);
}
this._scriptMethod(scriptInstance, SCRIPT_SWAP, scriptInstanceOld);
this.fire('swap', scriptName, scriptInstance);
this.fire(`swap:${scriptName}`, scriptInstance);
return true;
}
/**
* When an entity is cloned and it has entity script attributes that point to other entities in
* the same subtree that is cloned, then we want the new script attributes to point at the
* cloned entities. This method remaps the script attributes for this entity and it assumes
* that this entity is the result of the clone operation.
*
* @param {ScriptComponent} oldScriptComponent - The source script component that belongs to
* the entity that was being cloned.
* @param {object} duplicatedIdsMap - A dictionary with guid-entity values that contains the
* entities that were cloned.
* @private
*/ resolveDuplicatedEntityReferenceProperties(oldScriptComponent, duplicatedIdsMap) {
const newScriptComponent = this.entity.script;
// for each script in the old component
for(const scriptName in oldScriptComponent._scriptsIndex){
// get the script type from the script registry
const scriptType = this.system.app.scripts.get(scriptName);
if (!scriptType) {
continue;
}
// get the script from the component's index
const script = oldScriptComponent._scriptsIndex[scriptName];
if (!script || !script.instance) {
continue;
}
// if __attributesRaw exists then it means that the new entity
// has not yet initialized its attributes so put the new guid in there,
// otherwise it means that the attributes have already been initialized
// so convert the new guid to an entity
// and put it in the new attributes
const newAttributesRaw = newScriptComponent[scriptName].__attributesRaw ?? newScriptComponent._attributeDataMap.get(scriptName);
const newAttributes = newScriptComponent[scriptName].__attributes;
if (!newAttributesRaw && !newAttributes) {
continue;
}
// if we are using attributesRaw then use the guid otherwise use the entity
const useGuid = !!newAttributesRaw;
// get the old script attributes from the instance
const oldAttributes = script.instance.__attributes ?? newScriptComponent._attributeDataMap.get(scriptName);
for(const attributeName in oldAttributes){
if (!oldAttributes[attributeName]) {
continue;
}
// get the attribute definition from the script type
const attribute = scriptType.attributes?.get(attributeName) ?? this.system.app.scripts.getSchema(scriptName)?.attributes?.[attributeName];
if (!attribute) {
continue;
}
if (attribute.type === 'entity') {
// entity attributes
this._resolveEntityScriptAttribute(attribute, attributeName, oldAttributes[attributeName], useGuid, newAttributesRaw || newAttributes, duplicatedIdsMap);
} else if (attribute.type === 'json' && Array.isArray(attribute.schema)) {
// json attributes
const oldValue = oldAttributes[attributeName];
const newJsonValue = newAttributesRaw ? newAttributesRaw[attributeName] : newAttributes[attributeName];
for(let i = 0; i < attribute.schema.length; i++){
const field = attribute.schema[i];
if (field.type !== 'entity') {
continue;
}
if (attribute.array) {
for(let j = 0; j < oldValue.length; j++){
this._resolveEntityScriptAttribute(field, field.name, oldValue[j][field.name], useGuid, newJsonValue[j], duplicatedIdsMap);
}
} else {
this._resolveEntityScriptAttribute(field, field.name, oldValue[field.name], useGuid, newJsonValue, duplicatedIdsMap);
}
}
}
}
}
}
/**
* Move script instance to different position to alter update order of scripts within entity.
*
* @param {string|typeof ScriptType} nameOrType - The name or type of {@link ScriptType}.
* @param {number} ind - New position index.
* @returns {boolean} If it was successfully moved.
* @example
* entity.script.move('playerController', 0);
*/ move(nameOrType, ind) {
const len = this._scripts.length;
if (ind >= len || ind < 0) {
return false;
}
let scriptType = nameOrType;
let scriptName = nameOrType;
if (typeof scriptName !== 'string') {
scriptName = nameOrType.__name;
} else {
scriptType = null;
}
const scriptData = this._scriptsIndex[scriptName];
if (!scriptData || !scriptData.instance) {
return false;
}
// if script type specified, make sure instance of said type
const scriptInstance = scriptData.instance;
if (scriptType && !(scriptInstance instanceof scriptType)) {
return false;
}
const indOld = this._scripts.indexOf(scriptInstance);
if (indOld === -1 || indOld === ind) {
return false;
}
// move script to another position
this._scripts.splice(ind, 0, this._scripts.splice(indOld, 1)[0]);
// reset execution order for scripts and re-sort update and postUpdate lists
this._resetExecutionOrder(0, len);
this._updateList.sort();
this._postUpdateList.sort();
this.fire('move', scriptName, scriptInstance, ind, indOld);
this.fire(`move:${scriptName}`, scriptInstance, ind, indOld);
return true;
}
}
export { ScriptComponent };