This commit is contained in:
itqop 2025-10-18 17:31:16 +03:00
commit 80a175e5eb
12 changed files with 860 additions and 0 deletions

119
.gitignore vendored Normal file
View File

@ -0,0 +1,119 @@
# User-specific stuff
.idea/
*.iml
*.ipr
*.iws
# IntelliJ
out/
# mpeltonen/sbt-idea plugin
.idea_modules/
# JIRA plugin
atlassian-ide-plugin.xml
# Compiled class file
*.class
# Log file
*.log
# BlueJ files
*.ctxt
# Package Files #
*.jar
*.war
*.nar
*.ear
*.zip
*.tar.gz
*.rar
# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml
hs_err_pid*
*~
# temporary files which can be created if a process still has a handle open of a deleted file
.fuse_hidden*
# KDE directory preferences
.directory
# Linux trash folder which might appear on any partition or disk
.Trash-*
# .nfs files are created when an open file is removed but is still being accessed
.nfs*
# General
.DS_Store
.AppleDouble
.LSOverride
# Icon must end with two \r
Icon
# Thumbnails
._*
# Files that might appear in the root of a volume
.DocumentRevisions-V100
.fseventsd
.Spotlight-V100
.TemporaryItems
.Trashes
.VolumeIcon.icns
.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
.AppleDesktop
Network Trash Folder
Temporary Items
.apdisk
# Windows thumbnail cache files
Thumbs.db
Thumbs.db:encryptable
ehthumbs.db
ehthumbs_vista.db
# Dump file
*.stackdump
# Folder config file
[Dd]esktop.ini
# Recycle Bin used on file shares
$RECYCLE.BIN/
# Windows Installer files
*.cab
*.msi
*.msix
*.msm
*.msp
# Windows shortcuts
*.lnk
.gradle
build/
# Ignore Gradle GUI config
gradle-app.setting
# Cache of project
.gradletasknamecache
**/build/
# Common working directory
run/
runs/
# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
!gradle-wrapper.jar

172
build.gradle Normal file
View File

