com.robotsquid.squidhub 0.5.6 → 0.5.8

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.
@@ -36,7 +36,47 @@ namespace SquidHub.Editor
36
36
  set => EditorPrefs.SetString(Scope + "buildFolder", value);
37
37
  }
38
38
 
39
+ /// <summary>
40
+ /// The build profile `Build and Upload` uses, as an asset path.
41
+ ///
42
+ /// ⚠️ A PATH, not a cached object reference. EditorPrefs cannot hold one,
43
+ /// and a GUID would survive the profile being deleted as a dangling id
44
+ /// that resolves to nothing with no way to say what is missing.
45
+ /// </summary>
46
+ public static string BuildProfilePath
47
+ {
48
+ get => EditorPrefs.GetString(Scope + "buildProfile", "");
49
+ set => EditorPrefs.SetString(Scope + "buildProfile", value ?? "");
50
+ }
51
+
52
+ /// <summary>
53
+ /// Publish the build to players the moment it uploads.
54
+ ///
55
+ /// ⚠️ ON by default, and it only ever applies on the DEV environment.
56
+ /// The server refuses a key-authorised publish on prod — promoting a
57
+ /// build for every player is an owner decision made in the console, and
58
+ /// an upload key is not a person. See `setChannelWithKey`.
59
+ /// </summary>
60
+ public static bool PublishAfterUpload
61
+ {
62
+ get => EditorPrefs.GetBool(Scope + "publishAfterUpload", true);
63
+ set => EditorPrefs.SetBool(Scope + "publishAfterUpload", value);
64
+ }
65
+
39
66
  public static bool HasKey => Key.StartsWith("shu_");
67
+
68
+ /// <summary>
69
+ /// `manifest.json` beside `Assets/`, which is what `squidhub-release
70
+ /// build --manifest manifest.json` already reads.
71
+ ///
72
+ /// ⚠️ ONE source of truth, deliberately. Keeping the declaration in a
73
+ /// Unity-only asset would give the Editor and the CLI two answers to
74
+ /// "what does this game declare", and they would disagree on the day it
75
+ /// mattered. It is a normal project file and belongs in the repository.
76
+ /// </summary>
77
+ public static string ManifestPath =>
78
+ System.IO.Path.Combine(
79
+ System.IO.Path.GetDirectoryName(Application.dataPath) ?? "", "manifest.json");
40
80
  }
41
81
 
42
82
  internal sealed class SquidHubUploadSettingsWindow : EditorWindow
@@ -11,6 +11,7 @@
11
11
  using System;
12
12
  using System.Collections;
13
13
  using System.IO;
14
+ using System.Linq;
14
15
  using System.Text;
15
16
  using System.Text.RegularExpressions;
16
17
  using UnityEditor;
@@ -26,20 +27,39 @@ namespace SquidHub.Editor
26
27
  [MenuItem("SquidHub/Upload Build…", priority = 11)]
27
28
  private static void UploadBuild()
