diff --git a/assets b/assets index 55c602f2a..14b86f436 160000 --- a/assets +++ b/assets @@ -1 +1 @@ -Subproject commit 55c602f2adbbd84de541ea86e5e646c4d2a1df0b +Subproject commit 14b86f4369fddf61eb76116139eb33fa8f6e92d0 diff --git a/hmm.json b/hmm.json index 836b01c9b..cbd5fea30 100644 --- a/hmm.json +++ b/hmm.json @@ -146,7 +146,7 @@ "name": "polymod", "type": "git", "dir": null, - "ref": "d5a3b8995f64d20b95f844454e8c3b38c3d3a9fa", + "ref": "be712450e5d3ba446008884921bb56873b299a64", "url": "https://github.com/larsiusprime/polymod" }, { diff --git a/source/funkin/data/level/LevelData.hx b/source/funkin/data/level/LevelData.hx index 843389cae..f5e58ae16 100644 --- a/source/funkin/data/level/LevelData.hx +++ b/source/funkin/data/level/LevelData.hx @@ -17,7 +17,7 @@ typedef LevelData = var version:String; /** - * The title of the week, as seen in the top corner. + * The title of the level, as seen in the top corner. */ var name:String; @@ -27,21 +27,35 @@ typedef LevelData = @:jcustomparse(funkin.data.DataParse.stringNotEmpty) var titleAsset:String; + /** + * The props to display over the colored background. + * In the base game this is usually Boyfriend and the opponent. + */ @:default([]) var props:Array; - @:default(["bopeebo"]) + + /** + * The list of song IDs included in this level. + */ + @:default(['bopeebo']) var songs:Array; - @:default("#F9CF51") + + /** + * The background for the level behind the props. + */ + @:default('#F9CF51') @:optional var background:String; } +/** + * Data for a single prop for a story mode level. + */ typedef LevelPropData = { /** * The image to use for the prop. May optionally be a sprite sheet. */ - // @:jcustomparse(funkin.data.DataParse.stringNotEmpty) var assetPath:String; /** diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 889f63073..b1c6b511a 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -1,24 +1,25 @@ package funkin.modding; -import funkin.util.macro.ClassMacro; -import funkin.modding.module.ModuleHandler; -import funkin.data.song.SongData; -import funkin.data.stage.StageData; -import polymod.Polymod; -import polymod.backends.PolymodAssets.PolymodAssetType; -import polymod.format.ParseRules.TextFileFormat; -import funkin.data.event.SongEventRegistry; -import funkin.data.stage.StageRegistry; -import funkin.util.FileUtil; -import funkin.data.level.LevelRegistry; -import funkin.data.notestyle.NoteStyleRegistry; import funkin.data.dialogue.ConversationRegistry; import funkin.data.dialogue.DialogueBoxRegistry; import funkin.data.dialogue.SpeakerRegistry; +import funkin.data.event.SongEventRegistry; +import funkin.data.level.LevelRegistry; +import funkin.data.notestyle.NoteStyleRegistry; +import funkin.data.song.SongRegistry; +import funkin.data.stage.StageRegistry; +import funkin.modding.module.ModuleHandler; import funkin.play.character.CharacterData.CharacterDataParser; import funkin.save.Save; -import funkin.data.song.SongRegistry; +import funkin.util.FileUtil; +import funkin.util.macro.ClassMacro; +import polymod.backends.PolymodAssets.PolymodAssetType; +import polymod.format.ParseRules.TextFileFormat; +import polymod.Polymod; +/** + * A class for interacting with Polymod, the atomic modding framework for Haxe. + */ class PolymodHandler { /** @@ -27,16 +28,33 @@ class PolymodHandler * Bug fixes increment the patch version, new features increment the minor version. * Changes that break old mods increment the major version. */ - static final API_VERSION:String = "0.1.0"; + static final API_VERSION:String = '0.1.0'; /** * Where relative to the executable that mods are located. */ - static final MOD_FOLDER:String = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../example_mods" #elseif REDIRECT_ASSETS_FOLDER "../../../../example_mods" #else "mods" #end; + static final MOD_FOLDER:String = + #if (REDIRECT_ASSETS_FOLDER && macos) + '../../../../../../../example_mods' + #elseif REDIRECT_ASSETS_FOLDER + '../../../../example_mods' + #else + 'mods' + #end; - static final CORE_FOLDER:Null = #if (REDIRECT_ASSETS_FOLDER && macos) "../../../../../../../assets" #elseif REDIRECT_ASSETS_FOLDER "../../../../assets" #else null #end; + static final CORE_FOLDER:Null = + #if (REDIRECT_ASSETS_FOLDER && macos) + '../../../../../../../assets' + #elseif REDIRECT_ASSETS_FOLDER + '../../../../assets' + #else + null + #end; - public static function createModRoot() + /** + * If the mods folder doesn't exist, create it. + */ + public static function createModRoot():Void { FileUtil.createDirIfNotExists(MOD_FOLDER); } @@ -44,40 +62,44 @@ class PolymodHandler /** * Loads the game with ALL mods enabled with Polymod. */ - public static function loadAllMods() + public static function loadAllMods():Void { // Create the mod root if it doesn't exist. createModRoot(); - trace("Initializing Polymod (using all mods)..."); + trace('Initializing Polymod (using all mods)...'); loadModsById(getAllModIds()); } /** * Loads the game with configured mods enabled with Polymod. */ - public static function loadEnabledMods() + public static function loadEnabledMods():Void { // Create the mod root if it doesn't exist. createModRoot(); - trace("Initializing Polymod (using configured mods)..."); + trace('Initializing Polymod (using configured mods)...'); loadModsById(Save.instance.enabledModIds); } /** * Loads the game without any mods enabled with Polymod. */ - public static function loadNoMods() + public static function loadNoMods():Void { // Create the mod root if it doesn't exist. createModRoot(); // We still need to configure the debug print calls etc. - trace("Initializing Polymod (using no mods)..."); + trace('Initializing Polymod (using no mods)...'); loadModsById([]); } - public static function loadModsById(ids:Array) + /** + * Load all the mods with the given ids. + * @param ids The ORDERED list of mod ids to load. + */ + public static function loadModsById(ids:Array):Void { if (ids.length == 0) { @@ -90,7 +112,7 @@ class PolymodHandler buildImports(); - var loadedModList = polymod.Polymod.init( + var loadedModList:Array = polymod.Polymod.init( { // Root directory for all mods. modRoot: MOD_FOLDER, @@ -142,30 +164,40 @@ class PolymodHandler } #if debug - var fileList = Polymod.listModFiles(PolymodAssetType.IMAGE); + var fileList:Array = Polymod.listModFiles(PolymodAssetType.IMAGE); trace('Installed mods have replaced ${fileList.length} images.'); for (item in fileList) + { trace(' * $item'); + } fileList = Polymod.listModFiles(PolymodAssetType.TEXT); trace('Installed mods have added/replaced ${fileList.length} text files.'); for (item in fileList) + { trace(' * $item'); + } fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_MUSIC); trace('Installed mods have replaced ${fileList.length} music files.'); for (item in fileList) + { trace(' * $item'); + } fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_SOUND); trace('Installed mods have replaced ${fileList.length} sound files.'); for (item in fileList) + { trace(' * $item'); + } fileList = Polymod.listModFiles(PolymodAssetType.AUDIO_GENERIC); trace('Installed mods have replaced ${fileList.length} generic audio files.'); for (item in fileList) + { trace(' * $item'); + } #end } @@ -183,21 +215,21 @@ class PolymodHandler for (cls in ClassMacro.listClassesInPackage('polymod')) { if (cls == null) continue; - var className = Type.getClassName(cls); + var className:String = Type.getClassName(cls); Polymod.blacklistImport(className); } } static function buildParseRules():polymod.format.ParseRules { - var output = polymod.format.ParseRules.getDefault(); + var output:polymod.format.ParseRules = polymod.format.ParseRules.getDefault(); // Ensure TXT files have merge support. - output.addType("txt", TextFileFormat.LINES); + output.addType('txt', TextFileFormat.LINES); // Ensure script files have merge support. - output.addType("hscript", TextFileFormat.PLAINTEXT); - output.addType("hxs", TextFileFormat.PLAINTEXT); - output.addType("hxc", TextFileFormat.PLAINTEXT); - output.addType("hx", TextFileFormat.PLAINTEXT); + output.addType('hscript', TextFileFormat.PLAINTEXT); + output.addType('hxs', TextFileFormat.PLAINTEXT); + output.addType('hxc', TextFileFormat.PLAINTEXT); + output.addType('hx', TextFileFormat.PLAINTEXT); // You can specify the format of a specific file, with file extension. // output.addFile("data/introText.txt", TextFileFormat.LINES) @@ -208,17 +240,21 @@ class PolymodHandler { return { assetLibraryPaths: [ - "default" => "preload", "shared" => "shared", "songs" => "songs", "tutorial" => "tutorial", "week1" => "week1", "week2" => "week2", - "week3" => "week3", "week4" => "week4", "week5" => "week5", "week6" => "week6", "week7" => "week7", "weekend1" => "weekend1", + 'default' => 'preload', 'shared' => 'shared', 'songs' => 'songs', 'tutorial' => 'tutorial', 'week1' => 'week1', 'week2' => 'week2', + 'week3' => 'week3', 'week4' => 'week4', 'week5' => 'week5', 'week6' => 'week6', 'week7' => 'week7', 'weekend1' => 'weekend1', ], coreAssetRedirect: CORE_FOLDER, } } + /** + * Retrieve a list of metadata for ALL installed mods, including disabled mods. + * @return An array of mod metadata + */ public static function getAllMods():Array { trace('Scanning the mods folder...'); - var modMetadata = Polymod.scan( + var modMetadata:Array = Polymod.scan( { modRoot: MOD_FOLDER, apiVersionRule: API_VERSION, @@ -228,17 +264,25 @@ class PolymodHandler return modMetadata; } + /** + * Retrieve a list of ALL mod IDs, including disabled mods. + * @return An array of mod IDs + */ public static function getAllModIds():Array { - var modIds = [for (i in getAllMods()) i.id]; + var modIds:Array = [for (i in getAllMods()) i.id]; return modIds; } + /** + * Retrieve a list of metadata for all enabled mods. + * @return An array of mod metadata + */ public static function getEnabledMods():Array { - var modIds = Save.instance.enabledModIds; - var modMetadata = getAllMods(); - var enabledMods = []; + var modIds:Array = Save.instance.enabledModIds; + var modMetadata:Array = getAllMods(); + var enabledMods:Array = []; for (item in modMetadata) { if (modIds.indexOf(item.id) != -1) @@ -249,7 +293,11 @@ class PolymodHandler return enabledMods; } - public static function forceReloadAssets() + /** + * Clear and reload from disk all data assets. + * Useful for "hot reloading" for fast iteration! + */ + public static function forceReloadAssets():Void { // Forcibly clear scripts so that scripts can be edited. ModuleHandler.clearModuleCache(); diff --git a/source/funkin/ui/story/Level.hx b/source/funkin/ui/story/Level.hx index b548b7b1e..626fb8e52 100644 --- a/source/funkin/ui/story/Level.hx +++ b/source/funkin/ui/story/Level.hx @@ -51,6 +51,7 @@ class Level implements IRegistryEntry /** * Retrieve the title of the level for display on the menu. + * @return Title of the level as a string */ public function getTitle():String { @@ -58,16 +59,21 @@ class Level implements IRegistryEntry return _data.name; } + /** + * Construct the title graphic for the level. + * @return The constructed graphic as a sprite. + */ public function buildTitleGraphic():FlxSprite { - var result = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset)); + var result:FlxSprite = new FlxSprite().loadGraphic(Paths.image(_data.titleAsset)); return result; } /** * Get the list of songs in this level, as an array of names, for display on the menu. - * @return Array + * @param difficulty The difficulty of the level being displayed + * @return The display names of the songs in this level */ public function getSongDisplayNames(difficulty:String):Array { @@ -88,7 +94,9 @@ class Level implements IRegistryEntry /** * Whether this level is unlocked. If not, it will be greyed out on the menu and have a lock icon. - * TODO: Change this behavior in a later release. + * Override this in a script. + * @default `true` + * @return Whether this level is unlocked */ public function isUnlocked():Bool { @@ -97,6 +105,9 @@ class Level implements IRegistryEntry /** * Whether this level is visible. If not, it will not be shown on the menu at all. + * Override this in a script. + * @default `true` + * @return Whether this level is visible in the menu */ public function isVisible():Bool { @@ -106,6 +117,7 @@ class Level implements IRegistryEntry /** * Build a sprite for the background of the level. * Can be overriden by ScriptedLevel. Not used if `isBackgroundSimple` returns true. + * @return The constructed sprite */ public function buildBackground():FlxSprite { @@ -124,6 +136,7 @@ class Level implements IRegistryEntry /** * Returns true if the background is a solid color. * If you have a ScriptedLevel with a fancy background, you may want to override this to false. + * @return Whether the background is a simple color */ public function isBackgroundSimple():Bool { @@ -133,30 +146,36 @@ class Level implements IRegistryEntry /** * Returns true if the background is a solid color. * If you have a ScriptedLevel with a fancy background, you may want to override this to false. + * @return The background as a simple color. May not be valid if `isBackgroundSimple` returns false. */ public function getBackgroundColor():FlxColor { return FlxColor.fromString(_data.background); } + /** + * The list of difficulties the player can select from for this level. + * @return The difficulty IDs. + */ public function getDifficulties():Array { var difficulties:Array = []; - var songList = getSongs(); + var songList:Array = getSongs(); var firstSongId:String = songList[0]; var firstSong:Song = SongRegistry.instance.fetchEntry(firstSongId); if (firstSong != null) { - // Don't display alternate characters in Story Mode. - for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, "erect"])) + // Don't display alternate characters in Story Mode. Only show `default` and `erect` variations. + for (difficulty in firstSong.listDifficulties([Constants.DEFAULT_VARIATION, 'erect'])) { difficulties.push(difficulty); } } + // Sort in a specific order! Fall back to alphabetical. difficulties.sort(SortUtil.defaultsThenAlphabetically.bind(Constants.DEFAULT_DIFFICULTY_LIST)); // Filter to only include difficulties that are present in all songs @@ -169,7 +188,7 @@ class Level implements IRegistryEntry for (difficulty in difficulties) { - if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, "erect"])) + if (!song.hasDifficulty(difficulty, [Constants.DEFAULT_VARIATION, 'erect'])) { difficulties.remove(difficulty); } @@ -181,6 +200,11 @@ class Level implements IRegistryEntry return difficulties; } + /** + * Build the props for display over the colored background. + * @param existingProps The existing prop sprites, if any. + * @return The constructed prop sprites + */ public function buildProps(?existingProps:Array):Array { var props:Array = existingProps == null ? [] : [for (x in existingProps) x]; @@ -189,11 +213,13 @@ class Level implements IRegistryEntry var hiddenProps:Array = props.splice(_data.props.length - 1, props.length - 1); for (hiddenProp in hiddenProps) + { hiddenProp.visible = false; + } for (propIndex in 0..._data.props.length) { - var propData = _data.props[propIndex]; + var propData:LevelPropData = _data.props[propIndex]; // Attempt to reuse the `LevelProp` object. // This prevents animations from resetting. @@ -224,6 +250,10 @@ class Level implements IRegistryEntry return props; } + /** + * Called when the level is destroyed. + * TODO: Document when this gets called + */ public function destroy():Void {} public function toString():String @@ -231,6 +261,11 @@ class Level implements IRegistryEntry return 'Level($id)'; } + /** + * Retrieve and parse the JSON data for a level by ID. + * @param id The ID of the level + * @return The parsed level data, or null if not found or invalid + */ static function _fetchData(id:String):Null { return LevelRegistry.instance.parseEntryDataWithMigration(id, LevelRegistry.instance.fetchEntryVersion(id));