@ -0,0 +1,172 @@
plugins {
id 'java-library'
id 'maven-publish'
id 'idea'
id 'net.neoforged.moddev' version '2.0.115'
}
version = mod_version
group = mod_group_id
repositories {
mavenLocal()
}
base {
archivesName = mod_id
}
java.toolchain.languageVersion = JavaLanguageVersion.of(21)
neoForge {
// Specify the version of NeoForge to use.
version = project.neo_version
parchment {
mappingsVersion = project.parchment_mappings_version
minecraftVersion = project.parchment_minecraft_version
}
// This line is optional. Access Transformers are automatically detected
// accessTransformers.add('src/main/resources/META-INF/accesstransformer.cfg')
// Default run configurations.
// These can be tweaked, removed, or duplicated as needed.
runs {
client {
client()
// Comma-separated list of namespaces to load gametests from. Empty = all namespaces.
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
server {
server()
programArgument '--nogui'
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
// This run config launches GameTestServer and runs all registered gametests, then exits.
// By default, the server will crash when no gametests are provided.
// The gametest system is also enabled by default for other run configs under the /test command.
gameTestServer {
type = "gameTestServer"
systemProperty 'neoforge.enabledGameTestNamespaces', project.mod_id
}
data {
data()
// example of overriding the workingDirectory set in configureEach above, uncomment if you want to use it
// gameDirectory = project.file('run-data')
// Specify the modid for data generation, where to output the resulting resource, and where to look for existing resources.
programArguments.addAll '--mod', project.mod_id, '--all', '--output', file('src/generated/resources/').getAbsolutePath(), '--existing', file('src/main/resources/').getAbsolutePath()
}
// applies to all the run configs above
configureEach {
// Recommended logging data for a userdev environment
// The markers can be added/remove as needed separated by commas.
// "SCAN": For mods scan.
// "REGISTRIES": For firing of registry events.
// "REGISTRYDUMP": For getting the contents of all registries.
systemProperty 'forge.logging.markers', 'REGISTRIES'
// Recommended logging level for the console
// You can set various levels here.
// Please read: https://stackoverflow.com/questions/2031163/when-to-use-the-different-log-levels
logLevel = org.slf4j.event.Level.DEBUG
}
}
mods {
// define mod <-> source bindings
// these are used to tell the game which sources are for which mod
// mostly optional in a single mod project
// but multi mod projects should define one per mod
"${mod_id}" {
sourceSet(sourceSets.main)
}
}
}
// Include resources generated by data generators.
sourceSets.main.resources { srcDir 'src/generated/resources' }
dependencies {
// Gson для работы с JSON
implementation 'com.google.code.gson:gson:2.10.1'
// Example mod dependency with JEI
// The JEI API is declared for compile time use, while the full JEI artifact is used at runtime
// compileOnly "mezz.jei:jei-${mc_version}-common-api:${jei_version}"
// compileOnly "mezz.jei:jei-${mc_version}-forge-api:${jei_version}"
// runtimeOnly "mezz.jei:jei-${mc_version}-forge:${jei_version}"
// Example mod dependency using a mod jar from ./libs with a flat dir repository
// This maps to ./libs/coolmod-${mc_version}-${coolmod_version}.jar
// The group id is ignored when searching -- in this case, it is "blank"
// implementation "blank:coolmod-${mc_version}:${coolmod_version}"
// Example mod dependency using a file as dependency
// implementation files("libs/coolmod-${mc_version}-${coolmod_version}.jar")
// Example project dependency using a sister or child project:
// implementation project(":myproject")
// For more info:
// http://www.gradle.org/docs/current/userguide/artifact_dependencies_tutorial.html
// http://www.gradle.org/docs/current/userguide/dependency_management.html
}
// This block of code expands all declared replace properties in the specified resource targets.
// A missing property will result in an error. Properties are expanded using ${} Groovy notation.
var generateModMetadata = tasks.register("generateModMetadata", ProcessResources) {
var replaceProperties = [
minecraft_version : minecraft_version,
minecraft_version_range: minecraft_version_range,
neo_version : neo_version,
neo_version_range : neo_version_range,
loader_version_range : loader_version_range,
mod_id : mod_id,
mod_name : mod_name,
mod_license : mod_license,
mod_version : mod_version,
mod_authors : mod_authors,
mod_description : mod_description
]
inputs.properties replaceProperties
expand replaceProperties
from "src/main/templates"
into "build/generated/sources/modMetadata"
}
// Include the output of "generateModMetadata" as an input directory for the build
// this works with both building through Gradle and the IDE.
sourceSets.main.resources.srcDir generateModMetadata
// To avoid having to run "generateModMetadata" manually, make it run on every project reload
neoForge.ideSyncTask generateModMetadata
// Example configuration to allow publishing using the maven-publish plugin
publishing {
publications {
register('mavenJava', MavenPublication) {
from components.java
}
}
repositories {
maven {
url "file://${project.projectDir}/repo"
}
}
}
// IDEA no longer automatically downloads sources/javadoc jars for dependencies, so we need to explicitly enable the behavior.
idea {
module {
downloadSources = true
downloadJavadoc = true
}
}

40
gradle.properties Normal file
View File

@ -0,0 +1,40 @@
# Sets default memory used for gradle commands. Can be overridden by user or command line properties.
org.gradle.jvmargs=-Xmx2G
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.caching=true
org.gradle.configuration-cache=true
## Environment Properties
# You can find the latest versions here: https://projects.neoforged.net/neoforged/neoforge
# The Minecraft version must agree with the Neo version to get a valid artifact
minecraft_version=1.21.1
# The Minecraft version range can use any release version of Minecraft as bounds.
# Snapshots, pre-releases, and release candidates are not guaranteed to sort properly
# as they do not follow standard versioning conventions.
minecraft_version_range=[1.21.1,1.22)
# The Neo version must agree with the Minecraft version to get a valid artifact
neo_version=21.1.209
# The Neo version range can use any version of Neo as bounds
neo_version_range=[21,)
# The loader version range can only use the major version of FML as bounds
loader_version_range=[4,)
parchment_minecraft_version=1.21.1
parchment_mappings_version=2024.11.13
## Mod Properties
# The unique mod identifier for the mod. Must be lowercase in English locale. Must fit the regex [a-z][a-z0-9_]{1,63}
# Must match the String constant located in the main mod class annotated with @Mod.
mod_id=whitelist
# The human-readable display name for the mod.
mod_name=whitelist
# The license of the mod. Review your options at https://choosealicense.com/. All Rights Reserved is the default.
mod_license=All Rights Reserved
# The mod version. See https://semver.org/
mod_version=0.1-BETA
# The group ID for the mod. It is only important when publishing as an artifact to a Maven repository.
# This should match the base package used for the mod sources.
# See https://maven.apache.org/guides/mini/guide-naming-conventions.html
mod_group_id=org.itqop
# The authors of the mod. This is a simple text string that is used for display purposes in the mod list.
mod_authors=itqop
# The description of the mod. This is a simple multiline text string that is used for display purposes in the mod list.
mod_description=whitelist

View File

@ -0,0 +1 @@
distributionUrl=https\://services.gradle.org/distributions/gradle-8.8-bin.zip

11
settings.gradle Normal file
View File

@ -0,0 +1,11 @@
pluginManagement {
repositories {
mavenLocal()
gradlePluginPortal()
maven { url = 'https://maven.neoforged.net/releases' }
}
}
plugins {
id 'org.gradle.toolchains.foojay-resolver-convention' version '0.8.0'
}

View File

@ -0,0 +1,45 @@
package org.itqop.whitelist;
import net.neoforged.fml.event.config.ModConfigEvent;
import net.neoforged.neoforge.common.ModConfigSpec;
public final class Config {
private static final ModConfigSpec.Builder BUILDER = new ModConfigSpec.Builder();
// API настройки
private static final ModConfigSpec.ConfigValue<String> API_BASE_URL = BUILDER
.comment("Base URL for whitelist API")
.define("apiBaseUrl", "http://localhost:8080/api/v1/whitelist");
private static final ModConfigSpec.ConfigValue<String> API_KEY = BUILDER
.comment("API key for whitelist service")
.define("apiKey", "your-secret-api-key");
private static final ModConfigSpec.IntValue REQUEST_TIMEOUT = BUILDER
.comment("Request timeout in seconds")
.defineInRange("requestTimeout", 30, 5, 300);
private static final ModConfigSpec.BooleanValue ENABLE_LOGGING = BUILDER
.comment("Enable detailed API logging")
.define("enableLogging", true);
static final ModConfigSpec SPEC = BUILDER.build();
// кэш значений
public static String apiBaseUrl;
public static String apiKey;
public static int requestTimeout;
public static boolean enableLogging;
private Config() {}
// подписка идёт через modEventBus.addListener(Config::onLoad) без @EventBusSubscriber и без устаревшего 'bus'
static void onLoad(final ModConfigEvent event) {
if (event.getConfig().getSpec() != SPEC) return;
apiBaseUrl = API_BASE_URL.get();
apiKey = API_KEY.get();
requestTimeout = REQUEST_TIMEOUT.get();
enableLogging = ENABLE_LOGGING.get();
}
}

View File

@ -0,0 +1,44 @@
package org.itqop.whitelist;
import com.mojang.logging.LogUtils;
import net.minecraft.core.registries.Registries;
import net.minecraft.world.item.CreativeModeTab;
import net.neoforged.bus.api.IEventBus;
import net.neoforged.fml.ModContainer;
import net.neoforged.fml.common.Mod;
import net.neoforged.fml.config.ModConfig;
import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent;
import net.neoforged.neoforge.event.BuildCreativeModeTabContentsEvent;
import net.neoforged.neoforge.registries.DeferredRegister;
import org.slf4j.Logger;
@Mod(Whitelist.MODID)
public class Whitelist {
public static final String MODID = "whitelist";
private static final Logger LOGGER = LogUtils.getLogger();
public static final DeferredRegister.Blocks BLOCKS = DeferredRegister.createBlocks(MODID);
public static final DeferredRegister.Items ITEMS = DeferredRegister.createItems(MODID);
public static final DeferredRegister<CreativeModeTab> CREATIVE_MODE_TABS =
DeferredRegister.create(Registries.CREATIVE_MODE_TAB, MODID);
public Whitelist(IEventBus modEventBus, ModContainer modContainer) {
modEventBus.addListener(this::commonSetup);
modEventBus.addListener(this::addCreative);
modEventBus.addListener(Config::onLoad);
BLOCKS.register(modEventBus);
ITEMS.register(modEventBus);
CREATIVE_MODE_TABS.register(modEventBus);
modContainer.registerConfig(ModConfig.Type.COMMON, Config.SPEC);
}
private void commonSetup(final FMLCommonSetupEvent event) {
LOGGER.info("whitelist initialized");
}
private void addCreative(BuildCreativeModeTabContentsEvent event) {
}
}

View File

@ -0,0 +1,165 @@
package org.itqop.whitelist;
import com.google.gson.Gson;
import com.google.gson.JsonObject;
import com.google.gson.JsonParser;
import com.mojang.logging.LogUtils;
import org.slf4j.Logger;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.time.Duration;
import java.time.Instant;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
public class WhitelistApiClient {
private static final Logger LOGGER = LogUtils.getLogger();
private static final Gson GSON = new Gson();
private final HttpClient httpClient;
private final String baseUrl;
private final String apiKey;
private final int timeoutSeconds;
private final boolean enableLogging;
public WhitelistApiClient() {
this.baseUrl = Config.apiBaseUrl;
this.apiKey = Config.apiKey;
this.timeoutSeconds = Config.requestTimeout;
this.enableLogging = Config.enableLogging;
this.httpClient = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(timeoutSeconds))
.build();
}
public CompletableFuture<Boolean> addPlayer(String playerName, UUID playerUuid, String addedBy, String reason) {
JsonObject requestBody = new JsonObject();
requestBody.addProperty("player_name", playerName);
requestBody.addProperty("player_uuid", playerUuid.toString());
requestBody.addProperty("added_by", addedBy);
requestBody.addProperty("added_at", Instant.now().toString());
requestBody.addProperty("is_active", true);
if (reason != null && !reason.isEmpty()) {
requestBody.addProperty("reason", reason);
}
return makeRequest("POST", "/add", requestBody.toString())
.thenApply(response -> {
if (response.statusCode() == 201) {
if (enableLogging) {
LOGGER.info("Player {} successfully added to whitelist", playerName);
}
return true;
} else {
LOGGER.error("Error adding player {}: HTTP {}", playerName, response.statusCode());
return false;
}
})
.exceptionally(throwable -> {
LOGGER.error("Error adding player {}: {}", playerName, throwable.getMessage());
return false;
});
}
public CompletableFuture<Boolean> removePlayer(String playerName) {
JsonObject requestBody = new JsonObject();
requestBody.addProperty("player_name", playerName);
return makeRequest("POST", "/remove", requestBody.toString())
.thenApply(response -> {
if (response.statusCode() == 204) {
if (enableLogging) {
LOGGER.info("Player {} successfully removed from whitelist", playerName);
}
return true;
} else {
LOGGER.error("Error removing player {}: HTTP {}", playerName, response.statusCode());
return false;
}
})
.exceptionally(throwable -> {
LOGGER.error("Error removing player {}: {}", playerName, throwable.getMessage());
return false;
});
}
public CompletableFuture<WhitelistCheckResult> checkPlayer(String playerName) {
JsonObject requestBody = new JsonObject();
requestBody.addProperty("player_name", playerName);
return makeRequest("POST", "/check", requestBody.toString())
.thenApply(response -> {
if (response.statusCode() == 200) {
try {
JsonObject jsonResponse = JsonParser.parseString(response.body()).getAsJsonObject();
boolean isWhitelisted = jsonResponse.get("is_whitelisted").getAsBoolean();
String playerUuid = jsonResponse.has("player_uuid") && !jsonResponse.get("player_uuid").isJsonNull()
? jsonResponse.get("player_uuid").getAsString()
: null;
if (enableLogging) {
LOGGER.info("Player {} check: {}", playerName, isWhitelisted ? "whitelisted" : "not whitelisted");
}
return new WhitelistCheckResult(true, isWhitelisted, playerUuid);
} catch (Exception e) {
LOGGER.error("Error parsing response for player {}: {}", playerName, e.getMessage());
return new WhitelistCheckResult(false, false, null);
}
} else {
LOGGER.error("Error checking player {}: HTTP {}", playerName, response.statusCode());
return new WhitelistCheckResult(false, false, null);
}
})
.exceptionally(throwable -> {
LOGGER.error("Error checking player {}: {}", playerName, throwable.getMessage());
return new WhitelistCheckResult(false, false, null);
});
}
private CompletableFuture<HttpResponse<String>> makeRequest(String method, String endpoint, String body) {
HttpRequest.Builder requestBuilder = HttpRequest.newBuilder()
.uri(URI.create(baseUrl + endpoint))
.header("X-API-Key", apiKey)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(timeoutSeconds));
if ("POST".equals(method)) {
requestBuilder.POST(HttpRequest.BodyPublishers.ofString(body));
} else {
requestBuilder.GET();
}
HttpRequest request = requestBuilder.build();
return httpClient.sendAsync(request, HttpResponse.BodyHandlers.ofString());
}
public static class WhitelistCheckResult {
private final boolean success;
private final boolean isWhitelisted;
private final String playerUuid;
public WhitelistCheckResult(boolean success, boolean isWhitelisted, String playerUuid) {
this.success = success;
this.isWhitelisted = isWhitelisted;
this.playerUuid = playerUuid;
}
public boolean isSuccess() {
return success;
}
public boolean isWhitelisted() {
return isWhitelisted;
}
public String getPlayerUuid() {
return playerUuid;
}
}
}

