Skip to content

Commit 7ffab3c

Browse files
committed
ArraySchema: introduce .shuffle() and .move(). allow to toggle MOVE operations.
1 parent 1885762 commit 7ffab3c

File tree

4 files changed

+148
-49
lines changed

4 files changed

+148
-49
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@colyseus/schema",
3-
"version": "3.0.29",
3+
"version": "3.0.30",
44
"description": "Binary state serializer with delta encoding for games",
55
"bin": {
66
"schema-codegen": "./bin/schema-codegen",

src/types/custom/ArraySchema.ts

Lines changed: 68 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
2828
static [$encoder] = encodeArray;
2929
static [$decoder] = decodeArray;
3030

31+
protected isMovingItems = false;
32+
3133
/**
3234
* Determine if a property must be filtered.
3335
* - If returns false, the property is NOT going to be encoded.
@@ -60,7 +62,6 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
6062
}
6163

6264
constructor (...items: V[]) {
63-
6465
Object.defineProperty(this, $childType, {
6566
value: undefined,
6667
enumerable: false,
@@ -92,33 +93,42 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
9293
assertInstanceType(setValue, obj[$childType] as typeof Schema, obj, key);
9394

9495
const previousValue = obj.items[key as unknown as number];
95-
if (previousValue !== undefined) {
96-
if (setValue[$changes].isNew) {
97-
this[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
9896

99-
} else {
100-
if ((obj[$changes].getChange(Number(key)) & OPERATION.DELETE) === OPERATION.DELETE) {
101-
this[$changes].indexedOperation(Number(key), OPERATION.DELETE_AND_MOVE);
97+
if (!obj.isMovingItems) {
98+
obj.$changeAt(Number(key), setValue);
99+
100+
} else {
101+
if (previousValue !== undefined) {
102+
if (setValue[$changes].isNew) {
103+
obj[$changes].indexedOperation(Number(key), OPERATION.MOVE_AND_ADD);
104+
102105
} else {
103-
this[$changes].indexedOperation(Number(key), OPERATION.MOVE);
106+
if ((obj[$changes].getChange(Number(key)) & OPERATION.DELETE) === OPERATION.DELETE) {
107+
obj[$changes].indexedOperation(Number(key), OPERATION.DELETE_AND_MOVE);
108+
109+
} else {
110+
obj[$changes].indexedOperation(Number(key), OPERATION.MOVE);
111+
}
104112
}
113+
114+
} else if (setValue[$changes].isNew) {
115+
obj[$changes].indexedOperation(Number(key), OPERATION.ADD);
105116
}
106117

118+
setValue[$changes].setParent(this, obj[$changes].root, key);
119+
}
120+
121+
if (previousValue !== undefined) {
107122
// remove root reference from previous value
108123
previousValue[$changes].root?.remove(previousValue[$changes]);
109-
110-
} else if (setValue[$changes].isNew) {
111-
this[$changes].indexedOperation(Number(key), OPERATION.ADD);
112124
}
113125

114-
setValue[$changes].setParent(this, obj[$changes].root, key);
115-
116126
} else {
117127
obj.$changeAt(Number(key), setValue);
118128
}
119129

120-
this.items[key as unknown as number] = setValue;
121-
this.tmpItems[key as unknown as number] = setValue;
130+
obj.items[key as unknown as number] = setValue;
131+
obj.tmpItems[key as unknown as number] = setValue;
122132
}
123133

124134
return true;
@@ -251,9 +261,13 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
251261
return;
252262
}
253263

254-
const changeTree = this[$changes];
255-
const operation = changeTree.indexes?.[index]?.op ?? OPERATION.ADD;
264+
const operation = (this.items[index] !== undefined)
265+
? typeof(value) === "object"
266+
? OPERATION.DELETE_AND_ADD // schema child
267+
: OPERATION.REPLACE // primitive
268+
: OPERATION.ADD;
256269

270+
const changeTree = this[$changes];
257271
changeTree.change(index, operation);
258272

259273
//
@@ -390,13 +404,17 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
390404
* ```
391405
*/
392406
sort(compareFn: (a: V, b: V) => number = DEFAULT_SORT): this {
407+
this.isMovingItems = true;
408+
393409
const changeTree = this[$changes];
394410
const sortedItems = this.items.sort(compareFn);
395411

396412
// wouldn't OPERATION.MOVE make more sense here?
397413
sortedItems.forEach((_, i) => changeTree.change(i, OPERATION.REPLACE));
398414

399415
this.tmpItems.sort(compareFn);
416+
417+
this.isMovingItems = false;
400418
return this;
401419
}
402420

@@ -786,6 +804,39 @@ export class ArraySchema<V = any> implements Array<V>, Collection<number, V> {
786804
return this.items.toSpliced.apply(copy, arguments);
787805
}
788806

807+
shuffle() {
808+
return this.move((_) => {
809+
let currentIndex = this.items.length;
810+
while (currentIndex != 0) {
811+
let randomIndex = Math.floor(Math.random() * currentIndex);
812+
currentIndex--;
813+
[this[currentIndex], this[randomIndex]] = [this[randomIndex], this[currentIndex]];
814+
}
815+
});
816+
}
817+
818+
/**
819+
* Allows to move items around in the array.
820+
*
821+
* Example:
822+
* state.cards.move((cards) => {
823+
* [cards[4], cards[3]] = [cards[3], cards[4]];
824+
* [cards[3], cards[2]] = [cards[2], cards[3]];
825+
* [cards[2], cards[0]] = [cards[0], cards[2]];
826+
* [cards[1], cards[1]] = [cards[1], cards[1]];
827+
* [cards[0], cards[0]] = [cards[0], cards[0]];
828+
* })
829+
*
830+
* @param cb
831+
* @returns
832+
*/
833+
move(cb: (arr: this) => void) {
834+
this.isMovingItems = true;
835+
cb(this);
836+
this.isMovingItems = false;
837+
return this;
838+
}
839+
789840
protected [$getByIndex](index: number, isEncodeAll: boolean = false) {
790841
//
791842
// TODO: avoid unecessary `this.tmpItems` check during decoding.

test/ArraySchema.test.ts

Lines changed: 77 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,71 @@ describe("ArraySchema Tests", () => {
228228
});
229229
});
230230

