9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-23 00:49:31 +00:00
Revert "async saving player stats and advancements (#334)"

This reverts commit 107ae7954f.

Revert "optimize random tick (#357)"

This reverts commit 2e822d3714.

Revert "disable optimise-random-tick by default"

This reverts commit 20cc10e45f.

Revert "fix random tick do extra tick"

This reverts commit 4bf675075a.

Revert "fix tickingPos out of bounds"

This reverts commit 0eeb6e719c.

Revert "improve ServerStatsCounter compatibility"

This reverts commit 47c1783afc.

Revert "fix random tick"

This reverts commit aad17b0a5b.

Revert "revert level dat"

This reverts commit 8d36c9a5f7.
This commit is contained in:
hayanesuru
2025-06-08 19:22:17 +09:00
parent 8d36c9a5f7
commit f607930827
14 changed files with 171 additions and 785 deletions

View File

@@ -0,0 +1,37 @@
package org.dreeam.leaf.async;
import net.minecraft.Util;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class AsyncPlayerDataSaving {
public static final ExecutorService IO_POOL = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new com.google.common.util.concurrent.ThreadFactoryBuilder()
.setPriority(Thread.NORM_PRIORITY - 2)
.setNameFormat("Leaf IO Thread")
.setUncaughtExceptionHandler(Util::onThreadException)
.build(),
new ThreadPoolExecutor.DiscardPolicy()
);
private AsyncPlayerDataSaving() {
}
public static Optional<Future<?>> submit(Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
runnable.run();
return Optional.empty();
} else {
return Optional.of(IO_POOL.submit(runnable));
}
}
}

View File