28
29
  {
29
- if (!SquidHubUploadSettings.HasKey)
30
- {
31
- EditorUtility.DisplayDialog("SquidHub",
32
- "No upload key set. Create one in the publisher console under your game → " +
33
- "Upload keys, then paste it into SquidHub → Upload Settings.", "OK");
34
- SquidHubUploadSettingsWindow.GetWindow<SquidHubUploadSettingsWindow>();
35
- return;
36
- }
30
+ if (!EnsureKey()) return;
37
31
 
38
32
  var start = SquidHubUploadSettings.BuildFolder;
39
33
  var folder = EditorUtility.OpenFolderPanel(
40
34
  "Select the WebGL build folder (containing index.html)",
41
35
  string.IsNullOrEmpty(start) ? "" : start, "");
42
36
  if (string.IsNullOrEmpty(folder)) return;
37
+
38
+ Begin(folder);
39
+ }
40
+
41
+ /// <summary>Nothing can proceed without a key, and saying so early costs nothing.</summary>
42
+ internal static bool EnsureKey()
43
+ {
44
+ if (SquidHubUploadSettings.HasKey) return true;
45
+
46
+ EditorUtility.DisplayDialog("SquidHub",
47
+ "No upload key set. Create one in the publisher console under your game → " +
48
+ "Upload keys, then paste it into SquidHub → Upload Settings.", "OK");
49
+ SquidHubUploadSettingsWindow.GetWindow<SquidHubUploadSettingsWindow>();
50
+ return false;
51
+ }
52
+
53
+ /// <summary>
54
+ /// Scans a build folder, confirms, and uploads it.
55
+ ///
56
+ /// ⚠️ The entry point `Build and Upload` uses too, so a build made here
57
+ /// and a folder picked by hand go through exactly the same refusals.
58
+ /// </summary>
59
+ internal static void Begin(string folder)
60
+ {
61
+ if (!EnsureKey()) return;
62
+
43
63
  SquidHubUploadSettings.BuildFolder = folder;
44
64
 
45
65
  BuildScan scan;
@@ -63,17 +83,34 @@ namespace SquidHub.Editor
63
83
  return;
64
84
  }
65
85
 
86
+ var publish = SquidHubUploadSettings.PublishAfterUpload;
87
+
66
88
  if (!EditorUtility.DisplayDialog("SquidHub",
67
89
  $"Upload {scan.Files.Count} files ({scan.TotalBytes / 1024f / 1024f:F1} MB)?" +
68
90
  (scan.Skipped.Count > 0
69
91
  ? $"\n\n{scan.Skipped.Count} OS metadata file(s) will be skipped."
92
+ : "") +
93
+ // ⚠️ Said before the upload, not after. A build folder with no
94
+ // manifest.json declares nothing — save schema 1, no display
95
+ // constraints — and the publisher cannot tell from the result.
96
+ (scan.Files.Any(f => f.Path == "manifest.json")
97
+ ? ""
98
+ : "\n\nThere is no manifest.json in this folder, so the build declares " +
99
+ "nothing: save schema 1 and no display constraints. Set one up in " +
100
+ "SquidHub → Game Manifest… and rebuild.") +
101
+ // ⚠️ These describe a build that uploads fine and PLAYS badly,
102
+ // which is the kind nobody catches until a player does.
103
+ (scan.Warnings.Count > 0 ? "\n\n" + string.Join("\n\n", scan.Warnings) : "") +
104
+ (publish
105
+ ? "\n\nIt will be published to players if the server allows it — that is " +
106
+ "dev only. On prod, promote it in the console."
70
107
  : ""),
71
108
  "Upload", "Cancel"))
72
109
  {
73
110
  return;
74
111
  }
75
112
 
76
- Run(Upload(scan));
113
+ Run(Upload(scan, publish));
77
114
  }
78
115
 
79
116
  // ⚠️ Driven by EditorApplication.update rather than blocking. Sleeping
@@ -109,7 +146,7 @@ namespace SquidHub.Editor
109
146
  Debug.Log("[SquidHub] upload run finished.");
110
147
  }
111
148
 
112
- private static IEnumerator Upload(BuildScan scan)
149
+ private static IEnumerator Upload(BuildScan scan, bool publish)
113
150
  {
114
151
  var endpoint = SquidHubUploadSettings.Endpoint;
115
152
  var key = SquidHubUploadSettings.Key;
@@ -187,12 +224,98 @@ namespace SquidHub.Editor
187
224
  yield break;
188
225
  }
189
226
 
