Skip to content

Commit

Permalink
Cherry pick -> ESM Script base class
Browse files Browse the repository at this point in the history
  • Loading branch information
marklundin committed Sep 12, 2024
1 parent e953358 commit 3955e81
Show file tree
Hide file tree
Showing 9 changed files with 648 additions and 482 deletions.
81 changes: 71 additions & 10 deletions src/framework/components/script/component.js
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import { Debug } from '../../../core/debug.js';
import { SortedLoopArray } from '../../../core/sorted-loop-array.js';

import { ScriptAttributes } from '../../script/script-attributes.js';
import { ScriptAttributes, assignAttributesToScript } from '../../script/script-attributes.js';
import {
SCRIPT_INITIALIZE, SCRIPT_POST_INITIALIZE, SCRIPT_UPDATE,
SCRIPT_POST_UPDATE, SCRIPT_SWAP
} from '../../script/constants.js';

import { Component } from '../component.js';
import { Entity } from '../../entity.js';
import { ScriptType } from '../../script/script-type.js';
import { getScriptName } from '../../script/script.js';

const toLowerCamelCase = str => str[0].toLowerCase() + str.substring(1);

/**
* The ScriptComponent allows you to extend the functionality of an Entity by attaching your own
Expand All @@ -18,6 +22,14 @@ import { Entity } from '../../entity.js';
* @category Script
*/
class ScriptComponent extends Component {
/**
* A map of script name to initial component data.
*
* @type {Map<string, object>}
* @private
*/
_attributeDataMap = new Map();

/**
* 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:
Expand Down Expand Up @@ -191,7 +203,7 @@ class ScriptComponent extends Component {
* Sets the array of all script instances attached to an entity. This array is read-only and
* should not be modified by developer.
*
* @type {import('../../script/script-type.js').ScriptType[]}
* @type {import('../../script/script.js').Script[]}
*/
set scripts(value) {
this._scriptsData = value;
Expand All @@ -207,6 +219,11 @@ class ScriptComponent extends Component {

// 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;
}

Expand Down Expand Up @@ -336,6 +353,9 @@ class ScriptComponent extends Component {

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;
}

Expand Down Expand Up @@ -375,7 +395,38 @@ class ScriptComponent extends Component {

_onInitializeAttributes() {
for (let i = 0, len = this.scripts.length; i < len; i++) {
this.scripts[i].__initializeAttributes();
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);

}
}

Expand Down Expand Up @@ -607,8 +658,8 @@ class ScriptComponent extends Component {
/**
* Create a script instance and attach to an entity script component.
*
* @param {string|typeof import('../../script/script-type.js').ScriptType} nameOrType - The
* name or type of {@link ScriptType}.
* @param {string|typeof import('../../script/script.js').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.
Expand Down Expand Up @@ -639,7 +690,7 @@ class ScriptComponent extends Component {
if (typeof scriptType === 'string') {
scriptType = this.system.app.scripts.get(scriptType);
} else if (scriptType) {
scriptName = scriptType.__name;
scriptName = scriptType.__name ?? toLowerCamelCase(getScriptName(scriptType));
}

if (scriptType) {
Expand All @@ -652,6 +703,15 @@ class ScriptComponent extends Component {
attributes: args.attributes
});


// 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) {
Expand All @@ -669,9 +729,8 @@ class ScriptComponent extends Component {

this[scriptName] = scriptInstance;

if (!args.preloading) {
scriptInstance.__initializeAttributes();
}
if (!args.preloading)
this.initializeAttributes(scriptInstance);

this.fire('create', scriptName, scriptInstance);
this.fire(`create:${scriptName}`, scriptInstance);
Expand Down Expand Up @@ -737,6 +796,8 @@ class ScriptComponent extends Component {
delete this._scriptsIndex[scriptName];
if (!scriptData) return false;

this._attributeDataMap.delete(scriptName);

const scriptInstance = scriptData.instance;
if (scriptInstance && !scriptInstance._destroyed) {
scriptInstance.enabled = false;
Expand Down Expand Up @@ -807,7 +868,7 @@ class ScriptComponent extends Component {
return false;
}

scriptInstance.__initializeAttributes();
this.initializeAttributes(scriptInstance);

// add to component
this._scripts[ind] = scriptInstance;
Expand Down
26 changes: 13 additions & 13 deletions src/framework/handlers/script.js
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
import { platform } from '../../core/platform.js';
import { script } from '../script.js';
import { ScriptType } from '../script/script-type.js';
import { ScriptTypes } from '../script/script-types.js';
import { registerScript } from '../script/script.js';
import { registerScript } from '../script/script-create.js';
import { ResourceLoader } from './loader.js';

import { ResourceHandler } from './handler.js';
import { ScriptAttributes } from '../script/script-attributes.js';
import { Script } from '../script/script.js';

const toLowerCamelCase = str => str[0].toLowerCase() + str.substring(1);

Expand Down Expand Up @@ -154,21 +153,22 @@ class ScriptHandler extends ResourceHandler {
// @ts-ignore
import(importUrl.toString()).then((module) => {

const filename = importUrl.pathname.split('/').pop();
const scriptSchema = this._app.assets.find(filename, 'script').data.scripts;

for (const key in module) {
const scriptClass = module[key];
const extendsScriptType = scriptClass.prototype instanceof ScriptType;
const extendsScriptType = scriptClass.prototype instanceof Script;

if (extendsScriptType) {

// Check if attributes is defined directly on the class and not inherited
if (scriptClass.hasOwnProperty('attributes')) {
const attributes = new ScriptAttributes(scriptClass);
for (const key in scriptClass.attributes) {
attributes.add(key, scriptClass.attributes[key]);
}
scriptClass.attributes = attributes;
}
registerScript(scriptClass, toLowerCamelCase(scriptClass.name));
const scriptName = toLowerCamelCase(scriptClass.name);

// Register the script name
registerScript(scriptClass, scriptName);

// Store any schema associated with the script
this._app.scripts.addSchema(scriptName, scriptSchema[scriptName]);
}
}

Expand Down
48 changes: 46 additions & 2 deletions src/framework/script/script-attributes.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ import { CurveSet } from '../../core/math/curve-set.js';
import { Vec2 } from '../../core/math/vec2.js';
import { Vec3 } from '../../core/math/vec3.js';
import { Vec4 } from '../../core/math/vec4.js';

import { GraphNode } from '../../scene/graph-node.js';

import { Asset } from '../asset/asset.js';

const components = ['x', 'y', 'z', 'w'];
Expand Down Expand Up @@ -152,6 +150,50 @@ function rawToValue(app, args, value, old) {
return value;
}

/**
* @typedef {Object} AttributeSchema
* @property {"boolean"|"number"|"string"|"json"|"asset"|"entity"|"rgb"|"rgba"|"vec2"|"vec3"|"vec4"|"curve"} type - The Attribute type
* @property {boolean} [array] - True if this attribute is an array of `type`
*/

/**
* Takes an attribute schema, a value and current value, and return a new value.
*
* @param {import('../../framework/application.js').Application} app - The working application
* @param {AttributeSchema} schema - The attribute schema used to resolve properties
* @param {*} value - The raw value to create
* @param {*} current - The existing value
* @returns {*} The return value
*/
function attributeToValue(app, schema, value, current) {
if (schema.array) {
return value.map((item, index) => rawToValue(app, schema, item, current ? current[index] : null));
}

return rawToValue(app, schema, value, current);
}

/**
* Assigns values to a script instance based on a map of attributes schemas
* and a corresponding map of data.
*
* @param {import('../../framework/application.js').Application} app - The application instance
* @param {Object<string, AttributeSchema>} attributeSchemaMap - A map of names to Schemas
* @param {Object<string, *>} data - A Map of data to assign to the Script instance
* @param {import('../../framework/script/script.js').Script} script - A Script instance to assign values on
*/
export function assignAttributesToScript(app, attributeSchemaMap, data, script) {

// Iterate over the schema and assign corresponding data
for (const attributeName in attributeSchemaMap) {
const attributeSchema = attributeSchemaMap[attributeName];
const dataToAssign = data[attributeName];

// Assign the value to the script based on the attribute schema
script[attributeName] = attributeToValue(app, attributeSchema, dataToAssign, script);
}
}

/**
* Container of Script Attribute definitions. Implements an interface to add/remove attributes and
* store their definition for a {@link ScriptType}. Note: An instance of ScriptAttributes is
Expand All @@ -160,6 +202,8 @@ function rawToValue(app, args, value, old) {
* @category Script
*/
class ScriptAttributes {
static assignAttributesToScript = assignAttributesToScript;

/**
* Create a new ScriptAttributes instance.
*
Expand Down
Loading

0 comments on commit 3955e81

Please sign in to comment.