Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"generate-test-10": "bin/schema-codegen test-external/Callbacks.ts --namespace SchemaTest.Callbacks --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/Callbacks",
"generate-test-11": "bin/schema-codegen test-external/MapSchemaMoveNullifyType.ts --namespace SchemaTest.MapSchemaMoveNullifyType --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/MapSchemaMoveNullifyType",
"generate-test-12": "bin/schema-codegen test-external/ArraySchemaClear --namespace SchemaTest.ArraySchemaClear --output ../colyseus-unity-sdk/Assets/Colyseus/Tests/Editor/ColyseusTests/Schema/ArraySchemaClear",
"prepublishOnly": "npm run build"
"prepare": "npm run build"
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Happy to undo this if this PR is going to be merged. I needed my fork to build on install so I can use it directly from github.

},
"files": [
"src",
Expand Down
6 changes: 3 additions & 3 deletions src/encoder/ChangeTree.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { OPERATION } from "../encoding/spec.js";
import { Schema } from "../Schema.js";
import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refId, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex } from "../types/symbols.js";
import { $changes, $childType, $decoder, $onEncodeEnd, $encoder, $getByIndex, $refId, $refTypeFieldIndexes, $viewFieldIndexes, type $deleteByIndex, $inheritVisibility } from "../types/symbols.js";

