Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
c1b0897
Make hardcoded text translatable
StarmanMine142 Sep 26, 2026
47dee80
Update ModListScreen.java
StarmanMine142 Sep 26, 2026
ac46e10
Update ModListWidget.java
StarmanMine142 Sep 26, 2026
5cc4e9b
Update EntryListWidget.java
StarmanMine142 Sep 26, 2026
454d334
Update DescriptionListWidget.java
StarmanMine142 Sep 27, 2026
a126746
Update ModListEntry.java
StarmanMine142 Sep 27, 2026
c5010ea
Update ModListScreen.java
StarmanMine142 Sep 27, 2026
f10187b
Update en_US.lang
StarmanMine142 Sep 27, 2026
6cfd00c
Update ModListScreen.java
StarmanMine142 Sep 27, 2026
bb6fde2
Update HardcodedUtil.java
StarmanMine142 Sep 28, 2026
2648d1a
Update ModListScreen.java
StarmanMine142 Sep 28, 2026
df9918a
Update DescriptionListWidget.java
StarmanMine142 Sep 28, 2026
6ace480
Update HardcodedUtil.java
StarmanMine142 Sep 28, 2026
d105283
Update DescriptionListWidget.java
StarmanMine142 Oct 3, 2026
eabac7f
Update en_US.lang
StarmanMine142 Oct 3, 2026
594828d
Update ModListScreen.java
StarmanMine142 Oct 4, 2026
1a7f705
remove harcoded util
StarmanMine142 Oct 4, 2026
83b4a6f
Update en_US.lang
StarmanMine142 Oct 4, 2026
2f6d126
Update DescriptionListWidget.java
StarmanMine142 Oct 4, 2026
41adc87
Update ModListEntry.java
StarmanMine142 Oct 4, 2026
c8be18f
Update ModMenuConfig.java
StarmanMine142 Oct 4, 2026
f21ff5b
Update ModListSearch.java
StarmanMine142 Oct 4, 2026
72f175f
Update ModListEntry.java
StarmanMine142 Oct 4, 2026
7bbaffe
Update ModMenu.java
StarmanMine142 Oct 4, 2026
78a0e6a
Update en_US.lang
StarmanMine142 Oct 4, 2026
e327f4d
Update ModMenuConfig.java
StarmanMine142 Oct 5, 2026
8586917
Update en_US.lang
StarmanMine142 Oct 5, 2026
cbc3647
remove unique badge
StarmanMine142 Oct 5, 2026
eec369a
Search mod description
StarmanMine142 Oct 5, 2026
8dee600
remove unnecessary @SuppressWarnings
StarmanMine142 Oct 5, 2026
95c351e
Update ModMenuTexturedButtonWidget.java
StarmanMine142 Oct 8, 2026
a832445
Update ModListScreen.java
StarmanMine142 Oct 9, 2026
b235e3b
Update ModListScreen.java
StarmanMine142 Oct 9, 2026
b0c2ab6
Update en_US.lang
StarmanMine142 Oct 9, 2026
9851998
Update DescriptionListWidget.java
StarmanMine142 Oct 9, 2026
5192a04
Update en_US.lang
StarmanMine142 Oct 9, 2026
1922445
Add view credits, links for bta
StarmanMine142 Oct 9, 2026
2c94314
update buttons
StarmanMine142 Oct 9, 2026
6b480ef
add config screen
StarmanMine142 Oct 9, 2026
3ff9207
Not too important, not too unimportant.
StarmanMine142 Oct 9, 2026
2118c7d
more options
StarmanMine142 Oct 9, 2026
952123a
langs
StarmanMine142 Oct 9, 2026
7e7dc0e
unknown_icon.png
StarmanMine142 Oct 9, 2026
60ffabe
more
StarmanMine142 Oct 10, 2026
0e7a757
Update ModMenuConfigScreen.java
StarmanMine142 Oct 10, 2026
1474f08
I don't know why, but is better
StarmanMine142 Oct 10, 2026
961d68b
old smallest not commit
StarmanMine142 Oct 10, 2026
493cbd0
show mods and libs
StarmanMine142 Oct 12, 2026
35091e0
ukrainian
StarmanMine142 Oct 12, 2026
982d3d6
update checker and api
StarmanMine142 Oct 12, 2026
bf7218d
new
StarmanMine142 Oct 14, 2026
6a2bac8
newest
StarmanMine142 Oct 14, 2026
7eb94b9
summary
StarmanMine142 Oct 15, 2026
595f46d
fix parents and children
StarmanMine142 Oct 15, 2026
39815ad
roles and bta update check
StarmanMine142 Oct 16, 2026
a7cb351
russian and fix \n in entry
StarmanMine142 Oct 16, 2026
03583ee
now your chance to be a
StarmanMine142 Oct 16, 2026
c6932ec
bettering
StarmanMine142 Oct 17, 2026
9ff2529
it's not 59th commit
StarmanMine142 Oct 18, 2026
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
156 changes: 126 additions & 30 deletions src/main/java/io/github/prospector/modmenu/ModMenu.java
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
package io.github.prospector.modmenu;