190
- // ⚠️ Registered, NOT live. Promoting a build to players is a separate,
191
- // deliberate act in the console — saying "uploaded" without saying
192
- // that invites someone to assume their players already have it.
193
- EditorUtility.DisplayDialog("SquidHub",
194
- "Registered " + SquidHubBuildScan.JsonField(doneResponse, "buildHash") + ".\n\n" +
195
- "It is not live yet — promote it in the publisher console when you are ready.",
227
+ var buildHash = SquidHubBuildScan.JsonField(doneResponse, "buildHash");
228
+
229
+ // ⚠️ Surfaced, not swallowed. The server warns rather than fails when
230
+ // a manifest change had no effect, and that warning is the only thing
231
+ // standing between a publisher and believing their edit shipped.
232
+ var warning = SquidHubBuildScan.JsonField(doneResponse, "detail");
233
+ if (warning != null) Debug.LogWarning("[SquidHub] " + warning);
234
+
235
+ if (!publish)
236
+ {
237
+ // ⚠️ Registered, NOT live. Promoting a build to players is a
238
+ // separate, deliberate act — saying "uploaded" without saying
239
+ // that invites someone to assume their players already have it.
240
+ EditorUtility.DisplayDialog("SquidHub",
241
+ "Registered " + buildHash + ".\n\n" +
242
+ "It is not live yet — promote it in the publisher console when you are ready.",
243
+ "OK");
244
+ yield break;
245
+ }
246
+
247
+ yield return PublishStep(endpoint, key, buildHash);
248
+ }
249
+
250
+ /// <summary>
251
+ /// Asks the server to make the build live.
252
+ ///
253
+ /// ⚠️ THE SERVER DECIDES, and it refuses on prod. An upload key lives in
254
+ /// CI configs and Editor preferences; promoting a build for every player
255
+ /// is an owner decision made by a person in the console. A refusal here
256
+ /// is the system working, so it reports as information rather than as a
257
+ /// failure — the upload really did succeed.
258
+ /// </summary>
259
+ private static IEnumerator PublishStep(string endpoint, string key, string buildHash)
260
+ {
261
+ if (string.IsNullOrEmpty(buildHash))
262
+ {
263
+ Debug.LogError("[SquidHub] the server registered the build but named no buildHash, " +
264
+ "so it cannot be published. It is uploaded; promote it in the console.");
265
+ yield break;
266
+ }
267
+
268
+ EditorUtility.DisplayProgressBar("SquidHub", "Publishing…", 0.995f);
269
+
270
+ string response = null;
271
+ yield return Post(endpoint + "/api/ingest",
272
+ key,
273
+ "{\"intent\":\"publish\",\"buildHash\":" + SquidHubBuildScan.JsonString(buildHash) + "}",
274
+ r => response = r);
275
+
276
+ EditorUtility.ClearProgressBar();
277
+
278
+ // ⚠️ Post already reported the failure. Reaching here with null means
279
+ // the publish did not happen, and the build IS still uploaded — which
280
+ // is the one thing the publisher needs to be told.
281
+ if (response == null)
282
+ {
283
+ Debug.LogWarning("[SquidHub] " + buildHash + " is uploaded but was NOT published.");
284
+ yield break;
285
+ }
286
+
287
+ if (SquidHubBuildScan.JsonField(response, "ok") == "true")
288
+ {
289
+ var stranded = SquidHubBuildScan.JsonField(response, "strandedPlayers");
290
+ Debug.Log("[SquidHub] published " + buildHash + ".");
291
+ EditorUtility.DisplayDialog("SquidHub",
292
+ "Published " + buildHash + ". Players get it on their next launch." +
293
+ (stranded != null && stranded != "0"
294
+ ? "\n\n⚠️ " + stranded + " player(s) have a save this build's schema cannot read."
295
+ : ""),
296
+ "OK");
297
+ yield break;
298
+ }
299
+
300
+ var code = SquidHubBuildScan.JsonField(response, "code");
301
+ var message = SquidHubBuildScan.JsonField(response, "message") ?? response;
302
+
303
+ if (code == "MANUAL_ON_PROD")
304
+ {
305
+ Debug.Log("[SquidHub] uploaded " + buildHash + "; publishing on prod is manual.");
306
+ EditorUtility.DisplayDialog("SquidHub",
307
+ "Uploaded " + buildHash + ".\n\n" +
308
+ "It was not published: an upload key can publish on dev, never on prod. " +
309
+ "Promote it in the publisher console when you are ready.",
310
+ "OK");
311
+ yield break;
312
+ }
313
+
314
+ Debug.LogWarning("[SquidHub] uploaded " + buildHash + " but the publish was refused: " +
315
+ code + ": " + message);
316
+ EditorUtility.DisplayDialog("SquidHub — uploaded, not published",
317
+ "The build uploaded and is registered as " + buildHash + ", but publishing it was " +
318
+ "refused:\n\n" + message + "\n\nPromote it in the publisher console.",
196
319
  "OK");
197
320
  }
