Skip to content

Commit

Permalink
Fix chunk generation
Browse files Browse the repository at this point in the history
  • Loading branch information
senseiwells committed Jan 30, 2024
1 parent a405056 commit 36a8272
Show file tree
Hide file tree
Showing 20 changed files with 264 additions and 55 deletions.
2 changes: 1 addition & 1 deletion LICENSE
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
MIT License

Copyright (c) 2021 senseiwells
Copyright (c) 2024 senseiwells

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
Expand Down
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ dependencies {
})

modImplementation("net.fabricmc:fabric-loader:${property("loader_version")}")
modImplementation("net.fabricmc:fabric-language-kotlin:${property("fabric_kotlin_version")}")
modImplementation("net.fabricmc.fabric-api:fabric-api:${property("fabric_version")}")

// I've had some issues with ReplayStudio and slf4j (in dev)
Expand All @@ -44,7 +45,6 @@ dependencies {
exclude(group = "com.google.guava", module = "guava")
exclude(group = "com.google.code.gson", module = "gson")
})
include(modImplementation("net.fabricmc:fabric-language-kotlin:${property("fabric_kotlin_version")}")!!)
include(modImplementation("me.lucko:fabric-permissions-api:${property("permissions_version")}")!!)

modImplementation("com.github.gnembon:fabric-carpet:${property("carpet_version")}")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,8 @@

