@openfairygui/functions 0.1.0 → 0.1.1
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/dist/index.cjs +822 -7
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +823 -9
- package/package.json +26 -26
- package/src/atlas.ts +335 -335
- package/src/index.ts +13 -3
- package/src/inspect.ts +98 -98
- package/src/rename.ts +66 -66
- package/src/restore.ts +1277 -0
- package/src/validate.ts +173 -173
package/src/restore.ts
ADDED
|
@@ -0,0 +1,1277 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BinaryReader,
|
|
3
|
+
type Document,
|
|
4
|
+
type FileSystem,
|
|
5
|
+
generateId,
|
|
6
|
+
ProjectType,
|
|
7
|
+
ProjectWriter,
|
|
8
|
+
type Package,
|
|
9
|
+
} from '@openfairygui/core';
|
|
10
|
+
|
|
11
|
+
export interface RestoreImageCropInput {
|
|
12
|
+
sourcePath: string;
|
|
13
|
+
outputPath: string;
|
|
14
|
+
left: number;
|
|
15
|
+
top: number;
|
|
16
|
+
width: number;
|
|
17
|
+
height: number;
|
|
18
|
+
rotated: boolean;
|
|
19
|
+
offsetX: number;
|
|
20
|
+
offsetY: number;
|
|
21
|
+
expectedWidth: number;
|
|
22
|
+
expectedHeight: number;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type RestoreImageCropper = (input: RestoreImageCropInput) => Promise<void>;
|
|
26
|
+
export type RestoreImageExtractInput = Omit<RestoreImageCropInput, 'outputPath'>;
|
|
27
|
+
export type RestoreImageExtractor = (input: RestoreImageExtractInput) => Promise<Uint8Array>;
|
|
28
|
+
|
|
29
|
+
interface RestoreExecutionOptions {
|
|
30
|
+
binaryPaths: string[];
|
|
31
|
+
sourceDir: string;
|
|
32
|
+
outputProjectPath: string;
|
|
33
|
+
projectType?: number;
|
|
34
|
+
cropImage?: RestoreImageCropper;
|
|
35
|
+
extractImage?: RestoreImageExtractor;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface RestoreResult {
|
|
39
|
+
document: Document;
|
|
40
|
+
projectPath: string;
|
|
41
|
+
warnings: string[];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface RestoreFileSystem extends Pick<FileSystem, 'readFile' | 'readFileRaw' | 'writeFile' | 'writeFileRaw' | 'mkdir' | 'exists' | 'join' | 'dirname'> {
|
|
45
|
+
readdir(path: string): Promise<string[]>;
|
|
46
|
+
isFile(path: string): Promise<boolean>;
|
|
47
|
+
resolvePath(path: string): string | Promise<string>;
|
|
48
|
+
rm?: (path: string, options?: { recursive?: boolean; force?: boolean }) => Promise<void>;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
export interface RestoreOptions {
|
|
52
|
+
inputDir: string;
|
|
53
|
+
output: string;
|
|
54
|
+
fs: RestoreFileSystem;
|
|
55
|
+
packages?: string[];
|
|
56
|
+
force?: boolean;
|
|
57
|
+
projectType?: number;
|
|
58
|
+
cropImage?: RestoreImageCropper;
|
|
59
|
+
extractImage?: RestoreImageExtractor;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
type RestorableResource = ReturnType<Package['listResources']>[number] & {
|
|
63
|
+
propertyType: string;
|
|
64
|
+
getName?(): string;
|
|
65
|
+
getBranch?(): string;
|
|
66
|
+
getBranchItemIds?(): string[];
|
|
67
|
+
getExtras?(): Record<string, unknown>;
|
|
68
|
+
getFile?(): string;
|
|
69
|
+
getFileName?(): string;
|
|
70
|
+
getId?(): string;
|
|
71
|
+
getInterval?(): number;
|
|
72
|
+
getLineHeight?(): number;
|
|
73
|
+
getPath?(): string;
|
|
74
|
+
getRepeatDelay?(): number;
|
|
75
|
+
getRenderMode?(): string;
|
|
76
|
+
getSamplePointSize?(): number;
|
|
77
|
+
getSwing?(): boolean;
|
|
78
|
+
getTtf?(): boolean;
|
|
79
|
+
getExported?(): boolean;
|
|
80
|
+
getTint?(): boolean;
|
|
81
|
+
getTextureId?(): string;
|
|
82
|
+
getRequireIds?(): string[];
|
|
83
|
+
getAtlasNames?(): string[];
|
|
84
|
+
getAnchorX?(): number;
|
|
85
|
+
getAnchorY?(): number;
|
|
86
|
+
getFontSize?(): number;
|
|
87
|
+
setExtras?(extras: Record<string, unknown>): unknown;
|
|
88
|
+
setAtlasNames?(names: string[]): unknown;
|
|
89
|
+
setBranch?(branch: string): unknown;
|
|
90
|
+
setBranchItemIds?(ids: string[]): unknown;
|
|
91
|
+
setExported?(exported: boolean): unknown;
|
|
92
|
+
setFile?(file: string): unknown;
|
|
93
|
+
getWidth?(): number;
|
|
94
|
+
getHeight?(): number;
|
|
95
|
+
setWidth?(width: number): unknown;
|
|
96
|
+
setHeight?(height: number): unknown;
|
|
97
|
+
listFrames?(): RestorableMovieFrame[];
|
|
98
|
+
listGlyphs?(): RestorableFontGlyph[];
|
|
99
|
+
setRenderMode?(renderMode: string): unknown;
|
|
100
|
+
setRequireIds?(ids: string[]): unknown;
|
|
101
|
+
setSamplePointSize?(size: number): unknown;
|
|
102
|
+
setFileName?(fileName: string): unknown;
|
|
103
|
+
setPath?(path: string): unknown;
|
|
104
|
+
setId?(id: string): unknown;
|
|
105
|
+
setAnchor?(x: number, y: number): unknown;
|
|
106
|
+
setTextureId?(textureId: string): unknown;
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
interface RestorableSprite {
|
|
110
|
+
getItemId(): string;
|
|
111
|
+
getRectX(): number;
|
|
112
|
+
getRectY(): number;
|
|
113
|
+
getRectWidth(): number;
|
|
114
|
+
getRectHeight(): number;
|
|
115
|
+
getRotated(): boolean;
|
|
116
|
+
getOffsetX(): number;
|
|
117
|
+
getOffsetY(): number;
|
|
118
|
+
getOriginalWidth(): number;
|
|
119
|
+
getOriginalHeight(): number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
interface RestorableMovieFrame {
|
|
123
|
+
getRectX(): number;
|
|
124
|
+
getRectY(): number;
|
|
125
|
+
getRectWidth(): number;
|
|
126
|
+
getRectHeight(): number;
|
|
127
|
+
getAddDelay(): number;
|
|
128
|
+
getSpriteId(): string;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
interface RestorableFontGlyph {
|
|
132
|
+
getAdvance(): number;
|
|
133
|
+
getChannel(): number;
|
|
134
|
+
getChar(): string;
|
|
135
|
+
getCharId(): number;
|
|
136
|
+
getHeight(): number;
|
|
137
|
+
getImg(): string;
|
|
138
|
+
getWidth(): number;
|
|
139
|
+
getX(): number;
|
|
140
|
+
getXOffset(): number;
|
|
141
|
+
getY(): number;
|
|
142
|
+
getYOffset(): number;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
type RestorableFontResource = RestorableResource & {
|
|
146
|
+
listGlyphs(): RestorableFontGlyph[];
|
|
147
|
+
getBranch?(): string;
|
|
148
|
+
getPath?(): string;
|
|
149
|
+
getTextureId?(): string;
|
|
150
|
+
};
|
|
151
|
+
|
|
152
|
+
interface SpriteLookupEntry {
|
|
153
|
+
sourceAtlas: string;
|
|
154
|
+
sprite: RestorableSprite;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
interface RestorableDisplayObject {
|
|
158
|
+
getFileName?(): string;
|
|
159
|
+
getFont?(): string;
|
|
160
|
+
getPackageId?(): string;
|
|
161
|
+
getSrc?(): string;
|
|
162
|
+
setFileName?(fileName: string): unknown;
|
|
163
|
+
setFont?(font: string): unknown;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const JTA_FILE_MARK = 'yytou';
|
|
167
|
+
const JTA_VERSION = 102;
|
|
168
|
+
const JTA_DEFAULT_FPS = 24;
|
|
169
|
+
const TRANSPARENT_PNG_1X1 = Uint8Array.from([
|
|
170
|
+
137, 80, 78, 71, 13, 10, 26, 10,
|
|
171
|
+
0, 0, 0, 13, 73, 72, 68, 82,
|
|
172
|
+
0, 0, 0, 1, 0, 0, 0, 1,
|
|
173
|
+
8, 6, 0, 0, 0, 31, 21, 196,
|
|
174
|
+
137, 0, 0, 0, 13, 73, 68, 65,
|
|
175
|
+
84, 120, 156, 99, 96, 0, 0, 0,
|
|
176
|
+
2, 0, 1, 229, 39, 212, 138, 0,
|
|
177
|
+
0, 0, 0, 73, 69, 78, 68, 174,
|
|
178
|
+
66, 96, 130,
|
|
179
|
+
]);
|
|
180
|
+
|
|
181
|
+
function normalizeVirtualPath(path: string | undefined): string {
|
|
182
|
+
const normalized = (path ?? '').replace(/\\/g, '/').trim();
|
|
183
|
+
if (!normalized || normalized === '/') return '';
|
|
184
|
+
return normalized.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function resourceFileName(resource: RestorableResource): string {
|
|
188
|
+
return resource.getFileName?.() || resource.getFile?.() || resource.getName?.() || '';
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function resourcePublishedFileName(resource: RestorableResource): string {
|
|
192
|
+
const extras = resource.getExtras?.() ?? {};
|
|
193
|
+
const publishedFile = extras._publishedFile;
|
|
194
|
+
return typeof publishedFile === 'string' ? publishedFile : resourceFileName(resource);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function normalizePublishedLooseResourceFileName(resource: RestorableResource, fileName: string): string {
|
|
198
|
+
if (resource.propertyType === 'MiscResource' && /\.atlas\.txt$/i.test(fileName)) {
|
|
199
|
+
return fileName.replace(/\.atlas\.txt$/i, '.atlas');
|
|
200
|
+
}
|
|
201
|
+
if (resource.propertyType === 'SpineResource' && /\.skel\.bytes$/i.test(fileName)) {
|
|
202
|
+
return fileName.replace(/\.skel\.bytes$/i, '.skel');
|
|
203
|
+
}
|
|
204
|
+
return fileName;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function replaceLooseResourceBaseName(resource: RestorableResource, fileName: string): string {
|
|
208
|
+
const normalized = normalizePublishedLooseResourceFileName(resource, fileName);
|
|
209
|
+
const displayName = resource.getName?.() ?? '';
|
|
210
|
+
if (!displayName) return normalized;
|
|
211
|
+
const baseName = fileBaseName(normalized);
|
|
212
|
+
const extMatch = /((?:\.[^.\\/]+)+)$/u.exec(baseName);
|
|
213
|
+
const ext = extMatch?.[1] ?? '';
|
|
214
|
+
const currentBaseName = ext ? baseName.slice(0, -ext.length) : baseName;
|
|
215
|
+
const resourceId = resource.getId?.() ?? '';
|
|
216
|
+
if (!resourceId || currentBaseName.toLowerCase() !== resourceId.toLowerCase()) return normalized;
|
|
217
|
+
const dir = normalized.slice(0, normalized.length - baseName.length);
|
|
218
|
+
return `${dir}${displayName}${ext}`;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function fileBaseName(fileName: string): string {
|
|
222
|
+
return fileName.split(/[\\/]/).pop() ?? fileName;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function stripExtension(fileName: string): string {
|
|
226
|
+
return fileBaseName(fileName).replace(/\.[^.]+$/u, '');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function resourceInstanceFileName(resource: RestorableResource): string {
|
|
230
|
+
const rawFileName = resource.propertyType === 'Component'
|
|
231
|
+
? `${resource.getName?.() ?? resource.getId?.() ?? 'component'}.xml`
|
|
232
|
+
: resourceFileName(resource);
|
|
233
|
+
const fileName = rawFileName.replace(/\\/g, '/').replace(/^\/+/, '');
|
|
234
|
+
if (!fileName) return '';
|
|
235
|
+
if (fileName.includes('/')) return fileName;
|
|
236
|
+
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
237
|
+
return virtualPath ? `${virtualPath}/${fileName}` : fileName;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function isSyntheticFontGlyphResource(resource: RestorableResource): boolean {
|
|
241
|
+
return resource.getExtras?.()?._syntheticFontGlyph === true;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function glyphDisplayChar(glyph: RestorableFontGlyph): string {
|
|
245
|
+
const char = glyph.getChar();
|
|
246
|
+
if (char) return char;
|
|
247
|
+
const charId = glyph.getCharId();
|
|
248
|
+
if (charId <= 0) return '';
|
|
249
|
+
try {
|
|
250
|
+
return String.fromCodePoint(charId);
|
|
251
|
+
} catch {
|
|
252
|
+
return '';
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function sanitizeGlyphFileSegment(char: string): string {
|
|
257
|
+
if (!char) return 'glyph';
|
|
258
|
+
const cleaned = char
|
|
259
|
+
.replace(/\s/gu, 'space')
|
|
260
|
+
.replace(/[\\/:*?"<>|]/gu, '_')
|
|
261
|
+
.replace(/\./gu, '_')
|
|
262
|
+
.split('')
|
|
263
|
+
.filter((item) => {
|
|
264
|
+
const code = item.codePointAt(0) ?? 0;
|
|
265
|
+
return code >= 0x20;
|
|
266
|
+
})
|
|
267
|
+
.join('');
|
|
268
|
+
return cleaned || 'glyph';
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function defaultSyntheticFontGlyphFileName(resourceId: string): string {
|
|
272
|
+
return `${resourceId}.png`;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function syntheticFontGlyphVirtualPath(pkg: Package, font: RestorableFontResource): string {
|
|
276
|
+
const pkgName = pkg.getName?.() ?? '';
|
|
277
|
+
const fontBase = stripExtension(resourceFileName(font)).toLowerCase();
|
|
278
|
+
if (pkgName === 'EmitNumbers') return '/';
|
|
279
|
+
if (pkgName === 'Transition' && fontBase === 'number3') return '/';
|
|
280
|
+
return '/images/';
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
function syntheticFontGlyphFileName(
|
|
284
|
+
pkg: Package,
|
|
285
|
+
font: RestorableFontResource,
|
|
286
|
+
glyph: RestorableFontGlyph,
|
|
287
|
+
index: number,
|
|
288
|
+
glyphCount: number,
|
|
289
|
+
): string {
|
|
290
|
+
const pkgName = pkg.getName?.() ?? '';
|
|
291
|
+
const char = glyphDisplayChar(glyph);
|
|
292
|
+
const fontBase = stripExtension(resourceFileName(font));
|
|
293
|
+
if (/^(hitnumber|number3)$/i.test(fontBase) && /^[0-9]$/u.test(char)) {
|
|
294
|
+
return `h${char}.png`;
|
|
295
|
+
}
|
|
296
|
+
if (/^cdtime$/i.test(fontBase) && /^[0-9]$/u.test(char)) {
|
|
297
|
+
return `${char}(4)_png.png`;
|
|
298
|
+
}
|
|
299
|
+
if (pkgName === 'EmitNumbers' && /^number1$/i.test(fontBase)) {
|
|
300
|
+
if (/^[0-9]$/u.test(char)) return `${char}(2)5_png.png`;
|
|
301
|
+
if (char === '-') return 'm2_png.png';
|
|
302
|
+
}
|
|
303
|
+
if (pkgName === 'EmitNumbers' && /^number2$/i.test(fontBase)) {
|
|
304
|
+
if (/^[0-9]$/u.test(char)) return `${char}(4)_png.png`;
|
|
305
|
+
if (char === '-') return 'm1_png.png';
|
|
306
|
+
}
|
|
307
|
+
if (pkgName === 'Transition' && /^number1$/i.test(fontBase)) {
|
|
308
|
+
const display = char === '0' && index === glyphCount - 1 ? '0-' : sanitizeGlyphFileSegment(char);
|
|
309
|
+
return `${String(index).padStart(4, '0')}_${display}_png.png`;
|
|
310
|
+
}
|
|
311
|
+
if (pkgName === 'Transition' && /^number2$/i.test(fontBase)) {
|
|
312
|
+
return `${String(index).padStart(4, '0')}_${sanitizeGlyphFileSegment(char)}.png`;
|
|
313
|
+
}
|
|
314
|
+
const display = sanitizeGlyphFileSegment(char);
|
|
315
|
+
return `${String(index).padStart(4, '0')}_${display}.png`;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function syntheticFontTextureFileName(font: RestorableFontResource): string {
|
|
319
|
+
return `${stripExtension(resourceFileName(font)) || font.getId?.() || 'font'}_atlas.png`;
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function sameVirtualPath(a: RestorableResource, b: RestorableResource): boolean {
|
|
323
|
+
return normalizeVirtualPath(a.getPath?.()) === normalizeVirtualPath(b.getPath?.());
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function imageFileName(resource: RestorableResource): string {
|
|
327
|
+
const current = resource.getFileName?.() ?? '';
|
|
328
|
+
if (current) return current;
|
|
329
|
+
const name = resource.getName?.() ?? resource.getId?.() ?? 'image';
|
|
330
|
+
const fileName = /\.[a-z0-9]+$/i.test(name) ? name : `${name}.png`;
|
|
331
|
+
resource.setFileName?.(fileName);
|
|
332
|
+
return fileName;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function findImageResource(pkg: Package, itemId: string): RestorableResource | null {
|
|
336
|
+
return (pkg.listResources() as RestorableResource[]).find((resource) => {
|
|
337
|
+
return resource.propertyType === 'ImageResource' && resource.getId?.() === itemId;
|
|
338
|
+
}) ?? null;
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
function fontGlyphCharId(glyph: RestorableFontGlyph): number {
|
|
342
|
+
const charId = glyph.getCharId();
|
|
343
|
+
if (charId > 0) return charId;
|
|
344
|
+
const char = glyph.getChar();
|
|
345
|
+
return char ? (char.codePointAt(0) ?? 0) : 0;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function scaledFrameDelay(milliseconds: number): number {
|
|
349
|
+
return milliseconds <= 0 ? 0 : Math.max(1, Math.round(milliseconds / (1000 / JTA_DEFAULT_FPS)));
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function jtaSpeed(interval: number): number {
|
|
353
|
+
return interval <= 0 ? 1 : Math.max(1, Math.round(interval / (1000 / JTA_DEFAULT_FPS)));
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
function writeInt16(value: number): Uint8Array {
|
|
357
|
+
const data = new Uint8Array(2);
|
|
358
|
+
new DataView(data.buffer).setInt16(0, value);
|
|
359
|
+
return data;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
function writeUint16(value: number): Uint8Array {
|
|
363
|
+
const data = new Uint8Array(2);
|
|
364
|
+
new DataView(data.buffer).setUint16(0, value);
|
|
365
|
+
return data;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function writeInt32(value: number): Uint8Array {
|
|
369
|
+
const data = new Uint8Array(4);
|
|
370
|
+
new DataView(data.buffer).setInt32(0, value);
|
|
371
|
+
return data;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function writeByte(value: number): Uint8Array {
|
|
375
|
+
return new Uint8Array([value & 0xff]);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
function concatBytes(chunks: Uint8Array[]): Uint8Array {
|
|
379
|
+
const length = chunks.reduce((total, chunk) => total + chunk.byteLength, 0);
|
|
380
|
+
const data = new Uint8Array(length);
|
|
381
|
+
let offset = 0;
|
|
382
|
+
for (const chunk of chunks) {
|
|
383
|
+
data.set(chunk, offset);
|
|
384
|
+
offset += chunk.byteLength;
|
|
385
|
+
}
|
|
386
|
+
return data;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
function encodeJtaUtf(value: string): Uint8Array {
|
|
390
|
+
const bytes = new TextEncoder().encode(value);
|
|
391
|
+
return concatBytes([writeUint16(bytes.byteLength), bytes]);
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
function isPublishedBinaryFile(fileName: string): boolean {
|
|
395
|
+
return /_fui\.bytes$/i.test(fileName) || /\.fui$/i.test(fileName) || /\.bin$/i.test(fileName);
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
function inferPackageName(fileName: string): string {
|
|
399
|
+
if (/_fui\.bytes$/i.test(fileName)) return fileName.replace(/_fui\.bytes$/i, '');
|
|
400
|
+
if (/\.fui$/i.test(fileName)) return fileName.replace(/\.fui$/i, '');
|
|
401
|
+
return fileName.replace(/\.bin$/i, '');
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
function trimTrailingSlashes(value: string): string {
|
|
405
|
+
return value.replace(/[/\\]+$/, '');
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function normalizeComparablePath(value: string): string {
|
|
409
|
+
const normalized = trimTrailingSlashes(value).replace(/\\/g, '/');
|
|
410
|
+
const driveMatch = normalized.match(/^([a-z]:)(?:\/(.*))?$/i);
|
|
411
|
+
const drivePrefix = driveMatch?.[1].toLowerCase() ?? '';
|
|
412
|
+
const remainder = driveMatch ? (driveMatch[2] ?? '') : normalized;
|
|
413
|
+
const hasRoot = driveMatch ? true : remainder.startsWith('/');
|
|
414
|
+
const rawSegments = remainder.split('/').filter((segment) => segment.length > 0);
|
|
415
|
+
const segments: string[] = [];
|
|
416
|
+
|
|
417
|
+
for (const segment of rawSegments) {
|
|
418
|
+
if (segment === '.') continue;
|
|
419
|
+
if (segment === '..') {
|
|
420
|
+
if (segments.length > 0 && segments[segments.length - 1] !== '..') {
|
|
421
|
+
segments.pop();
|
|
422
|
+
} else if (!hasRoot) {
|
|
423
|
+
segments.push('..');
|
|
424
|
+
}
|
|
425
|
+
continue;
|
|
426
|
+
}
|
|
427
|
+
segments.push(segment);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const joined = segments.join('/');
|
|
431
|
+
const comparable = drivePrefix
|
|
432
|
+
? `${drivePrefix}/${joined}`.replace(/\/$/, '')
|
|
433
|
+
: hasRoot
|
|
434
|
+
? `/${joined}`.replace(/\/$/, '')
|
|
435
|
+
: joined || '.';
|
|
436
|
+
// Restore prefers a conservative same-directory guard: false positives are safer than
|
|
437
|
+
// missing a Windows-style case-only path alias and deleting the source publish dir.
|
|
438
|
+
return comparable.toLowerCase();
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
function dirname(filePath: string): string {
|
|
442
|
+
const trimmed = trimTrailingSlashes(filePath);
|
|
443
|
+
const match = trimmed.match(/^(.*)[/\\][^/\\]+$/);
|
|
444
|
+
return match?.[1] ?? '';
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
function basename(filePath: string): string {
|
|
448
|
+
const trimmed = trimTrailingSlashes(filePath);
|
|
449
|
+
const match = trimmed.match(/([^/\\]+)$/);
|
|
450
|
+
return match?.[1] ?? '';
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function resolveOutputProjectPath(output: string, fs: Pick<RestoreFileSystem, 'join'>): string {
|
|
454
|
+
if (/\.fairy$/i.test(output)) return output;
|
|
455
|
+
const normalizedOutput = trimTrailingSlashes(output);
|
|
456
|
+
const projectName = basename(normalizedOutput) || 'Restored';
|
|
457
|
+
return fs.join(normalizedOutput, `${projectName}.fairy`);
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
async function prepareRestoreOutputDir(
|
|
461
|
+
inputDir: string,
|
|
462
|
+
outputDir: string,
|
|
463
|
+
outputProjectPath: string,
|
|
464
|
+
fs: RestoreFileSystem,
|
|
465
|
+
force: boolean,
|
|
466
|
+
outputIsProjectFile: boolean,
|
|
467
|
+
): Promise<void> {
|
|
468
|
+
const [resolvedInputDir, resolvedOutputDir] = await Promise.all([
|
|
469
|
+
Promise.resolve(fs.resolvePath(inputDir)),
|
|
470
|
+
Promise.resolve(fs.resolvePath(outputDir)),
|
|
471
|
+
]);
|
|
472
|
+
if (normalizeComparablePath(resolvedInputDir) === normalizeComparablePath(resolvedOutputDir)) {
|
|
473
|
+
throw new Error('Restore output directory must be different from the published input directory.');
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
if (outputIsProjectFile) {
|
|
477
|
+
if (!(await fs.exists(outputDir))) {
|
|
478
|
+
await fs.mkdir(outputDir);
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
try {
|
|
482
|
+
await fs.readdir(outputDir);
|
|
483
|
+
} catch {
|
|
484
|
+
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (!(await fs.exists(outputProjectPath))) return;
|
|
488
|
+
if (!force) {
|
|
489
|
+
throw new Error(`Restore output file already exists: ${outputProjectPath}. Use --force to overwrite it.`);
|
|
490
|
+
}
|
|
491
|
+
if (!fs.rm) {
|
|
492
|
+
throw new Error('Restore output file already exists and the provided fs does not support rm(...).');
|
|
493
|
+
}
|
|
494
|
+
await fs.rm(outputProjectPath, { recursive: true, force: true });
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const exists = await fs.exists(outputDir);
|
|
499
|
+
if (!exists) {
|
|
500
|
+
await fs.mkdir(outputDir);
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
let entries: string[];
|
|
505
|
+
try {
|
|
506
|
+
entries = await fs.readdir(outputDir);
|
|
507
|
+
} catch {
|
|
508
|
+
throw new Error(`Restore output path is not a directory: ${outputDir}`);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
if (entries.length === 0) return;
|
|
512
|
+
if (!force) {
|
|
513
|
+
throw new Error(`Restore output directory is not empty: ${outputDir}. Use --force to overwrite it.`);
|
|
514
|
+
}
|
|
515
|
+
if (!fs.rm) {
|
|
516
|
+
throw new Error('Restore output directory is not empty and the provided fs does not support rm(...).');
|
|
517
|
+
}
|
|
518
|
+
await fs.rm(outputDir, { recursive: true, force: true });
|
|
519
|
+
await fs.mkdir(outputDir);
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
export async function restore(options: RestoreOptions): Promise<RestoreResult> {
|
|
523
|
+
const sourceDir = trimTrailingSlashes(options.inputDir);
|
|
524
|
+
const outputIsProjectFile = /\.fairy$/i.test(options.output);
|
|
525
|
+
const outputProjectPath = resolveOutputProjectPath(options.output, options.fs);
|
|
526
|
+
const outputDir = dirname(outputProjectPath) || '.';
|
|
527
|
+
await prepareRestoreOutputDir(sourceDir, outputDir, outputProjectPath, options.fs, options.force === true, outputIsProjectFile);
|
|
528
|
+
|
|
529
|
+
const packageFilter = options.packages?.length ? new Set(options.packages) : null;
|
|
530
|
+
const candidateBinaryPaths = (await options.fs.readdir(sourceDir))
|
|
531
|
+
.filter((name) => isPublishedBinaryFile(name))
|
|
532
|
+
.filter((name) => !packageFilter || packageFilter.has(inferPackageName(name)))
|
|
533
|
+
.map((name) => options.fs.join(sourceDir, name))
|
|
534
|
+
.sort((left, right) => left.localeCompare(right));
|
|
535
|
+
const binaryPaths = (await Promise.all(
|
|
536
|
+
candidateBinaryPaths.map(async (filePath) => (await options.fs.isFile(filePath)) ? filePath : null),
|
|
537
|
+
))
|
|
538
|
+
.filter((filePath): filePath is string => !!filePath)
|
|
539
|
+
.sort((left, right) => left.localeCompare(right));
|
|
540
|
+
|
|
541
|
+
if (binaryPaths.length === 0) {
|
|
542
|
+
throw new Error(`No FairyGUI published binary files found in ${sourceDir}.`);
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
const restorer = new RestoreWorkflow(options.fs);
|
|
546
|
+
return restorer.restore({
|
|
547
|
+
binaryPaths,
|
|
548
|
+
sourceDir,
|
|
549
|
+
outputProjectPath,
|
|
550
|
+
projectType: options.projectType,
|
|
551
|
+
cropImage: options.cropImage,
|
|
552
|
+
extractImage: options.extractImage,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
class RestoreWorkflow {
|
|
557
|
+
private readonly _fs: RestoreFileSystem;
|
|
558
|
+
|
|
559
|
+
constructor(fs: RestoreFileSystem) {
|
|
560
|
+
this._fs = fs;
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async restore(options: RestoreExecutionOptions): Promise<RestoreResult> {
|
|
564
|
+
const warnings: string[] = [];
|
|
565
|
+
const reader = new BinaryReader(this._fs);
|
|
566
|
+
const doc = await reader.readMany(options.binaryPaths);
|
|
567
|
+
this._initializeProjectDefaults(doc, options.projectType);
|
|
568
|
+
this._initializeImageFileNames(doc);
|
|
569
|
+
this._initializeLooseResourceFileNames(doc);
|
|
570
|
+
await this._synthesizeLooseSkeletonResources(doc, options.sourceDir);
|
|
571
|
+
this._initializeRestoredResourceRelations(doc);
|
|
572
|
+
this._initializePublishedFontTextureIds(doc);
|
|
573
|
+
this._initializeFontTextureImageResources(doc);
|
|
574
|
+
this._initializeFontGlyphImageResources(doc);
|
|
575
|
+
this._initializePublishedTextFontResources(doc);
|
|
576
|
+
this._initializeDisplayObjectFileNames(doc);
|
|
577
|
+
this._initializePublishedFontDefaults(doc);
|
|
578
|
+
|
|
579
|
+
const writer = new ProjectWriter(this._fs);
|
|
580
|
+
await writer.write(doc, options.outputProjectPath);
|
|
581
|
+
await this._restoreAssets(doc, options, warnings);
|
|
582
|
+
|
|
583
|
+
return {
|
|
584
|
+
document: doc,
|
|
585
|
+
projectPath: options.outputProjectPath,
|
|
586
|
+
warnings,
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
private _initializeProjectDefaults(doc: Document, projectType?: number): void {
|
|
591
|
+
doc.getRoot()
|
|
592
|
+
.setProjectId(generateId())
|
|
593
|
+
.setProjectType(projectType ?? ProjectType.Unity)
|
|
594
|
+
.setVersion('3.0')
|
|
595
|
+
.setSettings({
|
|
596
|
+
publish: {
|
|
597
|
+
binaryFormat: true,
|
|
598
|
+
fileExtension: 'bytes',
|
|
599
|
+
compressDesc: false,
|
|
600
|
+
},
|
|
601
|
+
common: {},
|
|
602
|
+
adaptation: {},
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
private _initializeImageFileNames(doc: Document): void {
|
|
607
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
608
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
609
|
+
if (resource.propertyType !== 'ImageResource') continue;
|
|
610
|
+
imageFileName(resource);
|
|
611
|
+
resource.setExtras?.({
|
|
612
|
+
...(resource.getExtras?.() ?? {}),
|
|
613
|
+
_suppressPackageSize: true,
|
|
614
|
+
});
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
private _initializeLooseResourceFileNames(doc: Document): void {
|
|
620
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
621
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
622
|
+
if (!['MiscResource', 'SpineResource', 'DragonBonesResource', 'SoundResource'].includes(resource.propertyType)) continue;
|
|
623
|
+
const current = resource.getFile?.() ?? '';
|
|
624
|
+
if (!current) continue;
|
|
625
|
+
const normalized = replaceLooseResourceBaseName(resource, current);
|
|
626
|
+
if (normalized !== current) resource.setFile?.(normalized);
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
private async _synthesizeLooseSkeletonResources(doc: Document, sourceDir: string): Promise<void> {
|
|
632
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
633
|
+
for (const resource of [...(pkg.listResources() as RestorableResource[])]) {
|
|
634
|
+
let current = resource;
|
|
635
|
+
if (resource.propertyType === 'DragonBonesResource' && /\.skel\.bytes$/i.test(resourceFileName(resource))) {
|
|
636
|
+
const normalizedFile = resourceFileName(resource).replace(/\.skel\.bytes$/i, '.skel');
|
|
637
|
+
const skeletonBase = stripExtension(normalizedFile);
|
|
638
|
+
const atlasBase = skeletonBase.replace(/-(?:pro|ess)$/i, '-pma');
|
|
639
|
+
const atlasSource = await this._resolveLooseSourceFile(pkg, sourceDir, `${atlasBase}.atlas`);
|
|
640
|
+
if (atlasSource) current = this._replaceSkeletonResourceType(doc, pkg, resource, 'SpineResource', normalizedFile);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
if (current.propertyType === 'SpineResource') {
|
|
644
|
+
await this._ensureSpineSidecarResources(doc, pkg, current, sourceDir);
|
|
645
|
+
} else if (current.propertyType === 'DragonBonesResource') {
|
|
646
|
+
await this._ensureDragonBonesSidecarResources(doc, pkg, current, sourceDir);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
private _initializeRestoredResourceRelations(doc: Document): void {
|
|
653
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
654
|
+
const resources = pkg.listResources() as RestorableResource[];
|
|
655
|
+
for (const resource of resources) {
|
|
656
|
+
if (resource.propertyType === 'SpineResource') {
|
|
657
|
+
this._initializeSpineResourceRelation(resource, resources);
|
|
658
|
+
} else if (resource.propertyType === 'DragonBonesResource') {
|
|
659
|
+
this._initializeDragonBonesResourceRelation(resource, resources);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
private _replaceSkeletonResourceType(
|
|
666
|
+
doc: Document,
|
|
667
|
+
pkg: Package,
|
|
668
|
+
resource: RestorableResource,
|
|
669
|
+
targetType: 'SpineResource' | 'DragonBonesResource',
|
|
670
|
+
fileName: string,
|
|
671
|
+
): RestorableResource {
|
|
672
|
+
const replacement = targetType === 'SpineResource'
|
|
673
|
+
? doc.createSpineResource(resource.getName?.() ?? '')
|
|
674
|
+
: doc.createDragonBonesResource(resource.getName?.() ?? '');
|
|
675
|
+
|
|
676
|
+
replacement
|
|
677
|
+
.setId?.(resource.getId?.() ?? '')
|
|
678
|
+
.setPath?.(resource.getPath?.() ?? '/')
|
|
679
|
+
.setFile?.(fileName)
|
|
680
|
+
.setExported?.(resource.getExported?.() ?? false)
|
|
681
|
+
.setWidth?.(resource.getWidth?.() ?? 0)
|
|
682
|
+
.setHeight?.(resource.getHeight?.() ?? 0)
|
|
683
|
+
.setRequireIds?.(resource.getRequireIds?.() ?? [])
|
|
684
|
+
.setAtlasNames?.(resource.getAtlasNames?.() ?? [])
|
|
685
|
+
.setAnchor?.(resource.getAnchorX?.() ?? 0, resource.getAnchorY?.() ?? 0)
|
|
686
|
+
.setBranch?.(resource.getBranch?.() ?? '')
|
|
687
|
+
.setBranchItemIds?.(resource.getBranchItemIds?.() ?? []);
|
|
688
|
+
replacement.setExtras?.({ ...(resource.getExtras?.() ?? {}) });
|
|
689
|
+
|
|
690
|
+
pkg.removeResource(resource as never);
|
|
691
|
+
pkg.addResource(replacement as never);
|
|
692
|
+
return replacement as RestorableResource;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
private async _ensureSpineSidecarResources(
|
|
696
|
+
doc: Document,
|
|
697
|
+
pkg: Package,
|
|
698
|
+
resource: RestorableResource,
|
|
699
|
+
sourceDir: string,
|
|
700
|
+
): Promise<void> {
|
|
701
|
+
const fileName = resourceFileName(resource).replace(/\.skel\.bytes$/i, '.skel');
|
|
702
|
+
const skeletonBase = stripExtension(fileName);
|
|
703
|
+
if (!skeletonBase) return;
|
|
704
|
+
const atlasBase = skeletonBase.replace(/-(?:pro|ess)$/i, '-pma');
|
|
705
|
+
const atlas = await this._ensureLooseMiscResource(doc, pkg, resource, sourceDir, `${atlasBase}.atlas`);
|
|
706
|
+
const texture = await this._ensureLooseImageResource(doc, pkg, resource, sourceDir, `${atlasBase}.png`);
|
|
707
|
+
const requireIds = [atlas?.getId?.(), texture?.getId?.()].filter((id): id is string => !!id);
|
|
708
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
709
|
+
if (atlas) resource.setAtlasNames?.([atlasBase]);
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
private async _ensureDragonBonesSidecarResources(
|
|
713
|
+
doc: Document,
|
|
714
|
+
pkg: Package,
|
|
715
|
+
resource: RestorableResource,
|
|
716
|
+
sourceDir: string,
|
|
717
|
+
): Promise<void> {
|
|
718
|
+
const skeletonBase = stripExtension(resourceFileName(resource)).replace(/_ske$/i, '');
|
|
719
|
+
if (!skeletonBase) return;
|
|
720
|
+
const textureJson = await this._ensureLooseMiscResource(doc, pkg, resource, sourceDir, `${skeletonBase}_tex.json`);
|
|
721
|
+
const textureImage = await this._ensureLooseImageResource(doc, pkg, resource, sourceDir, `${skeletonBase}.png`);
|
|
722
|
+
const requireIds = [textureJson?.getId?.(), textureImage?.getId?.()].filter((id): id is string => !!id);
|
|
723
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
private async _ensureLooseMiscResource(
|
|
727
|
+
doc: Document,
|
|
728
|
+
pkg: Package,
|
|
729
|
+
owner: RestorableResource,
|
|
730
|
+
sourceDir: string,
|
|
731
|
+
fileName: string,
|
|
732
|
+
): Promise<RestorableResource | null> {
|
|
733
|
+
const resources = pkg.listResources() as RestorableResource[];
|
|
734
|
+
const existing = this._findResourceByFile(resources, owner, 'MiscResource', fileName);
|
|
735
|
+
if (existing) return existing;
|
|
736
|
+
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
737
|
+
if (!sourcePath) return null;
|
|
738
|
+
const resource = doc.createMiscResource(stripExtension(fileName));
|
|
739
|
+
resource
|
|
740
|
+
.setId(generateId())
|
|
741
|
+
.setPath(owner.getPath?.() ?? '/')
|
|
742
|
+
.setBranch(owner.getBranch?.() ?? '')
|
|
743
|
+
.setBranchItemIds(owner.getBranchItemIds?.() ?? [])
|
|
744
|
+
.setExported(false)
|
|
745
|
+
.setFile(fileName);
|
|
746
|
+
resource.setExtras?.({ ...(resource.getExtras?.() ?? {}), _publishedFile: fileBaseName(sourcePath) });
|
|
747
|
+
pkg.addResource(resource);
|
|
748
|
+
return resource as RestorableResource;
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
private async _ensureLooseImageResource(
|
|
752
|
+
doc: Document,
|
|
753
|
+
pkg: Package,
|
|
754
|
+
owner: RestorableResource,
|
|
755
|
+
sourceDir: string,
|
|
756
|
+
fileName: string,
|
|
757
|
+
): Promise<RestorableResource | null> {
|
|
758
|
+
const resources = pkg.listResources() as RestorableResource[];
|
|
759
|
+
const existing = this._findResourceByFile(resources, owner, 'ImageResource', fileName);
|
|
760
|
+
if (existing) return existing;
|
|
761
|
+
const sourcePath = await this._resolveLooseSourceFile(pkg, sourceDir, fileName);
|
|
762
|
+
if (!sourcePath) return null;
|
|
763
|
+
const resource = doc.createImageResource(stripExtension(fileName));
|
|
764
|
+
resource
|
|
765
|
+
.setId(generateId())
|
|
766
|
+
.setPath(owner.getPath?.() ?? '/')
|
|
767
|
+
.setBranch(owner.getBranch?.() ?? '')
|
|
768
|
+
.setBranchItemIds(owner.getBranchItemIds?.() ?? [])
|
|
769
|
+
.setExported(false)
|
|
770
|
+
.setFileName(fileName);
|
|
771
|
+
resource.setExtras?.({
|
|
772
|
+
...(resource.getExtras?.() ?? {}),
|
|
773
|
+
_publishedFile: fileBaseName(sourcePath),
|
|
774
|
+
_suppressPackageSize: true,
|
|
775
|
+
_syntheticLooseImage: true,
|
|
776
|
+
});
|
|
777
|
+
pkg.addResource(resource);
|
|
778
|
+
return resource as RestorableResource;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
private _initializeDisplayObjectFileNames(doc: Document): void {
|
|
782
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
783
|
+
for (const component of pkg.listComponents()) {
|
|
784
|
+
for (const child of component.listChildren() as RestorableDisplayObject[]) {
|
|
785
|
+
if (!child.setFileName || child.getFileName?.()) continue;
|
|
786
|
+
const resource = this._resolveDisplayObjectResource(doc, pkg, child);
|
|
787
|
+
const fileName = resource ? resourceInstanceFileName(resource) : '';
|
|
788
|
+
if (fileName) child.setFileName(fileName);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
|
|
794
|
+
private _initializeFontGlyphImageResources(doc: Document): void {
|
|
795
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
796
|
+
for (const resource of [...pkg.listResources()] as RestorableFontResource[]) {
|
|
797
|
+
if (resource.propertyType !== 'FontResource') continue;
|
|
798
|
+
const glyphEntries = new Map<string, { glyph: RestorableFontGlyph; index: number }>();
|
|
799
|
+
for (const [index, glyph] of resource.listGlyphs().entries()) {
|
|
800
|
+
const glyphId = glyph.getImg?.() ?? '';
|
|
801
|
+
if (!glyphId || glyphEntries.has(glyphId)) continue;
|
|
802
|
+
glyphEntries.set(glyphId, { glyph, index });
|
|
803
|
+
}
|
|
804
|
+
for (const [glyphId, entry] of glyphEntries) {
|
|
805
|
+
if (pkg.getResourceById(glyphId)) continue;
|
|
806
|
+
const image = doc.createImageResource(glyphId);
|
|
807
|
+
image
|
|
808
|
+
.setId(glyphId)
|
|
809
|
+
.setPath(syntheticFontGlyphVirtualPath(pkg, resource))
|
|
810
|
+
.setBranch(resource.getBranch?.() ?? '')
|
|
811
|
+
.setFileName(syntheticFontGlyphFileName(pkg, resource, entry.glyph, entry.index, glyphEntries.size))
|
|
812
|
+
.setExtras({
|
|
813
|
+
...(image.getExtras?.() ?? {}),
|
|
814
|
+
_syntheticFontGlyph: true,
|
|
815
|
+
_packageOrderAfterId: resource.getId?.() ?? '',
|
|
816
|
+
_packageOrderWeight: 1,
|
|
817
|
+
_suppressPackageSize: true,
|
|
818
|
+
});
|
|
819
|
+
pkg.addResource(image);
|
|
820
|
+
}
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
private _initializeFontTextureImageResources(doc: Document): void {
|
|
826
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
827
|
+
for (const resource of [...pkg.listResources()] as RestorableFontResource[]) {
|
|
828
|
+
if (resource.propertyType !== 'FontResource') continue;
|
|
829
|
+
const textureId = resource.getTextureId?.() ?? '';
|
|
830
|
+
if (!textureId || pkg.getResourceById(textureId)) continue;
|
|
831
|
+
const image = doc.createImageResource(textureId);
|
|
832
|
+
image
|
|
833
|
+
.setId(textureId)
|
|
834
|
+
.setPath(resource.getPath?.() ?? '/')
|
|
835
|
+
.setBranch(resource.getBranch?.() ?? '')
|
|
836
|
+
.setFileName(syntheticFontTextureFileName(resource))
|
|
837
|
+
.setExtras({
|
|
838
|
+
...(image.getExtras?.() ?? {}),
|
|
839
|
+
_syntheticFontTexture: true,
|
|
840
|
+
_packageOrderAfterId: resource.getId?.() ?? '',
|
|
841
|
+
_packageOrderWeight: 0,
|
|
842
|
+
_suppressPackageSize: true,
|
|
843
|
+
});
|
|
844
|
+
pkg.addResource(image);
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
private _resolveDisplayObjectResource(
|
|
850
|
+
doc: Document,
|
|
851
|
+
pkg: Package,
|
|
852
|
+
child: RestorableDisplayObject,
|
|
853
|
+
): RestorableResource | null {
|
|
854
|
+
const src = child.getSrc?.() ?? '';
|
|
855
|
+
if (!src) return null;
|
|
856
|
+
const targetPackage = child.getPackageId?.()
|
|
857
|
+
? doc.getRoot().getPackageById(child.getPackageId?.() ?? '')
|
|
858
|
+
: pkg;
|
|
859
|
+
return (targetPackage?.getResourceById(src) as RestorableResource | null) ?? null;
|
|
860
|
+
}
|
|
861
|
+
|
|
862
|
+
private _initializeSpineResourceRelation(resource: RestorableResource, resources: RestorableResource[]): void {
|
|
863
|
+
const fileName = resourceFileName(resource);
|
|
864
|
+
const skeletonBase = stripExtension(fileName);
|
|
865
|
+
if (!skeletonBase) return;
|
|
866
|
+
const atlasBase = skeletonBase.replace(/-(?:pro|ess)$/i, '-pma');
|
|
867
|
+
const requireIds: string[] = [];
|
|
868
|
+
|
|
869
|
+
const atlas = this._findResourceByFile(resources, resource, 'MiscResource', `${atlasBase}.atlas`);
|
|
870
|
+
const texture = this._findResourceByFile(resources, resource, 'ImageResource', `${atlasBase}.png`);
|
|
871
|
+
if (atlas?.getId?.()) requireIds.push(atlas.getId());
|
|
872
|
+
if (texture?.getId?.()) requireIds.push(texture.getId());
|
|
873
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
874
|
+
if (atlas) resource.setAtlasNames?.([atlasBase]);
|
|
875
|
+
}
|
|
876
|
+
|
|
877
|
+
private _initializeDragonBonesResourceRelation(resource: RestorableResource, resources: RestorableResource[]): void {
|
|
878
|
+
const fileName = resourceFileName(resource);
|
|
879
|
+
const skeletonBase = stripExtension(fileName).replace(/_ske$/i, '');
|
|
880
|
+
if (!skeletonBase) return;
|
|
881
|
+
const requireIds: string[] = [];
|
|
882
|
+
|
|
883
|
+
const textureJson = this._findResourceByFile(resources, resource, 'MiscResource', `${skeletonBase}_tex.json`);
|
|
884
|
+
const textureImage = this._findResourceByFile(resources, resource, 'ImageResource', `${skeletonBase}.png`);
|
|
885
|
+
if (textureJson?.getId?.()) requireIds.push(textureJson.getId());
|
|
886
|
+
if (textureImage?.getId?.()) requireIds.push(textureImage.getId());
|
|
887
|
+
if (requireIds.length > 0) resource.setRequireIds?.(requireIds);
|
|
888
|
+
}
|
|
889
|
+
|
|
890
|
+
private _findResourceByFile(
|
|
891
|
+
resources: RestorableResource[],
|
|
892
|
+
owner: RestorableResource,
|
|
893
|
+
propertyType: string,
|
|
894
|
+
fileName: string,
|
|
895
|
+
): RestorableResource | null {
|
|
896
|
+
const expected = fileName.toLowerCase();
|
|
897
|
+
return resources.find((resource) => {
|
|
898
|
+
return resource.propertyType === propertyType
|
|
899
|
+
&& sameVirtualPath(owner, resource)
|
|
900
|
+
&& fileBaseName(resourceFileName(resource)).toLowerCase() === expected;
|
|
901
|
+
}) ?? null;
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
private _initializePublishedFontDefaults(doc: Document): void {
|
|
905
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
906
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
907
|
+
if (resource.propertyType !== 'FontResource') continue;
|
|
908
|
+
const fileName = resourceFileName(resource);
|
|
909
|
+
if (!/\bsdf\b/i.test(fileName)) continue;
|
|
910
|
+
if (!resource.getRenderMode?.()) resource.setRenderMode?.('sdfaa');
|
|
911
|
+
if (!resource.getSamplePointSize?.()) resource.setSamplePointSize?.(60);
|
|
912
|
+
}
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
private _initializePublishedTextFontResources(doc: Document): void {
|
|
917
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
918
|
+
const fontResources = (pkg.listResources() as RestorableResource[]).filter((resource) => resource.propertyType === 'FontResource');
|
|
919
|
+
const fontByFileName = new Map(
|
|
920
|
+
fontResources.map((resource) => [resourceFileName(resource).toLowerCase(), resource] as const),
|
|
921
|
+
);
|
|
922
|
+
const fontByDisplayName = new Map(
|
|
923
|
+
fontResources.map((resource) => [stripExtension(resourceFileName(resource)).toLowerCase(), resource] as const),
|
|
924
|
+
);
|
|
925
|
+
|
|
926
|
+
for (const component of pkg.listComponents()) {
|
|
927
|
+
for (const child of component.listChildren() as RestorableDisplayObject[]) {
|
|
928
|
+
const font = child.getFont?.() ?? '';
|
|
929
|
+
if (!font || font.startsWith('ui://')) continue;
|
|
930
|
+
if (!/\bsdf\b/i.test(font)) continue;
|
|
931
|
+
|
|
932
|
+
const normalized = font.trim().toLowerCase();
|
|
933
|
+
let resource = fontByDisplayName.get(normalized) ?? fontByFileName.get(`${normalized}.ttf`);
|
|
934
|
+
if (!resource) {
|
|
935
|
+
resource = doc.createFontResource(font.trim());
|
|
936
|
+
resource
|
|
937
|
+
.setId(generateId())
|
|
938
|
+
.setPath('/font/')
|
|
939
|
+
.setFileName(`${font.trim()}.ttf`)
|
|
940
|
+
.setExported(false)
|
|
941
|
+
.setRenderMode('sdfaa')
|
|
942
|
+
.setSamplePointSize(60)
|
|
943
|
+
.setTtf(true);
|
|
944
|
+
pkg.addResource(resource as never);
|
|
945
|
+
fontByDisplayName.set(normalized, resource);
|
|
946
|
+
fontByFileName.set(`${normalized}.ttf`, resource);
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
child.setFont?.(`ui://${pkg.getId()}${resource.getId?.() ?? ''}`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
private _initializePublishedFontTextureIds(doc: Document): void {
|
|
956
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
957
|
+
const resources = pkg.listResources() as RestorableResource[];
|
|
958
|
+
for (const resource of resources) {
|
|
959
|
+
if (resource.propertyType !== 'FontResource') continue;
|
|
960
|
+
if (resource.getTextureId?.()) continue;
|
|
961
|
+
if (resource.getTtf?.() !== true) continue;
|
|
962
|
+
const expectedFileName = syntheticFontTextureFileName(resource).toLowerCase();
|
|
963
|
+
const texture = resources.find((candidate) => {
|
|
964
|
+
return candidate.propertyType === 'ImageResource'
|
|
965
|
+
&& sameVirtualPath(resource, candidate)
|
|
966
|
+
&& fileBaseName(resourceFileName(candidate)).toLowerCase() === expectedFileName;
|
|
967
|
+
});
|
|
968
|
+
if (texture?.getId?.()) resource.setTextureId?.(texture.getId());
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
private async _restoreAssets(
|
|
974
|
+
doc: Document,
|
|
975
|
+
options: RestoreExecutionOptions,
|
|
976
|
+
warnings: string[],
|
|
977
|
+
): Promise<void> {
|
|
978
|
+
for (const pkg of doc.getRoot().listPackages()) {
|
|
979
|
+
await this._restoreAtlasImages(pkg, options);
|
|
980
|
+
await this._writeGeneratedResources(pkg, options, warnings);
|
|
981
|
+
await this._copyLooseResources(pkg, options, warnings);
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
|
|
985
|
+
private async _restoreAtlasImages(pkg: Package, options: RestoreExecutionOptions): Promise<void> {
|
|
986
|
+
if (!options.cropImage) return;
|
|
987
|
+
for (const atlas of pkg.listAtlases()) {
|
|
988
|
+
const sourceAtlas = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, atlas.getFile()));
|
|
989
|
+
if (!sourceAtlas) {
|
|
990
|
+
throw new Error(`Atlas image not found for package "${pkg.getName()}": ${this._sourceFileCandidates(pkg, atlas.getFile()).join(', ')}`);
|
|
991
|
+
}
|
|
992
|
+
for (const sprite of atlas.listSprites() as RestorableSprite[]) {
|
|
993
|
+
const image = findImageResource(pkg, sprite.getItemId());
|
|
994
|
+
if (!image) continue;
|
|
995
|
+
if (sprite.getRectWidth() <= 0 || sprite.getRectHeight() <= 0) continue;
|
|
996
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, image, imageFileName(image));
|
|
997
|
+
const imageWidth = image.getWidth?.() ?? 0;
|
|
998
|
+
const imageHeight = image.getHeight?.() ?? 0;
|
|
999
|
+
const spriteWidth = sprite.getRotated() ? sprite.getRectHeight() : sprite.getRectWidth();
|
|
1000
|
+
const spriteHeight = sprite.getRotated() ? sprite.getRectWidth() : sprite.getRectHeight();
|
|
1001
|
+
await this._mkdirForFile(outputPath);
|
|
1002
|
+
await options.cropImage({
|
|
1003
|
+
sourcePath: sourceAtlas,
|
|
1004
|
+
outputPath,
|
|
1005
|
+
left: sprite.getRectX(),
|
|
1006
|
+
top: sprite.getRectY(),
|
|
1007
|
+
width: sprite.getRectWidth(),
|
|
1008
|
+
height: sprite.getRectHeight(),
|
|
1009
|
+
rotated: sprite.getRotated(),
|
|
1010
|
+
offsetX: sprite.getOffsetX(),
|
|
1011
|
+
offsetY: sprite.getOffsetY(),
|
|
1012
|
+
expectedWidth: Math.max(imageWidth, sprite.getOriginalWidth(), spriteWidth),
|
|
1013
|
+
expectedHeight: Math.max(imageHeight, sprite.getOriginalHeight(), spriteHeight),
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
|
|
1019
|
+
private async _copyLooseResources(
|
|
1020
|
+
pkg: Package,
|
|
1021
|
+
options: RestoreExecutionOptions,
|
|
1022
|
+
warnings: string[],
|
|
1023
|
+
): Promise<void> {
|
|
1024
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
1025
|
+
const syntheticLooseImage = resource.getExtras?.()?._syntheticLooseImage === true;
|
|
1026
|
+
if (!['SoundResource', 'MiscResource', 'SpineResource', 'DragonBonesResource'].includes(resource.propertyType) && !syntheticLooseImage) {
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
const fileName = resourceFileName(resource);
|
|
1030
|
+
if (!fileName) continue;
|
|
1031
|
+
const sourcePath = await this._resolveSourceFile(
|
|
1032
|
+
options.sourceDir,
|
|
1033
|
+
this._sourceFileCandidates(pkg, resourcePublishedFileName(resource), fileName),
|
|
1034
|
+
);
|
|
1035
|
+
if (!sourcePath) {
|
|
1036
|
+
warnings.push(`Loose resource not found for package "${pkg.getName()}": ${fileName}`);
|
|
1037
|
+
continue;
|
|
1038
|
+
}
|
|
1039
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
1040
|
+
await this._mkdirForFile(outputPath);
|
|
1041
|
+
await this._fs.writeFileRaw(outputPath, await this._fs.readFileRaw(sourcePath));
|
|
1042
|
+
}
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
private async _writeGeneratedResources(
|
|
1046
|
+
pkg: Package,
|
|
1047
|
+
options: RestoreExecutionOptions,
|
|
1048
|
+
warnings: string[],
|
|
1049
|
+
): Promise<void> {
|
|
1050
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
1051
|
+
if (resource.propertyType === 'FontResource') {
|
|
1052
|
+
await this._writeFontFile(pkg, resource, options.outputProjectPath);
|
|
1053
|
+
} else if (resource.propertyType === 'MovieClipResource') {
|
|
1054
|
+
await this._writeMovieClipFile(pkg, resource, options, warnings);
|
|
1055
|
+
}
|
|
1056
|
+
}
|
|
1057
|
+
await this._writeSyntheticFontGlyphImages(pkg, options.outputProjectPath);
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
private async _writeFontFile(pkg: Package, resource: RestorableResource, outputProjectPath: string): Promise<void> {
|
|
1061
|
+
const fileName = resourceFileName(resource);
|
|
1062
|
+
if (!/\.fnt$/i.test(fileName)) return;
|
|
1063
|
+
const glyphs = resource.listGlyphs?.() ?? [];
|
|
1064
|
+
if (glyphs.length === 0) return;
|
|
1065
|
+
|
|
1066
|
+
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
1067
|
+
await this._mkdirForFile(outputPath);
|
|
1068
|
+
await this._fs.writeFile(outputPath, this._serializeFont(pkg, resource, glyphs));
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
private _serializeFont(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string {
|
|
1072
|
+
const isTtf = resource.getTtf?.() === true;
|
|
1073
|
+
const lines = isTtf
|
|
1074
|
+
? this._serializeTtfFontHeader(pkg, resource, glyphs)
|
|
1075
|
+
: [
|
|
1076
|
+
'info creator=UIBuilder',
|
|
1077
|
+
`common lineHeight=${resource.getLineHeight?.() ?? 0}`,
|
|
1078
|
+
];
|
|
1079
|
+
|
|
1080
|
+
for (const glyph of glyphs) {
|
|
1081
|
+
const charId = fontGlyphCharId(glyph);
|
|
1082
|
+
if (isTtf) {
|
|
1083
|
+
lines.push(
|
|
1084
|
+
`char id=${charId} x=${glyph.getX()} y=${glyph.getY()} width=${glyph.getWidth()} height=${glyph.getHeight()} `
|
|
1085
|
+
+ `xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()} page=0 chnl=${glyph.getChannel()}`,
|
|
1086
|
+
);
|
|
1087
|
+
} else {
|
|
1088
|
+
lines.push(
|
|
1089
|
+
`char id=${charId} img=${glyph.getImg()} xoffset=${glyph.getXOffset()} yoffset=${glyph.getYOffset()} xadvance=${glyph.getAdvance()}`,
|
|
1090
|
+
);
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
return `${lines.join('\n')}\n`;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
1097
|
+
private _serializeTtfFontHeader(pkg: Package, resource: RestorableResource, glyphs: RestorableFontGlyph[]): string[] {
|
|
1098
|
+
const fileName = resourceFileName(resource);
|
|
1099
|
+
const face = stripExtension(fileName) || resource.getName?.() || 'Font';
|
|
1100
|
+
const lineHeight = resource.getLineHeight?.() ?? 0;
|
|
1101
|
+
const fontSize = resource.getFontSize?.() ?? lineHeight;
|
|
1102
|
+
const textureId = resource.getTextureId?.() ?? '';
|
|
1103
|
+
const textureResource = textureId ? pkg.getResourceById(textureId) as RestorableResource | null : null;
|
|
1104
|
+
const textureName = textureResource ? resourceFileName(textureResource) : `${face}_atlas.png`;
|
|
1105
|
+
const scaleW = textureResource?.getWidth?.() ?? 256;
|
|
1106
|
+
const scaleH = textureResource?.getHeight?.() ?? 256;
|
|
1107
|
+
const base = Math.max(Math.min(fontSize, lineHeight) - 6, 0);
|
|
1108
|
+
return [
|
|
1109
|
+
`info face="${face}" size=${fontSize} bold=0 italic=0 charset="" unicode=1 stretchH=100 smooth=1 aa=1 padding=0,0,0,0 spacing=1,1 outline=0`,
|
|
1110
|
+
`common lineHeight=${lineHeight} base=${base} scaleW=${scaleW} scaleH=${scaleH} pages=1 packed=0 alphaChnl=${resource.getTint?.() ? 1 : 0} redChnl=0 greenChnl=0 blueChnl=0`,
|
|
1111
|
+
`page id=0 file="${textureName}"`,
|
|
1112
|
+
`chars count=${glyphs.length}`,
|
|
1113
|
+
];
|
|
1114
|
+
}
|
|
1115
|
+
|
|
1116
|
+
private async _writeMovieClipFile(
|
|
1117
|
+
pkg: Package,
|
|
1118
|
+
resource: RestorableResource,
|
|
1119
|
+
options: RestoreExecutionOptions,
|
|
1120
|
+
warnings: string[],
|
|
1121
|
+
): Promise<void> {
|
|
1122
|
+
const fileName = resourceFileName(resource);
|
|
1123
|
+
if (!/\.jta$/i.test(fileName)) return;
|
|
1124
|
+
const frames = resource.listFrames?.() ?? [];
|
|
1125
|
+
if (frames.length === 0) return;
|
|
1126
|
+
if (!options.extractImage) {
|
|
1127
|
+
warnings.push(`MovieClip file not generated for package "${pkg.getName()}": ${fileName}`);
|
|
1128
|
+
return;
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
const sprites = await this._buildSpriteLookup(pkg, options);
|
|
1132
|
+
const textures: Uint8Array[] = [];
|
|
1133
|
+
for (const [index, frame] of frames.entries()) {
|
|
1134
|
+
const spriteEntry = sprites.get(frame.getSpriteId());
|
|
1135
|
+
if (!spriteEntry) {
|
|
1136
|
+
warnings.push(`MovieClip frame sprite not found for package "${pkg.getName()}": ${fileName} frame ${index}`);
|
|
1137
|
+
return;
|
|
1138
|
+
}
|
|
1139
|
+
const sprite = spriteEntry.sprite;
|
|
1140
|
+
if (sprite.getRectWidth() <= 0 || sprite.getRectHeight() <= 0) {
|
|
1141
|
+
textures.push(new Uint8Array(0));
|
|
1142
|
+
continue;
|
|
1143
|
+
}
|
|
1144
|
+
textures.push(await options.extractImage({
|
|
1145
|
+
sourcePath: spriteEntry.sourceAtlas,
|
|
1146
|
+
left: sprite.getRectX(),
|
|
1147
|
+
top: sprite.getRectY(),
|
|
1148
|
+
width: sprite.getRectWidth(),
|
|
1149
|
+
height: sprite.getRectHeight(),
|
|
1150
|
+
rotated: sprite.getRotated(),
|
|
1151
|
+
offsetX: 0,
|
|
1152
|
+
offsetY: 0,
|
|
1153
|
+
expectedWidth: sprite.getRotated() ? sprite.getRectHeight() : sprite.getRectWidth(),
|
|
1154
|
+
expectedHeight: sprite.getRotated() ? sprite.getRectWidth() : sprite.getRectHeight(),
|
|
1155
|
+
}));
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1158
|
+
const outputPath = this._resourceOutputPath(options.outputProjectPath, pkg, resource, fileName);
|
|
1159
|
+
await this._mkdirForFile(outputPath);
|
|
1160
|
+
await this._fs.writeFileRaw(outputPath, this._serializeMovieClip(resource, frames, textures));
|
|
1161
|
+
}
|
|
1162
|
+
|
|
1163
|
+
private async _writeSyntheticFontGlyphImages(pkg: Package, outputProjectPath: string): Promise<void> {
|
|
1164
|
+
for (const resource of pkg.listResources() as RestorableResource[]) {
|
|
1165
|
+
if (resource.propertyType !== 'ImageResource' || !isSyntheticFontGlyphResource(resource)) continue;
|
|
1166
|
+
const fileName = resourceFileName(resource) || defaultSyntheticFontGlyphFileName(resource.getId?.() ?? 'glyph');
|
|
1167
|
+
const outputPath = this._resourceOutputPath(outputProjectPath, pkg, resource, fileName);
|
|
1168
|
+
await this._mkdirForFile(outputPath);
|
|
1169
|
+
await this._fs.writeFileRaw(outputPath, TRANSPARENT_PNG_1X1);
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
private async _buildSpriteLookup(
|
|
1174
|
+
pkg: Package,
|
|
1175
|
+
options: RestoreExecutionOptions,
|
|
1176
|
+
): Promise<Map<string, SpriteLookupEntry>> {
|
|
1177
|
+
const sprites = new Map<string, SpriteLookupEntry>();
|
|
1178
|
+
for (const atlas of pkg.listAtlases()) {
|
|
1179
|
+
const sourceAtlas = await this._resolveSourceFile(options.sourceDir, this._sourceFileCandidates(pkg, atlas.getFile()));
|
|
1180
|
+
if (!sourceAtlas) {
|
|
1181
|
+
throw new Error(`Atlas image not found for package "${pkg.getName()}": ${this._sourceFileCandidates(pkg, atlas.getFile()).join(', ')}`);
|
|
1182
|
+
}
|
|
1183
|
+
for (const sprite of atlas.listSprites() as RestorableSprite[]) {
|
|
1184
|
+
sprites.set(sprite.getItemId(), { sourceAtlas, sprite });
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
return sprites;
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
private _serializeMovieClip(
|
|
1191
|
+
resource: RestorableResource,
|
|
1192
|
+
frames: RestorableMovieFrame[],
|
|
1193
|
+
textures: Uint8Array[],
|
|
1194
|
+
): Uint8Array {
|
|
1195
|
+
const chunks: Uint8Array[] = [
|
|
1196
|
+
encodeJtaUtf(JTA_FILE_MARK),
|
|
1197
|
+
writeInt32(JTA_VERSION),
|
|
1198
|
+
writeByte(0),
|
|
1199
|
+
writeByte(0),
|
|
1200
|
+
writeByte(0),
|
|
1201
|
+
writeByte(0),
|
|
1202
|
+
writeUint16(0),
|
|
1203
|
+
writeUint16(0),
|
|
1204
|
+
writeUint16(resource.getWidth?.() ?? 0),
|
|
1205
|
+
writeUint16(resource.getHeight?.() ?? 0),
|
|
1206
|
+
writeByte(jtaSpeed(resource.getInterval?.() ?? 0)),
|
|
1207
|
+
writeByte(scaledFrameDelay(resource.getRepeatDelay?.() ?? 0)),
|
|
1208
|
+
writeByte(resource.getSwing?.() ? 1 : 0),
|
|
1209
|
+
writeInt16(frames.length),
|
|
1210
|
+
];
|
|
1211
|
+
|
|
1212
|
+
for (const [index, frame] of frames.entries()) {
|
|
1213
|
+
chunks.push(
|
|
1214
|
+
writeInt16(scaledFrameDelay(frame.getAddDelay())),
|
|
1215
|
+
writeInt16(frame.getRectX()),
|
|
1216
|
+
writeInt16(frame.getRectY()),
|
|
1217
|
+
writeInt16(frame.getRectWidth()),
|
|
1218
|
+
writeInt16(frame.getRectHeight()),
|
|
1219
|
+
writeInt16(textures[index]?.byteLength === 0 ? -1 : index),
|
|
1220
|
+
);
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
chunks.push(writeInt16(textures.length));
|
|
1224
|
+
for (const texture of textures) {
|
|
1225
|
+
chunks.push(writeInt32(texture.byteLength), texture);
|
|
1226
|
+
}
|
|
1227
|
+
|
|
1228
|
+
return concatBytes(chunks);
|
|
1229
|
+
}
|
|
1230
|
+
|
|
1231
|
+
private _sourceFileCandidates(pkg: Package, fileName: string, outputFileName = fileName): string[] {
|
|
1232
|
+
const publishName = pkg.getPublishName() || pkg.getName();
|
|
1233
|
+
return Array.from(new Set([
|
|
1234
|
+
`${publishName}_${fileName}`,
|
|
1235
|
+
fileName,
|
|
1236
|
+
`${publishName}_${outputFileName}`,
|
|
1237
|
+
outputFileName,
|
|
1238
|
+
]));
|
|
1239
|
+
}
|
|
1240
|
+
|
|
1241
|
+
private async _resolveLooseSourceFile(pkg: Package, sourceDir: string, outputFileName: string): Promise<string | null> {
|
|
1242
|
+
const candidates = outputFileName.endsWith('.atlas')
|
|
1243
|
+
? this._sourceFileCandidates(pkg, `${outputFileName}.txt`, outputFileName)
|
|
1244
|
+
: outputFileName.endsWith('.skel')
|
|
1245
|
+
? this._sourceFileCandidates(pkg, `${outputFileName}.bytes`, outputFileName)
|
|
1246
|
+
: this._sourceFileCandidates(pkg, outputFileName);
|
|
1247
|
+
return this._resolveSourceFile(sourceDir, candidates);
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
private async _resolveSourceFile(sourceDir: string, candidates: string[]): Promise<string | null> {
|
|
1251
|
+
for (const candidate of candidates) {
|
|
1252
|
+
const sourcePath = this._fs.join(sourceDir, candidate);
|
|
1253
|
+
if (await this._fs.isFile(sourcePath)) return sourcePath;
|
|
1254
|
+
}
|
|
1255
|
+
return null;
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
private _resourceOutputPath(
|
|
1259
|
+
outputProjectPath: string,
|
|
1260
|
+
pkg: Package,
|
|
1261
|
+
resource: RestorableResource,
|
|
1262
|
+
fileName: string,
|
|
1263
|
+
): string {
|
|
1264
|
+
const basePath = this._fs.dirname(outputProjectPath);
|
|
1265
|
+
const branch = resource.getBranch?.() ?? '';
|
|
1266
|
+
const assetsDir = branch ? `assets_${branch}` : 'assets';
|
|
1267
|
+
const virtualPath = normalizeVirtualPath(resource.getPath?.());
|
|
1268
|
+
const pkgDir = this._fs.join(basePath, assetsDir, pkg.getName());
|
|
1269
|
+
return virtualPath
|
|
1270
|
+
? this._fs.join(pkgDir, virtualPath, fileName)
|
|
1271
|
+
: this._fs.join(pkgDir, fileName);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
private async _mkdirForFile(filePath: string): Promise<void> {
|
|
1275
|
+
await this._fs.mkdir(this._fs.dirname(filePath));
|
|
1276
|
+
}
|
|
1277
|
+
}
|