@@ -1,7 +1,6 @@
package org.dreeam.leaf.async;
import net.minecraft.server.MinecraftServer;
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
import org.dreeam.leaf.async.tracker.MultithreadedTracker;
public class ShutdownExecutors {

View File

@@ -1,307 +0,0 @@
package org.dreeam.leaf.async.storage;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import it.unimi.dsi.fastutil.objects.Object2ObjectMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectMaps;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import net.minecraft.Util;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import org.jetbrains.annotations.Nullable;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeFormatterBuilder;
import java.time.format.SignStyle;
import java.time.temporal.ChronoField;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.*;
public class AsyncPlayerDataSaving {
public static final AsyncPlayerDataSaving INSTANCE = new AsyncPlayerDataSaving();
private static final Logger LOGGER = LogManager.getLogger("Leaf Async Player IO");
public static ExecutorService IO_POOL = null;
private static final DateTimeFormatter FORMATTER = new DateTimeFormatterBuilder()
.appendValue(ChronoField.YEAR, 4, 10, SignStyle.EXCEEDS_PAD)
.appendValue(ChronoField.MONTH_OF_YEAR, 2)
.appendValue(ChronoField.DAY_OF_MONTH, 2)
.appendValue(ChronoField.HOUR_OF_DAY, 2)
.appendValue(ChronoField.MINUTE_OF_HOUR, 2)
.appendValue(ChronoField.SECOND_OF_MINUTE, 2)
.appendValue(ChronoField.NANO_OF_SECOND, 9)
.toFormatter();
private record SaveTask(Ty ty, Callable<Void> callable, String name, UUID uuid) implements Runnable {
@Override
public void run() {
try {
callable.call();
} catch (Exception e) {
LOGGER.error("Failed to save player {} data for {}", ty, name, e);
} finally {
switch (ty) {
case ENTITY -> INSTANCE.entityFut.remove(uuid);
case STATS -> INSTANCE.statsFut.remove(uuid);
case ADVANCEMENTS -> INSTANCE.advancementsFut.remove(uuid);
}
}
}
}
private enum Ty {
ENTITY,
STATS,
ADVANCEMENTS,
}
// use same lock
private final Object2ObjectMap<UUID, Future<?>> entityFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<UUID, Future<?>> statsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<UUID, Future<?>> advancementsFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private final Object2ObjectMap<Path, Future<?>> levelDatFut = Object2ObjectMaps.synchronize(new Object2ObjectOpenHashMap<>(), this);
private AsyncPlayerDataSaving() {
}
public static void init() {
if (AsyncPlayerDataSaving.IO_POOL != null) {
throw new IllegalStateException("Already initialized");
}
AsyncPlayerDataSaving.IO_POOL = new ThreadPoolExecutor(
1, 1, 0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setPriority(Thread.NORM_PRIORITY - 2)
.setNameFormat("Leaf Async Player IO Thread")
.setUncaughtExceptionHandler(Util::onThreadException)
.build(),
new ThreadPoolExecutor.DiscardPolicy()
);
}
public void saveLevelData(Path path, @Nullable Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
if (runnable != null) {
runnable.run();
}
return;
}
var fut = levelDatFut.get(path);
if (fut != null) {
try {
while (true) {
try {
fut.get();
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (ExecutionException e) {
LOGGER.error("Failed to save level.dat for {}", path, e);
} finally {
levelDatFut.remove(path);
}
}
if (runnable != null) {
levelDatFut.put(path, IO_POOL.submit(() -> {
try {
runnable.run();
} catch (Exception e) {
LOGGER.error(e);
} finally {
levelDatFut.remove(path);
}
}));
}
}
public boolean isSaving(UUID uuid) {
var entity = entityFut.get(uuid);
var advancements = advancementsFut.get(uuid);
var stats = statsFut.get(uuid);
return entity != null || advancements != null || stats != null;
}
public void submitStats(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.STATS, uuid, playerName, callable);
}
public void submitEntity(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.ENTITY, uuid, playerName, callable);
}
public void submitAdvancements(UUID uuid, String playerName, Callable<Void> callable) {
submit(Ty.ADVANCEMENTS, uuid, playerName, callable);
}
private void submit(Ty type, UUID uuid, String playerName, Callable<Void> callable) {
if (!AsyncPlayerDataSave.enabled) {
try {
callable.call();
} catch (Exception e) {
LOGGER.error("Failed to save player {} data for {}", type, playerName, e);
}
return;
}
block(type, uuid, playerName);
var fut = IO_POOL.submit(new SaveTask(type, callable, playerName, uuid));
switch (type) {
case ENTITY -> entityFut.put(uuid, fut);
case ADVANCEMENTS -> advancementsFut.put(uuid, fut);
case STATS -> statsFut.put(uuid, fut);
}
}
public void blockStats(UUID uuid, String playerName) {
block(Ty.STATS, uuid, playerName);
}
public void blockEntity(UUID uuid, String playerName) {
block(Ty.ENTITY, uuid, playerName);
}
public void blockAdvancements(UUID uuid, String playerName) {
block(Ty.ADVANCEMENTS, uuid, playerName);
}
private void block(Ty type, UUID uuid, String playerName) {
if (!AsyncPlayerDataSave.enabled) {
return;
}
Future<?> fut = switch (type) {
case ENTITY -> entityFut.get(uuid);
case ADVANCEMENTS -> advancementsFut.get(uuid);
case STATS -> statsFut.get(uuid);
};
if (fut == null) {
return;
}
try {
while (true) {
try {
fut.get();
break;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
} catch (ExecutionException exception) {
LOGGER.warn("Failed to save player {} data for {}", type, playerName, exception);
fut.cancel(true);
} finally {
switch (type) {
case ENTITY -> entityFut.remove(uuid);
case ADVANCEMENTS -> advancementsFut.remove(uuid);
case STATS -> statsFut.remove(uuid);
}
}
}
private static final StandardCopyOption[] ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.ATOMIC_MOVE, StandardCopyOption.REPLACE_EXISTING};
private static final StandardCopyOption[] NO_ATOMIC_MOVE = new StandardCopyOption[]{StandardCopyOption.REPLACE_EXISTING};
public static void safeReplace(Path current, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
safeReplace(current, bytes, 0, bytes.length);
}
@SuppressWarnings("unused")
public static void safeReplaceBackup(Path current, Path old, String content) {
byte[] bytes = content.getBytes(StandardCharsets.UTF_8);
safeReplaceBackup(current, old, bytes, 0, bytes.length);
}
public static void safeReplace(Path current, byte[] bytes, int offset, int length) {
File latest = writeTempFile(current, bytes, offset, length);
Objects.requireNonNull(latest);
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(latest.toPath(), current, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
}
}
}
public static void safeReplaceBackup(Path current, Path backup, byte[] bytes, int offset, int length) {
File latest = writeTempFile(current, bytes, offset, length);
Objects.requireNonNull(latest);
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(current, backup, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(current, backup, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", current, backup, i, e);
}
}
for (int i = 1; i <= 10; i++) {
try {
try {
Files.move(latest.toPath(), current, ATOMIC_MOVE);
} catch (AtomicMoveNotSupportedException e) {
Files.move(latest.toPath(), current, NO_ATOMIC_MOVE);
}
break;
} catch (IOException e) {
LOGGER.error("Failed move {} to {} retries ({} / 10)", latest, current, i, e);
}
}
}
private static File writeTempFile(Path current, byte[] bytes, int offset, int length) {
Path dir = current.getParent();
for (int i = 1; i <= 10; i++) {
File temp = null;
try {
if (!dir.toFile().isDirectory()) {
Files.createDirectories(dir);
}
temp = tempFileDateTime(current).toFile();
if (temp.exists()) {
throw new FileAlreadyExistsException(temp.getPath());
}
// sync content and metadata to device
try (RandomAccessFile stream = new RandomAccessFile(temp, "rws")) {
stream.write(bytes, offset, length);
}
return temp;
} catch (IOException e) {
LOGGER.error("Failed write {} retries ({} / 10)", temp == null ? current : temp, i, e);
}
}
return null;
}
private static Path tempFileDateTime(Path path) {
String now = LocalDateTime.now().format(FORMATTER);
String last = path.getFileName().toString();
int dot = last.lastIndexOf('.');
String base = (dot == -1) ? last : last.substring(0, dot);
String ext = (dot == -1) ? "" : last.substring(dot);
String newExt = switch (ext) {
case ".json", ".dat" -> ext;
default -> ".temp";
};
return path.resolveSibling(base + "-" + now + newExt);
}
}

View File

@@ -1,9 +1,7 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.async.storage.AsyncPlayerDataSaving;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class AsyncPlayerDataSave extends ConfigModules {
@@ -11,9 +9,7 @@ public class AsyncPlayerDataSave extends ConfigModules {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save";
}
@Experimental
public static boolean enabled = false;
private static boolean asyncPlayerDataSaveInitialized;
@Override
public void onLoaded() {
@@ -22,13 +18,6 @@ public class AsyncPlayerDataSave extends ConfigModules {
"""
异步保存玩家数据.""");
if (asyncPlayerDataSaveInitialized) {
config.getConfigSection(getBasePath());
return;
}
asyncPlayerDataSaveInitialized = true;
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
if (enabled) AsyncPlayerDataSaving.init();
}
}

View File

@@ -1,20 +0,0 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class OptimizeRandomTick extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".optimise-random-tick";
}
@Experimental
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath(), enabled);
}
}

