com.robotsquid.squidhub 0.5.5 → 0.5.7

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/CHANGELOG.md CHANGED
@@ -1,5 +1,57 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.5.7
4
+
5
+ **The manifest is a form now, and a build can go out in one action.**
6
+
7
+ `SquidHub → Game Manifest…` edits `manifest.json` beside `Assets/` — id, title,
8
+ capabilities, save schema, orientations, aspect window, sound. It is the same
9
+ file `squidhub-release` reads, so the Editor and the CLI cannot disagree about
10
+ what a game declares.
11
+
12
+ `SquidHub → Build and Upload…` picks a build profile, builds it, and uploads the
13
+ result through the same refusals a hand-picked folder goes through.
14
+
15
+ **The post-build step now writes `manifest.json` into the output.** Without it a
16
+ build declares nothing — the server reads the manifest out of the uploaded bytes
17
+ and ignores anything posted alongside the upload, so every Unity build made
18
+ before this one carried save schema 1 and no display constraints regardless of
19
+ what the project's manifest said.
20
+
21
+ ⚠️ **"Publish the build" is ticked by default and works on DEV ONLY.** On prod
22
+ the server refuses a key-authorised publish and the Editor says so; the upload
23
+ still succeeds and the build arrives registered, not live. Promoting a build for
24
+ every player is an owner decision made by a person in the console — an upload
25
+ key lives in CI configs and Editor preferences, and letting one promote on prod
26
+ would mean a leaked key can replace the live build for everyone. `keyMayPublish`
27
+ is an allowlist, so an environment invented later is refused by default rather
28
+ than inheriting the dangerous answer.
29
+
30
+ ⚠️ **The manifest editor preserves keys it does not model.** `skus`,
31
+ `achievements` and `rewardTable` survive a save untouched. Round-tripping
32
+ through a typed model would have deleted a publisher's catalogue declaration and
33
+ they would have found out one release later.
34
+
35
+ ⚠️ **A capability tick is a declaration, not a grant.** The dialog says so beside
36
+ the boxes that need review.
37
+
38
+ ## 0.5.6
39
+
40
+ ⚠️ **Fixes an upload that did nothing and said nothing.** `SquidHub → Upload
41
+ Build…` in 0.5.5 ran, reported no error, and uploaded no files.
42
+
43
+ `yield return SomeOtherIEnumerator()` requires the coroutine driver to DESCEND
44
+ into the nested routine. Unity's own runner does; the hand-rolled one here did
45
+ not — it treated the nested iterator as an ordinary yielded value, discarded it,
46
+ and carried on. So the request that starts a build never executed, the response
47
+ stayed null, and the routine exited quietly on a null check.
48
+
49
+ The driver is now `SquidHubPump`, which has no `UnityEditor` dependency and is
50
+ unit-tested for exactly that nesting rule, three levels deep. The JSON field
51
+ reader moved into the tested file too, since a null that looks like an answer is
52
+ the same failure in a different costume, and the run now logs what it is doing
53
+ so a silent no-op cannot recur.
54
+
3
55
  ## 0.5.5
4
56
 
5
57
  **Upload a build straight from Unity.** `SquidHub → Upload Build…` picks the
@@ -83,12 +83,48 @@ namespace SquidHub.Editor
83
83
  File.WriteAllText(indexPath, result.Html);
84
84
  foreach (var note in result.Notes) log.Append(" ✓ ").Append(note).Append('\n');
85
85
 
86
+ WriteManifest(outDir, log);
87
+
86
88
  log.Append("\nPush it with:\n node src/cli.mjs push --game <slug> --dir ")
87
89
  .Append(outDir).Append(" --env=dev --verify");
88
90
 
89
91
  Debug.Log(log.ToString());
90
92
  }
91
93
 