231+
it("should allow mutating primitive value by index", () => {
232+
class State extends Schema {
233+
@type(['number']) primativeArr = new ArraySchema<Number>()
234+
}
235+
236+
const state = new State();
237+
const decodedState = createInstanceFromReflection(state);
238+
239+
state.primativeArr.push(1);
240+
state.primativeArr.push(2);
241+
state.primativeArr.push(3);
242+
243+
decodedState.decode(state.encode());
244+
assert.strictEqual(3, decodedState.primativeArr.length);
245+
246+
state.primativeArr[0] = -1
247+
decodedState.decode(state.encode());
248+
assert.strictEqual(3, decodedState.primativeArr.length);
249+
250+
state.primativeArr[1] = -2
251+
decodedState.decode(state.encode());
252+
assert.strictEqual(3, decodedState.primativeArr.length);
253+
254+
state.primativeArr[2] = -3
255+
decodedState.decode(state.encode());
256+
assert.strictEqual(3, decodedState.primativeArr.length);
257+
258+
assert.deepStrictEqual(state.toJSON(), decodedState.toJSON());
259+
assertRefIdCounts(state, decodedState);
260+
});
261+
262+
it("should allow mutating Schema value by index", () => {
263+
class Item extends Schema {
264+
@type('number') i: number;
265+
}
266+
class State extends Schema {
267+
@type([Item]) schemaArr = new ArraySchema<Item>()
268+
}
269+
270+
const state = new State();
271+
const decodedState = createInstanceFromReflection(state);
272+
273+
state.schemaArr.push(new Item().assign({ i: 1 }));
274+
state.schemaArr.push(new Item().assign({ i: 2 }));
275+
state.schemaArr.push(new Item().assign({ i: 3 }));
276+
277+
decodedState.decode(state.encode());
278+
assert.strictEqual(3, decodedState.schemaArr.length);
279+
280+
state.schemaArr[0] = new Item({ i: -1 });
281+
decodedState.decode(state.encode());
282+
assert.strictEqual(3, decodedState.schemaArr.length);
283+
284+
state.schemaArr[1] = new Item({ i: -2 })
285+
decodedState.decode(state.encode());
286+
assert.strictEqual(3, decodedState.schemaArr.length);
287+
288+
state.schemaArr[2] = new Item({ i: -3 })
289+
decodedState.decode(state.encode());
290+
assert.strictEqual(3, decodedState.schemaArr.length);
291+
292+
assert.deepStrictEqual(state.toJSON(), decodedState.toJSON());
293+
assertRefIdCounts(state, decodedState);
294+
});
295+
231296
it("should not crash when pushing an undefined value", () => {
232297
class Block extends Schema {
233298
@type("number") num: number;
@@ -2009,23 +2074,6 @@ describe("ArraySchema Tests", () => {
20092074
@type([Card]) cards = new ArraySchema<Card>();
20102075
}
20112076

2012-
function shuffle(array) {
2013-
let currentIndex = array.length;
2014-
2015-
// While there remain elements to shuffle...
2016-
while (currentIndex != 0) {
2017-
2018-
// Pick a remaining element...
2019-
let randomIndex = Math.floor(Math.random() * currentIndex);
2020-
currentIndex--;
2021-
2022-
// console.log(`[array[${currentIndex}], array[${randomIndex}]] = [array[${randomIndex}], array[${currentIndex}]];`);
2023-
2024-
// And swap it with the current element.
2025-
[array[currentIndex], array[randomIndex]] = [array[randomIndex], array[currentIndex]];
2026-
}
2027-
}
2028-
20292077
it("should push and move entries", () => {
20302078
const state = new MyState();
20312079
state.cards.push(new Card().assign({ id: 2 }));
@@ -2038,11 +2086,13 @@ describe("ArraySchema Tests", () => {
20382086

20392087
state.cards.push(new Card().assign({ id: 6 }));
20402088

2041-
[state.cards[4], state.cards[3]] = [state.cards[3], state.cards[4]];
2042-
[state.cards[3], state.cards[2]] = [state.cards[2], state.cards[3]];
2043-
[state.cards[2], state.cards[0]] = [state.cards[0], state.cards[2]];
2044-
[state.cards[1], state.cards[1]] = [state.cards[1], state.cards[1]];
2045-
[state.cards[0], state.cards[0]] = [state.cards[0], state.cards[0]];
2089+
state.cards.move(() => {
2090+
[state.cards[4], state.cards[3]] = [state.cards[3], state.cards[4]];
2091+
[state.cards[3], state.cards[2]] = [state.cards[2], state.cards[3]];
2092+
[state.cards[2], state.cards[0]] = [state.cards[0], state.cards[2]];
2093+
[state.cards[1], state.cards[1]] = [state.cards[1], state.cards[1]];
2094+
[state.cards[0], state.cards[0]] = [state.cards[0], state.cards[0]];
2095+
});
20462096

20472097
decodedState.decode(state.encode());
20482098

@@ -2076,7 +2126,7 @@ describe("ArraySchema Tests", () => {
20762126
decodedState.decode(state.encode());
20772127

20782128
state.cards.pop();
2079-
shuffle(state.cards);
2129+
state.cards.shuffle();
20802130

20812131
decodedState.decode(state.encode());
20822132
assert.deepStrictEqual(
@@ -2122,9 +2172,9 @@ describe("ArraySchema Tests", () => {
21222172
});
21232173

21242174
// swap refId 5 <=> 2
2125-
console.log("WILL SWAP!");
2126-
[state.cards[2], state.cards[0]] = [state.cards[0], state.cards[2]];
2127-
console.log("SWAPPED.");
2175+
state.cards.move(() => {
2176+
[state.cards[2], state.cards[0]] = [state.cards[0], state.cards[2]];
2177+
});
21282178

21292179
console.log(Schema.debugChangesDeep(state));
21302180

@@ -2171,10 +2221,7 @@ describe("ArraySchema Tests", () => {
21712221
decodedState.decode(state.encode());
21722222

21732223
state.cards.splice(2, 1);
2174-
2175-
[state.cards[2], state.cards[0]] = [state.cards[0], state.cards[2]];
2176-
[state.cards[1], state.cards[0]] = [state.cards[0], state.cards[1]];
2177-
[state.cards[0], state.cards[0]] = [state.cards[0], state.cards[0]];
2224+
state.cards.shuffle();
21782225

21792226
decodedState.decode(state.encode());
21802227

test/Schema.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,8 @@ export function assertRefIdCounts(source: Schema, target: Schema) {
4848
for (const refId in encoder.root.refCount) {
4949
const encoderRefCount = encoder.root.refCount[refId];
5050
const decoderRefCount = decoder.root.refCounts[refId] ?? 0;
51-
assert.strictEqual(encoderRefCount, decoderRefCount, `refCount mismatch for refId: ${refId}`);
51+
assert.strictEqual(encoderRefCount, decoderRefCount, `refCount mismatch for refId: ${refId} (encoder count: ${encoderRefCount}, decoder count: ${decoderRefCount})
52+
\nREF IDS:\n${Schema.debugRefIds(source)}`);
5253
}
5354
}
5455

0 commit comments

Comments
 (0)