Mirror for Friday Night Funkin
1package funkin.modding;
2
3import polymod.fs.ZipFileSystem;
4import funkin.data.dialogue.conversation.ConversationRegistry;
5import funkin.data.dialogue.dialoguebox.DialogueBoxRegistry;
6import funkin.data.dialogue.speaker.SpeakerRegistry;
7import funkin.data.event.SongEventRegistry;
8import funkin.data.story.level.LevelRegistry;
9import funkin.data.notestyle.NoteStyleRegistry;
10import funkin.play.notes.notekind.NoteKindManager;
11import funkin.data.song.SongRegistry;
12import funkin.data.freeplay.player.PlayerRegistry;
13import funkin.data.stage.StageRegistry;
14import funkin.data.stickers.StickerRegistry;
15import funkin.data.freeplay.album.AlbumRegistry;
16import funkin.modding.module.ModuleHandler;
17import funkin.play.character.CharacterData.CharacterDataParser;
18import funkin.save.Save;
19import funkin.util.FileUtil;
20import funkin.util.macro.ClassMacro;
21import polymod.backends.PolymodAssets.PolymodAssetType;
22import polymod.format.ParseRules.TextFileFormat;
23import polymod.Polymod;
24
25/**
26 * A class for interacting with Polymod, the atomic modding framework for Haxe.
27 */
28class PolymodHandler
29{
30 /**
31 * The API version for the current version of the game. Since 0.5.0, we've just made this the game version!
32 * Minor updates rarely impact mods but major versions often do.
33 */
34 // static final API_VERSION:String = Constants.VERSION;
35
36 /**
37 * The Semantic Versioning rule
38 * Indicates which mods are compatible with this version of the game.
39 * Using more complex rules allows mods from older compatible versions to stay functioning,
40 * while preventing mods made for future versions from being installed.
41 */
42 static final API_VERSION_RULE:String = ">=0.6.3 <0.7.0";
43
44 /**
45 * Where relative to the executable that mods are located.
46 */
47 static final MOD_FOLDER:String =
48 #if (REDIRECT_ASSETS_FOLDER && macos)
49 '../../../../../../../example_mods'
50 #elseif REDIRECT_ASSETS_FOLDER
51 '../../../../example_mods'
52 #else
53 'mods'
54 #end;
55
56 static final CORE_FOLDER:Null<String> =
57 #if (REDIRECT_ASSETS_FOLDER && macos)
58 '../../../../../../../assets'
59 #elseif REDIRECT_ASSETS_FOLDER
60 '../../../../assets'
61 #else
62 null
63 #end;
64
65 public static var loadedModIds:Array<String> = [];
66
67 // Use SysZipFileSystem on desktop and MemoryZipFilesystem on web.
68 static var modFileSystem:Null<ZipFileSystem> = null;
69
70 /**
71 * If the mods folder doesn't exist, create it.
72 */
73 public static function createModRoot():Void
74 {
75 FileUtil.createDirIfNotExists(MOD_FOLDER);
76 }
77
78 /**
79 * Loads the game with ALL mods enabled with Polymod.
80 */
81 public static function loadAllMods():Void
82 {
83 // Create the mod root if it doesn't exist.
84 createModRoot();
85 trace('Initializing Polymod (using all mods)...');
86 loadModsById(getAllModIds());
87 }
88
89 /**
90 * Loads the game with configured mods enabled with Polymod.
91 */
92 public static function loadEnabledMods():Void
93 {
94 // Create the mod root if it doesn't exist.
95 createModRoot();
96
97 trace('Initializing Polymod (using configured mods)...');
98 loadModsById(Save.instance.enabledModIds);
99 }
100
101 /**
102 * Loads the game without any mods enabled with Polymod.
103 */
104 public static function loadNoMods():Void
105 {
106 // Create the mod root if it doesn't exist.
107 createModRoot();
108
109 // We still need to configure the debug print calls etc.
110 trace('Initializing Polymod (using no mods)...');
111 loadModsById([]);
112 }
113
114 /**
115 * Load all the mods with the given ids.
116 * @param ids The ORDERED list of mod ids to load.
117 */
118 public static function loadModsById(ids:Array<String>):Void
119 {
120 if (ids.length == 0)
121 {
122 trace('You attempted to load zero mods.');
123 }
124 else
125 {
126 trace('Attempting to load ${ids.length} mods...');
127 }
128
129 buildImports();
130
131 if (modFileSystem == null) modFileSystem = buildFileSystem();
132
133 var loadedModList:Array<ModMetadata> = polymod.Polymod.init(
134 {
135 // Root directory for all mods.
136 modRoot: MOD_FOLDER,
137 // The directories for one or more mods to load.
138 dirs: ids,
139 // Framework being used to load assets.
140 framework: OPENFL,
141 // The current version of our API.
142 apiVersionRule: API_VERSION_RULE,
143 // Call this function any time an error occurs.
144 errorCallback: PolymodErrorHandler.onPolymodError,
145 // Enforce semantic version patterns for each mod.
146 // modVersions: null,
147 // A map telling Polymod what the asset type is for unfamiliar file extensions.
148 // extensionMap: [],
149
150 customFilesystem: modFileSystem,
151
152 frameworkParams: buildFrameworkParams(),
153
154 // List of filenames to ignore in mods. Use the default list to ignore the metadata file, etc.
155 ignoredFiles: buildIgnoreList(),
156
157 // Parsing rules for various data formats.
158 parseRules: buildParseRules(),
159
160 skipDependencyErrors: true,
161
162 // Parse hxc files and register the scripted classes in them.
163 useScriptedClasses: true,
164 loadScriptsAsync: #if html5 true #else false #end,
165 });
166
167 if (loadedModList == null)
168 {
169 trace('An error occurred! Failed when loading mods!');
170 }
171 else
172 {
173 if (loadedModList.length == 0)
174 {
175 trace('Mod loading complete. We loaded no mods / ${ids.length} mods.');
176 }
177 else
178 {
179 trace('Mod loading complete. We loaded ${loadedModList.length} / ${ids.length} mods.');
180 }
181 }
182
183 loadedModIds = [];
184 for (mod in loadedModList)
185 {
186 trace(' * ${mod.title} v${mod.modVersion} [${mod.id}]');
187 loadedModIds.push(mod.id);
188 }
189
190 #if FEATURE_DEBUG_FUNCTIONS
191 var fileList:Array<String> = Polymod.listModFiles(PolymodAssetType.IMAGE);
192 trace('Installed mods have replaced ${fileList.length} images.');
193 for (item in fileList)
194 {
195 trace(' * $item');
196 }
197
198 fileList = Polymod.listModFiles(PolymodAssetType.TEXT);
199 trace('Installed mods have added/replaced ${fileList.length} text files.');
200 for (item in fileList)
201 {
202 trace(' * $item');
203 }
204
205 fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC);
206 trace('Installed mods have replaced ${fileList.length} music files.');
207 for (item in fileList)
208 {
209 trace(' * $item');
210 }
211
212 fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND);
213 trace('Installed mods have replaced ${fileList.length} sound files.');
214 for (item in fileList)
215 {
216 trace(' * $item');
217 }
218
219 fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC);
220 trace('Installed mods have replaced ${fileList.length} generic audio files.');
221 for (item in fileList)
222 {
223 trace(' * $item');
224 }
225 #end
226 }
227
228 static function buildFileSystem():polymod.fs.ZipFileSystem
229 {
230 polymod.Polymod.onError = PolymodErrorHandler.onPolymodError;
231 return new ZipFileSystem(
232 {
233 modRoot: MOD_FOLDER,
234 autoScan: true
235 });
236 }
237
238 static function buildImports():Void
239 {
240 // Add default imports for common classes.
241 Polymod.addDefaultImport(funkin.Assets);
242 Polymod.addDefaultImport(funkin.Paths);
243
244 // Add import aliases for certain classes.
245 // NOTE: Scripted classes are automatically aliased to their parent class.
246 Polymod.addImportAlias('flixel.math.FlxPoint', flixel.math.FlxPoint.FlxBasePoint);
247
248 Polymod.addImportAlias('funkin.data.event.SongEventSchema', funkin.data.event.SongEventSchema.SongEventSchemaRaw);
249
250 // `lime.utils.Assets` literally just has a private `resolveClass` function for some reason? so we replace it with our own.
251 Polymod.addImportAlias('lime.utils.Assets', funkin.Assets);
252 Polymod.addImportAlias('openfl.utils.Assets', funkin.Assets);
253
254 // `funkin.util.FileUtil` has unrestricted access to the file system.
255 Polymod.addImportAlias('funkin.util.FileUtil', funkin.util.FileUtilSandboxed);
256
257 // Add blacklisting for prohibited classes and packages.
258
259 // `Sys`
260 // Sys.command() can run malicious processes
261 Polymod.blacklistImport('Sys');
262
263 // `Reflect`
264 // Reflect.callMethod() can access blacklisted packages, but some functions are whitelisted
265 Polymod.addImportAlias('Reflect', funkin.util.ReflectUtil);
266
267 // `Type`
268 // Type.createInstance(Type.resolveClass()) can access blacklisted packages, but some functions are whitelisted
269 Polymod.addImportAlias('Type', funkin.util.ReflectUtil);
270
271 // `cpp.Lib`
272 // Lib.load() can load malicious DLLs
273 Polymod.blacklistImport('cpp.Lib');
274
275 // `Unserializer`
276 // Unserializer.DEFAULT_RESOLVER.resolveClass() can access blacklisted packages
277 Polymod.blacklistImport('Unserializer');
278
279 // `lime.system.CFFI`
280 // Can load and execute compiled binaries.
281 Polymod.blacklistImport('lime.system.CFFI');
282
283 // `lime.system.JNI`
284 // Can load and execute compiled binaries.
285 Polymod.blacklistImport('lime.system.JNI');
286
287 // `lime.system.System`
288 // System.load() can load malicious DLLs
289 Polymod.blacklistImport('lime.system.System');
290
291 // `lime.utils.Assets`
292 // Literally just has a private `resolveClass` function for some reason?
293 Polymod.blacklistImport('lime.utils.Assets');
294 Polymod.blacklistImport('openfl.utils.Assets');
295 Polymod.blacklistImport('openfl.Lib');
296 Polymod.blacklistImport('openfl.system.ApplicationDomain');
297 Polymod.blacklistImport('openfl.net.SharedObject');
298
299 // `openfl.desktop.NativeProcess`
300 // Can load native processes on the host operating system.
301 Polymod.blacklistImport('openfl.desktop.NativeProcess');
302
303 // `funkin.api.*`
304 // Contains functions which may allow for cheating and such.
305 for (cls in ClassMacro.listClassesInPackage('funkin.api'))
306 {
307 if (cls == null) continue;
308 var className:String = Type.getClassName(cls);
309 Polymod.blacklistImport(className);
310 }
311
312 // `polymod.*`
313 // Contains functions which may allow for un-blacklisting other modules.
314 for (cls in ClassMacro.listClassesInPackage('polymod'))
315 {
316 if (cls == null) continue;
317 var className:String = Type.getClassName(cls);
318 Polymod.blacklistImport(className);
319 }
320
321 // `funkin.api.newgrounds.*`
322 // Contains functions which allow for cheating medals and leaderboards.
323 for (cls in ClassMacro.listClassesInPackage('funkin.api.newgrounds'))
324 {
325 if (cls == null) continue;
326 var className:String = Type.getClassName(cls);
327 Polymod.blacklistImport(className);
328 }
329
330 // `io.newgrounds.*`
331 // Contains functions which allow for cheating medals and leaderboards.
332 for (cls in ClassMacro.listClassesInPackage('io.newgrounds'))
333 {
334 if (cls == null) continue;
335 var className:String = Type.getClassName(cls);
336 Polymod.blacklistImport(className);
337 }
338
339 // `sys.*`
340 // Access to system utilities such as the file system.
341 for (cls in ClassMacro.listClassesInPackage('sys'))
342 {
343 if (cls == null) continue;
344 var className:String = Type.getClassName(cls);
345 Polymod.blacklistImport(className);
346 }
347 }
348
349 /**
350 * Build a list of file paths that will be ignored in mods.
351 */
352 static function buildIgnoreList():Array<String>
353 {
354 var result = Polymod.getDefaultIgnoreList();
355
356 result.push('.git');
357 result.push('.gitignore');
358 result.push('.gitattributes');
359 result.push('README.md');
360
361 return result;
362 }
363
364 static function buildParseRules():polymod.format.ParseRules
365 {
366 var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault();
367 // Ensure TXT files have merge support.
368 output.addType('txt', TextFileFormat.LINES);
369 // Ensure script files have merge support.
370 output.addType('hscript', TextFileFormat.PLAINTEXT);
371 output.addType('hxs', TextFileFormat.PLAINTEXT);
372 output.addType('hxc', TextFileFormat.PLAINTEXT);
373 output.addType('hx', TextFileFormat.PLAINTEXT);
374
375 // You can specify the format of a specific file, with file extension.
376 // output.addFile("data/introText.txt", TextFileFormat.LINES)
377 return output;
378 }
379
380 static inline function buildFrameworkParams():polymod.Polymod.FrameworkParams
381 {
382 return {
383 assetLibraryPaths: [
384 'default' => 'preload',
385 'shared' => 'shared',
386 'songs' => 'songs',
387 'videos' => 'videos',
388 'tutorial' => 'tutorial',
389 'week1' => 'week1',
390 'week2' => 'week2',
391 'week3' => 'week3',
392 'week4' => 'week4',
393 'week5' => 'week5',
394 'week6' => 'week6',
395 'week7' => 'week7',
396 'weekend1' => 'weekend1',
397 ],
398 coreAssetRedirect: CORE_FOLDER,
399 }
400 }
401
402 /**
403 * Retrieve a list of metadata for ALL installed mods, including disabled mods.
404 * @return An array of mod metadata
405 */
406 public static function getAllMods():Array<ModMetadata>
407 {
408 trace('Scanning the mods folder...');
409
410 if (modFileSystem == null) modFileSystem = buildFileSystem();
411
412 var modMetadata:Array<ModMetadata> = Polymod.scan(
413 {
414 modRoot: MOD_FOLDER,
415 apiVersionRule: API_VERSION_RULE,
416 fileSystem: modFileSystem,
417 errorCallback: PolymodErrorHandler.onPolymodError
418 });
419 trace('Found ${modMetadata.length} mods when scanning.');
420 return modMetadata;
421 }
422
423 /**
424 * Retrieve a list of ALL mod IDs, including disabled mods.
425 * @return An array of mod IDs
426 */
427 public static function getAllModIds():Array<String>
428 {
429 var modIds:Array<String> = [for (i in getAllMods()) i.id];
430 return modIds;
431 }
432
433 /**
434 * Retrieve a list of metadata for all enabled mods.
435 * @return An array of mod metadata
436 */
437 public static function getEnabledMods():Array<ModMetadata>
438 {
439 var modIds:Array<String> = Save.instance.enabledModIds;
440 var modMetadata:Array<ModMetadata> = getAllMods();
441 var enabledMods:Array<ModMetadata> = [];
442 for (item in modMetadata)
443 {
444 if (modIds.indexOf(item.id) != -1)
445 {
446 enabledMods.push(item);
447 }
448 }
449 return enabledMods;
450 }
451
452 /**
453 * Clear and reload from disk all data assets.
454 * Useful for "hot reloading" for fast iteration!
455 */
456 public static function forceReloadAssets():Void
457 {
458 // Forcibly clear scripts so that scripts can be edited.
459 ModuleHandler.clearModuleCache();
460 Polymod.clearScripts();
461
462 // Forcibly reload Polymod so it finds any new files.
463 // TODO: Replace this with loadEnabledMods().
464 funkin.modding.PolymodHandler.loadAllMods();
465
466 // Reload scripted classes so stages and modules will update.
467 Polymod.registerAllScriptClasses();
468
469 // Reload everything that is cached.
470 // Currently this freezes the game for a second but I guess that's tolerable?
471
472 // TODO: Reload event callbacks
473
474 // These MUST be imported at the top of the file and not referred to by fully qualified name,
475 // to ensure build macros work properly.
476 SongEventRegistry.loadEventCache();
477
478 SongRegistry.instance.loadEntries();
479 LevelRegistry.instance.loadEntries();
480 NoteStyleRegistry.instance.loadEntries();
481 PlayerRegistry.instance.loadEntries();
482 ConversationRegistry.instance.loadEntries();
483 DialogueBoxRegistry.instance.loadEntries();
484 SpeakerRegistry.instance.loadEntries();
485 AlbumRegistry.instance.loadEntries();
486 StageRegistry.instance.loadEntries();
487 StickerRegistry.instance.loadEntries();
488
489 CharacterDataParser.loadCharacterCache(); // TODO: Migrate characters to BaseRegistry.
490 NoteKindManager.loadScripts();
491 ModuleHandler.loadModuleCache();
492 }
493}