94
+ /// <summary>
95
+ /// Writes the game's declaration into the build.
96
+ ///
97
+ /// ⚠️ Without this file the build declares NOTHING. The server reads
98
+ /// `manifest.json` out of the uploaded bytes — never from anything the
99
+ /// client posts alongside the upload — so an absent manifest means the
100
+ /// default everything: save schema 1, no display constraints, and a
101
+ /// rollback guard that cannot fire.
102
+ ///
103
+ /// ⚠️ A WARNING, not a build failure. A publisher mid-setup has no
104
+ /// manifest yet and their build is still perfectly runnable; refusing it
105
+ /// would make the package harder to adopt than to ignore. An
106
+ /// UNREADABLE or INVALID manifest is louder, because that one is a file
107
+ /// they wrote and believe is in effect.
108
+ /// </summary>
109
+ private static void WriteManifest(string outDir, StringBuilder log)
110
+ {
111
+ if (!File.Exists(SquidHubUploadSettings.ManifestPath))
112
+ {
113
+ log.Append(" · no manifest.json — this build declares nothing. " +
114
+ "Set one up in SquidHub → Game Manifest…\n");
115
+ return;
116
+ }
117
+
118
+ if (!SquidHubManifestWindow.WriteInto(outDir, out var problem))
119
+ {
120
+ Debug.LogError("SquidHub: manifest.json was NOT written into the build, so it " +
121
+ "declares nothing:\n" + problem);
122
+ return;
123
+ }
124
+
125
+ log.Append(" ✓ wrote manifest.json\n");
126
+ }
127
+
92
128
  private static bool InstallBridge(string outDir, StringBuilder log)
93
129
  {
94
130
  var source = SquidHubPackage.FindBridge();
@@ -141,6 +141,29 @@ namespace SquidHub.Editor
141
141
  return "[" + string.Join(",", parts) + "]";
142
142
  }
143
143
 
144
+ /// <summary>
145
+ /// Reads one flat field out of a JSON response.
146
+ ///
147
+ /// ⚠️ A regex, not a parser, and deliberately only for the handful of
148
+ /// flat fields this flow reads. Shipping a parser in a package that
149
+ /// already asks the project to bring its own would be a second, worse
150
+ /// one to maintain.
151
+ ///
152
+ /// ⚠️ Returns null when absent, and the caller must treat that as a
153
+ /// failure rather than a value. Every silent no-op in this uploader has
154
+ /// come from a null that looked like an answer.
155
+ /// </summary>
156
+ public static string JsonField(string json, string name)
157
+ {
158
+ if (string.IsNullOrEmpty(json)) return null;
159
+ var match = System.Text.RegularExpressions.Regex.Match(
160
+ json,
161
+ "\"" + System.Text.RegularExpressions.Regex.Escape(name) +
162
+ "\"\\s*:\\s*(?:\"([^\"]*)\"|(true|false|-?[0-9.]+))");
163
+ if (!match.Success) return null;
164
+ return match.Groups[1].Success ? match.Groups[1].Value : match.Groups[2].Value;
165
+ }
166
+
144
167
  internal static string JsonString(string value)
