Mirror for Friday Night Funkin
at main 493 lines 16 kB view raw
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}