From 528dc0063547f1bd27588851782b36d7504a22e7 Mon Sep 17 00:00:00 2001 From: Scribble Date: Mon, 26 May 2025 22:34:39 +0200 Subject: [PATCH 1/2] [Savestates] Fixed world being unloaded after loadstate --- .../savestates/AccessorPlayerChunkMap.java | 22 ----- .../mixin/savestates/MixinPlayerChunkMap.java | 95 +++++++++++++++++++ .../tasmod/registries/TASmodKeybinds.java | 6 -- .../savestates/SavestateHandlerServer.java | 4 +- .../handlers/SavestateWorldHandler.java | 21 +++- .../com/minecrafttas/tasmod/util/Ducks.java | 26 +++++ src/main/resources/tasmod.mixin.json | 2 +- 7 files changed, 140 insertions(+), 36 deletions(-) delete mode 100644 src/main/java/com/minecrafttas/tasmod/mixin/savestates/AccessorPlayerChunkMap.java create mode 100644 src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/AccessorPlayerChunkMap.java b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/AccessorPlayerChunkMap.java deleted file mode 100644 index c2791f78..00000000 --- a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/AccessorPlayerChunkMap.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.minecrafttas.tasmod.mixin.savestates; - -import java.util.List; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; - -import com.minecrafttas.tasmod.savestates.handlers.SavestateWorldHandler; - -import net.minecraft.entity.player.EntityPlayerMP; -import net.minecraft.server.management.PlayerChunkMap; - -@Mixin(PlayerChunkMap.class) -public interface AccessorPlayerChunkMap { - - /** - * @return The players from the specified chunk map - * @see SavestateWorldHandler#addPlayerToChunkMap() - */ - @Accessor - public List getPlayers(); -} diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java new file mode 100644 index 00000000..cfa112ed --- /dev/null +++ b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java @@ -0,0 +1,95 @@ +package com.minecrafttas.tasmod.mixin.savestates; + +import java.util.Collections; +import java.util.Comparator; +import java.util.Iterator; +import java.util.List; + +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import com.google.common.collect.ComparisonChain; +import com.minecrafttas.tasmod.savestates.handlers.SavestateWorldHandler; +import com.minecrafttas.tasmod.util.Ducks.PlayerChunkMapDuck; + +import net.minecraft.entity.player.EntityPlayerMP; +import net.minecraft.server.management.PlayerChunkMap; +import net.minecraft.server.management.PlayerChunkMapEntry; +import net.minecraft.world.WorldProvider; +import net.minecraft.world.WorldServer; + +@Mixin(PlayerChunkMap.class) +public class MixinPlayerChunkMap implements PlayerChunkMapDuck { + + @Shadow + @Final + private List players; + @Shadow + private boolean sortMissingChunks; + @Shadow + @Final + private List entriesWithoutChunks; + @Shadow + private boolean sortSendToPlayers; + @Shadow + @Final + private List pendingSendToPlayers; + @Shadow + @Final + private WorldServer world; + + /** + * @return The players from the specified chunk map + * @see SavestateWorldHandler#addPlayerToChunkMap() + */ + @Override + public List getPlayers() { + return players; + } + + /** + * {@inheritDoc} + * @see SavestateWorldHandler#addPlayersToChunkMap() + */ + @Override + public void forceTick() { + this.sortMissingChunks = false; + Collections.sort(this.entriesWithoutChunks, new Comparator() { + public int compare(PlayerChunkMapEntry playerChunkMapEntry, PlayerChunkMapEntry playerChunkMapEntry2) { + return ComparisonChain.start().compare(playerChunkMapEntry.getClosestPlayerDistance(), playerChunkMapEntry2.getClosestPlayerDistance()).result(); + } + }); + + this.sortSendToPlayers = false; + Collections.sort(this.pendingSendToPlayers, new Comparator() { + + public int compare(PlayerChunkMapEntry playerChunkMapEntry, PlayerChunkMapEntry playerChunkMapEntry2) { + return ComparisonChain.start().compare(playerChunkMapEntry.getClosestPlayerDistance(), playerChunkMapEntry2.getClosestPlayerDistance()).result(); + } + }); + + if (!this.pendingSendToPlayers.isEmpty()) { + + int i = 81; + Iterator iterator2 = this.pendingSendToPlayers.iterator(); + + while (iterator2.hasNext()) { + PlayerChunkMapEntry playerChunkMapEntry3 = (PlayerChunkMapEntry) iterator2.next(); + if (playerChunkMapEntry3.sendToPlayers()) { + iterator2.remove(); + if (--i < 0) { + break; + } + } + } + } + + if (this.players.isEmpty()) { + WorldProvider worldProvider = this.world.provider; + if (!worldProvider.canRespawnHere()) { + this.world.getChunkProvider().queueUnloadAll(); + } + } + } +} diff --git a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java index 104a91ea..58558fe4 100644 --- a/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java +++ b/src/main/java/com/minecrafttas/tasmod/registries/TASmodKeybinds.java @@ -39,12 +39,6 @@ public enum TASmodKeybinds { TEST1("Various Testing", "TASmod", Keyboard.KEY_F12, () -> { }, VirtualKeybindings::isKeyDown), TEST2("Various Testing2", "TASmod", Keyboard.KEY_F7, () -> { - // try { - // TASmodClient.client = new Client("localhost", TASmod.networkingport - 1, TASmodPackets.values(), mc.getSession().getProfile().getName(), true); - // } catch (Exception e) { - // e.printStackTrace(); - // } -// TASmodClient.controller.setTASState(TASstate.PLAYBACK); }, VirtualKeybindings::isKeyDown); private Keybind keybind; diff --git a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerServer.java b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerServer.java index e7d5b18b..387078c5 100644 --- a/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerServer.java +++ b/src/main/java/com/minecrafttas/tasmod/savestates/SavestateHandlerServer.java @@ -394,9 +394,7 @@ public void loadState(int savestateIndex, boolean tickrate0, boolean changeIndex server.getPlayerList().sendMessage(new TextComponentString(TextFormatting.GREEN + "Savestate " + indexToLoad + " loaded")); // Add players to the chunk - server.getPlayerList().getPlayers().forEach(player -> { - worldHandler.addPlayerToServerChunk(player); - }); + worldHandler.addPlayersToServerChunks(); worldHandler.sendChunksToClient(); diff --git a/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java b/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java index bcabd82c..e236c372 100644 --- a/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java +++ b/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java @@ -4,10 +4,10 @@ import java.util.List; -import com.minecrafttas.tasmod.mixin.savestates.AccessorPlayerChunkMap; import com.minecrafttas.tasmod.mixin.savestates.MixinChunkProviderServer; import com.minecrafttas.tasmod.savestates.SavestateHandlerClient; import com.minecrafttas.tasmod.util.Ducks.ChunkProviderDuck; +import com.minecrafttas.tasmod.util.Ducks.PlayerChunkMapDuck; import com.minecrafttas.tasmod.util.Ducks.WorldServerDuck; import com.minecrafttas.tasmod.util.LoggerMarkers; @@ -55,6 +55,15 @@ public void enableLevelSaving() { } } + /** + * Add players to their respective chunks + */ + public void addPlayersToServerChunks() { + server.getPlayerList().getPlayers().forEach(player -> { + addPlayerToServerChunk(player); + }); + } + /** * Just like {@link SavestateHandlerClient#addPlayerToClientChunk(EntityPlayer)}, adds the player to the chunk on the server. * This prevents the player from being able to place block inside of him @@ -138,6 +147,11 @@ public void addPlayersToChunkMap() { break; } } + + // Tick the player chunk map to send the chunks to the clients + for (WorldServer world : worlds) { + ((PlayerChunkMapDuck) world.getPlayerChunkMap()).forceTick(); + } } /** @@ -150,15 +164,14 @@ private void addPlayerToChunkMap(WorldServer world, EntityPlayerMP player) { int playerChunkPosY = (int) player.posZ >> 4; PlayerChunkMap playerChunkMap = world.getPlayerChunkMap(); - List players = ((AccessorPlayerChunkMap) playerChunkMap).getPlayers(); + List players = ((PlayerChunkMapDuck) playerChunkMap).getPlayers(); if (players.contains(player)) { LOGGER.debug(LoggerMarkers.Savestate, "Not adding player {} to chunkmap, player already exists", player.getName()); } else { playerChunkMap.addPlayer(player); } - Chunk chunk = world.getChunkProvider().provideChunk(playerChunkPosX, playerChunkPosY); - chunk.addEntity(player); + world.getChunkProvider().provideChunk(playerChunkPosX, playerChunkPosY); world.spawnEntity(player); } diff --git a/src/main/java/com/minecrafttas/tasmod/util/Ducks.java b/src/main/java/com/minecrafttas/tasmod/util/Ducks.java index ea310379..c065e17c 100644 --- a/src/main/java/com/minecrafttas/tasmod/util/Ducks.java +++ b/src/main/java/com/minecrafttas/tasmod/util/Ducks.java @@ -1,5 +1,11 @@ package com.minecrafttas.tasmod.util; +import java.util.List; + +import com.minecrafttas.tasmod.mixin.savestates.MixinPlayerChunkMap; + +import net.minecraft.entity.player.EntityPlayerMP; + /** * Oh boy, ducks! I can't help but quack up when they waddle their way into the code. :duck: * But let me tell you a little secret: I have a love-hate relationship with ducks. Not the adorable feathered creatures, mind you, but those sneaky little programming devils that swim in the deep waters of out-of-scope variables. @@ -129,4 +135,24 @@ public static interface WorldClientDuck { */ public void clearEntityList(); } + + /** + * Quacks the {@link MixinPlayerChunkMap} + */ + public static interface PlayerChunkMapDuck { + /** + * @return The list of players of this chunk map + */ + public List getPlayers(); + + /** + *

Forces a tick in the chunk map without being dependent on the world time. + *

The chunk map is responsible for sending the necessary chunks to the client.
+ * However, to properly do that, the chunks have to be sorted first, which happens every few world ticks. + * + *

This method sorts and sends the chunks to the clients, without waiting for the world time + * @see MixinPlayerChunkMap#forceTick() + */ + public void forceTick(); + } } diff --git a/src/main/resources/tasmod.mixin.json b/src/main/resources/tasmod.mixin.json index 7c67b9f2..453b70cb 100644 --- a/src/main/resources/tasmod.mixin.json +++ b/src/main/resources/tasmod.mixin.json @@ -12,7 +12,7 @@ "savestates.AccessorAnvilChunkLoader", "savestates.AccessorChunkLoader", "savestates.AccessorEntityLivingBase", - "savestates.AccessorPlayerChunkMap", + "savestates.MixinPlayerChunkMap", "savestates.MixinChunkProviderServer", "savestates.MixinNetHandlerPlayServer", "savestates.MixinScoreboard", From 41d0d825818cbd1567139ecb1eb33da3ff10ecb4 Mon Sep 17 00:00:00 2001 From: Scribble Date: Tue, 27 May 2025 13:35:18 +0200 Subject: [PATCH 2/2] [Savestates] Merged WorldServerDuck into PlayerChunkMapDuck Merged both "sendingToClient" functionality and removed a mixin and duck in the process --- .../mixin/savestates/MixinPlayerChunkMap.java | 20 ++++++++- .../mixin/savestates/MixinWorldServer.java | 44 ------------------- .../handlers/SavestateWorldHandler.java | 10 +---- .../com/minecrafttas/tasmod/util/Ducks.java | 19 ++------ src/main/resources/tasmod.mixin.json | 1 - 5 files changed, 24 insertions(+), 70 deletions(-) delete mode 100644 src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinWorldServer.java diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java index cfa112ed..4651f2a0 100644 --- a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java +++ b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinPlayerChunkMap.java @@ -18,9 +18,10 @@ import net.minecraft.server.management.PlayerChunkMapEntry; import net.minecraft.world.WorldProvider; import net.minecraft.world.WorldServer; +import net.minecraft.world.chunk.Chunk; @Mixin(PlayerChunkMap.class) -public class MixinPlayerChunkMap implements PlayerChunkMapDuck { +public abstract class MixinPlayerChunkMap implements PlayerChunkMapDuck { @Shadow @Final @@ -54,6 +55,20 @@ public List getPlayers() { */ @Override public void forceTick() { + + /* + * Update the chunks to make them eligible to be sent to the client + * + * In the #sendToPlayers() method is a check where the chunk is not sent, + * when Chunk#isPopulated() is false. This would normally happen during the WorldServer#updateBlocks() method, + * but we want to send the chunks without updating the blocks, hence this is circumvented like this. + */ + for (Iterator iterator2 = this.getChunkIterator(); iterator2.hasNext();) { + Chunk chunk = (Chunk) iterator2.next(); + chunk.enqueueRelightChecks(); + chunk.onTick(false); + } + this.sortMissingChunks = false; Collections.sort(this.entriesWithoutChunks, new Comparator() { public int compare(PlayerChunkMapEntry playerChunkMapEntry, PlayerChunkMapEntry playerChunkMapEntry2) { @@ -92,4 +107,7 @@ public int compare(PlayerChunkMapEntry playerChunkMapEntry, PlayerChunkMapEntry } } } + + @Shadow + protected abstract Iterator getChunkIterator(); } diff --git a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinWorldServer.java b/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinWorldServer.java deleted file mode 100644 index c3f8b06b..00000000 --- a/src/main/java/com/minecrafttas/tasmod/mixin/savestates/MixinWorldServer.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.minecrafttas.tasmod.mixin.savestates; - -import java.util.Iterator; - -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Shadow; - -import com.minecrafttas.tasmod.util.Ducks.WorldServerDuck; - -import net.minecraft.server.management.PlayerChunkMap; -import net.minecraft.world.WorldServer; -import net.minecraft.world.chunk.Chunk; - -/** - * Adds the {@link #sendChunksToClient()} method to to the WorldServer - * - * @author Scribble - */ -@Mixin(WorldServer.class) -public class MixinWorldServer implements WorldServerDuck { - - @Shadow - private PlayerChunkMap playerChunkMap; - - /** - *

Tricks the {@link #playerChunkMap} into sending the loaded chunks to the client.
- * In theory, the playerChunkMap just needs to be ticked once,
- * in order for the {@link PlayerChunkMap#pendingSendToPlayers} chunks to be sent to the client. - *

This fails however because the {@link net.minecraft.server.management.PlayerChunkMapEntry#sendToPlayers() PlayerChunkMapEntry#sendToPlayers()} method, responsible for sending, has a {@link net.minecraft.world.chunk.Chunk#isPopulated() Chunk.isPopulated()} check.
- *

To make this check return true, the chunk needs to be ticked once (as seen in {@link net.minecraft.world.chunk.Chunk#onTick(boolean) Chunk.onTick()}).
- * In vanilla, this usually happens in the {@link WorldServer#updateBlocks()} method,
- * but calling this method here updates a lot of other things as well, which we do not want. - *

That's why we iterate through the chunks to tick each once, then send them off via the playerChunkMap. - */ - @Override - public void sendChunksToClient() { - for (Iterator iterator2 = this.playerChunkMap.getChunkIterator(); iterator2.hasNext();) { - Chunk chunk = (Chunk) iterator2.next(); - chunk.enqueueRelightChecks(); - chunk.onTick(false); - } - this.playerChunkMap.tick(); - } -} diff --git a/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java b/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java index e236c372..6a08c9b5 100644 --- a/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java +++ b/src/main/java/com/minecrafttas/tasmod/savestates/handlers/SavestateWorldHandler.java @@ -8,7 +8,6 @@ import com.minecrafttas.tasmod.savestates.SavestateHandlerClient; import com.minecrafttas.tasmod.util.Ducks.ChunkProviderDuck; import com.minecrafttas.tasmod.util.Ducks.PlayerChunkMapDuck; -import com.minecrafttas.tasmod.util.Ducks.WorldServerDuck; import com.minecrafttas.tasmod.util.LoggerMarkers; import net.minecraft.client.Minecraft; @@ -147,11 +146,6 @@ public void addPlayersToChunkMap() { break; } } - - // Tick the player chunk map to send the chunks to the clients - for (WorldServer world : worlds) { - ((PlayerChunkMapDuck) world.getPlayerChunkMap()).forceTick(); - } } /** @@ -218,8 +212,8 @@ public void sendChunksToClient() { WorldServer[] worlds = server.worlds; for (WorldServer world : worlds) { - WorldServerDuck worldTick = (WorldServerDuck) world; - worldTick.sendChunksToClient(); + PlayerChunkMapDuck chunkMap = (PlayerChunkMapDuck) world.getPlayerChunkMap(); + chunkMap.forceTick(); } } diff --git a/src/main/java/com/minecrafttas/tasmod/util/Ducks.java b/src/main/java/com/minecrafttas/tasmod/util/Ducks.java index c065e17c..bd13bd34 100644 --- a/src/main/java/com/minecrafttas/tasmod/util/Ducks.java +++ b/src/main/java/com/minecrafttas/tasmod/util/Ducks.java @@ -38,20 +38,6 @@ public static interface ChunkProviderDuck { public void unloadAllChunks(); } - /** - * Quacks the worldserver to implement custom chunk ticking behavior - * - * @author Scribble - */ - public static interface WorldServerDuck { - - /** - * Sends the chunks to the client - * @see com.minecrafttas.tasmod.mixin.savestates.MixinWorldServer#sendChunksToClient() MixinWorldServer#sendChunksToClient() - */ - public void sendChunksToClient(); - } - /** * Quacks the gui screen to spit out mouse positions independent of the display size */ @@ -149,8 +135,9 @@ public static interface PlayerChunkMapDuck { *

Forces a tick in the chunk map without being dependent on the world time. *

The chunk map is responsible for sending the necessary chunks to the client.
* However, to properly do that, the chunks have to be sorted first, which happens every few world ticks. - * - *

This method sorts and sends the chunks to the clients, without waiting for the world time + *

Under normal circumstances, WorldServer#tick() would update the world time to make this happen, + * but our goal with savestates is to load the chunks without advancing the world time. + * Hence why this method sorts and ticks the chunk map, without waiting for the world time * @see MixinPlayerChunkMap#forceTick() */ public void forceTick(); diff --git a/src/main/resources/tasmod.mixin.json b/src/main/resources/tasmod.mixin.json index 453b70cb..bf57b5be 100644 --- a/src/main/resources/tasmod.mixin.json +++ b/src/main/resources/tasmod.mixin.json @@ -16,7 +16,6 @@ "savestates.MixinChunkProviderServer", "savestates.MixinNetHandlerPlayServer", "savestates.MixinScoreboard", - "savestates.MixinWorldServer", // Events "events.MixinEntityPlayerMP",