squishjs 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/Asset.js +22 -11
- package/src/Squisher.js +24 -14
- package/src/squishHelpers/coordinates2d.js +7 -3
- package/test/Squisher.test.js +30 -0
package/package.json
CHANGED
package/src/Asset.js
CHANGED
|
@@ -165,23 +165,34 @@ class Asset {
|
|
|
165
165
|
const fileHash = this.getHash(assetId);
|
|
166
166
|
const filePath = `${assetPath}/${fileHash}`;
|
|
167
167
|
|
|
168
|
-
const writeStream = fs.createWriteStream(filePath);
|
|
169
168
|
const getModule = ASSET_URL.startsWith('https') ? https : http;
|
|
170
169
|
|
|
171
|
-
writeStream.on('close', () => {
|
|
172
|
-
resolve(filePath);
|
|
173
|
-
});
|
|
174
|
-
|
|
175
170
|
getModule.get(`${ASSET_URL}/${assetId}`, (res) => {
|
|
176
171
|
if (res.statusCode !== 200) {
|
|
172
|
+
res.resume();
|
|
177
173
|
reject('Bad response when downloading asset');
|
|
178
|
-
|
|
179
|
-
writeStream.on('finish', () => {
|
|
180
|
-
writeStream.close();
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
res.pipe(writeStream);
|
|
174
|
+
return;
|
|
184
175
|
}
|
|
176
|
+
|
|
177
|
+
// Only open the cache file once we know the response is good —
|
|
178
|
+
// otherwise a failed download leaves a zero-byte file behind
|
|
179
|
+
// that reads back as a valid cache hit forever after.
|
|
180
|
+
const writeStream = fs.createWriteStream(filePath);
|
|
181
|
+
|
|
182
|
+
writeStream.on('close', () => {
|
|
183
|
+
resolve(filePath);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
writeStream.on('error', (error) => {
|
|
187
|
+
fs.unlink(filePath, () => reject(error));
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
res.on('error', (error) => {
|
|
191
|
+
writeStream.destroy();
|
|
192
|
+
fs.unlink(filePath, () => reject(error));
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
res.pipe(writeStream);
|
|
185
196
|
}).on('error', error => {
|
|
186
197
|
console.error('Failed to download asset');
|
|
187
198
|
console.error(error);
|
package/src/Squisher.js
CHANGED
|
@@ -35,7 +35,9 @@ class Squisher {
|
|
|
35
35
|
|
|
36
36
|
if (this.game.tick) {
|
|
37
37
|
const tickRate = this.gameMetadata && this.gameMetadata.tickRate ? this.gameMetadata.tickRate : DEFAULT_TICK_RATE;
|
|
38
|
-
|
|
38
|
+
// Keep the handle so consumers can end the tick loop (repeated
|
|
39
|
+
// session create/destroy in one process leaks it otherwise).
|
|
40
|
+
this._tickInterval = setInterval(this.game.tick.bind(this.game), 1000 / tickRate);
|
|
39
41
|
}
|
|
40
42
|
}
|
|
41
43
|
|
|
@@ -67,30 +69,34 @@ class Squisher {
|
|
|
67
69
|
let toSquish = [];
|
|
68
70
|
|
|
69
71
|
const playerMap = {};
|
|
72
|
+
// Shared (visible-to-everyone) nodes in traversal order, kept so a
|
|
73
|
+
// player frame created mid-traversal can be seeded with everything
|
|
74
|
+
// shared that was squished before that player's first scoped node.
|
|
75
|
+
const sharedNodes = [];
|
|
70
76
|
|
|
71
77
|
if (this.customBottomLayer) {
|
|
72
78
|
const squishedLayer = [];
|
|
73
|
-
this.squishHelper(this.customBottomLayer.root, squishedLayer, this.customBottomLayer.scale, playerMap);
|
|
79
|
+
this.squishHelper(this.customBottomLayer.root, squishedLayer, this.customBottomLayer.scale, playerMap, new Set(), sharedNodes);
|
|
74
80
|
toSquish.push(squishedLayer);
|
|
75
81
|
}
|
|
76
|
-
|
|
82
|
+
|
|
77
83
|
for (const layerIndex in layers) {
|
|
78
84
|
const squishedLayer = [];
|
|
79
85
|
const layerInfo = layers[layerIndex];
|
|
80
|
-
|
|
86
|
+
|
|
81
87
|
const layerScale = layerInfo.scale ? {
|
|
82
88
|
x: this.scale.x * layerInfo.scale.x,
|
|
83
89
|
y: this.scale.y * layerInfo.scale.y
|
|
84
90
|
} : this.scale;
|
|
85
91
|
|
|
86
|
-
this.squishHelper(layerInfo.root, squishedLayer, scale || layerScale, playerMap);
|
|
92
|
+
this.squishHelper(layerInfo.root, squishedLayer, scale || layerScale, playerMap, new Set(), sharedNodes);
|
|
87
93
|
toSquish.push(squishedLayer);
|
|
88
94
|
}
|
|
89
95
|
|
|
90
96
|
|
|
91
|
-
if (this.customTopLayer) {
|
|
97
|
+
if (this.customTopLayer) {
|
|
92
98
|
const squishedLayer = [];
|
|
93
|
-
this.squishHelper(this.customTopLayer.root, squishedLayer, this.customTopLayer.scale, playerMap);
|
|
99
|
+
this.squishHelper(this.customTopLayer.root, squishedLayer, this.customTopLayer.scale, playerMap, new Set(), sharedNodes);
|
|
94
100
|
toSquish.push(squishedLayer);
|
|
95
101
|
}
|
|
96
102
|
|
|
@@ -123,7 +129,7 @@ class Squisher {
|
|
|
123
129
|
return !!(sound && sound.enabled === false);
|
|
124
130
|
}
|
|
125
131
|
|
|
126
|
-
squishHelper(node, squishedNodes, scale = {x: 1, y: 1}, playerMap = {}, playerIdFilter = new Set()) {
|
|
132
|
+
squishHelper(node, squishedNodes, scale = {x: 1, y: 1}, playerMap = {}, playerIdFilter = new Set(), sharedNodes = []) {
|
|
127
133
|
if (!node.node.listeners.has(this)) {
|
|
128
134
|
node.addListener(this);
|
|
129
135
|
}
|
|
@@ -139,7 +145,13 @@ class Squisher {
|
|
|
139
145
|
let playerIdsToRemove = new Set();
|
|
140
146
|
for (let playerId of playerIdFilter) {
|
|
141
147
|
if (!playerMap[playerId]) {
|
|
142
|
-
|
|
148
|
+
// First scoped node seen for this player. Seed their frame
|
|
149
|
+
// with every shared node squished so far — otherwise the
|
|
150
|
+
// frame would be missing everything shared (root
|
|
151
|
+
// background included) that preceded it in traversal.
|
|
152
|
+
playerMap[Number(playerId)] = sharedNodes
|
|
153
|
+
.filter(shared => !this._isAudioMutedFor(shared.node, playerId))
|
|
154
|
+
.map(shared => shared.squished);
|
|
143
155
|
}
|
|
144
156
|
|
|
145
157
|
if (node.node.playerIds.length === 0 || node.node.playerIds.findIndex(i => Number(i) === Number(playerId)) >= 0) {
|
|
@@ -154,10 +166,8 @@ class Squisher {
|
|
|
154
166
|
playerIdFilter.delete(id);
|
|
155
167
|
}
|
|
156
168
|
} else {
|
|
169
|
+
sharedNodes.push({ node, squished });
|
|
157
170
|
Object.keys(playerMap).forEach(playerId => {
|
|
158
|
-
if (!playerMap[playerId]) {
|
|
159
|
-
playerMap[Number(playerId)] = [];
|
|
160
|
-
}
|
|
161
171
|
if (!this._isAudioMutedFor(node, playerId)) {
|
|
162
172
|
playerMap[playerId].push(squished);
|
|
163
173
|
}
|
|
@@ -166,10 +176,10 @@ class Squisher {
|
|
|
166
176
|
|
|
167
177
|
for (let i = 0; i < node.node.children.length; i++) {
|
|
168
178
|
// if (node.node.children[i].node.playerIds
|
|
169
|
-
// make a new set so child calls within a single generation arent
|
|
179
|
+
// make a new set so child calls within a single generation arent
|
|
170
180
|
// modifying the same filter set
|
|
171
181
|
const pathFilter = new Set(playerIdFilter);
|
|
172
|
-
this.squishHelper(node.node.children[i], squishedNodes, scale, playerMap, pathFilter);
|
|
182
|
+
this.squishHelper(node.node.children[i], squishedNodes, scale, playerMap, pathFilter, sharedNodes);
|
|
173
183
|
}
|
|
174
184
|
|
|
175
185
|
}
|
|
@@ -28,12 +28,16 @@ const squishCoordinates2d = {
|
|
|
28
28
|
|
|
29
29
|
if (node.subType == subtypes.SHAPE_2D_CIRCLE) {
|
|
30
30
|
if (scale) {
|
|
31
|
+
// Math.floor on the integer byte is required, not decorative:
|
|
32
|
+
// a raw float here only decoded correctly because Node's
|
|
33
|
+
// Buffer truncated it on send. Consumers that don't go through
|
|
34
|
+
// Buffer (e.g. the in-browser LocalSession) would round.
|
|
31
35
|
const shiftedCenterX = clampCoord(squishHelper(scale.x, originalCoords[0]))
|
|
32
|
-
squished[0] = shiftedCenterX;
|
|
36
|
+
squished[0] = Math.floor(shiftedCenterX);
|
|
33
37
|
squished[1] = getFractional(shiftedCenterX);
|
|
34
38
|
|
|
35
39
|
const shiftedCenterY = clampCoord(squishHelper(scale.y, originalCoords[1]))
|
|
36
|
-
squished[2] = shiftedCenterY;
|
|
40
|
+
squished[2] = Math.floor(shiftedCenterY);
|
|
37
41
|
squished[3] = getFractional(shiftedCenterY);
|
|
38
42
|
|
|
39
43
|
let diagonal;
|
|
@@ -70,7 +74,7 @@ const squishCoordinates2d = {
|
|
|
70
74
|
|
|
71
75
|
const shifted = clampCoord(scaled + (removedSpace / 2));
|
|
72
76
|
|
|
73
|
-
squished[2 * i] = shifted;
|
|
77
|
+
squished[2 * i] = Math.floor(shifted);
|
|
74
78
|
squished[(2 * i) + 1] = getFractional(shifted);
|
|
75
79
|
|
|
76
80
|
} else {
|
package/test/Squisher.test.js
CHANGED
|
@@ -82,6 +82,36 @@ test("squisher does not crash on an asset node with an unregistered key", () =>
|
|
|
82
82
|
assert(frame && frame.length >= 1);
|
|
83
83
|
});
|
|
84
84
|
|
|
85
|
+
test("player frame includes shared nodes squished before the player's first scoped node", () => {
|
|
86
|
+
// The root background and an early shared sibling precede the scoped
|
|
87
|
+
// node in traversal order. The player's frame must still contain them
|
|
88
|
+
// (seeded on frame creation), in draw order.
|
|
89
|
+
const root = rectNode({ x: 0, y: 0, width: 100, height: 100, fill: COLORS.WHITE });
|
|
90
|
+
const earlyShared = rectNode({ x: 1, y: 1, width: 5, height: 5, fill: COLORS.GREEN });
|
|
91
|
+
const scoped = new GameNode.Shape({
|
|
92
|
+
shapeType: Shapes.POLYGON,
|
|
93
|
+
coordinates2d: ShapeUtils.rectangle(2, 2, 5, 5),
|
|
94
|
+
fill: COLORS.BLUE,
|
|
95
|
+
playerIds: [7]
|
|
96
|
+
});
|
|
97
|
+
const lateShared = rectNode({ x: 3, y: 3, width: 5, height: 5, fill: COLORS.RED });
|
|
98
|
+
|
|
99
|
+
root.addChild(earlyShared);
|
|
100
|
+
root.addChild(scoped);
|
|
101
|
+
root.addChild(lateShared);
|
|
102
|
+
|
|
103
|
+
const game = new FakeGame([{ root, scale: { x: 1, y: 1 } }]);
|
|
104
|
+
const squisher = new Squisher({ game });
|
|
105
|
+
|
|
106
|
+
const frame = squisher.getPlayerFrame(7);
|
|
107
|
+
assert(frame.length === 4);
|
|
108
|
+
const fills = frame.map(squished => unsquish(squished).node.fill);
|
|
109
|
+
verifyArrayEquality(fills[0], COLORS.WHITE);
|
|
110
|
+
verifyArrayEquality(fills[1], COLORS.GREEN);
|
|
111
|
+
verifyArrayEquality(fills[2], COLORS.BLUE);
|
|
112
|
+
verifyArrayEquality(fills[3], COLORS.RED);
|
|
113
|
+
});
|
|
114
|
+
|
|
85
115
|
test("squisher withholds muted audio from a player-scoped subtree", () => {
|
|
86
116
|
const root = new GameNode.Shape({
|
|
87
117
|
shapeType: Shapes.POLYGON,
|