diff --git a/Fabric/build.gradle.kts b/Fabric/build.gradle.kts new file mode 100644 index 0000000..8ecf95b --- /dev/null +++ b/Fabric/build.gradle.kts @@ -0,0 +1,46 @@ +val modId: String by project +val minecraftVersion: String by project +val fabricVersion: String by project + +plugins { + idea + id("fabric-loom") version "0.9-SNAPSHOT" +} + +dependencies { + minecraft("com.mojang:minecraft:$minecraftVersion") + modImplementation("net.fabricmc:fabric-loader:$fabricVersion") + mappings(loom.officialMojangMappings()) +} + +loom { + runs { + named("client") { + configName = "Fabric Client" + client() + runDir("run") + ideConfigGenerated(true) + } + + named("server") { + configName = "Fabric Server" + server() + runDir("run") + ideConfigGenerated(true) + } + } + + mixin { + add(sourceSets.main.get(), "$modId.refmap.json") + } +} + +tasks.processResources { + filesMatching("fabric.mod.json") { + expand(inputs.properties) + } +} + +tasks.remapJar { + archiveVersion.set(tasks.jar.get().archiveVersion) +} diff --git a/Fabric/src/main/resources/fabric.mod.json b/Fabric/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..774da9d --- /dev/null +++ b/Fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,22 @@ +{ + "schemaVersion": 1, + "id": "serverpropertiesreload", + "name": "${name}", + "description": "${description}", + "version": "${version}", + "license": "${license}", + + "authors": [ + "${author}" + ], + + "contact": { + "issues": "${issuesURL}", + "sources": "${sourcesURL}" + }, + + "environment": "server", + "mixins": [ + "serverpropertiesreload.mixins.json" + ] +} diff --git a/Forge/build.gradle.kts b/Forge/build.gradle.kts new file mode 100644 index 0000000..5a6f511 --- /dev/null +++ b/Forge/build.gradle.kts @@ -0,0 +1,95 @@ +import net.minecraftforge.gradle.userdev.UserDevExtension +import org.gradle.api.file.DuplicatesStrategy.INCLUDE +import org.spongepowered.asm.gradle.plugins.MixinExtension + +val modId: String by project +val minecraftVersion: String by project +val forgeVersion: String by project +val mixinVersion: String by project + +buildscript { + repositories { + maven("https://maven.minecraftforge.net") + maven("https://repo.spongepowered.org/maven") + mavenCentral() + } + + dependencies { + classpath(group = "net.minecraftforge.gradle", name = "ForgeGradle", version = "5.1.+") { isChanging = true } + classpath(group = "org.spongepowered", name = "mixingradle", version = "0.7-SNAPSHOT") + } +} + +plugins { + java + eclipse +} + +apply { + plugin("net.minecraftforge.gradle") + plugin("org.spongepowered.mixin") +} + +dependencies { + "minecraft"("net.minecraftforge:forge:$minecraftVersion-$forgeVersion") + + if (System.getProperty("idea.sync.active") != "true") { + annotationProcessor("org.spongepowered:mixin:$mixinVersion:processor") + } +} + +configure<UserDevExtension> { + mappings("official", minecraftVersion) + + runs { + create("client") { + taskName = "Client" + workingDirectory(rootProject.file("run")) + ideaModule("${rootProject.name}.${project.name}.main") + + property("mixin.env.remapRefMap", "true") + property("mixin.env.refMapRemappingFile", "$projectDir/build/createSrgToMcp/output.srg") + arg("-mixin.config=$modId.mixins.json") + + mods { + create(modId) { + source(sourceSets.main.get()) + source(rootProject.sourceSets.main.get()) + } + } + } + + create("server") { + taskName = "Server" + workingDirectory(rootProject.file("run")) + ideaModule("${rootProject.name}.${project.name}.main") + + property("mixin.env.remapRefMap", "true") + property("mixin.env.refMapRemappingFile", "$projectDir/build/createSrgToMcp/output.srg") + arg("-mixin.config=$modId.mixins.json") + + mods { + create(modId) { + source(sourceSets.main.get()) + source(rootProject.sourceSets.main.get()) + } + } + } + } +} + +configure<MixinExtension> { + add(sourceSets.main.get(), "$modId.refmap.json") +} + +tasks.processResources { + from(sourceSets.main.get().resources.srcDirs) { + include("META-INF/mods.toml") + expand(inputs.properties) + duplicatesStrategy = INCLUDE + } +} + +tasks.jar { + finalizedBy("reobfJar") +} diff --git a/Forge/src/main/java/chylex/serverproperties/ForgeMod.java b/Forge/src/main/java/chylex/serverproperties/ForgeMod.java new file mode 100644 index 0000000..7592a2b --- /dev/null +++ b/Forge/src/main/java/chylex/serverproperties/ForgeMod.java @@ -0,0 +1,12 @@ +package chylex.serverproperties; +import net.minecraftforge.fml.IExtensionPoint.DisplayTest; +import net.minecraftforge.fml.ModLoadingContext; +import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fmllegacy.network.FMLNetworkConstants; + +@Mod("serverpropertiesreload") +public final class ForgeMod { + public ForgeMod() { + ModLoadingContext.get().registerExtensionPoint(DisplayTest.class, () -> new DisplayTest(() -> FMLNetworkConstants.IGNORESERVERONLY, (a, b) -> true)); + } +} diff --git a/Forge/src/main/resources/META-INF/mods.toml b/Forge/src/main/resources/META-INF/mods.toml new file mode 100644 index 0000000..d9211a0 --- /dev/null +++ b/Forge/src/main/resources/META-INF/mods.toml @@ -0,0 +1,26 @@ +modLoader = "javafml" +loaderVersion = "[37,)" + +authors = "${author}" +license = "${license}" +issueTrackerURL = "${issuesURL}" + +[[mods]] +modId = "serverpropertiesreload" +version = "${version}" +displayName = "${name}" +description = "${description}" + +[[dependencies.serverpropertiesreload]] +modId = "minecraft" +mandatory = true +versionRange = "[1.17.1,)" +ordering = "NONE" +side = "BOTH" + +[[dependencies.serverpropertiesreload]] +modId = "forge" +mandatory = true +versionRange = "[37,)" +ordering = "NONE" +side = "BOTH" diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..54467bc --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,146 @@ +import org.gradle.api.file.DuplicatesStrategy.EXCLUDE +import java.text.SimpleDateFormat +import java.util.Date + +val modId: String by project +val modName: String by project +val modDescription: String by project +val modAuthor: String by project +val modVersion: String by project +val modLicense: String by project +val modSourcesURL: String by project +val modIssuesURL: String by project + +val minecraftVersion: String by project +val mixinVersion: String by project + +val modNameStripped = modName.replace(" ", "") +val jarVersion = "$minecraftVersion+v$modVersion" + +buildscript { + repositories { + maven("https://repo.spongepowered.org/maven") + } +} + +plugins { + `java-library` + idea + id("org.spongepowered.gradle.vanilla") version "0.2.1-SNAPSHOT" +} + +idea { + module { + excludeDirs.add(project.file("run")) + } +} + +repositories { + maven("https://repo.spongepowered.org/maven") + mavenCentral() +} + +dependencies { + implementation("org.spongepowered:mixin:$mixinVersion") + api("com.google.code.findbugs:jsr305:3.0.2") +} + +base { + archivesName.set("$modNameStripped-Common") +} + +minecraft { + version(minecraftVersion) + runs.clear() +} + +allprojects { + group = "com.$modAuthor.$modId" + version = modVersion + + apply(plugin = "java") + + dependencies { + implementation("org.jetbrains:annotations:22.0.0") + } + + extensions.getByType<JavaPluginExtension>().apply { + toolchain.languageVersion.set(JavaLanguageVersion.of(16)) + } + + tasks.withType<JavaCompile> { + options.encoding = "UTF-8" + options.release.set(16) + } +} + +subprojects { + repositories { + maven("https://repo.spongepowered.org/maven") + } + + dependencies { + implementation(rootProject) + } + + base { + archivesName.set("$modNameStripped-${project.name}") + } + + tasks.withType<JavaCompile> { + source({ rootProject.sourceSets.main.get().allSource }) + } + + tasks.processResources { + from(rootProject.sourceSets.main.get().resources) + + inputs.property("name", modName) + inputs.property("description", modDescription) + inputs.property("version", modVersion) + inputs.property("author", modAuthor) + inputs.property("license", modLicense) + inputs.property("sourcesURL", modSourcesURL) + inputs.property("issuesURL", modIssuesURL) + } + + tasks.jar { + archiveVersion.set(jarVersion) + + from(rootProject.file("LICENSE")) + + manifest { + attributes( + "Specification-Title" to modId, + "Specification-Vendor" to modAuthor, + "Specification-Version" to "1", + "Implementation-Title" to "$modNameStripped-${project.name}", + "Implementation-Vendor" to modAuthor, + "Implementation-Version" to modVersion, + "Implementation-Timestamp" to SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ssZ").format(Date()), + "MixinConfigs" to "$modId.mixins.json" + ) + } + } +} + +tasks.register("setupIdea") { + group = "mod" + dependsOn(project(":Forge").tasks.getByName("genIntellijRuns")) + dependsOn(project(":Fabric").tasks.getByName("genSources")) +} + +val copyJars = tasks.register<Copy>("copyJars") { + group = "build" + duplicatesStrategy = EXCLUDE + + for (subproject in subprojects) { + dependsOn(subproject.tasks.build) + from(subproject.base.libsDirectory.file("${subproject.base.archivesName.get()}-$jarVersion.jar")) + } + + into(file("${project.buildDir}/dist")) +} + +tasks.build { + finalizedBy(copyJars) +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..877abb8 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,19 @@ +# Mod +modId=serverpropertiesreload +modName=Server Properties Reload +modDescription=Adds '/properties' command to modify or reload server properties +modAuthor=chylex +modVersion=1.0.0 +modLicense=MPL-2.0 +modSourcesURL=https://github.com/chylex/Minecraft-Server-Properties-Reload +modIssuesURL=https://github.com/chylex/Minecraft-Server-Properties-Reload/issues + +# Dependencies +minecraftVersion=1.17.1 +forgeVersion=37.0.75 +fabricVersion=0.11.7 +mixinVersion=0.8.4 + +# Gradle +org.gradle.jvmargs=-Xmx3G +org.gradle.daemon=false diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..89ed03c --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,17 @@ +rootProject.name = "Server-Properties-Reload" + +pluginManagement { + repositories { + gradlePluginPortal() + maven(url = "https://maven.fabricmc.net/") { name = "Fabric" } + maven(url = "https://repo.spongepowered.org/repository/maven-public/") { name = "Sponge Snapshots" } + } +} + +if (settings.extra.has("forgeVersion")) { + include("Forge") +} + +if (settings.extra.has("fabricVersion")) { + include("Fabric") +} diff --git a/src/main/java/chylex/serverproperties/command/PropertiesCommand.java b/src/main/java/chylex/serverproperties/command/PropertiesCommand.java new file mode 100644 index 0000000..d4d15b4 --- /dev/null +++ b/src/main/java/chylex/serverproperties/command/PropertiesCommand.java @@ -0,0 +1,89 @@ +package chylex.serverproperties.command; + +import chylex.serverproperties.mixin.DedicatedServerPropertiesMixin; +import chylex.serverproperties.mixin.SettingsMixin; +import chylex.serverproperties.props.ServerProperties; +import chylex.serverproperties.props.ServerProperty; +import com.mojang.brigadier.CommandDispatcher; +import net.minecraft.ChatFormatting; +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.network.chat.TextComponent; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.dedicated.DedicatedServerProperties; +import java.nio.file.Paths; +import java.util.HashSet; +import java.util.Map.Entry; +import java.util.Set; +import static net.minecraft.commands.Commands.literal; + +public final class PropertiesCommand { + private PropertiesCommand() {} + + public static void register(final CommandDispatcher<CommandSourceStack> dispatcher) { + dispatcher.register(literal("properties") + .requires(s -> s.hasPermission(2)) + .then(literal("reload") + .executes(c -> reloadPropertiesFile(c.getSource()))) + ); + } + + @SuppressWarnings("CastToIncompatibleInterface") + private static int reloadPropertiesFile(final CommandSourceStack s) { + final MinecraftServer server = s.getServer(); + + if (!(server instanceof final DedicatedServer dedicatedServer)) { + s.sendFailure(new TextComponent("This command is only supported on dedicated servers!")); + return 0; + } + + final DedicatedServerProperties oldProperties = dedicatedServer.getProperties(); + final DedicatedServerProperties newProperties = DedicatedServerProperties.fromFile(Paths.get("server.properties")); + final Set<String> unknownPropertyNames = new HashSet<>(((SettingsMixin)newProperties).getProperties().stringPropertyNames()); + + s.sendSuccess(new TextComponent("Reloading server properties:"), true); + + int reloadedProperties = 0; + int failedProperties = 0; + + for (final Entry<String, ServerProperty<?>> entry : ServerProperties.all().stream().sorted(Entry.comparingByKey()).toList()) { + final String name = entry.getKey(); + final ServerProperty<?> prop = entry.getValue(); + + unknownPropertyNames.remove(name); + + if (prop.hasChanged(oldProperties, newProperties)) { + final String oldValue = prop.toStringFrom(oldProperties); + final String newValue = prop.toStringFrom(newProperties); + + try { + prop.apply(dedicatedServer, newProperties, (DedicatedServerPropertiesMixin)oldProperties); + } catch (final UnsupportedOperationException e) { + s.sendSuccess(new TextComponent(" " + name + ':').withStyle(ChatFormatting.RED) + .append(new TextComponent(" cannot be reloaded").withStyle(ChatFormatting.WHITE)), true); + + ++failedProperties; + continue; + } + + s.sendSuccess(new TextComponent(" " + name + ": ").withStyle(ChatFormatting.LIGHT_PURPLE) + .append(new TextComponent(oldValue).withStyle(ChatFormatting.WHITE)) + .append(new TextComponent(" -> ").withStyle(ChatFormatting.GRAY)) + .append(new TextComponent(newValue).withStyle(ChatFormatting.WHITE)), true); + + ++reloadedProperties; + } + } + + for (final String name : unknownPropertyNames.stream().sorted().toList()) { + s.sendSuccess(new TextComponent(" " + name + ':').withStyle(ChatFormatting.GRAY) + .append(new TextComponent(" skipped unknown property").withStyle(ChatFormatting.WHITE)), true); + } + + if (reloadedProperties == 0 && failedProperties == 0) { + s.sendSuccess(new TextComponent(" Found no changes").withStyle(ChatFormatting.GRAY), true); + } + + return reloadedProperties; + } +} diff --git a/src/main/java/chylex/serverproperties/mixin/CommandMixin.java b/src/main/java/chylex/serverproperties/mixin/CommandMixin.java new file mode 100644 index 0000000..19e901b --- /dev/null +++ b/src/main/java/chylex/serverproperties/mixin/CommandMixin.java @@ -0,0 +1,19 @@ +package chylex.serverproperties.mixin; + +import chylex.serverproperties.command.PropertiesCommand; +import net.minecraft.commands.Commands; +import net.minecraft.commands.Commands.CommandSelection; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Commands.class) +public class CommandMixin { + @Inject(method = "<init>", at = @At("RETURN")) + private void init(final CommandSelection commandSelection, final CallbackInfo ci) { + @SuppressWarnings("ConstantConditions") + final Commands commands = (Commands)(Object)this; + PropertiesCommand.register(commands.getDispatcher()); + } +} diff --git a/src/main/java/chylex/serverproperties/mixin/DedicatedServerPropertiesMixin.java b/src/main/java/chylex/serverproperties/mixin/DedicatedServerPropertiesMixin.java new file mode 100644 index 0000000..56362ea --- /dev/null +++ b/src/main/java/chylex/serverproperties/mixin/DedicatedServerPropertiesMixin.java @@ -0,0 +1,8 @@ +package chylex.serverproperties.mixin; + +import net.minecraft.server.dedicated.DedicatedServerProperties; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin(DedicatedServerProperties.class) +public interface DedicatedServerPropertiesMixin { +} diff --git a/src/main/java/chylex/serverproperties/mixin/SettingsMixin.java b/src/main/java/chylex/serverproperties/mixin/SettingsMixin.java new file mode 100644 index 0000000..d99bdc7 --- /dev/null +++ b/src/main/java/chylex/serverproperties/mixin/SettingsMixin.java @@ -0,0 +1,12 @@ +package chylex.serverproperties.mixin; + +import net.minecraft.server.dedicated.Settings; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import java.util.Properties; + +@Mixin(Settings.class) +public interface SettingsMixin { + @Accessor + Properties getProperties(); +} diff --git a/src/main/java/chylex/serverproperties/props/ServerProperties.java b/src/main/java/chylex/serverproperties/props/ServerProperties.java new file mode 100644 index 0000000..50c7fe2 --- /dev/null +++ b/src/main/java/chylex/serverproperties/props/ServerProperties.java @@ -0,0 +1,25 @@ +package chylex.serverproperties.props; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Map.Entry; + +public final class ServerProperties { + private static final Map<String, ServerProperty<?>> ALL = new HashMap<>(); + + public static Collection<Entry<String, ServerProperty<?>>> all() { + return Collections.unmodifiableCollection(ALL.entrySet()); + } + + private static void register(final String name, final ServerProperty<?> property) { + if (ALL.put(name, property) != null) { + throw new IllegalArgumentException("Server property with name '" + name + "' is already registered!"); + } + } + + static { + } + + private ServerProperties() {} +} diff --git a/src/main/java/chylex/serverproperties/props/ServerProperty.java b/src/main/java/chylex/serverproperties/props/ServerProperty.java new file mode 100644 index 0000000..c823f21 --- /dev/null +++ b/src/main/java/chylex/serverproperties/props/ServerProperty.java @@ -0,0 +1,28 @@ +package chylex.serverproperties.props; + +import chylex.serverproperties.mixin.DedicatedServerPropertiesMixin; +import net.minecraft.server.dedicated.DedicatedServer; +import net.minecraft.server.dedicated.DedicatedServerProperties; +import java.util.Objects; + +public abstract class ServerProperty<T> { + public final boolean hasChanged(final DedicatedServerProperties oldProperties, final DedicatedServerProperties newProperties) { + return !Objects.equals(get(oldProperties), get(newProperties)); + } + + public final void apply(final DedicatedServer server, final DedicatedServerProperties source, final DedicatedServerPropertiesMixin target) { + apply(server, target, get(source)); + } + + public final String toStringFrom(final DedicatedServerProperties source) { + return toString(get(source)); + } + + public abstract T get(DedicatedServerProperties properties); + + public abstract void apply(DedicatedServer server, DedicatedServerPropertiesMixin target, T value); + + public String toString(final T value) { + return Objects.toString(value, "<null>"); + } +} diff --git a/src/main/resources/pack.mcmeta b/src/main/resources/pack.mcmeta new file mode 100644 index 0000000..93cc1eb --- /dev/null +++ b/src/main/resources/pack.mcmeta @@ -0,0 +1,7 @@ +{ + "pack": { + "description": "Server Properties Reload", + "pack_format": 7, + "_comment": "" + } +} diff --git a/src/main/resources/serverpropertiesreload.mixins.json b/src/main/resources/serverpropertiesreload.mixins.json new file mode 100644 index 0000000..6d40789 --- /dev/null +++ b/src/main/resources/serverpropertiesreload.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "chylex.serverproperties.mixin", + "refmap": "serverpropertiesreload.refmap.json", + "compatibilityLevel": "JAVA_16", + "server": [ + "CommandMixin", + "DedicatedServerPropertiesMixin", + "SettingsMixin" + ], + "injectors": { + "defaultRequire": 1 + } +}