Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: Hot reload maps #293

Open
wants to merge 2 commits into
base: 1.20.4
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion src/main/java/xyz/nucleoid/plasmid/Plasmid.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@
import net.minecraft.util.ActionResult;
import net.minecraft.util.Hand;
import net.minecraft.util.Identifier;
import net.minecraft.util.dynamic.Codecs;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import xyz.nucleoid.plasmid.command.*;
Expand Down Expand Up @@ -114,6 +113,10 @@ private void registerCallbacks() {
GamePortalManager.INSTANCE.tick();
});

ServerLifecycleEvents.END_DATA_PACK_RELOAD.register(
(_server, resourceManager, success) -> GameSpaceManager.get().onReload(resourceManager, success)
);

ServerLifecycleEvents.SERVER_STARTING.register(server -> {
GameSpaceManager.openServer(server);
GamePortalManager.INSTANCE.setup(server);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package xyz.nucleoid.plasmid.duck;

import it.unimi.dsi.fastutil.longs.LongSet;

public interface ServerEntityManagerAccess {
void plasmid$clearChunks(LongSet chunksToDrop);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package xyz.nucleoid.plasmid.duck;

import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.world.gen.chunk.ChunkGenerator;

public interface ThreadedAnvilChunkStorageAccess {
void plasmid$setGenerator(ChunkGenerator generator);
void plasmid$clearChunks(LongSet chunksToDrop);
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package xyz.nucleoid.plasmid.game.event;

import net.minecraft.resource.LifecycledResourceManager;
import net.minecraft.text.Text;
import org.jetbrains.annotations.Nullable;
import xyz.nucleoid.plasmid.game.GameActivity;
Expand Down Expand Up @@ -32,6 +33,19 @@ public final class GameActivityEvents {
}
});

/**
* Called after datapacks are reloaded.
*/
public static final StimulusEvent<Reload> RELOAD = StimulusEvent.create(Reload.class, ctx -> (resourceManager, success) -> {
try {
for (var listener : ctx.getListeners()) {
listener.onReload(resourceManager, success);
}
} catch (Throwable throwable) {
ctx.handleException(throwable);
}
});

/**
* Called when a {@link GameActivity} should be disabled. This happens when a {@link GameActivity} is replaced by
* another on a {@link GameSpace} or when a {@link GameSpace} is closed.
Expand Down Expand Up @@ -143,6 +157,10 @@ public interface Tick {
void onTick();
}

public interface Reload {
void onReload(LifecycledResourceManager resourceManager, boolean success);
}

public interface RequestStart {
@Nullable
GameResult onRequestStart();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.minecraft.entity.player.PlayerEntity;
import net.minecraft.registry.RegistryKey;
import net.minecraft.resource.LifecycledResourceManager;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.util.Identifier;
Expand Down Expand Up @@ -192,6 +193,12 @@ void removePlayerFromGameSpace(ManagedGameSpace gameSpace, ServerPlayerEntity pl
this.playerToGameSpace.remove(player.getUuid(), gameSpace);
}

public void onReload(LifecycledResourceManager resourceManager, boolean success) {
for (var gameSpace : this.gameSpaces) {
gameSpace.onReload(resourceManager, success);
}
}

private void close() {
Stimuli.unregisterSelector(this.listenerSelector);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import com.google.common.collect.Lists;
import net.minecraft.registry.RegistryKey;
import net.minecraft.resource.LifecycledResourceManager;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.network.ServerPlayerEntity;
import net.minecraft.text.Text;
Expand Down Expand Up @@ -240,6 +241,10 @@ void onPlayerRemove(ServerPlayerEntity player) {
this.manager.removePlayerFromGameSpace(this, player);
}

void onReload(LifecycledResourceManager resourceManager, boolean success) {
this.state.invoker(GameActivityEvents.RELOAD).onReload(resourceManager, success);
}

void onAddWorld(RuntimeWorldHandle worldHandle) {
this.manager.addDimensionToGameSpace(this, worldHandle.asWorld().getRegistryKey());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
package xyz.nucleoid.plasmid.game.manager;

import com.google.common.collect.Iterators;
import it.unimi.dsi.fastutil.longs.LongSet;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import net.minecraft.registry.RegistryKey;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.GameRules;
import net.minecraft.world.World;
import net.minecraft.world.gen.chunk.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
import xyz.nucleoid.fantasy.Fantasy;
import xyz.nucleoid.fantasy.RuntimeWorldConfig;
import xyz.nucleoid.fantasy.RuntimeWorldHandle;
import xyz.nucleoid.fantasy.util.GameRuleStore;
import xyz.nucleoid.plasmid.game.world.GameSpaceWorlds;
import xyz.nucleoid.plasmid.duck.ThreadedAnvilChunkStorageAccess;

import java.util.Iterator;
import java.util.Map;
Expand All @@ -37,6 +40,23 @@ public ServerWorld add(RuntimeWorldConfig worldConfig) {
return worldHandle.asWorld();
}

@Override
public void regenerate(ServerWorld world, ChunkGenerator generator, LongSet chunksToDrop) {
if (!this.worlds.containsKey(world.getRegistryKey())) {
throw new IllegalArgumentException(String.format("The given world %s was not part of the game space!", world.getRegistryKey().getValue()));
}

var tacs = ((ThreadedAnvilChunkStorageAccess) world.getChunkManager().threadedAnvilChunkStorage);

// Set chunk generator
tacs.plasmid$setGenerator(generator);

// Clear all chunks
tacs.plasmid$clearChunks(chunksToDrop);

// We don't need to actually initiate a regeneration, as Minecraft will notice that they are missing and do it
}

@Override
public boolean remove(ServerWorld world) {
var dimension = world.getRegistryKey();
Expand Down
34 changes: 34 additions & 0 deletions src/main/java/xyz/nucleoid/plasmid/game/world/GameSpaceWorlds.java
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
package xyz.nucleoid.plasmid.game.world;

import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.server.world.ServerWorld;
import net.minecraft.world.gen.chunk.ChunkGenerator;
import org.jetbrains.annotations.NotNull;
import xyz.nucleoid.fantasy.RuntimeWorldConfig;
import xyz.nucleoid.map_templates.BlockBounds;
import xyz.nucleoid.plasmid.game.GameSpace;

import java.util.Iterator;
Expand All @@ -21,6 +24,37 @@ public interface GameSpaceWorlds extends Iterable<ServerWorld> {
*/
ServerWorld add(RuntimeWorldConfig worldConfig);

/**
* Regenerates a temporary world associated with this {@link GameSpace} with the given generator. This will
* reset all blocks and all entities (other than players) in the world.
* <p>
* The parameter `chunksToDrop` should contain all chunks which have been generated and are nonempty. Otherwise,
* unexpected behaviour (such as incorrect lighting or generation) may be encountered.
*
* @param world the world to regenerate
* @param generator the chunk generator to regenerate it with
* @param chunksToDrop the list of chunks to regenerate. This should consist of the world's nonempty, generated
* chunks.
*/
void regenerate(ServerWorld world, ChunkGenerator generator, LongSet chunksToDrop);

/**
* Regenerates a temporary world associated with this {@link GameSpace} with the given generator. This will
* reset all blocks and all entities (other than players) in the world.
* <p>
* The parameter `worldBounds` should contain all chunks which have been generated and are nonempty. For instance,
* when using an {@link xyz.nucleoid.plasmid.game.world.generator.TemplateChunkGenerator}, providing the union of
* the old and new {@link xyz.nucleoid.map_templates.MapTemplate}s's bounds is sufficient.
*
* @param world the world to regenerate
* @param generator the chunk generator to regenerate it with
* @param worldBounds the bounds of the world to regenerate
* chunks.
*/
default void regenerate(ServerWorld world, ChunkGenerator generator, BlockBounds worldBounds) {
this.regenerate(world, generator, worldBounds.asChunks());
}

/**
* Removes and deletes a temporary world that is associated with this {@link GameSpace}.
* The passed world must have been created through {@link GameSpaceWorlds#add(RuntimeWorldConfig)}.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package xyz.nucleoid.plasmid.mixin.game.world;

import net.minecraft.server.world.ChunkHolder;
import net.minecraft.server.world.ThreadedAnvilChunkStorage;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;

import java.util.concurrent.Executor;

@Mixin(ChunkHolder.class)
public interface ChunkHolderAccessor {
@Invoker("updateFutures")
void plasmid$updateFutures(ThreadedAnvilChunkStorage chunkStorage, Executor executor);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package xyz.nucleoid.plasmid.mixin.game.world;

import it.unimi.dsi.fastutil.longs.LongOpenHashSet;
import it.unimi.dsi.fastutil.longs.LongSet;
import net.minecraft.server.world.ServerEntityManager;
import net.minecraft.util.math.ChunkPos;
import net.minecraft.world.storage.ChunkDataList;
import org.spongepowered.asm.mixin.Final;
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.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import xyz.nucleoid.plasmid.duck.ServerEntityManagerAccess;

import java.util.List;
import java.util.Queue;
import java.util.function.Consumer;

@Mixin(ServerEntityManager.class)
public class ServerEntityManagerMixin<T> implements ServerEntityManagerAccess {
@Shadow @Final private Queue<ChunkDataList<T>> loadingQueue;
@Unique
private final LongSet plasmid$chunksToDropData = new LongOpenHashSet();

@Override
public void plasmid$clearChunks(LongSet chunksToDrop) {
this.plasmid$chunksToDropData.addAll(chunksToDrop);
}

@Inject(method = "scheduleRead", at = @At(value = "INVOKE", target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectMap;put(JLjava/lang/Object;)Ljava/lang/Object;", shift = At.Shift.AFTER), cancellable = true)
private void scheduleRead(long chunkPos, CallbackInfo ci) {
if (this.plasmid$chunksToDropData.remove(chunkPos)) {
this.loadingQueue.add(new ChunkDataList<>(new ChunkPos(chunkPos), List.of()));
ci.cancel();
}
}

@Inject(method = "trySave", at = @At("HEAD"))
private void trySave(long chunkPos, Consumer<T> action, CallbackInfoReturnable<Boolean> cir) {
this.plasmid$chunksToDropData.remove(chunkPos);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package xyz.nucleoid.plasmid.mixin.game.world;

import net.minecraft.server.world.ServerLightingProvider;
import net.minecraft.util.math.ChunkPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;

@Mixin(ServerLightingProvider.class)
public interface ServerLightingProviderAccessor {
@Invoker("updateChunkStatus")
void plasmid$updateChunkStatus(ChunkPos pos);
}
Loading
Loading