145
168
  {
146
169
  var sb = new System.Text.StringBuilder("\"");
@@ -0,0 +1,291 @@
1
+ // SquidHub → Build and Upload… — pick a build profile, build, upload, publish.
2
+ //
3
+ // ⚠️ Build profiles are a Unity 6 feature and this package supports 2021.3, so
4
+ // everything that touches `UnityEditor.Build.Profile` is behind a version
5
+ // guard. On an older Editor the window still uploads; it just cannot build.
6
+ //
7
+ // ⚠️ It uploads THE FOLDER IT JUST BUILT, through the same
8
+ // `SquidHubUploader.Begin` a hand-picked folder goes through. One set of
9
+ // refusals, so a build made here cannot bypass a check a manual upload applies.
10
+ using System.IO;
11
+ using System.Linq;
12
+ using UnityEditor;
13
+ using UnityEditor.Build.Reporting;
14
+ using UnityEngine;
15
+ #if UNITY_6000_0_OR_NEWER
16
+ using UnityEditor.Build.Profile;
17
+ #endif
18
+
19
+ namespace SquidHub.Editor
20
+ {
21
+ internal sealed class SquidHubBuildWindow : EditorWindow
22
+ {
23
+ private Vector2 _scroll;
24
+
25
+ #if UNITY_6000_0_OR_NEWER
26
+ private BuildProfile[] _profiles = new BuildProfile[0];
27
+ private string[] _names = new string[0];
28
+ private int _selected = -1;
29
+ #endif
30
+
31
+ [MenuItem("SquidHub/Build and Upload…", priority = 12)]
32
+ private static void Open()
33
+ {
34
+ var window = GetWindow<SquidHubBuildWindow>(true, "SquidHub Build and Upload", true);
35
+ window.minSize = new Vector2(520, 400);
36
+ window.Refresh();
37
+ }
38
+
39
+ private void Refresh()
40
+ {
41
+ #if UNITY_6000_0_OR_NEWER
42
+ // ⚠️ AssetDatabase, not BuildProfile's own enumerator. This is the
43
+ // public, version-stable way to find them, and it also picks up a
44
+ // profile created since the window was opened.
45
+ _profiles = AssetDatabase.FindAssets("t:BuildProfile")
46
+ .Select(AssetDatabase.GUIDToAssetPath)
47
+ .OrderBy(path => path)
48
+ .Select(AssetDatabase.LoadAssetAtPath<BuildProfile>)
49
+ .Where(profile => profile != null)
50
+ .ToArray();
51
+
52
+ _names = _profiles.Select(p => AssetDatabase.GetAssetPath(p)).ToArray();
53
+
54
+ var remembered = SquidHubUploadSettings.BuildProfilePath;
55
+ _selected = System.Array.IndexOf(_names, remembered);
56
+
57
+ // Fall back to whatever the Editor itself is set to build.
58
+ if (_selected < 0)
59
+ {
60
+ var active = BuildProfile.GetActiveBuildProfile();
61
+ if (active != null)
62
+ _selected = System.Array.IndexOf(_names, AssetDatabase.GetAssetPath(active));
63
+ }
64
+ if (_selected < 0 && _profiles.Length > 0) _selected = 0;
65
+ #endif
66
+ }
67
+
68
+ private void OnGUI()
69
+ {
70
+ _scroll = EditorGUILayout.BeginScrollView(_scroll);
71
+
72
+ DrawProfile();
73
+ EditorGUILayout.Space();
74
+ DrawOutput();
75
+ EditorGUILayout.Space();
76
+ DrawPublish();
77
+ EditorGUILayout.Space();
78
+ DrawManifestSummary();
79
+
80
+ EditorGUILayout.EndScrollView();
81
+ EditorGUILayout.Space();
82
+ DrawButtons();
83
+ }
84
+
85
+ private void DrawProfile()
86
+ {
87
+ EditorGUILayout.LabelField("Build profile", EditorStyles.boldLabel);
88
+ #if UNITY_6000_0_OR_NEWER
89
+ if (_profiles.Length == 0)
90
+ {
91
+ EditorGUILayout.HelpBox(
92
+ "No build profiles in this project. Create one in File → Build Profiles, " +
93
+ "targeting Web, then reopen this window.",
94
+ MessageType.Warning);
95
+ if (GUILayout.Button("Refresh")) Refresh();
96
+ return;
97
+ }
98
+
99
+ var chosen = EditorGUILayout.Popup("Profile", Mathf.Max(0, _selected), _names);
100
+ if (chosen != _selected)
101
+ {
102
+ _selected = chosen;
103
+ SquidHubUploadSettings.BuildProfilePath = _names[_selected];
104
+ }
105
+
106
+ // ⚠️ The profile's target is not checked here — there is no stable
107
+ // public accessor for it — so it is checked after the build from the
108
+ // BuildReport, which is authoritative. A non-Web profile builds and
109
+ // is then refused, rather than uploading something no browser runs.
110
+ EditorGUILayout.HelpBox(
111
+ "The profile must target Web. A build for any other platform is refused after " +
112
+ "building rather than uploaded.",
113
+ MessageType.None);
114
+
115
+ if (GUILayout.Button("Refresh list")) Refresh();
116
+ #else
117
+ EditorGUILayout.HelpBox(
118
+ "Build profiles need Unity 6. On this Editor, build with File → Build Settings " +
119
+ "and then use Upload, or SquidHub → Upload Build….",
120
+ MessageType.Info);
121
+ #endif
122
+ }
123
+
124
+ private void DrawOutput()
125
+ {
126
+ EditorGUILayout.LabelField("Output folder", EditorStyles.boldLabel);
127
+ using (new EditorGUILayout.HorizontalScope())
128
+ {
129
+ EditorGUILayout.SelectableLabel(
130
+ string.IsNullOrEmpty(SquidHubUploadSettings.BuildFolder)
131
+ ? "(none chosen)"
132
+ : SquidHubUploadSettings.BuildFolder,
133
+ EditorStyles.textField, GUILayout.Height(EditorGUIUtility.singleLineHeight));
134
+
135
+ if (GUILayout.Button("Choose…", GUILayout.Width(90)))
136
+ {
137
+ var start = SquidHubUploadSettings.BuildFolder;
138
+ var folder = EditorUtility.SaveFolderPanel(
139
+ "Build into", string.IsNullOrEmpty(start) ? "" : start, "web");
140
+ if (!string.IsNullOrEmpty(folder)) SquidHubUploadSettings.BuildFolder = folder;
141
+ }
142
+ }
143
+ }
144
+
145
+ private void DrawPublish()
146
+ {
147
+ EditorGUILayout.LabelField("On a successful upload", EditorStyles.boldLabel);
148
+
149
+ SquidHubUploadSettings.PublishAfterUpload = EditorGUILayout.ToggleLeft(
150
+ new GUIContent("Publish the build",
151
+ "Makes the build live for players. The server allows this on dev only."),
152
+ SquidHubUploadSettings.PublishAfterUpload);
153
+
154
+ if (SquidHubUploadSettings.PublishAfterUpload)
155
+ {
156
+ // ⚠️ Stated plainly rather than left to be discovered. The tick is
157
+ // on by default and does nothing on prod, and a publisher who
158
+ // believes otherwise ships a build they think is live.
159
+ EditorGUILayout.HelpBox(
160
+ "This works on dev only. On prod the server refuses it — promoting a build " +
161
+ "for every player is an owner decision made in the console, and an upload key " +
162
+ "is not a person. The upload still succeeds; you will be told it was not published.",
163
+ MessageType.Info);
164
+ }
165
+
166
+ EditorGUILayout.LabelField("Console", SquidHubUploadSettings.Endpoint, EditorStyles.miniLabel);
167
+ }
168
+
169
+ private void DrawManifestSummary()
170
+ {
171
+ EditorGUILayout.LabelField("Declaration", EditorStyles.boldLabel);
172
+
173
+ if (!File.Exists(SquidHubUploadSettings.ManifestPath))
174
+ {
175
+ EditorGUILayout.HelpBox(
176
+ "No manifest.json, so this build will declare nothing: save schema 1 and no " +
177
+ "display constraints.",
178
+ MessageType.Warning);
179
+ }
180
+ else
181
+ {
182
+ try
183
+ {
184
+ var manifest = SquidHubManifest.Load(SquidHubUploadSettings.ManifestPath);
185
+ var errors = manifest.Errors();
186
+ if (errors.Count > 0)
187
+ {
188
+ EditorGUILayout.HelpBox(
189
+ "manifest.json is not valid, and the build will not carry it:\n\n" +
190
+ string.Join("\n", errors),
191
+ MessageType.Error);
192
+ }
193
+ else
194
+ {
195
+ EditorGUILayout.LabelField(
196
+ manifest.GameId + " · save schema " + manifest.SaveSchemaVersion +
197
+ " · " + string.Join(", ", manifest.Capabilities),
198
+ EditorStyles.miniLabel);
199
+ }
200
+ }
201
+ catch (System.Exception e)
202
+ {
203
+ EditorGUILayout.HelpBox("manifest.json could not be read: " + e.Message, MessageType.Error);
204
+ }
205
+ }
206
+
207
+ if (GUILayout.Button("Edit manifest…")) SquidHubManifestWindow.Open();
208
+ }
209
+
210
+ private void DrawButtons()
211
+ {
212
+ using (new EditorGUILayout.HorizontalScope())
213
+ {
214
+ #if UNITY_6000_0_OR_NEWER
215
+ using (new EditorGUI.DisabledScope(_profiles.Length == 0 || _selected < 0))
216
+ {
217
+ if (GUILayout.Button("Build and Upload", GUILayout.Height(28))) BuildThenUpload();
218
+ if (GUILayout.Button("Build only", GUILayout.Height(28))) Build();
219
+ }
220
+ #endif
221
+ if (GUILayout.Button("Upload only", GUILayout.Height(28)))
222
+ {
223
+ var folder = SquidHubUploadSettings.BuildFolder;
224
+ if (string.IsNullOrEmpty(folder) || !Directory.Exists(folder))
225
+ EditorUtility.DisplayDialog("SquidHub", "Choose an output folder first.", "OK");
226
+ else
227
+ SquidHubUploader.Begin(folder);
228
+ }
229
+ }
230
+ }
231
+
232
+ #if UNITY_6000_0_OR_NEWER
233
+ private void BuildThenUpload()
234
+ {
235
+ // ⚠️ The key is checked BEFORE building. Discovering there is no key
236
+ // after a twenty-minute WebGL build is the kind of thing that makes
237
+ // people go back to the command line.
238
+ if (!SquidHubUploader.EnsureKey()) return;
239
+
240
+ var folder = Build();
241
+ if (folder != null) SquidHubUploader.Begin(folder);
242
+ }
243
+
244
+ /// <summary>The folder that was built into, or null when the build did not produce one.</summary>
245
+ private string Build()
246
+ {
247
+ var folder = SquidHubUploadSettings.BuildFolder;
248
+ if (string.IsNullOrEmpty(folder))
249
+ {
250
+ EditorUtility.DisplayDialog("SquidHub", "Choose an output folder first.", "OK");
251
+ return null;
252
+ }
253
+
254
+ var profile = _profiles[_selected];
255
+
256
+ // ⚠️ Made active first. A profile passed only to BuildPlayer leaves
257
+ // the Editor's own target where it was, and the next manual build
258
+ // from File → Build silently uses a different one.
259
+ BuildProfile.SetActiveBuildProfile(profile);
260
+
261
+ var report = BuildPipeline.BuildPlayer(new BuildPlayerWithProfileOptions
262
+ {
263
+ buildProfile = profile,
264
+ locationPathName = folder,
265
+ options = BuildOptions.None,
266
+ });
267
+
268
+ if (report == null || report.summary.result != BuildResult.Succeeded)
269
+ {
270
+ // ⚠️ No dialog. A failed build has already filled the Console with
271
+ // the reason, and a modal saying "it failed" on top of that just
272
+ // has to be dismissed before anyone can read it.
273
+ Debug.LogError("[SquidHub] the build did not succeed — nothing was uploaded.");
274
+ return null;
275
+ }
276
+
277
+ if (report.summary.platform != BuildTarget.WebGL)
278
+ {
279
+ EditorUtility.DisplayDialog("SquidHub",
280
+ "That profile built for " + report.summary.platform + ", not Web.\n\n" +
281
+ "SquidHub serves WebGL builds, and the bridge was not installed into this one. " +
282
+ "Point the profile at the Web platform and build again.",
283
+ "OK");
284
+ return null;
285
+ }
286
+
287
+ return folder;
288
+ }
289
+ #endif
290
+ }
291
+ }
@@ -0,0 +1,11 @@
1
+ fileFormatVersion: 2
2
+ guid: c05e02aea772f507162356a3628335ef
3
+ MonoImporter:
4
+ externalObjects: {}
5
+ serializedVersion: 2
6
+ defaultReferences: []
7
+ executionOrder: 0
8
+ icon: {instanceID: 0}
9
+ userData:
10
+ assetBundleName:
11
+ assetBundleVariant: