diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d153ce1 --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +gradlew +gradlew.bat + +# gradle + +.gradle/ +gradle/ +build/ +out/ +classes/ + +# eclipse + +*.launch + +# idea + +.idea/ +*.iml +*.ipr +*.iws + +# vscode + +.settings/ +.vscode/ +bin/ +.classpath +.project + +# fabric + +run/ +logs/ diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..6d0c9ec --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ + +The MIT License (MIT) + +Copyright (c) 2020 Samuel Čavoj + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..9767ca5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,75 @@ +plugins { + id 'fabric-loom' version '0.4-SNAPSHOT' + id 'maven-publish' +} + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +archivesBaseName = project.archives_base_name +version = project.mod_version +group = project.maven_group + +dependencies { + //to change the versions see the gradle.properties file + minecraft "com.mojang:minecraft:${project.minecraft_version}" + mappings "net.fabricmc:yarn:${project.yarn_mappings}:v2" + modImplementation "net.fabricmc:fabric-loader:${project.loader_version}" + + modImplementation "net.fabricmc.fabric-api:fabric-api:${project.fabric_version}" +} + +processResources { + inputs.property "version", project.version + + from(sourceSets.main.resources.srcDirs) { + include "fabric.mod.json" + expand "version": project.version + } + + from(sourceSets.main.resources.srcDirs) { + exclude "fabric.mod.json" + } +} + +// ensure that the encoding is set to UTF-8, no matter what the system default is +// this fixes some edge cases with special characters not displaying correctly +// see http://yodaconditions.net/blog/fix-for-java-file-encoding-problems-with-gradle.html +tasks.withType(JavaCompile) { + options.encoding = "UTF-8" +} + +// Loom will automatically attach sourcesJar to a RemapSourcesJar task and to the "build" task +// if it is present. +// If you remove this task, sources will not be generated. +task sourcesJar(type: Jar, dependsOn: classes) { + classifier = "sources" + from sourceSets.main.allSource +} + +jar { + from "LICENSE" +} + +publishing { + publications { + mavenJava(MavenPublication) { + artifact(remapJar) { + builtBy remapJar + } + artifact(sourcesJar) { + builtBy remapSourcesJar + } + } + } + + repositories { + maven { + url = mavenUrl + credentials { + username mavenUser + password mavenPassword + } + } + } +} diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..0633464 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,14 @@ +# Done to increase the memory available to gradle. +org.gradle.jvmargs=-Xmx1G + +# Fabric Properties +minecraft_version=1.16.2 +yarn_mappings=1.16.2+build.6 +loader_version=0.9.1+build.205 + +# Mod Properties +mod_version = 1.0 +maven_group = net.cavoj +archives_base_name = servertick + +fabric_version=0.17.2+build.396-1.16 \ No newline at end of file diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..5b60df3 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,10 @@ +pluginManagement { + repositories { + jcenter() + maven { + name = 'Fabric' + url = 'https://maven.fabricmc.net/' + } + gradlePluginPortal() + } +} diff --git a/src/main/java/net/cavoj/servertick/Packets.java b/src/main/java/net/cavoj/servertick/Packets.java new file mode 100644 index 0000000..65394d4 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/Packets.java @@ -0,0 +1,9 @@ +package net.cavoj.servertick; + +import net.minecraft.util.Identifier; + +public class Packets { + public static final Identifier PACKET_TOGGLE_DEBUG_SCREEN = new Identifier("servertick", "test"); + public static final Identifier PACKET_FULL_METRICS = new Identifier("servertick", "metrics/full"); + public static final Identifier PACKET_SAMPLE_METRICS = new Identifier("servertick", "metrics/sample"); +} diff --git a/src/main/java/net/cavoj/servertick/STClient.java b/src/main/java/net/cavoj/servertick/STClient.java new file mode 100644 index 0000000..45fb3fc --- /dev/null +++ b/src/main/java/net/cavoj/servertick/STClient.java @@ -0,0 +1,73 @@ +package net.cavoj.servertick; + +import io.netty.buffer.Unpooled; +import net.fabricmc.api.ClientModInitializer; +import net.fabricmc.fabric.api.network.ClientSidePacketRegistry; +import net.fabricmc.fabric.api.network.PacketContext; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.MetricsData; + +public class STClient implements ClientModInitializer { + private static STClient _instance; + + public STClient() { + if (_instance != null) { + throw new RuntimeException("Cannot have multiple instances"); + } + _instance = this; + } + + public static STClient getInstance() { + return _instance; + } + + @Override + public void onInitializeClient() { + ClientSidePacketRegistry.INSTANCE.register(Packets.PACKET_FULL_METRICS, this::processMetricsFullPacket); + ClientSidePacketRegistry.INSTANCE.register(Packets.PACKET_SAMPLE_METRICS, this::processMetricsSamplePacket); + } + + private void processMetricsFullPacket(PacketContext ctx, PacketByteBuf data) { + // not sure if I can do this on the network thread + if (this.metrics == null) + this.metrics = new MetricsData(); + ((SerializableMetricsData)this.metrics).deserialize(data); + } + + private void processMetricsSamplePacket(PacketContext ctx, PacketByteBuf data) { + long time = data.readLong(); + ctx.getTaskQueue().execute(() -> { + if (this.metrics != null) + this.metrics.pushSample(time); + }); + } + + private boolean debugTpsEnabled; + private MetricsData metrics; + + public void setTpsEnabled(boolean enabled) { + if (this.debugTpsEnabled != enabled) { + this.debugTpsEnabled = enabled; + updateTpsEnabled(); + } + } + + private void updateTpsEnabled() { + PacketByteBuf data = new PacketByteBuf(Unpooled.buffer()); + data.writeBoolean(this.debugTpsEnabled); + ClientSidePacketRegistry.INSTANCE.sendToServer(Packets.PACKET_TOGGLE_DEBUG_SCREEN, data); + } + + public void setMetricsData(MetricsData data) { + this.metrics = data; + } + + public MetricsData getMetricsData() { + return this.metrics; + } + + public void joined() { + setMetricsData(null); + setTpsEnabled(false); + } +} diff --git a/src/main/java/net/cavoj/servertick/STServer.java b/src/main/java/net/cavoj/servertick/STServer.java new file mode 100644 index 0000000..b1a1597 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/STServer.java @@ -0,0 +1,80 @@ +package net.cavoj.servertick; + +import io.netty.buffer.Unpooled; +import net.cavoj.servertick.mixin.server.MinecraftServerAccessor; +import net.fabricmc.api.DedicatedServerModInitializer; +import net.fabricmc.fabric.api.network.PacketContext; +import net.fabricmc.fabric.api.network.ServerSidePacketRegistry; +import net.minecraft.entity.player.PlayerEntity; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.MetricsData; + +import java.util.HashSet; +import java.util.Set; + +public class STServer implements DedicatedServerModInitializer { + private final Set listeners = new HashSet<>(); + private static STServer _instance; + + public STServer() { + if (_instance != null) { + throw new RuntimeException("Cannot have multiple instances"); + } + _instance = this; + } + + @Override + public void onInitializeServer() { + ServerSidePacketRegistry.INSTANCE.register(Packets.PACKET_TOGGLE_DEBUG_SCREEN, this::processTogglePacket); +// ServerTickEvents.END_SERVER_TICK.register(this::onTick); + } + + public static STServer getInstance() { + return _instance; + } + +// private int tickCounter = 0; +// private void onTick(MinecraftServer server) { +// tickCounter++; +// if (tickCounter > 20) { +// tickCounter = 0; +// // Send full metrics every so often to prevent desync +// listeners.forEach(player -> sendMetrics(server, player)); +// } +// } + + private void sendMetrics(MinecraftServer server, PlayerEntity player) { + MetricsData metrics = ((MinecraftServerAccessor)server).getMetricsData(); + PacketByteBuf data = new PacketByteBuf(Unpooled.buffer()); + ((SerializableMetricsData)metrics).serialize(data); + ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, Packets.PACKET_FULL_METRICS, data); + } + + private void processTogglePacket(PacketContext ctx, PacketByteBuf data) { + boolean state = data.readBoolean(); + PlayerEntity player = ctx.getPlayer(); + MinecraftServer server = ctx.getPlayer().getServer(); + ctx.getTaskQueue().execute(() -> { + if (state) { + if (player.hasPermissionLevel(4)) { + listeners.add(player); + sendMetrics(server, player); + } + } else { + listeners.remove(player); + } + }); + } + + public void pushSample(long time) { + PacketByteBuf data = new PacketByteBuf(Unpooled.buffer()); + data.writeLong(time); + listeners.forEach(player -> ServerSidePacketRegistry.INSTANCE.sendToPlayer(player, Packets.PACKET_SAMPLE_METRICS, data)); + } + + public void onPlayerDisconnected(ServerPlayerEntity player) { + this.listeners.remove(player); + } +} diff --git a/src/main/java/net/cavoj/servertick/SerializableMetricsData.java b/src/main/java/net/cavoj/servertick/SerializableMetricsData.java new file mode 100644 index 0000000..3724200 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/SerializableMetricsData.java @@ -0,0 +1,8 @@ +package net.cavoj.servertick; + +import net.minecraft.network.PacketByteBuf; + +public interface SerializableMetricsData { + void deserialize(PacketByteBuf data); + void serialize(PacketByteBuf data); +} diff --git a/src/main/java/net/cavoj/servertick/mixin/MetricsDataMixin.java b/src/main/java/net/cavoj/servertick/mixin/MetricsDataMixin.java new file mode 100644 index 0000000..279babf --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/MetricsDataMixin.java @@ -0,0 +1,43 @@ +package net.cavoj.servertick.mixin; + +import net.cavoj.servertick.STServer; +import net.cavoj.servertick.SerializableMetricsData; +import net.minecraft.network.PacketByteBuf; +import net.minecraft.util.MetricsData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(MetricsData.class) +public abstract class MetricsDataMixin implements SerializableMetricsData { + + @Shadow @Final private long[] samples; + + @Shadow private int writeIndex; + + @Shadow private int sampleCount; + + @Shadow private int startIndex; + + @Override + public void deserialize(PacketByteBuf data) { + this.writeIndex = data.readInt(); + this.sampleCount = data.readInt(); + this.startIndex = data.readInt(); + for (int i = 0; i < this.samples.length; i++) + this.samples[i] = data.readLong(); + } + + @Override + public void serialize(PacketByteBuf data) { + data.writeInt(this.writeIndex); + data.writeInt(this.sampleCount); + data.writeInt(this.startIndex); + for (int i = 0; i < this.samples.length; i++) + data.writeLong(this.samples[i]); + } + +} diff --git a/src/main/java/net/cavoj/servertick/mixin/client/ClientPlayNetworkHandlerMixin.java b/src/main/java/net/cavoj/servertick/mixin/client/ClientPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..d28057b --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/client/ClientPlayNetworkHandlerMixin.java @@ -0,0 +1,16 @@ +package net.cavoj.servertick.mixin.client; + +import net.cavoj.servertick.STClient; +import net.minecraft.client.network.ClientPlayNetworkHandler; +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(ClientPlayNetworkHandler.class) +public abstract class ClientPlayNetworkHandlerMixin { + @Inject(method = "onGameJoin", at = @At("HEAD")) + public void onGameJoin(CallbackInfo ci) { + STClient.getInstance().joined(); + } +} diff --git a/src/main/java/net/cavoj/servertick/mixin/client/DebugHudMixin.java b/src/main/java/net/cavoj/servertick/mixin/client/DebugHudMixin.java new file mode 100644 index 0000000..2dbc298 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/client/DebugHudMixin.java @@ -0,0 +1,31 @@ +package net.cavoj.servertick.mixin.client; + +import net.cavoj.servertick.STClient; +import net.minecraft.client.MinecraftClient; +import net.minecraft.client.gui.hud.DebugHud; +import net.minecraft.client.util.math.MatrixStack; +import net.minecraft.util.MetricsData; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(DebugHud.class) +public abstract class DebugHudMixin { + @Shadow protected abstract void drawMetricsData(MatrixStack matrixStack, MetricsData metricsData, int i, int j, boolean bl); + + @Shadow @Final private MinecraftClient client; + + @Inject(method = "render", at = @At("HEAD")) + public void render(MatrixStack matrices, CallbackInfo ci) { + if (this.client.options.debugTpsEnabled && this.client.getServer() == null) { + MetricsData metrics = STClient.getInstance().getMetricsData(); + if (metrics == null) return; + + int i = this.client.getWindow().getScaledWidth(); + this.drawMetricsData(matrices, metrics, i - Math.min(i / 2, 240), i / 2, false); + } + } +} diff --git a/src/main/java/net/cavoj/servertick/mixin/client/KeyboardMixin.java b/src/main/java/net/cavoj/servertick/mixin/client/KeyboardMixin.java new file mode 100644 index 0000000..ee4ffb3 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/client/KeyboardMixin.java @@ -0,0 +1,21 @@ +package net.cavoj.servertick.mixin.client; + +import net.cavoj.servertick.STClient; +import net.minecraft.client.Keyboard; +import net.minecraft.client.MinecraftClient; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Keyboard.class) +public abstract class KeyboardMixin { + @Shadow @Final private MinecraftClient client; + + @Inject(method = "onKey", at = @At("RETURN")) + protected void onKey(CallbackInfo ci) { + STClient.getInstance().setTpsEnabled(this.client.options.debugEnabled && this.client.options.debugTpsEnabled); + } +} diff --git a/src/main/java/net/cavoj/servertick/mixin/server/MetricsDataServerMixin.java b/src/main/java/net/cavoj/servertick/mixin/server/MetricsDataServerMixin.java new file mode 100644 index 0000000..5a47cb8 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/server/MetricsDataServerMixin.java @@ -0,0 +1,17 @@ +package net.cavoj.servertick.mixin.server; + +import net.cavoj.servertick.STServer; +import net.minecraft.util.MetricsData; +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(MetricsData.class) +public abstract class MetricsDataServerMixin { + + @Inject(method = "pushSample", at = @At("HEAD")) + public void pushSample(long time, CallbackInfo ci) { + STServer.getInstance().pushSample(time); + } +} diff --git a/src/main/java/net/cavoj/servertick/mixin/server/MinecraftServerAccessor.java b/src/main/java/net/cavoj/servertick/mixin/server/MinecraftServerAccessor.java new file mode 100644 index 0000000..fe67254 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/server/MinecraftServerAccessor.java @@ -0,0 +1,12 @@ +package net.cavoj.servertick.mixin.server; + +import net.minecraft.server.MinecraftServer; +import net.minecraft.util.MetricsData; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +@Mixin(MinecraftServer.class) +public interface MinecraftServerAccessor { + @Accessor("metricsData") + MetricsData getMetricsData(); +} diff --git a/src/main/java/net/cavoj/servertick/mixin/server/ServerPlayNetworkHandlerMixin.java b/src/main/java/net/cavoj/servertick/mixin/server/ServerPlayNetworkHandlerMixin.java new file mode 100644 index 0000000..26e1966 --- /dev/null +++ b/src/main/java/net/cavoj/servertick/mixin/server/ServerPlayNetworkHandlerMixin.java @@ -0,0 +1,20 @@ +package net.cavoj.servertick.mixin.server; + +import net.cavoj.servertick.STServer; +import net.minecraft.server.network.ServerPlayNetworkHandler; +import net.minecraft.server.network.ServerPlayerEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(ServerPlayNetworkHandler.class) +public abstract class ServerPlayNetworkHandlerMixin { + @Shadow public ServerPlayerEntity player; + + @Inject(method = "onDisconnected", at = @At("HEAD")) + public void onDisconnected(CallbackInfo ci) { + STServer.getInstance().onPlayerDisconnected(this.player); + } +} diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json new file mode 100644 index 0000000..f537f70 --- /dev/null +++ b/src/main/resources/fabric.mod.json @@ -0,0 +1,32 @@ +{ + "schemaVersion": 1, + "id": "servertick", + "version": "${version}", + "name": "ServerTick", + "description": "Makes the F3 TPS graph work on dedicated servers", + "authors": [ + "sammko" + ], + "contact": { + "homepage": "https://github.com/sammko/servertick-fabric", + "issues": "https://github.com/sammko/servertick-fabric/issues", + "email": "samuel@cavoj.net" + }, + "license": "MIT", + "environment": "*", + "entrypoints": { + "server": [ + "net.cavoj.servertick.STServer" + ], + "client": [ + "net.cavoj.servertick.STClient" + ] + }, + "mixins": [ + "servertick.mixins.json" + ], + "depends": { + "fabricloader": ">=0.7.4", + "minecraft": "1.16.x" + } +} diff --git a/src/main/resources/servertick.mixins.json b/src/main/resources/servertick.mixins.json new file mode 100644 index 0000000..0455bd0 --- /dev/null +++ b/src/main/resources/servertick.mixins.json @@ -0,0 +1,22 @@ +{ + "required": true, + "minVersion": "0.8", + "package": "net.cavoj.servertick.mixin", + "compatibilityLevel": "JAVA_8", + "client": [ + "client.ClientPlayNetworkHandlerMixin", + "client.DebugHudMixin", + "client.KeyboardMixin" + ], + "injectors": { + "defaultRequire": 1 + }, + "mixins": [ + "MetricsDataMixin" + ], + "server": [ + "server.MetricsDataServerMixin", + "server.MinecraftServerAccessor", + "server.ServerPlayNetworkHandlerMixin" + ] +}