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.
- package/CHANGELOG.md +70 -0
- package/Editor/SquidHubBuildProcessor.cs +36 -0
- package/Editor/SquidHubBuildScan.cs +45 -0
- package/Editor/SquidHubBuildWindow.cs +291 -0
- package/Editor/SquidHubBuildWindow.cs.meta +11 -0
- package/Editor/SquidHubHtml.cs +95 -1
- package/Editor/SquidHubJson.cs +346 -0
- package/Editor/SquidHubJson.cs.meta +11 -0
- package/Editor/SquidHubManifest.cs +355 -0
- package/Editor/SquidHubManifest.cs.meta +11 -0
- package/Editor/SquidHubManifestWindow.cs +292 -0
- package/Editor/SquidHubManifestWindow.cs.meta +11 -0
- package/Editor/SquidHubUploadSettings.cs +40 -0
- package/Editor/SquidHubUploader.cs +139 -16
- package/README.md +57 -12
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,75 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.5.8
|
|
4
|
+
|
|
5
|
+
**The game fills the frame, and Unity's chrome is gone.**
|
|
6
|
+
|
|
7
|
+
The portal already does the letterboxing: `game-frame.tsx` measures its box,
|
|
8
|
+
clamps it to the aspect window in `manifest.json` and hands the iframe an
|
|
9
|
+
element of exactly that size. Unity's Default template then rendered a fixed
|
|
10
|
+
960×600 box centred inside that — letterboxing a second time, at the wrong size,
|
|
11
|
+
on a white page, under a Unity logo, a build title and a second fullscreen
|
|
12
|
+
button. Measured in a browser at a 1555×1290 frame: the canvas was 960×600 at
|
|
13
|
+
offset (298, 326), using 29% of the area it was given.
|
|
14
|
+
|
|
15
|
+
The post-build step now injects a stylesheet that fills the frame and hides
|
|
16
|
+
`#unity-footer`. Verified in Chrome on a real build: the canvas fills the
|
|
17
|
+
viewport exactly, no scrollbars, and the WebGL render target follows at
|
|
18
|
+
3110×2580 for devicePixelRatio 2 — full resolution, not an upscale.
|
|
19
|
+
|
|
20
|
+
⚠️ **The footer is HIDDEN, never removed.** The stock bootstrap assigns
|
|
21
|
+
`document.querySelector("#unity-fullscreen-button").onclick` inside the promise
|
|
22
|
+
that resolves the instance. Delete the element and that throws, the chain falls
|
|
23
|
+
into `.catch(alert)`, and the player gets a modal dialog instead of a game.
|
|
24
|
+
|
|
25
|
+
⚠️ **The loading bar stays.** The portal draws no loading UI of its own, so it
|
|
26
|
+
is the only progress a player sees while a 30 MB build downloads.
|
|
27
|
+
|
|
28
|
+
⚠️ **The engine splash screen is untouched.** "Made with Unity" is a licence
|
|
29
|
+
term of Unity Personal and is not template chrome; this strips the template's
|
|
30
|
+
own logo bar, nothing else.
|
|
31
|
+
|
|
32
|
+
**Uploading an older build now warns rather than staying quiet** — one warning
|
|
33
|
+
for a build with no bridge at all, another for a bridged build made before frame
|
|
34
|
+
fitting. Neither refuses: both upload and register perfectly, they just play
|
|
35
|
+
badly, and the fix is a rebuild. It cannot be repaired at upload time — writing
|
|
36
|
+
a bridge tag for a file that is not in the folder trades the handshake for a 404.
|
|
37
|
+
|
|
38
|
+
## 0.5.7
|
|
39
|
+
|
|
40
|
+
**The manifest is a form now, and a build can go out in one action.**
|
|
41
|
+
|
|
42
|
+
`SquidHub → Game Manifest…` edits `manifest.json` beside `Assets/` — id, title,
|
|
43
|
+
capabilities, save schema, orientations, aspect window, sound. It is the same
|
|
44
|
+
file `squidhub-release` reads, so the Editor and the CLI cannot disagree about
|
|
45
|
+
what a game declares.
|
|
46
|
+
|
|
47
|
+
`SquidHub → Build and Upload…` picks a build profile, builds it, and uploads the
|
|
48
|
+
result through the same refusals a hand-picked folder goes through.
|
|
49
|
+
|
|
50
|
+
**The post-build step now writes `manifest.json` into the output.** Without it a
|
|
51
|
+
build declares nothing — the server reads the manifest out of the uploaded bytes
|
|
52
|
+
and ignores anything posted alongside the upload, so every Unity build made
|
|
53
|
+
before this one carried save schema 1 and no display constraints regardless of
|
|
54
|
+
what the project's manifest said.
|
|
55
|
+
|
|
56
|
+
⚠️ **"Publish the build" is ticked by default and works on DEV ONLY.** On prod
|
|
57
|
+
the server refuses a key-authorised publish and the Editor says so; the upload
|
|
58
|
+
still succeeds and the build arrives registered, not live. Promoting a build for
|
|
59
|
+
every player is an owner decision made by a person in the console — an upload
|
|
60
|
+
key lives in CI configs and Editor preferences, and letting one promote on prod
|
|
61
|
+
would mean a leaked key can replace the live build for everyone. `keyMayPublish`
|
|
62
|
+
is an allowlist, so an environment invented later is refused by default rather
|
|
63
|
+
than inheriting the dangerous answer.
|
|
64
|
+
|
|
65
|
+
⚠️ **The manifest editor preserves keys it does not model.** `skus`,
|
|
66
|
+
`achievements` and `rewardTable` survive a save untouched. Round-tripping
|
|
67
|
+
through a typed model would have deleted a publisher's catalogue declaration and
|
|
68
|
+
they would have found out one release later.
|
|
69
|
+
|
|
70
|
+
⚠️ **A capability tick is a declaration, not a grant.** The dialog says so beside
|
|
71
|
+
the boxes that need review.
|
|
72
|
+
|
|
3
73
|
## 0.5.6
|
|
4
74
|
|
|
5
75
|
⚠️ **Fixes an upload that did nothing and said nothing.** `SquidHub → Upload
|
|
@@ -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();
|
|
@@ -29,6 +29,8 @@ namespace SquidHub.Editor
|
|
|
29
29
|
public readonly List<string> Refusals = new List<string>();
|
|
30
30
|
/// <summary>OS metadata dropped. Reported, never silent.</summary>
|
|
31
31
|
public readonly List<string> Skipped = new List<string>();
|
|
32
|
+
/// <summary>Things wrong with the build that do not justify refusing it.</summary>
|
|
33
|
+
public readonly List<string> Warnings = new List<string>();
|
|
32
34
|
public long TotalBytes;
|
|
33
35
|
public bool HasEntryPoint;
|
|
34
36
|
}
|
|
@@ -111,9 +113,52 @@ namespace SquidHub.Editor
|
|
|
111
113
|
"Point this at the folder Unity built into, not at its parent.");
|
|
112
114
|
}
|
|
113
115
|
|
|
116
|
+
InspectEntryPoint(scan);
|
|
114
117
|
return scan;
|
|
115
118
|
}
|
|
116
119
|
|
|
120
|
+
/// <summary>
|
|
121
|
+
/// What the exported index.html says about how it was built.
|
|
122
|
+
///
|
|
123
|
+
/// ⚠️ WARNINGS, NEVER REFUSALS. Both of these describe a build that
|
|
124
|
+
/// uploads and registers perfectly — one just plays badly, and the other
|
|
125
|
+
/// is somebody's custom pipeline we have no business overruling from
|
|
126
|
+
/// here. The refusals above are for builds the SERVER would reject; this
|
|
127
|
+
/// is for builds the PLAYER would notice.
|
|
128
|
+
///
|
|
129
|
+
/// ⚠️ And it cannot be fixed at upload time, which is why it only warns.
|
|
130
|
+
/// Injecting the bridge tag here would name a squidhub-bridge.js that is
|
|
131
|
+
/// not in the folder — a 404 in place of the handshake, which is worse
|
|
132
|
+
/// than the problem it set out to solve. The fix is a rebuild.
|
|
133
|
+
/// </summary>
|
|
134
|
+
private static void InspectEntryPoint(BuildScan scan)
|
|
135
|
+
{
|
|
136
|
+
var entry = scan.Files.FirstOrDefault(f => f.Path == "index.html");
|
|
137
|
+
if (entry == null) return;
|
|
138
|
+
|
|
139
|
+
string html;
|
|
140
|
+
try { html = File.ReadAllText(entry.FullPath); }
|
|
141
|
+
catch { return; }
|
|
142
|
+
|
|
143
|
+
if (html.IndexOf("squidhub-bridge.js", StringComparison.OrdinalIgnoreCase) < 0)
|
|
144
|
+
{
|
|
145
|
+
scan.Warnings.Add(
|
|
146
|
+
"index.html does not reference squidhub-bridge.js, so this build was not made " +
|
|
147
|
+
"with the SquidHub package installed. There is no window.SquidHub and every " +
|
|
148
|
+
"handshake will time out. Build again with the package in the project.");
|
|
149
|
+
return;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (html.IndexOf(SquidHubHtml.FitStyleId, StringComparison.OrdinalIgnoreCase) < 0)
|
|
153
|
+
{
|
|
154
|
+
scan.Warnings.Add(
|
|
155
|
+
"This build predates frame fitting, so Unity's template chrome is still in it: " +
|
|
156
|
+
"the game renders in a fixed 960x600 box centred in whatever frame the portal " +
|
|
157
|
+
"gives it, on a white page, under a Unity logo and a second fullscreen button. " +
|
|
158
|
+
"Rebuild to fill the frame.");
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
117
162
|
private static string ToHex(byte[] bytes)
|
|
118
163
|
{
|
|
119
164
|
var chars = new char[bytes.Length * 2];
|
|
@@ -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
|
+
}
|
package/Editor/SquidHubHtml.cs
CHANGED
|
@@ -49,6 +49,66 @@ namespace SquidHub.Editor
|
|
|
49
49
|
"text/ecmascript", "application/ecmascript",
|
|
50
50
|
};
|
|
51
51
|
|
|
52
|
+
/// <summary>Marks our stylesheet, so a reprocess can recognise its own work.</summary>
|
|
53
|
+
internal const string FitStyleId = "squidhub-fit";
|
|
54
|
+
|
|
55
|
+
/// <summary>
|
|
56
|
+
/// Makes the game fill the frame the portal gives it, and hides the
|
|
57
|
+
/// template's own chrome.
|
|
58
|
+
///
|
|
59
|
+
/// ⚠️ THE PORTAL ALREADY DID THE LETTERBOXING. `game-frame.tsx` measures
|
|
60
|
+
/// its box, clamps it to the aspect window declared in manifest.json and
|
|
61
|
+
/// hands the iframe an element of exactly that size with black bars
|
|
62
|
+
/// around it. Unity's Default template then renders a fixed 960×600 box
|
|
63
|
+
/// centred inside that — letterboxing a second time, at the wrong size,
|
|
64
|
+
/// so a 4K monitor gets a postage stamp. Filling is not a preference
|
|
65
|
+
/// here; the frame is already the right shape.
|
|
66
|
+
///
|
|
67
|
+
/// ⚠️ `!important` is doing real work. The template's bootstrap sets
|
|
68
|
+
/// `fullscreenContainer.style.width = "960px"` as an INLINE style, and
|
|
69
|
+
/// the Minimal template writes inline px on the canvas itself. A plain
|
|
70
|
+
/// rule loses to both.
|
|
71
|
+
///
|
|
72
|
+
/// ⚠️ `#unity-footer` is HIDDEN, never removed. The stock bootstrap does
|
|
73
|
+
/// `document.querySelector("#unity-fullscreen-button").onclick = …`
|
|
74
|
+
/// inside the promise that resolves the instance; delete the element and
|
|
75
|
+
/// that throws, the chain falls into `.catch(alert)`, and the player gets
|
|
76
|
+
/// a modal dialog instead of a game. Hiding leaves every query answerable.
|
|
77
|
+
///
|
|
78
|
+
/// ⚠️ The loading bar STAYS. The portal draws no loading UI of its own,
|
|
79
|
+
/// so this is the only progress a player sees while a 30 MB build
|
|
80
|
+
/// downloads — hiding it would trade a Unity logo for a black screen.
|
|
81
|
+
///
|
|
82
|
+
/// ⚠️ An inline <style> is safe where an inline script is not:
|
|
83
|
+
/// gamehost sends `style-src 'self' 'unsafe-inline'` but no
|
|
84
|
+
/// `'unsafe-inline'` for scripts. That asymmetry is why this is a style
|
|
85
|
+
/// block and the bootstrap had to be hoisted to a file.
|
|
86
|
+
/// </summary>
|
|
87
|
+
private static readonly string FitStyle =
|
|
88
|
+
"<style id=\"" + FitStyleId + "\">\n" +
|
|
89
|
+
" /* SquidHub: the portal sizes and letterboxes the frame; the game fills it. */\n" +
|
|
90
|
+
" html, body { width: 100%; height: 100%; margin: 0; padding: 0; overflow: hidden; }\n" +
|
|
91
|
+
" #unity-container,\n" +
|
|
92
|
+
" #unity-container.unity-desktop,\n" +
|
|
93
|
+
" #unity-container.unity-mobile {\n" +
|
|
94
|
+
" position: absolute; left: 0; top: 0;\n" +
|
|
95
|
+
" width: 100%; height: 100%; transform: none;\n" +
|
|
96
|
+
" }\n" +
|
|
97
|
+
" #unity-fullscreen-container {\n" +
|
|
98
|
+
" position: absolute; left: 0; top: 0;\n" +
|
|
99
|
+
" width: 100% !important; height: 100% !important;\n" +
|
|
100
|
+
" }\n" +
|
|
101
|
+
" #unity-canvas {\n" +
|
|
102
|
+
" display: block; left: 0; top: 0;\n" +
|
|
103
|
+
" width: 100% !important; height: 100% !important;\n" +
|
|
104
|
+
" }\n" +
|
|
105
|
+
" /* Unity\u0027s own logo, build title and fullscreen button. The portal\n" +
|
|
106
|
+
" draws its own fullscreen control from display.fullscreen, and two\n" +
|
|
107
|
+
" of them fight. Hidden rather than removed — the bootstrap still\n" +
|
|
108
|
+
" queries the button and would throw on null. */\n" +
|
|
109
|
+
" #unity-footer { display: none !important; }\n" +
|
|
110
|
+
" </style>\n";
|
|
111
|
+
|
|
52
112
|
public static Result Apply(string html, string bridgeFileName)
|
|
53
113
|
{
|
|
54
114
|
var result = new Result { Html = html };
|
|
@@ -83,7 +143,7 @@ namespace SquidHub.Editor
|
|
|
83
143
|
});
|
|
84
144
|
|
|
85
145
|
var tag = "<script src=\"" + bridgeFileName + "\"></script>";
|
|
86
|
-
result.Html = InsertBridge(rewritten, tag, result);
|
|
146
|
+
result.Html = InsertFitStyle(InsertBridge(rewritten, tag, result), result);
|
|
87
147
|
result.Changed = !ReferenceEquals(result.Html, html) && result.Html != html;
|
|
88
148
|
return result;
|
|
89
149
|
}
|
|
@@ -98,6 +158,40 @@ namespace SquidHub.Editor
|
|
|
98
158
|
return ExecutableTypes.Contains(value.Trim());
|
|
99
159
|
}
|
|
100
160
|
|
|
161
|
+
/// <summary>
|
|
162
|
+
/// Puts the stylesheet last in <head>, so it follows the template's own
|
|
163
|
+
/// `TemplateData/style.css` and wins on order as well as on specificity.
|
|
164
|
+
/// </summary>
|
|
165
|
+
private static string InsertFitStyle(string html, Result result)
|
|
166
|
+
{
|
|
167
|
+
// Somebody's custom template may already carry it; do not stack a second.
|
|
168
|
+
if (html.IndexOf(FitStyleId, StringComparison.OrdinalIgnoreCase) >= 0)
|
|
169
|
+
{
|
|
170
|
+
result.Notes.Add("fit stylesheet already present — left alone");
|
|
171
|
+
return html;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
var at = html.IndexOf("</head>", StringComparison.OrdinalIgnoreCase);
|
|
175
|
+
if (at >= 0)
|
|
176
|
+
{
|
|
177
|
+
result.Notes.Add("canvas set to fill the frame, Unity footer hidden");
|
|
178
|
+
return html.Substring(0, at) + " " + FitStyle + html.Substring(at);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// ⚠️ A document with no </head> still gets it, before the first tag
|
|
182
|
+
// that could render. An unstyled page here is a game the player sees
|
|
183
|
+
// at 960×600 in the middle of a 4K frame, which reads as broken.
|
|
184
|
+
at = html.IndexOf("<body", StringComparison.OrdinalIgnoreCase);
|
|
185
|
+
if (at >= 0)
|
|
186
|
+
{
|
|
187
|
+
result.Notes.Add("no </head> — fit stylesheet injected before <body>");
|
|
188
|
+
return html.Substring(0, at) + FitStyle + html.Substring(at);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
result.Notes.Add("no </head> or <body> — fit stylesheet prepended");
|
|
192
|
+
return FitStyle + html;
|
|
193
|
+
}
|
|
194
|
+
|
|
101
195
|
// ⚠️ Before the FIRST script in the document, not merely before the
|
|
102
196
|
// loader. installBridge() patches AudioContext.destination on the
|
|
103
197
|
// instance, so it only catches contexts created after it runs — land
|