View File

@@ -1,99 +0,0 @@
package org.dreeam.leaf.world;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.level.levelgen.BitRandomSource;
import net.minecraft.world.level.levelgen.PositionalRandomFactory;
import net.minecraft.world.level.levelgen.RandomSupport;
import net.minecraft.world.level.material.FluidState;
import org.jetbrains.annotations.NotNull;
import java.util.OptionalLong;
public final class RandomTickSystem {
private final LongArrayList tickPos = new LongArrayList();
private static final long SCALE = 0x100000L;
private static final long CHUNK_BLOCKS = 4096L;
private static final int MASK = 0xfffff;
private static final int MASK_ONE_FOURTH = 0x300000;
public void tick(ServerLevel world) {
var simpleRandom = world.simpleRandom;
int j = tickPos.size();
for (int i = 0; i < j; i++) {
tickBlock(world, tickPos.getLong(i), simpleRandom);
}
tickPos.clear();
}
private static void tickBlock(ServerLevel world, long packed, RandomSource tickRand) {
final boolean doubleTickFluids = !ca.spottedleaf.moonrise.common.PlatformHooks.get().configFixMC224294();
BlockPos pos = BlockPos.of(packed);
LevelChunk chunk = world.chunkSource.getChunkAtIfLoadedImmediately(pos.getX() >> 4, pos.getZ() >> 4);
if (chunk == null) {
return;
}
BlockState state = chunk.getBlockStateFinal(pos.getX(), pos.getY(), pos.getZ());
state.randomTick(world, pos, tickRand);
if (doubleTickFluids) {
final FluidState fluidState = state.getFluidState();
if (fluidState.isRandomlyTicking()) {
fluidState.randomTick(world, pos, tickRand);
}
}
}
private long recompute(LevelChunk chunk, long tickSpeed) {
chunk.leaf$recompute();
long tickingCount = chunk.leaf$countTickingBlocks();
long numSections = chunk.leaf$countTickingSections();
if (tickingCount == 0L || numSections == 0L) {
chunk.leaf$setRandomTickChance(0L);
return 0L;
}
long chance = (tickSpeed * tickingCount * SCALE) / CHUNK_BLOCKS;
chunk.leaf$setRandomTickChance(chance);
return chance;
}
public void randomTickChunk(
RandomSource randomSource,
LevelChunk chunk,
long tickSpeed
) {
int a = randomSource.nextInt();
if ((a & MASK_ONE_FOURTH) != 0) {
return;
}
tickSpeed = tickSpeed * 4;
long chance = chunk.leaf$randomTickChance();
if (chance == 0L && (chance = recompute(chunk, tickSpeed)) == 0L) {
return;
}
if (chance >= (long) (a & MASK) || (chance = recompute(chunk, tickSpeed)) == 0L) {
return;
}
int tickingCount = chunk.leaf$countTickingBlocks();
OptionalLong pos = chunk.leaf$tickingPos(randomSource.nextInt(tickingCount));
if (pos.isPresent()) {
tickPos.add(pos.getAsLong());
}
if (chance > SCALE) {
chance -= SCALE;
long last = randomSource.nextInt() & MASK;
while (last < chance) {
pos = chunk.leaf$tickingPos(randomSource.nextInt(tickingCount));
if (pos.isPresent()) {
tickPos.add(pos.getAsLong());
}
chance -= SCALE;
}
}
}
}

View File

@@ -14,7 +14,7 @@ public class BotStatsCounter extends ServerStatsCounter {
private static final File UNKOWN_FILE = new File("BOT_STATS_REMOVE_THIS");
public BotStatsCounter(MinecraftServer server) {
super(server, UNKOWN_FILE, "", net.minecraft.Util.NIL_UUID); // Leaf
super(server, UNKOWN_FILE);
}
@Override