From fdcc23d346fb338b3950ae80320145f6f3fe68e1 Mon Sep 17 00:00:00 2001 From: kiry vajiravuth Date: Sat, 15 Aug 2026 13:06:38 +0700 Subject: [PATCH] grey out mods that are incompatible with the current API version --- source/funkin/modding/PolymodHandler.hx | 89 ++++++++++++++++++++++-- source/funkin/ui/modmenu/ModMenuItem.hx | 23 ++++++ source/funkin/ui/modmenu/ModMenuState.hx | 23 ++++-- 3 files changed, 125 insertions(+), 10 deletions(-) diff --git a/source/funkin/modding/PolymodHandler.hx b/source/funkin/modding/PolymodHandler.hx index 974132dcfa2..689c3b400c7 100644 --- a/source/funkin/modding/PolymodHandler.hx +++ b/source/funkin/modding/PolymodHandler.hx @@ -591,11 +591,37 @@ class PolymodHandler /** * Retrieve a list of metadata for ALL installed mods, including disabled mods. + * Mods that are incompatible with the current API version are excluded. * * @param force Force the game to reload the list of mods from the file system. * @return An array of mod metadata */ public static function getAllMods(force:Bool = false):Array + { + return scanMods(false, force); + } + + /** + * Retrieve a list of metadata for ALL installed mods, including disabled mods, + * WITHOUT filtering out mods that are incompatible with the current API version. + * Used by the Mod Menu so players can see why a mod won't load. + * + * @param force Force the game to reload the list of mods from the file system. + * @return An array of mod metadata + */ + public static function getAllModsIncludingIncompatible(force:Bool = false):Array + { + return scanMods(true, force); + } + + /** + * Scan the mods folder, optionally returning mods that are incompatible with the current API version. + * + * @param includeIncompatible Whether to return mods that don't satisfy `API_VERSION_RULE`. + * @param force Force the game to reload the list of mods from the file system. + * @return An array of mod metadata + */ + static function scanMods(includeIncompatible:Bool, force:Bool):Array { trace('Scanning the mods folder...'); @@ -603,12 +629,24 @@ class PolymodHandler var modMetadata:Array = []; try { - modMetadata = Polymod.scan({ - modRoot: MOD_FOLDER, - apiVersionRule: API_VERSION_RULE, - fileSystem: modFileSystem, - errorCallback: PolymodErrorHandler.onPolymodError - }); + // Omit the API version rule so incompatible mods show up in the scan result. + if (includeIncompatible) + { + modMetadata = Polymod.scan({ + modRoot: MOD_FOLDER, + fileSystem: modFileSystem, + errorCallback: PolymodErrorHandler.onPolymodError + }); + } + else + { + modMetadata = Polymod.scan({ + modRoot: MOD_FOLDER, + apiVersionRule: API_VERSION_RULE, + fileSystem: modFileSystem, + errorCallback: PolymodErrorHandler.onPolymodError + }); + } } catch (e:Dynamic) { @@ -619,6 +657,18 @@ class PolymodHandler return modMetadata; } + /** + * Check whether a mod is compatible with the current API version. + * + * @param mod The mod metadata to check. + * @return Whether the mod satisfies `API_VERSION_RULE`. + */ + public static function isModCompatible(mod:ModMetadata):Bool + { + if (mod == null) return true; + return mod.isCompatible(API_VERSION_RULE); + } + /** * Retrieve a list of ALL mod IDs, including disabled mods. * @return An array of mod IDs @@ -719,6 +769,33 @@ class PolymodHandler return disabledMods; } + /** + * Retrieve a list of metadata for all disabled mods, + * including mods that are incompatible with the current API version. + * Incompatible mods are sorted to the bottom of the list. + * @return An array of mod metadata, in alphabetical order by mod title. + */ + public static function getDisabledModsIncludingIncompatible():Array + { + var modMetadata:Array = getAllModsIncludingIncompatible(); + var enabledModIds:Array = Save.instance.enabledModIds.value; + var disabledMods:Array = modMetadata.filter((item) -> + { + return !enabledModIds.contains(item.id); + }); + + // Sort the mods by alphabetical mod title, pushing incompatible mods to the bottom. + disabledMods.sort((a, b) -> + { + var aCompatible:Bool = isModCompatible(a); + var bCompatible:Bool = isModCompatible(b); + if (aCompatible != bCompatible) return aCompatible ? -1 : 1; + return SortUtil.alphabetically(a.title, b.title); + }); + + return disabledMods; + } + /** * Clear and reload from disk all data assets, synchronously. * Useful for "hot reloading" for fast iteration! diff --git a/source/funkin/ui/modmenu/ModMenuItem.hx b/source/funkin/ui/modmenu/ModMenuItem.hx index 3364fe15c55..a7225d63ff9 100644 --- a/source/funkin/ui/modmenu/ModMenuItem.hx +++ b/source/funkin/ui/modmenu/ModMenuItem.hx @@ -10,6 +10,7 @@ import flixel.math.FlxRect; import funkin.Paths; import flixel.math.FlxMath; import flixel.tweens.FlxEase; +import lime.app.Application; /** * Represents an installed mod visually in the mod menu. @@ -29,6 +30,12 @@ class ModMenuItem extends FunkinSpriteGroup */ public var locked:Bool = false; + /** + * Whether the mod this item represents is compatible with the current API version. + * Incompatible mods are shown greyed out and cannot be enabled. + */ + public var compatible:Bool = true; + /** * The metadata for the mod this item represents. */ @@ -417,6 +424,22 @@ class ModMenuItem extends FunkinSpriteGroup return mod != null ? mod.modVersion : null; } + /** + * Marks this item as incompatible with the current API version. + * Greys out the item and replaces the description with a warning. + */ + public function setIncompatible():Void + { + this.compatible = false; + + titleText.color = FlxColor.GRAY; + descriptionText.color = FlxColor.RED; + descriptionText.setFormat(funkin.assets.Paths.font('ui/fonts/FunkinLingLong', 'otf'), 16, 0xFFFF0000); + descriptionText.localAlpha = 1.0; + descriptionText.text = 'INCOMPATIBLE WITH v${Application.current.meta.get('version')}\nCURRENT MOD VERSION v${mod?.apiVersion.toString() ?? 'UNKNOWN'}'; + if (modIcon != null) modIcon.localAlpha = 0.5; + } + override public function toString():String { return 'ModMenuItem(${getModId()})'; diff --git a/source/funkin/ui/modmenu/ModMenuState.hx b/source/funkin/ui/modmenu/ModMenuState.hx index b6e493e81ac..cba68f5a8ea 100644 --- a/source/funkin/ui/modmenu/ModMenuState.hx +++ b/source/funkin/ui/modmenu/ModMenuState.hx @@ -2089,7 +2089,7 @@ class ModMenuState extends MusicBeatState function refreshModList(doFade:Bool = true):Array { - PolymodHandler.getAllMods(true); + PolymodHandler.getAllModsIncludingIncompatible(true); itemsInFolder = FileUtil.readDir(PolymodHandler.MOD_FOLDER); tempDisabledMods = disabledModItems.modItems.map((item) -> item.mod); @@ -2161,7 +2161,7 @@ class ModMenuState extends MusicBeatState function buildDisabledModList():Array { - var disabledMods:Array = PolymodHandler.getDisabledMods(); + var disabledMods:Array = PolymodHandler.getDisabledModsIncludingIncompatible(); var newModId:Array = []; var liveIds:Array = disabledMods.map((m) -> m.id).concat(PolymodHandler.getEnabledMods().map((m) -> m.id)); @@ -2213,6 +2213,7 @@ class ModMenuState extends MusicBeatState if (disabledModItems.modItems.exists((it) -> it.getModId() == mod.id)) continue; var item = new ModMenuItem(mod); + if (!PolymodHandler.isModCompatible(mod)) item.setIncompatible(); item.localAlpha = 0; disabledModItems.addModRawWithoutLayout(item, disabledModItems.modItems.length); newItems.push(item); @@ -2223,9 +2224,9 @@ class ModMenuState extends MusicBeatState function buildEnabledModList():Void { - var enabledMods:Array = PolymodHandler.getEnabledMods(); + var enabledMods:Array = PolymodHandler.getEnabledMods().filter((m) -> PolymodHandler.isModCompatible(m)); - var liveIds:Array = enabledMods.map((m) -> m.id).concat(PolymodHandler.getDisabledMods().map((m) -> m.id)); + var liveIds:Array = enabledMods.map((m) -> m.id).concat(PolymodHandler.getDisabledModsIncludingIncompatible().map((m) -> m.id)); if (tempDisabledMods.length > 0 || tempEnabledMods.length > 0) { @@ -2316,6 +2317,11 @@ class ModMenuState extends MusicBeatState if (item == null) return false; if (!disabledModItems.modItems.contains(item)) return false; if (item.getModId() == BASE_GAME_MOD_ID) return false; + if (!PolymodHandler.isModCompatible(item.mod)) + { + blockedIncompatible(item); + return false; + } item.selected = false; @@ -2520,6 +2526,15 @@ class ModMenuState extends MusicBeatState }); } + /** + * Gives visual feedback when the player tries to enable an incompatible mod. + */ + function blockedIncompatible(item:ModMenuItem):Void + { + FunkinSound.playOnce(Paths.sound('ui/quick-panel/sounds/menu-deny'), 0.7); + item.flashBackground(); + } + function orderMod(modItem:Null, moveUp:Bool):Void { if (modItem == null) return;