@bitmagic/cli 0.1.64-dev.9 → 0.1.64
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/README.md +28 -15
- package/assets/blender-level/README.md +6 -1
- package/assets/blender-level/bmlevel/core.py +78 -1
- package/assets/blender-level/review.py +106 -11
- package/assets/blender-level/tests/test_core.py +49 -0
- package/dist/cli.d.ts +8 -0
- package/dist/commands/forge.d.ts +8 -2
- package/dist/commands/forge.js +10 -4
- package/dist/commands/forge.js.map +1 -1
- package/dist/commands/generate.js +1 -1
- package/dist/commands/generate.js.map +1 -1
- package/dist/commands/publish.d.ts +5 -0
- package/dist/commands/publish.js +29 -3
- package/dist/commands/publish.js.map +1 -1
- package/dist/editor/design.d.ts +1 -0
- package/dist/editor/design.js +7 -3
- package/dist/editor/design.js.map +1 -1
- package/dist/editor/game-info-panel.d.ts +33 -0
- package/dist/editor/game-info-panel.js +179 -0
- package/dist/editor/game-info-panel.js.map +1 -0
- package/dist/editor/game-info.d.ts +55 -0
- package/dist/editor/game-info.js +126 -0
- package/dist/editor/game-info.js.map +1 -0
- package/dist/editor/server.js +59 -3
- package/dist/editor/server.js.map +1 -1
- package/dist/editor/shell-page.js +196 -11
- package/dist/editor/shell-page.js.map +1 -1
- package/dist/forge/two-d-gate.d.ts +10 -0
- package/dist/forge/two-d-gate.js +35 -1
- package/dist/forge/two-d-gate.js.map +1 -1
- package/dist/ios/collect-urls.d.ts +2 -0
- package/dist/ios/collect-urls.js +2 -2
- package/dist/ios/collect-urls.js.map +1 -1
- package/dist/publish/client.d.ts +18 -0
- package/dist/publish/client.js.map +1 -1
- package/dist/publish/record.d.ts +9 -1
- package/dist/publish/record.js +2 -0
- package/dist/publish/record.js.map +1 -1
- package/dist/publish/urls.d.ts +29 -0
- package/dist/publish/urls.js +39 -0
- package/dist/publish/urls.js.map +1 -0
- package/dist/scaffold/blender-level-skill-files.d.ts +7 -0
- package/dist/scaffold/blender-level-skill-files.js +129 -21
- package/dist/scaffold/blender-level-skill-files.js.map +1 -1
- package/dist/scaffold/project-files.js +2 -2
- package/dist/scaffold/skill-files.js +18 -1
- package/dist/scaffold/skill-files.js.map +1 -1
- package/package.json +4 -4
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The Game-info dialog's CSS and markup, composed into `shell-page.ts`.
|
|
3
|
+
*
|
|
4
|
+
* Split out for the reason `publish-panel.ts` and `asset-detail-panel.ts` were: the shell page is
|
|
5
|
+
* the busiest file in the package. Style and markup carry no closure state, so they can leave
|
|
6
|
+
* without threading anything across a boundary; the behaviour stays inline there, because it
|
|
7
|
+
* needs the enclosing IIFE's element lookups and its `closeMore` / Escape chain.
|
|
8
|
+
*
|
|
9
|
+
* What this surface is for: the bar spelled out the game's name, id and environment side by side
|
|
10
|
+
* and had no room left, while the two things that decide how the whole project LOOKS — the
|
|
11
|
+
* accepted reference image and the cover art — were visible only in a terminal. One click on the
|
|
12
|
+
* name now answers both: what this game is, and what it is supposed to look like.
|
|
13
|
+
*
|
|
14
|
+
* Same template-literal rules as its host: **no backticks**, and a backslash in a regex has to be
|
|
15
|
+
* doubled to survive the literal. Every colour is a `--bm-*` token; `shell-page.test.ts` fails the
|
|
16
|
+
* build on a hex that is not black or white.
|
|
17
|
+
*/
|
|
18
|
+
/**
|
|
19
|
+
* Styles for `#info-overlay` and for the bar control that opens it.
|
|
20
|
+
*
|
|
21
|
+
* The trigger is not a pill: a pill is a control, and this is the page's identity — the game's
|
|
22
|
+
* name, in the weight the bar already gave it, that happens to be clickable. Only the hover and
|
|
23
|
+
* focus treatments are added, so it reads as a control the moment a pointer is near it.
|
|
24
|
+
*/
|
|
25
|
+
export const GAME_INFO_PANEL_CSS = `
|
|
26
|
+
/* The name IS the button. Bold and white like the label it replaces, with the id and env it
|
|
27
|
+
absorbed now one click away rather than two more slots in the bar. */
|
|
28
|
+
#game-info-toggle {
|
|
29
|
+
display: flex;
|
|
30
|
+
align-items: center;
|
|
31
|
+
gap: 6px;
|
|
32
|
+
max-width: 26vw;
|
|
33
|
+
padding: 4px 8px;
|
|
34
|
+
background: transparent;
|
|
35
|
+
border: 1px solid transparent;
|
|
36
|
+
border-radius: 6px;
|
|
37
|
+
color: var(--bm-text);
|
|
38
|
+
font-family: var(--bm-font);
|
|
39
|
+
font-size: 14px;
|
|
40
|
+
font-weight: 700;
|
|
41
|
+
line-height: 1.2;
|
|
42
|
+
cursor: pointer;
|
|
43
|
+
transition: background .15s ease, border-color .15s ease;
|
|
44
|
+
}
|
|
45
|
+
#game-info-toggle:hover { background: var(--bm-surface-alt); border-color: var(--bm-border-muted); }
|
|
46
|
+
#game-info-toggle:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
|
|
47
|
+
#game-info-toggle .name { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
48
|
+
/* The affordance. Dim, so the name still reads as the name rather than as a menu. */
|
|
49
|
+
#game-info-toggle .chevron { flex: none; color: var(--bm-text-dim); font-size: 10px; }
|
|
50
|
+
|
|
51
|
+
/* Level with the other two dialogs: all three are opened from different controls and never
|
|
52
|
+
stack. */
|
|
53
|
+
#info-overlay { z-index: 10; }
|
|
54
|
+
#info-dialog { width: min(560px, 92vw); max-height: 86vh; overflow-y: auto; padding: 20px; }
|
|
55
|
+
#info-dialog h2 { margin-bottom: 16px; }
|
|
56
|
+
/* A definition list, because that is what this is: a fixed set of fields with one value each.
|
|
57
|
+
Two columns so the values line up and the whole block is scannable without reading it. */
|
|
58
|
+
#info-facts { display: grid; grid-template-columns: auto 1fr; gap: 8px 16px; margin: 0 0 20px; }
|
|
59
|
+
#info-facts dt { font-size: 12px; font-weight: 700; text-transform: uppercase;
|
|
60
|
+
letter-spacing: 1.4px; color: var(--bm-text-muted); }
|
|
61
|
+
#info-facts dd { margin: 0; min-width: 0; color: var(--bm-text); overflow-wrap: anywhere; }
|
|
62
|
+
/* Ids, versions and modes are read character by character; names and genres are read as words. */
|
|
63
|
+
#info-facts dd.mono { font-family: var(--bm-font-mono); font-size: 12px; }
|
|
64
|
+
#info-facts dd .note { color: var(--bm-text-dim); }
|
|
65
|
+
/* The two pictures, side by side: they are the same kind of thing and are compared, not read
|
|
66
|
+
in order. They wrap to one column when the dialog is at its narrowest. */
|
|
67
|
+
#info-art { display: flex; flex-wrap: wrap; gap: 16px; }
|
|
68
|
+
.info-art-card { flex: 1 1 200px; min-width: 0; }
|
|
69
|
+
.info-art-card .bm-label { display: block; margin-bottom: 8px; }
|
|
70
|
+
/* A square frame either way, so a present picture and a missing one leave the dialog the same
|
|
71
|
+
height and the two cards stay level.
|
|
72
|
+
|
|
73
|
+
A button rather than a div, because with a picture in it that is what it is: the thumbnail is
|
|
74
|
+
a couple of hundred pixels of an image generated at 1024, which is not enough to judge the
|
|
75
|
+
look it is supposed to fix — clicking it opens the full size. Disabled while the card is
|
|
76
|
+
empty, so the control is dead exactly when there is nothing to open, and reachable by keyboard
|
|
77
|
+
for free either way. */
|
|
78
|
+
.info-art-frame {
|
|
79
|
+
display: flex;
|
|
80
|
+
align-items: center;
|
|
81
|
+
justify-content: center;
|
|
82
|
+
width: 100%;
|
|
83
|
+
aspect-ratio: 1 / 1;
|
|
84
|
+
padding: 12px;
|
|
85
|
+
background: var(--bm-surface-alt);
|
|
86
|
+
border: 1px solid var(--bm-border-muted);
|
|
87
|
+
border-radius: 6px;
|
|
88
|
+
color: inherit;
|
|
89
|
+
font: inherit;
|
|
90
|
+
text-align: center;
|
|
91
|
+
cursor: zoom-in;
|
|
92
|
+
transition: border-color .15s ease;
|
|
93
|
+
}
|
|
94
|
+
.info-art-frame:hover:not(:disabled) { border-color: var(--bm-text-dim); }
|
|
95
|
+
.info-art-frame:focus-visible { outline: none; box-shadow: var(--bm-focus-ring); }
|
|
96
|
+
/* Not dimmed like an ordinary disabled control: there is nothing to enable here, and greying the
|
|
97
|
+
"not yet" copy would hide the command that fixes it. */
|
|
98
|
+
.info-art-frame:disabled { cursor: default; }
|
|
99
|
+
/* The frame holds the picture AND the "not yet" copy, and shows exactly one of them: the class
|
|
100
|
+
is applied by openInfo(), because a page assembled as a string has no inline styles to toggle
|
|
101
|
+
(DESIGN.md, and the same rule the Creator follows). */
|
|
102
|
+
.info-art-frame img { display: none; max-width: 100%; max-height: 100%; border-radius: 4px; }
|
|
103
|
+
.info-art-frame.has-image img { display: block; }
|
|
104
|
+
.info-art-frame.has-image .empty { display: none; }
|
|
105
|
+
/* What to run to make the missing one, rather than the bare fact that it is missing. */
|
|
106
|
+
.info-art-frame .empty { font-size: 12px; color: var(--bm-text-dim); }
|
|
107
|
+
.info-art-frame .empty code { display: block; margin-top: 6px; color: var(--bm-aqua);
|
|
108
|
+
font-family: var(--bm-font-mono); }
|
|
109
|
+
.info-art-card .caption { margin-top: 8px; font-size: 12px; color: var(--bm-text-dim);
|
|
110
|
+
overflow-wrap: anywhere; }
|
|
111
|
+
.info-art-card .caption a { color: var(--bm-aqua); text-decoration: none; }
|
|
112
|
+
.info-art-card .caption a:hover { text-decoration: underline; }
|
|
113
|
+
/* An expiring reference URL is the one thing on this card worth interrupting for. */
|
|
114
|
+
.info-art-card .caption .stale { color: var(--bm-pumpkin); }
|
|
115
|
+
/* === The full-size view ===================================================================
|
|
116
|
+
A layer of its own above the dialog rather than a new browser tab: the dev view is the window
|
|
117
|
+
the creator is already in, and a tab showing a bare image on a loopback port is a place they
|
|
118
|
+
then have to come back from. Click anywhere, or Escape, and the dialog is still underneath
|
|
119
|
+
exactly as it was. */
|
|
120
|
+
#info-zoom { z-index: 11; flex-direction: column; gap: 12px; cursor: zoom-out; }
|
|
121
|
+
#info-zoom img { max-width: 92vw; max-height: 84vh; border-radius: 6px;
|
|
122
|
+
box-shadow: var(--bm-shadow-dialog); }
|
|
123
|
+
#info-zoom .hint { font-size: 12px; color: var(--bm-text-muted); }
|
|
124
|
+
|
|
125
|
+
/* The sidecar could not answer. Said in the dialog rather than in the bar's status line, which
|
|
126
|
+
belongs to the save state and would be overwritten by the next poll. */
|
|
127
|
+
#info-error { display: none; margin: 0 0 16px; font-size: 12px; color: var(--bm-pumpkin); }
|
|
128
|
+
#info-error.show { display: block; }
|
|
129
|
+
#info-actions { display: flex; justify-content: flex-end; margin-top: 20px; }
|
|
130
|
+
`;
|
|
131
|
+
/**
|
|
132
|
+
* Markup for the Game-info dialog.
|
|
133
|
+
*
|
|
134
|
+
* Every value is empty until `openInfo()` fills it from `/api/game-info`. Nothing may be
|
|
135
|
+
* interpolated at render time: this page is built once when the server starts, and `bitmagic
|
|
136
|
+
* cover` or `bitmagic reference accept` usually runs hours later in another terminal.
|
|
137
|
+
*/
|
|
138
|
+
export const GAME_INFO_PANEL_HTML = ` <div id="info-overlay" class="bm-overlay">
|
|
139
|
+
<div id="info-dialog" class="bm-dialog">
|
|
140
|
+
<h2 id="info-title"></h2>
|
|
141
|
+
<p id="info-error"></p>
|
|
142
|
+
<dl id="info-facts">
|
|
143
|
+
<dt>Game id</dt><dd id="info-game-id" class="mono"></dd>
|
|
144
|
+
<dt>Environment</dt><dd id="info-environment" class="mono"></dd>
|
|
145
|
+
<dt>Genre</dt><dd id="info-genre"></dd>
|
|
146
|
+
<dt>Physics</dt><dd id="info-physics" class="mono"></dd>
|
|
147
|
+
<dt>Art style</dt><dd id="info-art-style"></dd>
|
|
148
|
+
<dt>Platform</dt><dd id="info-platform"></dd>
|
|
149
|
+
<dt>Engine</dt><dd id="info-engine" class="mono"></dd>
|
|
150
|
+
</dl>
|
|
151
|
+
<div id="info-art">
|
|
152
|
+
<div class="info-art-card">
|
|
153
|
+
<span class="bm-label">Reference image</span>
|
|
154
|
+
<button id="info-reference-frame" class="info-art-frame" type="button" disabled title="See it full size">
|
|
155
|
+
<img id="info-reference-img" alt="This game's accepted reference image">
|
|
156
|
+
<span id="info-reference-empty" class="empty">No reference image yet.<code>bitmagic reference make</code></span>
|
|
157
|
+
</button>
|
|
158
|
+
<p id="info-reference-caption" class="caption"></p>
|
|
159
|
+
</div>
|
|
160
|
+
<div class="info-art-card">
|
|
161
|
+
<span class="bm-label">Cover art</span>
|
|
162
|
+
<button id="info-cover-frame" class="info-art-frame" type="button" disabled title="See it full size">
|
|
163
|
+
<img id="info-cover-img" alt="This game's cover art">
|
|
164
|
+
<span id="info-cover-empty" class="empty">No cover art yet.<code>bitmagic cover</code></span>
|
|
165
|
+
</button>
|
|
166
|
+
<p id="info-cover-caption" class="caption"></p>
|
|
167
|
+
</div>
|
|
168
|
+
</div>
|
|
169
|
+
<div id="info-actions">
|
|
170
|
+
<button id="info-close" class="bm-pill bm-pill--primary" type="button">Close</button>
|
|
171
|
+
</div>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
<div id="info-zoom" class="bm-overlay">
|
|
175
|
+
<img id="info-zoom-img" alt="">
|
|
176
|
+
<span class="hint">Click anywhere to go back.</span>
|
|
177
|
+
</div>
|
|
178
|
+
`;
|
|
179
|
+
//# sourceMappingURL=game-info-panel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-info-panel.js","sourceRoot":"","sources":["../../src/editor/game-info-panel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAyGlC,CAAC;AAEF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAwCnC,CAAC"}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import type { ProjectMetadata } from '../project/context.js';
|
|
2
|
+
/** The two pictures a project carries. Both are local files under `.bitmagic/`, both re-hostable. */
|
|
3
|
+
export type GameInfoImageKind = 'reference' | 'cover';
|
|
4
|
+
export interface GameInfoImage {
|
|
5
|
+
/** The public URL the record names — where this picture lives for everyone but this machine. */
|
|
6
|
+
url: string;
|
|
7
|
+
/** Whether the sidecar can serve local bytes. False means the dialog falls back to `url`. */
|
|
8
|
+
local: boolean;
|
|
9
|
+
/** When it was accepted (reference) or generated (cover), ISO. */
|
|
10
|
+
at: string;
|
|
11
|
+
/** Reference only: false when re-hosting failed, so `url` is the source and will expire. */
|
|
12
|
+
hosted?: boolean;
|
|
13
|
+
/** Reference only, and only when a generated candidate rather than a bare URL was accepted. */
|
|
14
|
+
candidateId?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface GameInfo {
|
|
17
|
+
gameName: string;
|
|
18
|
+
gameId: string;
|
|
19
|
+
environment: string;
|
|
20
|
+
engineVersion: string;
|
|
21
|
+
template: string;
|
|
22
|
+
/** `gameGenre` from the work copy, falling back to what `bitmagic.json` recorded at scaffold. */
|
|
23
|
+
genre: string;
|
|
24
|
+
/** `physicsMode` from the work copy. Null on the genres that declare none. */
|
|
25
|
+
physicsMode: string | null;
|
|
26
|
+
/** What every generation in this project inherits — `voxel` when game.json declares nothing. */
|
|
27
|
+
artStyle: string;
|
|
28
|
+
/** False when that style is the default rather than a declaration, which is worth saying. */
|
|
29
|
+
artStyleDeclared: boolean;
|
|
30
|
+
/** `desktop` unless the project says otherwise; null when it declares nothing at all. */
|
|
31
|
+
primaryPlatform: string | null;
|
|
32
|
+
reference: GameInfoImage | null;
|
|
33
|
+
cover: GameInfoImage | null;
|
|
34
|
+
}
|
|
35
|
+
export interface GameInfoSource {
|
|
36
|
+
root: string;
|
|
37
|
+
metadata: ProjectMetadata;
|
|
38
|
+
gameId: string;
|
|
39
|
+
gameName: string;
|
|
40
|
+
environment: string;
|
|
41
|
+
}
|
|
42
|
+
export declare function readGameInfo(source: GameInfoSource): GameInfo;
|
|
43
|
+
/**
|
|
44
|
+
* The bytes of one picture, for the dialog's thumbnail.
|
|
45
|
+
*
|
|
46
|
+
* Served from disk rather than linked to the public URL because the local copy is the one that is
|
|
47
|
+
* always there: an accepted reference whose re-host failed carries an Asset Forger URL that
|
|
48
|
+
* expires, and a cover's URL is on whichever environment generated it. The content type is
|
|
49
|
+
* sniffed rather than taken from the extension — both files are written as `.webp` whatever the
|
|
50
|
+
* generator actually returned.
|
|
51
|
+
*/
|
|
52
|
+
export declare function readGameInfoImage(root: string, kind: GameInfoImageKind): {
|
|
53
|
+
bytes: Buffer;
|
|
54
|
+
contentType: string;
|
|
55
|
+
} | null;
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* What this project IS — the facts the dev view's bar used to spell out, plus the two pictures
|
|
3
|
+
* that decide how it looks.
|
|
4
|
+
*
|
|
5
|
+
* The bar carried the game's name, its id and its environment as three separate slots in a row
|
|
6
|
+
* that had run out of them. They answer one question — "which game is this?" — so they collapsed
|
|
7
|
+
* behind the name, and the dialog that opens has room for the rest of the answer: the genre and
|
|
8
|
+
* physics the engine boots with, the art style every generation inherits, and the accepted
|
|
9
|
+
* reference and cover art. The reference in particular had no window anywhere: `bitmagic
|
|
10
|
+
* reference show` draws it in a terminal, which is not where the creator is looking.
|
|
11
|
+
*
|
|
12
|
+
* Read on every open, never captured at startup. `bitmagic dev` runs for hours and the commands
|
|
13
|
+
* that write these files (`reference accept`, `cover`) run in another terminal — often an agent's
|
|
14
|
+
* — so anything cached here would answer with the project as it was this morning.
|
|
15
|
+
*
|
|
16
|
+
* Best effort throughout, exactly like every other reader of these files: a missing or corrupt
|
|
17
|
+
* record reads as "none yet", never as an error. This is a window onto the project, and a window
|
|
18
|
+
* must not be the thing that fails.
|
|
19
|
+
*/
|
|
20
|
+
import * as fs from 'fs';
|
|
21
|
+
import * as path from 'path';
|
|
22
|
+
import { DEFAULT_ART_STYLE } from '@bitmagic/asset-core/art-style';
|
|
23
|
+
import { readArtStyleDeclaration } from '../project/art-style.js';
|
|
24
|
+
import { readPlatformDeclaration } from '../project/platform-declaration.js';
|
|
25
|
+
import { readAcceptedReference, referenceDirPath } from '../project/reference.js';
|
|
26
|
+
import { sniffImageContentType } from '../project/reference-file.js';
|
|
27
|
+
import { coverImagePath, readCoverRecord } from '../commands/cover.js';
|
|
28
|
+
import { aliasSourceDir } from '../scaffold/aliases.js';
|
|
29
|
+
/** One work-copy field, or undefined when the file is missing, unreadable or not an object. */
|
|
30
|
+
function workGameJson(root) {
|
|
31
|
+
const file = path.join(root, aliasSourceDir('work'), 'game.json');
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)
|
|
40
|
+
? parsed
|
|
41
|
+
: null;
|
|
42
|
+
}
|
|
43
|
+
function stringField(source, key) {
|
|
44
|
+
const value = source?.[key];
|
|
45
|
+
return typeof value === 'string' && value !== '' ? value : null;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* The local copy of one of the two pictures, when there is one.
|
|
49
|
+
*
|
|
50
|
+
* The reference's path comes out of a JSON file, so it is checked against the directory it is
|
|
51
|
+
* supposed to be in rather than trusted: this server serves whatever it returns, and
|
|
52
|
+
* `.bitmagic/reference.json` is a file a creator can hand-edit.
|
|
53
|
+
*/
|
|
54
|
+
function localImagePath(root, kind) {
|
|
55
|
+
if (kind === 'cover') {
|
|
56
|
+
const file = coverImagePath(root);
|
|
57
|
+
return fs.existsSync(file) ? file : null;
|
|
58
|
+
}
|
|
59
|
+
const accepted = readAcceptedReference(root);
|
|
60
|
+
if (!accepted?.imagePath)
|
|
61
|
+
return null;
|
|
62
|
+
const file = path.resolve(root, accepted.imagePath);
|
|
63
|
+
if (!file.startsWith(referenceDirPath(root) + path.sep))
|
|
64
|
+
return null;
|
|
65
|
+
return fs.existsSync(file) ? file : null;
|
|
66
|
+
}
|
|
67
|
+
function referenceImage(root) {
|
|
68
|
+
const accepted = readAcceptedReference(root);
|
|
69
|
+
if (!accepted)
|
|
70
|
+
return null;
|
|
71
|
+
return {
|
|
72
|
+
url: accepted.url,
|
|
73
|
+
local: localImagePath(root, 'reference') !== null,
|
|
74
|
+
at: accepted.acceptedAt,
|
|
75
|
+
hosted: accepted.hosted,
|
|
76
|
+
...(accepted.candidateId !== undefined ? { candidateId: accepted.candidateId } : {}),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function coverImage(root) {
|
|
80
|
+
const record = readCoverRecord(root);
|
|
81
|
+
if (!record)
|
|
82
|
+
return null;
|
|
83
|
+
return { url: record.url, local: localImagePath(root, 'cover') !== null, at: record.generatedAt };
|
|
84
|
+
}
|
|
85
|
+
export function readGameInfo(source) {
|
|
86
|
+
const game = workGameJson(source.root);
|
|
87
|
+
const style = readArtStyleDeclaration(source.root);
|
|
88
|
+
const platform = readPlatformDeclaration(source.root);
|
|
89
|
+
return {
|
|
90
|
+
gameName: source.gameName,
|
|
91
|
+
gameId: source.gameId,
|
|
92
|
+
environment: source.environment,
|
|
93
|
+
engineVersion: source.metadata.engineVersion,
|
|
94
|
+
template: source.metadata.template,
|
|
95
|
+
genre: stringField(game, 'gameGenre') ?? source.metadata.genre,
|
|
96
|
+
physicsMode: stringField(game, 'physicsMode'),
|
|
97
|
+
artStyle: style.artStyle ?? DEFAULT_ART_STYLE,
|
|
98
|
+
artStyleDeclared: style.artStyle !== undefined,
|
|
99
|
+
primaryPlatform: platform.primaryPlatform ?? null,
|
|
100
|
+
reference: referenceImage(source.root),
|
|
101
|
+
cover: coverImage(source.root),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* The bytes of one picture, for the dialog's thumbnail.
|
|
106
|
+
*
|
|
107
|
+
* Served from disk rather than linked to the public URL because the local copy is the one that is
|
|
108
|
+
* always there: an accepted reference whose re-host failed carries an Asset Forger URL that
|
|
109
|
+
* expires, and a cover's URL is on whichever environment generated it. The content type is
|
|
110
|
+
* sniffed rather than taken from the extension — both files are written as `.webp` whatever the
|
|
111
|
+
* generator actually returned.
|
|
112
|
+
*/
|
|
113
|
+
export function readGameInfoImage(root, kind) {
|
|
114
|
+
const file = localImagePath(root, kind);
|
|
115
|
+
if (file === null)
|
|
116
|
+
return null;
|
|
117
|
+
let bytes;
|
|
118
|
+
try {
|
|
119
|
+
bytes = fs.readFileSync(file);
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
return { bytes, contentType: sniffImageContentType(bytes)?.contentType ?? 'image/webp' };
|
|
125
|
+
}
|
|
126
|
+
//# sourceMappingURL=game-info.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"game-info.js","sourceRoot":"","sources":["../../src/editor/game-info.ts"],"names":[],"mappings":"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"}
|
package/dist/editor/server.js
CHANGED
|
@@ -25,10 +25,12 @@ import { applyModificationsToWorld } from '../forge/apply-modifications.js';
|
|
|
25
25
|
import { readProjectGameData } from '../forge/browser-host.js';
|
|
26
26
|
import { projectWorldJsonPath } from '../forge/run-pipeline.js';
|
|
27
27
|
import { renderEditorShell } from './shell-page.js';
|
|
28
|
+
import { readGameInfo, readGameInfoImage } from './game-info.js';
|
|
28
29
|
import { createCliUpdateReader } from './cli-update.js';
|
|
29
30
|
import { renderAssetPreviewPage } from './asset-preview-page.js';
|
|
30
31
|
import { renderAnimationPreviewPage } from './animation-preview-page.js';
|
|
31
32
|
import { readPublishRecord } from '../publish/record.js';
|
|
33
|
+
import { promotedPublishUrl } from '../publish/urls.js';
|
|
32
34
|
import { encodeQr } from '../render/qr.js';
|
|
33
35
|
import { encodeQrPng } from '../render/qr-png.js';
|
|
34
36
|
import { buildSceneModifications } from './save.js';
|
|
@@ -1040,6 +1042,56 @@ export async function startEditorServer(options) {
|
|
|
1040
1042
|
}
|
|
1041
1043
|
return;
|
|
1042
1044
|
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Who this project is, and what it is supposed to look like — the bar's name button opens
|
|
1047
|
+
* this.
|
|
1048
|
+
*
|
|
1049
|
+
* Read per request rather than captured at startup for the same reason `/api/state` reads
|
|
1050
|
+
* the publish record every poll: `bitmagic dev` runs for hours, and `bitmagic cover` or
|
|
1051
|
+
* `bitmagic reference accept` almost always runs later, in another terminal — often an
|
|
1052
|
+
* agent's. Cheap enough not to need a poll of its own: nothing reads it until the creator
|
|
1053
|
+
* opens the dialog.
|
|
1054
|
+
*/
|
|
1055
|
+
if (req.method === 'GET' && url === '/api/game-info') {
|
|
1056
|
+
sendJson(res, 200, {
|
|
1057
|
+
ok: true,
|
|
1058
|
+
info: readGameInfo({
|
|
1059
|
+
root: options.root,
|
|
1060
|
+
metadata: options.metadata,
|
|
1061
|
+
gameId: options.gameId,
|
|
1062
|
+
gameName: options.gameName,
|
|
1063
|
+
environment: options.environment,
|
|
1064
|
+
}),
|
|
1065
|
+
});
|
|
1066
|
+
return;
|
|
1067
|
+
}
|
|
1068
|
+
/**
|
|
1069
|
+
* The local copy of the reference or the cover, for the dialog's thumbnails.
|
|
1070
|
+
*
|
|
1071
|
+
* Local bytes rather than the public URL, because the local copy is the one that is always
|
|
1072
|
+
* reachable: a reference whose re-host failed carries an expiring Asset Forger URL, and a
|
|
1073
|
+
* cover's URL lives on whichever environment generated it. No caching — `bitmagic cover`
|
|
1074
|
+
* and `reference accept` rewrite these paths in place, and a browser holding the previous
|
|
1075
|
+
* picture would show a creator the art they just replaced.
|
|
1076
|
+
*/
|
|
1077
|
+
if (req.method === 'GET' && url === '/api/game-info/image') {
|
|
1078
|
+
const kind = query.get('kind');
|
|
1079
|
+
if (kind !== 'reference' && kind !== 'cover') {
|
|
1080
|
+
sendJson(res, 400, { ok: false, error: 'kind must be reference or cover' });
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const image = readGameInfoImage(options.root, kind);
|
|
1084
|
+
if (image === null) {
|
|
1085
|
+
sendJson(res, 404, { ok: false, error: `No local ${kind} image in this project` });
|
|
1086
|
+
return;
|
|
1087
|
+
}
|
|
1088
|
+
res.writeHead(200, {
|
|
1089
|
+
'Content-Type': image.contentType,
|
|
1090
|
+
'Content-Length': image.bytes.length,
|
|
1091
|
+
'Cache-Control': 'no-store',
|
|
1092
|
+
}).end(image.bytes);
|
|
1093
|
+
return;
|
|
1094
|
+
}
|
|
1043
1095
|
// Long-lived: the shell holds this open for its whole session. Must be checked before any
|
|
1044
1096
|
// route that would end the response.
|
|
1045
1097
|
if (req.method === 'GET' && url === '/api/events/stream') {
|
|
@@ -1237,13 +1289,17 @@ export async function startEditorServer(options) {
|
|
|
1237
1289
|
*/
|
|
1238
1290
|
if (req.method === 'GET' && url === '/api/publish/qr.png') {
|
|
1239
1291
|
const record = readPublishRecord(options.root);
|
|
1240
|
-
|
|
1292
|
+
// The shareable URL, matching both the panel's link and what the publishing terminal
|
|
1293
|
+
// drew — see `promotedPublishUrl`. Encoding `record.url` here would put a phone on one
|
|
1294
|
+
// frozen build while the link beside the code pointed at the newest.
|
|
1295
|
+
const matrix = record === null ? null : encodeQr(promotedPublishUrl(record));
|
|
1241
1296
|
if (matrix === null) {
|
|
1242
1297
|
sendJson(res, 404, { ok: false, error: 'This project has not been published yet.' });
|
|
1243
1298
|
return;
|
|
1244
1299
|
}
|
|
1245
|
-
// No caching:
|
|
1246
|
-
//
|
|
1300
|
+
// No caching: the shareable URL is stable across ordinary re-publishes, but a publish
|
|
1301
|
+
// that flips visibility moves it between the `/play/` page and the `/games/` form, and a
|
|
1302
|
+
// stale code would send a phone to the one that is no longer right.
|
|
1247
1303
|
res.writeHead(200, {
|
|
1248
1304
|
'Content-Type': 'image/png',
|
|
1249
1305
|
'Cache-Control': 'no-store',
|