@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.
Files changed (208) hide show
  1. package/LICENSE.md +29 -0
  2. package/README.md +151 -0
  3. package/assets/GmlSpec.xml +11419 -0
  4. package/dist/index.d.ts +16 -0
  5. package/dist/index.d.ts.map +1 -0
  6. package/dist/index.js +13 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/jsdoc.d.ts +79 -0
  9. package/dist/jsdoc.d.ts.map +1 -0
  10. package/dist/jsdoc.feather.d.ts +23 -0
  11. package/dist/jsdoc.feather.d.ts.map +1 -0
  12. package/dist/jsdoc.feather.js +143 -0
  13. package/dist/jsdoc.feather.js.map +1 -0
  14. package/dist/jsdoc.js +468 -0
  15. package/dist/jsdoc.js.map +1 -0
  16. package/dist/jsdoc.test.d.ts +2 -0
  17. package/dist/jsdoc.test.d.ts.map +1 -0
  18. package/dist/jsdoc.test.js +185 -0
  19. package/dist/jsdoc.test.js.map +1 -0
  20. package/dist/lexer.d.ts +3 -0
  21. package/dist/lexer.d.ts.map +1 -0
  22. package/dist/lexer.js +14 -0
  23. package/dist/lexer.js.map +1 -0
  24. package/dist/lexer.test.d.ts +2 -0
  25. package/dist/lexer.test.d.ts.map +1 -0
  26. package/dist/lexer.test.js +78 -0
  27. package/dist/lexer.test.js.map +1 -0
  28. package/dist/lib.objects.d.ts +190 -0
  29. package/dist/lib.objects.d.ts.map +1 -0
  30. package/dist/lib.objects.js +242 -0
  31. package/dist/lib.objects.js.map +1 -0
  32. package/dist/logger.d.ts +13 -0
  33. package/dist/logger.d.ts.map +1 -0
  34. package/dist/logger.js +14 -0
  35. package/dist/logger.js.map +1 -0
  36. package/dist/modules.d.ts +19 -0
  37. package/dist/modules.d.ts.map +1 -0
  38. package/dist/modules.js +320 -0
  39. package/dist/modules.js.map +1 -0
  40. package/dist/modules.test.d.ts +2 -0
  41. package/dist/modules.test.d.ts.map +1 -0
  42. package/dist/modules.test.js +57 -0
  43. package/dist/modules.test.js.map +1 -0
  44. package/dist/modules.types.d.ts +78 -0
  45. package/dist/modules.types.d.ts.map +1 -0
  46. package/dist/modules.types.js +2 -0
  47. package/dist/modules.types.js.map +1 -0
  48. package/dist/modules.util.d.ts +5 -0
  49. package/dist/modules.util.d.ts.map +1 -0
  50. package/dist/modules.util.js +13 -0
  51. package/dist/modules.util.js.map +1 -0
  52. package/dist/parser.d.ts +121 -0
  53. package/dist/parser.d.ts.map +1 -0
  54. package/dist/parser.js +571 -0
  55. package/dist/parser.js.map +1 -0
  56. package/dist/parser.test.d.ts +2 -0
  57. package/dist/parser.test.d.ts.map +1 -0
  58. package/dist/parser.test.js +143 -0
  59. package/dist/parser.test.js.map +1 -0
  60. package/dist/parser.utility.d.ts +29 -0
  61. package/dist/parser.utility.d.ts.map +1 -0
  62. package/dist/parser.utility.js +125 -0
  63. package/dist/parser.utility.js.map +1 -0
  64. package/dist/project.asset.d.ts +115 -0
  65. package/dist/project.asset.d.ts.map +1 -0
  66. package/dist/project.asset.js +802 -0
  67. package/dist/project.asset.js.map +1 -0
  68. package/dist/project.code.d.ts +130 -0
  69. package/dist/project.code.d.ts.map +1 -0
  70. package/dist/project.code.js +570 -0
  71. package/dist/project.code.js.map +1 -0
  72. package/dist/project.d.ts +533 -0
  73. package/dist/project.d.ts.map +1 -0
  74. package/dist/project.diagnostics.d.ts +32 -0
  75. package/dist/project.diagnostics.d.ts.map +1 -0
  76. package/dist/project.diagnostics.js +23 -0
  77. package/dist/project.diagnostics.js.map +1 -0
  78. package/dist/project.js +1216 -0
  79. package/dist/project.js.map +1 -0
  80. package/dist/project.location.d.ts +133 -0
  81. package/dist/project.location.d.ts.map +1 -0
  82. package/dist/project.location.js +219 -0
  83. package/dist/project.location.js.map +1 -0
  84. package/dist/project.native.d.ts +26 -0
  85. package/dist/project.native.d.ts.map +1 -0
  86. package/dist/project.native.js +321 -0
  87. package/dist/project.native.js.map +1 -0
  88. package/dist/project.spec.d.ts +1298 -0
  89. package/dist/project.spec.d.ts.map +1 -0
  90. package/dist/project.spec.js +263 -0
  91. package/dist/project.spec.js.map +1 -0
  92. package/dist/project.test.d.ts +2 -0
  93. package/dist/project.test.d.ts.map +1 -0
  94. package/dist/project.test.js +633 -0
  95. package/dist/project.test.js.map +1 -0
  96. package/dist/shaderDefaults.d.ts +3 -0
  97. package/dist/shaderDefaults.d.ts.map +1 -0
  98. package/dist/shaderDefaults.js +32 -0
  99. package/dist/shaderDefaults.js.map +1 -0
  100. package/dist/signifiers.d.ts +54 -0
  101. package/dist/signifiers.d.ts.map +1 -0
  102. package/dist/signifiers.flags.d.ts +38 -0
  103. package/dist/signifiers.flags.d.ts.map +1 -0
  104. package/dist/signifiers.flags.js +131 -0
  105. package/dist/signifiers.flags.js.map +1 -0
  106. package/dist/signifiers.js +117 -0
  107. package/dist/signifiers.js.map +1 -0
  108. package/dist/spine.d.ts +28 -0
  109. package/dist/spine.d.ts.map +1 -0
  110. package/dist/spine.js +64 -0
  111. package/dist/spine.js.map +1 -0
  112. package/dist/spine.test.d.ts +2 -0
  113. package/dist/spine.test.d.ts.map +1 -0
  114. package/dist/spine.test.js +420 -0
  115. package/dist/spine.test.js.map +1 -0
  116. package/dist/spine.types.d.ts +89 -0
  117. package/dist/spine.types.d.ts.map +1 -0
  118. package/dist/spine.types.js +2 -0
  119. package/dist/spine.types.js.map +1 -0
  120. package/dist/test.lib.d.ts +3 -0
  121. package/dist/test.lib.d.ts.map +1 -0
  122. package/dist/test.lib.js +16 -0
  123. package/dist/test.lib.js.map +1 -0
  124. package/dist/tokens.categories.d.ts +22 -0
  125. package/dist/tokens.categories.d.ts.map +1 -0
  126. package/dist/tokens.categories.js +78 -0
  127. package/dist/tokens.categories.js.map +1 -0
  128. package/dist/tokens.code.d.ts +2 -0
  129. package/dist/tokens.code.d.ts.map +1 -0
  130. package/dist/tokens.code.js +523 -0
  131. package/dist/tokens.code.js.map +1 -0
  132. package/dist/tokens.d.ts +130 -0
  133. package/dist/tokens.d.ts.map +1 -0
  134. package/dist/tokens.js +13 -0
  135. package/dist/tokens.js.map +1 -0
  136. package/dist/tokens.lib.d.ts +15 -0
  137. package/dist/tokens.lib.d.ts.map +1 -0
  138. package/dist/tokens.lib.js +12 -0
  139. package/dist/tokens.lib.js.map +1 -0
  140. package/dist/tokens.shared.d.ts +4 -0
  141. package/dist/tokens.shared.d.ts.map +1 -0
  142. package/dist/tokens.shared.js +35 -0
  143. package/dist/tokens.shared.js.map +1 -0
  144. package/dist/tokens.strings.d.ts +5 -0
  145. package/dist/tokens.strings.d.ts.map +1 -0
  146. package/dist/tokens.strings.js +111 -0
  147. package/dist/tokens.strings.js.map +1 -0
  148. package/dist/types.checks.d.ts +50 -0
  149. package/dist/types.checks.d.ts.map +1 -0
  150. package/dist/types.checks.js +246 -0
  151. package/dist/types.checks.js.map +1 -0
  152. package/dist/types.d.ts +152 -0
  153. package/dist/types.d.ts.map +1 -0
  154. package/dist/types.feather.d.ts +21 -0
  155. package/dist/types.feather.d.ts.map +1 -0
  156. package/dist/types.feather.js +156 -0
  157. package/dist/types.feather.js.map +1 -0
  158. package/dist/types.hover.d.ts +4 -0
  159. package/dist/types.hover.d.ts.map +1 -0
  160. package/dist/types.hover.js +99 -0
  161. package/dist/types.hover.js.map +1 -0
  162. package/dist/types.js +457 -0
  163. package/dist/types.js.map +1 -0
  164. package/dist/types.primitives.d.ts +10 -0
  165. package/dist/types.primitives.d.ts.map +1 -0
  166. package/dist/types.primitives.js +88 -0
  167. package/dist/types.primitives.js.map +1 -0
  168. package/dist/types.sprites.d.ts +8 -0
  169. package/dist/types.sprites.d.ts.map +1 -0
  170. package/dist/types.sprites.js +417 -0
  171. package/dist/types.sprites.js.map +1 -0
  172. package/dist/types.test.d.ts +2 -0
  173. package/dist/types.test.d.ts.map +1 -0
  174. package/dist/types.test.js +62 -0
  175. package/dist/types.test.js.map +1 -0
  176. package/dist/util.d.ts +50 -0
  177. package/dist/util.d.ts.map +1 -0
  178. package/dist/util.js +168 -0
  179. package/dist/util.js.map +1 -0
  180. package/dist/util.test.d.ts +3 -0
  181. package/dist/util.test.d.ts.map +1 -0
  182. package/dist/util.test.js +63 -0
  183. package/dist/util.test.js.map +1 -0
  184. package/dist/visitor.assign.d.ts +24 -0
  185. package/dist/visitor.assign.d.ts.map +1 -0
  186. package/dist/visitor.assign.js +112 -0
  187. package/dist/visitor.assign.js.map +1 -0
  188. package/dist/visitor.d.ts +89 -0
  189. package/dist/visitor.d.ts.map +1 -0
  190. package/dist/visitor.functionExpression.d.ts +7 -0
  191. package/dist/visitor.functionExpression.d.ts.map +1 -0
  192. package/dist/visitor.functionExpression.js +216 -0
  193. package/dist/visitor.functionExpression.js.map +1 -0
  194. package/dist/visitor.globals.d.ts +59 -0
  195. package/dist/visitor.globals.d.ts.map +1 -0
  196. package/dist/visitor.globals.js +271 -0
  197. package/dist/visitor.globals.js.map +1 -0
  198. package/dist/visitor.identifierAccessor.d.ts +6 -0
  199. package/dist/visitor.identifierAccessor.d.ts.map +1 -0
  200. package/dist/visitor.identifierAccessor.js +381 -0
  201. package/dist/visitor.identifierAccessor.js.map +1 -0
  202. package/dist/visitor.js +605 -0
  203. package/dist/visitor.js.map +1 -0
  204. package/dist/visitor.processor.d.ts +66 -0
  205. package/dist/visitor.processor.d.ts.map +1 -0
  206. package/dist/visitor.processor.js +147 -0
  207. package/dist/visitor.processor.js.map +1 -0
  208. package/package.json +63 -0
@@ -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