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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squishjs",
3
- "version": "1.4.1",
3
+ "version": "1.4.3",
4
4
  "description": "squish & unsquish stuff",
5
5
  "scripts": {
6
6
  "test": "node testRunner.js"
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
- } else {
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
- setInterval(this.game.tick.bind(this.game), 1000 / tickRate);
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
- playerMap[Number(playerId)] = [];
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 {
@@ -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,