View File

@ -0,0 +1,161 @@
package org.itqop.whitelist;
import com.mojang.brigadier.CommandDispatcher;
import com.mojang.brigadier.arguments.StringArgumentType;
import com.mojang.brigadier.context.CommandContext;
import com.mojang.logging.LogUtils;
import net.minecraft.commands.CommandSourceStack;
import net.minecraft.commands.Commands;
import net.minecraft.network.chat.Component;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.server.MinecraftServer;
import org.slf4j.Logger;
import java.util.UUID;
public class WhitelistCommands {
private static final Logger LOGGER = LogUtils.getLogger();
private static WhitelistApiClient apiClient;
public static void register(CommandDispatcher<CommandSourceStack> dispatcher) {
apiClient = new WhitelistApiClient();
dispatcher.register(Commands.literal("whitelist")
.then(Commands.literal("add")
.then(Commands.argument("player", StringArgumentType.string())
.executes(WhitelistCommands::addPlayer)
.then(Commands.argument("reason", StringArgumentType.greedyString())
.executes(WhitelistCommands::addPlayerWithReason))))
.then(Commands.literal("remove")
.then(Commands.argument("player", StringArgumentType.string())
.executes(WhitelistCommands::removePlayer)))
.then(Commands.literal("check")
.then(Commands.argument("player", StringArgumentType.string())
.executes(WhitelistCommands::checkPlayer)))
.then(Commands.literal("list")
.executes(WhitelistCommands::listPlayers))
.then(Commands.literal("count")
.executes(WhitelistCommands::countPlayers)));
}
private static int addPlayer(CommandContext<CommandSourceStack> context) {
return addPlayerWithReason(context);
}
private static int addPlayerWithReason(CommandContext<CommandSourceStack> context) {
String playerName = StringArgumentType.getString(context, "player");
String reason = "";
try {
reason = StringArgumentType.getString(context, "reason");
} catch (Exception ignored) {
}
String addedBy = context.getSource().getTextName();
MinecraftServer server = context.getSource().getServer();
ServerPlayer player = server.getPlayerList().getPlayerByName(playerName);
UUID playerUuid = player != null ? player.getUUID() : null;
context.getSource().sendSuccess(() ->
Component.literal("Adding player " + playerName + " to whitelist..."), false);
assert playerUuid != null;
apiClient.addPlayer(playerName, playerUuid, addedBy, reason)
.thenAccept(success -> {
if (success) {
context.getSource().sendSuccess(() ->
Component.literal("Player " + playerName + " successfully added to whitelist"), true);
} else {
context.getSource().sendFailure(
Component.literal("Error adding player " + playerName + " to whitelist"));
}
})
.exceptionally(throwable -> {
LOGGER.error("Error adding player: ", throwable);
context.getSource().sendFailure(
Component.literal("Error adding player " + playerName + ": " + throwable.getMessage()));
return null;
});
return 1;
}
private static int removePlayer(CommandContext<CommandSourceStack> context) {
String playerName = StringArgumentType.getString(context, "player");
context.getSource().sendSuccess(() ->
Component.literal("Removing player " + playerName + " from whitelist..."), false);
apiClient.removePlayer(playerName)
.thenAccept(success -> {
if (success) {
context.getSource().sendSuccess(() ->
Component.literal("Player " + playerName + " successfully removed from whitelist"), true);
} else {
context.getSource().sendFailure(
Component.literal("Error removing player " + playerName + " from whitelist"));
}
})
.exceptionally(throwable -> {
LOGGER.error("Error removing player: ", throwable);
context.getSource().sendFailure(
Component.literal("Error removing player " + playerName + ": " + throwable.getMessage()));
return null;
});
return 1;
}
private static int checkPlayer(CommandContext<CommandSourceStack> context) {
String playerName = StringArgumentType.getString(context, "player");
context.getSource().sendSuccess(() ->
Component.literal("Checking player " + playerName + " status..."), false);
apiClient.checkPlayer(playerName)
.thenAccept(result -> {
if (result.isSuccess()) {
String status = result.isWhitelisted() ? "whitelisted" : "not whitelisted";
String uuidInfo = result.getPlayerUuid() != null ?
" (UUID: " + result.getPlayerUuid() + ")" : "";
context.getSource().sendSuccess(() ->
Component.literal("Player " + playerName + " is " + status + uuidInfo), true);
} else {
context.getSource().sendFailure(
Component.literal("Error checking player " + playerName + " status"));
}
})
.exceptionally(throwable -> {
LOGGER.error("Error checking player: ", throwable);
context.getSource().sendFailure(
Component.literal("Error checking player " + playerName + ": " + throwable.getMessage()));
return null;
});
return 1;
}
private static int listPlayers(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() ->
Component.literal("Getting whitelist..."), false);
// TODO: Implement full list retrieval
context.getSource().sendSuccess(() ->
Component.literal("List functionality will be implemented in the next version"), true);
return 1;
}
private static int countPlayers(CommandContext<CommandSourceStack> context) {
context.getSource().sendSuccess(() ->
Component.literal("Counting players in whitelist..."), false);
// TODO: Implement player counting
context.getSource().sendSuccess(() ->
Component.literal("Count functionality will be implemented in the next version"), true);
return 1;
}
}