import type { MapSchema } from "../types/custom/MapSchema.js";
import type { ArraySchema } from "../types/custom/ArraySchema.js";
Expand Down Expand Up @@ -568,7 +568,7 @@ export class ChangeTree<T extends Ref = any> {
|| fieldHasViewTag;

//
// "isFiltered" may not be imedialely available during `change()` due to the instance not being attached to the root yet.
// "isFiltered" may not be immediately available during `change()` due to the instance not being attached to the root yet.
// when it's available, we need to enqueue the "changes" changeset into the "filteredChanges" changeset.
//
if (this.isFiltered) {
Expand All @@ -577,7 +577,7 @@ export class ChangeTree<T extends Ref = any> {
parentChangeTree.isFiltered &&
typeof (refType) !== "string" &&
!fieldHasViewTag &&
parentIsCollection
(parentIsCollection || (refType as any)?.[$inheritVisibility] === true)
Comment thread
FTWinston marked this conversation as resolved.
Outdated
);

if (!this.filteredChanges) {
Expand Down
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ registerType("collection", { constructor: CollectionSchema, });

// Utils
export { dumpChanges } from "./utils.js";
export { inheritVisibility } from "./types/utils.js";

// Encoder / Decoder
export { $track, $encoder, $decoder, $filter, $getByIndex, $deleteByIndex, $changes, $childType, $refId } from "./types/symbols.js";
Expand Down
6 changes: 6 additions & 0 deletions src/types/symbols.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,12 @@ export const $onEncodeEnd = '~onEncodeEnd';
*/
export const $onDecodeEnd = "~onDecodeEnd";

/**
* Used to mark that a Schema class should be visible whenever it's parent is visible, even if it doesn't have any view tags itself.
* This is used for "nested" Schema classes that are only used as fields of other Schema classes.
*/
export const $inheritVisibility = '~inheritVisibility';

/**
* Metadata
*/
Expand Down
12 changes: 11 additions & 1 deletion src/types/utils.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
import { Schema } from '../Schema.js';
import { $inheritVisibility } from './symbols.js';

export function spliceOne(arr: any[], index: number): boolean {
// manually splice an array
if (index === -1 || index >= arr.length) {
Expand All @@ -13,4 +16,11 @@ export function spliceOne(arr: any[], index: number): boolean {
arr.length = len;

return true;
}
}

/** Mark a Schema class so its instances share visibility with their Schema parent,
* the same way that ArraySchema (etc) items do. */
export function inheritVisibility<T extends new (...args: any[]) => Schema>(constructor: T): T {
(constructor as any)[$inheritVisibility] = true;
return constructor;
}
113 changes: 112 additions & 1 deletion test/Schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
// import "core-js";

import * as assert from "assert";
import { State, Player, DeepState, DeepMap, DeepChild, Position, DeepEntity, assertDeepStrictEqualEncodeAll, createInstanceFromReflection, getEncoder } from "./Schema";
import { State, Player, DeepState, DeepMap, DeepChild, Position, assertDeepStrictEqualEncodeAll, createInstanceFromReflection, getEncoder, InheritedPosition, InheritanceRoot, createClientWithView, encodeMultiple } from "./Schema";
import { Schema, ArraySchema, MapSchema, type, Metadata, $changes, Encoder, Decoder, SetSchema, schema, ToJSON, $refId } from "../src";
import { getNormalizedType } from "../src/Metadata";

Expand Down Expand Up @@ -1017,6 +1017,117 @@ describe("Type: Schema", () => {
assert.deepStrictEqual(decodedState.mapOfNumbers.toJSON(), { 'zero': 0, 'one': 1, 'two': 2, 'three': 3 });
});

it("should not encode nested Schema that wasn't initially assigned", () => {
const state = new InheritanceRoot();
const encoder = getEncoder(state);

const client = createClientWithView(state);
client.view.add(state.parent);

// Initial encode: child property is undefined
encodeMultiple(encoder, state, [client]);

// Assign a child Schema that does NOT use @inheritVisibility
state.parent.standardChild = new Position(1, 2, 3);

/**
* Encode an assignment of a child field:
* since the "standardChild" field (Position) does not use
* @inheritVisibility, its fields are not visible to the client
* even though the parent InheritanceParent is visible.
*/
encodeMultiple(encoder, state, [client]);

assert.notStrictEqual(client.state.parent, undefined);
assert.notStrictEqual(client.state.parent.standardChild, undefined);
assert.strictEqual(client.state.parent.standardChild.x, undefined);
assert.strictEqual(client.state.parent.standardChild.y, undefined);
assert.strictEqual(client.state.parent.standardChild.z, undefined);
});

it("should not encode nested Schema with @view that wasn't initially assigned", () => {
const state = new InheritanceRoot();
const encoder = getEncoder(state);

const client = createClientWithView(state);
client.view.add(state.parent);

// Initial encode: child property is undefined
encodeMultiple(encoder, state, [client]);

// Assign a child Schema that uses @view
state.parent.viewChild = new Position(1, 2, 3);

/**
* Encode an assignment of a child field:
* the child "viewChild" field (Position) is marked with @view,
* so it shares visibility with its parent and
* its fields are encoded for the client.
*/
encodeMultiple(encoder, state, [client]);

assert.notStrictEqual(client.state.parent, undefined);
assert.notStrictEqual(client.state.parent.viewChild, undefined);
assert.strictEqual(client.state.parent.viewChild.x, undefined);
assert.strictEqual(client.state.parent.viewChild.y, undefined);
assert.strictEqual(client.state.parent.viewChild.z, undefined);
});

it("should encode nested Schema wrapped in ArraySchema that wasn't initially assigned", () => {
const state = new InheritanceRoot();
const encoder = getEncoder(state);

const client = createClientWithView(state);
client.view.add(state.parent);

// Initial encode: child property is undefined
encodeMultiple(encoder, state, [client]);

// Assign a child Schema wrapped in an ArraySchema, to demonstrate this workaround.
state.parent.arrayChild.push(new Position(1, 2, 3));

/**
* Encode an assignment of a child field wrapped in an ArraySchema
* the child "arrayChild" field (Position), being in an ArraySchema,
* shares visibility with its parent and its fields are encoded for the client.
*/
encodeMultiple(encoder, state, [client]);

assert.notStrictEqual(client.state.parent, undefined);
assert.strictEqual(client.state.parent.arrayChild.length, 1);
assert.strictEqual(client.state.parent.arrayChild[0].x, 1);
assert.strictEqual(client.state.parent.arrayChild[0].y, 2);
assert.strictEqual(client.state.parent.arrayChild[0].z, 3);
});

it("should encode nested Schema with @inheritVisibility that wasn't initially assigned", () => {
const state = new InheritanceRoot();
const encoder = getEncoder(state);

const client = createClientWithView(state);
client.view.add(state.parent);

// Initial encode: child property is undefined
encodeMultiple(encoder, state, [client]);

// Assign a child Schema that uses @inheritVisibility
state.parent.inheritingChild = new InheritedPosition(1, 2, 3);

/**
* Encode an assignment of a child field:
* the child "inheritingChild" field (InheritedPosition) is marked with
* @inheritVisibility, so it shares visibility with its parent and
* its fields are encoded for the client.
*/
encodeMultiple(encoder, state, [client]);

assert.notStrictEqual(client.state.parent, undefined);
assert.notStrictEqual(client.state.parent.inheritingChild, undefined);
assert.strictEqual(client.state.parent.inheritingChild.x, 1);
assert.strictEqual(client.state.parent.inheritingChild.y, 2);
assert.strictEqual(client.state.parent.inheritingChild.z, 3);
});

describe("no changes", () => {
it("empty state", () => {
const state = new State();
Expand Down
27 changes: 26 additions & 1 deletion test/Schema.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import * as assert from "assert";

import { Schema, type, ArraySchema, MapSchema, Reflection, Iterator, StateView } from "../src";
import { Schema, type, ArraySchema, MapSchema, Reflection, Iterator, StateView, inheritVisibility, view } from "../src";
import { Decoder } from "../src/decoder/Decoder";
import { Encoder } from "../src/encoder/Encoder";
import { CallbackProxy, getDecoderStateCallbacks, SchemaCallbackProxy } from "../src/decoder/strategy/getDecoderStateCallbacks";
Expand Down Expand Up @@ -249,6 +249,31 @@ export class Position extends Schema {
}
}

@inheritVisibility
export class InheritedPosition extends Schema {
@type("float32") x: number;
@type("float32") y: number;
@type("float32") z: number;

constructor(x: number, y: number, z: number) {
super();
this.x = x;
this.y = y;
this.z = z;
}
}

export class InheritanceParent extends Schema {
@type(Position) standardChild: Position | undefined = undefined;
@view() @type(Position) viewChild: Position | undefined = undefined;
@type([Position]) arrayChild: ArraySchema<Position> = new ArraySchema<Position>();
@type(InheritedPosition) inheritingChild: InheritedPosition | undefined = undefined;
}

export class InheritanceRoot extends Schema {
@view() @type(InheritanceParent) parent = new InheritanceParent();
}

export class Another extends Schema {
@type(Position) position: Position = new Position(0, 0, 0);
}
Expand Down