mirror of
https://github.com/Winds-Studio/Leaf.git
synced 2025-12-23 00:49:31 +00:00
Revert
Revert "async saving player stats and advancements (#334)" This reverts commit107ae7954f. Revert "optimize random tick (#357)" This reverts commit2e822d3714. Revert "disable optimise-random-tick by default" This reverts commit20cc10e45f. Revert "fix random tick do extra tick" This reverts commit4bf675075a. Revert "fix tickingPos out of bounds" This reverts commit0eeb6e719c. Revert "improve ServerStatsCounter compatibility" This reverts commit47c1783afc. Revert "fix random tick" This reverts commitaad17b0a5b. Revert "revert level dat" This reverts commit8d36c9a5f7.
This commit is contained in:
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user