198
321
 
package/README.md CHANGED
@@ -68,6 +68,8 @@ Nothing to configure. On a WebGL build this package:
68
68
  2. **Writes `squidhub-bridge.js`** into the output and injects it ahead of the
69
69
  Unity loader.
70
70
  3. **Hoists every inline `<script>`** out of `index.html` into an external file.
71
+ 4. **Makes the game fill the frame** and hides Unity's own logo bar, build title
72
+ and fullscreen button.
71
73
 
72
74
  Step 3 is the one that matters. `squidhub-gamehost` serves
73
75
  `script-src 'self' 'wasm-unsafe-eval' blob:` with **no `'unsafe-inline'`**, and
@@ -76,29 +78,71 @@ all the markup is there, and nothing runs — the only signal is a CSP violation
76
78
  in the console. This is the single most common first-integration failure, and
77
79
  you should never see it.
78
80
 
81
+ ⚠️ **Step 4 is why your game is not a 960×600 box in the corner.** The portal
82
+ already letterboxes — it measures its box, clamps it to the aspect window you
83
+ declared and hands the frame an element of exactly that size. Unity's stock
84
+ template then renders a fixed 960×600 centred inside *that*, on a white page,
85
+ under a Unity logo and a second fullscreen button that fights the portal's own.
86
+ The footer is hidden rather than removed, because the stock bootstrap queries
87
+ the fullscreen button and throws on null — which surfaces as an `alert()` where
88
+ the game should be. Your loading bar is left alone: the portal draws none, so it
89
+ is the only progress a player sees.
90
+
79
91
  Then push the folder:
80
92
 
81
93
  ```sh
82
94
  node src/cli.mjs push --game <slug> --dir <BuildFolder> --env=dev --verify
83
95
  ```
84
96
 
97
+ ## Declaring what your game is
98
+
99
+ `SquidHub → Game Manifest…` edits `manifest.json` beside `Assets/` — the game's
100
+ id and title, which capabilities it uses, its save schema version, the
101
+ orientations and aspect window it wants, and whether it makes sound.
102
+
103
+ It is the same file `squidhub-release build --manifest manifest.json` reads, so
104
+ the Editor and the CLI cannot disagree about what your game declares. It belongs
105
+ in your repository.
106
+
107
+ The build writes it into the output folder for you. **That is the only thing
108
+ that makes a declaration take effect** — the server reads `manifest.json` out of
109
+ the uploaded bytes and ignores anything a client posts alongside the upload, so
110
+ a build folder without one declares nothing at all: save schema 1, no display
111
+ constraints, and a rollback guard that cannot fire.
112
+
113
+ ⚠️ **Ticking a capability does not grant it.** Capabilities are granted by the
114
+ platform at review. Declaring `commerce.spend` says your game uses it; until it
115
+ is granted the broker still answers `DENIED`, and your game has to handle that.
116
+ New games have `saves` and `analytics`.
117
+
118
+ ⚠️ **Keys the dialog does not model are preserved.** `skus`, `achievements` and
119
+ `rewardTable` live in the same file and survive a save untouched.
120
+
85
121
  ## Uploading from Unity
86
122
 
