From 3a734a38b91fc76e23e7fd064f72f72736ebb06e Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 22 Aug 2024 09:40:57 +0200 Subject: [PATCH 01/51] Add increasing and decreasing tickrate --- .../minecrafttas/lotas_light/LoTASLight.java | 2 - .../lotas_light/LoTASLightClient.java | 78 ++++++++++++++++--- .../event/EventClientGameLoop.java | 14 ++++ .../lotas_light/mixin/MixinMinecraft.java | 19 +++++ .../mixin/MixinTickRateManager.java | 21 +++++ .../assets/lotaslight/lang/en_us.json | 2 + src/main/resources/lotaslight.mixins.json | 2 + 7 files changed, 126 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/event/EventClientGameLoop.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index c9d68c3..767010f 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -10,8 +10,6 @@ public class LoTASLight implements ModInitializer { @Override public void onInitialize() { - // TODO Auto-generated method stub - } } diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index c3aadd1..09902c5 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -2,12 +2,18 @@ import org.lwjgl.glfw.GLFW; +import com.minecrafttas.lotas_light.event.EventClientGameLoop; import com.minecrafttas.lotas_light.tickratechanger.TickrateChangerClient; import com.mojang.blaze3d.platform.InputConstants; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.minecraft.client.KeyMapping; +import net.minecraft.client.Minecraft; +import net.minecraft.client.server.IntegratedServer; +import net.minecraft.network.chat.Component; +import net.minecraft.server.ServerTickRateManager; +import net.minecraft.world.TickRateManager; public class LoTASLightClient implements ClientModInitializer { @@ -20,6 +26,9 @@ public class LoTASLightClient implements ClientModInitializer { private KeyMapping savestate = new KeyMapping("key.lotaslight.savestate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_J, "keycategory.lotaslight.lotaslight"); private KeyMapping loadstate = new KeyMapping("key.lotaslight.loadstate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_K, "keycategory.lotaslight.lotaslight"); + private float[] rates = new float[] { .1f, .2f, .5f, 1f, 2f, 5f, 10f, 20f, 40f, 100f }; + private short rateIndex = 7; + @Override public void onInitializeClient() { registerKeybindings(); @@ -33,23 +42,72 @@ private void registerKeybindings() { KeyBindingHelper.registerKeyBinding(savestate); KeyBindingHelper.registerKeyBinding(loadstate); - while (increaseTickrate.consumeClick()) { + EventClientGameLoop.EVENT.register(client -> { + while (increaseTickrate.consumeClick()) { + increaseTickrate(client); + } + while (decreaseTickrate.consumeClick()) { + decreaseTickrate(client); + } + while (freezeTickrate.consumeClick()) { + freezeTickrate(client); + } + while (advanceTickrate.consumeClick()) { + advanceTickrate(client); + } + while (savestate.consumeClick()) { + savestate(); + } + while (loadstate.consumeClick()) { + loadstate(); + } - } - while (decreaseTickrate.consumeClick()) { + }); + } + private void increaseTickrate(Minecraft client) { + TickRateManager clientTickrateChanger = client.level.tickRateManager(); + IntegratedServer server = client.getSingleplayerServer(); + if (server == null) { + return; } - while (freezeTickrate.consumeClick()) { + ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + rateIndex++; + rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); + float tickrate = rates[rateIndex]; + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + clientTickrateChanger.setTickRate(tickrate); + serverTickrateChanger.setTickRate(tickrate); + } + private void decreaseTickrate(Minecraft client) { + TickRateManager clientTickrateChanger = client.level.tickRateManager(); + IntegratedServer server = client.getSingleplayerServer(); + if (server == null) { + return; } - while (advanceTickrate.consumeClick()) { + ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + rateIndex--; + rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); + float tickrate = rates[rateIndex]; + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + clientTickrateChanger.setTickRate(tickrate); + serverTickrateChanger.setTickRate(tickrate); + } - } - while (savestate.consumeClick()) { + private void freezeTickrate(Minecraft client) { - } - while (loadstate.consumeClick()) { + } + + private void advanceTickrate(Minecraft client) { + + } + + private void savestate() { + + } + + private void loadstate() { - } } } diff --git a/src/main/java/com/minecrafttas/lotas_light/event/EventClientGameLoop.java b/src/main/java/com/minecrafttas/lotas_light/event/EventClientGameLoop.java new file mode 100644 index 0000000..8dcd36f --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/event/EventClientGameLoop.java @@ -0,0 +1,14 @@ +package com.minecrafttas.lotas_light.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.client.Minecraft; + +public interface EventClientGameLoop { + public static Event EVENT = EventFactory.createArrayBacked(EventClientGameLoop.class, (listeners) -> (client) -> { + for (EventClientGameLoop listener : listeners) + listener.onGameLoop(client); + }); + + public void onGameLoop(Minecraft client); +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java new file mode 100644 index 0000000..404a29b --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java @@ -0,0 +1,19 @@ +package com.minecrafttas.lotas_light.mixin; + +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; + +import com.minecrafttas.lotas_light.event.EventClientGameLoop; + +import net.minecraft.client.Minecraft; + +@Mixin(Minecraft.class) +public class MixinMinecraft { + + @Inject(method = "run", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runTick(Z)V")) + public void inject_Run(CallbackInfo ci) { + EventClientGameLoop.EVENT.invoker().onGameLoop((Minecraft) (Object) this); + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java new file mode 100644 index 0000000..c09b420 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -0,0 +1,21 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; + +import net.minecraft.world.TickRateManager; + +@Mixin(TickRateManager.class) +public abstract class MixinTickRateManager { + + @ModifyReturnValue(method = "isEntityFrozen", at = @At(value = "RETURN")) + public boolean modifyReturn_IsEntityFrozen(boolean original) { + return !runsNormally(); + } + + @Shadow + public abstract boolean runsNormally(); +} diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index eec40b3..eb399f4 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -6,5 +6,7 @@ "key.lotaslight.savestate":"Savestate", "key.lotaslight.loadstate":"Loadstate", + "msg.lotaslight.setTickrate":"Set the tickrate to %s", + "keycategory.lotaslight.lotaslight":"LoTAS-Light" } \ No newline at end of file diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 77d8ab6..97a28b7 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -3,6 +3,8 @@ "package": "com.minecrafttas.lotas_light.mixin", "compatibilityLevel": "JAVA_21", "mixins": [ + "MixinTickRateManager", + "MixinMinecraft" ], "injectors": { "defaultRequire": 1 From 2ecf7c5b24b347d5d0b1975b3b8efa45f54b4476 Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 22 Aug 2024 20:00:48 +0200 Subject: [PATCH 02/51] Add freeze and advance --- gradle.properties | 2 +- .../minecrafttas/lotas_light/LoTASLight.java | 4 - .../lotas_light/LoTASLightClient.java | 22 +- .../lotas_light/mixin/MixinTickCommand.java | 17 ++ .../mixin/MixinTickRateManager.java | 6 + .../TickrateChangerClient.java | 230 --------------- .../TickrateChangerServer.java | 266 ------------------ .../assets/lotaslight/lang/en_us.json | 2 +- src/main/resources/lotaslight.mixins.json | 3 +- 9 files changed, 46 insertions(+), 506 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java delete mode 100644 src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerClient.java delete mode 100644 src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerServer.java diff --git a/gradle.properties b/gradle.properties index 9fb3478..dbd3f6d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -9,7 +9,7 @@ yarn_mappings=1.21+build.2 loader_version=0.15.11 # Mod Properties -mod_version=1.0.0 +mod_version=0.1-dev maven_group=com.mincecrafttas archives_base_name=LoTAS-Light-1.21 diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index 767010f..a52fa20 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -1,13 +1,9 @@ package com.minecrafttas.lotas_light; -import com.minecrafttas.lotas_light.tickratechanger.TickrateChangerServer; - import net.fabricmc.api.ModInitializer; public class LoTASLight implements ModInitializer { - public static TickrateChangerServer tickratechangerServer = new TickrateChangerServer(); - @Override public void onInitialize() { } diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index 09902c5..e32dbf4 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -3,7 +3,6 @@ import org.lwjgl.glfw.GLFW; import com.minecrafttas.lotas_light.event.EventClientGameLoop; -import com.minecrafttas.lotas_light.tickratechanger.TickrateChangerClient; import com.mojang.blaze3d.platform.InputConstants; import net.fabricmc.api.ClientModInitializer; @@ -17,8 +16,6 @@ public class LoTASLightClient implements ClientModInitializer { - public static TickrateChangerClient tickratechangerClient = new TickrateChangerClient(); - private KeyMapping increaseTickrate = new KeyMapping("key.lotaslight.increaseTickrate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_PERIOD, "keycategory.lotaslight.lotaslight"); private KeyMapping decreaseTickrate = new KeyMapping("key.lotaslight.decreaseTickrate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_COMMA, "keycategory.lotaslight.lotaslight"); private KeyMapping freezeTickrate = new KeyMapping("key.lotaslight.freezeTickrate", InputConstants.Type.KEYSYM, GLFW.GLFW_KEY_F8, "keycategory.lotaslight.lotaslight"); @@ -72,6 +69,7 @@ private void increaseTickrate(Minecraft client) { return; } ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + rateIndex++; rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; @@ -87,6 +85,7 @@ private void decreaseTickrate(Minecraft client) { return; } ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + rateIndex--; rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; @@ -96,11 +95,28 @@ private void decreaseTickrate(Minecraft client) { } private void freezeTickrate(Minecraft client) { + TickRateManager clientTickrateChanger = client.level.tickRateManager(); + IntegratedServer server = client.getSingleplayerServer(); + if (server == null) { + return; + } + ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + boolean isFrozen = clientTickrateChanger.isFrozen() && serverTickrateChanger.isFrozen(); + clientTickrateChanger.setFrozen(!isFrozen); + serverTickrateChanger.setFrozen(!isFrozen); } private void advanceTickrate(Minecraft client) { + TickRateManager clientTickrateChanger = client.level.tickRateManager(); + IntegratedServer server = client.getSingleplayerServer(); + if (server == null) { + return; + } + ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + clientTickrateChanger.setFrozenTicksToRun(1); + serverTickrateChanger.setFrozenTicksToRun(1); } private void savestate() { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java new file mode 100644 index 0000000..3d573fe --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java @@ -0,0 +1,17 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; + +import net.minecraft.server.commands.TickCommand; + +@Mixin(TickCommand.class) +public class MixinTickCommand { + + @ModifyExpressionValue(method = "register", at = @At(value = "CONSTANT", args = "floatValue=1.0F")) + private static float modifyArg_FloatArg(float original) { + return .1f; + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index c09b420..1fc0f16 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -4,6 +4,7 @@ import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.injection.At; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.llamalad7.mixinextras.injector.ModifyReturnValue; import net.minecraft.world.TickRateManager; @@ -16,6 +17,11 @@ public boolean modifyReturn_IsEntityFrozen(boolean original) { return !runsNormally(); } + @ModifyExpressionValue(method = "setTickRate", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F")) + public float modifyExpressionValue_SetTickRate(float original, float f) { + return f; + } + @Shadow public abstract boolean runsNormally(); } diff --git a/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerClient.java b/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerClient.java deleted file mode 100644 index e318a06..0000000 --- a/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerClient.java +++ /dev/null @@ -1,230 +0,0 @@ -package com.minecrafttas.lotas_light.tickratechanger; - -import net.minecraft.client.Minecraft; - -/** - * Changes the {@link Minecraft#timer} variable - * - * @author Scribble - * - */ -public class TickrateChangerClient { - /** - * The current tickrate of the client - */ - public float ticksPerSecond; - - /** - * The tickrate before {@link #ticksPerSecond} was changed to 0, used to toggle - * pausing - */ - public float tickrateSaved = 20F; - - /** - * True if the tickrate is 20 and the client should advance 1 tick - */ - public boolean advanceTick = false; - - public long millisecondsPerTick = 50L; - - public TickrateChangerClient() { - this(20f); - } - - public TickrateChangerClient(float initialTickrate) { - ticksPerSecond = initialTickrate; - } - - /** - * Changes both client and server tickrates - * - * @param tickrate The new tickrate of client and server - */ - public void changeTickrate(float tickrate) { - changeClientTickrate(tickrate); - changeServerTickrate(tickrate); - } - - public void changeClientTickrate(float tickrate) { - changeClientTickrate(tickrate, true); - } - - /** - * Changes the tickrate of the client
- * If tickrate is zero, it will pause the game and store the previous tickrate - * in {@link #tickrateSaved} - * - * @param tickrate The new tickrate of the client - */ - public void changeClientTickrate(float tickrate, boolean log) { - if (tickrate < 0) { - return; - } - Minecraft mc = Minecraft.getInstance(); - if (tickrate > 0) { - millisecondsPerTick = (long) (1000F / tickrate); -// mc.timer.tickLength = millisecondsPerTick; - - } else if (tickrate == 0F) { - if (ticksPerSecond != 0) { - tickrateSaved = ticksPerSecond; - } -// mc.timer.tickLength = Float.MAX_VALUE; - } - ticksPerSecond = tickrate; - if (log) - log("Setting the client tickrate to " + ticksPerSecond); - } - - /** - * Attempts to change the tickrate on the server. Sends a - * {@link TASmodPackets#TICKRATE_CHANGE} packet to the server - * - * @param tickrate The new server tickrate - */ - public void changeServerTickrate(float tickrate) { - if (tickrate < 0) { - return; - } - - try { - // request tickrate change -// TASmodClient.client.send(new TASmodBufferBuilder(TASmodPackets.TICKRATE_CHANGE).writeFloat(tickrate)); - - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Toggles between tickrate 0 and tickrate > 0 - */ - public void togglePause() { - try { - // request tickrate change -// TASmodClient.client.send(new TASmodBufferBuilder(TASmodPackets.TICKRATE_ZERO).writeTickratePauseState(TickratePauseState.TOGGLE)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Pauses and unpauses the client, used in main menus - */ - public void togglePauseClient() { - if (ticksPerSecond > 0) { - tickrateSaved = ticksPerSecond; - pauseClientGame(true); - } else if (ticksPerSecond == 0) { - pauseClientGame(false); - } - } - - /** - * Enables tickrate 0 - * - * @param pause True if the game should be paused, false if unpause - */ - public void pauseGame(boolean pause) { - if (pause) { - changeTickrate(0F); - } else { - advanceTick = false; - changeTickrate(tickrateSaved); - } - } - - /** - * Pauses the game without sending a command to the server - * - * @param pause The state of the client - */ - public void pauseClientGame(boolean pause) { - if (pause) { - changeClientTickrate(0F); - } else { - changeClientTickrate(tickrateSaved); - } - } - - /** - * Advances the game by 1 tick. Sends a {@link AdvanceTickratePacket} to the - * server or calls {@link #advanceClientTick()} if the world is null - */ - public void advanceTick() { - if (Minecraft.getInstance().level != null) { - advanceServerTick(); - } else { - advanceClientTick(); - } - } - - /** - * Sends a {@link AdvanceTickratePacket} to the server to advance the server - */ - public void advanceServerTick() { - try { -// TASmodClient.client.send(new TASmodBufferBuilder(TASmodPackets.TICKRATE_ADVANCE)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Advances the game by 1 tick. Doesn't send a packet to the server - */ - public void advanceClientTick() { - if (ticksPerSecond == 0) { - advanceTick = true; - changeClientTickrate(tickrateSaved); - } - } - - public void joinServer() { - changeServerTickrate(ticksPerSecond); - } - - private static void log(String msg) { -// LOGGER.debug(LoggerMarkers.Tickrate, msg); - } - -// @Override -// public PacketID[] getAcceptedPacketIDs() { -// return new TASmodPackets[] { TASmodPackets.TICKRATE_CHANGE, TASmodPackets.TICKRATE_ADVANCE, TASmodPackets.TICKRATE_ZERO }; -// } -// -// @Override -// public void onClientPacket(PacketID id, ByteBuffer buf, String username) throws PacketNotImplementedException, WrongSideException, Exception { -// TASmodPackets packet = (TASmodPackets) id; -// -// switch (packet) { -// case TICKRATE_CHANGE: -// float tickrate = TASmodBufferBuilder.readFloat(buf); -// changeClientTickrate(tickrate); -// break; -// case TICKRATE_ADVANCE: -// advanceClientTick(); -// break; -// case TICKRATE_ZERO: -// TickratePauseState state = TASmodBufferBuilder.readTickratePauseState(buf); -// -// switch (state) { -// case PAUSE: -// pauseClientGame(true); -// break; -// case UNPAUSE: -// pauseClientGame(false); -// break; -// case TOGGLE: -// togglePauseClient(); -// default: -// break; -// } -// break; -// -// default: -// throw new PacketNotImplementedException(packet, this.getClass(), Side.CLIENT); -// } -// } - -} diff --git a/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerServer.java b/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerServer.java deleted file mode 100644 index 5c89996..0000000 --- a/src/main/java/com/minecrafttas/lotas_light/tickratechanger/TickrateChangerServer.java +++ /dev/null @@ -1,266 +0,0 @@ -package com.minecrafttas.lotas_light.tickratechanger; - -/** - * Controls the tickrate on the server side - * - * The tickrate is controlled in MinecraftServer.run() where the server is - * halted for 50 milliseconds minus the time the tick took to execute. - *

- * To change the tickrate on server and all clients use - * {@link #changeTickrate(float)}. - *

- * You can individually set the tickrate with - * {@link #changeClientTickrate(float)} and - * {@link #changeServerTickrate(float)}. - *

- * - * - * @author Scribble - * - */ -public class TickrateChangerServer { - - /** - * The current tickrate of the client - */ - public float ticksPerSecond = 20F; - - /** - * How long the server should sleep - */ - public long millisecondsPerTick = 50L; - - /** - * The tickrate before {@link #ticksPerSecond} was changed to 0, used to toggle - * pausing - */ - public float tickrateSaved = 20F; - - /** - * True if the tickrate is 20 and the server should advance 1 tick - */ - public boolean advanceTick = false; - - /** - * Changes both client and server tickrates. - *

- * Tickrates can be tickrate>=0 with 0 pausing the game. - * - * @param tickrate The new tickrate of client and server - */ - public void changeTickrate(float tickrate) { - changeClientTickrate(tickrate); - changeServerTickrate(tickrate); - } - - public void changeClientTickrate(float tickrate) { - changeClientTickrate(tickrate, false); - } - - /** - * Changes the tickrate of all clients. Sends a {@link TASmodPackets#TICKRATE_CHANGE} packet to all clients - * - * @param tickrate The new tickrate of the client - * @param log If a message should logged - */ - public void changeClientTickrate(float tickrate, boolean log) { - if (log) - log("Changing the tickrate " + tickrate + " to all clients"); - - try { -// TASmod.server.sendToAll(new TASmodBufferBuilder(TASmodPackets.TICKRATE_CHANGE).writeFloat(tickrate)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Changes the tickrate of the server - * - * @param tickrate The new tickrate of the server - */ - public void changeServerTickrate(float tickrate) { - changeServerTickrate(tickrate, true); - } - - /** - * Changes the tickrate of the server - * - * @param tickrate The new tickrate of the server - * @param log If a message should logged - */ - public void changeServerTickrate(float tickrate, boolean log) { - if (tickrate > 0) { - millisecondsPerTick = (long) (1000L / tickrate); - } else if (tickrate == 0) { - if (ticksPerSecond != 0) { - tickrateSaved = ticksPerSecond; - } - } - ticksPerSecond = tickrate; -// EventListenerRegistry.fireEvent(EventTickratechanger.EventServerTickrateChange.class, tickrate); - if (log) { - log("Setting the server tickrate to " + ticksPerSecond); - } - } - - /** - * Toggles between tickrate 0 and tickrate > 0 - */ - public void togglePause() { - if (ticksPerSecond > 0) { - changeTickrate(0); - } else if (ticksPerSecond == 0) { - changeTickrate(tickrateSaved); - } - } - - /** - * Enables tickrate 0 - * - * @param pause True if the game should be paused, false if unpause - */ - public void pauseGame(boolean pause) { - if (pause) { - changeTickrate(0); - } else { - advanceTick = false; - changeTickrate(tickrateSaved); - } - } - - /** - * Pauses the game without sending a command to the clients - * - * @param pause The state of the server - */ - public void pauseServerGame(boolean pause) { - if (pause) { - changeServerTickrate(0F); - } else { - changeServerTickrate(tickrateSaved); - } - } - - /** - * Advances the game by 1 tick. - */ - public void advanceTick() { - advanceServerTick(); - advanceClientTick(); - } - - /** - * Sends a {@link TASmodPackets#TICKRATE_ADVANCE} packet to all clients - */ - private void advanceClientTick() { - // Do not check for ticksPerSecond==0 here, because at this point, ticksPerSecond is 20 for one tick! - try { -// TASmod.server.sendToAll(new TASmodBufferBuilder(TASmodPackets.TICKRATE_ADVANCE)); - } catch (Exception e) { - e.printStackTrace(); - } - } - - /** - * Advances the server by 1 tick - */ - private void advanceServerTick() { - if (ticksPerSecond == 0) { - advanceTick = true; - changeServerTickrate(tickrateSaved); - } - } - - /** - * Fired when a player joined the server - * - * @param player The player that joins the server - */ -// @Override -// public void onPlayerJoinedServerSide(EntityPlayerMP player) { -// if (TASmod.getServerInstance().isDedicatedServer()) { -// log("Sending the current tickrate (" + ticksPerSecond + ") to " + player.getName()); -// -// try { -// TASmod.server.sendTo(player, new TASmodBufferBuilder(TASmodPackets.TICKRATE_CHANGE).writeFloat(ticksPerSecond)); -// } catch (Exception e) { -// e.printStackTrace(); -// } -// } -// } - - /** - * The message to log - * - * @param msg - */ - private void log(String msg) { -// logger.debug(LoggerMarkers.Tickrate, msg); - } - -// @Override -// public void onServerStop(MinecraftServer server) { -// if (ticksPerSecond == 0 || advanceTick) { -// pauseGame(false); -// } -// } - - /** - * Enum for sending paused states for the tickratechanger - */ - public static enum TickratePauseState { - /** - * Set's the game to tickrate 0 - */ - PAUSE, - /** - * Set's the game to "tickrate saved" - */ - UNPAUSE, - /** - * Toggles between {@link #PAUSE} and {@link #UNPAUSE} - */ - TOGGLE; - } - -// @Override -// public PacketID[] getAcceptedPacketIDs() { -// return new TASmodPackets[] { TASmodPackets.TICKRATE_CHANGE, TASmodPackets.TICKRATE_ADVANCE, TASmodPackets.TICKRATE_ZERO }; -// } -// -// @Override -// public void onServerPacket(PacketID id, ByteBuffer buf, String username) throws PacketNotImplementedException, WrongSideException, Exception { -// TASmodPackets packet = (TASmodPackets) id; -// -// switch (packet) { -// case TICKRATE_CHANGE: -// float tickrate = TASmodBufferBuilder.readFloat(buf); -// changeTickrate(tickrate); -// break; -// case TICKRATE_ADVANCE: -// advanceTick(); -// break; -// case TICKRATE_ZERO: -// TickratePauseState state = TASmodBufferBuilder.readTickratePauseState(buf); -// -// switch (state) { -// case PAUSE: -// pauseGame(true); -// break; -// case UNPAUSE: -// pauseGame(false); -// break; -// case TOGGLE: -// togglePause(); -// default: -// break; -// } -// break; -// -// default: -// throw new PacketNotImplementedException(packet, this.getClass(), Side.SERVER); -// } -// } - -} diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index eb399f4..a6e3420 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -9,4 +9,4 @@ "msg.lotaslight.setTickrate":"Set the tickrate to %s", "keycategory.lotaslight.lotaslight":"LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 97a28b7..71d97ce 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -4,7 +4,8 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "MixinTickRateManager", - "MixinMinecraft" + "MixinMinecraft", + "MixinTickCommand" ], "injectors": { "defaultRequire": 1 From 91f43eea8945ba5cc9e57db1005c68007d4cb19c Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 24 Aug 2024 17:35:28 +0200 Subject: [PATCH 03/51] Fix tr0, add changing audio pitch --- .../lotas_light/LoTASLightClient.java | 28 +++--- .../lotas_light/duck/SoundPitchDuck.java | 7 ++ .../lotas_light/duck/Tickratechanger.java | 12 +++ .../mixin/AccessorSoundEngine.java | 13 +++ .../lotas_light/mixin/MixinMinecraft.java | 11 +++ .../mixin/MixinMinecraftServer.java | 70 +++++++++++++++ .../lotas_light/mixin/MixinTickCommand.java | 2 +- .../mixin/MixinTickRateManager.java | 90 +++++++++++++++++-- .../MixinTickrateChangerAchievements.java | 22 +++++ .../mixin/MixinTickrateChangerAudioPitch.java | 55 ++++++++++++ .../MixinTickrateChangerEnchantmentGlimm.java | 37 ++++++++ src/main/resources/lotaslight.mixins.json | 7 +- 12 files changed, 334 insertions(+), 20 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/duck/SoundPitchDuck.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/AccessorSoundEngine.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraftServer.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index e32dbf4..380f41d 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -2,6 +2,7 @@ import org.lwjgl.glfw.GLFW; +import com.minecrafttas.lotas_light.duck.Tickratechanger; import com.minecrafttas.lotas_light.event.EventClientGameLoop; import com.mojang.blaze3d.platform.InputConstants; @@ -74,8 +75,8 @@ private void increaseTickrate(Minecraft client) { rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); - clientTickrateChanger.setTickRate(tickrate); serverTickrateChanger.setTickRate(tickrate); + clientTickrateChanger.setTickRate(tickrate); } private void decreaseTickrate(Minecraft client) { @@ -90,33 +91,38 @@ private void decreaseTickrate(Minecraft client) { rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); - clientTickrateChanger.setTickRate(tickrate); serverTickrateChanger.setTickRate(tickrate); + clientTickrateChanger.setTickRate(tickrate); } private void freezeTickrate(Minecraft client) { - TickRateManager clientTickrateChanger = client.level.tickRateManager(); + TickRateManager clientTickrateManager = client.level.tickRateManager(); IntegratedServer server = client.getSingleplayerServer(); if (server == null) { return; } - ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + ServerTickRateManager serverTickrateManager = server.tickRateManager(); + + Tickratechanger clientTickrateChanger = (Tickratechanger) clientTickrateManager; + Tickratechanger serverTickrateChanger = (Tickratechanger) serverTickrateManager; - boolean isFrozen = clientTickrateChanger.isFrozen() && serverTickrateChanger.isFrozen(); - clientTickrateChanger.setFrozen(!isFrozen); - serverTickrateChanger.setFrozen(!isFrozen); + serverTickrateChanger.toggleTickrate0(); + clientTickrateChanger.toggleTickrate0(); } private void advanceTickrate(Minecraft client) { - TickRateManager clientTickrateChanger = client.level.tickRateManager(); + TickRateManager clientTickrateManager = client.level.tickRateManager(); IntegratedServer server = client.getSingleplayerServer(); if (server == null) { return; } - ServerTickRateManager serverTickrateChanger = server.tickRateManager(); + ServerTickRateManager serverTickrateManager = server.tickRateManager(); + + Tickratechanger clientTickrateChanger = (Tickratechanger) clientTickrateManager; + Tickratechanger serverTickrateChanger = (Tickratechanger) serverTickrateManager; - clientTickrateChanger.setFrozenTicksToRun(1); - serverTickrateChanger.setFrozenTicksToRun(1); + serverTickrateChanger.advanceTick(); + clientTickrateChanger.advanceTick(); } private void savestate() { diff --git a/src/main/java/com/minecrafttas/lotas_light/duck/SoundPitchDuck.java b/src/main/java/com/minecrafttas/lotas_light/duck/SoundPitchDuck.java new file mode 100644 index 0000000..2f5c7dc --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/duck/SoundPitchDuck.java @@ -0,0 +1,7 @@ +package com.minecrafttas.lotas_light.duck; + +public interface SoundPitchDuck { + + void updatePitch(); + +} diff --git a/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java b/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java new file mode 100644 index 0000000..ae8990d --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java @@ -0,0 +1,12 @@ +package com.minecrafttas.lotas_light.duck; + +public interface Tickratechanger { + + public float getTickrateSaved(); + + public void toggleTickrate0(); + + public void advanceTick(); + + public boolean isAdvanceTick(); +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorSoundEngine.java b/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorSoundEngine.java new file mode 100644 index 0000000..30555ca --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorSoundEngine.java @@ -0,0 +1,13 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.client.sounds.SoundManager; + +@Mixin(SoundManager.class) +public interface AccessorSoundEngine { + @Accessor + public SoundEngine getSoundEngine(); +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java index 404a29b..77929c4 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraft.java @@ -1,13 +1,16 @@ package com.minecrafttas.lotas_light.mixin; 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; +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; import com.minecrafttas.lotas_light.event.EventClientGameLoop; import net.minecraft.client.Minecraft; +import net.minecraft.client.multiplayer.ClientLevel; @Mixin(Minecraft.class) public class MixinMinecraft { @@ -16,4 +19,12 @@ public class MixinMinecraft { public void inject_Run(CallbackInfo ci) { EventClientGameLoop.EVENT.invoker().onGameLoop((Minecraft) (Object) this); } + + @Shadow + private ClientLevel level; + + @ModifyExpressionValue(method = "getTickTargetMillis", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F")) + public float modifyExpressionValue_GetTargetMillis(float original) { + return this.level.tickRateManager().millisecondsPerTick(); + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraftServer.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraftServer.java new file mode 100644 index 0000000..1ced147 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinMinecraftServer.java @@ -0,0 +1,70 @@ +package com.minecrafttas.lotas_light.mixin; + +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.ModifyVariable; +import org.spongepowered.asm.mixin.injection.Redirect; + +import com.minecrafttas.lotas_light.duck.Tickratechanger; + +import net.minecraft.Util; +import net.minecraft.server.MinecraftServer; +import net.minecraft.server.ServerTickRateManager; + +@Mixin(MinecraftServer.class) +public class MixinMinecraftServer { + + @Shadow + @Final + private ServerTickRateManager tickRateManager; + + @Shadow + private long nextTickTimeNanos; + + private long offset = 0; + private long currentTime = 0; + + @ModifyVariable(method = "runServer", at = @At(value = "STORE"), index = 1, ordinal = 0) + public long modifyVariable_preventOverload(long original) { + if (isTickrateZero()) + return 50L; + else + return original; + } + + @Redirect(method = "runServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getNanos()J")) + public long redirectGetMeasuringTimeMsInRun() { + return getCurrentTime(); + } + + @Redirect(method = "haveTime", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getNanos()J")) + public long redirectGetMeasuringTimeMsInShouldKeepTicking() { + return getCurrentTime(); + } + + private boolean isTickrateZero() { + return tickRateManager.tickrate() == 0f; + } + + private boolean isTickAdvance() { + return ((Tickratechanger) tickRateManager).isAdvanceTick(); + } + + /** + * Returns the time dependant on if the current tickrate is tickrate 0 + * @return In tickrates>0 the vanilla time - offset or the current time in tickrate 0 + */ + private long getCurrentTime() { + if (!isTickrateZero() || isTickAdvance()) { + currentTime = Util.getNanos(); //Set the current time that will be returned if the player decides to activate tickrate 0 + return Util.getNanos() - offset; //Returns the Current time - offset which was set while tickrate 0 was active + } else { + offset = Util.getNanos() - currentTime; //Creating the offset from the measured time and the stopped time + this.nextTickTimeNanos = currentTime + 50L; + + return currentTime; + } + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java index 3d573fe..b4629c9 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java @@ -12,6 +12,6 @@ public class MixinTickCommand { @ModifyExpressionValue(method = "register", at = @At(value = "CONSTANT", args = "floatValue=1.0F")) private static float modifyArg_FloatArg(float original) { - return .1f; + return 0f; } } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index 1fc0f16..705514e 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -1,27 +1,103 @@ package com.minecrafttas.lotas_light.mixin; +import org.objectweb.asm.Opcodes; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; -import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import com.minecrafttas.lotas_light.duck.SoundPitchDuck; +import com.minecrafttas.lotas_light.duck.Tickratechanger; +import net.minecraft.client.Minecraft; import net.minecraft.world.TickRateManager; @Mixin(TickRateManager.class) -public abstract class MixinTickRateManager { +public abstract class MixinTickRateManager implements Tickratechanger { + @Unique + private float tickrateSaved; + @Unique + private boolean advanceTickrate; - @ModifyReturnValue(method = "isEntityFrozen", at = @At(value = "RETURN")) - public boolean modifyReturn_IsEntityFrozen(boolean original) { - return !runsNormally(); - } + @Shadow + private float tickrate; @ModifyExpressionValue(method = "setTickRate", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F")) public float modifyExpressionValue_SetTickRate(float original, float f) { + f = Math.max(0f, f); + if (this.tickrate != 0) { + tickrateSaved = tickrate; + } return f; } @Shadow - public abstract boolean runsNormally(); + private long nanosecondsPerTick; + + @Redirect(method = "setTickRate", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/world/TickRateManager;nanosecondsPerTick:J")) + private void redirect_setTickRate(TickRateManager manager, long original, float tickrate) { + if (tickrate != 0) { + nanosecondsPerTick = original; + } else { + nanosecondsPerTick = Long.MAX_VALUE; + } + updatePitch(); + } + + @Override + public float getTickrateSaved() { + return tickrateSaved; + } + + @Override + public void toggleTickrate0() { + advanceTickrate = false; + if (tickrate == 0) { + setTickRate(tickrateSaved); + } else { + setTickRate(0f); + } + } + + @Override + public void advanceTick() { + if (tickrate == 0) { + setTickRate(tickrateSaved); + this.advanceTickrate = true; + } + } + + @Override + public boolean isAdvanceTick() { + return advanceTickrate; + } + + @Inject(method = "tick", at = @At("RETURN")) + public void inject_Tick(CallbackInfo ci) { + if (advanceTickrate) { + this.advanceTickrate = false; + setTickRate(0); + } + } + + private static void updatePitch() { + AccessorSoundEngine soundEngine = (AccessorSoundEngine) Minecraft.getInstance().getSoundManager(); + + if (soundEngine == null) + return; + + SoundPitchDuck soundManager = (SoundPitchDuck) soundEngine.getSoundEngine(); + + if (soundManager == null) + return; + + soundManager.updatePitch(); + } + + @Shadow + protected abstract void setTickRate(float f); } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java new file mode 100644 index 0000000..6627b35 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java @@ -0,0 +1,22 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import net.minecraft.client.Minecraft; + +// TODO Currently broken! Not registered in mixin.json +@Mixin(targets = "net/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance") +public class MixinTickrateChangerAchievements { + + @ModifyVariable(method = "Lnet/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance;render(II)Z", at = @At(value = "STORE"), ordinal = 0, index = 3) + public long modifyAnimationTime(long animationTimer) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) + return (long) mc.level.tickRateManager().millisecondsPerTick(); + else + return animationTimer; + } + +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java new file mode 100644 index 0000000..dde6c42 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java @@ -0,0 +1,55 @@ +package com.minecrafttas.lotas_light.mixin; + +import java.util.Map; + +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.CallbackInfoReturnable; + +import com.minecrafttas.lotas_light.duck.SoundPitchDuck; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.resources.sounds.SoundInstance; +import net.minecraft.client.sounds.ChannelAccess.ChannelHandle; +import net.minecraft.client.sounds.SoundEngine; +import net.minecraft.util.Mth; + +/** + * Slows down Audio + * @author Scribble + */ +@Mixin(SoundEngine.class) +public abstract class MixinTickrateChangerAudioPitch implements SoundPitchDuck { + + @Shadow + private Map instanceToChannel; + + @Shadow + private boolean loaded; + + @Inject(method = "calculatePitch", at = @At(value = "HEAD"), cancellable = true) + public void redosetPitch(SoundInstance soundInstance, CallbackInfoReturnable ci) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) { + ci.setReturnValue(Mth.clamp(soundInstance.getPitch(), 0.5F, 2.0F) * (mc.level.tickRateManager().tickrate() / 20F)); + ci.cancel(); + } + } + + @Override + public void updatePitch() { + if (this.loaded) { + this.instanceToChannel.forEach((soundInstance, channelHandle) -> { + channelHandle.execute(channel -> { + channel.setPitch(calculatePitch(soundInstance)); + }); + }); + } + } + + @Shadow + protected abstract float calculatePitch(SoundInstance soundInstance); + +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java new file mode 100644 index 0000000..91a9ada --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java @@ -0,0 +1,37 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.RenderStateShard; + +/** + * Slows down the Enchantment *foil* + * @author Scribble + */ +//TODO Currently broken! Not registered in mixin.json +@Mixin(RenderStateShard.class) +public abstract class MixinTickrateChangerEnchantmentGlimm { + + @ModifyVariable(method = "renderFoilLayer", at = @At("STORE"), index = 2, ordinal = 0) + private static float modifyrenderEffect1(float f) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) + return (mc.level.tickRateManager().millisecondsPerTick() % 3000L) / 3000.0F / 8F; + else + return f; + } + + @ModifyVariable(method = "renderFoilLayer", at = @At("STORE"), index = 3, ordinal = 1) + private static float modifyrenderEffect2(float f) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) + return (mc.level.tickRateManager().millisecondsPerTick() % 4873L) / 4873.0F / 8F; + else + return f; + } + +} +//#endif \ No newline at end of file diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 71d97ce..727a162 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -4,8 +4,13 @@ "compatibilityLevel": "JAVA_21", "mixins": [ "MixinTickRateManager", + "MixinTickCommand", + "MixinMinecraftServer", + ], + "client":[ "MixinMinecraft", - "MixinTickCommand" + "AccessorSoundEngine", + "MixinTickrateChangerAudioPitch" ], "injectors": { "defaultRequire": 1 From 6a8666b6889a1becc7865c715f0adb6197c4dda2 Mon Sep 17 00:00:00 2001 From: Scribble Date: Wed, 4 Sep 2024 22:29:22 +0200 Subject: [PATCH 04/51] Added config files --- .../minecrafttas/lotas_light/LoTASLight.java | 5 + .../lotas_light/config/AbstractDataFile.java | 154 ++++++++++++++++++ .../lotas_light/config/Configuration.java | 107 ++++++++++++ 3 files changed, 266 insertions(+) create mode 100644 src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/config/Configuration.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index a52fa20..f7879a2 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -1,9 +1,14 @@ package com.minecrafttas.lotas_light; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + import net.fabricmc.api.ModInitializer; public class LoTASLight implements ModInitializer { + public static Logger LOGGER = LogManager.getLogger("LoTAS-Light"); + @Override public void onInitialize() { } diff --git a/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java b/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java new file mode 100644 index 0000000..e19366b --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java @@ -0,0 +1,154 @@ +package com.minecrafttas.lotas_light.config; + +import java.io.FileNotFoundException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.InvalidPropertiesFormatException; +import java.util.Properties; + +import com.minecrafttas.lotas_light.LoTASLight; + +public abstract class AbstractDataFile { + + /** + * The save location of this data file + */ + protected final Path file; + + /** + * The name of this data file, used in logging + */ + protected final String name; + /** + * The comment stored in the data file, to help recognize the file + */ + protected final String comment; + + /** + * The properties of this data file. + */ + protected Properties properties; + + /** + * Creates an abstract data file and creates it's directory if it doesn't exist + * @param file The {@link #file save location} of the data file + * @param name The {@link #name} of the data file, used in logging + * @param comment The {@link #comment} in the data file + */ + protected AbstractDataFile(Path file, String name, String comment) { + this.file = file; + this.name = name; + this.comment = comment; + this.properties = new Properties(); + + createDirectory(file); + } + + /** + * Creates the directory for the file if it doesn't exist + * @param file The file to create the directory for + */ + protected void createDirectory(Path file) { + try { + Files.createDirectories(file.getParent()); + } catch (IOException e) { + LoTASLight.LOGGER.catching(e); + } + } + + public void load() { + if (Files.exists(file)) { + load(file); + } + } + + public void load(Path file) { + InputStream fis; + Properties newProp = new Properties(); + try { + fis = Files.newInputStream(file); + newProp.load(fis); + fis.close(); + } catch (InvalidPropertiesFormatException e) { + LoTASLight.LOGGER.error("The {} file could not be read", name, e); + return; + } catch (FileNotFoundException e) { + LoTASLight.LOGGER.warn("No {} file found: {}", name, file); + return; + } catch (IOException e) { + LoTASLight.LOGGER.error("An error occured while reading the {} file", file, e); + return; + } + this.properties = newProp; + } + + /** + * Loads the xml {@link #file} into {@link #properties} if it exists + */ + public void loadFromXML() { + if (Files.exists(file)) { + loadFromXML(file); + } + } + + /** + * @param file The xml file to load into {@link #properties} + */ + public void loadFromXML(Path file) { + InputStream fis; + Properties newProp = new Properties(); + try { + fis = Files.newInputStream(file); + newProp.loadFromXML(fis); + fis.close(); + } catch (InvalidPropertiesFormatException e) { + LoTASLight.LOGGER.error("The {} file could not be read", name, e); + return; + } catch (FileNotFoundException e) { + LoTASLight.LOGGER.warn("No {} file found: {}", name, file); + return; + } catch (IOException e) { + LoTASLight.LOGGER.error("An error occured while reading the {} file", file, e); + return; + } + this.properties = newProp; + } + + public void save() { + this.save(file); + } + + public void save(Path file) { + try { + OutputStream fos = Files.newOutputStream(file); + properties.store(fos, comment); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } + + /** + * Saves the {@link #properties} to the {@link #file} location + */ + public void saveToXML() { + this.saveToXML(file); + } + + /** + * Saves the {@link #properties} to a specified file + * @param file The file to save the {@link #properties} to + */ + public void saveToXML(Path file) { + try { + OutputStream fos = Files.newOutputStream(file); + properties.storeToXML(fos, comment, "UTF-8"); + fos.close(); + } catch (IOException e) { + e.printStackTrace(); + } + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java new file mode 100644 index 0000000..5f1d22a --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java @@ -0,0 +1,107 @@ +package com.minecrafttas.lotas_light.config; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Properties; + +/** + * A very simple configuration class + * + * @author Scribble + */ + +public class Configuration extends AbstractDataFile { + + private enum ConfigOptions { + + DEFAULT_TICKRATE("defaultTickrate", "20.0"), + SHOW_MESSAGES("showMessages", "true"); + + private String key; + private String defaultValue; + + ConfigOptions(String key, String defaultValue) { + this.key = key; + this.defaultValue = defaultValue; + } + + public String getConfigKey() { + return key; + } + + public String getDefaultValue() { + return defaultValue; + } + } + + public Configuration(String comment, Path configFile) { + super(configFile, "config", comment); + } + + @Override + public void loadFromXML() { + if (Files.exists(file)) { + loadFromXML(file); + } + if (properties == null || !Files.exists(file)) { + properties = generateDefault(); + saveToXML(); + } + } + + /** + * Generates the default property list from the values provided in {@link #registry} + * @return The default property list + */ + public Properties generateDefault() { + Properties newProperties = new Properties(); + + for (ConfigOptions configOption : ConfigOptions.values()) { + newProperties.put(configOption.getConfigKey(), configOption.getDefaultValue()); + } + return newProperties; + } + + public String get(ConfigOptions configOption) { + return properties.getProperty(configOption.getConfigKey(), configOption.getDefaultValue()); + } + + public int getInt(ConfigOptions configOption) { + return Integer.parseInt(get(configOption)); + } + + public boolean getBoolean(ConfigOptions configOption) { + return Boolean.parseBoolean(get(configOption)); + } + + public boolean has(ConfigOptions configOption) { + return properties.contains(configOption.getConfigKey()); + } + + public void set(ConfigOptions configOption, String value) { + if (properties == null) { + throw new NullPointerException("Config needs to be loaded first, before trying to set a value"); + } + properties.setProperty(configOption.getConfigKey(), value); + saveToXML(); + } + + public void set(ConfigOptions configOption, int value) { + String val = Integer.toString(value); + set(configOption, val); + } + + public void set(ConfigOptions configOption, boolean value) { + String val = Boolean.toString(value); + set(configOption, val); + } + + public void reset(ConfigOptions configOption) { + set(configOption, configOption.getDefaultValue()); + } + + public void delete(ConfigOptions configOption) { + properties.remove(configOption); + saveToXML(); + } +} From 4c478317dcb138b9be0b2c3bfb3f1af2dd9e9830 Mon Sep 17 00:00:00 2001 From: Scribble Date: Wed, 18 Sep 2024 21:45:47 +0200 Subject: [PATCH 05/51] Reentering the world will keep the tickrate applied --- src/client/java/com/example/ExampleModClient.java | 10 ---------- .../example/mixin/client/ExampleClientMixin.java | 15 --------------- src/client/resources/modid.client.mixins.json | 11 ----------- .../lotas_light/LoTASLightClient.java | 1 + .../lotas_light/mixin/MixinTickRateManager.java | 15 ++++++++++++--- src/main/resources/lotaslight.mixins.json | 2 +- 6 files changed, 14 insertions(+), 40 deletions(-) delete mode 100644 src/client/java/com/example/ExampleModClient.java delete mode 100644 src/client/java/com/example/mixin/client/ExampleClientMixin.java delete mode 100644 src/client/resources/modid.client.mixins.json diff --git a/src/client/java/com/example/ExampleModClient.java b/src/client/java/com/example/ExampleModClient.java deleted file mode 100644 index e2b0436..0000000 --- a/src/client/java/com/example/ExampleModClient.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.example; - -import net.fabricmc.api.ClientModInitializer; - -public class ExampleModClient implements ClientModInitializer { - @Override - public void onInitializeClient() { - // This entrypoint is suitable for setting up client-specific logic, such as rendering. - } -} \ No newline at end of file diff --git a/src/client/java/com/example/mixin/client/ExampleClientMixin.java b/src/client/java/com/example/mixin/client/ExampleClientMixin.java deleted file mode 100644 index 7ee50d1..0000000 --- a/src/client/java/com/example/mixin/client/ExampleClientMixin.java +++ /dev/null @@ -1,15 +0,0 @@ -package com.example.mixin.client; - -import net.minecraft.client.MinecraftClient; -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(MinecraftClient.class) -public class ExampleClientMixin { - @Inject(at = @At("HEAD"), method = "run") - private void init(CallbackInfo info) { - // This code is injected into the start of MinecraftClient.run()V - } -} \ No newline at end of file diff --git a/src/client/resources/modid.client.mixins.json b/src/client/resources/modid.client.mixins.json deleted file mode 100644 index 9341450..0000000 --- a/src/client/resources/modid.client.mixins.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "required": true, - "package": "com.example.mixin.client", - "compatibilityLevel": "JAVA_21", - "client": [ - "ExampleClientMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index 380f41d..c3b07ee 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -61,6 +61,7 @@ private void registerKeybindings() { } }); + } private void increaseTickrate(Minecraft client) { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index 705514e..b3d4135 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -14,6 +14,7 @@ import com.minecrafttas.lotas_light.duck.Tickratechanger; import net.minecraft.client.Minecraft; +import net.minecraft.util.TimeUtil; import net.minecraft.world.TickRateManager; @Mixin(TickRateManager.class) @@ -23,8 +24,18 @@ public abstract class MixinTickRateManager implements Tickratechanger { @Unique private boolean advanceTickrate; + private static float tickrateMirror = 20f; + @Shadow private float tickrate; + @Shadow + private long nanosecondsPerTick; + + @Inject(method = "", at = @At(value = "RETURN")) + public void inject_trcConstructor(CallbackInfo ci) { + this.tickrate = tickrateMirror; + this.nanosecondsPerTick = (long) ((double) TimeUtil.NANOSECONDS_PER_SECOND / (double) tickrateMirror); + } @ModifyExpressionValue(method = "setTickRate", at = @At(value = "INVOKE", target = "Ljava/lang/Math;max(FF)F")) public float modifyExpressionValue_SetTickRate(float original, float f) { @@ -32,12 +43,10 @@ public float modifyExpressionValue_SetTickRate(float original, float f) { if (this.tickrate != 0) { tickrateSaved = tickrate; } + tickrateMirror = f; return f; } - @Shadow - private long nanosecondsPerTick; - @Redirect(method = "setTickRate", at = @At(value = "FIELD", opcode = Opcodes.PUTFIELD, target = "Lnet/minecraft/world/TickRateManager;nanosecondsPerTick:J")) private void redirect_setTickRate(TickRateManager manager, long original, float tickrate) { if (tickrate != 0) { diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 727a162..83b75f2 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -5,7 +5,7 @@ "mixins": [ "MixinTickRateManager", "MixinTickCommand", - "MixinMinecraftServer", + "MixinMinecraftServer" ], "client":[ "MixinMinecraft", From 6669aa13f573deacadca41d802397d62baaf004b Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 19 Sep 2024 21:40:18 +0200 Subject: [PATCH 06/51] Add config command --- .../minecrafttas/lotas_light/LoTASLight.java | 7 +++ .../lotas_light/LoTASLightClient.java | 29 +++++++++++- .../command/LoTASLightCommand.java | 45 +++++++++++++++++++ .../lotas_light/config/Configuration.java | 2 +- .../mixin/MixinTickRateManager.java | 4 +- .../assets/lotaslight/lang/en_us.json | 3 ++ 6 files changed, 86 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/command/LoTASLightCommand.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index f7879a2..2d265ac 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -3,7 +3,10 @@ import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; +import com.minecrafttas.lotas_light.command.LoTASLightCommand; + import net.fabricmc.api.ModInitializer; +import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; public class LoTASLight implements ModInitializer { @@ -11,6 +14,10 @@ public class LoTASLight implements ModInitializer { @Override public void onInitialize() { + + CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { + LoTASLightCommand.register(dispatcher); + }); } } diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index c3b07ee..5748e65 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -1,13 +1,18 @@ package com.minecrafttas.lotas_light; +import java.nio.file.Path; + import org.lwjgl.glfw.GLFW; +import com.minecrafttas.lotas_light.config.Configuration; +import com.minecrafttas.lotas_light.config.Configuration.ConfigOptions; import com.minecrafttas.lotas_light.duck.Tickratechanger; import com.minecrafttas.lotas_light.event.EventClientGameLoop; import com.mojang.blaze3d.platform.InputConstants; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; +import net.minecraft.ChatFormatting; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; import net.minecraft.client.server.IntegratedServer; @@ -27,8 +32,16 @@ public class LoTASLightClient implements ClientModInitializer { private float[] rates = new float[] { .1f, .2f, .5f, 1f, 2f, 5f, 10f, 20f, 40f, 100f }; private short rateIndex = 7; + private Path configpath; + public static Configuration config; + private boolean showHint = true; + @Override public void onInitializeClient() { + Minecraft mc = Minecraft.getInstance(); + configpath = mc.gameDirectory.toPath().resolve("configs/lotas-light.cfg"); + config = new Configuration("LoTAS-Light config", configpath); + config.loadFromXML(); registerKeybindings(); } @@ -75,7 +88,13 @@ private void increaseTickrate(Minecraft client) { rateIndex++; rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; - client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + if (config.getBoolean(ConfigOptions.SHOW_MESSAGES)) { + if (showHint) { + showHint = false; + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.turnOff", tickrate).withStyle(ChatFormatting.YELLOW)); + } + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + } serverTickrateChanger.setTickRate(tickrate); clientTickrateChanger.setTickRate(tickrate); } @@ -91,7 +110,13 @@ private void decreaseTickrate(Minecraft client) { rateIndex--; rateIndex = (short) Math.clamp(rateIndex, 0, rates.length - 1); float tickrate = rates[rateIndex]; - client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + if (config.getBoolean(ConfigOptions.SHOW_MESSAGES)) { + if (showHint) { + showHint = false; + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.turnOff", tickrate).withStyle(ChatFormatting.YELLOW)); + } + client.gui.getChat().addMessage(Component.translatable("msg.lotaslight.setTickrate", tickrate)); + } serverTickrateChanger.setTickRate(tickrate); clientTickrateChanger.setTickRate(tickrate); } diff --git a/src/main/java/com/minecrafttas/lotas_light/command/LoTASLightCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/LoTASLightCommand.java new file mode 100644 index 0000000..ea83b43 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/command/LoTASLightCommand.java @@ -0,0 +1,45 @@ +package com.minecrafttas.lotas_light.command; + +import com.minecrafttas.lotas_light.LoTASLightClient; +import com.minecrafttas.lotas_light.config.Configuration.ConfigOptions; +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.FloatArgumentType; +import com.mojang.brigadier.context.CommandContext; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.Component; + +public class LoTASLightCommand { + public static void register(CommandDispatcher commandDispatcher) { + //@formatter:off + commandDispatcher + .register(Commands.literal("lotaslight") + .then(Commands.literal("showMessages") + .then(Commands.literal("true").executes(LoTASLightCommand::showMessages)) + .then(Commands.literal("false").executes(LoTASLightCommand::hideMessages)) + ) + .then(Commands.literal("defaultTickrate") + .then(Commands.argument("tickrate", FloatArgumentType.floatArg(.1f, 60f)).executes(LoTASLightCommand::defaultTickrate))) + ); + //@formatter:on + } + + public static int showMessages(CommandContext context) { + LoTASLightClient.config.set(ConfigOptions.SHOW_MESSAGES, true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.showmsg.true"), true); + return 1; + } + + public static int hideMessages(CommandContext context) { + LoTASLightClient.config.set(ConfigOptions.SHOW_MESSAGES, false); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.showmsg.false"), true); + return 1; + } + + public static int defaultTickrate(CommandContext context) { + float tickrate = context.getArgument("tickrate", Float.class); + LoTASLightClient.config.set(ConfigOptions.DEFAULT_TICKRATE, Float.toString(tickrate)); + return 1; + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java index 5f1d22a..b5003d0 100644 --- a/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java +++ b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java @@ -12,7 +12,7 @@ public class Configuration extends AbstractDataFile { - private enum ConfigOptions { + public enum ConfigOptions { DEFAULT_TICKRATE("defaultTickrate", "20.0"), SHOW_MESSAGES("showMessages", "true"); diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index b3d4135..7153390 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -10,6 +10,8 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.minecrafttas.lotas_light.LoTASLightClient; +import com.minecrafttas.lotas_light.config.Configuration.ConfigOptions; import com.minecrafttas.lotas_light.duck.SoundPitchDuck; import com.minecrafttas.lotas_light.duck.Tickratechanger; @@ -24,7 +26,7 @@ public abstract class MixinTickRateManager implements Tickratechanger { @Unique private boolean advanceTickrate; - private static float tickrateMirror = 20f; + private static float tickrateMirror = Float.parseFloat(LoTASLightClient.config.get(ConfigOptions.DEFAULT_TICKRATE)); @Shadow private float tickrate; diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index a6e3420..96929bd 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -7,6 +7,9 @@ "key.lotaslight.loadstate":"Loadstate", "msg.lotaslight.setTickrate":"Set the tickrate to %s", + "msg.lotaslight.turnOff":"To turn off these messages, use /lotaslight showMessage false", + "msg.lotaslight.showmsg.true":"Tickrate messages enabled", + "msg.lotaslight.showmsg.false":"Tickrate messages disabled", "keycategory.lotaslight.lotaslight":"LoTAS-Light" } From e09ca0f5cb55ea92f2b920f5c8ffa034a8bdb1c4 Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 19 Sep 2024 22:45:59 +0200 Subject: [PATCH 07/51] Added new event --- .../lotas_light/LoTASLightClient.java | 15 +++++++++++++ .../event/HudRenderExperienceCallback.java | 15 +++++++++++++ .../lotas_light/mixin/MixinGui.java | 20 ++++++++++++++++++ .../resources/assets/lotaslight/potion.png | Bin 0 -> 33722 bytes .../lotaslight/textures/gui/iblk9xrq3z.png | Bin 0 -> 33722 bytes src/main/resources/fabric.mod.json | 2 +- src/main/resources/lotaslight.mixins.json | 3 ++- 7 files changed, 53 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/event/HudRenderExperienceCallback.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinGui.java create mode 100644 src/main/resources/assets/lotaslight/potion.png create mode 100644 src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index 5748e65..ffd701e 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -8,15 +8,20 @@ import com.minecrafttas.lotas_light.config.Configuration.ConfigOptions; import com.minecrafttas.lotas_light.duck.Tickratechanger; import com.minecrafttas.lotas_light.event.EventClientGameLoop; +import com.minecrafttas.lotas_light.event.HudRenderExperienceCallback; import com.mojang.blaze3d.platform.InputConstants; +import com.mojang.blaze3d.systems.RenderSystem; import net.fabricmc.api.ClientModInitializer; import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper; import net.minecraft.ChatFormatting; +import net.minecraft.client.DeltaTracker; import net.minecraft.client.KeyMapping; import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.server.IntegratedServer; import net.minecraft.network.chat.Component; +import net.minecraft.resources.ResourceLocation; import net.minecraft.server.ServerTickRateManager; import net.minecraft.world.TickRateManager; @@ -43,6 +48,7 @@ public void onInitializeClient() { config = new Configuration("LoTAS-Light config", configpath); config.loadFromXML(); registerKeybindings(); + HudRenderExperienceCallback.EVENT.register(this::drawHud); } private void registerKeybindings() { @@ -151,6 +157,15 @@ private void advanceTickrate(Minecraft client) { clientTickrateChanger.advanceTick(); } + private void drawHud(GuiGraphics context, DeltaTracker deltaTicks) { + RenderSystem.enableBlend(); + RenderSystem.setShaderColor(1, 1, 1, .5f); + context.blit(ResourceLocation.fromNamespaceAndPath("lotaslight", "textures/gui/iblk9xrq3z.png"), Minecraft.getInstance().getWindow().getGuiScaledWidth() / 2 - 10, Minecraft.getInstance().getWindow().getGuiScaledHeight() + - 50, 0, 0, 20, 20, 20, 20); + RenderSystem.setShaderColor(1, 1, 1, 1); + RenderSystem.disableBlend(); + } + private void savestate() { } diff --git a/src/main/java/com/minecrafttas/lotas_light/event/HudRenderExperienceCallback.java b/src/main/java/com/minecrafttas/lotas_light/event/HudRenderExperienceCallback.java new file mode 100644 index 0000000..f7cdda1 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/event/HudRenderExperienceCallback.java @@ -0,0 +1,15 @@ +package com.minecrafttas.lotas_light.event; + +import net.fabricmc.fabric.api.event.Event; +import net.fabricmc.fabric.api.event.EventFactory; +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.gui.GuiGraphics; + +public interface HudRenderExperienceCallback { + public static Event EVENT = EventFactory.createArrayBacked(HudRenderExperienceCallback.class, (listeners) -> (matrixStack, delta) -> { + for (HudRenderExperienceCallback listener : listeners) + listener.onRenderPre(matrixStack, delta); + }); + + public void onRenderPre(GuiGraphics drawContext, DeltaTracker tickCounter); +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinGui.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinGui.java new file mode 100644 index 0000000..378444b --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinGui.java @@ -0,0 +1,20 @@ +package com.minecrafttas.lotas_light.mixin; + +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; + +import com.minecrafttas.lotas_light.event.HudRenderExperienceCallback; + +import net.minecraft.client.DeltaTracker; +import net.minecraft.client.gui.Gui; +import net.minecraft.client.gui.GuiGraphics; + +@Mixin(Gui.class) +public class MixinGui { + @Inject(method = "renderExperienceLevel", at = @At("HEAD")) + private void onRenderExperienceLevel(GuiGraphics guiGraphics, DeltaTracker deltaTracker, CallbackInfo ci) { + HudRenderExperienceCallback.EVENT.invoker().onRenderPre(guiGraphics, deltaTracker); + } +} diff --git a/src/main/resources/assets/lotaslight/potion.png b/src/main/resources/assets/lotaslight/potion.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ee743cdbc3542f66943dd13813c6b474486d07 GIT binary patch literal 33722 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 E0!?febpQYW literal 0 HcmV?d00001 diff --git a/src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png b/src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png new file mode 100644 index 0000000000000000000000000000000000000000..f8ee743cdbc3542f66943dd13813c6b474486d07 GIT binary patch literal 33722 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 E0!?febpQYW literal 0 HcmV?d00001 diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index 29b1ce6..9eff77f 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -13,7 +13,7 @@ "sources": "https://github.com/MinecraftTAS/LoTAS-Light" }, "license": "GPLv3", - "icon": "assets/modid/icon.png", + "icon": "assets/lotaslight/potion.png", "environment": "*", "entrypoints": { "main": [ diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 83b75f2..050bbad 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -10,7 +10,8 @@ "client":[ "MixinMinecraft", "AccessorSoundEngine", - "MixinTickrateChangerAudioPitch" + "MixinTickrateChangerAudioPitch", + "MixinGui" ], "injectors": { "defaultRequire": 1 From 9bbeda9fbbfbe5fc1de3bef1b87676621b27483c Mon Sep 17 00:00:00 2001 From: Scribble Date: Fri, 20 Sep 2024 12:08:57 +0200 Subject: [PATCH 08/51] Update Crowdin configuration file --- crowdin.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 crowdin.yml diff --git a/crowdin.yml b/crowdin.yml new file mode 100644 index 0000000..229514f --- /dev/null +++ b/crowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /src/main/resources/assets/lotaslight/lang/en_us.json + translation: /src/main/resources/assets/lotaslight/lang/%locale_with_underscore%.%file_extension% From bca7bfdc9a1970d7298de714ab8e3569d93d22be Mon Sep 17 00:00:00 2001 From: Scribble Date: Fri, 20 Sep 2024 16:48:06 +0200 Subject: [PATCH 09/51] Set up crowdin translations and automatic updates (#3) --- src/main/resources/assets/lotaslight/lang/de_de.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/es_es.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/fr_fr.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/ja_jp.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/ko_kr.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/pl_pl.json | 9 +++++++++ src/main/resources/assets/lotaslight/lang/zh_cn.json | 9 +++++++++ 7 files changed, 63 insertions(+) create mode 100644 src/main/resources/assets/lotaslight/lang/de_de.json create mode 100644 src/main/resources/assets/lotaslight/lang/es_es.json create mode 100644 src/main/resources/assets/lotaslight/lang/fr_fr.json create mode 100644 src/main/resources/assets/lotaslight/lang/ja_jp.json create mode 100644 src/main/resources/assets/lotaslight/lang/ko_kr.json create mode 100644 src/main/resources/assets/lotaslight/lang/pl_pl.json create mode 100644 src/main/resources/assets/lotaslight/lang/zh_cn.json diff --git a/src/main/resources/assets/lotaslight/lang/de_de.json b/src/main/resources/assets/lotaslight/lang/de_de.json new file mode 100644 index 0000000..3bd163c --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/de_de.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Tickrate erhöhen", + "key.lotaslight.decreaseTickrate": "Tickrate verringern", + "key.lotaslight.freezeTickrate": "Tickrate stoppen", + "key.lotaslight.advanceTickrate": "Advance einen Tick", + "key.lotaslight.savestate": "Sicherungspunkt erstellen", + "key.lotaslight.loadstate": "Sicherungspunkt laden", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/es_es.json b/src/main/resources/assets/lotaslight/lang/es_es.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/es_es.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/fr_fr.json b/src/main/resources/assets/lotaslight/lang/fr_fr.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/fr_fr.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/ja_jp.json b/src/main/resources/assets/lotaslight/lang/ja_jp.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/ja_jp.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/ko_kr.json b/src/main/resources/assets/lotaslight/lang/ko_kr.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/ko_kr.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/pl_pl.json b/src/main/resources/assets/lotaslight/lang/pl_pl.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/pl_pl.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/zh_cn.json b/src/main/resources/assets/lotaslight/lang/zh_cn.json new file mode 100644 index 0000000..ff1642f --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/zh_cn.json @@ -0,0 +1,9 @@ +{ + "key.lotaslight.increaseTickrate": "Increase Tickrate", + "key.lotaslight.decreaseTickrate": "Decrease Tickrate", + "key.lotaslight.freezeTickrate": "Freeze Tickrate", + "key.lotaslight.advanceTickrate": "Advance Tick", + "key.lotaslight.savestate": "Savestate", + "key.lotaslight.loadstate": "Loadstate", + "keycategory.lotaslight.lotaslight": "LoTAS-Light" +} \ No newline at end of file From ec5b0b1dabf67aa7ec348db5f9a76ece5428ab27 Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 21 Sep 2024 16:01:26 +0200 Subject: [PATCH 10/51] Add worldborder, toasts, subtitles, glint to trc --- .../lotas_light/duck/Tickratechanger.java | 2 ++ .../mixin/MixinTickRateManager.java | 14 ++++++++++ .../MixinTickrateChangerAchievements.java | 12 ++++---- .../MixinTickrateChangerEnchantmentGlimm.java | 21 ++++---------- .../MixinTickrateChangerSubtitleOverlay.java | 28 +++++++++++++++++++ .../MixinTickrateChangerWorldborder.java | 23 +++++++++++++++ .../assets/lotaslight/lang/en_us.json | 2 +- src/main/resources/lotaslight.mixins.json | 6 +++- 8 files changed, 86 insertions(+), 22 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java diff --git a/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java b/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java index ae8990d..2cd2483 100644 --- a/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java +++ b/src/main/java/com/minecrafttas/lotas_light/duck/Tickratechanger.java @@ -9,4 +9,6 @@ public interface Tickratechanger { public void advanceTick(); public boolean isAdvanceTick(); + + public long getAdjustedMilliseconds(); } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index 7153390..73d9123 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -28,6 +28,10 @@ public abstract class MixinTickRateManager implements Tickratechanger { private static float tickrateMirror = Float.parseFloat(LoTASLightClient.config.get(ConfigOptions.DEFAULT_TICKRATE)); + private static long timeOffset = 0L; + private static long timeSinceTC = System.currentTimeMillis(); + private static long fakeTimeSinceTC = System.currentTimeMillis(); + @Shadow private float tickrate; @Shadow @@ -45,6 +49,9 @@ public float modifyExpressionValue_SetTickRate(float original, float f) { if (this.tickrate != 0) { tickrateSaved = tickrate; } + long time = System.currentTimeMillis() - timeSinceTC - timeOffset; + fakeTimeSinceTC += (long) (time * (tickrate / 20F)); + timeSinceTC = System.currentTimeMillis() - timeOffset; tickrateMirror = f; return f; } @@ -109,6 +116,13 @@ private static void updatePitch() { soundManager.updatePitch(); } + @Override + public long getAdjustedMilliseconds() { + long time = System.currentTimeMillis() - timeSinceTC - timeOffset; + time *= (tickrate / 20F); + return (long) (fakeTimeSinceTC + time); + } + @Shadow protected abstract void setTickRate(float f); } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java index 6627b35..61eb8e7 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java @@ -2,7 +2,9 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.minecrafttas.lotas_light.duck.Tickratechanger; import net.minecraft.client.Minecraft; @@ -10,13 +12,13 @@ @Mixin(targets = "net/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance") public class MixinTickrateChangerAchievements { - @ModifyVariable(method = "Lnet/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance;render(II)Z", at = @At(value = "STORE"), ordinal = 0, index = 3) - public long modifyAnimationTime(long animationTimer) { + @ModifyExpressionValue(method = "Lnet/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance;render(II)Z", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getMillis()J")) + public long modifyAnimationTimeAdvancements(long millis) { Minecraft mc = Minecraft.getInstance(); if (mc.level != null) - return (long) mc.level.tickRateManager().millisecondsPerTick(); + return ((Tickratechanger) mc.level.tickRateManager()).getAdjustedMilliseconds(); else - return animationTimer; + return millis; } } diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java index 91a9ada..32221aa 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java @@ -2,7 +2,9 @@ import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyVariable; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.minecrafttas.lotas_light.duck.Tickratechanger; import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.RenderStateShard; @@ -11,27 +13,16 @@ * Slows down the Enchantment *foil* * @author Scribble */ -//TODO Currently broken! Not registered in mixin.json @Mixin(RenderStateShard.class) public abstract class MixinTickrateChangerEnchantmentGlimm { - @ModifyVariable(method = "renderFoilLayer", at = @At("STORE"), index = 2, ordinal = 0) - private static float modifyrenderEffect1(float f) { - Minecraft mc = Minecraft.getInstance(); - if (mc.level != null) - return (mc.level.tickRateManager().millisecondsPerTick() % 3000L) / 3000.0F / 8F; - else - return f; - } - - @ModifyVariable(method = "renderFoilLayer", at = @At("STORE"), index = 3, ordinal = 1) - private static float modifyrenderEffect2(float f) { + @ModifyExpressionValue(method = "setupGlintTexturing", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getMillis()J")) + private static long modifyrenderEffect(long f) { Minecraft mc = Minecraft.getInstance(); if (mc.level != null) - return (mc.level.tickRateManager().millisecondsPerTick() % 4873L) / 4873.0F / 8F; + return ((Tickratechanger) mc.level.tickRateManager()).getAdjustedMilliseconds(); else return f; } } -//#endif \ No newline at end of file diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java new file mode 100644 index 0000000..22bd7db --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java @@ -0,0 +1,28 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.ModifyConstant; + +import com.minecrafttas.lotas_light.duck.Tickratechanger; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.components.SubtitleOverlay; +import net.minecraft.world.TickRateManager; + +/** + * Slows down the Subtitles + * @author Scribble + */ +@Mixin(SubtitleOverlay.class) +public class MixinTickrateChangerSubtitleOverlay { + + @ModifyConstant(method = "render", constant = @Constant(doubleValue = 3000D)) + public double applyTickrate2(double threethousand) { + Minecraft mc = Minecraft.getInstance(); + TickRateManager tickrateManager = mc.level.tickRateManager(); + Tickratechanger tickrateChanger = (Tickratechanger) mc.level.tickRateManager(); + float multiplier = tickrateManager.tickrate() == 0 ? 20F / tickrateChanger.getTickrateSaved() : 20F / tickrateManager.tickrate(); + return threethousand * multiplier; + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java new file mode 100644 index 0000000..690821c --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java @@ -0,0 +1,23 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import com.minecrafttas.lotas_light.duck.Tickratechanger; + +import net.minecraft.client.Minecraft; +import net.minecraft.client.renderer.LevelRenderer; + +@Mixin(LevelRenderer.class) +public class MixinTickrateChangerWorldborder { + + @ModifyExpressionValue(method = "renderWorldBorder", at = @At(value = "INVOKE", target = "Lnet/minecraft/Util;getMillis()J")) + public long modifyAnimationTimeWorldBorder(long millis) { + Minecraft mc = Minecraft.getInstance(); + if (mc.level != null) + return ((Tickratechanger) mc.level.tickRateManager()).getAdjustedMilliseconds(); + else + return millis; + } +} diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index 96929bd..6344ebf 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -2,7 +2,7 @@ "key.lotaslight.increaseTickrate":"Increase Tickrate", "key.lotaslight.decreaseTickrate":"Decrease Tickrate", "key.lotaslight.freezeTickrate":"Freeze Tickrate", - "key.lotaslight.advanceTickrate":"Advance Tickrate", + "key.lotaslight.advanceTickrate":"Advance Tick", "key.lotaslight.savestate":"Savestate", "key.lotaslight.loadstate":"Loadstate", diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 050bbad..5420229 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -11,7 +11,11 @@ "MixinMinecraft", "AccessorSoundEngine", "MixinTickrateChangerAudioPitch", - "MixinGui" + "MixinGui", + "MixinTickrateChangerEnchantmentGlimm", + "MixinTickrateChangerAchievements", + "MixinTickrateChangerWorldborder", + "MixinTickrateChangerSubtitleOverlay" ], "injectors": { "defaultRequire": 1 From 0e2bd283937e44c965704464ddfda42bd4e9bfce Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 21 Sep 2024 16:07:58 +0200 Subject: [PATCH 11/51] Clean up resources --- .../lotas_light/LoTASLightClient.java | 2 +- src/main/resources/assets/lotaslight/icon.png | Bin 453 -> 0 bytes .../lotaslight/textures/gui/iblk9xrq3z.png | Bin 33722 -> 0 bytes 3 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/main/resources/assets/lotaslight/icon.png delete mode 100644 src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java index ffd701e..75292b6 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLightClient.java @@ -160,7 +160,7 @@ private void advanceTickrate(Minecraft client) { private void drawHud(GuiGraphics context, DeltaTracker deltaTicks) { RenderSystem.enableBlend(); RenderSystem.setShaderColor(1, 1, 1, .5f); - context.blit(ResourceLocation.fromNamespaceAndPath("lotaslight", "textures/gui/iblk9xrq3z.png"), Minecraft.getInstance().getWindow().getGuiScaledWidth() / 2 - 10, Minecraft.getInstance().getWindow().getGuiScaledHeight() + context.blit(ResourceLocation.fromNamespaceAndPath("lotaslight", "potion.png"), Minecraft.getInstance().getWindow().getGuiScaledWidth() / 2 - 10, Minecraft.getInstance().getWindow().getGuiScaledHeight() - 50, 0, 0, 20, 20, 20, 20); RenderSystem.setShaderColor(1, 1, 1, 1); RenderSystem.disableBlend(); diff --git a/src/main/resources/assets/lotaslight/icon.png b/src/main/resources/assets/lotaslight/icon.png deleted file mode 100644 index 047b91f2347de5cf95f23284476fddbe21ba23fe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 453 zcmV;$0XqJPP)QAFYGys`80vegN0XDFh0OXKz&i8?Le#x7{1X)R+00000NkvXXu0mjf73i~T diff --git a/src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png b/src/main/resources/assets/lotaslight/textures/gui/iblk9xrq3z.png deleted file mode 100644 index f8ee743cdbc3542f66943dd13813c6b474486d07..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 33722 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 E0!?febpQYW From 42912c413e15a81ae44a8406e7a390ca0b1fbf89 Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 21 Sep 2024 16:14:34 +0200 Subject: [PATCH 12/51] Fix documentation --- .../minecrafttas/lotas_light/mixin/MixinTickCommand.java | 5 +++++ .../lotas_light/mixin/MixinTickRateManager.java | 6 ++++++ .../lotas_light/mixin/MixinTickrateChangerAchievements.java | 6 +++++- .../lotas_light/mixin/MixinTickrateChangerAudioPitch.java | 2 +- .../mixin/MixinTickrateChangerEnchantmentGlimm.java | 2 +- .../mixin/MixinTickrateChangerSubtitleOverlay.java | 2 +- .../lotas_light/mixin/MixinTickrateChangerWorldborder.java | 4 ++++ 7 files changed, 23 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java index b4629c9..deb6091 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickCommand.java @@ -7,6 +7,11 @@ import net.minecraft.server.commands.TickCommand; +/** + * Changes the lower bound of the tick command to 0 + * + * @author Scribble + */ @Mixin(TickCommand.class) public class MixinTickCommand { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java index 73d9123..3107fc1 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickRateManager.java @@ -19,6 +19,12 @@ import net.minecraft.util.TimeUtil; import net.minecraft.world.TickRateManager; +/** + * Changes the vanilla tickrate manager to allow for lower tickrates, + * better freeze and stepping funcitonality. + * + * @author Scribble + */ @Mixin(TickRateManager.class) public abstract class MixinTickRateManager implements Tickratechanger { @Unique diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java index 61eb8e7..3af8d87 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAchievements.java @@ -8,7 +8,11 @@ import net.minecraft.client.Minecraft; -// TODO Currently broken! Not registered in mixin.json +/** + * Slows down advancement toasts + * + * @author Scribble, Pancake + */ @Mixin(targets = "net/minecraft/client/gui/components/toasts/ToastComponent$ToastInstance") public class MixinTickrateChangerAchievements { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java index dde6c42..c3bfc8f 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerAudioPitch.java @@ -18,7 +18,7 @@ /** * Slows down Audio - * @author Scribble + * @author Scribble, Pancake */ @Mixin(SoundEngine.class) public abstract class MixinTickrateChangerAudioPitch implements SoundPitchDuck { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java index 32221aa..cfa3b32 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerEnchantmentGlimm.java @@ -11,7 +11,7 @@ /** * Slows down the Enchantment *foil* - * @author Scribble + * @author Scribble, Pancake */ @Mixin(RenderStateShard.class) public abstract class MixinTickrateChangerEnchantmentGlimm { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java index 22bd7db..5a5c197 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerSubtitleOverlay.java @@ -12,7 +12,7 @@ /** * Slows down the Subtitles - * @author Scribble + * @author Scribble, Pancake */ @Mixin(SubtitleOverlay.class) public class MixinTickrateChangerSubtitleOverlay { diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java index 690821c..205dd15 100644 --- a/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/MixinTickrateChangerWorldborder.java @@ -9,6 +9,10 @@ import net.minecraft.client.Minecraft; import net.minecraft.client.renderer.LevelRenderer; +/** + * Slows down the worldborder animation speed + * @author Scribble, Pancake + */ @Mixin(LevelRenderer.class) public class MixinTickrateChangerWorldborder { From af76aa4c8e80bca6aa38f195fd21068365769dc1 Mon Sep 17 00:00:00 2001 From: Scribble Date: Wed, 25 Sep 2024 14:50:51 +0200 Subject: [PATCH 13/51] New Crowdin updates (#4) --- README.md | 14 ++++++++++++++ .../assets/lotaslight/lang/de_de.json | 6 +++++- .../assets/lotaslight/lang/es_es.json | 6 +++++- .../assets/lotaslight/lang/fr_fr.json | 18 +++++++++++------- .../assets/lotaslight/lang/ja_jp.json | 6 +++++- .../assets/lotaslight/lang/ko_kr.json | 6 +++++- .../assets/lotaslight/lang/lol_us.json | 13 +++++++++++++ .../assets/lotaslight/lang/pl_pl.json | 18 +++++++++++------- .../assets/lotaslight/lang/zh_cn.json | 18 +++++++++++------- 9 files changed, 80 insertions(+), 25 deletions(-) create mode 100644 src/main/resources/assets/lotaslight/lang/lol_us.json diff --git a/README.md b/README.md index 1ff26d8..03f251c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,4 @@ +[![Crowdin](https://badges.crowdin.net/lotas-light/localized.svg)](https://crowdin.com/project/lotas-light) # LoTAS-Light A very slimmed down version of [LoTAS](https://github.com/MinecraftTAS/LoTAS), in an attempt to make it easily upgradable to newer versions. @@ -12,3 +13,16 @@ Needs Fabric API - Savestates - Saving (J) - Loading (K) + +## Translations +Help translate this mod with [Crowdin](https://crowdin.com/project/lotas-light) + +### Contributions +- French + - Elvee +- German + - ScribbleTAS +- LOLCAT + - ScribbleTAS +- Polish + - 4NTJ \ No newline at end of file diff --git a/src/main/resources/assets/lotaslight/lang/de_de.json b/src/main/resources/assets/lotaslight/lang/de_de.json index 3bd163c..55cb999 100644 --- a/src/main/resources/assets/lotaslight/lang/de_de.json +++ b/src/main/resources/assets/lotaslight/lang/de_de.json @@ -5,5 +5,9 @@ "key.lotaslight.advanceTickrate": "Advance einen Tick", "key.lotaslight.savestate": "Sicherungspunkt erstellen", "key.lotaslight.loadstate": "Sicherungspunkt laden", + "msg.lotaslight.setTickrate": "Setze die Tickrate auf %s", + "msg.lotaslight.turnOff": "Um diese Nachrichten auszuschalten, benutze /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Tickrate Nachrichten eingeschaltet", + "msg.lotaslight.showmsg.false": "Tickrate Nachrichten ausgeschaltet", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/es_es.json b/src/main/resources/assets/lotaslight/lang/es_es.json index ff1642f..df61dcf 100644 --- a/src/main/resources/assets/lotaslight/lang/es_es.json +++ b/src/main/resources/assets/lotaslight/lang/es_es.json @@ -5,5 +5,9 @@ "key.lotaslight.advanceTickrate": "Advance Tick", "key.lotaslight.savestate": "Savestate", "key.lotaslight.loadstate": "Loadstate", + "msg.lotaslight.setTickrate": "Set the tickrate to %s", + "msg.lotaslight.turnOff": "To turn off these messages, use /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Tickrate messages enabled", + "msg.lotaslight.showmsg.false": "Tickrate messages disabled", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/fr_fr.json b/src/main/resources/assets/lotaslight/lang/fr_fr.json index ff1642f..1d86ab2 100644 --- a/src/main/resources/assets/lotaslight/lang/fr_fr.json +++ b/src/main/resources/assets/lotaslight/lang/fr_fr.json @@ -1,9 +1,13 @@ { - "key.lotaslight.increaseTickrate": "Increase Tickrate", - "key.lotaslight.decreaseTickrate": "Decrease Tickrate", - "key.lotaslight.freezeTickrate": "Freeze Tickrate", - "key.lotaslight.advanceTickrate": "Advance Tick", - "key.lotaslight.savestate": "Savestate", - "key.lotaslight.loadstate": "Loadstate", + "key.lotaslight.increaseTickrate": "Augmenter le Tickrate", + "key.lotaslight.decreaseTickrate": "Diminuer le Tickrate", + "key.lotaslight.freezeTickrate": "Geler", + "key.lotaslight.advanceTickrate": "Passer au prochain Tick", + "key.lotaslight.savestate": "Sauvegarder", + "key.lotaslight.loadstate": "Charger", + "msg.lotaslight.setTickrate": "Fixer le Tickrate à %s", + "msg.lotaslight.turnOff": "Pour ne plus recevoir ces messages, faites /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Messages de Tickrate activés", + "msg.lotaslight.showmsg.false": "Messages de Tickrate désactivés", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/ja_jp.json b/src/main/resources/assets/lotaslight/lang/ja_jp.json index ff1642f..df61dcf 100644 --- a/src/main/resources/assets/lotaslight/lang/ja_jp.json +++ b/src/main/resources/assets/lotaslight/lang/ja_jp.json @@ -5,5 +5,9 @@ "key.lotaslight.advanceTickrate": "Advance Tick", "key.lotaslight.savestate": "Savestate", "key.lotaslight.loadstate": "Loadstate", + "msg.lotaslight.setTickrate": "Set the tickrate to %s", + "msg.lotaslight.turnOff": "To turn off these messages, use /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Tickrate messages enabled", + "msg.lotaslight.showmsg.false": "Tickrate messages disabled", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/ko_kr.json b/src/main/resources/assets/lotaslight/lang/ko_kr.json index ff1642f..df61dcf 100644 --- a/src/main/resources/assets/lotaslight/lang/ko_kr.json +++ b/src/main/resources/assets/lotaslight/lang/ko_kr.json @@ -5,5 +5,9 @@ "key.lotaslight.advanceTickrate": "Advance Tick", "key.lotaslight.savestate": "Savestate", "key.lotaslight.loadstate": "Loadstate", + "msg.lotaslight.setTickrate": "Set the tickrate to %s", + "msg.lotaslight.turnOff": "To turn off these messages, use /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Tickrate messages enabled", + "msg.lotaslight.showmsg.false": "Tickrate messages disabled", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/lol_us.json b/src/main/resources/assets/lotaslight/lang/lol_us.json new file mode 100644 index 0000000..a4cc97b --- /dev/null +++ b/src/main/resources/assets/lotaslight/lang/lol_us.json @@ -0,0 +1,13 @@ +{ + "key.lotaslight.increaseTickrate": "fstr tiktk", + "key.lotaslight.decreaseTickrate": "S L O W A T I K T O K K", + "key.lotaslight.freezeTickrate": "Stahp TikTo...", + "key.lotaslight.advanceTickrate": "1 Tik, no tokk", + "key.lotaslight.savestate": "Mayke Déjà-vu-point", + "key.lotaslight.loadstate": "Lode Déjà-vu-point", + "msg.lotaslight.setTickrate": "Put tiktokk 2 %s", + "msg.lotaslight.turnOff": "MSGS ANNOYING? Yeet with /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Msgs r b4ck", + "msg.lotaslight.showmsg.false": "Msgs banned 2 shadow realm", + "keycategory.lotaslight.lotaslight": "LoTAS-NotF4t" +} diff --git a/src/main/resources/assets/lotaslight/lang/pl_pl.json b/src/main/resources/assets/lotaslight/lang/pl_pl.json index ff1642f..fcb65ad 100644 --- a/src/main/resources/assets/lotaslight/lang/pl_pl.json +++ b/src/main/resources/assets/lotaslight/lang/pl_pl.json @@ -1,9 +1,13 @@ { - "key.lotaslight.increaseTickrate": "Increase Tickrate", - "key.lotaslight.decreaseTickrate": "Decrease Tickrate", - "key.lotaslight.freezeTickrate": "Freeze Tickrate", - "key.lotaslight.advanceTickrate": "Advance Tick", - "key.lotaslight.savestate": "Savestate", - "key.lotaslight.loadstate": "Loadstate", + "key.lotaslight.increaseTickrate": "Zwiększ tempo", + "key.lotaslight.decreaseTickrate": "Zmniejsz tempo", + "key.lotaslight.freezeTickrate": "Zamróź grę", + "key.lotaslight.advanceTickrate": "Przesuń tick", + "key.lotaslight.savestate": "Zapisz stan świata", + "key.lotaslight.loadstate": "Załaduj zapisany stan świata", + "msg.lotaslight.setTickrate": "Ustawiono tempo gry na %s", + "msg.lotaslight.turnOff": "Aby wyłączyć te wiadomości, użyj /lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "Włączono wiadomości o tempie gry", + "msg.lotaslight.showmsg.false": "Wyłączono wiadomości o tempie gry", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} diff --git a/src/main/resources/assets/lotaslight/lang/zh_cn.json b/src/main/resources/assets/lotaslight/lang/zh_cn.json index ff1642f..4b53918 100644 --- a/src/main/resources/assets/lotaslight/lang/zh_cn.json +++ b/src/main/resources/assets/lotaslight/lang/zh_cn.json @@ -1,9 +1,13 @@ { - "key.lotaslight.increaseTickrate": "Increase Tickrate", - "key.lotaslight.decreaseTickrate": "Decrease Tickrate", - "key.lotaslight.freezeTickrate": "Freeze Tickrate", - "key.lotaslight.advanceTickrate": "Advance Tick", - "key.lotaslight.savestate": "Savestate", - "key.lotaslight.loadstate": "Loadstate", + "key.lotaslight.increaseTickrate": "增加刻率", + "key.lotaslight.decreaseTickrate": "减小刻率", + "key.lotaslight.freezeTickrate": "高级暂停", + "key.lotaslight.advanceTickrate": "步进刻", + "key.lotaslight.savestate": "存档", + "key.lotaslight.loadstate": "读档", + "msg.lotaslight.setTickrate": "已将刻率设置为%s", + "msg.lotaslight.turnOff": "想要关闭这些消息,请使用/lotaslight showMessage false", + "msg.lotaslight.showmsg.true": "刻率消息已启用", + "msg.lotaslight.showmsg.false": "刻率消息已禁用", "keycategory.lotaslight.lotaslight": "LoTAS-Light" -} \ No newline at end of file +} From 55923272e418c6ab0e8c134a4d33b2bd11d4138e Mon Sep 17 00:00:00 2001 From: Scribble Date: Thu, 26 Sep 2024 21:53:54 +0200 Subject: [PATCH 14/51] Added savestate command --- .../minecrafttas/lotas_light/LoTASLight.java | 2 + .../lotas_light/command/SavestateCommand.java | 107 ++++++++++++++++++ .../savestates/SavestateIndexer.java | 5 + .../savestates/SavestateManager.java | 12 ++ .../assets/lotaslight/lang/en_us.json | 10 +- 5 files changed, 133 insertions(+), 3 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index 2d265ac..7b6fdab 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -4,6 +4,7 @@ import org.apache.logging.log4j.Logger; import com.minecrafttas.lotas_light.command.LoTASLightCommand; +import com.minecrafttas.lotas_light.command.SavestateCommand; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; @@ -17,6 +18,7 @@ public void onInitialize() { CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { LoTASLightCommand.register(dispatcher); + SavestateCommand.register(dispatcher); }); } diff --git a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java new file mode 100644 index 0000000..e256f2c --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java @@ -0,0 +1,107 @@ +package com.minecrafttas.lotas_light.command; + +import com.mojang.brigadier.CommandDispatcher; +import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.context.CommandContext; + +import net.minecraft.commands.CommandSourceStack; +import net.minecraft.commands.Commands; +import net.minecraft.network.chat.ClickEvent; +import net.minecraft.network.chat.Component; + +public class SavestateCommand { + public static void register(CommandDispatcher commandDispatcher) { + //@formatter:off + commandDispatcher + .register(Commands.literal("savestate") + .then(Commands.literal("save") + .executes(SavestateCommand::saveNew) + .then(Commands.argument("index", IntegerArgumentType.integer(1)) + .executes(SavestateCommand::saveIndex) + ) + ) + .then(Commands.literal("load") + .executes(SavestateCommand::loadRecent) + .then(Commands.argument("index", IntegerArgumentType.integer(0)) + .executes(SavestateCommand::loadIndex) + ) + ) + .then(Commands.literal("delete") + .then(Commands.argument("index", IntegerArgumentType.integer(1)) + .executes(SavestateCommand::delete) + .then(Commands.argument("indexTo", IntegerArgumentType.integer(1)) + .executes(SavestateCommand::deleteMore) + .then(Commands.literal("force").executes(SavestateCommand::deleteDis)) + ) + ) + ) + .then(Commands.literal("reload") + .executes(SavestateCommand::reload) + ) + ); + //@formatter:on + } + + private static int saveNew(CommandContext context) { + int index = -1; + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save"), true); + return 0; + } + + private static int saveIndex(CommandContext context) { + int index = context.getArgument("index", Integer.class); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save"), true); + return index; + } + + private static int loadRecent(CommandContext context) { + int index = -1; + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load"), true); + return 0; + } + + private static int loadIndex(CommandContext context) { + int index = context.getArgument("index", Integer.class); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load"), true); + return index; + } + + private static int delete(CommandContext context) { + int index = context.getArgument("index", Integer.class); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.delete"), true); + return index; + } + + private static int deleteMore(CommandContext context) { + int index = context.getArgument("index", Integer.class); + int indexTo = context.getArgument("indexTo", Integer.class); + int count = (index + 1) - indexTo; + + String translationKey = "msg.lotaslight.savestate.deleteMore"; + + String key2 = translationKey += count == 1 ? ".singular" : ".plural"; + //@formatter:off + context.getSource().sendSuccess( + () -> Component.translatable(key2, count, "e") + .withStyle( + style -> style.withClickEvent( + new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/savestate delete %s %s force", index, indexTo)) + ) + ), + true); + //@formatter:on + return index; + } + + private static int deleteDis(CommandContext context) { + int index = context.getArgument("index", Integer.class); + int indexTo = context.getArgument("indexTo", Integer.class); + context.getSource().sendSuccess(() -> Component.literal("Yeet"), true); + return index; + } + + private static int reload(CommandContext context) { + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.reload"), true); + return 0; + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java new file mode 100644 index 0000000..c7f75bb --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java @@ -0,0 +1,5 @@ +package com.minecrafttas.lotas_light.savestates; + +class SavestateIndexer { + +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java new file mode 100644 index 0000000..d39a1bf --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java @@ -0,0 +1,12 @@ +package com.minecrafttas.lotas_light.savestates; + +public class SavestateManager { + + public void saveState(int index) { + + } + + public void loadState(int index) { + + } +} diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index 6344ebf..2cf7d8e 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -1,15 +1,19 @@ { + "keycategory.lotaslight.lotaslight":"LoTAS-Light", "key.lotaslight.increaseTickrate":"Increase Tickrate", "key.lotaslight.decreaseTickrate":"Decrease Tickrate", "key.lotaslight.freezeTickrate":"Freeze Tickrate", "key.lotaslight.advanceTickrate":"Advance Tick", "key.lotaslight.savestate":"Savestate", "key.lotaslight.loadstate":"Loadstate", - "msg.lotaslight.setTickrate":"Set the tickrate to %s", "msg.lotaslight.turnOff":"To turn off these messages, use /lotaslight showMessage false", "msg.lotaslight.showmsg.true":"Tickrate messages enabled", "msg.lotaslight.showmsg.false":"Tickrate messages disabled", - - "keycategory.lotaslight.lotaslight":"LoTAS-Light" + "msg.lotaslight.savestate.save":"Creating a new savestate at index %s", + "msg.lotaslight.savestate.load":"Load savestate from index %s", + "msg.lotaslight.savestate.delete":"Trying to delete savestate at index %s", + "msg.lotaslight.savestate.deleteMore.singular":"You are about to delete %d savestate! Do you want to continue? %s[YES]", + "msg.lotaslight.savestate.deleteMore.plural":"You are about to delete %d savestates! Do you want to continue? %s[YES]", + "msg.lotaslight.savestate.reload":"Reloading savestate index. Necessary when you edit the savestates outside of Minecraft." } From 8a33df8a320afb99370779112d3f5506c0d99d6b Mon Sep 17 00:00:00 2001 From: Scribble Date: Fri, 27 Sep 2024 13:13:37 +0200 Subject: [PATCH 15/51] Fixed click event in savestate command --- .../lotas_light/command/SavestateCommand.java | 35 ++++++++++++------- .../savestates/SavestateDataFile.java | 35 +++++++++++++++++++ .../savestates/SavestateHandler.java | 14 ++++++++ .../savestates/SavestateManager.java | 12 ------- .../assets/lotaslight/lang/en_us.json | 6 ++-- 5 files changed, 75 insertions(+), 27 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java delete mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java diff --git a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java index e256f2c..17520b4 100644 --- a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java @@ -4,10 +4,13 @@ import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.context.CommandContext; +import net.minecraft.ChatFormatting; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.network.chat.ClickEvent; import net.minecraft.network.chat.Component; +import net.minecraft.network.chat.ComponentUtils; +import net.minecraft.network.chat.HoverEvent; public class SavestateCommand { public static void register(CommandDispatcher commandDispatcher) { @@ -44,31 +47,31 @@ public static void register(CommandDispatcher commandDispatc private static int saveNew(CommandContext context) { int index = -1; - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save"), true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", index).withStyle(ChatFormatting.GREEN), true); return 0; } private static int saveIndex(CommandContext context) { int index = context.getArgument("index", Integer.class); - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save"), true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", index).withStyle(ChatFormatting.GREEN), true); return index; } private static int loadRecent(CommandContext context) { int index = -1; - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load"), true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load", index).withStyle(ChatFormatting.GREEN), true); return 0; } private static int loadIndex(CommandContext context) { int index = context.getArgument("index", Integer.class); - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load"), true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load", index).withStyle(ChatFormatting.GREEN), true); return index; } private static int delete(CommandContext context) { int index = context.getArgument("index", Integer.class); - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.delete"), true); + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.delete", index).withStyle(ChatFormatting.GREEN), true); return index; } @@ -77,17 +80,23 @@ private static int deleteMore(CommandContext context) { int indexTo = context.getArgument("indexTo", Integer.class); int count = (index + 1) - indexTo; - String translationKey = "msg.lotaslight.savestate.deleteMore"; + String translationKey = "msg.lotaslight.savestate.deleteMore" + (count == 1 ? ".singular" : ".plural"); - String key2 = translationKey += count == 1 ? ".singular" : ".plural"; //@formatter:off - context.getSource().sendSuccess( - () -> Component.translatable(key2, count, "e") + Component countComponent = Component.literal(Integer.toString(count)).withStyle(ChatFormatting.RED); + + Component confirmationComponent = ComponentUtils.wrapInSquareBrackets(Component.translatable("msg.lotaslight.savestate.deleteMore.confirm", true) .withStyle( - style -> style.withClickEvent( - new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/savestate delete %s %s force", index, indexTo)) - ) - ), + style -> style + .withClickEvent( + new ClickEvent(ClickEvent.Action.RUN_COMMAND, String.format("/savestate delete %s %s force", index, indexTo)) + ) + .withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, Component.translatable("msg.lotaslight.savestate.deleteMore.hover").withStyle(ChatFormatting.DARK_RED))) + )).withStyle(ChatFormatting.GREEN); + + + context.getSource().sendSuccess( + () -> Component.translatable(translationKey, countComponent, confirmationComponent).withStyle(ChatFormatting.YELLOW), true); //@formatter:on return index; diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java new file mode 100644 index 0000000..1897ade --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java @@ -0,0 +1,35 @@ +package com.minecrafttas.lotas_light.savestates; + +import java.nio.file.Path; + +import com.minecrafttas.lotas_light.config.AbstractDataFile; + +public class SavestateDataFile extends AbstractDataFile { + + public SavestateDataFile(Path file) { + super(file, "savestatedata", "Data for this savestate from LoTAS"); + } + + public enum DataValues { + INDEX("currentIndex"), + NAME("savestateName"); + + private String configname; + + private DataValues(String configname) { + this.configname = configname; + } + + public String getConfigName() { + return configname; + } + } + + public void set(DataValues key, String val) { + properties.setProperty(key.getConfigName(), val); + } + + public String get(DataValues key) { + return properties.getProperty(key.getConfigName()); + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java new file mode 100644 index 0000000..c20f467 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java @@ -0,0 +1,14 @@ +package com.minecrafttas.lotas_light.savestates; + +import java.nio.file.Path; + +public class SavestateHandler { + + public void saveState(Path sourcePath, Path targetPath) { + + } + + public void loadState(Path sourcePath, Path targetPath) { + + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java deleted file mode 100644 index d39a1bf..0000000 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateManager.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.minecrafttas.lotas_light.savestates; - -public class SavestateManager { - - public void saveState(int index) { - - } - - public void loadState(int index) { - - } -} diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index 2cf7d8e..8b38a7d 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -13,7 +13,9 @@ "msg.lotaslight.savestate.save":"Creating a new savestate at index %s", "msg.lotaslight.savestate.load":"Load savestate from index %s", "msg.lotaslight.savestate.delete":"Trying to delete savestate at index %s", - "msg.lotaslight.savestate.deleteMore.singular":"You are about to delete %d savestate! Do you want to continue? %s[YES]", - "msg.lotaslight.savestate.deleteMore.plural":"You are about to delete %d savestates! Do you want to continue? %s[YES]", + "msg.lotaslight.savestate.deleteMore.singular":"You are about to delete %s savestate! Do you want to continue? %s", + "msg.lotaslight.savestate.deleteMore.plural":"You are about to delete %s savestates! Do you want to continue? %s", + "msg.lotaslight.savestate.deleteMore.confirm":"YES", + "msg.lotaslight.savestate.deleteMore.hover":"This can not be undone or stopped!", "msg.lotaslight.savestate.reload":"Reloading savestate index. Necessary when you edit the savestates outside of Minecraft." } From 90be9f06b309c777bd37ed12ac4664e2da782d56 Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 28 Sep 2024 18:44:11 +0200 Subject: [PATCH 16/51] Started on savestate indexer --- .../savestates/SavestateDataFile.java | 6 +- .../savestates/SavestateIndexer.java | 70 ++++++++++++++++++- 2 files changed, 74 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java index 1897ade..fa62bf8 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java @@ -7,7 +7,7 @@ public class SavestateDataFile extends AbstractDataFile { public SavestateDataFile(Path file) { - super(file, "savestatedata", "Data for this savestate from LoTAS"); + super(file, "savestatedata", "Data for this savestate from LoTAS-Light"); } public enum DataValues { @@ -32,4 +32,8 @@ public void set(DataValues key, String val) { public String get(DataValues key) { return properties.getProperty(key.getConfigName()); } + + public Path getPath() { + return this.file; + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java index c7f75bb..b847212 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java @@ -1,5 +1,73 @@ package com.minecrafttas.lotas_light.savestates; -class SavestateIndexer { +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import java.util.stream.Stream; +import org.apache.logging.log4j.Logger; + +/** + * Manages the savestates on the filesystem and assignes new indizes + * + * @author Scribble + */ +public class SavestateIndexer { + + private final Logger logger; + private final Path savestateBaseDirectory; + private final String worldname; + private final Map savestateIndex; + + private static final Path savestateDatPath = Path.of("tas/savestate.dat"); + + public SavestateIndexer(Logger logger, Path savestateBaseDirectory, String worldname) { + this.logger = logger; + this.savestateBaseDirectory = savestateBaseDirectory; + this.worldname = worldname; + savestateIndex = new HashMap<>(); + } + + private void createSavestateDir() { + Path savestateDir = savestateBaseDirectory.resolve(worldname); + try { + Files.createDirectories(savestateDir); + } catch (IOException e) { + logger.catching(e); + } + } + + public void refresh() { + logger.trace("Refreshing savestate indexes"); + Path savestateDir = savestateBaseDirectory.resolve(worldname); + Thread t = new Thread(new Runnable() { + + @Override + public void run() { + Stream stream = null; + try { + stream = Files.list(savestateDir); // Get a list of paths in the specified directory + } catch (IOException e) { + logger.catching(e); + return; + } + + //@formatter:off + Set pathSet = stream + .filter(file -> Files.isDirectory(file)) + .filter(file -> file.getFileName().startsWith(String.format("%s-Savestate", worldname))) + .collect(Collectors.toSet()); + //@formatter:on + + pathSet.forEach(path -> { + + }); + } + }); + t.run(); + } } From acc707fac8b46dfe057bdfcd4b0ef237902cb3e6 Mon Sep 17 00:00:00 2001 From: Scribble Date: Fri, 4 Oct 2024 18:20:49 +0200 Subject: [PATCH 17/51] Continued on savestates --- .../minecrafttas/lotas_light/LoTASLight.java | 19 +++- .../lotas_light/config/AbstractDataFile.java | 21 ++-- .../lotas_light/config/Configuration.java | 7 +- .../mixin/AccessorLevelStorage.java | 12 ++ .../savestates/SavestateDataFile.java | 39 ------- .../savestates/SavestateHandler.java | 34 +++++- .../savestates/SavestateIndexer.java | 106 ++++++++++++++++-- .../exceptions/LoadstateException.java | 11 ++ .../exceptions/SavestateDeleteException.java | 7 ++ .../exceptions/SavestateException.java | 9 ++ src/main/resources/lotaslight.mixins.json | 3 +- 11 files changed, 210 insertions(+), 58 deletions(-) create mode 100644 src/main/java/com/minecrafttas/lotas_light/mixin/AccessorLevelStorage.java delete mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/LoadstateException.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateDeleteException.java create mode 100644 src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateException.java diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index 7b6fdab..2439d73 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -1,18 +1,25 @@ package com.minecrafttas.lotas_light; +import java.nio.file.Path; + import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import com.minecrafttas.lotas_light.command.LoTASLightCommand; import com.minecrafttas.lotas_light.command.SavestateCommand; +import com.minecrafttas.lotas_light.mixin.AccessorLevelStorage; +import com.minecrafttas.lotas_light.savestates.SavestateHandler; import net.fabricmc.api.ModInitializer; import net.fabricmc.fabric.api.command.v2.CommandRegistrationCallback; +import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents; public class LoTASLight implements ModInitializer { public static Logger LOGGER = LogManager.getLogger("LoTAS-Light"); + public static SavestateHandler savestateHandler = null; + @Override public void onInitialize() { @@ -20,6 +27,16 @@ public void onInitialize() { LoTASLightCommand.register(dispatcher); SavestateCommand.register(dispatcher); }); - } + ServerLifecycleEvents.SERVER_STARTED.register(server -> { + Path savesDir = server.isSingleplayer() ? server.getServerDirectory().resolve("saves") : server.getServerDirectory(); + Path savestateBaseDir = savesDir.resolve("savestates"); + String worldname = ((AccessorLevelStorage) server).getStorageSource().getLevelId(); + savestateHandler = new SavestateHandler(LOGGER, savesDir, savestateBaseDir, worldname); + }); + + ServerLifecycleEvents.SERVER_STOPPED.register(server -> { + savestateHandler = null; + }); + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java b/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java index e19366b..9c902f6 100644 --- a/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java +++ b/src/main/java/com/minecrafttas/lotas_light/config/AbstractDataFile.java @@ -9,10 +9,16 @@ import java.util.InvalidPropertiesFormatException; import java.util.Properties; +import org.apache.logging.log4j.Logger; + import com.minecrafttas.lotas_light.LoTASLight; public abstract class AbstractDataFile { + /** + * The logger + */ + protected final Logger logger; /** * The save location of this data file */ @@ -38,7 +44,8 @@ public abstract class AbstractDataFile { * @param name The {@link #name} of the data file, used in logging * @param comment The {@link #comment} in the data file */ - protected AbstractDataFile(Path file, String name, String comment) { + protected AbstractDataFile(Logger logger, Path file, String name, String comment) { + this.logger = logger; this.file = file; this.name = name; this.comment = comment; @@ -73,13 +80,13 @@ public void load(Path file) { newProp.load(fis); fis.close(); } catch (InvalidPropertiesFormatException e) { - LoTASLight.LOGGER.error("The {} file could not be read", name, e); + logger.error("The {} file could not be read", name, e); return; } catch (FileNotFoundException e) { - LoTASLight.LOGGER.warn("No {} file found: {}", name, file); + logger.warn("No {} file found: {}", name, file); return; } catch (IOException e) { - LoTASLight.LOGGER.error("An error occured while reading the {} file", file, e); + logger.error("An error occured while reading the {} file", file, e); return; } this.properties = newProp; @@ -105,13 +112,13 @@ public void loadFromXML(Path file) { newProp.loadFromXML(fis); fis.close(); } catch (InvalidPropertiesFormatException e) { - LoTASLight.LOGGER.error("The {} file could not be read", name, e); + logger.error("The {} file could not be read", name, e); return; } catch (FileNotFoundException e) { - LoTASLight.LOGGER.warn("No {} file found: {}", name, file); + logger.warn("No {} file found: {}", name, file); return; } catch (IOException e) { - LoTASLight.LOGGER.error("An error occured while reading the {} file", file, e); + logger.error("An error occured while reading the {} file", file, e); return; } this.properties = newProp; diff --git a/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java index b5003d0..5e63482 100644 --- a/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java +++ b/src/main/java/com/minecrafttas/lotas_light/config/Configuration.java @@ -4,6 +4,8 @@ import java.nio.file.Path; import java.util.Properties; +import com.minecrafttas.lotas_light.LoTASLight; + /** * A very simple configuration class * @@ -15,7 +17,8 @@ public class Configuration extends AbstractDataFile { public enum ConfigOptions { DEFAULT_TICKRATE("defaultTickrate", "20.0"), - SHOW_MESSAGES("showMessages", "true"); + SHOW_MESSAGES("showMessages", "true"), + SAVESTATE_DATEFORMAT("savestateDateformat", "DD-MM-YYYY hh:mm:ss"); private String key; private String defaultValue; @@ -35,7 +38,7 @@ public String getDefaultValue() { } public Configuration(String comment, Path configFile) { - super(configFile, "config", comment); + super(LoTASLight.LOGGER, configFile, "config", comment); } @Override diff --git a/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorLevelStorage.java b/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorLevelStorage.java new file mode 100644 index 0000000..8f06e76 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/mixin/AccessorLevelStorage.java @@ -0,0 +1,12 @@ +package com.minecrafttas.lotas_light.mixin; + +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; + +import net.minecraft.server.MinecraftServer; + +@Mixin(MinecraftServer.class) +public interface AccessorLevelStorage { + @Accessor + public net.minecraft.world.level.storage.LevelStorageSource.LevelStorageAccess getStorageSource(); +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java deleted file mode 100644 index fa62bf8..0000000 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateDataFile.java +++ /dev/null @@ -1,39 +0,0 @@ -package com.minecrafttas.lotas_light.savestates; - -import java.nio.file.Path; - -import com.minecrafttas.lotas_light.config.AbstractDataFile; - -public class SavestateDataFile extends AbstractDataFile { - - public SavestateDataFile(Path file) { - super(file, "savestatedata", "Data for this savestate from LoTAS-Light"); - } - - public enum DataValues { - INDEX("currentIndex"), - NAME("savestateName"); - - private String configname; - - private DataValues(String configname) { - this.configname = configname; - } - - public String getConfigName() { - return configname; - } - } - - public void set(DataValues key, String val) { - properties.setProperty(key.getConfigName(), val); - } - - public String get(DataValues key) { - return properties.getProperty(key.getConfigName()); - } - - public Path getPath() { - return this.file; - } -} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java index c20f467..248a476 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java @@ -2,13 +2,45 @@ import java.nio.file.Path; +import org.apache.logging.log4j.Logger; + public class SavestateHandler { - public void saveState(Path sourcePath, Path targetPath) { + private final Logger logger; + private final SavestateIndexer indexer; + + /** + * Creates a new SavestateHandler + * + * @param logger The logger to use + * @param savesDir The directory where Minecrafts world files are stored. On client it's .minecraft/saves on the server it's the server directory + * @param savestateBaseDir The base directory of the savestates. Is in savesDir/savestates + * @param worldname The name of the world that is going to be savestated + */ + public SavestateHandler(Logger logger, Path savesDir, Path savestateBaseDir, String worldname) { + logger.debug("Created savestate handler with saves: {}, savestates: {}, worldname: {}", savesDir, savestateBaseDir, worldname); + this.logger = logger; + this.indexer = new SavestateIndexer(logger, savesDir, savestateBaseDir, worldname); + } + + public void saveState(int index, SavestateCallback cb) { + saveState(index, true, cb); + } + + public void saveState(int index, boolean pauseTickrate, SavestateCallback cb) { + saveState(index, pauseTickrate, true, cb); + } + + public void saveState(int index, boolean pauseTickrate, boolean changeIndex, SavestateCallback cb) { } public void loadState(Path sourcePath, Path targetPath) { } + + @FunctionalInterface + public interface SavestateCallback { + public void invoke(int index, Path targetPath, Path sourcePath); + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java index b847212..21b7040 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java @@ -3,14 +3,19 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.HashMap; -import java.util.Map; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.ArrayList; +import java.util.Date; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; import org.apache.logging.log4j.Logger; +import com.minecrafttas.lotas_light.LoTASLight; +import com.minecrafttas.lotas_light.config.AbstractDataFile; + /** * Manages the savestates on the filesystem and assignes new indizes * @@ -20,27 +25,37 @@ public class SavestateIndexer { private final Logger logger; private final Path savestateBaseDirectory; + private Path savesDir; private final String worldname; - private final Map savestateIndex; + private final Path currentSavestateDir; + private final ArrayList savestateList; + private Savestate currentSavestate; private static final Path savestateDatPath = Path.of("tas/savestate.dat"); - public SavestateIndexer(Logger logger, Path savestateBaseDirectory, String worldname) { + public SavestateIndexer(Logger logger, Path savesDir, Path savestateBaseDirectory, String worldname) { this.logger = logger; this.savestateBaseDirectory = savestateBaseDirectory; + this.savesDir = savesDir; this.worldname = worldname; - savestateIndex = new HashMap<>(); + this.currentSavestateDir = savestateBaseDirectory.resolve(String.format("%s-Savestates", worldname)); + savestateList = new ArrayList<>(); + createSavestateDir(); + currentSavestate = new Savestate(savesDir.resolve(worldname).resolve(savestateDatPath)); } private void createSavestateDir() { - Path savestateDir = savestateBaseDirectory.resolve(worldname); try { - Files.createDirectories(savestateDir); + Files.createDirectories(currentSavestateDir); } catch (IOException e) { logger.catching(e); } } + public Savestate createMostRecentSavestate() { + return null; + } + public void refresh() { logger.trace("Refreshing savestate indexes"); Path savestateDir = savestateBaseDirectory.resolve(worldname); @@ -63,6 +78,8 @@ public void run() { .collect(Collectors.toSet()); //@formatter:on + stream.close(); + pathSet.forEach(path -> { }); @@ -70,4 +87,79 @@ public void run() { }); t.run(); } + + public class Savestate extends AbstractDataFile { + + protected Savestate(Path file) { + super(LoTASLight.LOGGER, file, "Savestate", "Stores savestate related data"); + } + + private enum Options { + INDEX, + NAME, + DATE; + + @Override + public String toString() { + return super.toString().toLowerCase(); + } + } + + private int index; + private Path sourceDir; + private Path targetDir; + private String name; + private Date date; + + public int getIndex() { + return index; + } + + public Path getSourceDir() { + return sourceDir; + } + + public Path getTargetDir() { + return targetDir; + } + + public String getName() { + return name; + } + + public Date getDate() { + return date; + } + + @Override + public void saveToXML() { + properties.setProperty(Options.INDEX.toString(), Integer.toString(index)); + properties.setProperty(Options.NAME.toString(), name); + properties.setProperty(Options.DATE.toString(), Long.toString(ChronoUnit.SECONDS.between(Instant.EPOCH, date.toInstant()))); + super.saveToXML(); + } + + @Override + public void loadFromXML() { + super.loadFromXML(); + try { + this.index = Integer.parseInt(properties.getProperty(Options.INDEX.toString())); + } catch (Exception e) { + logger.error("Can't parse '{}' in {}", Options.INDEX.toString(), currentSavestateDir.resolve(savestateDatPath)); + logger.catching(e); + } + this.name = properties.getProperty(Options.NAME.toString()); + try { + this.date = parseDate(properties.getProperty(Options.DATE.toString())); + } catch (Exception e) { + logger.error("Can't parse '{}' in {}", Options.DATE.toString(), currentSavestateDir.resolve(savestateDatPath)); + logger.catching(e); + } + } + + private Date parseDate(String dateString) throws Exception { + long unixTimestamp = Long.parseLong(dateString); + return Date.from(Instant.ofEpochSecond(unixTimestamp)); + } + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/LoadstateException.java b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/LoadstateException.java new file mode 100644 index 0000000..c9554b4 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/LoadstateException.java @@ -0,0 +1,11 @@ +package com.minecrafttas.lotas_light.savestates.exceptions; + +public class LoadstateException extends Exception { + public LoadstateException(String s) { + super(s); + } + + public LoadstateException(Throwable t) { + super(t); + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateDeleteException.java b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateDeleteException.java new file mode 100644 index 0000000..c7d169c --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateDeleteException.java @@ -0,0 +1,7 @@ +package com.minecrafttas.lotas_light.savestates.exceptions; + +public class SavestateDeleteException extends Exception { + public SavestateDeleteException(String s) { + super(s); + } +} diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateException.java b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateException.java new file mode 100644 index 0000000..00f79c6 --- /dev/null +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/exceptions/SavestateException.java @@ -0,0 +1,9 @@ +package com.minecrafttas.lotas_light.savestates.exceptions; + +public class SavestateException extends Exception { + + public SavestateException(String s) { + super(s); + } + +} diff --git a/src/main/resources/lotaslight.mixins.json b/src/main/resources/lotaslight.mixins.json index 5420229..20b8f96 100644 --- a/src/main/resources/lotaslight.mixins.json +++ b/src/main/resources/lotaslight.mixins.json @@ -5,7 +5,8 @@ "mixins": [ "MixinTickRateManager", "MixinTickCommand", - "MixinMinecraftServer" + "MixinMinecraftServer", + "AccessorLevelStorage" ], "client":[ "MixinMinecraft", From 2890932c483ff7b2a05c052f0a9e9c8426d916df Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 5 Oct 2024 22:31:26 +0200 Subject: [PATCH 18/51] Add functionality to SavestateIndexer --- build.gradle | 2 +- .../lotas_light/command/SavestateCommand.java | 9 +- .../savestates/SavestateHandler.java | 20 ++- .../savestates/SavestateIndexer.java | 134 +++++++++++++++--- src/main/resources/log4j.xml | 27 ++++ 5 files changed, 166 insertions(+), 26 deletions(-) create mode 100644 src/main/resources/log4j.xml diff --git a/build.gradle b/build.gradle index 4fb3138..1297742 100644 --- a/build.gradle +++ b/build.gradle @@ -19,7 +19,7 @@ repositories { } loom { - + log4jConfigs.from(file('src/main/resources/log4j.xml')) } dependencies { diff --git a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java index 17520b4..3c05f61 100644 --- a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java @@ -1,5 +1,6 @@ package com.minecrafttas.lotas_light.command; +import com.minecrafttas.lotas_light.LoTASLight; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.context.CommandContext; @@ -47,13 +48,17 @@ public static void register(CommandDispatcher commandDispatc private static int saveNew(CommandContext context) { int index = -1; - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", index).withStyle(ChatFormatting.GREEN), true); + LoTASLight.savestateHandler.saveState(index, (newindex, targetPath, sourcePath) -> { + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", newindex).withStyle(ChatFormatting.GREEN), true); + }); return 0; } private static int saveIndex(CommandContext context) { int index = context.getArgument("index", Integer.class); - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", index).withStyle(ChatFormatting.GREEN), true); + LoTASLight.savestateHandler.saveState(index, (newindex, targetPath, sourcePath) -> { + context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", newindex).withStyle(ChatFormatting.GREEN), true); + }); return index; } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java index 248a476..2026add 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java @@ -4,6 +4,8 @@ import org.apache.logging.log4j.Logger; +import com.minecrafttas.lotas_light.savestates.SavestateIndexer.SavestatePaths; + public class SavestateHandler { private final Logger logger; @@ -23,6 +25,10 @@ public SavestateHandler(Logger logger, Path savesDir, Path savestateBaseDir, Str this.indexer = new SavestateIndexer(logger, savesDir, savestateBaseDir, worldname); } + public void saveState(int index) { + saveState(index, null); + } + public void saveState(int index, SavestateCallback cb) { saveState(index, true, cb); } @@ -32,10 +38,22 @@ public void saveState(int index, boolean pauseTickrate, SavestateCallback cb) { } public void saveState(int index, boolean pauseTickrate, boolean changeIndex, SavestateCallback cb) { + SavestatePaths paths = indexer.createSavestate(index); + logger.debug("Source: {}, Target: {}", paths.getSourceFolder(), paths.getTargetFolder()); + if (cb != null) { + cb.invoke(paths.getIndex(), paths.getSourceFolder(), paths.getTargetFolder()); + } + } + + public void loadState(int index, SavestateCallback cb) { + loadState(index, true, cb); + } + public void loadState(int index, boolean pauseTickrate, SavestateCallback cb) { + loadState(index, pauseTickrate, true, cb); } - public void loadState(Path sourcePath, Path targetPath) { + private void loadState(int index, boolean pauseTickrate, boolean changeIndex, SavestateCallback cb) { } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java index 21b7040..db52ebc 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java @@ -5,8 +5,11 @@ import java.nio.file.Path; import java.time.Instant; import java.time.temporal.ChronoUnit; -import java.util.ArrayList; import java.util.Date; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Properties; import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -28,10 +31,10 @@ public class SavestateIndexer { private Path savesDir; private final String worldname; private final Path currentSavestateDir; - private final ArrayList savestateList; + private final LinkedHashMap savestateList; private Savestate currentSavestate; - private static final Path savestateDatPath = Path.of("tas/savestate.dat"); + private static final Path savestateDatPath = Path.of("tas/savestate.xml"); public SavestateIndexer(Logger logger, Path savesDir, Path savestateBaseDirectory, String worldname) { this.logger = logger; @@ -39,7 +42,7 @@ public SavestateIndexer(Logger logger, Path savesDir, Path savestateBaseDirector this.savesDir = savesDir; this.worldname = worldname; this.currentSavestateDir = savestateBaseDirectory.resolve(String.format("%s-Savestates", worldname)); - savestateList = new ArrayList<>(); + savestateList = new LinkedHashMap<>(); createSavestateDir(); currentSavestate = new Savestate(savesDir.resolve(worldname).resolve(savestateDatPath)); } @@ -52,8 +55,43 @@ private void createSavestateDir() { } } - public Savestate createMostRecentSavestate() { - return null; + public SavestatePaths createSavestate(int index) { + return createSavestate(index, null); + } + + public SavestatePaths createSavestate(int index, String name) { + if (index < 0) { + index = currentSavestate.getIndex() + 1; + } + + if (name == null) { + name = "Savestate #" + index; + } + + currentSavestate.index = index; + currentSavestate.name = name; + currentSavestate.date = new Date(); + + currentSavestate.saveToXML(); + + savestateList.put(index, currentSavestate.clone()); + sortSavestateList(); + + Path sourceDir = savesDir.resolve(worldname); + Path targetDir = currentSavestateDir.resolve(worldname + index); + + return SavestatePaths.of(index, sourceDir, targetDir); + } + + private void sortSavestateList() { + LinkedHashMap copy = new LinkedHashMap<>(); + //@formatter:off + savestateList.entrySet().stream() + .sorted(Map.Entry.comparingByKey()) + .forEach(entry -> copy.put(entry.getKey(), entry.getValue())); + //@formatter:on + savestateList.clear(); + savestateList.putAll(copy); } public void refresh() { @@ -88,10 +126,43 @@ public void run() { t.run(); } + public Set getIndexList() { + return savestateList.keySet(); + } + + public LinkedHashMap getSavestateList(int amount) { + LinkedHashMap out = new LinkedHashMap<>(); + if (amount <= 0) { + savestateList.forEach((key, value) -> out.put(key, value.getName())); + return out; + } + + LinkedHashMap copy = new LinkedHashMap<>(savestateList); + for (int i = 0; i < amount; i++) { + Entry entry = copy.pollLastEntry(); + if (entry == null) + break; + out.put(entry.getKey(), entry.getValue().getName()); + } + return new LinkedHashMap<>(out.reversed()); + } + public class Savestate extends AbstractDataFile { - protected Savestate(Path file) { + private int index; + private String name; + private Date date; + + private Savestate(Path file) { + super(LoTASLight.LOGGER, file, "Savestate", "Stores savestate related data"); + } + + private Savestate(Path file, Properties properties, int index, String name, Date date) { super(LoTASLight.LOGGER, file, "Savestate", "Stores savestate related data"); + this.index = index; + this.name = name; + this.date = date; + this.properties = properties; } private enum Options { @@ -105,24 +176,10 @@ public String toString() { } } - private int index; - private Path sourceDir; - private Path targetDir; - private String name; - private Date date; - public int getIndex() { return index; } - public Path getSourceDir() { - return sourceDir; - } - - public Path getTargetDir() { - return targetDir; - } - public String getName() { return name; } @@ -157,9 +214,42 @@ public void loadFromXML() { } } - private Date parseDate(String dateString) throws Exception { + @Override + protected Savestate clone() { + return new Savestate(file, properties, index, name, date); + } + + private static Date parseDate(String dateString) throws Exception { long unixTimestamp = Long.parseLong(dateString); return Date.from(Instant.ofEpochSecond(unixTimestamp)); } } + + public static class SavestatePaths { + private final int index; + private final Path sourceFolder; + private final Path targetFolder; + + private SavestatePaths(int index, Path sourceFolder, Path targetFolder) { + this.index = index; + this.sourceFolder = sourceFolder; + this.targetFolder = targetFolder; + } + + public int getIndex() { + return index; + } + + public Path getSourceFolder() { + return sourceFolder; + } + + public Path getTargetFolder() { + return targetFolder; + } + + public static SavestatePaths of(int index, Path sourceFolder, Path targetFolder) { + return new SavestatePaths(index, sourceFolder, targetFolder); + } + } } diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml new file mode 100644 index 0000000..7f0d998 --- /dev/null +++ b/src/main/resources/log4j.xml @@ -0,0 +1,27 @@ + + + + + + %style{[%d{HH:mm:ss}]}{blue} %highlight{[%t/%level]}{FATAL=red, ERROR=red, WARN=yellow, INFO=green, DEBUG=green, TRACE=blue} %style{(%logger{1}%notEmpty{/%marker})}{cyan} %highlight{%msg%n}{FATAL=red, ERROR=red, WARN=normal, INFO=normal, DEBUG=normal, TRACE=normal} + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file From bec59851970c9524c384f522908f7ade2ca9ca9d Mon Sep 17 00:00:00 2001 From: Scribble Date: Sat, 5 Oct 2024 23:36:58 +0200 Subject: [PATCH 19/51] New Crowdin updates (#5) --- README.md | 4 ++++ .../assets/lotaslight/lang/ko_kr.json | 20 +++++++++---------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index 03f251c..67c7401 100644 --- a/README.md +++ b/README.md @@ -18,10 +18,14 @@ Needs Fabric API Help translate this mod with [Crowdin](https://crowdin.com/project/lotas-light) ### Contributions +- Chinese Simplified + - Blom - French - Elvee - German - ScribbleTAS +- Korean + - swwgdxed - LOLCAT - ScribbleTAS - Polish diff --git a/src/main/resources/assets/lotaslight/lang/ko_kr.json b/src/main/resources/assets/lotaslight/lang/ko_kr.json index df61dcf..ecaf0be 100644 --- a/src/main/resources/assets/lotaslight/lang/ko_kr.json +++ b/src/main/resources/assets/lotaslight/lang/ko_kr.json @@ -1,13 +1,13 @@ { - "key.lotaslight.increaseTickrate": "Increase Tickrate", - "key.lotaslight.decreaseTickrate": "Decrease Tickrate", - "key.lotaslight.freezeTickrate": "Freeze Tickrate", - "key.lotaslight.advanceTickrate": "Advance Tick", - "key.lotaslight.savestate": "Savestate", - "key.lotaslight.loadstate": "Loadstate", - "msg.lotaslight.setTickrate": "Set the tickrate to %s", - "msg.lotaslight.turnOff": "To turn off these messages, use /lotaslight showMessage false", - "msg.lotaslight.showmsg.true": "Tickrate messages enabled", - "msg.lotaslight.showmsg.false": "Tickrate messages disabled", + "key.lotaslight.increaseTickrate": "틱레이트 늘리기", + "key.lotaslight.decreaseTickrate": "틱레이트 줄이기", + "key.lotaslight.freezeTickrate": "멈추기", + "key.lotaslight.advanceTickrate": "1틱 전진", + "key.lotaslight.savestate": "상태 저장", + "key.lotaslight.loadstate": "상태 불러오기", + "msg.lotaslight.setTickrate": "틱레이트를 %s로 설정함", + "msg.lotaslight.turnOff": "이런 메시지를 끄시고 싶다면, /lotaslight showMessage false를 사용하세요", + "msg.lotaslight.showmsg.true": "틱레이트 메시지 활성화", + "msg.lotaslight.showmsg.false": "틱레이트 메시지 비활성화", "keycategory.lotaslight.lotaslight": "LoTAS-Light" } From 0d18e4aca5280bafb54ac925c5936f4654831b6c Mon Sep 17 00:00:00 2001 From: Scribble Date: Mon, 7 Oct 2024 14:02:13 +0200 Subject: [PATCH 20/51] Fix log4j config, bump loader version --- gradle.properties | 2 +- src/main/java/com/minecrafttas/lotas_light/LoTASLight.java | 2 +- src/main/resources/log4j.xml | 2 -- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/gradle.properties b/gradle.properties index dbd3f6d..58e7ee7 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,7 +6,7 @@ org.gradle.parallel=true # check these on https://fabricmc.net/develop minecraft_version=1.21.1 yarn_mappings=1.21+build.2 -loader_version=0.15.11 +loader_version=0.16.5 # Mod Properties mod_version=0.1-dev diff --git a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java index 2439d73..c1ef534 100644 --- a/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java +++ b/src/main/java/com/minecrafttas/lotas_light/LoTASLight.java @@ -22,7 +22,7 @@ public class LoTASLight implements ModInitializer { @Override public void onInitialize() { - + LOGGER.debug("Initializing LoTAS-Light"); CommandRegistrationCallback.EVENT.register((dispatcher, registryAccess, environment) -> { LoTASLightCommand.register(dispatcher); SavestateCommand.register(dispatcher); diff --git a/src/main/resources/log4j.xml b/src/main/resources/log4j.xml index 7f0d998..831e116 100644 --- a/src/main/resources/log4j.xml +++ b/src/main/resources/log4j.xml @@ -18,10 +18,8 @@ - - \ No newline at end of file From 5af4a279293dde3a584f1b9785e57b2d8e0989f7 Mon Sep 17 00:00:00 2001 From: Scribble Date: Tue, 8 Oct 2024 23:25:44 +0200 Subject: [PATCH 21/51] Add the ability to name savestates - Improved `/savestate info` output --- .../lotas_light/command/SavestateCommand.java | 91 ++++++++++++++++++- .../savestates/SavestateHandler.java | 57 +++++++++--- .../savestates/SavestateIndexer.java | 26 ++++-- .../assets/lotaslight/lang/en_us.json | 8 +- 4 files changed, 150 insertions(+), 32 deletions(-) diff --git a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java index 3c05f61..6e57535 100644 --- a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java @@ -1,11 +1,17 @@ package com.minecrafttas.lotas_light.command; +import java.text.SimpleDateFormat; +import java.util.List; + import com.minecrafttas.lotas_light.LoTASLight; +import com.minecrafttas.lotas_light.savestates.SavestateIndexer.Savestate; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; +import com.mojang.brigadier.arguments.StringArgumentType; import com.mojang.brigadier.context.CommandContext; import net.minecraft.ChatFormatting; +import net.minecraft.client.resources.language.I18n; import net.minecraft.commands.CommandSourceStack; import net.minecraft.commands.Commands; import net.minecraft.network.chat.ClickEvent; @@ -22,7 +28,10 @@ public static void register(CommandDispatcher commandDispatc .executes(SavestateCommand::saveNew) .then(Commands.argument("index", IntegerArgumentType.integer(1)) .executes(SavestateCommand::saveIndex) - ) + .then(Commands.argument("name", StringArgumentType.greedyString()) + .executes(SavestateCommand::saveNameIndex))) + .then(Commands.argument("name", StringArgumentType.greedyString()) + .executes(SavestateCommand::saveName)) ) .then(Commands.literal("load") .executes(SavestateCommand::loadRecent) @@ -42,26 +51,71 @@ public static void register(CommandDispatcher commandDispatc .then(Commands.literal("reload") .executes(SavestateCommand::reload) ) + .then(Commands.literal("info") + .executes(SavestateCommand::info) + ) + ); //@formatter:on } private static int saveNew(CommandContext context) { int index = -1; - LoTASLight.savestateHandler.saveState(index, (newindex, targetPath, sourcePath) -> { - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", newindex).withStyle(ChatFormatting.GREEN), true); + LoTASLight.savestateHandler.saveState(index, (paths) -> { + //@formatter:off + context.getSource().sendSuccess(() -> + Component.translatable("msg.lotaslight.savestate.save", + Component.literal(paths.getName()).withStyle(ChatFormatting.YELLOW), + Component.literal(Integer.toString(paths.getIndex())).withStyle(ChatFormatting.AQUA) + ).withStyle(ChatFormatting.GREEN), true); + //@formatter:on }); return 0; } private static int saveIndex(CommandContext context) { int index = context.getArgument("index", Integer.class); - LoTASLight.savestateHandler.saveState(index, (newindex, targetPath, sourcePath) -> { - context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.save", newindex).withStyle(ChatFormatting.GREEN), true); + LoTASLight.savestateHandler.saveState(index, (paths) -> { + //@formatter:off + context.getSource().sendSuccess(() -> + Component.translatable("msg.lotaslight.savestate.save", + Component.literal(paths.getName()).withStyle(ChatFormatting.YELLOW), + Component.literal(Integer.toString(paths.getIndex())).withStyle(ChatFormatting.AQUA) + ).withStyle(ChatFormatting.GREEN), true); + //@formatter:on }); return index; } + private static int saveName(CommandContext context) { + String name = context.getArgument("name", String.class); + LoTASLight.savestateHandler.saveState(name, (paths) -> { + //@formatter:off + context.getSource().sendSuccess(() -> + Component.translatable("msg.lotaslight.savestate.save", + Component.literal(paths.getName()).withStyle(ChatFormatting.YELLOW), + Component.literal(Integer.toString(paths.getIndex())).withStyle(ChatFormatting.AQUA) + ).withStyle(ChatFormatting.GREEN), true); + //@formatter:on + }); + return 0; + } + + private static int saveNameIndex(CommandContext context) { + int index = context.getArgument("index", Integer.class); + String name = context.getArgument("name", String.class); + LoTASLight.savestateHandler.saveState(index, name, (paths) -> { + //@formatter:off + context.getSource().sendSuccess(() -> + Component.translatable("msg.lotaslight.savestate.save", + Component.literal(paths.getName()).withStyle(ChatFormatting.YELLOW), + Component.literal(Integer.toString(paths.getIndex())).withStyle(ChatFormatting.AQUA) + ).withStyle(ChatFormatting.GREEN), true); + //@formatter:on + }); + return 0; + } + private static int loadRecent(CommandContext context) { int index = -1; context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.load", index).withStyle(ChatFormatting.GREEN), true); @@ -118,4 +172,31 @@ private static int reload(CommandContext context) { context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.reload"), true); return 0; } + + private static int info(CommandContext context) { + List savestateList = LoTASLight.savestateHandler.getSavestateInfo(5); + String format = I18n.get("msg.lotaslight.savestate.dateformat"); + context.getSource().sendSystemMessage(Component.literal(" ")); + SimpleDateFormat dateFormat = new SimpleDateFormat(format); + for (Savestate savestate : savestateList) { + String index = Integer.toString(savestate.getIndex()); + //@formatter:off + context.getSource().sendSystemMessage( + Component.translatable("%s: %s", + Component.literal(index).withStyle(ChatFormatting.AQUA), + Component.literal(savestate.getName()).withStyle(ChatFormatting.YELLOW) + ).withStyle(t-> + t.withClickEvent(new ClickEvent(ClickEvent.Action.SUGGEST_COMMAND, String.format("/savestate load %s", savestate.getIndex()))) + ).withStyle(t -> + t.withHoverEvent(new HoverEvent(HoverEvent.Action.SHOW_TEXT, + Component.translatable("msg.lotaslight.savestate.info.hover", + Component.literal(dateFormat.format(savestate.getDate())).withStyle(ChatFormatting.GOLD), + index + ).withStyle(ChatFormatting.GREEN))) + ) + ); + //@formatter:on + } + return 0; + } } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java index 2026add..678d8eb 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateHandler.java @@ -1,6 +1,7 @@ package com.minecrafttas.lotas_light.savestates; import java.nio.file.Path; +import java.util.List; import org.apache.logging.log4j.Logger; @@ -25,40 +26,66 @@ public SavestateHandler(Logger logger, Path savesDir, Path savestateBaseDir, Str this.indexer = new SavestateIndexer(logger, savesDir, savestateBaseDir, worldname); } - public void saveState(int index) { - saveState(index, null); + public void saveState(SavestateCallback cb, SavestateFlags... options) { + saveState(-1, null, cb, options); } - public void saveState(int index, SavestateCallback cb) { - saveState(index, true, cb); + public void saveState(int index, SavestateCallback cb, SavestateFlags... options) { + saveState(index, null, cb, options); } - public void saveState(int index, boolean pauseTickrate, SavestateCallback cb) { - saveState(index, pauseTickrate, true, cb); + public void saveState(String name, SavestateCallback cb, SavestateFlags... options) { + saveState(-1, name, cb, options); } - public void saveState(int index, boolean pauseTickrate, boolean changeIndex, SavestateCallback cb) { - SavestatePaths paths = indexer.createSavestate(index); + public void saveState(int index, String name, SavestateCallback cb, SavestateFlags... options) { + SavestatePaths paths = indexer.createSavestate(index, name); logger.debug("Source: {}, Target: {}", paths.getSourceFolder(), paths.getTargetFolder()); if (cb != null) { - cb.invoke(paths.getIndex(), paths.getSourceFolder(), paths.getTargetFolder()); + cb.invoke(paths); } } - public void loadState(int index, SavestateCallback cb) { - loadState(index, true, cb); + public void loadState(SavestateCallback cb, SavestateFlags... options) { + loadState(-1, null, cb, options); } - public void loadState(int index, boolean pauseTickrate, SavestateCallback cb) { - loadState(index, pauseTickrate, true, cb); + public void loadState(int index, SavestateCallback cb, SavestateFlags... options) { + loadState(index, null, cb, options); } - private void loadState(int index, boolean pauseTickrate, boolean changeIndex, SavestateCallback cb) { + public void loadState(String name, SavestateCallback cb, SavestateFlags... options) { + loadState(-1, name, cb, options); + } + + public void loadState(int index, String name, SavestateCallback cb, SavestateFlags... options) { } @FunctionalInterface public interface SavestateCallback { - public void invoke(int index, Path targetPath, Path sourcePath); + public void invoke(SavestatePaths path); + } + + public List getSavestateInfo(int tail) { + return indexer.getSavestateList(tail); + } + + /** + * Acts as flags for savestates and loadstates + * + * Add these to the parameters to block certain savestate behaviour + * + * @author Scribble + */ + public static enum SavestateFlags { + /** + * Stops changing updating the current index when savestating. + */ + BLOCK_CHANGE_INDEX, + /** + * Stops setting the tickrate to 0 after a savestate/loadstate + */ + BLOCK_PAUSE_TICKRATE; } } diff --git a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java index db52ebc..f979f73 100644 --- a/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java +++ b/src/main/java/com/minecrafttas/lotas_light/savestates/SavestateIndexer.java @@ -7,6 +7,8 @@ import java.time.temporal.ChronoUnit; import java.util.Date; import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; import java.util.Map; import java.util.Map.Entry; import java.util.Properties; @@ -80,7 +82,7 @@ public SavestatePaths createSavestate(int index, String name) { Path sourceDir = savesDir.resolve(worldname); Path targetDir = currentSavestateDir.resolve(worldname + index); - return SavestatePaths.of(index, sourceDir, targetDir); + return SavestatePaths.of(index, name, sourceDir, targetDir); } private void sortSavestateList() { @@ -130,10 +132,10 @@ public Set getIndexList() { return savestateList.keySet(); } - public LinkedHashMap getSavestateList(int amount) { - LinkedHashMap out = new LinkedHashMap<>(); + public List getSavestateList(int amount) { + List out = new LinkedList<>(); if (amount <= 0) { - savestateList.forEach((key, value) -> out.put(key, value.getName())); + savestateList.forEach((key, value) -> out.add(value)); return out; } @@ -142,9 +144,9 @@ public LinkedHashMap getSavestateList(int amount) { Entry entry = copy.pollLastEntry(); if (entry == null) break; - out.put(entry.getKey(), entry.getValue().getName()); + out.addFirst(entry.getValue()); } - return new LinkedHashMap<>(out.reversed()); + return out; } public class Savestate extends AbstractDataFile { @@ -227,11 +229,13 @@ private static Date parseDate(String dateString) throws Exception { public static class SavestatePaths { private final int index; + private final String name; private final Path sourceFolder; private final Path targetFolder; - private SavestatePaths(int index, Path sourceFolder, Path targetFolder) { + private SavestatePaths(int index, String name, Path sourceFolder, Path targetFolder) { this.index = index; + this.name = name; this.sourceFolder = sourceFolder; this.targetFolder = targetFolder; } @@ -240,6 +244,10 @@ public int getIndex() { return index; } + public String getName() { + return name; + } + public Path getSourceFolder() { return sourceFolder; } @@ -248,8 +256,8 @@ public Path getTargetFolder() { return targetFolder; } - public static SavestatePaths of(int index, Path sourceFolder, Path targetFolder) { - return new SavestatePaths(index, sourceFolder, targetFolder); + public static SavestatePaths of(int index, String name, Path sourceFolder, Path targetFolder) { + return new SavestatePaths(index, name, sourceFolder, targetFolder); } } } diff --git a/src/main/resources/assets/lotaslight/lang/en_us.json b/src/main/resources/assets/lotaslight/lang/en_us.json index 8b38a7d..026f103 100644 --- a/src/main/resources/assets/lotaslight/lang/en_us.json +++ b/src/main/resources/assets/lotaslight/lang/en_us.json @@ -10,12 +10,14 @@ "msg.lotaslight.turnOff":"To turn off these messages, use /lotaslight showMessage false", "msg.lotaslight.showmsg.true":"Tickrate messages enabled", "msg.lotaslight.showmsg.false":"Tickrate messages disabled", - "msg.lotaslight.savestate.save":"Creating a new savestate at index %s", - "msg.lotaslight.savestate.load":"Load savestate from index %s", + "msg.lotaslight.savestate.save":"Creating a new savestate %s at index %s", + "msg.lotaslight.savestate.load":"Load savestate %s from index %s", "msg.lotaslight.savestate.delete":"Trying to delete savestate at index %s", "msg.lotaslight.savestate.deleteMore.singular":"You are about to delete %s savestate! Do you want to continue? %s", "msg.lotaslight.savestate.deleteMore.plural":"You are about to delete %s savestates! Do you want to continue? %s", "msg.lotaslight.savestate.deleteMore.confirm":"YES", "msg.lotaslight.savestate.deleteMore.hover":"This can not be undone or stopped!", - "msg.lotaslight.savestate.reload":"Reloading savestate index. Necessary when you edit the savestates outside of Minecraft." + "msg.lotaslight.savestate.reload":"Reloading savestate index. Necessary when you edit the savestates outside of Minecraft.", + "msg.lotaslight.savestate.dateformat":"MM/dd/yyyy hh:mm:ss a", + "msg.lotaslight.savestate.info.hover":"%s\nClick to load savestate #%s" } From cd86808c65f0d163dd8e152df48dcf8116350768 Mon Sep 17 00:00:00 2001 From: Scribble Date: Wed, 9 Oct 2024 23:52:53 +0200 Subject: [PATCH 22/51] Added reloading and error handling to savestates --- .../lotas_light/command/SavestateCommand.java | 53 ++++++--- .../savestates/SavestateHandler.java | 4 + .../savestates/SavestateIndexer.java | 104 +++++++++++++++--- .../assets/lotaslight/lang/en_us.json | 3 +- 4 files changed, 134 insertions(+), 30 deletions(-) diff --git a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java index 6e57535..e5506c6 100644 --- a/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java +++ b/src/main/java/com/minecrafttas/lotas_light/command/SavestateCommand.java @@ -2,8 +2,10 @@ import java.text.SimpleDateFormat; import java.util.List; +import java.util.function.UnaryOperator; import com.minecrafttas.lotas_light.LoTASLight; +import com.minecrafttas.lotas_light.savestates.SavestateIndexer.FailedSavestate; import com.minecrafttas.lotas_light.savestates.SavestateIndexer.Savestate; import com.mojang.brigadier.CommandDispatcher; import com.mojang.brigadier.arguments.IntegerArgumentType; @@ -18,6 +20,7 @@ import net.minecraft.network.chat.Component; import net.minecraft.network.chat.ComponentUtils; import net.minecraft.network.chat.HoverEvent; +import net.minecraft.network.chat.Style; public class SavestateCommand { public static void register(CommandDispatcher commandDispatcher) { @@ -170,6 +173,7 @@ private static int deleteDis(CommandContext context) { private static int reload(CommandContext context) { context.getSource().sendSuccess(() -> Component.translatable("msg.lotaslight.savestate.reload"), true); + LoTASLight.savestateHandler.reload(); return 0; } @@ -179,23 +183,46 @@ private static int info(CommandContext context) { context.getSource().sendSystemMessage(Component.literal(" ")); SimpleDateFormat dateFormat = new SimpleDateFormat(format); for (Savestate savestate : savestateList) { - String index = Integer.toString(savestate.getIndex()); + + String index = savestate.getIndex() == null ? "" : Integer.toString(savestate.getIndex()); + String name = savestate.getName() == null ? "" : savestate.getName(); + String date = savestate.getDate() == null ? "" : dateFormat.format(savestate.getDate()); + //@formatter:off - context.getSource().sendSystemMessage( - Component.translatable("%s: %s", + UnaryOperator