View File

@ -0,0 +1,18 @@
package org.itqop.whitelist;
import com.mojang.logging.LogUtils;
import net.neoforged.bus.api.SubscribeEvent;
import net.neoforged.fml.common.EventBusSubscriber;
import net.neoforged.neoforge.event.RegisterCommandsEvent;
import org.slf4j.Logger;
@EventBusSubscriber(modid = Whitelist.MODID)
public class WhitelistEventHandler {
private static final Logger LOGGER = LogUtils.getLogger();
@SubscribeEvent
public static void registerCommands(RegisterCommandsEvent event) {
WhitelistCommands.register(event.getDispatcher());
LOGGER.info("Whitelist commands registered");
}
}

View File

@ -0,0 +1,5 @@
{
"itemGroup.whitelist": "Example Mod Tab",
"block.whitelist.example_block": "Example Block",
"item.whitelist.example_item": "Example Item"
}

View File

@ -0,0 +1,79 @@
# This is an example mods.toml file. It contains the data relating to the loading mods.
# There are several mandatory fields (#mandatory), and many more that are optional (#optional).
# The overall format is standard TOML format, v0.5.0.
# Note that there are a couple of TOML lists in this file.
# Find more information on toml format here: https://github.com/toml-lang/toml
# The name of the mod loader type to load - for regular FML @Mod mods it should be javafml
modLoader = "javafml" #mandatory
# A version range to match for said mod loader - for regular FML @Mod it will be the the FML version. This is currently 47.
loaderVersion = "${loader_version_range}" #mandatory
# The license for you mod. This is mandatory metadata and allows for easier comprehension of your redistributive properties.
# Review your options at https://choosealicense.com/. All rights reserved is the default copyright stance, and is thus the default here.
license = "${mod_license}"
# A URL to refer people to when problems occur with this mod
#issueTrackerURL="https://change.me.to.your.issue.tracker.example.invalid/" #optional
# A list of mods - how many allowed here is determined by the individual mod loader
[[mods]] #mandatory
# The modid of the mod
modId = "${mod_id}" #mandatory
# The version number of the mod
version = "${mod_version}" #mandatory
# A display name for the mod
displayName = "${mod_name}" #mandatory
# A URL to query for updates for this mod. See the JSON update specification https://docs.neoforge.net/docs/misc/updatechecker/
#updateJSONURL="https://change.me.example.invalid/updates.json" #optional
# A URL for the "homepage" for this mod, displayed in the mod UI
#displayURL="https://change.me.to.your.mods.homepage.example.invalid/" #optional
# A file name (in the root of the mod JAR) containing a logo for display
#logoFile="whitelist.png" #optional
# A text field displayed in the mod UI
#credits="" #optional
# A text field displayed in the mod UI
authors = "${mod_authors}" #optional
# The description text for the mod (multi line!) (#mandatory)
description = '''${mod_description}'''
# The [[mixins]] block allows you to declare your mixin config to FML so that it gets loaded.
#[[mixins]]
#config="${mod_id}.mixins.json"
# The [[accessTransformers]] block allows you to declare where your AT file is.
# If this block is omitted, a fallback attempt will be made to load an AT from META-INF/accesstransformer.cfg
#[[accessTransformers]]
#file="META-INF/accesstransformer.cfg"
# The coremods config file path is not configurable and is always loaded from META-INF/coremods.json
# A dependency - use the . to indicate dependency for a specific modid. Dependencies are optional.
[[dependencies."${mod_id}"]] #optional
# the modid of the dependency
modId = "neoforge" #mandatory
# The type of the dependency. Can be one of "required", "optional", "incompatible" or "discouraged" (case insensitive).
# 'required' requires the mod to exist, 'optional' does not
# 'incompatible' will prevent the game from loading when the mod exists, and 'discouraged' will show a warning
type = "required" #mandatory
# Optional field describing why the dependency is required or why it is incompatible
# reason="..."
# The version range of the dependency
versionRange = "${neo_version_range}" #mandatory
# An ordering relationship for the dependency.
# BEFORE - This mod is loaded BEFORE the dependency
# AFTER - This mod is loaded AFTER the dependency
ordering = "NONE"
# Side this dependency is applied on - BOTH, CLIENT, or SERVER
side = "BOTH"
# Here's another dependency
[[dependencies."${mod_id}"]]
modId = "minecraft"
type = "required"
# This version range declares a minimum of the current minecraft version up to but not including the next major version
versionRange = "${minecraft_version_range}"
ordering = "NONE"
side = "BOTH"
# Features are specific properties of the game environment, that you may want to declare you require. This example declares
# that your mod requires GL version 3.2 or higher. Other features will be added. They are side aware so declaring this won't
# stop your mod loading on the server for example.
#[features."${mod_id}"]
#openGLVersion="[3.2,)"