87
123
  `SquidHub → Upload Build…` does the whole publish — no console, no command line,
88
- no repository checkout.
124
+ no repository checkout. `SquidHub → Build and Upload…` picks a **build profile**,
125
+ builds it and uploads the result in one action.
89
126
 
90
127
  **Once:** in the publisher console open your game → **Upload keys** → create one,
91
128
  then paste it into `SquidHub → Upload Settings`.
92
129
 
93
130
  ⚠️ **The key never enters your project.** It lives in EditorPrefs, per machine
94
- and per project path, so it cannot reach your repository. It can do exactly one
95
- thing: upload a build for the one game it was minted for — it cannot read
96
- anything, publish to a channel, or touch another game. If it leaks, revoke it in
97
- the console; the worst it could have done is leave an unwanted build somewhere
98
- no player can see.
131
+ and per project path, so it cannot reach your repository. It can upload a build
132
+ for the one game it was minted for, and on **dev** publish that build — it
133
+ cannot read anything, touch another game, or publish on prod. If it leaks,
134
+ revoke it in the console.
135
+
136
+ ### Publishing on upload
137
+
138
+ **Publish the build** is ticked by default in `SquidHub → Build and Upload…`.
99
139
 
100
- ⚠️ **Uploading is not publishing.** The build arrives registered and not live.
101
- Promoting it to players stays a deliberate act in the console.
140
+ ⚠️ **It works on dev only.** On prod the server refuses it and says so; the
141
+ upload still succeeds and the build arrives registered and not live. Promoting a
142
+ build for every player is an owner decision made by a person in the console, and
143
+ an upload key is not a person — it sits in CI configs and Editor preferences.
144
+ That asymmetry is deliberate: it is what keeps a leaked key from replacing the
145
+ live build for everyone.
102
146
 
103
147
  The upload is refused before a byte moves if the build is precompressed or has
104
148
  no `index.html` at its top level, and OS metadata is dropped rather than sent —
@@ -141,19 +185,20 @@ blocked at load and shows up as a console error, not as a missing style.
141
185
  Everything must ship inside the build.
142
186
 
143
187
  ⚠️ **Do not write your own rotate prompt or call `screen.orientation.lock()`.**
144
- Declare the orientations you support in your `manifest.json` and the portal
145
- draws the prompt outside your frame. Two prompts fight, and yours is the one the
188
+ Declare the orientations you support in `SquidHub → Game Manifest…` and the
189
+ portal draws the prompt outside your frame. Two prompts fight, and yours is the one the
146
190
  player sees first.
147
191
 
148
192
  ⚠️ **The mute button is the portal's, but the muting happens in your frame.** A
149
193
  cross-origin iframe has no `muted` property, so the SDK interposes a gain node
150
194
  on your `AudioContext`. It works without you doing anything — but only because
151
195
  the bridge is injected ahead of the loader, which is why step 2 above is not
152
- merely tidy. Declare `"sound": true` in your manifest or no button appears.
196
+ merely tidy. Tick **Sound** in `SquidHub → Game Manifest…` or no button appears.
153
197
 
154
198
  ⚠️ **Editing `manifest.json` alone changes nothing.** It is excluded from the
155
199
  build hash, so a manifest-only re-upload registers no new version and your edit
156
- is discarded. Change a real file and rebuild.
200
+ is discarded — the server now says so in a warning rather than staying silent.
201
+ Change a real file and rebuild, or set an override in the console.
157
202
 
158
203
  ## ⚠️ Status
159
204
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "com.robotsquid.squidhub",
3
- "version": "0.5.6",
3
+ "version": "0.5.8",
4
4
  "displayName": "SquidHub",
5
5
  "description": "Cloud saves, analytics and the coin economy for Unity WebGL games published on SquidHub. Installs the C# wrapper, the .jslib half and the page bridge, and rewrites the exported index.html so it survives the platform's CSP.",
6
6
  "unity": "2021.3",