├── .gitignore ├── LICENSE ├── README.md ├── build.gradle ├── common ├── build.gradle └── src │ └── main │ ├── java │ └── eu │ │ └── midnightdust │ │ ├── core │ │ ├── MidnightLib.java │ │ ├── config │ │ │ └── MidnightLibConfig.java │ │ ├── mixin │ │ │ └── MixinOptionsScreen.java │ │ └── screen │ │ │ └── MidnightConfigOverviewScreen.java │ │ └── lib │ │ ├── config │ │ ├── AutoCommand.java │ │ └── MidnightConfig.java │ │ └── util │ │ ├── MidnightColorUtil.java │ │ └── PlatformFunctions.java │ └── resources │ ├── architectury.common.json │ ├── assets │ └── midnightlib │ │ ├── icon.png │ │ ├── lang │ │ ├── de_de.json │ │ ├── en_us.json │ │ ├── es_ar.json │ │ ├── fr_fr.json │ │ ├── ms_my.json │ │ ├── pt_br.json │ │ ├── ru_ru.json │ │ ├── tt_ru.json │ │ ├── uk_ua.json │ │ ├── zh_cn.json │ │ ├── zh_tw.json │ │ └── zlm_arab.json │ │ └── textures │ │ └── gui │ │ └── sprites │ │ └── icon │ │ ├── explorer.png │ │ ├── midnightlib.png │ │ └── reset.png │ └── midnightlib.mixins.json ├── fabric ├── build.gradle └── src │ └── main │ ├── java │ └── eu │ │ └── midnightdust │ │ ├── fabric │ │ └── core │ │ │ └── MidnightLibFabric.java │ │ └── lib │ │ ├── config │ │ └── AutoModMenu.java │ │ └── util │ │ └── fabric │ │ └── PlatformFunctionsImpl.java │ └── resources │ └── fabric.mod.json ├── gradle.properties ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── neoforge ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── java │ └── eu │ │ └── midnightdust │ │ ├── lib │ │ └── util │ │ │ └── neoforge │ │ │ └── PlatformFunctionsImpl.java │ │ └── neoforge │ │ └── MidnightLibNeoForge.java │ └── resources │ ├── META-INF │ └── neoforge.mods.toml │ └── midnightlib.png ├── quilt ├── build.gradle ├── gradle.properties └── src │ └── main │ ├── java │ └── eu │ │ └── midnightdust │ │ ├── lib │ │ └── util │ │ │ └── fabric │ │ │ └── PlatformFunctionsImpl.java │ │ └── quilt │ │ └── core │ │ ├── MidnightLibClientQuilt.java │ │ └── MidnightLibServerQuilt.java │ └── resources │ └── quilt.mod.json ├── settings.gradle └── test-fabric ├── build.gradle └── src └── main ├── java └── eu │ └── midnightdust │ └── fabric │ └── example │ ├── MLExampleFabric.java │ ├── MidnightLibExtras.java │ └── config │ └── MidnightConfigExample.java └── resources ├── assets └── modid │ └── lang │ ├── en_us.json │ └── es_ar.json └── fabric.mod.json /.gitignore: -------------------------------------------------------------------------------- 1 | build/ 2 | *.ipr 3 | run/ 4 | *.iws 5 | out/ 6 | *.iml 7 | .gradle/ 8 | output/ 9 | bin/ 10 | libs/ 11 | 12 | .classpath 13 | .project 14 | .idea/ 15 | classes/ 16 | .metadata 17 | .vscode 18 | .settings 19 | *.launch 20 | .architectury-transformer/debug.log 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 MidnightDust 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # MidnightLib 2 | Lightweight Common Library for Minecraft mods. 3 | Provides a config api with a nice GUI and common utilities, all in a very small file. 4 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import groovy.json.JsonSlurper 2 | import groovy.json.JsonOutput 3 | 4 | plugins { 5 | id "architectury-plugin" version "3.4-SNAPSHOT" 6 | id "dev.architectury.loom" version "1.7-SNAPSHOT" apply false 7 | id "me.shedaniel.unified-publishing" version "0.1.+" apply false 8 | id 'com.github.johnrengelman.shadow' version '8.1.1' apply false 9 | } 10 | 11 | architectury { 12 | minecraft = rootProject.minecraft_version 13 | } 14 | 15 | subprojects { 16 | apply plugin: "dev.architectury.loom" 17 | 18 | dependencies { 19 | minecraft "com.mojang:minecraft:${rootProject.minecraft_version}" 20 | // The following line declares the yarn mappings you may select this one as well. 21 | mappings loom.layered { 22 | it.mappings("net.fabricmc:yarn:$rootProject.yarn_mappings:v2") 23 | it.mappings("dev.architectury:yarn-mappings-patch-neoforge:$rootProject.yarn_mappings_patch_neoforge_version") 24 | } 25 | } 26 | } 27 | 28 | allprojects { 29 | apply plugin: "java" 30 | apply plugin: "architectury-plugin" 31 | apply plugin: "maven-publish" 32 | 33 | archivesBaseName = rootProject.archives_base_name 34 | version = rootProject.mod_version 35 | group = rootProject.maven_group 36 | 37 | repositories { 38 | // Add repositories to retrieve artifacts from in here. 39 | // You should only use this when depending on other mods because 40 | // Loom adds the essential maven repositories to download Minecraft and libraries from automatically. 41 | // See https://docs.gradle.org/current/userguide/declaring_repositories.html 42 | // for more information about repositories. 43 | } 44 | 45 | tasks.withType(JavaCompile) { 46 | options.encoding = "UTF-8" 47 | options.release = 21 48 | } 49 | ext { 50 | releaseChangelog = { 51 | def changes = new StringBuilder() 52 | changes << "## MidnightLib v$project.version for $project.minecraft_version\n[View the changelog](https://www.github.com/TeamMidnightDust/MidnightLib/commits/)" 53 | def proc = "git log --max-count=1 --pretty=format:%s".execute() 54 | proc.in.eachLine { line -> 55 | def processedLine = line.toString() 56 | if (!processedLine.contains("New translations") && !processedLine.contains("Merge") && !processedLine.contains("branch")) { 57 | changes << "\n- ${processedLine.capitalize()}" 58 | } 59 | } 60 | proc.waitFor() 61 | return changes.toString() 62 | } 63 | } 64 | processResources { 65 | // Minify json resources 66 | doLast { 67 | fileTree(dir: outputs.files.asPath, include: "**/*.json").each { 68 | File file -> file.text = JsonOutput.toJson(new JsonSlurper().parse(file)) 69 | } 70 | } 71 | } 72 | 73 | java { 74 | withSourcesJar() 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /common/build.gradle: -------------------------------------------------------------------------------- 1 | architectury { 2 | common(rootProject.enabled_platforms.split(",")) 3 | } 4 | 5 | dependencies { 6 | // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies 7 | // Do NOT use other classes from fabric loader 8 | modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" 9 | } 10 | 11 | publishing { 12 | publications { 13 | mavenCommon(MavenPublication) { 14 | artifactId = rootProject.archives_base_name 15 | from components.java 16 | } 17 | } 18 | 19 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 20 | repositories { 21 | // Add repositories to publish to here. 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/core/MidnightLib.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.core; 2 | 3 | import eu.midnightdust.core.config.MidnightLibConfig; 4 | import eu.midnightdust.lib.config.AutoCommand; 5 | import eu.midnightdust.lib.config.MidnightConfig; 6 | import net.fabricmc.api.EnvType; 7 | import net.fabricmc.api.Environment; 8 | import org.slf4j.Logger; 9 | import org.slf4j.LoggerFactory; 10 | 11 | import javax.swing.UIManager; 12 | import java.lang.reflect.Field; 13 | import java.util.ArrayList; 14 | import java.util.List; 15 | 16 | import static net.minecraft.client.MinecraftClient.IS_SYSTEM_MAC; 17 | 18 | public class MidnightLib { 19 | public static List hiddenMods = new ArrayList<>(); 20 | public static final String MOD_ID = "midnightlib"; 21 | public static final Logger LOGGER = LoggerFactory.getLogger(MOD_ID); 22 | 23 | @Environment(EnvType.CLIENT) 24 | public static void onInitializeClient() { 25 | try { if (!IS_SYSTEM_MAC) { 26 | System.setProperty("java.awt.headless", "false"); 27 | UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName()); 28 | }} catch (Exception | Error e) { LOGGER.error("Error setting system look and feel", e); } 29 | MidnightLibConfig.init(MOD_ID, MidnightLibConfig.class); 30 | } 31 | public static void registerAutoCommand() { 32 | MidnightConfig.configClass.forEach((modid, config) -> { 33 | for (Field field : config.getFields()) { 34 | if (field.isAnnotationPresent(MidnightConfig.Entry.class) && !field.isAnnotationPresent(MidnightConfig.Client.class) && !field.isAnnotationPresent(MidnightConfig.Hidden.class)) 35 | new AutoCommand(field, modid); 36 | } 37 | }); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/core/config/MidnightLibConfig.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.core.config; 2 | 3 | import eu.midnightdust.lib.config.MidnightConfig; 4 | import eu.midnightdust.lib.util.PlatformFunctions; 5 | 6 | import java.util.Objects; 7 | 8 | public class MidnightLibConfig extends MidnightConfig { 9 | public static final boolean HAS_MODMENU = PlatformFunctions.isModLoaded("modmenu") || Objects.equals(PlatformFunctions.getPlatformName(), "neoforge"); 10 | 11 | @Entry public static ConfigButton config_screen_list = HAS_MODMENU ? ConfigButton.MODMENU : ConfigButton.TRUE; 12 | 13 | public enum ConfigButton { 14 | TRUE, FALSE, MODMENU 15 | } 16 | 17 | public static boolean shouldShowButton() { 18 | return config_screen_list.equals(ConfigButton.TRUE) || (config_screen_list.equals(ConfigButton.MODMENU) && !HAS_MODMENU); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/core/mixin/MixinOptionsScreen.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.core.mixin; 2 | 3 | import eu.midnightdust.core.screen.MidnightConfigOverviewScreen; 4 | import net.minecraft.client.gui.screen.Screen; 5 | import net.minecraft.client.gui.screen.option.OptionsScreen; 6 | import net.minecraft.client.gui.widget.TextIconButtonWidget; 7 | import net.minecraft.client.gui.widget.ThreePartsLayoutWidget; 8 | import net.minecraft.text.Text; 9 | import net.minecraft.util.Identifier; 10 | import org.spongepowered.asm.mixin.Final; 11 | import org.spongepowered.asm.mixin.Mixin; 12 | import org.spongepowered.asm.mixin.Shadow; 13 | import org.spongepowered.asm.mixin.Unique; 14 | import org.spongepowered.asm.mixin.injection.At; 15 | import org.spongepowered.asm.mixin.injection.Inject; 16 | import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; 17 | 18 | import java.util.Objects; 19 | 20 | import static eu.midnightdust.core.MidnightLib.MOD_ID; 21 | import static eu.midnightdust.core.config.MidnightLibConfig.shouldShowButton; 22 | 23 | @Mixin(OptionsScreen.class) 24 | public abstract class MixinOptionsScreen extends Screen { 25 | @Shadow @Final private ThreePartsLayoutWidget layout; 26 | @Unique TextIconButtonWidget midnightlib$button = TextIconButtonWidget.builder(Text.translatable("midnightlib.overview.title"), ( 27 | buttonWidget) -> Objects.requireNonNull(client).setScreen(new MidnightConfigOverviewScreen(this)), true) 28 | .texture(Identifier.of(MOD_ID,"icon/"+MOD_ID), 16, 16).dimension(20, 20).build(); 29 | 30 | private MixinOptionsScreen(Text title) {super(title);} 31 | 32 | @Inject(at = @At("HEAD"), method = "init") 33 | public void midnightlib$onInit(CallbackInfo ci) { 34 | if (shouldShowButton()) { 35 | this.midnightlib$setButtonPos(); 36 | this.addDrawableChild(midnightlib$button); 37 | } 38 | } 39 | 40 | @Inject(at = @At("TAIL"), method = "refreshWidgetPositions") 41 | public void midnightlib$onResize(CallbackInfo ci) { 42 | if (shouldShowButton()) this.midnightlib$setButtonPos(); 43 | } 44 | 45 | @Unique 46 | public void midnightlib$setButtonPos() { 47 | midnightlib$button.setPosition(layout.getWidth() / 2 + 158, layout.getY() + layout.getFooterHeight() - 4); 48 | } 49 | } -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/core/screen/MidnightConfigOverviewScreen.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.core.screen; 2 | 3 | import eu.midnightdust.core.MidnightLib; 4 | import eu.midnightdust.lib.config.MidnightConfig; 5 | import net.fabricmc.api.EnvType; 6 | import net.fabricmc.api.Environment; 7 | import net.minecraft.client.gui.DrawContext; 8 | import net.minecraft.client.gui.screen.Screen; 9 | import net.minecraft.client.gui.widget.ButtonWidget; 10 | import net.minecraft.screen.ScreenTexts; 11 | import net.minecraft.text.Text; 12 | 13 | import java.util.ArrayList; 14 | import java.util.Collections; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | import static eu.midnightdust.lib.config.MidnightConfig.MidnightConfigListWidget; 19 | 20 | @Environment(EnvType.CLIENT) 21 | public class MidnightConfigOverviewScreen extends Screen { 22 | 23 | public MidnightConfigOverviewScreen(Screen parent) { 24 | super(Text.translatable( "midnightlib.overview.title")); 25 | this.parent = parent; 26 | } 27 | private final Screen parent; 28 | private MidnightConfigListWidget list; 29 | 30 | @Override 31 | protected void init() { 32 | this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, (button) -> Objects.requireNonNull(client).setScreen(parent)).dimensions(this.width / 2 - 100, this.height - 26, 200, 20).build()); 33 | 34 | this.addSelectableChild(this.list = new MidnightConfigListWidget(this.client, this.width, this.height - 57, 24, 25)); 35 | List sortedMods = new ArrayList<>(MidnightConfig.configClass.keySet()); 36 | Collections.sort(sortedMods); 37 | sortedMods.forEach((modid) -> { 38 | if (!MidnightLib.hiddenMods.contains(modid)) { 39 | list.addButton(List.of(ButtonWidget.builder(Text.translatable(modid +".midnightconfig.title"), (button) -> 40 | Objects.requireNonNull(client).setScreen(MidnightConfig.getScreen(this, modid))).dimensions(this.width / 2 - 125, this.height - 28, 250, 20).build()), null, null); 41 | }}); 42 | super.init(); 43 | } 44 | @Override 45 | public void render(DrawContext context, int mouseX, int mouseY, float delta) { 46 | super.render(context, mouseX, mouseY, delta); 47 | this.list.render(context, mouseX, mouseY, delta); 48 | context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 10, 0xFFFFFF); 49 | } 50 | } -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/lib/config/AutoCommand.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.config; 2 | 3 | import com.mojang.brigadier.arguments.*; 4 | import com.mojang.brigadier.context.CommandContext; 5 | import eu.midnightdust.lib.util.PlatformFunctions; 6 | import net.minecraft.server.command.CommandManager; 7 | import net.minecraft.server.command.ServerCommandSource; 8 | import net.minecraft.text.Text; 9 | 10 | import java.lang.reflect.Field; 11 | import java.util.List; 12 | import java.util.Objects; 13 | 14 | import static eu.midnightdust.lib.config.MidnightConfig.Entry; 15 | 16 | public class AutoCommand { 17 | final static String VALUE = "value"; 18 | final Field field; 19 | final Class type; 20 | final String modid; 21 | final boolean isList; 22 | 23 | public AutoCommand(Field field, String modid) { 24 | this.field = field; this.modid = modid; 25 | this.type = MidnightConfig.getUnderlyingType(field); 26 | this.isList = field.getType() == List.class; 27 | 28 | var command = CommandManager.literal(field.getName()).executes(this::getValue); 29 | 30 | if (type.isEnum()) { 31 | for (Object enumValue : field.getType().getEnumConstants()) 32 | command = command.then(CommandManager.literal(enumValue.toString()) 33 | .executes(ctx -> this.setValue(ctx.getSource(), enumValue, ""))); 34 | } else if (isList) { 35 | for (String action : new String[]{"add", "remove"}) 36 | command = command.then(CommandManager.literal(action) 37 | .then(CommandManager.argument(VALUE, getArgType()).executes(ctx -> setValueFromArg(ctx, action)))); 38 | } else command = command.then(CommandManager.argument(VALUE, getArgType()).executes(ctx -> setValueFromArg(ctx, ""))); 39 | 40 | PlatformFunctions.registerCommand(CommandManager.literal("midnightconfig").requires(source -> source.hasPermissionLevel(2)).then(CommandManager.literal(modid).then(command))); 41 | } 42 | 43 | public ArgumentType getArgType() { 44 | Entry entry = field.getAnnotation(Entry.class); 45 | if (type == int.class) return IntegerArgumentType.integer((int) entry.min(), (int) entry.max()); 46 | else if (type == double.class) return DoubleArgumentType.doubleArg(entry.min(), entry.max()); 47 | else if (type == float.class) return FloatArgumentType.floatArg((float) entry.min(), (float) entry.max()); 48 | else if (type == boolean.class) return BoolArgumentType.bool(); 49 | return StringArgumentType.string(); 50 | } 51 | 52 | public int setValueFromArg(CommandContext context, String action) { 53 | if (type == int.class) return setValue(context.getSource(), IntegerArgumentType.getInteger(context, VALUE), action); 54 | else if (type == double.class) return setValue(context.getSource(), DoubleArgumentType.getDouble(context, VALUE), action); 55 | else if (type == float.class) return setValue(context.getSource(), FloatArgumentType.getFloat(context, VALUE), action); 56 | else if (type == boolean.class) return setValue(context.getSource(), BoolArgumentType.getBool(context, VALUE), action); 57 | return setValue(context.getSource(), StringArgumentType.getString(context, VALUE), action); 58 | } 59 | private int setValue(ServerCommandSource source, Object value, String action) { 60 | boolean add = Objects.equals(action, "add"); 61 | try { 62 | if (!isList) field.set(null, value); 63 | else { 64 | @SuppressWarnings("unchecked") var list = (List) field.get(null); 65 | if (add) list.add(value); 66 | else if (!list.contains(value)) throw new IllegalArgumentException("List does not contain this string!"); 67 | else list.remove(value); 68 | } 69 | MidnightConfig.write(modid); 70 | } 71 | catch (Exception e) { 72 | source.sendError(Text.literal(isList ? "Could not %s %s %s %s: %s".formatted(add ? "add" : "remove", value, add ? "to" : "from", field.getName(), e) : "Could not set %s to value %s: %s".formatted(field.getName(), value, e))); 73 | return 0; 74 | } 75 | source.sendFeedback(() -> Text.literal(isList ? "Successfully %s %s %s %s".formatted(add ? "added" : "removed", value, add ? "to" : "from", field.getName()) : 76 | "Successfully set %s to %s".formatted(field.getName(), value)), true); 77 | return 1; 78 | } 79 | private int getValue(CommandContext context) { 80 | context.getSource().sendFeedback(() -> { 81 | try { return Text.literal("The value of %s is %s".formatted(field.getName(), field.get(null))); 82 | } catch (IllegalAccessException e) {throw new RuntimeException(e);} 83 | }, true); 84 | return 0; 85 | } 86 | } -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/lib/config/MidnightConfig.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.config; 2 | 3 | import com.google.common.collect.Lists; 4 | import com.google.gson.*; import com.google.gson.stream.*; 5 | import eu.midnightdust.lib.util.PlatformFunctions; 6 | import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; 7 | import net.minecraft.client.MinecraftClient; import net.minecraft.client.font.TextRenderer; import net.minecraft.client.gui.DrawContext; 8 | import net.minecraft.client.gui.Element; import net.minecraft.client.gui.Selectable; 9 | import net.minecraft.client.gui.screen.ConfirmLinkScreen; import net.minecraft.client.gui.screen.Screen; 10 | import net.minecraft.client.gui.tab.GridScreenTab; import net.minecraft.client.gui.tab.Tab; import net.minecraft.client.gui.tab.TabManager; 11 | import net.minecraft.client.gui.tooltip.Tooltip; import net.minecraft.client.gui.widget.*; 12 | import net.minecraft.client.render.RenderLayer; 13 | import net.minecraft.client.resource.language.I18n; 14 | import net.minecraft.registry.Registries; 15 | import net.minecraft.screen.ScreenTexts; 16 | import net.minecraft.text.Style; import net.minecraft.text.Text; 17 | import net.minecraft.text.TranslatableTextContent; 18 | import net.minecraft.util.Formatting; import net.minecraft.util.Identifier; 19 | import net.minecraft.util.TranslatableOption; 20 | import org.jetbrains.annotations.Nullable; 21 | 22 | import javax.swing.*; import javax.swing.filechooser.FileNameExtensionFilter; 23 | import java.awt.Color; 24 | import java.io.IOException; 25 | import java.lang.annotation.*; 26 | import java.lang.reflect.Field; import java.lang.reflect.Modifier; import java.lang.reflect.ParameterizedType; 27 | import java.nio.file.Files; import java.nio.file.Path; 28 | import java.util.*; 29 | import java.util.function.BiFunction; import java.util.function.Function; import java.util.function.Predicate; 30 | import java.util.regex.Pattern; 31 | 32 | import static net.minecraft.client.MinecraftClient.IS_SYSTEM_MAC; 33 | 34 | /** MidnightConfig by Martin "Motschen" Prokoph 35 | * Single class config library - feel free to copy! 36 | * Based on ... 37 | * Credits to Minenash */ 38 | 39 | @SuppressWarnings("unchecked") 40 | public abstract class MidnightConfig { 41 | private static final Pattern INTEGER_ONLY = Pattern.compile("(-?[0-9]*)"); 42 | private static final Pattern DECIMAL_ONLY = Pattern.compile("-?(\\d+\\.?\\d*|\\d*\\.?\\d+|\\.)"); 43 | private static final Pattern HEXADECIMAL_ONLY = Pattern.compile("(-?[#0-9a-fA-F]*)"); 44 | 45 | private static final LinkedHashMap entries = new LinkedHashMap<>(); // modid:fieldName -> EntryInfo 46 | private static boolean reloadScreen = false; 47 | 48 | public static class EntryInfo { 49 | public Entry entry; 50 | public Comment comment; 51 | public Condition[] conditions; 52 | public final Field field; 53 | public final Class dataType; 54 | public final String modid, fieldName; 55 | int listIndex; 56 | Object defaultValue, value, function; 57 | String tempValue; // The value visible in the config screen 58 | boolean inLimits = true; 59 | Text name, error; 60 | ClickableWidget actionButton; // color picker button / explorer button 61 | Tab tab; 62 | boolean conditionsMet = true; 63 | 64 | public EntryInfo(Field field, String modid) { 65 | this.field = field; this.modid = modid; 66 | if (field != null) { 67 | this.fieldName = field.getName(); 68 | this.dataType = getUnderlyingType(field); 69 | this.entry = field.getAnnotation(Entry.class); 70 | this.comment = field.getAnnotation(Comment.class); 71 | this.conditions = field.getAnnotationsByType(Condition.class); 72 | } else { this.fieldName = ""; this.dataType = null; } 73 | if (entry != null && !entry.name().isEmpty()) this.name = Text.translatable(entry.name()); 74 | else if (comment != null && !comment.name().isEmpty()) this.name = Text.translatable(comment.name()); 75 | } 76 | public void setValue(Object value) { 77 | if (this.field.getType() != List.class) { this.value = value; 78 | this.tempValue = value.toString(); 79 | } else { writeList(this.listIndex, value); 80 | this.tempValue = toTemporaryValue(); } 81 | } 82 | public String toTemporaryValue() { 83 | if (this.field.getType() != List.class) return this.value.toString(); 84 | else try { return ((List) this.value).get(this.listIndex).toString(); } catch (Exception ignored) {return "";} 85 | } 86 | public void updateFieldValue() { 87 | try { 88 | if (this.field.get(null) != value) entries.values().forEach(EntryInfo::updateConditions); 89 | this.field.set(null, this.value); 90 | } catch (IllegalAccessException ignored) {} 91 | } 92 | @SuppressWarnings("ConstantValue") //pertains to requiredModLoaded 93 | public void updateConditions() { 94 | boolean prevConditionState = this.conditionsMet; 95 | if (this.conditions.length > 0) this.conditionsMet = true; // reset conditions 96 | for (Condition condition : this.conditions) { 97 | if (!condition.requiredModId().isEmpty() && !PlatformFunctions.isModLoaded(condition.requiredModId())) 98 | this.conditionsMet = false; 99 | String requiredOption = condition.requiredOption().contains(":") ? condition.requiredOption() : (this.modid + ":" + condition.requiredOption()); 100 | if (entries.get(requiredOption) instanceof EntryInfo info) 101 | this.conditionsMet &= List.of(condition.requiredValue()).contains(info.tempValue); 102 | if (!this.conditionsMet) break; 103 | } 104 | if (prevConditionState != this.conditionsMet) reloadScreen = true; 105 | } 106 | public void writeList(int index, T value) { 107 | var list = (List) this.value; 108 | if (index >= list.size()) list.add(value); 109 | else list.set(index, value); 110 | } 111 | public Tooltip getTooltip(boolean isButton) { 112 | String key = this.modid + ".midnightconfig."+this.fieldName+(!isButton ? ".label" : "" )+".tooltip"; 113 | return Tooltip.of(isButton && this.error != null ? this.error : I18n.hasTranslation(key) ? Text.translatable(key) : Text.empty()); 114 | } 115 | } 116 | 117 | public static final Map> configClass = new HashMap<>(); 118 | private static Path path; 119 | 120 | private static final Gson gson = new GsonBuilder() 121 | .excludeFieldsWithModifiers(Modifier.TRANSIENT).excludeFieldsWithModifiers(Modifier.PRIVATE) 122 | .addSerializationExclusionStrategy(new NonEntryExclusionStrategy()) 123 | .registerTypeAdapter(Identifier.class, new TypeAdapter() { 124 | public void write(JsonWriter out, Identifier id) throws IOException { out.value(id.toString()); } 125 | public Identifier read(JsonReader in) throws IOException { return Identifier.of(in.nextString()); } 126 | }).setPrettyPrinting().create(); 127 | 128 | public static void loadValuesFromJson(String modid) { 129 | try { gson.fromJson(Files.newBufferedReader(path), configClass.get(modid)); } 130 | catch (Exception e) { write(modid); } 131 | entries.values().forEach(info -> { 132 | if (info.field != null && info.entry != null) { 133 | try { 134 | info.value = info.field.get(null); 135 | info.tempValue = info.toTemporaryValue(); 136 | info.updateConditions(); 137 | } catch (IllegalAccessException ignored) {} 138 | } 139 | }); 140 | } 141 | public static void init(String modid, Class config) { 142 | path = PlatformFunctions.getConfigDirectory().resolve(modid + ".json"); 143 | configClass.put(modid, config); 144 | 145 | for (Field field : config.getFields()) { 146 | EntryInfo info = new EntryInfo(field, modid); 147 | if ((field.isAnnotationPresent(Entry.class) || field.isAnnotationPresent(Comment.class)) && !field.isAnnotationPresent(Server.class) && !field.isAnnotationPresent(Hidden.class) && PlatformFunctions.isClientEnv()) 148 | initClient(modid, field, info); 149 | if (field.isAnnotationPresent(Entry.class)) 150 | try { info.defaultValue = field.get(null); 151 | } catch (IllegalAccessException ignored) {} 152 | } 153 | loadValuesFromJson(modid); 154 | } 155 | @Environment(EnvType.CLIENT) 156 | private static void initClient(String modid, Field field, EntryInfo info) { 157 | Entry e = info.entry; 158 | String key = modid + ":" + field.getName(); 159 | if (e != null) { 160 | if (info.dataType == int.class) textField(info, Integer::parseInt, INTEGER_ONLY, (int) e.min(), (int) e.max(), true); 161 | else if (info.dataType == float.class) textField(info, Float::parseFloat, DECIMAL_ONLY, (float) e.min(), (float) e.max(), false); 162 | else if (info.dataType == double.class) textField(info, Double::parseDouble, DECIMAL_ONLY, e.min(), e.max(), false); 163 | else if (info.dataType == String.class || info.dataType == Identifier.class) textField(info, String::length, null, Math.min(e.min(), 0), Math.max(e.max(), 1), true); 164 | else if (info.dataType == boolean.class) { 165 | Function func = value -> Text.translatable((Boolean) value ? "gui.yes" : "gui.no").formatted((Boolean) value ? Formatting.GREEN : Formatting.RED); 166 | info.function = new AbstractMap.SimpleEntry>(button -> { 167 | info.setValue(!(Boolean) info.value); button.setMessage(func.apply(info.value)); 168 | }, func); 169 | } else if (info.dataType.isEnum()) { 170 | List values = Arrays.asList(field.getType().getEnumConstants()); 171 | Function func = value -> getEnumTranslatableText(value, modid, info); 172 | info.function = new AbstractMap.SimpleEntry>(button -> { 173 | int index = values.indexOf(info.value) + 1; 174 | info.setValue(values.get(index >= values.size() ? 0 : index)); 175 | button.setMessage(func.apply(info.value)); 176 | }, func); 177 | } 178 | } 179 | entries.put(key, info); 180 | } 181 | public static Class getUnderlyingType(Field field) { 182 | Class rawType = field.getType(); 183 | if (field.getType() == List.class) 184 | rawType = (Class) ((ParameterizedType) field.getGenericType()).getActualTypeArguments()[0]; 185 | try { return (Class) rawType.getField("TYPE").get(null); // Tries to get primitive types from non-primitives (e.g. Boolean -> boolean) 186 | } catch (NoSuchFieldException | IllegalAccessException ignored) { return rawType; } 187 | } 188 | 189 | private static Text getEnumTranslatableText(Object value, String modid, EntryInfo info) { 190 | if (value instanceof TranslatableOption translatableOption) return translatableOption.getText(); 191 | 192 | String translationKey = "%s.midnightconfig.enum.%s.%s".formatted(modid, info.dataType.getSimpleName(), info.toTemporaryValue()); 193 | return I18n.hasTranslation(translationKey) ? Text.translatable(translationKey) : Text.literal(info.toTemporaryValue()); 194 | } 195 | 196 | private static void textField(EntryInfo info, Function f, Pattern pattern, double min, double max, boolean cast) { 197 | boolean isNumber = pattern != null; 198 | info.function = (BiFunction>) (t, b) -> s -> { 199 | s = s.trim(); 200 | if (!(s.isEmpty() || !isNumber || pattern.matcher(s).matches()) || 201 | (info.dataType == Identifier.class && Identifier.validate(s).isError())) return false; 202 | 203 | Number value = 0; boolean inLimits = false; info.error = null; 204 | if (!(isNumber && s.isEmpty()) && !s.equals("-") && !s.equals(".")) { 205 | try { value = f.apply(s); } catch(NumberFormatException e){ return false; } 206 | inLimits = value.doubleValue() >= min && value.doubleValue() <= max; 207 | info.error = inLimits? null : Text.literal(value.doubleValue() < min ? 208 | "§cMinimum " + (isNumber? "value" : "length") + (cast? " is " + (int)min : " is " + min) : 209 | "§cMaximum " + (isNumber? "value" : "length") + (cast? " is " + (int)max : " is " + max)).formatted(Formatting.RED); 210 | t.setTooltip(info.getTooltip(true)); 211 | } 212 | 213 | info.tempValue = s; 214 | t.setEditableColor(inLimits? 0xFFFFFFFF : 0xFFFF7777); 215 | info.inLimits = inLimits; 216 | b.active = entries.values().stream().allMatch(e -> e.inLimits); 217 | 218 | if (inLimits) { 219 | if (info.dataType == Identifier.class) info.setValue(Identifier.tryParse(s)); 220 | else info.setValue(isNumber ? value : s); 221 | } 222 | 223 | if (info.entry.isColor()) { 224 | if (!s.contains("#")) s = '#' + s; 225 | if (!HEXADECIMAL_ONLY.matcher(s).matches()) return false; 226 | try { info.actionButton.setMessage(Text.literal("⬛").setStyle(Style.EMPTY.withColor(Color.decode(info.tempValue).getRGB()))); 227 | } catch (Exception ignored) {} 228 | } 229 | return true; 230 | }; 231 | } 232 | public static MidnightConfig getClass(String modid) { 233 | try { return configClass.get(modid).getDeclaredConstructor().newInstance(); } catch (Exception e) {throw new RuntimeException(e);} 234 | } 235 | public static void write(String modid) { getClass(modid).writeChanges(modid); } 236 | 237 | public void writeChanges(String modid) { 238 | try { if (!Files.exists(path = PlatformFunctions.getConfigDirectory().resolve(modid + ".json"))) Files.createFile(path); 239 | Files.write(path, gson.toJson(getClass(modid)).getBytes()); 240 | } catch (Exception e) { e.fillInStackTrace(); } 241 | } 242 | 243 | @SuppressWarnings("unused") // Utility for mod authors 244 | public static @Nullable Object getDefaultValue(String modid, String entry) { 245 | String key = modid + ":" + entry; 246 | return entries.containsKey(key) ? entries.get(key).defaultValue : null; 247 | } 248 | 249 | public void onTabInit(String tabName, MidnightConfigListWidget list, MidnightConfigScreen screen) {} 250 | 251 | @Environment(EnvType.CLIENT) 252 | public static Screen getScreen(Screen parent, String modid) { 253 | return new MidnightConfigScreen(parent, modid); 254 | } 255 | @Environment(EnvType.CLIENT) 256 | public static class MidnightConfigScreen extends Screen { 257 | protected MidnightConfigScreen(Screen parent, String modid) { 258 | super(Text.translatable(modid + ".midnightconfig.title")); 259 | this.parent = parent; this.modid = modid; 260 | this.translationPrefix = modid + ".midnightconfig."; 261 | loadValuesFromJson(modid); 262 | entries.values().forEach(info -> { 263 | if (info.modid.equals(modid)) { 264 | String tabId = info.entry != null ? info.entry.category() : info.comment.category(); 265 | String name = translationPrefix + "category." + tabId; 266 | if (!I18n.hasTranslation(name) && tabId.equals("default")) 267 | name = translationPrefix + "title"; 268 | if (!tabs.containsKey(name)) { 269 | Tab tab = new GridScreenTab(Text.translatable(name)); 270 | info.tab = tab; tabs.put(name, tab); 271 | } else info.tab = tabs.get(name); 272 | } 273 | }); 274 | tabNavigation = TabNavigationWidget.builder(tabManager, this.width).tabs(tabs.values().toArray(new Tab[0])).build(); 275 | tabNavigation.selectTab(0, false); 276 | tabNavigation.init(); 277 | prevTab = tabManager.getCurrentTab(); 278 | } 279 | public final String translationPrefix, modid; 280 | public final Screen parent; 281 | public MidnightConfigListWidget list; 282 | public TabManager tabManager = new TabManager(a -> {}, a -> {}); 283 | public Map tabs = new LinkedHashMap<>(); 284 | public Tab prevTab; 285 | public TabNavigationWidget tabNavigation; 286 | public ButtonWidget done; 287 | public double scrollProgress = 0d; 288 | 289 | // Real Time config update // 290 | @Override 291 | public void tick() { 292 | super.tick(); 293 | if (prevTab != null && prevTab != tabManager.getCurrentTab()) { 294 | prevTab = tabManager.getCurrentTab(); 295 | updateList(); list.setScrollY(0); 296 | } 297 | scrollProgress = list.getScrollY(); 298 | for (EntryInfo info : entries.values()) info.updateFieldValue(); 299 | updateButtons(); 300 | if (reloadScreen) { updateList(); reloadScreen = false; } 301 | } 302 | public void updateButtons() { 303 | if (this.list != null) { 304 | for (ButtonEntry entry : this.list.children()) { 305 | if (entry.buttons != null && entry.buttons.size() > 1 && entry.info.field != null) { 306 | if (entry.buttons.get(0) instanceof ClickableWidget widget) 307 | if (widget.isFocused() || widget.isHovered()) widget.setTooltip(entry.info.getTooltip(true)); 308 | if (entry.buttons.get(1) instanceof ButtonWidget button) 309 | button.active = !Objects.equals(String.valueOf(entry.info.value), String.valueOf(entry.info.defaultValue)) && entry.info.conditionsMet; 310 | }}}} 311 | @Override 312 | public boolean keyPressed(int keyCode, int scanCode, int modifiers) { 313 | if (this.tabNavigation.trySwitchTabsWithKey(keyCode)) return true; 314 | return super.keyPressed(keyCode, scanCode, modifiers); 315 | } 316 | @Override 317 | public void close() { 318 | loadValuesFromJson(modid); cleanup(); 319 | Objects.requireNonNull(client).setScreen(parent); 320 | } 321 | private void cleanup() { 322 | entries.values().forEach(info -> { 323 | info.error = null; info.value = null; info.tempValue = null; info.actionButton = null; info.listIndex = 0; info.tab = null; info.inLimits = true; 324 | }); 325 | } 326 | @Override 327 | public void init() { 328 | super.init(); 329 | tabNavigation.setWidth(this.width); tabNavigation.init(); 330 | if (tabs.size() > 1) this.addDrawableChild(tabNavigation); 331 | 332 | this.addDrawableChild(ButtonWidget.builder(ScreenTexts.CANCEL, button -> this.close()).dimensions(this.width / 2 - 154, this.height - 26, 150, 20).build()); 333 | done = this.addDrawableChild(ButtonWidget.builder(ScreenTexts.DONE, (button) -> { 334 | for (EntryInfo info : entries.values()) if (info.modid.equals(modid)) info.updateFieldValue(); 335 | write(modid); cleanup(); 336 | Objects.requireNonNull(client).setScreen(parent); 337 | }).dimensions(this.width / 2 + 4, this.height - 26, 150, 20).build()); 338 | 339 | this.list = new MidnightConfigListWidget(this.client, this.width, this.height - 57, 24, 25); 340 | this.addSelectableChild(this.list); fillList(); 341 | if (tabs.size() > 1) list.renderHeaderSeparator = false; 342 | } 343 | public void updateList() { 344 | this.list.clear(); fillList(); 345 | } 346 | public void fillList() { 347 | MidnightConfig.getClass(modid).onTabInit(prevTab.getTitle().getContent() instanceof TranslatableTextContent translatable ? translatable.getKey().replace("%s.midnightconfig.category.".formatted(modid), "") : prevTab.getTitle().toString(), list, this); 348 | for (EntryInfo info : entries.values()) { 349 | info.updateConditions(); 350 | if (!info.conditionsMet) { 351 | boolean visibleButLocked = false; 352 | for (Condition condition : info.conditions) { 353 | visibleButLocked |= condition.visibleButLocked(); 354 | } 355 | if (!visibleButLocked) continue; 356 | } 357 | if (info.modid.equals(modid) && (info.tab == null || info.tab == tabManager.getCurrentTab())) { 358 | Text name = Objects.requireNonNullElseGet(info.name, () -> Text.translatable(translationPrefix + info.fieldName)); 359 | TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> { 360 | info.value = info.defaultValue; info.listIndex = 0; 361 | info.tempValue = info.toTemporaryValue(); 362 | updateList(); 363 | }), true).texture(Identifier.of("midnightlib","icon/reset"), 12, 12).dimension(20, 20).build(); 364 | resetButton.setPosition(width - 205 + 150 + 25, 0); 365 | 366 | if (info.function != null) { 367 | ClickableWidget widget; 368 | Entry e = info.entry; 369 | if (info.function instanceof Map.Entry) { // Enums & booleans 370 | var values = (Map.Entry>) info.function; 371 | if (info.dataType.isEnum()) { 372 | values.setValue(value -> getEnumTranslatableText(value, modid, info)); 373 | } 374 | widget = ButtonWidget.builder(values.getValue().apply(info.value), values.getKey()).dimensions(width - 185, 0, 150, 20).tooltip(info.getTooltip(true)).build(); 375 | } else if (e.isSlider()) 376 | widget = new MidnightSliderWidget(width - 185, 0, 150, 20, Text.of(info.tempValue), (Double.parseDouble(info.tempValue) - e.min()) / (e.max() - e.min()), info); 377 | else widget = new TextFieldWidget(textRenderer, width - 185, 0, 150, 20, Text.empty()); 378 | if (widget instanceof TextFieldWidget textField) { 379 | textField.setMaxLength(e.width()); textField.setText(info.tempValue); 380 | Predicate processor = ((BiFunction>) info.function).apply(textField, done); 381 | textField.setTextPredicate(processor); 382 | } 383 | widget.setTooltip(info.getTooltip(true)); 384 | 385 | ButtonWidget cycleButton = null; 386 | if (info.field.getType() == List.class) { 387 | cycleButton = ButtonWidget.builder(Text.literal(String.valueOf(info.listIndex)).formatted(Formatting.GOLD), (button -> { 388 | var values = (List) info.value; 389 | values.remove(""); 390 | info.listIndex = info.listIndex + 1; 391 | if (info.listIndex > values.size()) info.listIndex = 0; 392 | info.tempValue = info.toTemporaryValue(); 393 | if (info.listIndex == values.size()) info.tempValue = ""; 394 | updateList(); 395 | })).dimensions(width - 185, 0, 20, 20).build(); 396 | } 397 | if (e.isColor()) { 398 | ButtonWidget colorButton = ButtonWidget.builder(Text.literal("⬛"), 399 | button -> new Thread(() -> { 400 | Color newColor = JColorChooser.showDialog(null, Text.translatable("midnightconfig.colorChooser.title").getString(), Color.decode(!Objects.equals(info.tempValue, "") ? info.tempValue : "#FFFFFF")); 401 | if (newColor != null) { 402 | info.setValue("#" + Integer.toHexString(newColor.getRGB()).substring(2)); 403 | updateList(); 404 | } 405 | }).start() 406 | ).dimensions(width - 185, 0, 20, 20).build(); 407 | try { colorButton.setMessage(Text.literal("⬛").setStyle(Style.EMPTY.withColor(Color.decode(info.tempValue).getRGB()))); 408 | } catch (Exception ignored) {} 409 | info.actionButton = colorButton; 410 | } else if (e.selectionMode() > -1) { 411 | ButtonWidget explorerButton = TextIconButtonWidget.builder(Text.empty(), 412 | button -> new Thread(() -> { 413 | JFileChooser fileChooser = new JFileChooser(info.tempValue); 414 | fileChooser.setFileSelectionMode(e.selectionMode()); fileChooser.setDialogType(e.fileChooserType()); 415 | fileChooser.setDialogTitle(Text.translatable(translationPrefix + info.fieldName + ".fileChooser").getString()); 416 | if ((e.selectionMode() == JFileChooser.FILES_ONLY || e.selectionMode() == JFileChooser.FILES_AND_DIRECTORIES) && Arrays.stream(e.fileExtensions()).noneMatch("*"::equals)) 417 | fileChooser.setFileFilter(new FileNameExtensionFilter( 418 | Text.translatable(translationPrefix + info.fieldName + ".fileFilter").getString(), e.fileExtensions())); 419 | if (fileChooser.showDialog(null, null) == JFileChooser.APPROVE_OPTION) { 420 | info.setValue(fileChooser.getSelectedFile().getAbsolutePath()); 421 | updateList(); 422 | } 423 | }).start(), true 424 | ).texture(Identifier.of("midnightlib", "icon/explorer"), 12, 12).dimension(20, 20).build(); 425 | explorerButton.setPosition(width - 185, 0); 426 | info.actionButton = explorerButton; 427 | } 428 | List widgets = Lists.newArrayList(widget, resetButton); 429 | if (info.actionButton != null) { 430 | if (IS_SYSTEM_MAC) info.actionButton.active = false; 431 | widget.setWidth(widget.getWidth() - 22); widget.setX(widget.getX() + 22); 432 | widgets.add(info.actionButton); 433 | } if (cycleButton != null) { 434 | if (info.actionButton != null) info.actionButton.setX(info.actionButton.getX() + 22); 435 | widget.setWidth(widget.getWidth() - 22); widget.setX(widget.getX() + 22); 436 | widgets.add(cycleButton); 437 | } 438 | if (!info.conditionsMet) widgets.forEach(w -> w.active = false); 439 | this.list.addButton(widgets, name, info); 440 | } else this.list.addButton(List.of(), name, info); 441 | } list.setScrollY(scrollProgress); 442 | updateButtons(); 443 | } 444 | } 445 | @Override 446 | public void render(DrawContext context, int mouseX, int mouseY, float delta) { 447 | super.render(context, mouseX, mouseY, delta); 448 | this.list.render(context, mouseX, mouseY, delta); 449 | if (tabs.size() < 2) context.drawCenteredTextWithShadow(textRenderer, title, width / 2, 10, 0xFFFFFF); 450 | } 451 | } 452 | @Environment(EnvType.CLIENT) 453 | public static class MidnightConfigListWidget extends ElementListWidget { 454 | public boolean renderHeaderSeparator = true; 455 | public MidnightConfigListWidget(MinecraftClient client, int width, int height, int y, int itemHeight) { super(client, width, height, y, itemHeight); } 456 | @Override public int getScrollbarX() { return this.width -7; } 457 | 458 | @Override 459 | protected void drawHeaderAndFooterSeparators(DrawContext context) { 460 | if (renderHeaderSeparator) super.drawHeaderAndFooterSeparators(context); 461 | else context.drawTexture(RenderLayer::getGuiTextured, this.client.world == null ? Screen.FOOTER_SEPARATOR_TEXTURE : Screen.INWORLD_FOOTER_SEPARATOR_TEXTURE, this.getX(), this.getBottom(), 0, 0, this.getWidth(), 2, 32, 2); 462 | } 463 | public void addButton(List buttons, Text text, EntryInfo info) { this.addEntry(new ButtonEntry(buttons, text, info)); } 464 | public void clear() { this.clearEntries(); } 465 | @Override public int getRowWidth() { return 10000; } 466 | } 467 | public static class ButtonEntry extends ElementListWidget.Entry { 468 | private static final TextRenderer textRenderer = MinecraftClient.getInstance().textRenderer; 469 | public final Text text; 470 | public final List buttons; 471 | public final EntryInfo info; 472 | public boolean centered = false; 473 | public MultilineTextWidget title; 474 | 475 | public ButtonEntry(List buttons, Text text, EntryInfo info) { 476 | this.buttons = buttons; this.text = text; this.info = info; 477 | if (info != null && info.comment != null) this.centered = info.comment.centered(); 478 | int scaledWidth = MinecraftClient.getInstance().getWindow().getScaledWidth(); 479 | 480 | if (text != null && (!text.getString().contains("spacer") || !buttons.isEmpty())) { 481 | title = new MultilineTextWidget((centered) ? (scaledWidth / 2 - (textRenderer.getWidth(text) / 2)) : 12, 0, Text.of(text), textRenderer); 482 | if (info != null) title.setTooltip(info.getTooltip(false)); 483 | title.setMaxWidth(buttons.size() > 1 ? buttons.get(1).getX() - 24 : scaledWidth - 24); 484 | } 485 | } 486 | public void render(DrawContext context, int index, int y, int x, int entryWidth, int entryHeight, int mouseX, int mouseY, boolean hovered, float tickDelta) { 487 | buttons.forEach(b -> { b.setY(y); b.render(context, mouseX, mouseY, tickDelta);}); 488 | if (title != null) { 489 | title.setY(y+5); 490 | title.renderWidget(context, mouseX, mouseY, tickDelta); 491 | 492 | boolean tooltipVisible = mouseX >= title.getX() && mouseX < title.getWidth() + title.getX() && mouseY >= title.getY() && mouseY < title.getHeight() + title.getY(); 493 | if (tooltipVisible && title.getTooltip() != null) context.drawOrderedTooltip(textRenderer, title.getTooltip().getLines(MinecraftClient.getInstance()), mouseX, mouseY); 494 | 495 | if (info.entry != null && !this.buttons.isEmpty() && this.buttons.getFirst() instanceof ClickableWidget widget) { 496 | int idMode = this.info.entry.idMode(); 497 | if (idMode != -1) context.drawItem(idMode == 0 ? Registries.ITEM.get(Identifier.tryParse(this.info.tempValue)).getDefaultStack() : Registries.BLOCK.get(Identifier.tryParse(this.info.tempValue)).asItem().getDefaultStack(), widget.getX() + widget.getWidth() - 18, y + 2); 498 | } 499 | } 500 | } 501 | 502 | @Override 503 | public boolean mouseClicked(double mouseX, double mouseY, int button) { 504 | if (this.info != null && this.info.comment != null && !this.info.comment.url().isBlank()) 505 | ConfirmLinkScreen.open(MinecraftClient.getInstance().currentScreen, this.info.comment.url(), true); 506 | return super.mouseClicked(mouseX, mouseY, button); 507 | } 508 | 509 | public List children() {return Lists.newArrayList(buttons);} 510 | public List selectableChildren() {return Lists.newArrayList(buttons);} 511 | } 512 | public static class MidnightSliderWidget extends SliderWidget { 513 | private final EntryInfo info; private final Entry e; 514 | public MidnightSliderWidget(int x, int y, int width, int height, Text text, double value, EntryInfo info) { 515 | super(x, y, width, height, text, value); 516 | this.e = info.entry; 517 | this.info = info; 518 | } 519 | 520 | @Override 521 | public void updateMessage() { this.setMessage(Text.of(info.tempValue)); } 522 | 523 | @Override 524 | public void applyValue() { 525 | if (info.dataType == int.class) info.setValue(((Number) (e.min() + value * (e.max() - e.min()))).intValue()); 526 | else if (info.dataType == double.class) info.setValue(Math.round((e.min() + value * (e.max() - e.min())) * (double) e.precision()) / (double) e.precision()); 527 | else if (info.dataType == float.class) info.setValue(Math.round((e.min() + value * (e.max() - e.min())) * (float) e.precision()) / (float) e.precision()); 528 | } 529 | } 530 | public static class NonEntryExclusionStrategy implements ExclusionStrategy { 531 | public boolean shouldSkipClass(Class clazz) { return false; } 532 | public boolean shouldSkipField(FieldAttributes fieldAttributes) { return fieldAttributes.getAnnotation(Entry.class) == null; } 533 | } 534 | 535 | /** 536 | * Entry Annotation
537 | * - width: The maximum character length of the {@link String}, {@link Identifier} or String/Identifier {@link List<>} field
538 | * - min: The minimum value of the int, float or double field
539 | * - max: The maximum value of the int, float or double field
540 | * - name: Will be used instead of the default translation key, if not empty
541 | * - selectionMode: The selection mode of the file picker button for {@link String} fields, 542 | * -1 for none, {@link JFileChooser#FILES_ONLY} for files, {@link JFileChooser#DIRECTORIES_ONLY} for directories, 543 | * {@link JFileChooser#FILES_AND_DIRECTORIES} for both (default: -1). Remember to set the translation key 544 | * [modid].midnightconfig.[fieldName].fileChooser.title for the file picker dialog title
545 | * - fileChooserType: The type of the file picker button for {@link String} fields, 546 | * can be {@link JFileChooser#OPEN_DIALOG} or {@link JFileChooser#SAVE_DIALOG} (default: {@link JFileChooser#OPEN_DIALOG}). 547 | * Remember to set the translation key [modid].midnightconfig.[fieldName].fileFilter.description for the file filter description 548 | * if "*" is not used as file extension
549 | * - fileExtensions: The file extensions for the file picker button for {@link String} fields (default: {"*"}), 550 | * only works if selectionMode is {@link JFileChooser#FILES_ONLY} or {@link JFileChooser#FILES_AND_DIRECTORIES}
551 | * - isColor: If the field is a hexadecimal color code (default: false)
552 | * - isSlider: If the field is a slider (default: false)
553 | * - precision: The precision of the float or double field (default: 100)
554 | * - category: The category of the field in the config screen (default: "default")
555 | * */ 556 | @Retention(RetentionPolicy.RUNTIME) 557 | @Target(ElementType.FIELD) 558 | public @interface Entry { 559 | int width() default 400; 560 | double min() default Double.MIN_NORMAL; 561 | double max() default Double.MAX_VALUE; 562 | String name() default ""; 563 | int selectionMode() default -1; // -1 for none, 0 for file, 1 for directory, 2 for both 564 | int fileChooserType() default JFileChooser.OPEN_DIALOG; 565 | String[] fileExtensions() default {"*"}; 566 | int idMode() default -1; // -1 for none, 0 for item, 1 for block 567 | boolean isColor() default false; 568 | boolean isSlider() default false; 569 | int precision() default 100; 570 | String category() default "default"; 571 | @Deprecated String requiredMod() default ""; 572 | } 573 | 574 | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Client {} 575 | 576 | /** 577 | * Hides the entry in config screens, but still makes it accessible through the command {@code /midnightconfig MOD_ID ENTRY} and directly editing the config file. 578 | */ 579 | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Server {} 580 | 581 | /** 582 | * Hides the entry entirely. 583 | * Accessible only through directly editing the config file. 584 | * Perfect for saving persistent internal data. 585 | */ 586 | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Hidden {} 587 | 588 | /** 589 | * Comment Annotation
590 | * - {@link Comment#centered()}: If the comment should be centered
591 | * - {@link Comment#category()}: The category of the comment in the config screen
592 | * - {@link Comment#name()}: Will be used instead of the default translation key, if not empty
593 | * - {@link Comment#url()}: The url of the comment should link to in the config screen (none if left empty)
594 | * */ 595 | @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.FIELD) public @interface Comment { 596 | boolean centered() default false; 597 | String category() default "default"; 598 | String name() default ""; 599 | String url() default ""; 600 | @Deprecated String requiredMod() default ""; 601 | } 602 | /** 603 | * Condition Annotation
604 | * - {@link Condition#requiredModId()}: The id of a mod that is required to be loaded.
605 | * - {@link Condition#requiredOption()}: The {@link Field} which will be used to check the condition. Can also access options of other MidnightLib mods ("modid:optionName").
606 | * - {@link Condition#requiredValue()}: The value that {@link Condition#requiredOption()} should be set to for the condition to be met.
607 | * - {@link Condition#visibleButLocked()}: The behaviour to take when {@link Condition#requiredModId} is not loaded 608 | * or {@link Condition#requiredOption()} returns a value that is not {@link Condition#requiredValue()}.
609 | * true – Option is visible, but not editable
610 | * false – Option is completely hidden 611 | */ 612 | @Retention(RetentionPolicy.RUNTIME) 613 | @Repeatable(Conditions.class) 614 | @Target(ElementType.FIELD) 615 | public @interface Condition { 616 | String requiredModId() default ""; 617 | String requiredOption() default ""; 618 | String[] requiredValue() default {"true"}; 619 | boolean visibleButLocked() default false; 620 | } 621 | 622 | @Retention(RetentionPolicy.RUNTIME) 623 | @Target(ElementType.FIELD) 624 | public @interface Conditions { 625 | Condition[] value(); 626 | } 627 | } -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/lib/util/MidnightColorUtil.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.util; 2 | 3 | import java.awt.Color; 4 | 5 | public class MidnightColorUtil { 6 | /** 7 | * @param colorStr e.g. "FFFFFF" or "#FFFFFF" 8 | * @return Color as RGB 9 | */ 10 | public static Color hex2Rgb(String colorStr) { 11 | try { return Color.decode("#" + colorStr.replace("#", "")); 12 | } catch (Exception ignored) { return Color.BLACK; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /common/src/main/java/eu/midnightdust/lib/util/PlatformFunctions.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.util; 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 4 | import dev.architectury.injectables.annotations.ExpectPlatform; 5 | import net.minecraft.server.command.ServerCommandSource; 6 | 7 | import java.nio.file.Path; 8 | 9 | public class PlatformFunctions { 10 | @ExpectPlatform 11 | public static String getPlatformName() { 12 | // Just throw an error, the content should get replaced at runtime. 13 | throw new AssertionError(); 14 | } 15 | @ExpectPlatform 16 | public static Path getConfigDirectory() { 17 | // Just throw an error, the content should get replaced at runtime. 18 | throw new AssertionError(); 19 | } 20 | @ExpectPlatform 21 | public static boolean isClientEnv() { 22 | // Just throw an error, the content should get replaced at runtime. 23 | throw new AssertionError(); 24 | } 25 | @ExpectPlatform 26 | public static boolean isModLoaded(String modid) { 27 | // Just throw an error, the content should get replaced at runtime. 28 | throw new AssertionError(); 29 | } 30 | @ExpectPlatform 31 | public static void registerCommand(LiteralArgumentBuilder command) { 32 | // Just throw an error, the content should get replaced at runtime. 33 | throw new AssertionError(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /common/src/main/resources/architectury.common.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/common/src/main/resources/assets/midnightlib/icon.png -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/de_de.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"MidnightConfig Übersicht", 3 | "midnightlib.midnightconfig.title":"MidnightLib Konfiguration", 4 | "midnightlib.midnightconfig.config_screen_list":"Konfigurationsübersicht", 5 | "modmenu.summaryTranslation.midnightlib": "Code-Bibliothek für einfache Konfiguration.", 6 | "midnightconfig.colorChooser.title": "Wähle eine Farbe" 7 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"MidnightConfig Overview", 3 | "midnightlib.midnightconfig.title":"MidnightLib Config", 4 | "midnightlib.midnightconfig.config_screen_list":"Enable Config Screen List", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§aYes", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§cNo", 7 | "midnightlib.midnightconfig.enum.ConfigButton.MODMENU":"§bModMenu", 8 | "midnightlib.modrinth":"Modrinth", 9 | "midnightlib.curseforge":"CurseForge", 10 | "midnightlib.wiki":"Wiki", 11 | "modmenu.summaryTranslation.midnightlib": "Common Library for easy configuration.", 12 | "midnightconfig.colorChooser.title": "Choose a color" 13 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/es_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title": "Visión general de MidnightConfig", 3 | "midnightlib.midnightconfig.title": "Configuración de MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list": "Habilitar lista de pantallas de configuración", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE": "§aSí", 6 | "modmenu.summaryTranslation.midnightlib": "Librería común para facilitar la configuración.", 7 | "midnightconfig.colorChooser.title": "Elegí un color" 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/fr_fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"Vue d'ensemble de MidnightConfig", 3 | "midnightlib.midnightconfig.title":"Configuration de MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list":"Activer la liste de l'écran de configuration", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§aOui", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§cNon", 7 | "modmenu.summaryTranslation.midnightlib": "Bibliothèque commune pour les mods de la Team MidnightDust." 8 | } 9 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/ms_my.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title": "Ikhtisar MidnightConfig", 3 | "midnightlib.midnightconfig.title": "Konfigurasi MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list": "Dayakan Senarai Skrin Konfigurasi", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE": "§aYa", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE": "§cTidak", 7 | "modmenu.summaryTranslation.midnightlib": "Pustaka Biasa untuk konfigurasi mudah." 8 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/pt_br.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"Visão geral do MidnightConfig", 3 | "midnightlib.midnightconfig.title":"Configuração MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list":"Ativar lista de telas de configuração", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§aVerdadeiro", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§cFalso", 7 | "modmenu.summaryTranslation.midnightlib": "Biblioteca comum para mods do Team MidnightDust." 8 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/ru_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title": "Обзор MidnightConfig", 3 | "midnightlib.midnightconfig.title": "Конфигурация MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list": "Включить список экранов настройки", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE": "§aДа", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE": "§cНет", 7 | "midnightlib.midnightconfig.enum.ConfigButton.MODMENU": "§bModMenu", 8 | "midnightlib.wiki": "Вики", 9 | "modmenu.summaryTranslation.midnightlib": "Общая библиотека для простой настройки.", 10 | "midnightconfig.colorChooser.title": "Выберите цвет" 11 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/tt_ru.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"MidnightConfig күзәтү", 3 | "midnightlib.midnightconfig.title":"MidnightLib көйләүләре", 4 | "midnightlib.midnightconfig.config_screen_list":"Көйләүләр экранының исемлеген кушу", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§aӘйе", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§cЮк", 7 | "midnightlib.wiki":"Вики", 8 | "modmenu.summaryTranslation.midnightlib": "MidnightDust төркеменең модлары өчен гомуми китапханә." 9 | } 10 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/uk_ua.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"Огляд MidnightConfig", 3 | "midnightlib.midnightconfig.title":"Конфігурація MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list":"Увімкнути список екрана конфігурації", 5 | "midnightlib.wiki":"Вікі", 6 | "modmenu.summaryTranslation.midnightlib": "Загальна бібліотека для модів команди MidnightDust." 7 | } 8 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/zh_cn.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"MidnightConfig 概述", 3 | "midnightlib.midnightconfig.title":"MidnightLib 配置", 4 | "midnightlib.midnightconfig.config_screen_list":"启用配置屏幕列表", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§a是", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§c否", 7 | "midnightlib.midnightconfig.enum.ConfigButton.MODMENU":"§b模组菜单", 8 | "modmenu.summaryTranslation.midnightlib": "一个便于模组配置的通用库模组", 9 | "midnightconfig.colorChooser.title": "选择一种颜色" 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/zh_tw.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title":"MidnightConfig 概述", 3 | "midnightlib.midnightconfig.title":"MidnightLib 設定", 4 | "midnightlib.midnightconfig.config_screen_list":"啟用設定畫面列表", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE":"§a是", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE":"§c否", 7 | "midnightlib.midnightconfig.enum.ConfigButton.MODMENU":"§b模組選單", 8 | "midnightlib.wiki":"維基", 9 | "modmenu.summaryTranslation.midnightlib": "MidnightDust 團隊的常用程式庫模組。" 10 | } 11 | -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/lang/zlm_arab.json: -------------------------------------------------------------------------------- 1 | { 2 | "midnightlib.overview.title": "اختصار MidnightConfig", 3 | "midnightlib.midnightconfig.title": "کونفيݢوراسي MidnightLib", 4 | "midnightlib.midnightconfig.config_screen_list": "داياکن سناراي سکرين کونفيݢوراسي", 5 | "midnightlib.midnightconfig.enum.ConfigButton.TRUE": "§aيا", 6 | "midnightlib.midnightconfig.enum.ConfigButton.FALSE": "§cتيدق", 7 | "midnightlib.wiki": "ويکي", 8 | "modmenu.summaryTranslation.midnightlib": "ڤوستاک بياسا اونتوق کونفيݢوراسي موده." 9 | } -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/explorer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/explorer.png -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/midnightlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/midnightlib.png -------------------------------------------------------------------------------- /common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/reset.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/common/src/main/resources/assets/midnightlib/textures/gui/sprites/icon/reset.png -------------------------------------------------------------------------------- /common/src/main/resources/midnightlib.mixins.json: -------------------------------------------------------------------------------- 1 | {"required": true,"minVersion": "0.8","package": "eu.midnightdust.core.mixin","compatibilityLevel": "JAVA_17","client": ["MixinOptionsScreen"],"injectors": {"defaultRequire": 1}} -------------------------------------------------------------------------------- /fabric/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.johnrengelman.shadow' 3 | id "me.shedaniel.unified-publishing" 4 | } 5 | repositories { 6 | maven { url "https://maven.terraformersmc.com/releases" } 7 | } 8 | 9 | architectury { 10 | platformSetupLoomIde() 11 | fabric() 12 | } 13 | 14 | loom { 15 | } 16 | 17 | configurations { 18 | common 19 | shadowCommon // Don't use shadow from the shadow plugin since it *excludes* files. 20 | compileClasspath.extendsFrom common 21 | runtimeClasspath.extendsFrom common 22 | developmentFabric.extendsFrom common 23 | archivesBaseName = rootProject.archives_base_name 24 | version = rootProject.mod_version + "-" + project.name + "+" + rootProject.minecraft_version 25 | } 26 | 27 | dependencies { 28 | modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" 29 | modApi "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}" 30 | modCompileOnly ("com.terraformersmc:modmenu:${rootProject.mod_menu_version}") 31 | 32 | common(project(path: ":common", configuration: "namedElements")) { transitive false } 33 | shadowCommon(project(path: ":common", configuration: "transformProductionFabric")) { transitive false } 34 | } 35 | 36 | processResources { 37 | inputs.property "version", rootProject.version 38 | 39 | filesMatching("fabric.mod.json") { 40 | expand "version": rootProject.version 41 | } 42 | } 43 | 44 | shadowJar { 45 | exclude "architectury.common.json" 46 | 47 | configurations = [project.configurations.shadowCommon] 48 | archiveClassifier = "dev-shadow" 49 | } 50 | 51 | remapJar { 52 | input.set shadowJar.archiveFile 53 | dependsOn shadowJar 54 | } 55 | 56 | sourcesJar { 57 | def commonSources = project(":common").sourcesJar 58 | dependsOn commonSources 59 | from commonSources.archiveFile.map { zipTree(it) } 60 | } 61 | 62 | components.java { 63 | withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) { 64 | skip() 65 | } 66 | } 67 | 68 | unifiedPublishing { 69 | project { 70 | displayName = "MidnightLib $rootProject.version - Fabric $project.minecraft_version" 71 | releaseType = "$project.release_type" 72 | changelog = releaseChangelog() 73 | gameVersions = [] 74 | gameLoaders = ["fabric","quilt"] 75 | mainPublication remapJar 76 | relations { 77 | depends { 78 | curseforge = "fabric-api" 79 | modrinth = "fabric-api" 80 | } 81 | } 82 | 83 | var CURSEFORGE_TOKEN = project.findProperty("CURSEFORGE_TOKEN") ?: System.getenv("CURSEFORGE_TOKEN") 84 | if (CURSEFORGE_TOKEN != null) { 85 | curseforge { 86 | token = CURSEFORGE_TOKEN 87 | id = rootProject.curseforge_id 88 | gameVersions.addAll "Java 21", project.minecraft_version 89 | if (project.supported_versions != "") gameVersions.addAll project.supported_versions 90 | } 91 | } 92 | 93 | var MODRINTH_TOKEN = project.findProperty("MODRINTH_TOKEN") ?: System.getenv("MODRINTH_TOKEN") 94 | if (MODRINTH_TOKEN != null) { 95 | modrinth { 96 | token = MODRINTH_TOKEN 97 | id = rootProject.modrinth_id 98 | version = rootProject.mod_version + "+" + rootProject.minecraft_version + "-" + project.name 99 | gameVersions.addAll project.minecraft_version 100 | if (project.supported_versions != "") gameVersions.addAll project.supported_versions 101 | } 102 | } 103 | } 104 | } -------------------------------------------------------------------------------- /fabric/src/main/java/eu/midnightdust/fabric/core/MidnightLibFabric.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.fabric.core; 2 | 3 | import eu.midnightdust.core.MidnightLib; 4 | import net.fabricmc.api.*; 5 | 6 | public class MidnightLibFabric implements DedicatedServerModInitializer, ClientModInitializer { 7 | @Override @Environment(EnvType.CLIENT) 8 | public void onInitializeClient() { 9 | MidnightLib.onInitializeClient(); 10 | MidnightLib.registerAutoCommand(); 11 | } 12 | @Override 13 | public void onInitializeServer() {MidnightLib.registerAutoCommand();} 14 | } 15 | -------------------------------------------------------------------------------- /fabric/src/main/java/eu/midnightdust/lib/config/AutoModMenu.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.config; 2 | 3 | import com.terraformersmc.modmenu.api.ConfigScreenFactory; 4 | import com.terraformersmc.modmenu.api.ModMenuApi; 5 | import eu.midnightdust.core.MidnightLib; 6 | import eu.midnightdust.core.config.MidnightLibConfig; 7 | 8 | import java.util.HashMap; 9 | import java.util.Map; 10 | 11 | public class AutoModMenu implements ModMenuApi { 12 | 13 | @Override 14 | public ConfigScreenFactory getModConfigScreenFactory() { 15 | return parent -> MidnightLibConfig.getScreen(parent,"midnightlib"); 16 | } 17 | 18 | @Override 19 | public Map> getProvidedConfigScreenFactories() { 20 | HashMap> map = new HashMap<>(); 21 | MidnightConfig.configClass.forEach((modid, cClass) -> { 22 | if (!MidnightLib.hiddenMods.contains(modid)) 23 | map.put(modid, parent -> MidnightConfig.getScreen(parent, modid)); 24 | }); return map; 25 | } 26 | } -------------------------------------------------------------------------------- /fabric/src/main/java/eu/midnightdust/lib/util/fabric/PlatformFunctionsImpl.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.util.fabric; 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 4 | import eu.midnightdust.lib.util.PlatformFunctions; 5 | import net.fabricmc.api.EnvType; 6 | import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; 7 | import net.fabricmc.loader.api.FabricLoader; 8 | import net.minecraft.server.command.ServerCommandSource; 9 | 10 | import java.nio.file.Path; 11 | 12 | public class PlatformFunctionsImpl { 13 | public static String getPlatformName() { 14 | return "fabric"; 15 | } 16 | /** 17 | * This is our actual method to {@link PlatformFunctions#getConfigDirectory()}. 18 | */ 19 | public static Path getConfigDirectory() { 20 | return FabricLoader.getInstance().getConfigDir(); 21 | } 22 | public static boolean isClientEnv() { 23 | return FabricLoader.getInstance().getEnvironmentType() == EnvType.CLIENT; 24 | } 25 | public static boolean isModLoaded(String modid) { 26 | return FabricLoader.getInstance().isModLoaded(modid); 27 | } 28 | public static void registerCommand(LiteralArgumentBuilder command) { 29 | CommandRegistrationCallback.EVENT.register((dispatcher, dedicated, registrationEnvironment) -> dispatcher.register(command)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /fabric/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "midnightlib", 4 | "version": "${version}", 5 | 6 | "name": "MidnightLib", 7 | "description": "Common Library for Team MidnightDust's mods.", 8 | "authors": [ 9 | "Motschen", 10 | "TeamMidnightDust" 11 | ], 12 | "contributors": [ 13 | "maloryware", 14 | "Jaffe2718" 15 | ], 16 | "contact": { 17 | "homepage": "https://www.midnightdust.eu/", 18 | "sources": "https://github.com/TeamMidnightDust/MidnightLib", 19 | "issues": "https://github.com/TeamMidnightDust/MidnightLib/issues" 20 | }, 21 | 22 | "license": "MIT", 23 | "icon": "assets/midnightlib/icon.png", 24 | 25 | "environment": "*", 26 | "entrypoints": { 27 | "server": [ 28 | "eu.midnightdust.fabric.core.MidnightLibFabric" 29 | ], 30 | "client": [ 31 | "eu.midnightdust.fabric.core.MidnightLibFabric" 32 | ], 33 | "modmenu": [ 34 | "eu.midnightdust.lib.config.AutoModMenu" 35 | ] 36 | }, 37 | "depends": { 38 | "fabric-resource-loader-v0": "*", 39 | "minecraft": ">=1.21" 40 | }, 41 | 42 | "mixins": [ 43 | "midnightlib.mixins.json" 44 | ], 45 | 46 | "custom": { 47 | "modmenu": { 48 | "links": { 49 | "modmenu.discord": "http://discord.midnightdust.eu/", 50 | "modmenu.website": "https://midnightdust.eu/midnightlib", 51 | "midnightlib.wiki": "https://midnightdust.eu/wiki/midnightlib" 52 | }, 53 | "badges": [ "library" ] 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4096M 2 | 3 | minecraft_version=1.21.4 4 | supported_versions=1.21.5 5 | yarn_mappings=1.21.4+build.1 6 | enabled_platforms=fabric,neoforge 7 | 8 | archives_base_name=midnightlib 9 | mod_version=1.7.3 10 | maven_group=eu.midnightdust 11 | release_type=release 12 | curseforge_id=488090 13 | modrinth_id=codAaoxh 14 | 15 | fabric_loader_version=0.16.9 16 | fabric_api_version=0.110.5+1.21.4 17 | 18 | neoforge_version=21.4.3-beta 19 | yarn_mappings_patch_neoforge_version = 1.21+build.4 20 | 21 | mod_menu_version = 9.0.0 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # 4 | # Copyright © 2015-2021 the original authors. 5 | # 6 | # Licensed under the Apache License, Version 2.0 (the "License"); 7 | # you may not use this file except in compliance with the License. 8 | # You may obtain a copy of the License at 9 | # 10 | # https://www.apache.org/licenses/LICENSE-2.0 11 | # 12 | # Unless required by applicable law or agreed to in writing, software 13 | # distributed under the License is distributed on an "AS IS" BASIS, 14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 15 | # See the License for the specific language governing permissions and 16 | # limitations under the License. 17 | # 18 | 19 | ############################################################################## 20 | # 21 | # Gradle start up script for POSIX generated by Gradle. 22 | # 23 | # Important for running: 24 | # 25 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is 26 | # noncompliant, but you have some other compliant shell such as ksh or 27 | # bash, then to run this script, type that shell name before the whole 28 | # command line, like: 29 | # 30 | # ksh Gradle 31 | # 32 | # Busybox and similar reduced shells will NOT work, because this script 33 | # requires all of these POSIX shell features: 34 | # * functions; 35 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}», 36 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»; 37 | # * compound commands having a testable exit status, especially «case»; 38 | # * various built-in commands including «command», «set», and «ulimit». 39 | # 40 | # Important for patching: 41 | # 42 | # (2) This script targets any POSIX shell, so it avoids extensions provided 43 | # by Bash, Ksh, etc; in particular arrays are avoided. 44 | # 45 | # The "traditional" practice of packing multiple parameters into a 46 | # space-separated string is a well documented source of bugs and security 47 | # problems, so this is (mostly) avoided, by progressively accumulating 48 | # options in "$@", and eventually passing that to Java. 49 | # 50 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, 51 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; 52 | # see the in-line comments for details. 53 | # 54 | # There are tweaks for specific operating systems such as AIX, CygWin, 55 | # Darwin, MinGW, and NonStop. 56 | # 57 | # (3) This script is generated from the Groovy template 58 | # https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt 59 | # within the Gradle project. 60 | # 61 | # You can find Gradle at https://github.com/gradle/gradle/. 62 | # 63 | ############################################################################## 64 | 65 | # Attempt to set APP_HOME 66 | 67 | # Resolve links: $0 may be a link 68 | app_path=$0 69 | 70 | # Need this for daisy-chained symlinks. 71 | while 72 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path 73 | [ -h "$app_path" ] 74 | do 75 | ls=$( ls -ld "$app_path" ) 76 | link=${ls#*' -> '} 77 | case $link in #( 78 | /*) app_path=$link ;; #( 79 | *) app_path=$APP_HOME$link ;; 80 | esac 81 | done 82 | 83 | APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit 84 | 85 | APP_NAME="Gradle" 86 | APP_BASE_NAME=${0##*/} 87 | 88 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 89 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' 90 | 91 | # Use the maximum available, or set MAX_FD != -1 to use that value. 92 | MAX_FD=maximum 93 | 94 | warn () { 95 | echo "$*" 96 | } >&2 97 | 98 | die () { 99 | echo 100 | echo "$*" 101 | echo 102 | exit 1 103 | } >&2 104 | 105 | # OS specific support (must be 'true' or 'false'). 106 | cygwin=false 107 | msys=false 108 | darwin=false 109 | nonstop=false 110 | case "$( uname )" in #( 111 | CYGWIN* ) cygwin=true ;; #( 112 | Darwin* ) darwin=true ;; #( 113 | MSYS* | MINGW* ) msys=true ;; #( 114 | NONSTOP* ) nonstop=true ;; 115 | esac 116 | 117 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 118 | 119 | 120 | # Determine the Java command to use to start the JVM. 121 | if [ -n "$JAVA_HOME" ] ; then 122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 123 | # IBM's JDK on AIX uses strange locations for the executables 124 | JAVACMD=$JAVA_HOME/jre/sh/java 125 | else 126 | JAVACMD=$JAVA_HOME/bin/java 127 | fi 128 | if [ ! -x "$JAVACMD" ] ; then 129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 130 | 131 | Please set the JAVA_HOME variable in your environment to match the 132 | location of your Java installation." 133 | fi 134 | else 135 | JAVACMD=java 136 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 137 | 138 | Please set the JAVA_HOME variable in your environment to match the 139 | location of your Java installation." 140 | fi 141 | 142 | # Increase the maximum file descriptors if we can. 143 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then 144 | case $MAX_FD in #( 145 | max*) 146 | MAX_FD=$( ulimit -H -n ) || 147 | warn "Could not query maximum file descriptor limit" 148 | esac 149 | case $MAX_FD in #( 150 | '' | soft) :;; #( 151 | *) 152 | ulimit -n "$MAX_FD" || 153 | warn "Could not set maximum file descriptor limit to $MAX_FD" 154 | esac 155 | fi 156 | 157 | # Collect all arguments for the java command, stacking in reverse order: 158 | # * args from the command line 159 | # * the main class name 160 | # * -classpath 161 | # * -D...appname settings 162 | # * --module-path (only if needed) 163 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. 164 | 165 | # For Cygwin or MSYS, switch paths to Windows format before running java 166 | if "$cygwin" || "$msys" ; then 167 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) 168 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) 169 | 170 | JAVACMD=$( cygpath --unix "$JAVACMD" ) 171 | 172 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 173 | for arg do 174 | if 175 | case $arg in #( 176 | -*) false ;; # don't mess with options #( 177 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath 178 | [ -e "$t" ] ;; #( 179 | *) false ;; 180 | esac 181 | then 182 | arg=$( cygpath --path --ignore --mixed "$arg" ) 183 | fi 184 | # Roll the args list around exactly as many times as the number of 185 | # args, so each arg winds up back in the position where it started, but 186 | # possibly modified. 187 | # 188 | # NB: a `for` loop captures its iteration list before it begins, so 189 | # changing the positional parameters here affects neither the number of 190 | # iterations, nor the values presented in `arg`. 191 | shift # remove old arg 192 | set -- "$@" "$arg" # push replacement arg 193 | done 194 | fi 195 | 196 | # Collect all arguments for the java command; 197 | # * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of 198 | # shell script including quotes and variable substitutions, so put them in 199 | # double quotes to make sure that they get re-expanded; and 200 | # * put everything else in single quotes, so that it's not re-expanded. 201 | 202 | set -- \ 203 | "-Dorg.gradle.appname=$APP_BASE_NAME" \ 204 | -classpath "$CLASSPATH" \ 205 | org.gradle.wrapper.GradleWrapperMain \ 206 | "$@" 207 | 208 | # Stop when "xargs" is not available. 209 | if ! command -v xargs >/dev/null 2>&1 210 | then 211 | die "xargs is not available" 212 | fi 213 | 214 | # Use "xargs" to parse quoted args. 215 | # 216 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed. 217 | # 218 | # In Bash we could simply go: 219 | # 220 | # readarray ARGS < <( xargs -n1 <<<"$var" ) && 221 | # set -- "${ARGS[@]}" "$@" 222 | # 223 | # but POSIX shell has neither arrays nor command substitution, so instead we 224 | # post-process each arg (as a line of input to sed) to backslash-escape any 225 | # character that might be a shell metacharacter, then use eval to reverse 226 | # that process (while maintaining the separation between arguments), and wrap 227 | # the whole thing up as a single "set" statement. 228 | # 229 | # This will of course break if any of these variables contains a newline or 230 | # an unmatched quote. 231 | # 232 | 233 | eval "set -- $( 234 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | 235 | xargs -n1 | 236 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | 237 | tr '\n' ' ' 238 | )" '"$@"' 239 | 240 | exec "$JAVACMD" "$@" 241 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | 17 | @if "%DEBUG%"=="" @echo off 18 | @rem ########################################################################## 19 | @rem 20 | @rem Gradle startup script for Windows 21 | @rem 22 | @rem ########################################################################## 23 | 24 | @rem Set local scope for the variables with windows NT shell 25 | if "%OS%"=="Windows_NT" setlocal 26 | 27 | set DIRNAME=%~dp0 28 | if "%DIRNAME%"=="" set DIRNAME=. 29 | set APP_BASE_NAME=%~n0 30 | set APP_HOME=%DIRNAME% 31 | 32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 34 | 35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 37 | 38 | @rem Find java.exe 39 | if defined JAVA_HOME goto findJavaFromJavaHome 40 | 41 | set JAVA_EXE=java.exe 42 | %JAVA_EXE% -version >NUL 2>&1 43 | if %ERRORLEVEL% equ 0 goto execute 44 | 45 | echo. 46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 47 | echo. 48 | echo Please set the JAVA_HOME variable in your environment to match the 49 | echo location of your Java installation. 50 | 51 | goto fail 52 | 53 | :findJavaFromJavaHome 54 | set JAVA_HOME=%JAVA_HOME:"=% 55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 56 | 57 | if exist "%JAVA_EXE%" goto execute 58 | 59 | echo. 60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 61 | echo. 62 | echo Please set the JAVA_HOME variable in your environment to match the 63 | echo location of your Java installation. 64 | 65 | goto fail 66 | 67 | :execute 68 | @rem Setup the command line 69 | 70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 71 | 72 | 73 | @rem Execute Gradle 74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 75 | 76 | :end 77 | @rem End local scope for the variables with windows NT shell 78 | if %ERRORLEVEL% equ 0 goto mainEnd 79 | 80 | :fail 81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 82 | rem the _cmd.exe /c_ return code! 83 | set EXIT_CODE=%ERRORLEVEL% 84 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 85 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 86 | exit /b %EXIT_CODE% 87 | 88 | :mainEnd 89 | if "%OS%"=="Windows_NT" endlocal 90 | 91 | :omega 92 | -------------------------------------------------------------------------------- /neoforge/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.johnrengelman.shadow' 3 | id "me.shedaniel.unified-publishing" 4 | } 5 | 6 | repositories { 7 | maven { 8 | name = 'NeoForged' 9 | url = 'https://maven.neoforged.net/releases' 10 | } 11 | } 12 | 13 | 14 | architectury { 15 | platformSetupLoomIde() 16 | neoForge() 17 | } 18 | 19 | loom { 20 | accessWidenerPath = project(":common").loom.accessWidenerPath 21 | } 22 | 23 | configurations { 24 | common { 25 | canBeResolved = true 26 | canBeConsumed = false 27 | } 28 | compileClasspath.extendsFrom common 29 | runtimeClasspath.extendsFrom common 30 | developmentNeoForge.extendsFrom common 31 | 32 | // Files in this configuration will be bundled into your mod using the Shadow plugin. 33 | // Don't use the `shadow` configuration from the plugin itself as it's meant for excluding files. 34 | shadowBundle { 35 | canBeResolved = true 36 | canBeConsumed = false 37 | } 38 | archivesBaseName = rootProject.archives_base_name 39 | version = rootProject.mod_version + "-" + project.name + "+" + rootProject.minecraft_version 40 | } 41 | 42 | dependencies { 43 | neoForge "net.neoforged:neoforge:$rootProject.neoforge_version" 44 | 45 | common(project(path: ':common', configuration: 'namedElements')) { transitive false } 46 | shadowBundle project(path: ':common', configuration: 'transformProductionNeoForge') 47 | } 48 | 49 | processResources { 50 | inputs.property 'version', rootProject.version 51 | 52 | filesMatching('META-INF/neoforge.mods.toml') { 53 | expand version: rootProject.version 54 | } 55 | } 56 | 57 | shadowJar { 58 | configurations = [project.configurations.shadowBundle] 59 | archiveClassifier = 'dev-shadow' 60 | } 61 | 62 | remapJar { 63 | input.set shadowJar.archiveFile 64 | } 65 | 66 | sourcesJar { 67 | def commonSources = project(":common").sourcesJar 68 | dependsOn commonSources 69 | from commonSources.archiveFile.map { zipTree(it) } 70 | } 71 | 72 | components.java { 73 | withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) { 74 | skip() 75 | } 76 | } 77 | 78 | unifiedPublishing { 79 | project { 80 | displayName = "MidnightLib $rootProject.version - NeoForge $project.minecraft_version" 81 | releaseType = "$project.release_type" 82 | changelog = releaseChangelog() 83 | gameVersions = [] 84 | gameLoaders = ["neoforge"] 85 | mainPublication remapJar 86 | 87 | var CURSEFORGE_TOKEN = project.findProperty("CURSEFORGE_TOKEN") ?: System.getenv("CURSEFORGE_TOKEN") 88 | if (CURSEFORGE_TOKEN != null) { 89 | curseforge { 90 | token = CURSEFORGE_TOKEN 91 | id = rootProject.curseforge_id 92 | gameVersions.addAll "Java 21", project.minecraft_version 93 | if (project.supported_versions != "") gameVersions.addAll project.supported_versions 94 | } 95 | } 96 | 97 | var MODRINTH_TOKEN = project.findProperty("MODRINTH_TOKEN") ?: System.getenv("MODRINTH_TOKEN") 98 | if (MODRINTH_TOKEN != null) { 99 | modrinth { 100 | token = MODRINTH_TOKEN 101 | id = rootProject.modrinth_id 102 | version = rootProject.mod_version + "+" + rootProject.minecraft_version + "-" + project.name 103 | gameVersions.addAll project.minecraft_version 104 | if (project.supported_versions != "") gameVersions.addAll project.supported_versions 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /neoforge/gradle.properties: -------------------------------------------------------------------------------- 1 | loom.platform=neoforge -------------------------------------------------------------------------------- /neoforge/src/main/java/eu/midnightdust/lib/util/neoforge/PlatformFunctionsImpl.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.util.neoforge; 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 4 | import eu.midnightdust.lib.util.PlatformFunctions; 5 | import net.minecraft.server.command.ServerCommandSource; 6 | import net.neoforged.fml.ModList; 7 | import net.neoforged.fml.loading.FMLEnvironment; 8 | import net.neoforged.fml.loading.FMLPaths; 9 | 10 | import java.nio.file.Path; 11 | 12 | import static eu.midnightdust.neoforge.MidnightLibNeoForge.commands; 13 | 14 | public class PlatformFunctionsImpl { 15 | public static String getPlatformName() { 16 | return "neoforge"; 17 | } 18 | /** 19 | * This is our actual method to {@link PlatformFunctions#getConfigDirectory()}. 20 | */ 21 | public static Path getConfigDirectory() { 22 | return FMLPaths.CONFIGDIR.get(); 23 | } 24 | public static boolean isClientEnv() { 25 | return FMLEnvironment.dist.isClient(); 26 | } 27 | public static boolean isModLoaded(String modid) { 28 | return ModList.get().isLoaded(modid); 29 | } 30 | public static void registerCommand(LiteralArgumentBuilder command) { 31 | commands.add(command); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /neoforge/src/main/java/eu/midnightdust/neoforge/MidnightLibNeoForge.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.neoforge; 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 4 | import eu.midnightdust.core.MidnightLib; 5 | import eu.midnightdust.lib.config.MidnightConfig; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | import net.neoforged.api.distmarker.Dist; 8 | import net.neoforged.bus.api.SubscribeEvent; 9 | import net.neoforged.fml.ModList; 10 | import net.neoforged.fml.common.EventBusSubscriber; 11 | import net.neoforged.fml.common.Mod; 12 | import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent; 13 | import net.neoforged.fml.loading.FMLEnvironment; 14 | import net.neoforged.neoforge.client.gui.IConfigScreenFactory; 15 | import net.neoforged.neoforge.event.RegisterCommandsEvent; 16 | 17 | import java.util.ArrayList; 18 | import java.util.ConcurrentModificationException; 19 | import java.util.List; 20 | 21 | @Mod("midnightlib") 22 | public class MidnightLibNeoForge { 23 | public static List> commands = new ArrayList<>(); 24 | 25 | public MidnightLibNeoForge() { 26 | if (FMLEnvironment.dist == Dist.CLIENT) MidnightLib.onInitializeClient(); 27 | } 28 | 29 | @EventBusSubscriber(modid = "midnightlib", bus = EventBusSubscriber.Bus.MOD, value = Dist.CLIENT) 30 | public static class MidnightLibBusEvents { 31 | @SubscribeEvent 32 | public static void onPostInit(FMLClientSetupEvent event) { 33 | ModList.get().forEachModContainer((modid, modContainer) -> { 34 | if (MidnightConfig.configClass.containsKey(modid) && !MidnightLib.hiddenMods.contains(modid)) { 35 | modContainer.registerExtensionPoint(IConfigScreenFactory.class, (minecraftClient, screen) -> MidnightConfig.getScreen(screen, modid)); 36 | } 37 | }); 38 | MidnightLib.registerAutoCommand(); 39 | } 40 | } 41 | 42 | @EventBusSubscriber(modid = "midnightlib") 43 | public static class MidnightLibEvents { 44 | @SubscribeEvent 45 | public static void registerCommands(RegisterCommandsEvent event) { 46 | try { 47 | commands.forEach(command -> event.getDispatcher().register(command)); 48 | } 49 | catch (ConcurrentModificationException ignored) {} 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /neoforge/src/main/resources/META-INF/neoforge.mods.toml: -------------------------------------------------------------------------------- 1 | modLoader = "javafml" 2 | loaderVersion = "[2,)" 3 | #issueTrackerURL = "" 4 | license = "MIT License" 5 | 6 | [[mods]] 7 | modId = "midnightlib" 8 | version = "${version}" 9 | displayName = "MidnightLib" 10 | logoFile = "midnightlib.png" 11 | authors = "TeamMidnightDust, Motschen" 12 | description = ''' 13 | Common Library for Team MidnightDust's mods. 14 | ''' 15 | 16 | [[mixins]] 17 | config = "midnightlib.mixins.json" 18 | 19 | [[dependencies.midnightlib]] 20 | modId = "neoforge" 21 | mandatory = true 22 | versionRange = "[20.5,)" 23 | ordering = "NONE" 24 | side = "BOTH" 25 | 26 | [[dependencies.midnightlib]] 27 | modId = "minecraft" 28 | mandatory = true 29 | versionRange = "[1.20.5,)" 30 | ordering = "NONE" 31 | side = "BOTH" -------------------------------------------------------------------------------- /neoforge/src/main/resources/midnightlib.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/TeamMidnightDust/MidnightLib/a1ad6dd1f085d1782de9f0ab11efd70b8290460d/neoforge/src/main/resources/midnightlib.png -------------------------------------------------------------------------------- /quilt/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.github.johnrengelman.shadow" version "7.1.2" 3 | } 4 | 5 | repositories { 6 | maven { url "https://maven.quiltmc.org/repository/release/" } 7 | } 8 | 9 | architectury { 10 | platformSetupLoomIde() 11 | loader("quilt") 12 | } 13 | 14 | loom { 15 | } 16 | 17 | configurations { 18 | common 19 | shadowCommon // Don't use shadow from the shadow plugin because we don't want IDEA to index this. 20 | compileClasspath.extendsFrom common 21 | runtimeClasspath.extendsFrom common 22 | developmentQuilt.extendsFrom common 23 | archivesBaseName = rootProject.archives_base_name + "-quilt" 24 | } 25 | 26 | dependencies { 27 | modImplementation "org.quiltmc:quilt-loader:${rootProject.quilt_loader_version}" 28 | modApi "org.quiltmc.quilted-fabric-api:quilted-fabric-api:${rootProject.quilt_fabric_api_version}" 29 | // Remove the next few lines if you don't want to depend on the API 30 | 31 | common(project(path: ":common", configuration: "namedElements")) { transitive false } 32 | shadowCommon(project(path: ":common", configuration: "transformProductionQuilt")) { transitive false } 33 | common(project(path: ":fabric-like", configuration: "namedElements")) { transitive false } 34 | shadowCommon(project(path: ":fabric-like", configuration: "transformProductionQuilt")) { transitive false } 35 | } 36 | 37 | processResources { 38 | inputs.property "group", rootProject.maven_group 39 | inputs.property "version", project.version 40 | 41 | filesMatching("quilt.mod.json") { 42 | expand "group": rootProject.maven_group, 43 | "version": project.version 44 | } 45 | } 46 | 47 | shadowJar { 48 | exclude "architectury.common.json" 49 | 50 | configurations = [project.configurations.shadowCommon] 51 | classifier "dev-shadow" 52 | } 53 | 54 | remapJar { 55 | input.set shadowJar.archiveFile 56 | dependsOn shadowJar 57 | classifier null 58 | } 59 | 60 | jar { 61 | classifier "dev" 62 | } 63 | 64 | sourcesJar { 65 | def commonSources = project(":common").sourcesJar 66 | dependsOn commonSources 67 | from commonSources.archiveFile.map { zipTree(it) } 68 | } 69 | 70 | components.java { 71 | withVariantsFromConfiguration(project.configurations.shadowRuntimeElements) { 72 | skip() 73 | } 74 | } 75 | 76 | publishing { 77 | publications { 78 | mavenQuilt(MavenPublication) { 79 | artifactId = rootProject.archives_base_name + "-" + project.name 80 | from components.java 81 | } 82 | } 83 | 84 | // See https://docs.gradle.org/current/userguide/publishing_maven.html for information on how to set up publishing. 85 | repositories { 86 | // Add repositories to publish to here. 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /quilt/gradle.properties: -------------------------------------------------------------------------------- 1 | loom.platform=quilt -------------------------------------------------------------------------------- /quilt/src/main/java/eu/midnightdust/lib/util/fabric/PlatformFunctionsImpl.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.lib.util.fabric; 2 | 3 | import com.mojang.brigadier.builder.LiteralArgumentBuilder; 4 | import eu.midnightdust.lib.util.PlatformFunctions; 5 | import net.fabricmc.api.EnvType; 6 | import net.minecraft.server.command.ServerCommandSource; 7 | import org.quiltmc.loader.api.QuiltLoader; 8 | import org.quiltmc.loader.api.minecraft.MinecraftQuiltLoader; 9 | import org.quiltmc.qsl.command.api.CommandRegistrationCallback; 10 | 11 | import java.nio.file.Path; 12 | 13 | public class PlatformFunctionsImpl { 14 | /** 15 | * This is our actual method to {@link PlatformFunctions#getConfigDirectory()}. 16 | */ 17 | public static Path getConfigDirectory() { 18 | return QuiltLoader.getConfigDir(); 19 | } 20 | public static boolean isClientEnv() { 21 | return MinecraftQuiltLoader.getEnvironmentType() == EnvType.CLIENT; 22 | } 23 | public static boolean isModLoaded(String modid) { 24 | return QuiltLoader.isModLoaded(modid); 25 | } 26 | public static void registerCommand(LiteralArgumentBuilder command) { 27 | CommandRegistrationCallback.EVENT.register((dispatcher, dedicated, registrationEnvironment) -> dispatcher.register(command)); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /quilt/src/main/java/eu/midnightdust/quilt/core/MidnightLibClientQuilt.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.quilt.core; 2 | 3 | import eu.midnightdust.core.MidnightLibClient; 4 | import eu.midnightdust.lib.util.MidnightColorUtil; 5 | import org.quiltmc.loader.api.ModContainer; 6 | import org.quiltmc.qsl.base.api.entrypoint.client.ClientModInitializer; 7 | import org.quiltmc.qsl.lifecycle.api.client.event.ClientTickEvents; 8 | 9 | public class MidnightLibClientQuilt implements ClientModInitializer { 10 | @Override 11 | public void onInitializeClient(ModContainer mod) { 12 | MidnightLibClient.onInitializeClient(); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /quilt/src/main/java/eu/midnightdust/quilt/core/MidnightLibServerQuilt.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.quilt.core; 2 | 3 | import eu.midnightdust.core.MidnightLibServer; 4 | import org.quiltmc.loader.api.ModContainer; 5 | import org.quiltmc.qsl.base.api.entrypoint.server.DedicatedServerModInitializer; 6 | 7 | public class MidnightLibServerQuilt implements DedicatedServerModInitializer { 8 | @Override 9 | public void onInitializeServer(ModContainer mod) { 10 | MidnightLibServer.onInitializeServer(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /quilt/src/main/resources/quilt.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schema_version": 1, 3 | "quilt_loader": { 4 | "group": "${group}", 5 | "id": "midnightlib", 6 | "version": "${version}", 7 | "intermediate_mappings": "net.fabricmc:intermediary", 8 | "entrypoints": { 9 | "client_init": [ 10 | "eu.midnightdust.quilt.core.MidnightLibClientQuilt" 11 | ], 12 | "server_init": [ 13 | "eu.midnightdust.quilt.core.MidnightLibServerQuilt" 14 | ], 15 | "modmenu": [ 16 | "eu.midnightdust.lib.config.AutoModMenu" 17 | ] 18 | }, 19 | "depends": [ 20 | { 21 | "id": "quilt_loader", 22 | "version": "*" 23 | }, 24 | { 25 | "id": "quilt_base", 26 | "version": "*" 27 | }, 28 | { 29 | "id": "minecraft", 30 | "version": ">=1.19.4" 31 | } 32 | ], 33 | "metadata": { 34 | "name": "MidnightLib (Quilt)", 35 | "description": "Common Library for Team MidnightDust's mods. Provides a config api, automatic integration with other mods, common utils, and cosmetics.", 36 | "license": "MIT", 37 | "environment": "*", 38 | "contributors": { 39 | "Motschen": "Author", 40 | "TeamMidnightDust": "Mascot" 41 | }, 42 | "contact": { 43 | "email": "mail@midnightdust.eu", 44 | "homepage": "https://modrinth.com/mod/midnightlib", 45 | "issues": "https://github.com/TeamMidnightDust/MidnightLib/issues", 46 | "sources": "https://github.com/TeamMidnightDust/MidnightLib" 47 | }, 48 | "icon": "assets/midnightlib/icon.png" 49 | } 50 | }, 51 | "mixin": [ 52 | "midnightlib.mixins.json" 53 | ], 54 | "modmenu": { 55 | "links": { 56 | "modmenu.discord": "https://discord.midnightdust.eu/", 57 | "modmenu.website": "https://www.midnightdust.eu/", 58 | "midnightlib.curseforge": "https://www.curseforge.com/minecraft/mc-mods/midnightlib", 59 | "midnightlib.modrinth": "https://modrinth.com/mod/midnightlib", 60 | "midnightlib.wiki": "https://github.com/TeamMidnightDust/MidnightLib/wiki" 61 | }, 62 | "badges": [ "library" ] 63 | } 64 | } -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | repositories { 3 | maven { url "https://maven.fabricmc.net/" } 4 | maven { url "https://maven.architectury.dev/" } 5 | maven { url "https://maven.neoforged.net/releases" } 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | include("common") 11 | include("fabric") 12 | include("test-fabric") 13 | include("neoforge") 14 | include("test-neoforge") 15 | //include("quilt") 16 | 17 | rootProject.name = "midnightlib" 18 | -------------------------------------------------------------------------------- /test-fabric/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.github.johnrengelman.shadow' 3 | id "me.shedaniel.unified-publishing" 4 | } 5 | repositories { 6 | maven { url "https://maven.terraformersmc.com/releases" } 7 | } 8 | 9 | architectury { 10 | platformSetupLoomIde() 11 | fabric() 12 | } 13 | 14 | loom { 15 | } 16 | 17 | configurations { 18 | common 19 | shadowCommon // Don't use shadow from the shadow plugin since it *excludes* files. 20 | compileClasspath.extendsFrom common 21 | runtimeClasspath.extendsFrom common 22 | developmentFabric.extendsFrom common 23 | archivesBaseName = rootProject.archives_base_name + "-fabric" 24 | } 25 | 26 | dependencies { 27 | modImplementation "net.fabricmc:fabric-loader:${rootProject.fabric_loader_version}" 28 | modApi "net.fabricmc.fabric-api:fabric-api:${rootProject.fabric_api_version}" 29 | 30 | implementation project(path: ":fabric", configuration: "namedElements") 31 | common(project(path: ":common", configuration: "namedElements")) { transitive false } 32 | } -------------------------------------------------------------------------------- /test-fabric/src/main/java/eu/midnightdust/fabric/example/MLExampleFabric.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.fabric.example; 2 | 3 | import eu.midnightdust.fabric.example.config.MidnightConfigExample; 4 | import net.fabricmc.api.ModInitializer; 5 | 6 | public class MLExampleFabric implements ModInitializer { 7 | @Override 8 | public void onInitialize() { 9 | MidnightConfigExample.init("modid", MidnightConfigExample.class); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test-fabric/src/main/java/eu/midnightdust/fabric/example/MidnightLibExtras.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.fabric.example; 2 | 3 | import com.google.common.collect.Lists; 4 | import eu.midnightdust.lib.config.MidnightConfig; 5 | import net.minecraft.client.MinecraftClient; 6 | import net.minecraft.client.gui.tooltip.Tooltip; 7 | import net.minecraft.client.gui.widget.ButtonWidget; 8 | import net.minecraft.client.gui.widget.ClickableWidget; 9 | import net.minecraft.client.gui.widget.TextIconButtonWidget; 10 | import net.minecraft.client.option.KeyBinding; 11 | import net.minecraft.client.util.InputUtil; 12 | import net.minecraft.text.MutableText; 13 | import net.minecraft.text.Text; 14 | import net.minecraft.util.Formatting; 15 | import net.minecraft.util.Identifier; 16 | import org.jetbrains.annotations.Nullable; 17 | import org.lwjgl.glfw.GLFW; 18 | 19 | /* 20 | Pre-made additional (niche) functionality that is not included in MidnightLib to keep the file size small. 21 | Feel free to copy the parts you need :) 22 | */ 23 | public class MidnightLibExtras { 24 | public static class KeybindButton extends ButtonWidget { 25 | public static ButtonWidget focusedButton; 26 | 27 | public static void add(KeyBinding binding, MidnightConfig.MidnightConfigListWidget list, MidnightConfig.MidnightConfigScreen screen) { 28 | KeybindButton editButton = new KeybindButton(screen.width - 185, 0, 150, 20, binding); 29 | TextIconButtonWidget resetButton = TextIconButtonWidget.builder(Text.translatable("controls.reset"), (button -> { 30 | binding.setBoundKey(binding.getDefaultKey()); 31 | screen.updateList(); 32 | }), true).texture(Identifier.of("midnightlib","icon/reset"), 12, 12).dimension(20, 20).build(); 33 | resetButton.setPosition(screen.width - 205 + 150 + 25, 0); 34 | editButton.resetButton = resetButton; 35 | editButton.updateMessage(false); 36 | MidnightConfig.EntryInfo info = new MidnightConfig.EntryInfo(null, screen.modid); 37 | 38 | list.addButton(Lists.newArrayList(editButton, resetButton), Text.translatable(binding.getTranslationKey()), info); 39 | } 40 | 41 | private final KeyBinding binding; 42 | private @Nullable ClickableWidget resetButton; 43 | public KeybindButton(int x, int y, int width, int height, KeyBinding binding) { 44 | super(x, y, width, height, binding.getBoundKeyLocalizedText(), (button) -> { 45 | ((KeybindButton) button).updateMessage(true); 46 | focusedButton = button; 47 | }, (textSupplier) -> binding.isUnbound() ? Text.translatable("narrator.controls.unbound", binding.getTranslationKey()) : Text.translatable("narrator.controls.bound", binding.getTranslationKey(), textSupplier.get())); 48 | this.binding = binding; 49 | updateMessage(false); 50 | } 51 | @Override 52 | public boolean keyPressed(int keyCode, int scanCode, int modifiers) { 53 | if (focusedButton == this) { 54 | if (keyCode == GLFW.GLFW_KEY_ESCAPE) { 55 | this.binding.setBoundKey(InputUtil.UNKNOWN_KEY); 56 | } else { 57 | this.binding.setBoundKey(InputUtil.fromKeyCode(keyCode, scanCode)); 58 | } 59 | updateMessage(false); 60 | 61 | focusedButton = null; 62 | return true; 63 | } 64 | return super.keyPressed(keyCode, scanCode, modifiers); 65 | } 66 | 67 | public void updateMessage(boolean focused) { 68 | boolean hasConflicts = false; 69 | MutableText conflictingBindings = Text.empty(); 70 | if (focused) this.setMessage(Text.literal("> ").append(this.binding.getBoundKeyLocalizedText().copy().formatted(Formatting.WHITE, Formatting.UNDERLINE)).append(" <").formatted(Formatting.YELLOW)); 71 | else { 72 | this.setMessage(this.binding.getBoundKeyLocalizedText()); 73 | 74 | if (!this.binding.isUnbound()) { 75 | for(KeyBinding keyBinding : MinecraftClient.getInstance().options.allKeys) { 76 | if (keyBinding != this.binding && this.binding.equals(keyBinding)) { 77 | if (hasConflicts) conflictingBindings.append(", "); 78 | 79 | hasConflicts = true; 80 | conflictingBindings.append(Text.translatable(keyBinding.getTranslationKey())); 81 | } 82 | } 83 | } 84 | } 85 | 86 | if (this.resetButton != null) this.resetButton.active = !this.binding.isDefault(); 87 | 88 | if (hasConflicts) { 89 | this.setMessage(Text.literal("[ ").append(this.getMessage().copy().formatted(Formatting.WHITE)).append(" ]").formatted(Formatting.RED)); 90 | this.setTooltip(Tooltip.of(Text.translatable("controls.keybinds.duplicateKeybinds", conflictingBindings))); 91 | } else { 92 | this.setTooltip(null); 93 | } 94 | } 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /test-fabric/src/main/java/eu/midnightdust/fabric/example/config/MidnightConfigExample.java: -------------------------------------------------------------------------------- 1 | package eu.midnightdust.fabric.example.config; 2 | 3 | import com.google.common.collect.Lists; 4 | import eu.midnightdust.fabric.example.MidnightLibExtras; 5 | import eu.midnightdust.lib.config.MidnightConfig; 6 | import net.minecraft.text.MutableText; 7 | import net.minecraft.text.Text; 8 | import net.minecraft.util.Formatting; 9 | import net.minecraft.client.MinecraftClient; 10 | import net.minecraft.util.Identifier; 11 | import net.minecraft.util.TranslatableOption; 12 | 13 | import javax.swing.*; 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | /** Every option in a MidnightConfig class has to be public and static, so we can access it from other classes. 19 | * The config class also has to extend MidnightConfig*/ 20 | @SuppressWarnings({"unused", "DefaultAnnotationParam"}) 21 | public class MidnightConfigExample extends MidnightConfig { 22 | public static final String TEXT = "text"; 23 | public static final String NUMBERS = "numbers"; 24 | public static final String SLIDERS = "sliders"; 25 | public static final String LISTS = "lists"; 26 | public static final String FILES = "files"; 27 | public static final String CONDITIONS = "conditions"; 28 | public static final String EXTRAS = "extras"; 29 | 30 | @Comment(category = TEXT) public static Comment text1; // Comments are rendered like an option without a button and are excluded from the config file 31 | @Comment(category = TEXT, centered = true) public static Comment text2; // Centered comments are the same as normal ones - just centered! 32 | @Comment(category = TEXT) public static Comment spacer1; // Comments containing the word "spacer" will just appear as a blank line 33 | @Entry(category = TEXT) public static boolean showInfo = true; // Example for a boolean option 34 | @Entry(category = TEXT, name="I am a (non-primitive) Boolean") public static Boolean nonPrimitive = true; // Example for a non-primative boolean option 35 | @Entry(category = TEXT) public static String name = "Hello World!"; // Example for a string option, which is in a category! 36 | @Entry(category = TEXT, width = 7, min = 7, isColor = true, name = "I am a color!") public static String titleColor = "#ffffff"; // The isColor property adds a color chooser for a hexadecimal color 37 | @Entry(category = TEXT, idMode = 0) public static Identifier id = Identifier.ofVanilla("diamond"); // Example for an identifier with matching items displayed next to it! 38 | @Entry(category = TEXT) public static ModPlatform modPlatform = ModPlatform.FABRIC; // Example for an enum option 39 | public enum ModPlatform { // Enums allow the user to cycle through predefined options 40 | QUILT, FABRIC, FORGE, NEOFORGE, VANILLA 41 | } 42 | @Entry(category = TEXT) public static GraphicsSteps graphicsSteps = GraphicsSteps.FABULOUS; // Example for an enum option with TranslatableOption 43 | 44 | @Comment(category = TEXT, name = "§nMidnightLib Wiki", centered = true, url = "https://www.midnightdust.eu/wiki/midnightlib/") public static Comment wiki; // Example for a comment with a url 45 | 46 | @Entry(category = NUMBERS) public static int fabric = 16777215; // Example for an int option 47 | @Entry(category = NUMBERS) public static double world = 1.4D; // Example for a double option 48 | @Entry(category = NUMBERS, min=69,max=420) public static int hello = 420; // - The entered number has to be larger than 69 and smaller than 420 49 | @Entry(category = SLIDERS, name = "I am an int slider.",isSlider = true, min = 0, max = 100) public static int intSlider = 35; // Int fields can also be displayed as a Slider 50 | @Entry(category = SLIDERS, name = "I am a float slider!", isSlider = true, min = 0f, max = 1f, precision = 1000) public static float floatSlider = 0.24f; // And so can floats! Precision defines the amount of decimal places 51 | @Entry(category = SLIDERS, name = "I am a non-primitive double slider!", isSlider = true, min = 0d, max = 4d, precision = 10000) public static Double nonPrimitiveDoubleSlider = 3.76d; // Even works for non-primitive fields 52 | // The name field can be used to specify a custom translation string or plain text 53 | @Entry(category = LISTS, name = "I am a string list!") public static List stringList = Lists.newArrayList("String1", "String2"); // Array String Lists are also supported 54 | @Entry(category = LISTS, isColor = true, name = "I am a color list!") public static List colorList = Lists.newArrayList("#ac5f99", "#11aa44"); // Lists also support colors 55 | @Entry(category = LISTS, name = "I am an identifier list!", idMode = 1) public static List idList = Lists.newArrayList(Identifier.ofVanilla("dirt")); // A list of block identifiers 56 | @Entry(category = LISTS, name = "I am an integer list!") public static List intList = Lists.newArrayList(69, 420); 57 | @Entry(category = LISTS, name = "I am a float list!") public static List floatList = Lists.newArrayList(4.1f, -1.3f, -1f); 58 | 59 | @Entry(category = FILES, 60 | selectionMode = JFileChooser.FILES_ONLY, 61 | fileExtensions = {"json", "txt", "log"}, // Define valid file extensions 62 | fileChooserType = JFileChooser.SAVE_DIALOG, 63 | name = "I am a file!") 64 | public static String myFile = ""; // The isFile property adds a file picker button 65 | 66 | @Entry(category = FILES, 67 | selectionMode = JFileChooser.DIRECTORIES_ONLY, 68 | fileChooserType = JFileChooser.OPEN_DIALOG, 69 | name = "I am a directory!") 70 | public static String myDirectory = ""; // The isDirectory property adds a directory picker button 71 | 72 | @Entry(category = FILES, 73 | selectionMode = JFileChooser.FILES_AND_DIRECTORIES, 74 | fileExtensions = {"png", "jpg", "jpeg"}, 75 | fileChooserType = JFileChooser.OPEN_DIALOG, 76 | name = "I can choose both files & directories!") 77 | public static String myFileOrDirectory = ""; // The isFileOrDirectory property adds a file or directory picker button 78 | @Entry(category = FILES, 79 | selectionMode = JFileChooser.FILES_AND_DIRECTORIES, 80 | fileExtensions = {"png", "jpg", "jpeg"}, 81 | fileChooserType = JFileChooser.OPEN_DIALOG, 82 | name = "I am a mf file/directory list!") 83 | public static List fileOrDirectoryList = new ArrayList<>(); // Yes, that's right – you can even have lists of files/directories 84 | 85 | @Condition(requiredModId = "midnightlib") // Conditional options are here! 86 | @Entry(category = CONDITIONS, name="Turn me on!") 87 | public static boolean turnMeOn = false; 88 | @Condition(requiredOption = "modid:turnMeOn", visibleButLocked = true) 89 | @Entry(category = CONDITIONS, name="Turn me off (locked)!") 90 | public static Boolean turnMeOff = true; 91 | @Condition(requiredOption = "turnMeOn") // You can also use multiple conditions for the same entry 92 | @Condition(requiredOption = "modid:turnMeOff", requiredValue = "false") 93 | @Entry(category = CONDITIONS, name="Which is the best modloader?") 94 | public static String bestModloader = ""; 95 | @Condition(requiredOption = "turnMeOn") 96 | @Condition(requiredOption = "turnMeOff", requiredValue = "false") 97 | @Condition(requiredOption = "bestModloader", requiredValue = "Forge") 98 | @Comment(category = CONDITIONS, name="❌ You have bad taste :(", centered = true) // Don't take this too seriously btw :) 99 | public static Comment answerForge; // Comments can also be conditional! 100 | @Condition(requiredOption = "turnMeOn") 101 | @Condition(requiredOption = "turnMeOff", requiredValue = "false") 102 | @Condition(requiredOption = "bestModloader", requiredValue = "NeoForge") 103 | @Comment(category = CONDITIONS, name="⛏ Not quite, but it's alright!", centered = true) 104 | public static Comment answerNeoforge; 105 | @Condition(requiredOption = "turnMeOn") 106 | @Condition(requiredOption = "turnMeOff", requiredValue = "false") 107 | @Condition(requiredOption = "bestModloader", requiredValue = "Fabric") 108 | @Comment(category = CONDITIONS, name="⭐ Correct! Fabric (and Quilt) are the best!", centered = true) 109 | public static Comment answerFabric; 110 | @Condition(requiredOption = "turnMeOn") 111 | @Condition(requiredOption = "turnMeOff", requiredValue = "false") 112 | @Condition(requiredOption = "bestModloader", requiredValue = "Quilt") 113 | @Comment(category = CONDITIONS, name="⭐ Correct! Quilt (and Fabric) are the best!", centered = true) 114 | public static Comment answerQuilt; 115 | 116 | @Entry(category = CONDITIONS, name="Enter any prime number below 10") 117 | public static int primeNumber = 0; 118 | @Comment(category = CONDITIONS, name="Correct!") 119 | @Condition(requiredOption = "primeNumber", requiredValue = {"2", "3", "5", "7"}) 120 | public static Comment answerPrime; 121 | 122 | @Condition(requiredOption = "midnightlib:config_screen_list", requiredValue = "FALSE") // Access options of other mods that are also using MidnightLib 123 | @Comment(category = CONDITIONS) public static Comment spaceracer; 124 | @Condition(requiredOption = "midnightlib:config_screen_list", requiredValue = "FALSE") 125 | @Comment(category = CONDITIONS, name="You disabled MidnightLib's config screen list. Why? :(", centered = true) public static Comment why; 126 | 127 | public static int imposter = 16777215; // - Entries without an @Entry or @Comment annotation are ignored 128 | 129 | public enum GraphicsSteps implements TranslatableOption { 130 | FAST(0, "options.graphics.fast"), 131 | FANCY(1, "options.graphics.fancy"), 132 | FABULOUS(2, "options.graphics.fabulous"); 133 | 134 | private final int id; 135 | private final String translationKey; 136 | 137 | GraphicsSteps(int id, String translationKey) { 138 | this.id = id; 139 | this.translationKey = translationKey; 140 | } 141 | 142 | @Override 143 | public Text getText() { 144 | MutableText mutableText = Text.translatable(this.getTranslationKey()); 145 | return this == GraphicsSteps.FABULOUS ? mutableText.formatted(Formatting.ITALIC).formatted(Formatting.AQUA) : mutableText; 146 | } 147 | 148 | @Override 149 | public int getId() { 150 | return this.id; 151 | } 152 | 153 | @Override 154 | public String getTranslationKey() { 155 | return this.translationKey; 156 | } 157 | } 158 | 159 | @Condition(requiredModId = "thismoddoesnotexist") 160 | @Comment(category = EXTRAS) public static Comment iAmJustADummy; // We only have this to initialize an empty tab for the keybinds below 161 | 162 | @Override 163 | public void onTabInit(String tabName, MidnightConfigListWidget list, MidnightConfigScreen screen) { 164 | if (Objects.equals(tabName, EXTRAS)) { 165 | MidnightLibExtras.KeybindButton.add(MinecraftClient.getInstance().options.advancementsKey, list, screen); 166 | MidnightLibExtras.KeybindButton.add(MinecraftClient.getInstance().options.dropKey, list, screen); 167 | } 168 | } 169 | 170 | } -------------------------------------------------------------------------------- /test-fabric/src/main/resources/assets/modid/lang/en_us.json: -------------------------------------------------------------------------------- 1 | { 2 | "modid.midnightconfig.title":"I am a title", 3 | "modid.midnightconfig.text1":"I am a comment *u*", 4 | "modid.midnightconfig.text2":"I am a centered comment (╯°□°)╯︵ ┻━┻", 5 | "modid.midnightconfig.name":"I am a string!", 6 | "modid.midnightconfig.name.label.tooltip":"I am a label tooltip \nWohoo!", 7 | "modid.midnightconfig.name.tooltip":"I am a tooltip uwu \nI am a new line", 8 | "modid.midnightconfig.fabric":"I am an int", 9 | "modid.midnightconfig.world":"I am a double", 10 | "modid.midnightconfig.showInfo":"I am a boolean", 11 | "modid.midnightconfig.hello":"I am a limited int!", 12 | "modid.midnightconfig.id":"I am an Item Identifier!", 13 | "modid.midnightconfig.modPlatform":"I am an enum!", 14 | "modid.midnightconfig.enum.ModPlatform.FORGE":"Forge", 15 | "modid.midnightconfig.enum.ModPlatform.FABRIC":"Fabric", 16 | "modid.midnightconfig.enum.ModPlatform.QUILT":"Quilt", 17 | "modid.midnightconfig.enum.ModPlatform.NEOFORGE":"NeoForge", 18 | "modid.midnightconfig.enum.ModPlatform.VANILLA":"Vanilla", 19 | "modid.midnightconfig.graphicsSteps":"I am an enum with TranslatableOption!", 20 | "modid.midnightconfig.myFileOrDirectory.fileChooser": "Select an image or directory", 21 | "modid.midnightconfig.myFileOrDirectory.fileFilter": "Supported Images (.png, .jpg, .jpeg)", 22 | "modid.midnightconfig.category.numbers": "Numbers", 23 | "modid.midnightconfig.category.text": "Text", 24 | "modid.midnightconfig.category.sliders": "Sliders", 25 | "modid.midnightconfig.category.lists": "Lists", 26 | "modid.midnightconfig.category.files": "Files", 27 | "modid.midnightconfig.category.conditions": "Quiz", 28 | "modid.midnightconfig.category.extras": "Extras", 29 | "modid.midnightconfig.category.multiConditions": "Multi-Conditions" 30 | } -------------------------------------------------------------------------------- /test-fabric/src/main/resources/assets/modid/lang/es_ar.json: -------------------------------------------------------------------------------- 1 | { 2 | "modid.midnightconfig.title": "Soy un título", 3 | "modid.midnightconfig.text1": "Soy un comentario *u*", 4 | "modid.midnightconfig.text2": "Soy un comentario centrado (╯°□°)╯︵ ┻━┻", 5 | "modid.midnightconfig.name": "¡Soy una cadena de texto!", 6 | "modid.midnightconfig.name.label.tooltip": "Soy el tooltip de una etiqueta \n¡Wujuu!", 7 | "modid.midnightconfig.name.tooltip": "Soy un tooltip uwu \nY una nueva línea", 8 | "modid.midnightconfig.fabric": "Soy un entero", 9 | "modid.midnightconfig.world": "Soy un número decimal", 10 | "modid.midnightconfig.showInfo": "Soy un booleano", 11 | "modid.midnightconfig.hello": "¡Soy un entero limitado!", 12 | "modid.midnightconfig.id": "¡Soy un identificador de ítem!", 13 | "modid.midnightconfig.modPlatform": "¡Soy un enumerador!", 14 | "modid.midnightconfig.enum.ModPlatform.FORGE": "Forge", 15 | "modid.midnightconfig.enum.ModPlatform.FABRIC": "Fabric", 16 | "modid.midnightconfig.enum.ModPlatform.QUILT": "Quilt", 17 | "modid.midnightconfig.enum.ModPlatform.NEOFORGE": "NeoForge", 18 | "modid.midnightconfig.enum.ModPlatform.VANILLA": "Vanilla", 19 | "modid.midnightconfig.myFileOrDirectory.fileChooser": "Seleccioná una imagen o carpeta", 20 | "modid.midnightconfig.myFileOrDirectory.fileFilter": "Imágenes compatibles (.png, .jpg, .jpeg)", 21 | "modid.midnightconfig.category.numbers": "Números", 22 | "modid.midnightconfig.category.text": "Texto", 23 | "modid.midnightconfig.category.sliders": "Deslizadores", 24 | "modid.midnightconfig.category.lists": "Listas", 25 | "modid.midnightconfig.category.files": "Archivos", 26 | "modid.midnightconfig.category.conditions": "Cuestionario", 27 | "modid.midnightconfig.category.multiConditions": "Condiciones múltiples" 28 | } 29 | -------------------------------------------------------------------------------- /test-fabric/src/main/resources/fabric.mod.json: -------------------------------------------------------------------------------- 1 | { 2 | "schemaVersion": 1, 3 | "id": "midnightlib-example", 4 | "version": "${version}", 5 | 6 | "name": "MidnightLib Example", 7 | "description": "Wow, you can do so much.", 8 | "authors": [ "MidnightDust" ], 9 | 10 | "license": "CC0", 11 | "icon": "assets/midnightlib/icon.png", 12 | 13 | "environment": "*", 14 | "entrypoints": { 15 | "main": [ 16 | "eu.midnightdust.fabric.example.MLExampleFabric" 17 | ] 18 | }, 19 | "depends": { 20 | "fabric-resource-loader-v0": "*", 21 | "midnightlib": ">=1.6.0" 22 | } 23 | } 24 | --------------------------------------------------------------------------------