import com.google.gson.FieldNamingPolicy;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import io.github.prospector.modmenu.api.ModMenuApi;
import io.github.prospector.modmenu.config.ModMenuConfig;
import io.github.prospector.modmenu.config.ModMenuConfigManager;
import io.github.prospector.modmenu.util.HardcodedUtil;
import net.fabricmc.api.ModInitializer;
import io.github.prospector.modmenu.util.TestModContainer;
import io.github.prospector.modmenu.util.UpdateCheckerUtil;
import io.github.prospector.modmenu.util.mod.FabricMod;
import io.github.prospector.modmenu.util.mod.Mod;
import net.fabricmc.api.ClientModInitializer;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import net.fabricmc.loader.api.metadata.CustomValue;
Expand All @@ -17,15 +20,18 @@
import net.minecraft.client.gui.options.ScreenOptions;
import net.minecraft.client.gui.options.data.OptionsPages;
import net.minecraft.core.data.registry.Registries;
import net.minecraft.core.lang.I18n;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.text.NumberFormat;
import java.util.*;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Function;

public class ModMenu implements ModInitializer {
public class ModMenu implements ClientModInitializer {
public static final String MOD_ID = "modmenu";
public static final String GITHUB_REF = "Turnip-Labs/ModMenu";
static {
Registries.NAMESPACES.register(MOD_ID, MOD_ID);
}
Expand All @@ -35,11 +41,19 @@ public class ModMenu implements ModInitializer {
private static final Map<String, Runnable> LEGACY_CONFIG_SCREEN_TASKS = new HashMap<>();
public static final List<String> LIBRARY_MODS = new ArrayList<>();
public static final Set<String> CLIENTSIDE_MODS = new HashSet<>();
public static final Set<String> DEPRECATED_MODS = new HashSet<>();
public static final Set<String> DEPRECATED_MODS = new HashSet<>();
public static final Set<String> PATCHWORK_FORGE_MODS = new HashSet<>();
public static final Map<String, Map<String, Map.Entry<Integer, Integer>>> CUSTOM_BADGE_MODS = new HashMap<>();
public static final Map<String, Map<String, Map.Entry<Integer, Integer>>> CUSTOM_BADGE_MODS = new HashMap<>();
public static final Map<ModContainer, List<ModContainer>> PARENT_MAP = new HashMap<>();
private static Map<String, Function<Screen, ? extends Screen>> configScreenFactories = new HashMap<>();
private static final Map<String, Function<Screen, ? extends Screen>> configScreenFactories = new HashMap<>();
public static final Map<String, Mod> MODS = new ConcurrentHashMap<>();
public static final boolean DEV_ENVIRONMENT = FabricLoader.getInstance().isDevelopmentEnvironment();

private static int cachedDisplayedModCount = -1;

public static void clearModCountCache() {
cachedDisplayedModCount = -1;
}

public static boolean hasConfigScreenFactory(String modid) {
return configScreenFactories.containsKey(modid);
Expand All @@ -55,6 +69,72 @@ public static void openConfigScreen(String modid) {
if (opener != null) opener.run();
}

public static String getDisplayedModCount() {
if (cachedDisplayedModCount == -1) {
boolean includeChildren = ModMenuConfig.COUNT_CHILDREN.getValue();
boolean includeLibraries = ModMenuConfig.COUNT_LIBRARIES.getValue();
boolean includeHidden = ModMenuConfig.COUNT_HIDDEN_MODS.getValue();

long count = MODS.values().stream().filter(mod -> {
boolean isChild = mod.getParent() != null;
if (!includeChildren && isChild) {
return false;
}

boolean isLibrary = ModMenu.LIBRARY_MODS.contains(mod.getId());
if (!includeLibraries && isLibrary) {
return false;
}

return includeHidden || !mod.isHidden();
}).count();

if (Boolean.getBoolean("modmenu.debug")) {
count += TestModContainer.getTestModContainers().size();
}

cachedDisplayedModCount = Math.toIntExact(count);
}
return NumberFormat.getInstance().format(cachedDisplayedModCount);
}

public static String createModsButtonText(boolean title) {
var titleStyle = ModMenuConfig.MODS_BUTTON_STYLE.getValue();
var gameMenuStyle = ModMenuConfig.GAME_MENU_BUTTON_STYLE.getValue();

boolean isIcon = title ?
titleStyle == ModMenuConfig.TitleMenuButtonStyle.ICON :
gameMenuStyle == ModMenuConfig.GameMenuButtonStyle.ICON;

boolean isShort = title ?
titleStyle == ModMenuConfig.TitleMenuButtonStyle.INSERT :
gameMenuStyle == ModMenuConfig.GameMenuButtonStyle.INSERT;

I18n i18n = I18n.getInstance();
String modsText = i18n.translateKey("modmenu.title");

if (ModMenuConfig.MOD_COUNT_LOCATION.getValue().isOnModsButton() && !isIcon) {
String count = getDisplayedModCount();
if (!isShort) {
modsText += " " + i18n.translateKeyAndFormat("modmenu.loaded", count);
} else {
String specificKey = "modmenu.loaded." + count;
String key;

String nKey = i18n.translateKey(specificKey);
boolean hasSpecific = !nKey.equals(specificKey);
key = hasSpecific ? specificKey : "modmenu.loaded";

String secretKey = specificKey + ".secret";
if (ModMenuConfig.EASTER_EGGS.getValue() && !i18n.translateKey(secretKey).equals(secretKey)) {
key = secretKey;
}
modsText += " " + i18n.translateKeyAndFormat(key, count);
}
}
return modsText;
}

public static void addLegacyConfigScreenTask(String modid, Runnable task) {
LEGACY_CONFIG_SCREEN_TASKS.putIfAbsent(modid, task);
}
Expand All @@ -69,29 +149,35 @@ public static void addLibraryMod(String modid) {
LIBRARY_MODS.add(modid);
}

@SuppressWarnings("RedundantCollectionOperation")
@Override
public void onInitialize() {
public void onInitializeClient() {
ModMenuConfigManager.initializeConfig();
FabricLoader.getInstance().getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> {
FabricLoader loader = FabricLoader.getInstance();
loader.getEntrypointContainers("modmenu", ModMenuApi.class).forEach(entrypoint -> {
ModMenuApi api = entrypoint.getEntrypoint();
ModContainer mod = entrypoint.getProvider();
try {
api.getClass().getDeclaredMethod("getConfigScreenFactory"); // Make sure the method is implemented
configScreenFactories.put(mod.getMetadata().getId(), api.getConfigScreenFactory());
} catch (NoSuchMethodException ignored) {}
api.attachCustomBadges((name, outlineColor, fillColor) -> {
Map<String, Map.Entry<Integer, Integer>> map = new HashMap<>();
map.put(name, new AbstractMap.SimpleEntry<>(outlineColor, fillColor));
CUSTOM_BADGE_MODS.put(mod.getMetadata().getId(), map);
});
});
try {
api.getClass().getDeclaredMethod("getModConfigScreenFactory");
var factory = api.getModConfigScreenFactory();
if (factory != null) {
configScreenFactories.put(mod.getMetadata().getId(), factory::create);
}
} catch (NoSuchMethodException ignored) {}
api.attachCustomBadges((name, outlineColor, fillColor) -> {
Map<String, Map.Entry<Integer, Integer>> map = new HashMap<>();
map.put(name, new AbstractMap.SimpleEntry<>(outlineColor, fillColor));
CUSTOM_BADGE_MODS.put(mod.getMetadata().getId(), map);
});
});
configScreenFactories.put("minecraft", (screenBase -> new ScreenOptions(screenBase, OptionsPages.GENERAL)));
Collection<ModContainer> mods = FabricLoader.getInstance().getAllMods();
HardcodedUtil.initializeHardcodings();
Collection<ModContainer> mods = loader.getAllMods();
for (ModContainer mod : mods) {
ModMetadata metadata = mod.getMetadata();
String id = metadata.getId();

MODS.put(id, new FabricMod(mod));
UpdateCheckerUtil.checkForUpdates();

// API badges
if (metadata.containsCustomValue("modmenu:api") && metadata.getCustomValue("modmenu:api").getAsBoolean()) {
addLibraryMod(id);
Expand All @@ -107,9 +193,9 @@ public void onInitialize() {
}

// Deprecated badges
if (metadata.containsCustomValue("modmenu:deprecated") && metadata.getCustomValue("modmenu:deprecated").getAsBoolean()) {
DEPRECATED_MODS.add(id);
}
if (metadata.containsCustomValue("modmenu:deprecated") && metadata.getCustomValue("modmenu:deprecated").getAsBoolean()) {
DEPRECATED_MODS.add(id);
}

// Patchwork (unused)
if (metadata.containsCustomValue("patchwork:source") && metadata.getCustomValue("patchwork:source").getAsObject() != null) {
Expand All @@ -123,19 +209,29 @@ public void onInitialize() {
if (metadata.containsCustomValue("modmenu:parent")) {
String parentId = metadata.getCustomValue("modmenu:parent").getAsString();
if (parentId != null) {
Optional<ModContainer> parent = FabricLoader.getInstance().getModContainer(parentId);
Optional<ModContainer> parent = loader.getModContainer(parentId);
parent.ifPresent(modContainer -> {
final List<ModContainer> list = PARENT_MAP.computeIfAbsent(modContainer, k -> new ArrayList<>());
list.add(mod);
});
}
} else {
HardcodedUtil.hardcodeModuleMetadata(mod, metadata, id);
}

// Hardcode marking as library
if (id.equals("fabricloader")
|| id.equals("java")
|| id.equals("mixinextras")) {
addLibraryMod(id);
}
}
}

public static String getFormattedModCount() {
return NumberFormat.getInstance().format(FabricLoader.getInstance().getAllMods().size());
public static boolean areModUpdatesAvailable() {
for (Mod mod : MODS.values()) {
if (mod.getUpdateInfo() != null && mod.getUpdateInfo().isUpdateAvailable()) {
return true;
}
}
return false;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package io.github.prospector.modmenu.api;

import net.minecraft.client.gui.Screen;

@FunctionalInterface
public interface ConfigScreenFactory<S extends Screen> {
S create(Screen parent);
}
97 changes: 59 additions & 38 deletions src/main/java/io/github/prospector/modmenu/api/ModMenuApi.java
Original file line number Diff line number Diff line change
@@ -1,67 +1,88 @@
package io.github.prospector.modmenu.api;


import io.github.prospector.modmenu.ModMenu;
import io.github.prospector.modmenu.gui.ModsScreen;
import io.github.prospector.modmenu.util.NullScreenFactory;
import io.github.prospector.modmenu.util.TriConsumer;
import net.minecraft.client.gui.Screen;
import org.jetbrains.annotations.ApiStatus;

import java.util.Optional;
import java.util.function.Function;
import java.util.function.Supplier;
import java.util.Map;

@SuppressWarnings("unused")
public interface ModMenuApi {
/**
* Replaced with {@link ModMenuApi#getConfigScreen(Screen)}, with
* the ModMenuApi implemented onto a class that is added as an
* entry point to your fabric mod metadata.
* Used for creating a {@link Screen} instance of the Mod Menu
* "Mods" screen
*
* @deprecated Will be removed in 1.15 snapshots.
* @param previous The screen before opening
* @return A "Mods" Screen
*/
@Deprecated
static void addConfigOverride(String modid, Runnable action) {
ModMenu.addLegacyConfigScreenTask(modid, action);
static Screen createModsScreen(Screen previous) {
return new ModsScreen(previous);
}

/**
* Used to determine the owner of this API implementation.
* Used for creating text just like what would appear
* on a Mod Menu Mods button
*
* @deprecated This method is deprecated as Fabric Loader
* itself provides provider info for entrypoints.
* @return The text that would be displayed on a Mods button
*/
@Deprecated
String getModId();
static String createModsButtonText() {
return ModMenu.createModsButtonText(true);
}

/**
* Replaced with {@link ModMenuApi#getConfigScreenFactory()}, which
* now allows ModMenu to open the screen for you, rather than depending
* on you to open it, and gets rid of the messy Optional->Supplier wrapping.
* Used to construct a new config screen instance when your mod's
* configuration button is selected on the mod menu screen. The
* screen instance parameter is the active mod menu screen.
*
* @deprecated For internal use only.
* @return A factory for constructing config screen instances.
*/
@Deprecated
@ApiStatus.Internal
default Optional<Supplier<Screen>> getConfigScreen(Screen screen) {
return Optional.empty();
default ConfigScreenFactory<?> getModConfigScreenFactory() {
return new NullScreenFactory<>();
}

/**
* Used to construct a new config screen instance when your mod's
* configuration button is selected on the mod menu screen. The
* screen instance parameter is the active mod menu screen.
* Used for mods that have their own update checking logic.
* By returning your own {@link UpdateChecker} instance, you can override ModMenus built-in update checking logic.
*
* @return A factory function for constructing config screen instances.
* @return An {@link UpdateChecker} or <code>null</code> if ModMenu should handle update checking.
*/
default Function<Screen, ? extends Screen> getConfigScreenFactory() {
return screen -> getConfigScreen(screen).map(Supplier::get).orElse(null);
default UpdateChecker getUpdateChecker() {
return null;
}

/**
* Used to provide config screen factories for other mods. This takes second
* priority to a mod's own config screen factory provider. For example, if
* mod `xyz` supplies a config screen factory, mod `abc` providing a config
* screen to `xyz` will be pointless, as the one provided by `xyz` will be
* used.
* <p>
* This method is NOT meant to be used to add a config screen factory to
* your own mod.
*
* @return a map of mod ids to screen factories.
*/
default Map<String, ConfigScreenFactory<?>> getProvidedConfigScreenFactories() {
return Map.of();
}

/**
* Used to provide update checkers for other mods. A mod registering its own
* update checker will take priority over any provided ones should both exist.
*
* @return a map of mod ids to update checkers.
*/
default Map<String, UpdateChecker> getProvidedUpdateCheckers() {
return Map.of();
}

/**
* Add custom badges for mods to use.
*
* @param consumer the consumer used to add new badges.
*/
default void attachCustomBadges(TriConsumer<String, Integer, Integer> consumer) {
}
/**
* Add custom badges for mods to use.
*
* @param consumer the consumer used to add new badges.
*/
default void attachCustomBadges(TriConsumer<String, Integer, Integer> consumer) {
}
}
19 changes: 19 additions & 0 deletions src/main/java/io/github/prospector/modmenu/api/UpdateChannel.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package io.github.prospector.modmenu.api;

import io.github.prospector.modmenu.config.ModMenuConfig;

/**
* Supported update channels, in ascending order by stability.
*/
public enum UpdateChannel {
ALPHA,
BETA,
RELEASE;

/**
* @return the user's preferred update channel.
*/
public static UpdateChannel getUserPreference() {
return ModMenuConfig.UPDATE_CHANNEL.getValue();
}
}
Loading