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
+  }
+}