@bscotch/gml-parser 1.14.2
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/LICENSE.md +29 -0
- package/README.md +151 -0
- package/assets/GmlSpec.xml +11419 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/index.js.map +1 -0
- package/dist/jsdoc.d.ts +79 -0
- package/dist/jsdoc.d.ts.map +1 -0
- package/dist/jsdoc.feather.d.ts +23 -0
- package/dist/jsdoc.feather.d.ts.map +1 -0
- package/dist/jsdoc.feather.js +143 -0
- package/dist/jsdoc.feather.js.map +1 -0
- package/dist/jsdoc.js +468 -0
- package/dist/jsdoc.js.map +1 -0
- package/dist/jsdoc.test.d.ts +2 -0
- package/dist/jsdoc.test.d.ts.map +1 -0
- package/dist/jsdoc.test.js +185 -0
- package/dist/jsdoc.test.js.map +1 -0
- package/dist/lexer.d.ts +3 -0
- package/dist/lexer.d.ts.map +1 -0
- package/dist/lexer.js +14 -0
- package/dist/lexer.js.map +1 -0
- package/dist/lexer.test.d.ts +2 -0
- package/dist/lexer.test.d.ts.map +1 -0
- package/dist/lexer.test.js +78 -0
- package/dist/lexer.test.js.map +1 -0
- package/dist/lib.objects.d.ts +190 -0
- package/dist/lib.objects.d.ts.map +1 -0
- package/dist/lib.objects.js +242 -0
- package/dist/lib.objects.js.map +1 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +14 -0
- package/dist/logger.js.map +1 -0
- package/dist/modules.d.ts +19 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +320 -0
- package/dist/modules.js.map +1 -0
- package/dist/modules.test.d.ts +2 -0
- package/dist/modules.test.d.ts.map +1 -0
- package/dist/modules.test.js +57 -0
- package/dist/modules.test.js.map +1 -0
- package/dist/modules.types.d.ts +78 -0
- package/dist/modules.types.d.ts.map +1 -0
- package/dist/modules.types.js +2 -0
- package/dist/modules.types.js.map +1 -0
- package/dist/modules.util.d.ts +5 -0
- package/dist/modules.util.d.ts.map +1 -0
- package/dist/modules.util.js +13 -0
- package/dist/modules.util.js.map +1 -0
- package/dist/parser.d.ts +121 -0
- package/dist/parser.d.ts.map +1 -0
- package/dist/parser.js +571 -0
- package/dist/parser.js.map +1 -0
- package/dist/parser.test.d.ts +2 -0
- package/dist/parser.test.d.ts.map +1 -0
- package/dist/parser.test.js +143 -0
- package/dist/parser.test.js.map +1 -0
- package/dist/parser.utility.d.ts +29 -0
- package/dist/parser.utility.d.ts.map +1 -0
- package/dist/parser.utility.js +125 -0
- package/dist/parser.utility.js.map +1 -0
- package/dist/project.asset.d.ts +115 -0
- package/dist/project.asset.d.ts.map +1 -0
- package/dist/project.asset.js +802 -0
- package/dist/project.asset.js.map +1 -0
- package/dist/project.code.d.ts +130 -0
- package/dist/project.code.d.ts.map +1 -0
- package/dist/project.code.js +570 -0
- package/dist/project.code.js.map +1 -0
- package/dist/project.d.ts +533 -0
- package/dist/project.d.ts.map +1 -0
- package/dist/project.diagnostics.d.ts +32 -0
- package/dist/project.diagnostics.d.ts.map +1 -0
- package/dist/project.diagnostics.js +23 -0
- package/dist/project.diagnostics.js.map +1 -0
- package/dist/project.js +1216 -0
- package/dist/project.js.map +1 -0
- package/dist/project.location.d.ts +133 -0
- package/dist/project.location.d.ts.map +1 -0
- package/dist/project.location.js +219 -0
- package/dist/project.location.js.map +1 -0
- package/dist/project.native.d.ts +26 -0
- package/dist/project.native.d.ts.map +1 -0
- package/dist/project.native.js +321 -0
- package/dist/project.native.js.map +1 -0
- package/dist/project.spec.d.ts +1298 -0
- package/dist/project.spec.d.ts.map +1 -0
- package/dist/project.spec.js +263 -0
- package/dist/project.spec.js.map +1 -0
- package/dist/project.test.d.ts +2 -0
- package/dist/project.test.d.ts.map +1 -0
- package/dist/project.test.js +633 -0
- package/dist/project.test.js.map +1 -0
- package/dist/shaderDefaults.d.ts +3 -0
- package/dist/shaderDefaults.d.ts.map +1 -0
- package/dist/shaderDefaults.js +32 -0
- package/dist/shaderDefaults.js.map +1 -0
- package/dist/signifiers.d.ts +54 -0
- package/dist/signifiers.d.ts.map +1 -0
- package/dist/signifiers.flags.d.ts +38 -0
- package/dist/signifiers.flags.d.ts.map +1 -0
- package/dist/signifiers.flags.js +131 -0
- package/dist/signifiers.flags.js.map +1 -0
- package/dist/signifiers.js +117 -0
- package/dist/signifiers.js.map +1 -0
- package/dist/spine.d.ts +28 -0
- package/dist/spine.d.ts.map +1 -0
- package/dist/spine.js +64 -0
- package/dist/spine.js.map +1 -0
- package/dist/spine.test.d.ts +2 -0
- package/dist/spine.test.d.ts.map +1 -0
- package/dist/spine.test.js +420 -0
- package/dist/spine.test.js.map +1 -0
- package/dist/spine.types.d.ts +89 -0
- package/dist/spine.types.d.ts.map +1 -0
- package/dist/spine.types.js +2 -0
- package/dist/spine.types.js.map +1 -0
- package/dist/test.lib.d.ts +3 -0
- package/dist/test.lib.d.ts.map +1 -0
- package/dist/test.lib.js +16 -0
- package/dist/test.lib.js.map +1 -0
- package/dist/tokens.categories.d.ts +22 -0
- package/dist/tokens.categories.d.ts.map +1 -0
- package/dist/tokens.categories.js +78 -0
- package/dist/tokens.categories.js.map +1 -0
- package/dist/tokens.code.d.ts +2 -0
- package/dist/tokens.code.d.ts.map +1 -0
- package/dist/tokens.code.js +523 -0
- package/dist/tokens.code.js.map +1 -0
- package/dist/tokens.d.ts +130 -0
- package/dist/tokens.d.ts.map +1 -0
- package/dist/tokens.js +13 -0
- package/dist/tokens.js.map +1 -0
- package/dist/tokens.lib.d.ts +15 -0
- package/dist/tokens.lib.d.ts.map +1 -0
- package/dist/tokens.lib.js +12 -0
- package/dist/tokens.lib.js.map +1 -0
- package/dist/tokens.shared.d.ts +4 -0
- package/dist/tokens.shared.d.ts.map +1 -0
- package/dist/tokens.shared.js +35 -0
- package/dist/tokens.shared.js.map +1 -0
- package/dist/tokens.strings.d.ts +5 -0
- package/dist/tokens.strings.d.ts.map +1 -0
- package/dist/tokens.strings.js +111 -0
- package/dist/tokens.strings.js.map +1 -0
- package/dist/types.checks.d.ts +50 -0
- package/dist/types.checks.d.ts.map +1 -0
- package/dist/types.checks.js +246 -0
- package/dist/types.checks.js.map +1 -0
- package/dist/types.d.ts +152 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.feather.d.ts +21 -0
- package/dist/types.feather.d.ts.map +1 -0
- package/dist/types.feather.js +156 -0
- package/dist/types.feather.js.map +1 -0
- package/dist/types.hover.d.ts +4 -0
- package/dist/types.hover.d.ts.map +1 -0
- package/dist/types.hover.js +99 -0
- package/dist/types.hover.js.map +1 -0
- package/dist/types.js +457 -0
- package/dist/types.js.map +1 -0
- package/dist/types.primitives.d.ts +10 -0
- package/dist/types.primitives.d.ts.map +1 -0
- package/dist/types.primitives.js +88 -0
- package/dist/types.primitives.js.map +1 -0
- package/dist/types.sprites.d.ts +8 -0
- package/dist/types.sprites.d.ts.map +1 -0
- package/dist/types.sprites.js +417 -0
- package/dist/types.sprites.js.map +1 -0
- package/dist/types.test.d.ts +2 -0
- package/dist/types.test.d.ts.map +1 -0
- package/dist/types.test.js +62 -0
- package/dist/types.test.js.map +1 -0
- package/dist/util.d.ts +50 -0
- package/dist/util.d.ts.map +1 -0
- package/dist/util.js +168 -0
- package/dist/util.js.map +1 -0
- package/dist/util.test.d.ts +3 -0
- package/dist/util.test.d.ts.map +1 -0
- package/dist/util.test.js +63 -0
- package/dist/util.test.js.map +1 -0
- package/dist/visitor.assign.d.ts +24 -0
- package/dist/visitor.assign.d.ts.map +1 -0
- package/dist/visitor.assign.js +112 -0
- package/dist/visitor.assign.js.map +1 -0
- package/dist/visitor.d.ts +89 -0
- package/dist/visitor.d.ts.map +1 -0
- package/dist/visitor.functionExpression.d.ts +7 -0
- package/dist/visitor.functionExpression.d.ts.map +1 -0
- package/dist/visitor.functionExpression.js +216 -0
- package/dist/visitor.functionExpression.js.map +1 -0
- package/dist/visitor.globals.d.ts +59 -0
- package/dist/visitor.globals.d.ts.map +1 -0
- package/dist/visitor.globals.js +271 -0
- package/dist/visitor.globals.js.map +1 -0
- package/dist/visitor.identifierAccessor.d.ts +6 -0
- package/dist/visitor.identifierAccessor.d.ts.map +1 -0
- package/dist/visitor.identifierAccessor.js +381 -0
- package/dist/visitor.identifierAccessor.js.map +1 -0
- package/dist/visitor.js +605 -0
- package/dist/visitor.js.map +1 -0
- package/dist/visitor.processor.d.ts +66 -0
- package/dist/visitor.processor.d.ts.map +1 -0
- package/dist/visitor.processor.js +147 -0
- package/dist/visitor.processor.js.map +1 -0
- package/package.json +63 -0
package/dist/project.js
ADDED
|
@@ -0,0 +1,1216 @@
|
|
|
1
|
+
import { __decorate, __metadata } from "tslib";
|
|
2
|
+
import { pathy } from '@bscotch/pathy';
|
|
3
|
+
import { getDefaultsForNewSound, isValidSoundName, isValidSpriteName, stitchConfigFilename, stitchConfigSchema, } from '@bscotch/stitch-config';
|
|
4
|
+
import { sequential } from '@bscotch/utility';
|
|
5
|
+
import { SoundChannel, SpriteType, Yy, yypFolderSchema, yyRoomSchema, yySpriteSchema, } from '@bscotch/yy';
|
|
6
|
+
import { EventEmitter } from 'events';
|
|
7
|
+
import { logger } from './logger.js';
|
|
8
|
+
import { importAssets } from './modules.js';
|
|
9
|
+
import { Asset, isAssetOfKind } from './project.asset.js';
|
|
10
|
+
import { Code } from './project.code.js';
|
|
11
|
+
import { Native } from './project.native.js';
|
|
12
|
+
import { fshDefault, vshDefault } from './shaderDefaults.js';
|
|
13
|
+
import { Signifier } from './signifiers.js';
|
|
14
|
+
import { Type } from './types.js';
|
|
15
|
+
import { assert, assertIsValidIdentifier, getPngSize, groupPathToPosix, ok, throwError, } from './util.js';
|
|
16
|
+
export { setLogger } from './logger.js';
|
|
17
|
+
export class Project {
|
|
18
|
+
yypPath;
|
|
19
|
+
options;
|
|
20
|
+
yyp;
|
|
21
|
+
/** Until this resolves, assume that this.yyp is not yet read */
|
|
22
|
+
yypWaiter;
|
|
23
|
+
config;
|
|
24
|
+
assets = new Map();
|
|
25
|
+
/**
|
|
26
|
+
* Store the "native" functions, constants, and enums on
|
|
27
|
+
* a per-project basis, but separately from the project-specific
|
|
28
|
+
* symbols. The native symbols and types are loaded from the spec,
|
|
29
|
+
* so they can vary between projects. */
|
|
30
|
+
native;
|
|
31
|
+
helpLinks;
|
|
32
|
+
/**
|
|
33
|
+
* When resolved, the GML spec has been loaded and the
|
|
34
|
+
* `native` property has been populated.
|
|
35
|
+
*/
|
|
36
|
+
nativeWaiter;
|
|
37
|
+
/**
|
|
38
|
+
* The type of the 'global' struct, which contains all globalvars
|
|
39
|
+
* and globally defined functions. */
|
|
40
|
+
self;
|
|
41
|
+
/**
|
|
42
|
+
* The `global` symbol, which has type `self`. */
|
|
43
|
+
symbol;
|
|
44
|
+
/**
|
|
45
|
+
* Non-native global types, which can be referenced in JSDocs
|
|
46
|
+
* and in a symbol's types. */
|
|
47
|
+
types = new Map();
|
|
48
|
+
emitter = new EventEmitter();
|
|
49
|
+
/** Code that needs to be reprocessed, for one reason or another. */
|
|
50
|
+
dirtyFiles = new Set();
|
|
51
|
+
constructor(yypPath, options) {
|
|
52
|
+
this.yypPath = yypPath;
|
|
53
|
+
this.options = options;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @internal For tracking changed code files that will need to be re-parsed.
|
|
57
|
+
*/
|
|
58
|
+
queueDirtyFileUpdate(code) {
|
|
59
|
+
this.dirtyFiles.add(code);
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* @internal Drain the queue of dirty files, updating their diagnostics
|
|
63
|
+
*/
|
|
64
|
+
drainDirtyFileUpdateQueue() {
|
|
65
|
+
for (const code of this.dirtyFiles) {
|
|
66
|
+
code.updateDiagnostics();
|
|
67
|
+
// await code.reload(code.content);
|
|
68
|
+
}
|
|
69
|
+
this.dirtyFiles.clear();
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Update the YYP file to list a specific GameMaker IDE version.
|
|
73
|
+
* Note that the GameMaker IDE will overwrite this with whatever
|
|
74
|
+
* its own version is -- this feature is useful for external tools
|
|
75
|
+
* like Stitch that can manage multiple GameMaker IDE versions.
|
|
76
|
+
*/
|
|
77
|
+
async setIdeVersion(version) {
|
|
78
|
+
assert(version.match(/^\d+\.\d+\.\d+\.\d+$/), 'Invalid version string');
|
|
79
|
+
this.yyp.MetaData.IDEVersion = version;
|
|
80
|
+
await this.saveYyp();
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* The current version of the GameMaker IDE listed in
|
|
84
|
+
* this project's YYP file. This is the GameMaker version that
|
|
85
|
+
* the project was last opened with.
|
|
86
|
+
*/
|
|
87
|
+
get ideVersion() {
|
|
88
|
+
return this.yyp.MetaData.IDEVersion;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The directory in which the project lives.
|
|
92
|
+
*/
|
|
93
|
+
get dir() {
|
|
94
|
+
return pathy(this.yypPath).up();
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Get the Stitch config for this project, which defines
|
|
98
|
+
* various settings that may impact rule around adding
|
|
99
|
+
* assets, parsing logs, etc.
|
|
100
|
+
*/
|
|
101
|
+
get stitchConfig() {
|
|
102
|
+
return this.dir
|
|
103
|
+
.join(stitchConfigFilename)
|
|
104
|
+
.withValidator(stitchConfigSchema);
|
|
105
|
+
}
|
|
106
|
+
/** List the names of the GameMaker configs defined by this project. */
|
|
107
|
+
get configs() {
|
|
108
|
+
const configs = [];
|
|
109
|
+
let configTree = [this.yyp.configs];
|
|
110
|
+
while (configTree.length) {
|
|
111
|
+
const nextTree = [];
|
|
112
|
+
for (const config of configTree) {
|
|
113
|
+
configs.push(config.name);
|
|
114
|
+
nextTree.push(...(config.children || []));
|
|
115
|
+
}
|
|
116
|
+
configTree = nextTree;
|
|
117
|
+
}
|
|
118
|
+
return configs;
|
|
119
|
+
}
|
|
120
|
+
/** List the project's "datafiles" (a.k.a. "Included Files"), in the same format as they appear in the YYP file. */
|
|
121
|
+
get datafiles() {
|
|
122
|
+
return this.yyp.IncludedFiles;
|
|
123
|
+
}
|
|
124
|
+
/** List the Folders in this project, normalized to regular POSIX paths
|
|
125
|
+
* @example ['my/folder', 'my/other/folder']
|
|
126
|
+
*/
|
|
127
|
+
get folders() {
|
|
128
|
+
return this.yyp.Folders.map((f) => groupPathToPosix(f.folderPath));
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Run a callback when diagnostics are emitted. Returns an unsubscribe function. */
|
|
132
|
+
onDiagnostics(callback) {
|
|
133
|
+
this.emitter.on('diagnostics', callback);
|
|
134
|
+
return () => this.emitter.off('diagnostics', callback);
|
|
135
|
+
}
|
|
136
|
+
/** @internal Method that can be called after some code has been parsed to report diagnostics to listeners. */
|
|
137
|
+
emitDiagnostics(code, diagnostics) {
|
|
138
|
+
// Ensure they are valid diagnostics
|
|
139
|
+
for (const diagnostic of diagnostics) {
|
|
140
|
+
ok(diagnostic.$tag === 'diagnostic');
|
|
141
|
+
ok(diagnostic.location);
|
|
142
|
+
}
|
|
143
|
+
this.emitter.emit('diagnostics', {
|
|
144
|
+
code: code instanceof Code ? code : undefined,
|
|
145
|
+
filePath: code instanceof Code ? code.path.absolute : code,
|
|
146
|
+
diagnostics,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* Since GameMaker assets are global they must have unique names independent of their type. Find an asset give it's name. Note that this is case-insensitive!
|
|
151
|
+
* @param name The name of the asset to find, case-insensitive.
|
|
152
|
+
*/
|
|
153
|
+
getAssetByName(name, options) {
|
|
154
|
+
assert(name || !options?.assertExists, 'No asset name provided');
|
|
155
|
+
if (!name) {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
const asset = this.assets.get(name.toLocaleLowerCase());
|
|
159
|
+
assert(asset || !options?.assertExists, `Asset "${name}" does not exist.`);
|
|
160
|
+
return asset;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* @param name The name of the asset to find and remove, case-insensitive.
|
|
164
|
+
*/
|
|
165
|
+
async removeAssetByName(name) {
|
|
166
|
+
if (!name)
|
|
167
|
+
return;
|
|
168
|
+
name = name.toLocaleLowerCase();
|
|
169
|
+
const asset = this.assets.get(name);
|
|
170
|
+
if (!asset)
|
|
171
|
+
return;
|
|
172
|
+
// Remove the asset from the yyp
|
|
173
|
+
const resourceIdx = this.yyp.resources.findIndex((r) => r.id.name.toLocaleLowerCase() === name);
|
|
174
|
+
// If it's a room, remove it from the room order list
|
|
175
|
+
if (isAssetOfKind(asset, 'rooms')) {
|
|
176
|
+
this.yyp.RoomOrderNodes = this.yyp.RoomOrderNodes.filter((node) => {
|
|
177
|
+
node.roomId.path.toLowerCase() !== asset.resource.id.path.toLowerCase();
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
// If it'll be referenced in other assets, remove those references
|
|
181
|
+
else if (isAssetOfKind(asset, 'objects') ||
|
|
182
|
+
isAssetOfKind(asset, 'sprites')) {
|
|
183
|
+
for (const other of this.assets.values()) {
|
|
184
|
+
if (isAssetOfKind(asset, 'sprites') &&
|
|
185
|
+
isAssetOfKind(other, 'objects')) {
|
|
186
|
+
// If this object has this sprite, unset it!
|
|
187
|
+
if (other.sprite?.name === asset.name) {
|
|
188
|
+
other.sprite = undefined;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
else if (isAssetOfKind(asset, 'objects') &&
|
|
192
|
+
isAssetOfKind(other, 'objects')) {
|
|
193
|
+
// Then this object might be referenced in a collision event
|
|
194
|
+
// with the other object.
|
|
195
|
+
const yy = other.yy;
|
|
196
|
+
const [keepEvents, removeEvents] = yy.eventList.reduce((acc, event) => {
|
|
197
|
+
if (event.collisionObjectId?.name === asset.name) {
|
|
198
|
+
acc[1].push(event);
|
|
199
|
+
}
|
|
200
|
+
else {
|
|
201
|
+
acc[0].push(event);
|
|
202
|
+
}
|
|
203
|
+
return acc;
|
|
204
|
+
}, [[], []]);
|
|
205
|
+
if (removeEvents.length) {
|
|
206
|
+
// Remove the collision events
|
|
207
|
+
other.yy.eventList = keepEvents;
|
|
208
|
+
await other.saveYy();
|
|
209
|
+
// Remove the leftover collision event file
|
|
210
|
+
const eventFile = other.dir.join(`Collision_${asset.name}.gml`);
|
|
211
|
+
await eventFile.delete({ recursive: true });
|
|
212
|
+
await other.reload();
|
|
213
|
+
}
|
|
214
|
+
// It could also be listed as the parent
|
|
215
|
+
if (other.parent?.name === asset.name) {
|
|
216
|
+
other.parent = undefined;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
this.assets.delete(name);
|
|
222
|
+
if (resourceIdx > -1) {
|
|
223
|
+
this.yyp.resources.splice(resourceIdx, 1);
|
|
224
|
+
await this.saveYyp();
|
|
225
|
+
}
|
|
226
|
+
// Clean up
|
|
227
|
+
await asset.onRemove();
|
|
228
|
+
}
|
|
229
|
+
getAsset(path) {
|
|
230
|
+
return this.assets.get(this.assetNameFromPath(path));
|
|
231
|
+
}
|
|
232
|
+
getGmlFile(path) {
|
|
233
|
+
const resource = this.getAsset(path);
|
|
234
|
+
if (!resource) {
|
|
235
|
+
return;
|
|
236
|
+
}
|
|
237
|
+
return resource.getGmlFile(path);
|
|
238
|
+
}
|
|
239
|
+
/** Normalize path information for a datafile ("Included File") */
|
|
240
|
+
parseIncludedFilePath(filePath, name) {
|
|
241
|
+
filePath.replace(/[/\\]+$/, '/').replace(/\/$/, '');
|
|
242
|
+
if (!name) {
|
|
243
|
+
({ folder: filePath, name } =
|
|
244
|
+
filePath.match(/^(?<folder>.*)[/\\](?<name>[^/\\]+)$/)?.groups || {});
|
|
245
|
+
}
|
|
246
|
+
assert(filePath, `Invalid folder: ${filePath}`);
|
|
247
|
+
assert(name, `Invalid name: ${name}`);
|
|
248
|
+
assert(filePath === 'datafiles' || filePath.startsWith('datafiles/'), `Folder must be in datafiles: ${filePath}`);
|
|
249
|
+
return { filePath, name };
|
|
250
|
+
}
|
|
251
|
+
findIncludedFile(filePath, name) {
|
|
252
|
+
({ filePath, name } = this.parseIncludedFilePath(filePath, name));
|
|
253
|
+
return this.datafiles.find((f) => f.name.toLowerCase() === name.toLowerCase() &&
|
|
254
|
+
f.filePath.toLowerCase() === filePath.toLowerCase());
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Ensure that the included files listed in the YYP exactly match
|
|
258
|
+
* the files in the `datafiles` directory.
|
|
259
|
+
*/
|
|
260
|
+
async syncIncludedFiles() {
|
|
261
|
+
const includedFiles = (await this.dir.join('datafiles').listChildrenRecursively()).map((f) => {
|
|
262
|
+
/** The filepath relative to the project dir (starts with 'datafiles') */
|
|
263
|
+
const fullPath = f.relativeFrom(this.dir);
|
|
264
|
+
// Will throw with unexpected paths, preventing anything from being
|
|
265
|
+
// overwritten. This is a better outcome than skipping those files.
|
|
266
|
+
const { filePath, name } = this.parseIncludedFilePath(fullPath);
|
|
267
|
+
const existing = this.findIncludedFile(filePath, name);
|
|
268
|
+
return existing || { filePath, name };
|
|
269
|
+
});
|
|
270
|
+
// Note: Should check if there have been any changes, and only write if not!
|
|
271
|
+
// No need to compare with what's already in there, just overwrite it!
|
|
272
|
+
// GameMaker seems to sort these by full path, so we'll do the same to
|
|
273
|
+
// prevent git noise.
|
|
274
|
+
// @ts-expect-error The schema will ensure it's written correctly
|
|
275
|
+
this.yyp.IncludedFiles = includedFiles;
|
|
276
|
+
await this.saveYyp();
|
|
277
|
+
}
|
|
278
|
+
/** @internal Load an Asset instance into the project's data model. For use by methods that load the project, add assets, etc. */
|
|
279
|
+
registerAsset(resource) {
|
|
280
|
+
const name = this.assetNameFromPath(resource.dir);
|
|
281
|
+
ok(!this.assets.has(name), `Resource ${name} already exists`);
|
|
282
|
+
this.assets.set(name, resource);
|
|
283
|
+
}
|
|
284
|
+
/**
|
|
285
|
+
* @param from The name of the asset to rename, case-insensitive.
|
|
286
|
+
* @param to The new name for the asset, which must be a valid identifier that doesn't already have an associated asset. The name will be set in the provided casing, but must be unique case-insensitively.
|
|
287
|
+
*/
|
|
288
|
+
async renameAsset(from, to) {
|
|
289
|
+
const asset = this.getAssetByName(from, { assertExists: true });
|
|
290
|
+
assertIsValidIdentifier(to);
|
|
291
|
+
const toAsset = this.getAssetByName(to);
|
|
292
|
+
assert(!toAsset, `Cannot rename. An asset named "${to}" already exists`);
|
|
293
|
+
// Create a new asset with the new name, copying over the old asset's files and updating them as needed
|
|
294
|
+
const newAssetDir = asset.dir.up().join(to);
|
|
295
|
+
const reset = async () => await newAssetDir.delete({ force: true, recursive: true });
|
|
296
|
+
await newAssetDir.ensureDirectory();
|
|
297
|
+
await newAssetDir.isEmptyDirectory({ assert: true });
|
|
298
|
+
await asset.dir.copy(newAssetDir);
|
|
299
|
+
// The yy files contain the old 'name' field, and there may be
|
|
300
|
+
// other files named after the old asset name.
|
|
301
|
+
// Rename all copied files that have the old asset name in them
|
|
302
|
+
const oldNamePattern = new RegExp(`\\b${from}\\b`, 'gi');
|
|
303
|
+
let newYyFile;
|
|
304
|
+
await newAssetDir.listChildrenRecursively({
|
|
305
|
+
filter: async (p) => {
|
|
306
|
+
if (await p.isDirectory())
|
|
307
|
+
return;
|
|
308
|
+
// Get the relative path
|
|
309
|
+
const relative = p.relativeFrom(newAssetDir);
|
|
310
|
+
const newRelative = relative.replaceAll(oldNamePattern, to);
|
|
311
|
+
if (newRelative === relative)
|
|
312
|
+
return;
|
|
313
|
+
// Rename!
|
|
314
|
+
const newFile = newAssetDir.join(newRelative);
|
|
315
|
+
if (newRelative === `${to}.yy`) {
|
|
316
|
+
newYyFile = newFile;
|
|
317
|
+
}
|
|
318
|
+
await p.copy(newFile);
|
|
319
|
+
await p.delete();
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
if (!newYyFile) {
|
|
323
|
+
await reset();
|
|
324
|
+
throwError(`Could not find yy after copying files`);
|
|
325
|
+
}
|
|
326
|
+
// Update the "name" field
|
|
327
|
+
const yy = await Yy.read(newYyFile.absolute, asset.assetKind);
|
|
328
|
+
yy.name = to;
|
|
329
|
+
if (isAssetOfKind(asset, 'sounds')) {
|
|
330
|
+
// Then we've renamed the sound file and need to update that in the yy!
|
|
331
|
+
const yySound = yy;
|
|
332
|
+
yySound.soundFile = yySound.soundFile.replace(oldNamePattern, to);
|
|
333
|
+
}
|
|
334
|
+
else if (isAssetOfKind(asset, 'sprites')) {
|
|
335
|
+
const yySprite = yy;
|
|
336
|
+
// Update the sequence track references.
|
|
337
|
+
// They're in an absurd, deeply nested structure that changes
|
|
338
|
+
// periodically. So the easiest thing is to stringify it, replace all,
|
|
339
|
+
// and reparse it.
|
|
340
|
+
if (yySprite.sequence?.tracks?.length) {
|
|
341
|
+
for (let i = 0; i < yySprite.sequence.tracks.length; i++) {
|
|
342
|
+
const track = yySprite.sequence.tracks[i];
|
|
343
|
+
let stringified = Yy.stringify(track);
|
|
344
|
+
stringified = stringified.replaceAll(oldNamePattern, to);
|
|
345
|
+
yySprite.sequence.tracks[i] = Yy.parse(stringified);
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
await Yy.write(newYyFile.absolute, yy, asset.assetKind, this.yyp);
|
|
350
|
+
// Register the new asset
|
|
351
|
+
const info = await this.addAssetToYyp(newYyFile.absolute);
|
|
352
|
+
const newAsset = await Asset.from(this, info);
|
|
353
|
+
assert(newAsset, `Could not create new asset ${to}`);
|
|
354
|
+
this.registerAsset(newAsset);
|
|
355
|
+
if (isAssetOfKind(newAsset, 'sprites')) {
|
|
356
|
+
// Then find any object that had its sprite set to the old one,
|
|
357
|
+
// and set it to the new one
|
|
358
|
+
for (const obj of this.assets.values()) {
|
|
359
|
+
if (!isAssetOfKind(obj, 'objects'))
|
|
360
|
+
continue;
|
|
361
|
+
if (!obj.sprite)
|
|
362
|
+
continue;
|
|
363
|
+
console.log('Checking old sprite name', obj.sprite.name, asset.name, newAsset.name);
|
|
364
|
+
if (obj.sprite?.name === asset.name) {
|
|
365
|
+
console.log('UPDATING SPRITE');
|
|
366
|
+
obj.sprite = newAsset;
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
// Remove the old asset
|
|
371
|
+
await this.removeAssetByName(from);
|
|
372
|
+
// Fully process the change
|
|
373
|
+
await this.initiallyParseAssetCode([newAsset]);
|
|
374
|
+
// Update the code from all refs to have the new name
|
|
375
|
+
await this.renameSignifier(asset.signifier, to);
|
|
376
|
+
if (isAssetOfKind(newAsset, 'objects')) {
|
|
377
|
+
// Update immediate children to have the new asset as the parent
|
|
378
|
+
for (const child of asset.children) {
|
|
379
|
+
child.parent = newAsset;
|
|
380
|
+
}
|
|
381
|
+
// Update any rooms that reference the old object name
|
|
382
|
+
for (const room of this.assets.values()) {
|
|
383
|
+
if (!isAssetOfKind(room, 'rooms'))
|
|
384
|
+
continue;
|
|
385
|
+
await room.renameRoomInstanceObjects(from, to);
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
async renameSignifier(signifier, newName) {
|
|
390
|
+
assertIsValidIdentifier(newName);
|
|
391
|
+
// Rename the signifier
|
|
392
|
+
const files = new Set();
|
|
393
|
+
signifier.refs.forEach((ref) => files.add(ref.start.file));
|
|
394
|
+
const waits = [];
|
|
395
|
+
for (const file of files) {
|
|
396
|
+
waits.push(file.renameSignifier(signifier, newName));
|
|
397
|
+
}
|
|
398
|
+
await Promise.all(waits);
|
|
399
|
+
}
|
|
400
|
+
async import(fromProject, options = {}) {
|
|
401
|
+
if (typeof fromProject === 'string') {
|
|
402
|
+
fromProject = await Project.initialize(fromProject);
|
|
403
|
+
}
|
|
404
|
+
return await importAssets(fromProject, this, options);
|
|
405
|
+
}
|
|
406
|
+
async duplicateAsset(sourceName, newPath) {
|
|
407
|
+
const source = this.getAssetByName(sourceName, { assertExists: true });
|
|
408
|
+
const parsed = await this.parseNewAssetPath(newPath);
|
|
409
|
+
assert(parsed, `Invalid new asset path: ${newPath}`);
|
|
410
|
+
// Copy all files in the source's directory to a new directory named
|
|
411
|
+
// after the new name.
|
|
412
|
+
const kind = source.assetKind;
|
|
413
|
+
const cloneDir = this.dir.join(`${kind}/${parsed.name}`);
|
|
414
|
+
await cloneDir.ensureDirectory();
|
|
415
|
+
await source.dir.copy(cloneDir);
|
|
416
|
+
// Rename any files named after the original asset
|
|
417
|
+
const oldNamePattern = new RegExp(`\\b${sourceName}\\b`, 'gi');
|
|
418
|
+
let yyFile;
|
|
419
|
+
await cloneDir.listChildrenRecursively({
|
|
420
|
+
filter: async (p) => {
|
|
421
|
+
if (await p.isDirectory())
|
|
422
|
+
return;
|
|
423
|
+
// Get the relative path
|
|
424
|
+
const relative = p.relativeFrom(cloneDir);
|
|
425
|
+
const newRelative = relative.replaceAll(oldNamePattern, parsed.name);
|
|
426
|
+
if (newRelative === relative)
|
|
427
|
+
return;
|
|
428
|
+
// Rename!
|
|
429
|
+
const newFile = cloneDir.join(newRelative);
|
|
430
|
+
await p.copy(newFile);
|
|
431
|
+
await p.delete();
|
|
432
|
+
if (newFile.hasExtension('yy') &&
|
|
433
|
+
newFile.name.toLowerCase() === parsed.name.toLowerCase()) {
|
|
434
|
+
yyFile = newFile;
|
|
435
|
+
}
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
assert(yyFile, `Could not find yy file for new asset ${parsed.name}`);
|
|
439
|
+
// Update the yy files to replace the old name with the new
|
|
440
|
+
// Just read them as text so we don't have to deal with parsing
|
|
441
|
+
const content = await yyFile.read({ encoding: 'utf8' });
|
|
442
|
+
const newContent = content.replaceAll(new RegExp(`"${sourceName}"`, 'gi'), `"${parsed.name}"`);
|
|
443
|
+
await yyFile.write(newContent);
|
|
444
|
+
// Add the new asset to the yyp file
|
|
445
|
+
const info = await this.addAssetToYyp(yyFile.absolute);
|
|
446
|
+
const newAsset = await Asset.from(this, info);
|
|
447
|
+
assert(newAsset, `Could not create new asset ${parsed.name}`);
|
|
448
|
+
this.registerAsset(newAsset);
|
|
449
|
+
return newAsset;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* Create a new sound asset. Will not do anything if the asset by this name already exists (but will log an error).
|
|
453
|
+
* @param newSoundPath The POSIX-style path within the asset tree where you want this sound to be created, where the last component is the name of the sound asset.
|
|
454
|
+
* @param fromFile The path to the source sound file to copy into the new asset's directory.
|
|
455
|
+
* @example project.createSound('folder/of/sounds/snd_my_new_sound', 'path/to/sound.mp3');
|
|
456
|
+
*/
|
|
457
|
+
async createSound(newSoundPath, fromFile) {
|
|
458
|
+
// Create the yy file
|
|
459
|
+
const parsed = await this.parseNewAssetPath(newSoundPath);
|
|
460
|
+
if (!parsed) {
|
|
461
|
+
return;
|
|
462
|
+
}
|
|
463
|
+
const { name, folder } = parsed;
|
|
464
|
+
assert(isValidSoundName(name, this.config), `Sound name '${name}' does not match allowed patterns`);
|
|
465
|
+
const defaults = getDefaultsForNewSound(name, this.config);
|
|
466
|
+
const soundDir = this.dir.join(`sounds/${name}`);
|
|
467
|
+
await soundDir.ensureDirectory();
|
|
468
|
+
const soundYy = soundDir.join(`${name}.yy`);
|
|
469
|
+
// Copy the sound file over
|
|
470
|
+
fromFile = pathy(fromFile);
|
|
471
|
+
const soundFileName = `${name}${fromFile.extname}`;
|
|
472
|
+
await fromFile.copy(soundDir.join(soundFileName));
|
|
473
|
+
await Yy.write(soundYy.absolute, {
|
|
474
|
+
name,
|
|
475
|
+
parent: {
|
|
476
|
+
name: folder.name,
|
|
477
|
+
path: folder.folderPath,
|
|
478
|
+
},
|
|
479
|
+
type: defaults?.mono ? SoundChannel.Mono : SoundChannel.Stereo,
|
|
480
|
+
soundFile: soundFileName,
|
|
481
|
+
duration: 0,
|
|
482
|
+
}, 'sounds', this.yyp);
|
|
483
|
+
// Update the yyp file
|
|
484
|
+
const info = await this.addAssetToYyp(soundYy.absolute);
|
|
485
|
+
// Create and add the asset
|
|
486
|
+
const asset = await Asset.from(this, info);
|
|
487
|
+
if (asset) {
|
|
488
|
+
this.registerAsset(asset);
|
|
489
|
+
}
|
|
490
|
+
return asset;
|
|
491
|
+
}
|
|
492
|
+
/**
|
|
493
|
+
* Create a new room asset. Will not do anything if the asset by this name already exists (but will log an error).
|
|
494
|
+
* @param newRoomPath The POSIX-style path within the asset tree where you want this asset to be created, where the last component is the name of the asset.
|
|
495
|
+
* @example project.createRoom('folder/of/rooms/rm_my_room');
|
|
496
|
+
*/
|
|
497
|
+
async createRoom(newRoomPath) {
|
|
498
|
+
const parsed = await this.parseNewAssetPath(newRoomPath);
|
|
499
|
+
if (!parsed) {
|
|
500
|
+
return;
|
|
501
|
+
}
|
|
502
|
+
const { name, folder } = parsed;
|
|
503
|
+
const roomDir = this.dir.join(`rooms/${name}`);
|
|
504
|
+
await roomDir.ensureDirectory();
|
|
505
|
+
const roomYy = roomDir.join(`${name}.yy`);
|
|
506
|
+
const yy = yyRoomSchema.parse({
|
|
507
|
+
name,
|
|
508
|
+
parent: {
|
|
509
|
+
name: folder.name,
|
|
510
|
+
path: folder.folderPath,
|
|
511
|
+
},
|
|
512
|
+
layers: [{ resourceType: 'GMRBackgroundLayer' }],
|
|
513
|
+
views: [...Array(8)].map(() => ({})),
|
|
514
|
+
});
|
|
515
|
+
yy.views[0].visible = true;
|
|
516
|
+
await Yy.write(roomYy.absolute, yy, 'rooms', this.yyp);
|
|
517
|
+
// Update the yyp file
|
|
518
|
+
const info = await this.addAssetToYyp(roomYy.absolute, { skipSave: true });
|
|
519
|
+
this.yyp.RoomOrderNodes.push({ roomId: info.id });
|
|
520
|
+
await this.saveYyp();
|
|
521
|
+
// Create and add the asset
|
|
522
|
+
const asset = await Asset.from(this, info);
|
|
523
|
+
if (asset) {
|
|
524
|
+
this.registerAsset(asset);
|
|
525
|
+
}
|
|
526
|
+
return asset;
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Create a new sprite asset. Will not do anything if the asset by this name already exists (but will log an error).
|
|
530
|
+
* @param newSpritePath The POSIX-style path within the asset tree where you want this asset to be created, where the last component is the name of the asset.
|
|
531
|
+
* @param fromImageFile Path to a source PNG image to use as the first frame of the sprite.
|
|
532
|
+
* @example project.createSprite('folder/of/sprites/sp_my_sprite', 'path/to/sprite.png');
|
|
533
|
+
*/
|
|
534
|
+
async createSprite(newSpritePath, fromImageFile) {
|
|
535
|
+
// Create the yy file
|
|
536
|
+
const parsed = await this.parseNewAssetPath(newSpritePath);
|
|
537
|
+
if (!parsed) {
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
const { name, folder } = parsed;
|
|
541
|
+
assert(isValidSpriteName(name, this.config), `Sprite name '${name}' does not match allowed patterns`);
|
|
542
|
+
const spriteDir = this.dir.join(`sprites/${name}`);
|
|
543
|
+
await spriteDir.ensureDirectory();
|
|
544
|
+
const spriteYy = spriteDir.join(`${name}.yy`);
|
|
545
|
+
// Get the source image dimensions
|
|
546
|
+
fromImageFile = pathy(fromImageFile);
|
|
547
|
+
assert(fromImageFile.hasExtension('png'), `Expected a .png file`);
|
|
548
|
+
const { width, height } = await getPngSize(fromImageFile);
|
|
549
|
+
const xorigin = Math.floor(width / 2) - 1;
|
|
550
|
+
const yorigin = Math.floor(height / 2) - 1;
|
|
551
|
+
const frames = [];
|
|
552
|
+
frames.length = 1;
|
|
553
|
+
const yy = yySpriteSchema.parse({
|
|
554
|
+
name,
|
|
555
|
+
parent: {
|
|
556
|
+
name: folder.name,
|
|
557
|
+
path: folder.folderPath,
|
|
558
|
+
},
|
|
559
|
+
type: SpriteType.Default,
|
|
560
|
+
width,
|
|
561
|
+
height,
|
|
562
|
+
sequence: { xorigin, yorigin },
|
|
563
|
+
frames,
|
|
564
|
+
});
|
|
565
|
+
// Now we'll have a frameId
|
|
566
|
+
const frameId = yy.frames[0].name;
|
|
567
|
+
assert(frameId, `Expected a frameId`);
|
|
568
|
+
await fromImageFile.copy(spriteDir.join(`${frameId}.png`));
|
|
569
|
+
await Yy.write(spriteYy.absolute, yy, 'sprites', this.yyp);
|
|
570
|
+
// Update the yyp file
|
|
571
|
+
const info = await this.addAssetToYyp(spriteYy.absolute);
|
|
572
|
+
// Create and add the asset
|
|
573
|
+
const asset = await Asset.from(this, info);
|
|
574
|
+
if (asset) {
|
|
575
|
+
this.registerAsset(asset);
|
|
576
|
+
}
|
|
577
|
+
return asset;
|
|
578
|
+
}
|
|
579
|
+
/**
|
|
580
|
+
* Add an object to the yyp file. The string can include separators,
|
|
581
|
+
* in which case folders will be ensured up to the final component.
|
|
582
|
+
* @param newObjectName The POSIX-style path within the asset tree where you want this asset to be created, where the last component is the name of the asset.
|
|
583
|
+
*/
|
|
584
|
+
async createObject(newObjectName) {
|
|
585
|
+
// Create the yy file
|
|
586
|
+
const parsed = await this.parseNewAssetPath(newObjectName);
|
|
587
|
+
if (!parsed) {
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
const { name, folder } = parsed;
|
|
591
|
+
const objectDir = this.dir.join(`objects/${name}`);
|
|
592
|
+
await objectDir.ensureDirectory();
|
|
593
|
+
const objectYy = objectDir.join(`${name}.yy`);
|
|
594
|
+
const objectCreateFile = objectDir.join('Create_0.gml');
|
|
595
|
+
await objectCreateFile.write('/// ');
|
|
596
|
+
await Yy.write(objectYy.absolute, {
|
|
597
|
+
name,
|
|
598
|
+
parent: {
|
|
599
|
+
name: folder.name,
|
|
600
|
+
path: folder.folderPath,
|
|
601
|
+
},
|
|
602
|
+
// Include the Create event by default
|
|
603
|
+
eventList: [{ eventNum: 0, eventType: 0 }],
|
|
604
|
+
}, 'objects', this.yyp);
|
|
605
|
+
// Update the yyp file
|
|
606
|
+
const info = await this.addAssetToYyp(objectYy.absolute);
|
|
607
|
+
// Create and add the asset
|
|
608
|
+
const asset = await Asset.from(this, info);
|
|
609
|
+
if (asset) {
|
|
610
|
+
this.registerAsset(asset);
|
|
611
|
+
}
|
|
612
|
+
return asset;
|
|
613
|
+
}
|
|
614
|
+
async createShader(path) {
|
|
615
|
+
// Create the yy file
|
|
616
|
+
const parsed = await this.parseNewAssetPath(path);
|
|
617
|
+
if (!parsed) {
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
620
|
+
const { name, folder } = parsed;
|
|
621
|
+
const shaderDir = this.dir.join(`shaders/${name}`);
|
|
622
|
+
await shaderDir.ensureDirectory();
|
|
623
|
+
const shaderYy = shaderDir.join(`${name}.yy`);
|
|
624
|
+
await Yy.write(shaderYy.absolute, {
|
|
625
|
+
name,
|
|
626
|
+
parent: {
|
|
627
|
+
name: folder.name,
|
|
628
|
+
path: folder.folderPath,
|
|
629
|
+
},
|
|
630
|
+
}, 'shaders', this.yyp);
|
|
631
|
+
// Create the fsh and vsh files
|
|
632
|
+
const fsh = shaderYy.changeExtension('fsh');
|
|
633
|
+
await fsh.write(fshDefault);
|
|
634
|
+
const vsh = shaderYy.changeExtension('vsh');
|
|
635
|
+
await vsh.write(vshDefault);
|
|
636
|
+
// Update the yyp file
|
|
637
|
+
const info = await this.addAssetToYyp(shaderYy.absolute);
|
|
638
|
+
// Create and add the asset
|
|
639
|
+
const asset = await Asset.from(this, info);
|
|
640
|
+
if (asset) {
|
|
641
|
+
this.registerAsset(asset);
|
|
642
|
+
}
|
|
643
|
+
return asset;
|
|
644
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Add a script to the yyp file. The path string can include separators,
|
|
647
|
+
* in which case folders will be ensured up to the final component.
|
|
648
|
+
*/
|
|
649
|
+
async createScript(path) {
|
|
650
|
+
// Create the yy file
|
|
651
|
+
const parsed = await this.parseNewAssetPath(path);
|
|
652
|
+
if (!parsed) {
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
const { name, folder } = parsed;
|
|
656
|
+
assertIsValidIdentifier(name);
|
|
657
|
+
const scriptDir = this.dir.join(`scripts/${name}`);
|
|
658
|
+
await scriptDir.ensureDirectory();
|
|
659
|
+
const scriptYy = scriptDir.join(`${name}.yy`);
|
|
660
|
+
await Yy.write(scriptYy.absolute, {
|
|
661
|
+
name,
|
|
662
|
+
parent: {
|
|
663
|
+
name: folder.name,
|
|
664
|
+
path: folder.folderPath,
|
|
665
|
+
},
|
|
666
|
+
}, 'scripts', this.yyp);
|
|
667
|
+
// Create the gml file
|
|
668
|
+
const scriptGml = scriptYy.changeExtension('gml');
|
|
669
|
+
await scriptGml.write('/// ');
|
|
670
|
+
// Update the yyp file
|
|
671
|
+
const info = await this.addAssetToYyp(scriptYy.absolute);
|
|
672
|
+
// Create and add the asset
|
|
673
|
+
const asset = await Asset.from(this, info);
|
|
674
|
+
if (asset) {
|
|
675
|
+
this.registerAsset(asset);
|
|
676
|
+
}
|
|
677
|
+
return asset;
|
|
678
|
+
}
|
|
679
|
+
async parseNewAssetPath(path) {
|
|
680
|
+
const parts = path.split(/[/\\]+/);
|
|
681
|
+
const name = parts.pop();
|
|
682
|
+
if (!name) {
|
|
683
|
+
logger.error(`Attempted to add script with no name: ${path}`);
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
assertIsValidIdentifier(name);
|
|
687
|
+
const existingAsset = this.getAssetByName(name);
|
|
688
|
+
if (existingAsset) {
|
|
689
|
+
logger.error(`An asset named ${path} (${existingAsset.assetKind}) already exists`);
|
|
690
|
+
return;
|
|
691
|
+
}
|
|
692
|
+
if (!parts.length) {
|
|
693
|
+
logger.error(`Adding scripts to the root directory is not supported.`);
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
const folder = (await this.createFolder(parts));
|
|
697
|
+
return { folder, name };
|
|
698
|
+
}
|
|
699
|
+
/**
|
|
700
|
+
* Given the path to a yy file for an asset, ensure
|
|
701
|
+
* it has an entry in the yyp file. */
|
|
702
|
+
async addAssetToYyp(yyPath, options) {
|
|
703
|
+
assert(yyPath.endsWith('.yy'), `Expected yy file, got ${yyPath}`);
|
|
704
|
+
const parts = yyPath.split(/[/\\]+/).slice(-3);
|
|
705
|
+
assert(parts.length === 3, `Expected path with at least 3 parts, got ${yyPath}`);
|
|
706
|
+
const [type, name, basename] = parts;
|
|
707
|
+
const resourceEntry = {
|
|
708
|
+
id: {
|
|
709
|
+
name,
|
|
710
|
+
path: `${type}/${name}/${basename}`,
|
|
711
|
+
},
|
|
712
|
+
};
|
|
713
|
+
// Insert the resource into a random spot in the list to avoid git conflicts,
|
|
714
|
+
// avoiding the last spot because that's where changes are most likely to be.
|
|
715
|
+
// (This only matters for older project versions -- the newer version )
|
|
716
|
+
const lastAllowed = Math.max(0, this.yyp.resources.length - 1);
|
|
717
|
+
const insertAt = Math.floor(Math.random() * lastAllowed);
|
|
718
|
+
this.yyp.resources.splice(insertAt, 0, resourceEntry);
|
|
719
|
+
if (!options?.skipSave) {
|
|
720
|
+
await this.saveYyp();
|
|
721
|
+
}
|
|
722
|
+
return resourceEntry;
|
|
723
|
+
}
|
|
724
|
+
parseFolderPath(path) {
|
|
725
|
+
const parts = Array.isArray(path) ? path : path.split(/[/\\]+/);
|
|
726
|
+
const full = `folders/${parts.join('/')}.yy`;
|
|
727
|
+
const prefix = `folders/${parts.join('/')}/`;
|
|
728
|
+
return { parts, full, prefix };
|
|
729
|
+
}
|
|
730
|
+
listAssetsInFolder(path, options) {
|
|
731
|
+
const { full, prefix } = this.parseFolderPath(path);
|
|
732
|
+
const foundAssets = [];
|
|
733
|
+
for (const asset of this.assets.values()) {
|
|
734
|
+
const assetFolder = asset.yy?.parent;
|
|
735
|
+
if (assetFolder.path === full) {
|
|
736
|
+
foundAssets.push(asset);
|
|
737
|
+
}
|
|
738
|
+
else if (options?.recursive && assetFolder.path.startsWith(prefix)) {
|
|
739
|
+
foundAssets.push(asset);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
return foundAssets;
|
|
743
|
+
}
|
|
744
|
+
/**
|
|
745
|
+
* Delete a folder recursively. Only allowed if there are no assets
|
|
746
|
+
* in this or any subfolder.
|
|
747
|
+
*/
|
|
748
|
+
async deleteFolder(path) {
|
|
749
|
+
const assets = this.listAssetsInFolder(path, { recursive: true });
|
|
750
|
+
const { full, prefix } = this.parseFolderPath(path);
|
|
751
|
+
assert(!assets.length, 'Cannot delete folder containing assets!');
|
|
752
|
+
for (let f = this.yyp.Folders.length - 1; f >= 0; f--) {
|
|
753
|
+
const currentFolder = this.yyp.Folders[f];
|
|
754
|
+
// If this is the "old" folder, delete it
|
|
755
|
+
if (full === currentFolder.folderPath ||
|
|
756
|
+
currentFolder.folderPath.startsWith(prefix)) {
|
|
757
|
+
this.yyp.Folders.splice(f, 1);
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
await this.saveYyp();
|
|
762
|
+
}
|
|
763
|
+
/**
|
|
764
|
+
* Rename an existing folder. Allows for renaming any part of
|
|
765
|
+
* the path (useful both "moving" and "renaming" a folder).
|
|
766
|
+
* Array inputs are interpreted as pre-split paths. If the new
|
|
767
|
+
* name matches an existing folder, it will in effect be "merged"
|
|
768
|
+
* with that existing folder.
|
|
769
|
+
*
|
|
770
|
+
* Returns the list of folders and assets that are now in a new
|
|
771
|
+
* location. */
|
|
772
|
+
async renameFolder(oldPath, newPath) {
|
|
773
|
+
const oldParts = Array.isArray(oldPath) ? oldPath : oldPath.split(/[/\\]+/);
|
|
774
|
+
const newParts = Array.isArray(newPath) ? newPath : newPath.split(/[/\\]+/);
|
|
775
|
+
if (!oldParts.length) {
|
|
776
|
+
logger.warn(`Cannot rename root folder`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
if (oldParts.join('/') === newParts.join('/')) {
|
|
780
|
+
logger.warn(`Folder is already named that. Skipping rename.`);
|
|
781
|
+
return;
|
|
782
|
+
}
|
|
783
|
+
// Ensure the new folder exists
|
|
784
|
+
const targetFolder = await this.createFolder(newParts);
|
|
785
|
+
if (!targetFolder)
|
|
786
|
+
return;
|
|
787
|
+
// Move subfolders from the old folder to the new folder
|
|
788
|
+
const oldPathFull = `folders/${oldParts.join('/')}.yy`;
|
|
789
|
+
const oldPathPrefix = `folders/${oldParts.join('/')}/`;
|
|
790
|
+
const newPathPrefix = `folders/${newParts.join('/')}/`;
|
|
791
|
+
const movedFolders = [];
|
|
792
|
+
// Start from the end so we can delete as we go
|
|
793
|
+
for (let f = this.yyp.Folders.length - 1; f >= 0; f--) {
|
|
794
|
+
const currentFolder = this.yyp.Folders[f];
|
|
795
|
+
// If this is the "old" folder, delete it
|
|
796
|
+
if (oldPathFull === currentFolder.folderPath) {
|
|
797
|
+
this.yyp.Folders.splice(f, 1);
|
|
798
|
+
movedFolders.push([currentFolder, undefined]);
|
|
799
|
+
continue;
|
|
800
|
+
}
|
|
801
|
+
// If this is a subfolder of the old folder, move it
|
|
802
|
+
if (currentFolder.folderPath.startsWith(oldPathPrefix)) {
|
|
803
|
+
const newPath = currentFolder.folderPath.replace(oldPathPrefix, newPathPrefix);
|
|
804
|
+
this.yyp.Folders[f] = {
|
|
805
|
+
...currentFolder,
|
|
806
|
+
folderPath: newPath,
|
|
807
|
+
};
|
|
808
|
+
movedFolders.push([currentFolder, this.yyp.Folders[f]]);
|
|
809
|
+
}
|
|
810
|
+
}
|
|
811
|
+
await this.saveYyp();
|
|
812
|
+
// Move assets from the old folder to the new folder
|
|
813
|
+
const movedAssets = [];
|
|
814
|
+
for (const asset of this.assets.values()) {
|
|
815
|
+
const assetFolder = asset.yy?.parent;
|
|
816
|
+
let moved = false;
|
|
817
|
+
if (assetFolder.path === oldPathFull) {
|
|
818
|
+
asset.yy.parent = {
|
|
819
|
+
name: targetFolder.name,
|
|
820
|
+
path: targetFolder.folderPath,
|
|
821
|
+
};
|
|
822
|
+
moved = true;
|
|
823
|
+
}
|
|
824
|
+
else if (assetFolder.path.startsWith(oldPathPrefix)) {
|
|
825
|
+
// The name comes from a subfolder, so just need to update the path
|
|
826
|
+
asset.yy.parent = {
|
|
827
|
+
name: assetFolder.name,
|
|
828
|
+
path: assetFolder.path.replace(oldPathPrefix, newPathPrefix),
|
|
829
|
+
};
|
|
830
|
+
moved = true;
|
|
831
|
+
}
|
|
832
|
+
if (moved) {
|
|
833
|
+
movedAssets.push(asset);
|
|
834
|
+
await asset.saveYy();
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
return { movedFolders, movedAssets };
|
|
838
|
+
}
|
|
839
|
+
/**
|
|
840
|
+
* Add a folder to the yyp file. The string can include separators,
|
|
841
|
+
* in which case nested folders will be created. If an array is provided,
|
|
842
|
+
* it is interpreted as a pre-split path. */
|
|
843
|
+
async createFolder(path, options) {
|
|
844
|
+
const parts = Array.isArray(path) ? path : path.split(/[/\\]+/);
|
|
845
|
+
const folders = this.yyp.Folders;
|
|
846
|
+
let current = 'folders/';
|
|
847
|
+
let folder;
|
|
848
|
+
/** A random location in the list where this new folder should be put,
|
|
849
|
+
* to reduce git conflicts.*/
|
|
850
|
+
const insertAt = Math.max(Math.floor(Math.random() * folders.length - 1), 0);
|
|
851
|
+
for (let i = 0; i < parts.length; i++) {
|
|
852
|
+
const part = parts[i];
|
|
853
|
+
if (!part) {
|
|
854
|
+
continue;
|
|
855
|
+
}
|
|
856
|
+
const thisFolderPath = current + part + '.yy';
|
|
857
|
+
folder = folders.find((f) => f.folderPath === thisFolderPath);
|
|
858
|
+
if (!folder) {
|
|
859
|
+
folder = yypFolderSchema.parse({
|
|
860
|
+
folderPath: thisFolderPath,
|
|
861
|
+
name: part,
|
|
862
|
+
});
|
|
863
|
+
folders.splice(insertAt, 0, folder);
|
|
864
|
+
}
|
|
865
|
+
current += part + '/';
|
|
866
|
+
}
|
|
867
|
+
if (!options?.skipSave) {
|
|
868
|
+
await this.saveYyp();
|
|
869
|
+
}
|
|
870
|
+
return folder;
|
|
871
|
+
}
|
|
872
|
+
async saveYyp() {
|
|
873
|
+
await Yy.write(this.yypPath.absolute, this.yyp, 'project');
|
|
874
|
+
}
|
|
875
|
+
/**
|
|
876
|
+
* The name of a resource, *in lower case*, from
|
|
877
|
+
* a path. This is used as the key for looking up resources.
|
|
878
|
+
*
|
|
879
|
+
* The path can be to the asset's folder, or to any file within
|
|
880
|
+
* that folder.
|
|
881
|
+
*/
|
|
882
|
+
assetNameFromPath(path) {
|
|
883
|
+
const parts = path.relativeFrom(this.dir).split(/[/\\]+/);
|
|
884
|
+
return parts[1]?.toLocaleLowerCase?.();
|
|
885
|
+
}
|
|
886
|
+
/**
|
|
887
|
+
* When first creating an instance, we need to get all project file
|
|
888
|
+
* content into memory for fast access. In particular, we need all
|
|
889
|
+
* yyp, yy, and gml files for scripts and objects. For other asset types
|
|
890
|
+
* we just need their names and yyp filepaths.
|
|
891
|
+
*
|
|
892
|
+
* Can be called at any time -- will only operate on new assets.
|
|
893
|
+
*
|
|
894
|
+
* Returns the list of added assets. Assets are instanced and registered but their
|
|
895
|
+
* code is not parsed!
|
|
896
|
+
*/
|
|
897
|
+
async loadAssets(options) {
|
|
898
|
+
const t = Date.now();
|
|
899
|
+
// Load AudioGroup assets
|
|
900
|
+
for (const audioGroup of this.yyp.AudioGroups) {
|
|
901
|
+
if (!this.self.getMember(audioGroup.name)) {
|
|
902
|
+
const signifier = new Signifier(this.self, audioGroup.name, new Type('Asset.GMAudioGroup'));
|
|
903
|
+
signifier.global = true;
|
|
904
|
+
signifier.writable = false;
|
|
905
|
+
this.self.addMember(signifier);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
// We'll say that resources take 80% of loading,
|
|
909
|
+
// and distribute that across the number of resources.
|
|
910
|
+
const perAssetIncrement = this.yyp.resources.length / 80;
|
|
911
|
+
const resourceWaits = [];
|
|
912
|
+
for (const resourceInfo of this.yyp.resources) {
|
|
913
|
+
assert(resourceInfo.id.name, `Resource ${resourceInfo.id.path} has no name`);
|
|
914
|
+
const name = resourceInfo.id.name.toLocaleLowerCase();
|
|
915
|
+
// Skip it if we already have it
|
|
916
|
+
if (this.assets.has(name)) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
resourceWaits.push(Asset.from(this, resourceInfo).then((r) => {
|
|
920
|
+
if (!r) {
|
|
921
|
+
logger.warn(`Resource ${resourceInfo.id.name} has no yy file`);
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
this.registerAsset(r);
|
|
925
|
+
options?.onLoadProgress?.(perAssetIncrement, `Loading assets...`);
|
|
926
|
+
return r;
|
|
927
|
+
}));
|
|
928
|
+
}
|
|
929
|
+
const addedAssets = await Promise.all(resourceWaits);
|
|
930
|
+
options?.onLoadProgress?.(1, `Loaded ${this.assets.size} resources`);
|
|
931
|
+
logger.log(`Loaded ${this.assets.size} resources in ${Date.now() - t}ms`);
|
|
932
|
+
return addedAssets.filter((x) => x);
|
|
933
|
+
}
|
|
934
|
+
async loadHelpLinks() {
|
|
935
|
+
// Need the path to the IDE folder. Can probably get by with the default
|
|
936
|
+
// installation location...
|
|
937
|
+
await this.nativeWaiter;
|
|
938
|
+
const file = await Native.findHelpLinksFile(this.ideVersion);
|
|
939
|
+
const content = await file?.read({ fallback: {} });
|
|
940
|
+
this.helpLinks = new Proxy(content || {}, {
|
|
941
|
+
get: (target, key) => {
|
|
942
|
+
const baseUrl = `https://beta-manual.yoyogames.com/#t=`;
|
|
943
|
+
if (typeof key === 'string' && key in target) {
|
|
944
|
+
return `${baseUrl}${encodeURIComponent(target[key])}.htm`;
|
|
945
|
+
}
|
|
946
|
+
else if (typeof key === 'string') {
|
|
947
|
+
return `${baseUrl}Content.htm&rhsearch=${encodeURIComponent(key)}&ux=${encodeURIComponent(key)}`;
|
|
948
|
+
}
|
|
949
|
+
return `${baseUrl}Content.htm`;
|
|
950
|
+
},
|
|
951
|
+
});
|
|
952
|
+
}
|
|
953
|
+
async reloadConfig() {
|
|
954
|
+
this.config = await this.stitchConfig.read({ fallback: {} });
|
|
955
|
+
return this.config;
|
|
956
|
+
}
|
|
957
|
+
async getWindowsName() {
|
|
958
|
+
const windowOptionsFile = this.dir.join('options/windows/options_windows.yy');
|
|
959
|
+
if (!(await windowOptionsFile.exists())) {
|
|
960
|
+
return;
|
|
961
|
+
}
|
|
962
|
+
const content = (await Yy.read(windowOptionsFile.absolute));
|
|
963
|
+
return content.option_windows_display_name;
|
|
964
|
+
}
|
|
965
|
+
/**
|
|
966
|
+
* Load the GML spec for the project's runtime version, falling
|
|
967
|
+
* back on the included spec if necessary.
|
|
968
|
+
*/
|
|
969
|
+
async loadGmlSpec() {
|
|
970
|
+
const t = Date.now();
|
|
971
|
+
this.self = new Type('Struct').named('global');
|
|
972
|
+
this.symbol = new Signifier(this.self, 'global', this.self);
|
|
973
|
+
this.symbol.global = true;
|
|
974
|
+
this.symbol.writable = false;
|
|
975
|
+
this.symbol.def = {};
|
|
976
|
+
let runtimeVersion;
|
|
977
|
+
// Check for a stitch config file that specifies the runtime version.
|
|
978
|
+
// If it exists, use that version. It's likely that it is correct, and this
|
|
979
|
+
// way we don't have to download the releases summary.
|
|
980
|
+
if (this.config.runtimeVersion) {
|
|
981
|
+
logger.info('Found stitch config');
|
|
982
|
+
runtimeVersion = this.config.runtimeVersion;
|
|
983
|
+
}
|
|
984
|
+
await this.yypWaiter; // To ensure that `this.ideVersion` exists
|
|
985
|
+
const specFiles = await Native.listSpecFiles({
|
|
986
|
+
ideVersion: this.ideVersion,
|
|
987
|
+
runtimeVersion,
|
|
988
|
+
});
|
|
989
|
+
this.native = await Native.from(specFiles, this.self, this.types);
|
|
990
|
+
logger.log(`Loaded GML spec in ${Date.now() - t}ms`);
|
|
991
|
+
}
|
|
992
|
+
/**
|
|
993
|
+
* Call to reload the project's yyp file (e.g. because it has changed
|
|
994
|
+
* on disk) and add/remove any resources.
|
|
995
|
+
*/
|
|
996
|
+
async reloadYyp() {
|
|
997
|
+
// Update the YYP and identify new/deleted assets
|
|
998
|
+
// const oldYyp = this.yyp;
|
|
999
|
+
assert(this.yypPath, 'Cannot reload YYP without a path');
|
|
1000
|
+
this.yyp = await Yy.read(this.yypPath.absolute, 'project');
|
|
1001
|
+
// // NOTE: This is disabled because it's doesn't behave well
|
|
1002
|
+
// // Remove old assets
|
|
1003
|
+
// const assetIds = new Map(
|
|
1004
|
+
// this.yyp.resources.map((r) => [r.id.path, r.id]),
|
|
1005
|
+
// );
|
|
1006
|
+
// const removedAssets = oldYyp.resources.filter(
|
|
1007
|
+
// (r) => !assetIds.has(r.id.path),
|
|
1008
|
+
// );
|
|
1009
|
+
// for (const removedAsset of removedAssets) {
|
|
1010
|
+
// await this.removeAssetByName(removedAsset.id.name);
|
|
1011
|
+
// }
|
|
1012
|
+
// Add new assets
|
|
1013
|
+
const newAssets = await this.loadAssets();
|
|
1014
|
+
await this.initiallyParseAssetCode(newAssets);
|
|
1015
|
+
// Try to keep anything that got touched *clean*
|
|
1016
|
+
this.drainDirtyFileUpdateQueue();
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* @internal
|
|
1020
|
+
* Initialize a collection of new assets by parsing their GML */
|
|
1021
|
+
initiallyParseAssetCode(assets) {
|
|
1022
|
+
// Do scripts before objects
|
|
1023
|
+
assets = [...assets.values()].sort((a, b) => {
|
|
1024
|
+
if (a.assetKind === b.assetKind) {
|
|
1025
|
+
return a.name.localeCompare(b.name);
|
|
1026
|
+
}
|
|
1027
|
+
if (a.assetKind === 'scripts') {
|
|
1028
|
+
return -1;
|
|
1029
|
+
}
|
|
1030
|
+
if (b.assetKind === 'scripts') {
|
|
1031
|
+
return 1;
|
|
1032
|
+
}
|
|
1033
|
+
if (a.assetKind === 'objects') {
|
|
1034
|
+
return -1;
|
|
1035
|
+
}
|
|
1036
|
+
if (b.assetKind === 'objects') {
|
|
1037
|
+
return 1;
|
|
1038
|
+
}
|
|
1039
|
+
return a.name.localeCompare(b.name);
|
|
1040
|
+
});
|
|
1041
|
+
logger.info('Discovering globals...');
|
|
1042
|
+
for (const asset of assets) {
|
|
1043
|
+
asset.updateGlobals(true);
|
|
1044
|
+
}
|
|
1045
|
+
// Discover all symbols and their references
|
|
1046
|
+
logger.info('Discovering symbols...');
|
|
1047
|
+
for (const asset of assets) {
|
|
1048
|
+
asset.updateAllSymbols(true);
|
|
1049
|
+
}
|
|
1050
|
+
// Second pass
|
|
1051
|
+
// TODO: Find a better way than brute-forcing to resolve cross-file references
|
|
1052
|
+
for (const pass of [1]) {
|
|
1053
|
+
logger.info(`Re-processing pass ${pass}...`);
|
|
1054
|
+
// const reloads: Promise<any>[] = [];
|
|
1055
|
+
for (const asset of assets) {
|
|
1056
|
+
asset.updateGlobals();
|
|
1057
|
+
asset.updateAllSymbols();
|
|
1058
|
+
// for (const file of asset.gmlFilesArray) {
|
|
1059
|
+
// reloads.push(file.reload(file.content));
|
|
1060
|
+
// }
|
|
1061
|
+
}
|
|
1062
|
+
// await Promise.all(reloads);
|
|
1063
|
+
}
|
|
1064
|
+
// But for now, that's what we'll do!
|
|
1065
|
+
logger.info('Updating diagnostics...');
|
|
1066
|
+
for (const asset of assets) {
|
|
1067
|
+
asset.updateDiagnostics();
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
async initialize(options) {
|
|
1071
|
+
logger.info('Initializing project...');
|
|
1072
|
+
if (options?.onDiagnostics) {
|
|
1073
|
+
this.onDiagnostics(options.onDiagnostics);
|
|
1074
|
+
}
|
|
1075
|
+
let t = Date.now();
|
|
1076
|
+
await this.reloadConfig();
|
|
1077
|
+
assert(this.yypPath, 'Cannot initialize without a path');
|
|
1078
|
+
this.yypWaiter = Yy.read(this.yypPath.absolute, 'project').then((yyp) => {
|
|
1079
|
+
this.yyp = yyp;
|
|
1080
|
+
options?.onLoadProgress?.(5, 'Loaded project file');
|
|
1081
|
+
logger.info('Loaded yyp file!');
|
|
1082
|
+
});
|
|
1083
|
+
this.nativeWaiter = this.loadGmlSpec();
|
|
1084
|
+
void this.nativeWaiter.then(() => {
|
|
1085
|
+
options?.onLoadProgress?.(5, 'Loaded GML spec');
|
|
1086
|
+
});
|
|
1087
|
+
logger.info('Loading asset files...');
|
|
1088
|
+
await Promise.all([
|
|
1089
|
+
this.nativeWaiter,
|
|
1090
|
+
this.yypWaiter,
|
|
1091
|
+
this.loadHelpLinks(),
|
|
1092
|
+
]);
|
|
1093
|
+
const assets = await this.loadAssets(options);
|
|
1094
|
+
logger.log('Resources', this.assets.size, 'loaded files in', Date.now() - t, 'ms');
|
|
1095
|
+
t = Date.now();
|
|
1096
|
+
// Discover all globals
|
|
1097
|
+
// Sort assets by type, with objects 2nd to last and scripts last
|
|
1098
|
+
// to minimize the number of things that need to be updated after
|
|
1099
|
+
// loading.
|
|
1100
|
+
options?.onLoadProgress?.(1, 'Parsing resource code...');
|
|
1101
|
+
await this.initiallyParseAssetCode(assets);
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Create a new project instance and initialize it.
|
|
1105
|
+
*/
|
|
1106
|
+
static async initialize(yypPath, options) {
|
|
1107
|
+
let path = pathy(yypPath);
|
|
1108
|
+
if (await path.isDirectory()) {
|
|
1109
|
+
const children = await path.listChildren();
|
|
1110
|
+
path = children.find((p) => p.hasExtension('yyp'));
|
|
1111
|
+
ok(path, 'No yyp file found in project directory');
|
|
1112
|
+
}
|
|
1113
|
+
await path.exists({ assert: true });
|
|
1114
|
+
const project = new Project(path, options);
|
|
1115
|
+
await project.initialize(options);
|
|
1116
|
+
return project;
|
|
1117
|
+
}
|
|
1118
|
+
static fallbackGmlSpecPath = pathy(import.meta.url).resolveTo('../../assets/GmlSpec.xml');
|
|
1119
|
+
}
|
|
1120
|
+
__decorate([
|
|
1121
|
+
sequential,
|
|
1122
|
+
__metadata("design:type", Function),
|
|
1123
|
+
__metadata("design:paramtypes", [Object]),
|
|
1124
|
+
__metadata("design:returntype", Promise)
|
|
1125
|
+
], Project.prototype, "removeAssetByName", null);
|
|
1126
|
+
__decorate([
|
|
1127
|
+
sequential,
|
|
1128
|
+
__metadata("design:type", Function),
|
|
1129
|
+
__metadata("design:paramtypes", []),
|
|
1130
|
+
__metadata("design:returntype", Promise)
|
|
1131
|
+
], Project.prototype, "syncIncludedFiles", null);
|
|
1132
|
+
__decorate([
|
|
1133
|
+
sequential,
|
|
1134
|
+
__metadata("design:type", Function),
|
|
1135
|
+
__metadata("design:paramtypes", [String, String]),
|
|
1136
|
+
__metadata("design:returntype", Promise)
|
|
1137
|
+
], Project.prototype, "renameAsset", null);
|
|
1138
|
+
__decorate([
|
|
1139
|
+
sequential,
|
|
1140
|
+
__metadata("design:type", Function),
|
|
1141
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1142
|
+
__metadata("design:returntype", Promise)
|
|
1143
|
+
], Project.prototype, "import", null);
|
|
1144
|
+
__decorate([
|
|
1145
|
+
sequential,
|
|
1146
|
+
__metadata("design:type", Function),
|
|
1147
|
+
__metadata("design:paramtypes", [String, String]),
|
|
1148
|
+
__metadata("design:returntype", Promise)
|
|
1149
|
+
], Project.prototype, "duplicateAsset", null);
|
|
1150
|
+
__decorate([
|
|
1151
|
+
sequential,
|
|
1152
|
+
__metadata("design:type", Function),
|
|
1153
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
1154
|
+
__metadata("design:returntype", Promise)
|
|
1155
|
+
], Project.prototype, "createSound", null);
|
|
1156
|
+
__decorate([
|
|
1157
|
+
sequential,
|
|
1158
|
+
__metadata("design:type", Function),
|
|
1159
|
+
__metadata("design:paramtypes", [String]),
|
|
1160
|
+
__metadata("design:returntype", Promise)
|
|
1161
|
+
], Project.prototype, "createRoom", null);
|
|
1162
|
+
__decorate([
|
|
1163
|
+
sequential,
|
|
1164
|
+
__metadata("design:type", Function),
|
|
1165
|
+
__metadata("design:paramtypes", [String, Object]),
|
|
1166
|
+
__metadata("design:returntype", Promise)
|
|
1167
|
+
], Project.prototype, "createSprite", null);
|
|
1168
|
+
__decorate([
|
|
1169
|
+
sequential,
|
|
1170
|
+
__metadata("design:type", Function),
|
|
1171
|
+
__metadata("design:paramtypes", [String]),
|
|
1172
|
+
__metadata("design:returntype", Promise)
|
|
1173
|
+
], Project.prototype, "createObject", null);
|
|
1174
|
+
__decorate([
|
|
1175
|
+
sequential,
|
|
1176
|
+
__metadata("design:type", Function),
|
|
1177
|
+
__metadata("design:paramtypes", [String]),
|
|
1178
|
+
__metadata("design:returntype", Promise)
|
|
1179
|
+
], Project.prototype, "createShader", null);
|
|
1180
|
+
__decorate([
|
|
1181
|
+
sequential,
|
|
1182
|
+
__metadata("design:type", Function),
|
|
1183
|
+
__metadata("design:paramtypes", [String]),
|
|
1184
|
+
__metadata("design:returntype", Promise)
|
|
1185
|
+
], Project.prototype, "createScript", null);
|
|
1186
|
+
__decorate([
|
|
1187
|
+
sequential,
|
|
1188
|
+
__metadata("design:type", Function),
|
|
1189
|
+
__metadata("design:paramtypes", [Object]),
|
|
1190
|
+
__metadata("design:returntype", Promise)
|
|
1191
|
+
], Project.prototype, "deleteFolder", null);
|
|
1192
|
+
__decorate([
|
|
1193
|
+
sequential,
|
|
1194
|
+
__metadata("design:type", Function),
|
|
1195
|
+
__metadata("design:paramtypes", [Object, Object]),
|
|
1196
|
+
__metadata("design:returntype", Promise)
|
|
1197
|
+
], Project.prototype, "renameFolder", null);
|
|
1198
|
+
__decorate([
|
|
1199
|
+
sequential,
|
|
1200
|
+
__metadata("design:type", Function),
|
|
1201
|
+
__metadata("design:paramtypes", []),
|
|
1202
|
+
__metadata("design:returntype", Promise)
|
|
1203
|
+
], Project.prototype, "saveYyp", null);
|
|
1204
|
+
__decorate([
|
|
1205
|
+
sequential,
|
|
1206
|
+
__metadata("design:type", Function),
|
|
1207
|
+
__metadata("design:paramtypes", []),
|
|
1208
|
+
__metadata("design:returntype", Promise)
|
|
1209
|
+
], Project.prototype, "reloadConfig", null);
|
|
1210
|
+
__decorate([
|
|
1211
|
+
sequential,
|
|
1212
|
+
__metadata("design:type", Function),
|
|
1213
|
+
__metadata("design:paramtypes", []),
|
|
1214
|
+
__metadata("design:returntype", Promise)
|
|
1215
|
+
], Project.prototype, "getWindowsName", null);
|
|
1216
|
+
//# sourceMappingURL=project.js.map
|