UNPKG

dcl-npc-toolkit-ai-version

Version:

A collection of tools for creating Non-Player-Characters (NPCs). These are capable of having conversations with the player, and play different animations. AI usage is added atop of it

802 lines 34.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.Schema = void 0; const spec_1 = require("./spec"); const annotations_1 = require("./annotations"); const encode = require("./encoding/encode"); const decode = require("./encoding/decode"); const ArraySchema_1 = require("./types/ArraySchema"); const MapSchema_1 = require("./types/MapSchema"); const CollectionSchema_1 = require("./types/CollectionSchema"); const SetSchema_1 = require("./types/SetSchema"); const ChangeTree_1 = require("./changes/ChangeTree"); const filters_1 = require("./filters"); const typeRegistry_1 = require("./types/typeRegistry"); const ReferenceTracker_1 = require("./changes/ReferenceTracker"); const utils_1 = require("./types/utils"); class EncodeSchemaError extends Error { } function assertType(value, type, klass, field) { let typeofTarget; let allowNull = false; switch (type) { case "number": case "int8": case "uint8": case "int16": case "uint16": case "int32": case "uint32": case "int64": case "uint64": case "float32": case "float64": typeofTarget = "number"; if (isNaN(value)) { console.log(`trying to encode "NaN" in ${klass.constructor.name}#${field}`); } break; case "string": typeofTarget = "string"; allowNull = true; break; case "boolean": // boolean is always encoded as true/false based on truthiness return; } if (typeof (value) !== typeofTarget && (!allowNull || (allowNull && value !== null))) { let foundValue = `'${JSON.stringify(value)}'${(value && value.constructor && ` (${value.constructor.name})`) || ''}`; throw new EncodeSchemaError(`a '${typeofTarget}' was expected, but ${foundValue} was provided in ${klass.constructor.name}#${field}`); } } function assertInstanceType(value, type, klass, field) { if (!(value instanceof type)) { throw new EncodeSchemaError(`a '${type.name}' was expected, but '${value.constructor.name}' was provided in ${klass.constructor.name}#${field}`); } } function encodePrimitiveType(type, bytes, value, klass, field) { assertType(value, type, klass, field); const encodeFunc = encode[type]; if (encodeFunc) { encodeFunc(bytes, value); } else { throw new EncodeSchemaError(`a '${type}' was expected, but ${value} was provided in ${klass.constructor.name}#${field}`); } } function decodePrimitiveType(type, bytes, it) { return decode[type](bytes, it); } /** * Schema encoder / decoder */ class Schema { static { this._definition = annotations_1.SchemaDefinition.create(); } static onError(e) { console.error(e); } static is(type) { return (type['_definition'] && type['_definition'].schema !== undefined); } onChange(callback) { return (0, utils_1.addCallback)((this.$callbacks || (this.$callbacks = {})), spec_1.OPERATION.REPLACE, callback); } onRemove(callback) { return (0, utils_1.addCallback)((this.$callbacks || (this.$callbacks = {})), spec_1.OPERATION.DELETE, callback); } // allow inherited classes to have a constructor constructor(...args) { // fix enumerability of fields for end-user Object.defineProperties(this, { $changes: { value: new ChangeTree_1.ChangeTree(this, undefined, new ReferenceTracker_1.ReferenceTracker()), enumerable: false, writable: true }, // $listeners: { // value: undefined, // enumerable: false, // writable: true // }, $callbacks: { value: undefined, enumerable: false, writable: true }, }); const descriptors = this._definition.descriptors; if (descriptors) { Object.defineProperties(this, descriptors); } // // Assign initial values // if (args[0]) { this.assign(args[0]); } } assign(props) { Object.assign(this, props); return this; } get _definition() { return this.constructor._definition; } /** * (Server-side): Flag a property to be encoded for the next patch. * @param instance Schema instance * @param property string representing the property name, or number representing the index of the property. * @param operation OPERATION to perform (detected automatically) */ setDirty(property, operation) { this.$changes.change(property, operation); } /** * Client-side: listen for changes on property. * @param prop the property name * @param callback callback to be triggered on property change * @param immediate trigger immediatelly if property has been already set. */ listen(prop, callback, immediate = true) { if (!this.$callbacks) { this.$callbacks = {}; } if (!this.$callbacks[prop]) { this.$callbacks[prop] = []; } this.$callbacks[prop].push(callback); if (immediate && this[prop] !== undefined) { callback(this[prop], undefined); } // return un-register callback. return () => (0, utils_1.spliceOne)(this.$callbacks[prop], this.$callbacks[prop].indexOf(callback)); } decode(bytes, it = { offset: 0 }, ref = this) { const allChanges = []; const $root = this.$changes.root; const totalBytes = bytes.length; let refId = 0; $root.refs.set(refId, this); while (it.offset < totalBytes) { let byte = bytes[it.offset++]; if (byte == spec_1.SWITCH_TO_STRUCTURE) { refId = decode.number(bytes, it); const nextRef = $root.refs.get(refId); // // Trying to access a reference that haven't been decoded yet. // if (!nextRef) { throw new Error(`"refId" not found: ${refId}`); } ref = nextRef; continue; } const changeTree = ref['$changes']; const isSchema = (ref['_definition'] !== undefined); const operation = (isSchema) ? (byte >> 6) << 6 // "compressed" index + operation : byte; // "uncompressed" index + operation (array/map items) if (operation === spec_1.OPERATION.CLEAR) { // // TODO: refactor me! // The `.clear()` method is calling `$root.removeRef(refId)` for // each item inside this collection // ref.clear(allChanges); continue; } const fieldIndex = (isSchema) ? byte % (operation || 255) // if "REPLACE" operation (0), use 255 : decode.number(bytes, it); const fieldName = (isSchema) ? (ref['_definition'].fieldsByIndex[fieldIndex]) : ""; let type = changeTree.getType(fieldIndex); let value; let previousValue; let dynamicIndex; if (!isSchema) { previousValue = ref['getByIndex'](fieldIndex); if ((operation & spec_1.OPERATION.ADD) === spec_1.OPERATION.ADD) { // ADD or DELETE_AND_ADD dynamicIndex = (ref instanceof MapSchema_1.MapSchema) ? decode.string(bytes, it) : fieldIndex; ref['setIndex'](fieldIndex, dynamicIndex); } else { // here dynamicIndex = ref['getIndex'](fieldIndex); } } else { previousValue = ref[`_${fieldName}`]; } // // Delete operations // if ((operation & spec_1.OPERATION.DELETE) === spec_1.OPERATION.DELETE) { if (operation !== spec_1.OPERATION.DELETE_AND_ADD) { ref['deleteByIndex'](fieldIndex); } // Flag `refId` for garbage collection. if (previousValue && previousValue['$changes']) { $root.removeRef(previousValue['$changes'].refId); } value = null; } if (fieldName === undefined) { console.warn("@colyseus/schema: definition mismatch"); // // keep skipping next bytes until reaches a known structure // by local decoder. // const nextIterator = { offset: it.offset }; while (it.offset < totalBytes) { if (decode.switchStructureCheck(bytes, it)) { nextIterator.offset = it.offset + 1; if ($root.refs.has(decode.number(bytes, nextIterator))) { break; } } it.offset++; } continue; } else if (operation === spec_1.OPERATION.DELETE) { // // FIXME: refactor me. // Don't do anything. // } else if (Schema.is(type)) { const refId = decode.number(bytes, it); value = $root.refs.get(refId); if (operation !== spec_1.OPERATION.REPLACE) { const childType = this.getSchemaType(bytes, it, type); if (!value) { value = this.createTypeInstance(childType); value.$changes.refId = refId; if (previousValue) { value.$callbacks = previousValue.$callbacks; // value.$listeners = previousValue.$listeners; if (previousValue['$changes'].refId && refId !== previousValue['$changes'].refId) { $root.removeRef(previousValue['$changes'].refId); } } } $root.addRef(refId, value, (value !== previousValue)); } } else if (typeof (type) === "string") { // // primitive value (number, string, boolean, etc) // value = decodePrimitiveType(type, bytes, it); } else { const typeDef = (0, typeRegistry_1.getType)(Object.keys(type)[0]); const refId = decode.number(bytes, it); const valueRef = ($root.refs.has(refId)) ? previousValue || $root.refs.get(refId) : new typeDef.constructor(); value = valueRef.clone(true); value.$changes.refId = refId; // preserve schema callbacks if (previousValue) { value['$callbacks'] = previousValue['$callbacks']; if (previousValue['$changes'].refId && refId !== previousValue['$changes'].refId) { $root.removeRef(previousValue['$changes'].refId); // // Trigger onRemove if structure has been replaced. // const entries = previousValue.entries(); let iter; while ((iter = entries.next()) && !iter.done) { const [key, value] = iter.value; allChanges.push({ refId, op: spec_1.OPERATION.DELETE, field: key, value: undefined, previousValue: value, }); } } } $root.addRef(refId, value, (valueRef !== previousValue)); } if (value !== null && value !== undefined) { if (value['$changes']) { value['$changes'].setParent(changeTree.ref, changeTree.root, fieldIndex); } if (ref instanceof Schema) { ref[fieldName] = value; // ref[`_${fieldName}`] = value; } else if (ref instanceof MapSchema_1.MapSchema) { // const key = ref['$indexes'].get(field); const key = dynamicIndex; // ref.set(key, value); ref['$items'].set(key, value); ref['$changes'].allChanges.add(fieldIndex); } else if (ref instanceof ArraySchema_1.ArraySchema) { // const key = ref['$indexes'][field]; // console.log("SETTING FOR ArraySchema =>", { field, key, value }); // ref[key] = value; ref.setAt(fieldIndex, value); } else if (ref instanceof CollectionSchema_1.CollectionSchema) { const index = ref.add(value); ref['setIndex'](fieldIndex, index); } else if (ref instanceof SetSchema_1.SetSchema) { const index = ref.add(value); if (index !== false) { ref['setIndex'](fieldIndex, index); } } } if (previousValue !== value) { allChanges.push({ refId, op: operation, field: fieldName, dynamicIndex, value, previousValue, }); } } this._triggerChanges(allChanges); // drop references of unused schemas $root.garbageCollectDeletedRefs(); return allChanges; } encode(encodeAll = false, bytes = [], useFilters = false) { const rootChangeTree = this.$changes; const refIdsVisited = new WeakSet(); const changeTrees = [rootChangeTree]; let numChangeTrees = 1; for (let i = 0; i < numChangeTrees; i++) { const changeTree = changeTrees[i]; const ref = changeTree.ref; const isSchema = (ref instanceof Schema); // Generate unique refId for the ChangeTree. changeTree.ensureRefId(); // mark this ChangeTree as visited. refIdsVisited.add(changeTree); // root `refId` is skipped. if (changeTree !== rootChangeTree && (changeTree.changed || encodeAll)) { encode.uint8(bytes, spec_1.SWITCH_TO_STRUCTURE); encode.number(bytes, changeTree.refId); } const changes = (encodeAll) ? Array.from(changeTree.allChanges) : Array.from(changeTree.changes.values()); for (let j = 0, cl = changes.length; j < cl; j++) { const operation = (encodeAll) ? { op: spec_1.OPERATION.ADD, index: changes[j] } : changes[j]; const fieldIndex = operation.index; const field = (isSchema) ? ref['_definition'].fieldsByIndex && ref['_definition'].fieldsByIndex[fieldIndex] : fieldIndex; // cache begin index if `useFilters` const beginIndex = bytes.length; // encode field index + operation if (operation.op !== spec_1.OPERATION.TOUCH) { if (isSchema) { // // Compress `fieldIndex` + `operation` into a single byte. // This adds a limitaion of 64 fields per Schema structure // encode.uint8(bytes, (fieldIndex | operation.op)); } else { encode.uint8(bytes, operation.op); // custom operations if (operation.op === spec_1.OPERATION.CLEAR) { continue; } // indexed operations encode.number(bytes, fieldIndex); } } // // encode "alias" for dynamic fields (maps) // if (!isSchema && (operation.op & spec_1.OPERATION.ADD) == spec_1.OPERATION.ADD // ADD or DELETE_AND_ADD ) { if (ref instanceof MapSchema_1.MapSchema) { // // MapSchema dynamic key // const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex); encode.string(bytes, dynamicIndex); } } if (operation.op === spec_1.OPERATION.DELETE) { // // TODO: delete from filter cache data. // // if (useFilters) { // delete changeTree.caches[fieldIndex]; // } continue; } // const type = changeTree.childType || ref._schema[field]; const type = changeTree.getType(fieldIndex); // const type = changeTree.getType(fieldIndex); const value = changeTree.getValue(fieldIndex); // Enqueue ChangeTree to be visited if (value && value['$changes'] && !refIdsVisited.has(value['$changes'])) { changeTrees.push(value['$changes']); value['$changes'].ensureRefId(); numChangeTrees++; } if (operation.op === spec_1.OPERATION.TOUCH) { continue; } if (Schema.is(type)) { assertInstanceType(value, type, ref, field); // // Encode refId for this instance. // The actual instance is going to be encoded on next `changeTree` iteration. // encode.number(bytes, value.$changes.refId); // Try to encode inherited TYPE_ID if it's an ADD operation. if ((operation.op & spec_1.OPERATION.ADD) === spec_1.OPERATION.ADD) { this.tryEncodeTypeId(bytes, type, value.constructor); } } else if (typeof (type) === "string") { // // Primitive values // encodePrimitiveType(type, bytes, value, ref, field); } else { // // Custom type (MapSchema, ArraySchema, etc) // const definition = (0, typeRegistry_1.getType)(Object.keys(type)[0]); // // ensure a ArraySchema has been provided // assertInstanceType(ref[`_${field}`], definition.constructor, ref, field); // // Encode refId for this instance. // The actual instance is going to be encoded on next `changeTree` iteration. // encode.number(bytes, value.$changes.refId); } if (useFilters) { // cache begin / end index changeTree.cache(fieldIndex, bytes.slice(beginIndex)); } } if (!encodeAll && !useFilters) { changeTree.discard(); } } return bytes; } encodeAll(useFilters) { return this.encode(true, [], useFilters); } applyFilters(client, encodeAll = false) { const root = this; const refIdsDissallowed = new Set(); const $filterState = filters_1.ClientState.get(client); const changeTrees = [this.$changes]; let numChangeTrees = 1; let filteredBytes = []; for (let i = 0; i < numChangeTrees; i++) { const changeTree = changeTrees[i]; if (refIdsDissallowed.has(changeTree.refId)) { // console.log("REFID IS NOT ALLOWED. SKIP.", { refId: changeTree.refId }) continue; } const ref = changeTree.ref; const isSchema = ref instanceof Schema; encode.uint8(filteredBytes, spec_1.SWITCH_TO_STRUCTURE); encode.number(filteredBytes, changeTree.refId); const clientHasRefId = $filterState.refIds.has(changeTree); const isEncodeAll = (encodeAll || !clientHasRefId); // console.log("REF:", ref.constructor.name); // console.log("Encode all?", isEncodeAll); // // include `changeTree` on list of known refIds by this client. // $filterState.addRefId(changeTree); const containerIndexes = $filterState.containerIndexes.get(changeTree); const changes = (isEncodeAll) ? Array.from(changeTree.allChanges) : Array.from(changeTree.changes.values()); // // WORKAROUND: tries to re-evaluate previously not included @filter() attributes // - see "DELETE a field of Schema" test case. // if (!encodeAll && isSchema && ref._definition.indexesWithFilters) { const indexesWithFilters = ref._definition.indexesWithFilters; indexesWithFilters.forEach(indexWithFilter => { if (!containerIndexes.has(indexWithFilter) && changeTree.allChanges.has(indexWithFilter)) { if (isEncodeAll) { changes.push(indexWithFilter); } else { changes.push({ op: spec_1.OPERATION.ADD, index: indexWithFilter, }); } } }); } for (let j = 0, cl = changes.length; j < cl; j++) { const change = (isEncodeAll) ? { op: spec_1.OPERATION.ADD, index: changes[j] } : changes[j]; // custom operations if (change.op === spec_1.OPERATION.CLEAR) { encode.uint8(filteredBytes, change.op); continue; } const fieldIndex = change.index; // // Deleting fields: encode the operation + field index // if (change.op === spec_1.OPERATION.DELETE) { // // DELETE operations also need to go through filtering. // // TODO: cache the previous value so we can access the value (primitive or `refId`) // (check against `$filterState.refIds`) // if (isSchema) { encode.uint8(filteredBytes, change.op | fieldIndex); } else { encode.uint8(filteredBytes, change.op); encode.number(filteredBytes, fieldIndex); } continue; } // indexed operation const value = changeTree.getValue(fieldIndex); const type = changeTree.getType(fieldIndex); if (isSchema) { // Is a Schema! const filter = (ref._definition.filters && ref._definition.filters[fieldIndex]); if (filter && !filter.call(ref, client, value, root)) { if (value && value['$changes']) { refIdsDissallowed.add(value['$changes'].refId); ; } continue; } } else { // Is a collection! (map, array, etc.) const parent = changeTree.parent; const filter = changeTree.getChildrenFilter(); if (filter && !filter.call(parent, client, ref['$indexes'].get(fieldIndex), value, root)) { if (value && value['$changes']) { refIdsDissallowed.add(value['$changes'].refId); } continue; } } // visit child ChangeTree on further iteration. if (value['$changes']) { changeTrees.push(value['$changes']); numChangeTrees++; } // // Copy cached bytes // if (change.op !== spec_1.OPERATION.TOUCH) { // // TODO: refactor me! // if (change.op === spec_1.OPERATION.ADD || isSchema) { // // use cached bytes directly if is from Schema type. // filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []); containerIndexes.add(fieldIndex); } else { if (containerIndexes.has(fieldIndex)) { // // use cached bytes if already has the field // filteredBytes.push.apply(filteredBytes, changeTree.caches[fieldIndex] ?? []); } else { // // force ADD operation if field is not known by this client. // containerIndexes.add(fieldIndex); encode.uint8(filteredBytes, spec_1.OPERATION.ADD); encode.number(filteredBytes, fieldIndex); if (ref instanceof MapSchema_1.MapSchema) { // // MapSchema dynamic key // const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex); encode.string(filteredBytes, dynamicIndex); } if (value['$changes']) { encode.number(filteredBytes, value['$changes'].refId); } else { // "encodePrimitiveType" without type checking. // the type checking has been done on the first .encode() call. encode[type](filteredBytes, value); } } } } else if (value['$changes'] && !isSchema) { // // TODO: // - track ADD/REPLACE/DELETE instances on `$filterState` // - do NOT always encode dynamicIndex for MapSchema. // (If client already has that key, only the first index is necessary.) // encode.uint8(filteredBytes, spec_1.OPERATION.ADD); encode.number(filteredBytes, fieldIndex); if (ref instanceof MapSchema_1.MapSchema) { // // MapSchema dynamic key // const dynamicIndex = changeTree.ref['$indexes'].get(fieldIndex); encode.string(filteredBytes, dynamicIndex); } encode.number(filteredBytes, value['$changes'].refId); } } ; } return filteredBytes; } clone() { const cloned = new (this.constructor); const schema = this._definition.schema; for (let field in schema) { if (typeof (this[field]) === "object" && typeof (this[field]?.clone) === "function") { // deep clone cloned[field] = this[field].clone(); } else { // primitive values cloned[field] = this[field]; } } return cloned; } toJSON() { const schema = this._definition.schema; const deprecated = this._definition.deprecated; const obj = {}; for (let field in schema) { if (!deprecated[field] && this[field] !== null && typeof (this[field]) !== "undefined") { obj[field] = (typeof (this[field]['toJSON']) === "function") ? this[field]['toJSON']() : this[`_${field}`]; } } return obj; } discardAllChanges() { this.$changes.discardAll(); } getByIndex(index) { return this[this._definition.fieldsByIndex[index]]; } deleteByIndex(index) { this[this._definition.fieldsByIndex[index]] = undefined; } tryEncodeTypeId(bytes, type, targetType) { if (type._typeid !== targetType._typeid) { encode.uint8(bytes, spec_1.TYPE_ID); encode.number(bytes, targetType._typeid); } } getSchemaType(bytes, it, defaultType) { let type; if (bytes[it.offset] === spec_1.TYPE_ID) { it.offset++; type = this.constructor._context.get(decode.number(bytes, it)); } return type || defaultType; } createTypeInstance(type) { let instance = new type(); // assign root on $changes instance.$changes.root = this.$changes.root; return instance; } _triggerChanges(changes) { const uniqueRefIds = new Set(); const $refs = this.$changes.root.refs; for (let i = 0; i < changes.length; i++) { const change = changes[i]; const refId = change.refId; const ref = $refs.get(refId); const $callbacks = ref['$callbacks']; // // trigger onRemove on child structure. // if ((change.op & spec_1.OPERATION.DELETE) === spec_1.OPERATION.DELETE && change.previousValue instanceof Schema) { change.previousValue['$callbacks']?.[spec_1.OPERATION.DELETE]?.forEach(callback => callback()); } // no callbacks defined, skip this structure! if (!$callbacks) { continue; } if (ref instanceof Schema) { if (!uniqueRefIds.has(refId)) { try { // trigger onChange $callbacks?.[spec_1.OPERATION.REPLACE]?.forEach(callback => callback()); } catch (e) { Schema.onError(e); } } try { if ($callbacks.hasOwnProperty(change.field)) { $callbacks[change.field]?.forEach((callback) => callback(change.value, change.previousValue)); } } catch (e) { Schema.onError(e); } } else { // is a collection of items if (change.op === spec_1.OPERATION.ADD && change.previousValue === undefined) { // triger onAdd $callbacks[spec_1.OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field)); } else if (change.op === spec_1.OPERATION.DELETE) { // // FIXME: `previousValue` should always be available. // ADD + DELETE operations are still encoding DELETE operation. // if (change.previousValue !== undefined) { // triger onRemove $callbacks[spec_1.OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field)); } } else if (change.op === spec_1.OPERATION.DELETE_AND_ADD) { // triger onRemove if (change.previousValue !== undefined) { $callbacks[spec_1.OPERATION.DELETE]?.forEach(callback => callback(change.previousValue, change.dynamicIndex ?? change.field)); } // triger onAdd $callbacks[spec_1.OPERATION.ADD]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field)); } // trigger onChange if (change.value !== change.previousValue) { $callbacks[spec_1.OPERATION.REPLACE]?.forEach(callback => callback(change.value, change.dynamicIndex ?? change.field)); } } uniqueRefIds.add(refId); } } } exports.Schema = Schema; //# sourceMappingURL=Schema.js.map