public interface ServerReplay$ChunkMapInvoker extends ChunkMapInvoker {
@Override
default ChunkHolder getVisibleChunkIfExists(long pos) {
return this.replay$getVisibleChunkIfPresent(pos);
default ChunkHolder getUpdatingChunkIfPresent(long pos) {
return this.replay$getUpdatingChunkIfPresent(pos);
}

@NotNull
Expand All @@ -17,7 +17,7 @@ default ThreadedLevelLightEngine getLightEngine() {
return this.replay$getLightEngine();
}

ChunkHolder replay$getVisibleChunkIfPresent(long pos);
ChunkHolder replay$getUpdatingChunkIfPresent(long pos);

ThreadedLevelLightEngine replay$getLightEngine();
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,17 @@
package me.senseiwells.replay.mixin;

import me.senseiwells.replay.chunk.ChunkRecorder;
import me.senseiwells.replay.chunk.ChunkRecorders;
import me.senseiwells.replay.config.ReplayConfig;
import me.senseiwells.replay.player.PlayerRecorder;
import me.senseiwells.replay.player.PlayerRecorders;
import net.fabricmc.fabric.api.event.lifecycle.v1.ServerLifecycleEvents;
import net.minecraft.server.MinecraftServer;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

@Mixin(MinecraftServer.class)
public class MinecraftServerMixin {
Expand All @@ -24,14 +28,29 @@ private void onServerLoaded(CallbackInfo ci) {
}

@Inject(
method = "stopServer",
method = "saveAllChunks",
at = @At("TAIL")
)
private void onServerStopped(CallbackInfo ci) {
private void onSave(
boolean suppressLog,
boolean flush,
boolean forced,
CallbackInfoReturnable<Boolean> cir
) {
ReplayConfig.write();
}

@Inject(
method = "stopServer",
at = @At("TAIL")
)
private void onServerStopped(CallbackInfo ci) {
for (PlayerRecorder recorder : PlayerRecorders.all()) {
recorder.stop();
}

for (ChunkRecorder recorder : ChunkRecorders.all()) {
recorder.stop();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,9 @@ private void onLoggedIn(
CallbackInfo ci
) {
GameProfile profile = this.authenticatedProfile;
if (profile != null && ReplayConfig.getEnabled() && PlayerRecorders.predicate.test(new ReplayPlayerContext(this.server, profile))) {
ServerReplay.logger.info("Started to record player '{}'", profile.getName());
if (profile != null && ReplayConfig.getEnabled() && ReplayConfig.predicate.test(new ReplayPlayerContext(this.server, profile))) {
PlayerRecorder recorder = PlayerRecorders.create(this.server, profile);
recorder.logStart();
recorder.afterLogin();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package me.senseiwells.replay.mixin;
package me.senseiwells.replay.mixin.chunk;

import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
import me.senseiwells.replay.chunk.ChunkRecorder;
Expand Down
64 changes: 64 additions & 0 deletions src/main/java/me/senseiwells/replay/mixin/chunk/ChunkMapMixin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package me.senseiwells.replay.mixin.chunk;

import com.llamalad7.mixinextras.sugar.Local;
import me.senseiwells.replay.chunk.ChunkRecorder;
import me.senseiwells.replay.chunk.ChunkRecorders;
import me.senseiwells.replay.config.ReplayConfig;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.world.level.ChunkPos;
import org.spongepowered.asm.mixin.Debug;
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 org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;

import java.util.function.BooleanSupplier;

@Debug(export = true)
@Mixin(ChunkMap.class)
public class ChunkMapMixin {
@Inject(
method = "updateChunkScheduling",
at = @At(
value = "INVOKE",
target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectLinkedOpenHashMap;put(JLjava/lang/Object;)Ljava/lang/Object;",
remap = false
)
)
private void onUpdateChunkMap(
long chunkPos,
int newLevel,
ChunkHolder holder,
int oldLevel,
CallbackInfoReturnable<ChunkHolder> cir
) {
ChunkPos pos = new ChunkPos(chunkPos);
for (ChunkRecorder recorder : ChunkRecorders.all()) {
if (recorder.getChunks().contains(pos)) {
recorder.unpause(pos);
}
}
}

@Inject(
method = "processUnloads",
at = @At(
value = "INVOKE",
target = "Lit/unimi/dsi/fastutil/longs/Long2ObjectLinkedOpenHashMap;put(JLjava/lang/Object;)Ljava/lang/Object;",
remap = false
)
)
private void onUnloadChunk(
BooleanSupplier hasMoreTime,
CallbackInfo ci,
@Local ChunkHolder holder
) {
if (ReplayConfig.getSkipWhenChunksUnloaded()) {
for (ChunkRecorder recorder : ChunkRecorders.all()) {
recorder.pause(holder.getPos());
}
}
}
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package me.senseiwells.replay.mixin;
package me.senseiwells.replay.mixin.chunk;

import me.senseiwells.replay.chunk.ChunkRecorder;
import me.senseiwells.replay.chunk.ChunkRecorders;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package me.senseiwells.replay.mixin.chunk;

import com.mojang.datafixers.util.Either;
import net.minecraft.server.level.ChunkHolder;
import net.minecraft.server.level.ServerChunkCache;
import net.minecraft.world.level.chunk.ChunkAccess;
import net.minecraft.world.level.chunk.ChunkStatus;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Invoker;

import java.util.concurrent.CompletableFuture;

@Mixin(ServerChunkCache.class)
public interface ServerChunkCacheInvoker {
@Invoker("getChunkFutureMainThread")
CompletableFuture<Either<ChunkAccess, ChunkHolder.ChunkLoadingFailure>> getChunkAsync(
int x,
int y,
ChunkStatus status,
boolean load
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,11 @@

@Mixin(ChunkMap.class)
public abstract class ChunkMapMixin implements ServerReplay$ChunkMapInvoker {
@Nullable @Shadow protected abstract ChunkHolder getVisibleChunkIfPresent(long chunkPos);

@Shadow @Final private ThreadedLevelLightEngine lightEngine;

@Override
public ChunkHolder replay$getVisibleChunkIfPresent(long pos) {
return this.getVisibleChunkIfPresent(pos);
public ChunkHolder replay$getUpdatingChunkIfPresent(long pos) {
return this.getUpdatingChunkIfPresent(pos);
}

@NotNull
Expand Down
19 changes: 17 additions & 2 deletions src/main/kotlin/me/senseiwells/replay/chunk/ChunkArea.kt
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,23 @@ class ChunkArea(
}

override fun iterator(): Iterator<ChunkPos> {
// TODO: Implement this faster
return ChunkPos.rangeClosed(this.from, this.to).iterator()
val dx = this.to.x - this.from.x + 1
val dz = this.to.z - this.from.z + 1
val total = dx * dz
return object: Iterator<ChunkPos> {
private var index = 0

override fun hasNext(): Boolean {
return this.index < total
}

override fun next(): ChunkPos {
val x = this.index % dx
val z = this.index / dx
this.index++
return ChunkPos(from.x + x, from.z + z)
}
}
}

override fun equals(other: Any?): Boolean {
Expand Down
76 changes: 66 additions & 10 deletions src/main/kotlin/me/senseiwells/replay/chunk/ChunkRecorder.kt
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
package me.senseiwells.replay.chunk

import me.senseiwells.replay.config.ReplayConfig
import me.senseiwells.replay.mixin.rejoin.ChunkMapAccessor
import me.senseiwells.replay.recorder.ChunkSender
import me.senseiwells.replay.recorder.ReplayRecorder
import me.senseiwells.replay.rejoin.RejoinedReplayPlayer
import me.senseiwells.replay.util.ducks.ChunkMapInvoker
import net.minecraft.core.UUIDUtil
import net.minecraft.network.chat.Component
import net.minecraft.network.protocol.Packet
import net.minecraft.network.protocol.game.ClientboundAddEntityPacket
import net.minecraft.network.protocol.game.ClientboundSetEntityDataPacket
Expand All @@ -16,20 +20,21 @@ import net.minecraft.world.entity.EntityType
import net.minecraft.world.level.ChunkPos
import net.minecraft.world.level.levelgen.Heightmap
import net.minecraft.world.phys.Vec3
import org.jetbrains.annotations.ApiStatus.Internal
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
import java.util.function.Consumer

// TODO:
// - When everything is unloaded in the chunk area then we skip forward...
// - What happens when chunks are initially loaded?
class ChunkRecorder internal constructor(
val chunks: ChunkArea,
val recorderName: String,
recordings: Path
): ReplayRecorder(chunks.level.server, PROFILE,recordings), ChunkSender {
private val dummy = ServerPlayer(this.server, this.chunks.level, PROFILE, ClientInformation.createDefault())

private var totalPausedTime: Long = 0
private var lastPaused: Long = 0

override val level: ServerLevel
get() = this.chunks.level

Expand All @@ -38,13 +43,10 @@ class ChunkRecorder internal constructor(
}

override fun start(): Boolean {
// We load all the chunks to ensure we can
// correctly record the initial chunks.
for (pos in this.chunks) {
this.level.chunkSource.addRegionTicket(ChunkRecorders.RECORDING_TICKET, pos, 1, pos)
}

val center = this.getCenterChunk()
// Load the chunk
this.level.getChunk(center.x, center.z)

val x = center.middleBlockX
val z = center.middleBlockZ
val y = this.level.getHeight(Heightmap.Types.WORLD_SURFACE, x, z)
Expand Down Expand Up @@ -87,6 +89,10 @@ class ChunkRecorder internal constructor(
}
}

override fun getTimestamp(): Long {
return super.getTimestamp() - this.totalPausedTime - this.getCurrentPause()
}

override fun canContinueRecording(): Boolean {
return true
}
Expand Down Expand Up @@ -115,7 +121,57 @@ class ChunkRecorder internal constructor(
(tracking as ChunkRecorderTrackedEntity).addRecorder(this)
}

@Internal
fun pause(unloaded: ChunkPos) {
if (!this.paused() && ReplayConfig.skipWhenChunksUnloaded && this.chunks.contains(unloaded)) {
for (pos in this.chunks) {
if (this.level.chunkSource.hasChunk(pos.x, pos.z)) {
return
}
}
this.lastPaused = System.currentTimeMillis()

if (ReplayConfig.notifyPlayersLoadingChunks) {
this.ignore {
this.server.playerList.broadcastSystemMessage(
Component.literal("Paused recording for ${this.getName()}, chunks were unloaded"),
false
)
}
}
}
}

@Internal
fun unpause(loaded: ChunkPos) {
if (this.paused() && this.chunks.contains(loaded)) {
this.totalPausedTime += this.getCurrentPause()
this.lastPaused = 0L

if (ReplayConfig.notifyPlayersLoadingChunks) {
this.ignore {
this.server.playerList.broadcastSystemMessage(
Component.literal("Resumed recording for ${this.getName()}, chunks were loaded"),
false
)
}
}
}
}

@Internal
internal fun paused(): Boolean {
return this.lastPaused != 0L
}

private fun getCurrentPause(): Long {
if (this.paused()) {
return System.currentTimeMillis() - this.lastPaused
}
return 0L
}

companion object {
private val PROFILE = UUIDUtil.createOfflineProfile("ChunkRecorder")
private val PROFILE = UUIDUtil.createOfflineProfile("CR")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -116,7 +116,7 @@ object ReplayCommand {
context.source.sendSuccess({ Component.literal("ServerReplay is now enabled!") }, true)

for (player in context.source.server.playerList.players) {
if (PlayerRecorders.predicate.test(ReplayPlayerContext.of(player))) {
if (ReplayConfig.predicate.test(ReplayPlayerContext.of(player))) {
PlayerRecorders.create(player).tryStart()
}
}
Expand Down Expand Up @@ -257,7 +257,7 @@ object ReplayCommand {
if (recorders.isNotEmpty()) {
builder.append("Currently Recording $type:").append("\n")
for ((recorder, compressed) in recorders.map { it to it.getCompressedRecordingSize() }) {
val seconds = recorder.getRecordingTimeMS() / 1000
val seconds = recorder.getTotalRecordingTime() / 1000
val hours = seconds / 3600
val minutes = seconds % 3600 / 60
val secs = seconds % 60
Expand Down
Loading

0 comments on commit 36a8272

Please sign in to comment.