Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 83 additions & 6 deletions source/funkin/modding/PolymodHandler.hx
Original file line number Diff line number Diff line change
Expand Up @@ -591,24 +591,62 @@ 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<ModMetadata>
{
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<ModMetadata>
{
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<ModMetadata>
{
trace('Scanning the mods folder...');

if (modFileSystem == null || force) modFileSystem = buildFileSystem();
var modMetadata:Array<ModMetadata> = [];
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)
{
Expand All @@ -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
Expand Down Expand Up @@ -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<ModMetadata>
{
var modMetadata:Array<ModMetadata> = getAllModsIncludingIncompatible();
var enabledModIds:Array<String> = Save.instance.enabledModIds.value;
var disabledMods:Array<ModMetadata> = 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!
Expand Down
23 changes: 23 additions & 0 deletions source/funkin/ui/modmenu/ModMenuItem.hx
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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.
*/
Expand Down Expand Up @@ -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()})';
Expand Down
23 changes: 19 additions & 4 deletions source/funkin/ui/modmenu/ModMenuState.hx
Original file line number Diff line number Diff line change
Expand Up @@ -2089,7 +2089,7 @@ class ModMenuState extends MusicBeatState

function refreshModList(doFade:Bool = true):Array<ModMenuItem>
{
PolymodHandler.getAllMods(true);
PolymodHandler.getAllModsIncludingIncompatible(true);
itemsInFolder = FileUtil.readDir(PolymodHandler.MOD_FOLDER);

tempDisabledMods = disabledModItems.modItems.map((item) -> item.mod);
Expand Down Expand Up @@ -2161,7 +2161,7 @@ class ModMenuState extends MusicBeatState

function buildDisabledModList():Array<ModMenuItem>
{
var disabledMods:Array<ModMetadata> = PolymodHandler.getDisabledMods();
var disabledMods:Array<ModMetadata> = PolymodHandler.getDisabledModsIncludingIncompatible();
var newModId:Array<String> = [];

var liveIds:Array<String> = disabledMods.map((m) -> m.id).concat(PolymodHandler.getEnabledMods().map((m) -> m.id));
Expand Down Expand Up @@ -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);
Expand All @@ -2223,9 +2224,9 @@ class ModMenuState extends MusicBeatState

function buildEnabledModList():Void
{
var enabledMods:Array<ModMetadata> = PolymodHandler.getEnabledMods();
var enabledMods:Array<ModMetadata> = PolymodHandler.getEnabledMods().filter((m) -> PolymodHandler.isModCompatible(m));

var liveIds:Array<String> = enabledMods.map((m) -> m.id).concat(PolymodHandler.getDisabledMods().map((m) -> m.id));
var liveIds:Array<String> = enabledMods.map((m) -> m.id).concat(PolymodHandler.getDisabledModsIncludingIncompatible().map((m) -> m.id));

if (tempDisabledMods.length > 0 || tempEnabledMods.length > 0)
{
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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<ModMenuItem>, moveUp:Bool):Void
{
if (modItem == null) return;
Expand Down
Loading