squishjs 1.3.7 → 1.3.9

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.
@@ -0,0 +1,8 @@
1
+ {
2
+ "permissions": {
3
+ "allow": [
4
+ "Read(//Users/josephgarcia/homegames/homegames-client/**)",
5
+ "Read(//Users/josephgarcia/homegames/homegames-web/**)"
6
+ ]
7
+ }
8
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "squishjs",
3
- "version": "1.3.7",
3
+ "version": "1.3.9",
4
4
  "description": "squish & unsquish stuff",
5
5
  "scripts": {
6
6
  "test": "node testRunner.js"
package/src/Asset.js CHANGED
@@ -1,4 +1,8 @@
1
1
  const path = require('path');
2
+ const https = require('https');
3
+ const http = require('http');
4
+ const fs = require('fs');
5
+ const crypto = require('crypto');
2
6
  const { getAppDataPath } = require('./utils');
3
7
 
4
8
  // assets is the old stuff, save it for backward compatibility
@@ -20,14 +24,6 @@ class Asset {
20
24
  if (data) {
21
25
  this.data = data;
22
26
  }
23
-
24
- // this is dumb. was trying something but made it dumb.
25
- this.https = require('https');
26
- this.http = require('http');
27
- this.fs = require('fs');
28
- this.crypto = require('crypto');
29
- this.path = require('path');
30
- this.process = require('process');
31
27
  }
32
28
 
33
29
  getConfigValue(key, _default = undefined) {
@@ -62,9 +58,9 @@ class Asset {
62
58
  let _config = {};
63
59
 
64
60
  for (let i = 0; i < options.length; i++) {
65
- if (this.fs.existsSync(`${options[i]}/config.json`)) {
61
+ if (fs.existsSync(`${options[i]}/config.json`)) {
66
62
  console.log(`Using config at ${options[i]}`);
67
- _config = JSON.parse(this.fs.readFileSync(`${options[i]}/config.json`));
63
+ _config = JSON.parse(fs.readFileSync(`${options[i]}/config.json`));
68
64
  break;
69
65
  }
70
66
  }
@@ -76,7 +72,7 @@ class Asset {
76
72
 
77
73
 
78
74
  getHash(str) {
79
- const shasum = this.crypto.createHash('sha1');
75
+ const shasum = crypto.createHash('sha1');
80
76
  shasum.update(str);
81
77
  return shasum.digest('hex');
82
78
  };
@@ -98,7 +94,7 @@ class Asset {
98
94
  return resolve(true);
99
95
  }
100
96
  const fileLocation = this.getFileLocation(this.info.id);
101
- this.fs.exists(fileLocation, (exists) => {
97
+ fs.exists(fileLocation, (exists) => {
102
98
  resolve(exists && fileLocation);
103
99
  });
104
100
  });
@@ -124,8 +120,8 @@ class Asset {
124
120
 
125
121
  download(force) {
126
122
  const HG_ASSET_PATH = path.join(getAppDataPath(), 'asset-cache');
127
- if (!this.fs.existsSync(HG_ASSET_PATH)) {
128
- this.fs.mkdirSync(HG_ASSET_PATH);
123
+ if (!fs.existsSync(HG_ASSET_PATH)) {
124
+ fs.mkdirSync(HG_ASSET_PATH);
129
125
  }
130
126
  return new Promise((resolve, reject) => {
131
127
  this.existsLocally().then(fileLocation => {
@@ -150,7 +146,7 @@ class Asset {
150
146
  resolve(this.data);
151
147
  } else {
152
148
  this.download().then(fileLocation => {
153
- this.fs.readFile(fileLocation, (err, buf) => {
149
+ fs.readFile(fileLocation, (err, buf) => {
154
150
  if (err) {
155
151
  reject(err);
156
152
  } else {
@@ -164,13 +160,13 @@ class Asset {
164
160
  });
165
161
  }
166
162
 
167
- doDownload(assetId, path) {
163
+ doDownload(assetId, assetPath) {
168
164
  return new Promise((resolve, reject) => {
169
165
  const fileHash = this.getHash(assetId);
170
- const filePath = `${path}/${fileHash}`;
166
+ const filePath = `${assetPath}/${fileHash}`;
171
167
 
172
- const writeStream = this.fs.createWriteStream(filePath);
173
- const getModule = ASSET_URL.startsWith('https') ? this.https : this.http;
168
+ const writeStream = fs.createWriteStream(filePath);
169
+ const getModule = ASSET_URL.startsWith('https') ? https : http;
174
170
 
175
171
  writeStream.on('close', () => {
176
172
  resolve(filePath);
package/src/Squisher.js CHANGED
@@ -11,7 +11,7 @@ class Squisher {
11
11
 
12
12
  this.game = game;
13
13
  this.gameMetadata = game.constructor.metadata && game.constructor.metadata();
14
- this.assets = {};
14
+ this._assetBufferCache = {};
15
15
  this.playerSettings = {};
16
16
 
17
17
  this.customBottomLayer = customBottomLayer;
@@ -23,6 +23,13 @@ class Squisher {
23
23
  this.playerFrames = {};
24
24
 
25
25
  this.listeners = new Set();
26
+ // Coalescing: a single tick often mutates many nodes, each firing
27
+ // onStateChange. Instead of re-squishing the whole tree + broadcasting
28
+ // per mutation, we mark dirty and flush once at the end of the event
29
+ // loop turn. Consumers that need the current state synchronously (e.g.
30
+ // sending an initial frame to a player who just joined) call flush().
31
+ this._dirty = false;
32
+ this._flushScheduled = false;
26
33
  this.scale = scale || {x: 1, y: 1};
27
34
  this.state = this.squish(this.game.getLayers());
28
35
 
@@ -97,6 +104,10 @@ class Squisher {
97
104
  }
98
105
 
99
106
  handleNewAsset(key, asset) {
107
+ // Invalidate just this key so initialize() re-fetches only its data
108
+ if (this._assetBufferCache) {
109
+ delete this._assetBufferCache[key];
110
+ }
100
111
  return this.initialize();
101
112
  }
102
113
 
@@ -169,9 +180,6 @@ class Squisher {
169
180
 
170
181
  initialize() {
171
182
  return new Promise((resolve, reject) => {
172
-
173
-
174
- const assets = Object.assign({}, this.assets || {});
175
183
 
176
184
  const gameMetadata = this.game.constructor.metadata && this.game.constructor.metadata();
177
185
 
@@ -190,65 +198,84 @@ class Squisher {
190
198
  Object.assign(gameAssets, this.game.getAssets());
191
199
  }
192
200
 
193
- const allAssets = Object.assign(assets, gameAssets);
201
+ const assetTypeMap = {
202
+ 'image': 1,
203
+ 'audio': 2,
204
+ 'font': 3
205
+ };
194
206
 
195
- let assetBundleSize = 0;
196
- let finishedCount = 0;
197
- const totalCount = Object.keys(allAssets).length;
207
+ const assetKeys = Object.keys(gameAssets);
208
+ const totalCount = assetKeys.length;
198
209
 
199
210
  if (totalCount === 0) {
200
- this.assetBundle = [];
201
- resolve([]);
211
+ this.assetBundle = Buffer.alloc(0);
212
+ resolve(this.assetBundle);
202
213
  return;
203
214
  }
204
-
205
- for (const key in allAssets) {
206
- allAssets[key].getData().then(buf => {
207
- const assetKeyLength = 32;
208
- let keyIndex = 0;
209
- const assetKeyArray = new Array(32);
210
- while (keyIndex < assetKeyLength && keyIndex < key.length) {
211
- assetKeyArray[keyIndex] = key.charCodeAt(keyIndex);
212
- keyIndex++;
215
+
216
+ if (!this._assetBufferCache) {
217
+ this._assetBufferCache = {};
218
+ }
219
+
220
+ let finishedCount = 0;
221
+ const perAssetBuffers = new Array(totalCount);
222
+
223
+ for (let idx = 0; idx < totalCount; idx++) {
224
+ const key = assetKeys[idx];
225
+
226
+ // If we already have a packed buffer for this key, reuse it
227
+ if (this._assetBufferCache[key]) {
228
+ perAssetBuffers[idx] = this._assetBufferCache[key];
229
+ finishedCount += 1;
230
+ if (finishedCount === totalCount) {
231
+ this.assetBundle = Buffer.concat(perAssetBuffers);
232
+ resolve(this.assetBundle);
213
233
  }
214
-
215
- const encodedLength = (buf.length + assetKeyLength).toString(36);
216
-
217
- const assetTypeMap = {
218
- 'image': 1,
219
- 'audio': 2,
220
- 'font': 3
221
- };
222
-
223
- const assetType = assetTypeMap[allAssets[key].info.type];
224
-
234
+ continue;
235
+ }
236
+
237
+ gameAssets[key].getData().then(buf => {
238
+ const assetKeyLength = 32;
225
239
  const encodedMaxLength = 10;
226
- let encodedLengthString = '';
227
- for (let i = 0; i < (encodedMaxLength - encodedLength.length); i++) {
228
- encodedLengthString += '0';
240
+
241
+ const headerLength = 2 + encodedMaxLength + assetKeyLength;
242
+ const assetBuf = Buffer.alloc(headerLength + buf.length);
243
+
244
+ assetBuf[0] = ASSET_TYPE;
245
+ assetBuf[1] = assetTypeMap[gameAssets[key].info.type];
246
+
247
+ // Encode the length (data + key length) as base-36, left-padded
248
+ const encodedLength = (buf.length + assetKeyLength).toString(36);
249
+ const padLen = encodedMaxLength - encodedLength.length;
250
+ for (let i = 0; i < padLen; i++) {
251
+ assetBuf[2 + i] = 48; // '0' charCode
229
252
  }
230
- for (let j = encodedLength.length; j < encodedMaxLength; j++) {
231
- encodedLengthString += encodedLength.charAt(j - encodedLength.length);
253
+ for (let i = 0; i < encodedLength.length; i++) {
254
+ assetBuf[2 + padLen + i] = encodedLength.charCodeAt(i);
232
255
  }
233
- const encodedLengthArray = new Array(encodedMaxLength);
234
- for (let i = 0; i < encodedMaxLength; i++) {
235
- encodedLengthArray[i] = encodedLength.charCodeAt(i);
256
+
257
+ // Write asset key (up to 32 chars, rest stays 0)
258
+ const keyWriteLen = Math.min(assetKeyLength, key.length);
259
+ for (let i = 0; i < keyWriteLen; i++) {
260
+ assetBuf[2 + encodedMaxLength + i] = key.charCodeAt(i);
236
261
  }
237
-
238
- this.assets[key] = [ASSET_TYPE, assetType, ...encodedLengthArray, ...assetKeyArray, ...buf];
239
- assetBundleSize += this.assets[key].length;
240
- finishedCount += 1;
241
262
 
242
- if (finishedCount == totalCount) {
243
- const newAssetBundle = new Array(assetBundleSize);
244
- let index = 0;
245
- for (const key in this.assets) {
246
- for (let y = 0; y < this.assets[key].length; y++) {
247
- newAssetBundle[index++] = this.assets[key][y];
248
- }
263
+ // Copy the raw asset data
264
+ if (Buffer.isBuffer(buf)) {
265
+ buf.copy(assetBuf, headerLength);
266
+ } else {
267
+ for (let i = 0; i < buf.length; i++) {
268
+ assetBuf[headerLength + i] = buf[i];
249
269
  }
250
- this.assetBundle = newAssetBundle;
251
- resolve(newAssetBundle);
270
+ }
271
+
272
+ this._assetBufferCache[key] = assetBuf;
273
+ perAssetBuffers[idx] = assetBuf;
274
+ finishedCount += 1;
275
+
276
+ if (finishedCount === totalCount) {
277
+ this.assetBundle = Buffer.concat(perAssetBuffers);
278
+ resolve(this.assetBundle);
252
279
  }
253
280
  }).catch(err => {
254
281
  console.error('Unable to get asset data for key ' + key);
@@ -261,10 +288,25 @@ class Squisher {
261
288
  }
262
289
 
263
290
  handleStateChange(node, layerName) {
264
- if (this.listeners.size > 0) {
265
- this.state = this.squish(this.game.getLayers());
266
- this.broadcast();
267
- }
291
+ if (this.listeners.size === 0) return;
292
+ this._dirty = true;
293
+ if (this._flushScheduled) return;
294
+ this._flushScheduled = true;
295
+ const schedule = (typeof setImmediate === 'function')
296
+ ? setImmediate
297
+ : (fn) => setTimeout(fn, 0);
298
+ schedule(() => this.flush());
299
+ }
300
+
301
+ // Squish + broadcast any pending state changes synchronously. Safe to call
302
+ // when nothing is pending (no-op). Coalesces a burst of mutations into a
303
+ // single squish + single broadcast carrying the latest state.
304
+ flush() {
305
+ this._flushScheduled = false;
306
+ if (!this._dirty) return;
307
+ this._dirty = false;
308
+ this.state = this.squish(this.game.getLayers());
309
+ this.broadcast();
268
310
  }
269
311
 
270
312
  broadcast() {
@@ -6,7 +6,7 @@ const squishAsset = {
6
6
  type: ASSET_SUBTYPE,
7
7
  squish: (a, scale) => {
8
8
  const assetKey = Object.keys(a)[0];
9
- const squishedAssets = new Array(10 + assetKey.length);
9
+ const squishedAssets = new Array(18 + assetKey.length);
10
10
 
11
11
  const asset = a[assetKey];
12
12
 
@@ -18,6 +18,15 @@ const squishAsset = {
18
18
 
19
19
  const startTimeSecond = asset.startTime || 0;
20
20
 
21
+ // Crop values are percentages (0-100) of the source image, expressed as
22
+ // the amount removed from each edge. They sample a sub-region of the
23
+ // source and are independent of layer scale (which affects pos/size, i.e.
24
+ // where the cropped region lands on screen). Default to 0 (no crop).
25
+ const cropLeft = asset.cropLeft || 0;
26
+ const cropTop = asset.cropTop || 0;
27
+ const cropRight = asset.cropRight || 0;
28
+ const cropBottom = asset.cropBottom || 0;
29
+
21
30
  squishedAssets[0] = Math.floor(posX);
22
31
  squishedAssets[1] = getFractional(posX);
23
32
 
@@ -33,8 +42,20 @@ const squishAsset = {
33
42
  squishedAssets[8] = Math.floor(startTimeSecond);
34
43
  squishedAssets[9] = getFractional(startTimeSecond);
35
44
 
45
+ squishedAssets[10] = Math.floor(cropLeft);
46
+ squishedAssets[11] = getFractional(cropLeft);
47
+
48
+ squishedAssets[12] = Math.floor(cropTop);
49
+ squishedAssets[13] = getFractional(cropTop);
50
+
51
+ squishedAssets[14] = Math.floor(cropRight);
52
+ squishedAssets[15] = getFractional(cropRight);
53
+
54
+ squishedAssets[16] = Math.floor(cropBottom);
55
+ squishedAssets[17] = getFractional(cropBottom);
56
+
36
57
  for (let i = 0; i < assetKey.length; i++) {
37
- squishedAssets[10 + i] = assetKey.codePointAt(i);
58
+ squishedAssets[18 + i] = assetKey.codePointAt(i);
38
59
  }
39
60
 
40
61
  return squishedAssets;
@@ -48,7 +69,12 @@ const squishAsset = {
48
69
 
49
70
  const startTime = squished[8] + squished[9] / 100;
50
71
 
51
- const assetKey = String.fromCodePoint.apply(null, squished.slice(10));
72
+ const cropLeft = squished[10] + squished[11] / 100;
73
+ const cropTop = squished[12] + squished[13] / 100;
74
+ const cropRight = squished[14] + squished[15] / 100;
75
+ const cropBottom = squished[16] + squished[17] / 100;
76
+
77
+ const assetKey = String.fromCodePoint.apply(null, squished.slice(18));
52
78
 
53
79
  return {
54
80
  [assetKey]: {
@@ -60,7 +86,11 @@ const squishAsset = {
60
86
  x: assetSizeX,
61
87
  y: assetSizeY
62
88
  },
63
- startTime
89
+ startTime,
90
+ cropLeft,
91
+ cropTop,
92
+ cropRight,
93
+ cropBottom
64
94
  }
65
95
  }
66
96
  }
@@ -69,4 +99,4 @@ const squishAsset = {
69
99
  module.exports = {
70
100
  ASSET_SUBTYPE,
71
101
  squishAsset
72
- };
102
+ };
@@ -56,6 +56,8 @@ test("test layer with child added", () => {
56
56
 
57
57
  // adding a child should trigger an update in the squisher and result in a new state
58
58
  layer.addChild(child);
59
+ // state changes are coalesced; flush to materialize them synchronously
60
+ squisher.flush();
59
61
 
60
62
  const expectedLayerValue = [squish(layer), squish(child)].flat();
61
63
  const stateWithChild = Array.from(squisher.state);
@@ -5,48 +5,49 @@ const { unsquish } = require('../src/squish');
5
5
 
6
6
  const { FakeGame, verifyArrayEquality, rectNode, textNode, polygonNode, circleNode, lineNode } = require('./utils');
7
7
 
8
- test("squisher listener", () => {
8
+ test("squisher listener coalesces state changes", () => {
9
9
 
10
10
  const root = lineNode({coords: [[20, 20], [80, 80]], color: COLORS.ORANGE});
11
-
12
- const game = new FakeGame([
11
+
12
+ const game = new FakeGame([
13
13
  {
14
14
  root,
15
15
  scale: {
16
16
  x: 1,
17
17
  y: 1
18
18
  }
19
- }
19
+ }
20
20
  ]);
21
21
 
22
22
  const squisher = new Squisher({ game });
23
23
 
24
24
  let timesInvoked = 0;
25
+ let lastState = null;
25
26
 
26
27
  squisher.addListener((newSquishedState) => {
27
- if (timesInvoked === 0) {
28
- assert(newSquishedState.length == 1);
29
- const updatedRoot = unsquish(newSquishedState[0]);
30
- verifyArrayEquality(updatedRoot.node.color, COLORS.BLACK);
31
- } else if (timesInvoked === 1) {
32
- assert(newSquishedState.length == 1);
33
- const updatedRoot = unsquish(newSquishedState[0]);
34
- verifyArrayEquality(updatedRoot.node.color, COLORS.GREEN);
35
- }
36
28
  timesInvoked++;
29
+ lastState = newSquishedState;
37
30
  });
38
31
 
39
32
  assert(timesInvoked === 0);
40
33
 
34
+ // A burst of mutations in the same turn is deferred — nothing broadcast yet.
41
35
  root.node.color = COLORS.BLACK;
42
36
  root.node.onStateChange();
43
-
44
- assert(timesInvoked === 1);
45
-
46
37
  root.node.color = COLORS.GREEN;
47
38
  root.node.onStateChange();
48
39
 
49
- assert(timesInvoked === 2);
40
+ assert(timesInvoked === 0);
41
+
42
+ // flush() coalesces the burst into a single broadcast with the final state.
43
+ squisher.flush();
44
+ assert(timesInvoked === 1);
45
+ assert(lastState.length == 1);
46
+ verifyArrayEquality(unsquish(lastState[0]).node.color, COLORS.GREEN);
47
+
48
+ // flush() with nothing pending is a no-op.
49
+ squisher.flush();
50
+ assert(timesInvoked === 1);
50
51
  });
51
52
 
52
53
 
package/test/main.test.js CHANGED
@@ -435,6 +435,7 @@ test("Simple shape with updates", () => {
435
435
  base.update({
436
436
  fill: COLORS.BLUE
437
437
  });
438
+ squisher.flush();
438
439
 
439
440
  const state2 = Array.from(squisher.state);
440
441
  verifyArrayEquality(unsquish(state2[0]).node.fill, COLORS.BLUE);
@@ -442,6 +443,7 @@ test("Simple shape with updates", () => {
442
443
  base.update({
443
444
  fill: COLORS.GREEN
444
445
  });
446
+ squisher.flush();
445
447
 
446
448
  const state3 = Array.from(squisher.state);
447
449
  verifyArrayEquality(unsquish(state3[0]).node.fill, COLORS.GREEN);