9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2025-12-25 18:09:17 +00:00

Some work

This commit is contained in:
Dreeam
2025-04-04 16:06:47 -04:00
parent 91887de295
commit f96b3e9f76
39 changed files with 135 additions and 224 deletions

View File

@@ -0,0 +1,9 @@
package org.dreeam.leaf.async;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class AsyncChunkSending {
public static final Logger LOGGER = LogManager.getLogger(AsyncChunkSending.class.getSimpleName());
}

View File

@@ -0,0 +1,33 @@
package org.dreeam.leaf.async;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import java.util.concurrent.ExecutorService;
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, 0, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<>(),
new com.google.common.util.concurrent.ThreadFactoryBuilder()
.setPriority(Thread.NORM_PRIORITY - 2)
.setNameFormat("Leaf IO Thread")
.setUncaughtExceptionHandler(new net.minecraft.DefaultUncaughtExceptionHandlerWithName(net.minecraft.server.MinecraftServer.LOGGER))
.build(),
new ThreadPoolExecutor.DiscardPolicy()
);
private AsyncPlayerDataSaving() {
}
public static void save(Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
runnable.run();
} else {
IO_POOL.execute(runnable);
}
}
}

View File

@@ -0,0 +1,167 @@
package org.dreeam.leaf.async.locate;
import ca.spottedleaf.moonrise.common.util.TickThread;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.mojang.datafixers.util.Pair;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderSet;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.tags.TagKey;
import net.minecraft.world.level.chunk.ChunkGenerator;
import net.minecraft.world.level.levelgen.structure.Structure;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.function.Consumer;
// Original project: https://github.com/thebrightspark/AsyncLocator
public class AsyncLocator {
private static final ExecutorService LOCATING_EXECUTOR_SERVICE;
private AsyncLocator() {
}
public static class AsyncLocatorThread extends TickThread {
private static final AtomicInteger THREAD_COUNTER = new AtomicInteger(0);
public AsyncLocatorThread(Runnable run, String name) {
super(run, name, THREAD_COUNTER.incrementAndGet());
}
@Override
public void run() {
super.run();
}
}
static {
int threads = org.dreeam.leaf.config.modules.async.AsyncLocator.asyncLocatorThreads;
LOCATING_EXECUTOR_SERVICE = new ThreadPoolExecutor(
1,
threads,
org.dreeam.leaf.config.modules.async.AsyncLocator.asyncLocatorKeepalive,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setThreadFactory(
r -> new AsyncLocatorThread(r, "Leaf Async Locator Thread") {
@Override
public void run() {
r.run();
}
}
)
.setNameFormat("Leaf Async Locator Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build()
);
}
public static void shutdownExecutorService() {
if (LOCATING_EXECUTOR_SERVICE != null) {
LOCATING_EXECUTOR_SERVICE.shutdown();
}
}
/**
* Queues a task to locate a feature using {@link ServerLevel#findNearestMapStructure(TagKey, BlockPos, int, boolean)}
* and returns a {@link LocateTask} with the futures for it.
*/
public static LocateTask<BlockPos> locate(
ServerLevel level,
TagKey<Structure> structureTag,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
CompletableFuture<BlockPos> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateLevel(completableFuture, level, structureTag, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
/**
* Queues a task to locate a feature using
* {@link ChunkGenerator#findNearestMapStructure(ServerLevel, HolderSet, BlockPos, int, boolean)} and returns a
* {@link LocateTask} with the futures for it.
*/
public static LocateTask<Pair<BlockPos, Holder<Structure>>> locate(
ServerLevel level,
HolderSet<Structure> structureSet,
BlockPos pos,
int searchRadius,
boolean skipKnownStructures
) {
CompletableFuture<Pair<BlockPos, Holder<Structure>>> completableFuture = new CompletableFuture<>();
Future<?> future = LOCATING_EXECUTOR_SERVICE.submit(
() -> doLocateChunkGenerator(completableFuture, level, structureSet, pos, searchRadius, skipKnownStructures)
);
return new LocateTask<>(level.getServer(), completableFuture, future);
}
private static void doLocateLevel(
CompletableFuture<BlockPos> completableFuture,
ServerLevel level,
TagKey<Structure> structureTag,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
BlockPos foundPos = level.findNearestMapStructure(structureTag, pos, searchRadius, skipExistingChunks);
completableFuture.complete(foundPos);
}
private static void doLocateChunkGenerator(
CompletableFuture<Pair<BlockPos, Holder<Structure>>> completableFuture,
ServerLevel level,
HolderSet<Structure> structureSet,
BlockPos pos,
int searchRadius,
boolean skipExistingChunks
) {
Pair<BlockPos, Holder<Structure>> foundPair = level.getChunkSource().getGenerator()
.findNearestMapStructure(level, structureSet, pos, searchRadius, skipExistingChunks);
completableFuture.complete(foundPair);
}
/**
* Holder of the futures for an async locate task as well as providing some helper functions.
* The completableFuture will be completed once the call to
* {@link ServerLevel#findNearestMapStructure(TagKey, BlockPos, int, boolean)} has completed, and will hold the
* result of it.
* The taskFuture is the future for the {@link Runnable} itself in the executor service.
*/
public record LocateTask<T>(MinecraftServer server, CompletableFuture<T> completableFuture, Future<?> taskFuture) {
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action.
* Bear in mind that the action will be executed from the task's thread. If you intend to change any game data,
* it's strongly advised you use {@link #thenOnServerThread(Consumer)} instead so that it's queued and executed
* on the main server thread instead.
*/
public LocateTask<T> then(Consumer<T> action) {
completableFuture.thenAccept(action);
return this;
}
/**
* Helper function that calls {@link CompletableFuture#thenAccept(Consumer)} with the given action on the server
* thread.
*/
public LocateTask<T> thenOnServerThread(Consumer<T> action) {
completableFuture.thenAccept(pos -> server.scheduleOnMain(() -> action.accept(pos)));
return this;
}
/**
* Helper function that cancels both completableFuture and taskFuture.
*/
public void cancel() {
taskFuture.cancel(true);
completableFuture.cancel(false);
}
}
}

View File

@@ -0,0 +1,184 @@
package org.dreeam.leaf.async.tracker;
import ca.spottedleaf.moonrise.common.list.ReferenceList;
import ca.spottedleaf.moonrise.common.misc.NearbyPlayers;
import ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel;
import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup;
import ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.minecraft.server.level.ChunkMap;
import net.minecraft.server.level.FullChunkStatus;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.Entity;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
public class MultithreadedTracker {
private static final String THREAD_PREFIX = "Leaf Async Tracker";
private static final Logger LOGGER = LogManager.getLogger(THREAD_PREFIX);
private static long lastWarnMillis = System.currentTimeMillis();
private static final ThreadPoolExecutor trackerExecutor = new ThreadPoolExecutor(
getCorePoolSize(),
getMaxPoolSize(),
getKeepAliveTime(), TimeUnit.SECONDS,
getQueueImpl(),
getThreadFactory(),
getRejectedPolicy()
);
private MultithreadedTracker() {
}
public static Executor getTrackerExecutor() {
return trackerExecutor;
}
public static void tick(ChunkSystemServerLevel level) {
try {
if (!org.dreeam.leaf.config.modules.async.MultithreadedTracker.compatModeEnabled) {
tickAsync(level);
} else {
tickAsyncWithCompatMode(level);
}
} catch (Exception e) {
LOGGER.error("Error occurred while executing async task.", e);
}
}
private static void tickAsync(ChunkSystemServerLevel level) {
final NearbyPlayers nearbyPlayers = level.moonrise$getNearbyPlayers();
final ServerEntityLookup entityLookup = (ServerEntityLookup) level.moonrise$getEntityLookup();
final ReferenceList<Entity> trackerEntities = entityLookup.trackerEntities;
final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
// Move tracking to off-main
trackerExecutor.execute(() -> {
for (final Entity entity : trackerEntitiesRaw) {
if (entity == null) continue;
final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
if (tracker == null) continue;
tracker.moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
tracker.serverEntity.sendChanges();
}
});
}
private static void tickAsyncWithCompatMode(ChunkSystemServerLevel level) {
final NearbyPlayers nearbyPlayers = level.moonrise$getNearbyPlayers();
final ServerEntityLookup entityLookup = (ServerEntityLookup) level.moonrise$getEntityLookup();
final ReferenceList<Entity> trackerEntities = entityLookup.trackerEntities;
final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
final Runnable[] sendChangesTasks = new Runnable[trackerEntitiesRaw.length];
int index = 0;
for (final Entity entity : trackerEntitiesRaw) {
if (entity == null) continue;
final ChunkMap.TrackedEntity tracker = ((EntityTrackerEntity) entity).moonrise$getTrackedEntity();
if (tracker == null) continue;
tracker.moonrise$tick(nearbyPlayers.getChunk(entity.chunkPosition()));
sendChangesTasks[index++] = () -> tracker.serverEntity.sendChanges(); // Collect send changes to task array
}
// batch submit tasks
trackerExecutor.execute(() -> {
for (final Runnable sendChanges : sendChangesTasks) {
if (sendChanges == null) continue;
sendChanges.run();
}
});
}
// Original ChunkMap#newTrackerTick of Paper
// Just for diff usage for future update
private static void tickOriginal(ServerLevel level) {
final ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup entityLookup = (ca.spottedleaf.moonrise.patches.chunk_system.level.entity.server.ServerEntityLookup) ((ca.spottedleaf.moonrise.patches.chunk_system.level.ChunkSystemServerLevel) level).moonrise$getEntityLookup();
final ca.spottedleaf.moonrise.common.list.ReferenceList<net.minecraft.world.entity.Entity> trackerEntities = entityLookup.trackerEntities;
final Entity[] trackerEntitiesRaw = trackerEntities.getRawDataUnchecked();
for (int i = 0, len = trackerEntities.size(); i < len; ++i) {
final Entity entity = trackerEntitiesRaw[i];
final ChunkMap.TrackedEntity tracker = ((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerEntity) entity).moonrise$getTrackedEntity();
if (tracker == null) {
continue;
}
((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity) tracker).moonrise$tick(((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkData().nearbyPlayers);
if (((ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity) tracker).moonrise$hasPlayers()
|| ((ca.spottedleaf.moonrise.patches.chunk_system.entity.ChunkSystemEntity) entity).moonrise$getChunkStatus().isOrAfter(FullChunkStatus.ENTITY_TICKING)) {
tracker.serverEntity.sendChanges();
}
}
}
private static int getCorePoolSize() {
return 1;
}
private static int getMaxPoolSize() {
return org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads;
}
private static long getKeepAliveTime() {
return org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerKeepalive;
}
private static BlockingQueue<Runnable> getQueueImpl() {
final int queueCapacity = org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerQueueSize;
return new LinkedBlockingQueue<>(queueCapacity);
}
private static @NotNull ThreadFactory getThreadFactory() {
return new ThreadFactoryBuilder()
.setThreadFactory(MultithreadedTrackerThread::new)
.setNameFormat(THREAD_PREFIX + " Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build();
}
private static @NotNull RejectedExecutionHandler getRejectedPolicy() {
return (rejectedTask, executor) -> {
BlockingQueue<Runnable> workQueue = executor.getQueue();
if (!executor.isShutdown()) {
if (!workQueue.isEmpty()) {
List<Runnable> pendingTasks = new ArrayList<>(workQueue.size());
workQueue.drainTo(pendingTasks);
for (Runnable pendingTask : pendingTasks) {
pendingTask.run();
}
}
rejectedTask.run();
}
if (System.currentTimeMillis() - lastWarnMillis > 30000L) {
LOGGER.warn("Async entity tracker is busy! Tracking tasks will be done in the server thread. Increasing max-threads in Leaf config may help.");
lastWarnMillis = System.currentTimeMillis();
}
};
}
public static class MultithreadedTrackerThread extends Thread {
public MultithreadedTrackerThread(Runnable runnable) {
super(runnable);
}
}
}

View File

@@ -0,0 +1,30 @@
package org.dreeam.leaf.async.world;
import ca.spottedleaf.moonrise.common.util.TickThread;
import org.jetbrains.annotations.NotNull;
import java.util.concurrent.ThreadFactory;
public class SparklyPaperServerLevelTickExecutorThreadFactory implements ThreadFactory {
private final String worldName;
public SparklyPaperServerLevelTickExecutorThreadFactory(final String worldName) {
this.worldName = worldName;
}
@Override
public Thread newThread(@NotNull Runnable runnable) {
TickThread.ServerLevelTickThread tickThread = new TickThread.ServerLevelTickThread(runnable, "Leaf World Ticking Thread - " + this.worldName);
if (tickThread.isDaemon()) {
tickThread.setDaemon(false);
}
if (tickThread.getPriority() != 5) {
tickThread.setPriority(5);
}
return tickThread;
}
}

View File

@@ -0,0 +1,31 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class AsyncBlockFinding extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-block-finding";
}
@Experimental
public static boolean enabled = false;
public static boolean asyncBlockFindingInitialized;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
**Experimental feature**
This moves the expensive search calculations to a background thread while
keeping the actual block validation on the main thread.""",
"""
这会将昂贵的搜索计算移至后台线程, 同时在主线程上保持实际的方块验证.""");
if (!asyncBlockFindingInitialized) {
asyncBlockFindingInitialized = true;
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
}
}
}

View File

@@ -0,0 +1,27 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class AsyncChunkSend extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-chunk-send";
}
public static boolean enabled = false;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(),
"""
Makes chunk packet preparation and sending asynchronous to improve server performance.
This can significantly reduce main thread load when many players are loading chunks.""",
"""
使区块数据包准备和发送异步化以提高服务器性能.
当许多玩家同时加载区块时, 这可以显著减少主线程负载.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
}
}

View File

@@ -0,0 +1,37 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
public class AsyncLocator extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-locator";
}
public static boolean enabled = false;
public static int asyncLocatorThreads = 0;
public static int asyncLocatorKeepalive = 60;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Whether or not asynchronous locator should be enabled.
This offloads structure locating to other threads.
Only for locate command, dolphin treasure finding and eye of ender currently.""",
"""
是否启用异步结构搜索.
目前可用于 /locate 指令, 海豚寻宝和末影之眼.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
asyncLocatorThreads = config.getInt(getBasePath() + ".threads", asyncLocatorThreads);
asyncLocatorKeepalive = config.getInt(getBasePath() + ".keepalive", asyncLocatorKeepalive);
if (asyncLocatorThreads <= 0)
asyncLocatorThreads = 1;
if (!enabled)
asyncLocatorThreads = 0;
else
LeafConfig.LOGGER.info("Using {} threads for Async Locator", asyncLocatorThreads);
}
}

View File

@@ -0,0 +1,34 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class AsyncMobSpawning extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-mob-spawning";
}
public static boolean enabled = true;
public static boolean asyncMobSpawningInitialized;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Whether or not asynchronous mob spawning should be enabled.
On servers with many entities, this can improve performance by up to 15%. You must have
paper's per-player-mob-spawns setting set to true for this to work.
One quick note - this does not actually spawn mobs async (that would be very unsafe).
This just offloads some expensive calculations that are required for mob spawning.""",
"""
是否异步化生物生成.
在实体较多的服务器上, 异步生成可最高带来15%的性能提升.
须在Paper配置文件中打开 per-player-mob-spawns 才能生效.""");
// This prevents us from changing the value during a reload.
if (!asyncMobSpawningInitialized) {
asyncMobSpawningInitialized = true;
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
}
}
}

View File

@@ -0,0 +1,28 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class AsyncPlayerDataSave extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-playerdata-save";
}
@Experimental
public static boolean enabled = false;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(),
"""
**Experimental feature, may have data lost in some circumstances!**
Make PlayerData saving asynchronously.""",
"""
**实验性功能, 在部分场景下可能丢失玩家数据!**
异步保存玩家数据.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
}
}

View File

@@ -0,0 +1,53 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
public class MultithreadedTracker extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-entity-tracker";
}
public static boolean enabled = false;
public static boolean compatModeEnabled = false;
public static int asyncEntityTrackerMaxThreads = 0;
public static int asyncEntityTrackerKeepalive = 60;
public static int asyncEntityTrackerQueueSize = 0;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Make entity tracking saving asynchronously, can improve performance significantly,
especially in some massive entities in small area situations.""",
"""
异步实体跟踪,
在实体数量多且密集的情况下效果明显.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
compatModeEnabled = config.getBoolean(getBasePath() + ".compat-mode", compatModeEnabled, config.pickStringRegionBased("""
Enable compat mode ONLY if Citizens or NPC plugins using real entity has installed,
Compat mode fixed visible issue with player type NPCs of Citizens,
But still recommend to use packet based / virtual entity NPC plugin, e.g. ZNPC Plus, Adyeshach, Fancy NPC or else.""",
"""
是否启用兼容模式,
如果你的服务器安装了 Citizens 或其他类似非发包 NPC 插件, 请开启此项."""));
asyncEntityTrackerMaxThreads = config.getInt(getBasePath() + ".max-threads", asyncEntityTrackerMaxThreads);
asyncEntityTrackerKeepalive = config.getInt(getBasePath() + ".keepalive", asyncEntityTrackerKeepalive);
asyncEntityTrackerQueueSize = config.getInt(getBasePath() + ".queue-size", asyncEntityTrackerQueueSize);
if (asyncEntityTrackerMaxThreads < 0)
asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncEntityTrackerMaxThreads, 1);
else if (asyncEntityTrackerMaxThreads == 0)
asyncEntityTrackerMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1);
if (!enabled)
asyncEntityTrackerMaxThreads = 0;
else
LeafConfig.LOGGER.info("Using {} threads for Async Entity Tracker", asyncEntityTrackerMaxThreads);
if (asyncEntityTrackerQueueSize <= 0)
asyncEntityTrackerQueueSize = asyncEntityTrackerMaxThreads * 384;
}
}

View File

@@ -0,0 +1,40 @@
package org.dreeam.leaf.config.modules.async;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class SparklyPaperParallelWorldTicking extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".parallel-world-tracking";
} // TODO: Correct config key when stable
@Experimental
public static boolean enabled = false;
public static int threads = 8;
public static boolean logContainerCreationStacktraces = false;
public static boolean disableHardThrow = false;
public static boolean runAsyncTasksSync = false;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(),
"""
**Experimental feature**
Enables parallel world ticking to improve performance on multi-core systems..""",
"""
**实验性功能**
启用并行世界处理以提高多核系统的性能.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
threads = config.getInt(getBasePath() + ".threads", threads);
threads = enabled ? threads : 0;
logContainerCreationStacktraces = config.getBoolean(getBasePath() + ".log-container-creation-stacktraces", logContainerCreationStacktraces);
logContainerCreationStacktraces = enabled && logContainerCreationStacktraces;
disableHardThrow = config.getBoolean(getBasePath() + ".disable-hard-throw", disableHardThrow);
disableHardThrow = enabled && disableHardThrow;
runAsyncTasksSync = config.getBoolean(getBasePath() + ".run-async-tasks-sync", runAsyncTasksSync);
runAsyncTasksSync = enabled && runAsyncTasksSync;
}
}

View File

@@ -87,6 +87,9 @@ public class GaleWorldConfiguration extends ConfigurationPart {
public int duration = 100;
public int nearbyItemMaxAge = 1200;
public int checkForMinecartNearItemInterval = 20;
// Still recommend to turn-off `checkForMinecartNearItemWhileActive`
// Since `Reduce-hopper-item-checks.patch` will cause lag under massive dropped items
public boolean checkForMinecartNearItemWhileActive = false; // Leaf - Reduce active items finding hopper nearby check
public boolean checkForMinecartNearItemWhileInactive = true;
public double maxItemHorizontalDistance = 24.0;
public double maxItemVerticalDistance = 4.0;
@@ -132,7 +135,7 @@ public class GaleWorldConfiguration extends ConfigurationPart {
}
public boolean arrowMovementResetsDespawnCounter = true; // Gale - Purpur - make arrow movement resetting despawn counter configurable
public boolean arrowMovementResetsDespawnCounter = false; // Gale - Purpur - make arrow movement resetting despawn counter configurable // Leaf - KeYi - Disable arrow despawn counter by default
public boolean entitiesCanRandomStrollIntoNonTickingChunks = true; // Gale - MultiPaper - prevent entities random strolling into non-ticking chunks
public double entityWakeUpDurationRatioStandardDeviation = 0.2; // Gale - variable entity wake-up duration
public boolean hideFlamesOnEntitiesWithFireResistance = false; // Gale - Slice - hide flames on entities with fire resistance

View File

@@ -101,10 +101,10 @@ public abstract class AbstractPaperVersionFetcher implements VersionFetcher {
// Gale end - branding changes - version fetcher
return switch (distance) {
case DISTANCE_ERROR -> text("Error obtaining version information", NamedTextColor.YELLOW);
case 0 -> text("You are running the latest version", NamedTextColor.GREEN);
case DISTANCE_UNKNOWN -> text("Unknown version", NamedTextColor.YELLOW);
default -> text("You are " + distance + " version(s) behind", NamedTextColor.YELLOW)
case DISTANCE_ERROR -> text("* Error obtaining version information", NamedTextColor.RED); // Purpur - Rebrand
case 0 -> text("* You are running the latest version", NamedTextColor.GREEN); // Purpur - Rebrand
case DISTANCE_UNKNOWN -> text("* Unknown version", NamedTextColor.YELLOW); // Purpur - Rebrand
default -> text("* You are " + distance + " version(s) behind", NamedTextColor.YELLOW) // Purpur - Rebrand
.append(Component.newline())
.append(text("Download the new version at: ")
.append(text(this.downloadPage, NamedTextColor.GOLD) // Gale - branding changes - version fetcher
@@ -149,6 +149,6 @@ public abstract class AbstractPaperVersionFetcher implements VersionFetcher {
return null;
}
return text("Previous version: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC);
return text("Previous: " + oldVersion, NamedTextColor.GRAY, TextDecoration.ITALIC); // Purpur - Rebrand
}
}

View File

@@ -0,0 +1,95 @@
package su.plo.matter;
import com.google.common.collect.Iterables;
import net.minecraft.server.level.ServerLevel;
import java.math.BigInteger;
import java.security.SecureRandom;
import java.util.Optional;
public class Globals {
public static final int WORLD_SEED_LONGS = 16;
public static final int WORLD_SEED_BITS = WORLD_SEED_LONGS * 64;
public static final long[] worldSeed = new long[WORLD_SEED_LONGS];
public static final ThreadLocal<Integer> dimension = ThreadLocal.withInitial(() -> 0);
public enum Salt {
UNDEFINED,
BASTION_FEATURE,
WOODLAND_MANSION_FEATURE,
MINESHAFT_FEATURE,
BURIED_TREASURE_FEATURE,
NETHER_FORTRESS_FEATURE,
PILLAGER_OUTPOST_FEATURE,
GEODE_FEATURE,
NETHER_FOSSIL_FEATURE,
OCEAN_MONUMENT_FEATURE,
RUINED_PORTAL_FEATURE,
POTENTIONAL_FEATURE,
GENERATE_FEATURE,
JIGSAW_PLACEMENT,
STRONGHOLDS,
POPULATION,
DECORATION,
SLIME_CHUNK
}
public static void setupGlobals(ServerLevel world) {
if (!org.dreeam.leaf.config.modules.misc.SecureSeed.enabled) return;
long[] seed = world.getServer().getWorldData().worldGenOptions().featureSeed();
System.arraycopy(seed, 0, worldSeed, 0, WORLD_SEED_LONGS);
int worldIndex = Iterables.indexOf(world.getServer().levelKeys(), it -> it == world.dimension());
if (worldIndex == -1)
worldIndex = world.getServer().levelKeys().size(); // if we are in world construction it may not have been added to the map yet
dimension.set(worldIndex);
}
public static long[] createRandomWorldSeed() {
long[] seed = new long[WORLD_SEED_LONGS];
SecureRandom rand = new SecureRandom();
for (int i = 0; i < WORLD_SEED_LONGS; i++) {
seed[i] = rand.nextLong();
}
return seed;
}
// 1024-bit string -> 16 * 64 long[]
public static Optional<long[]> parseSeed(String seedStr) {
if (seedStr.isEmpty()) return Optional.empty();
if (seedStr.length() != WORLD_SEED_BITS) {
throw new IllegalArgumentException("Secure seed length must be " + WORLD_SEED_BITS + "-bit but found " + seedStr.length() + "-bit.");
}
long[] seed = new long[WORLD_SEED_LONGS];
for (int i = 0; i < WORLD_SEED_LONGS; i++) {
int start = i * 64;
int end = start + 64;
String seedSection = seedStr.substring(start, end);
BigInteger seedInDecimal = new BigInteger(seedSection, 2);
seed[i] = seedInDecimal.longValue();
}
return Optional.of(seed);
}
// 16 * 64 long[] -> 1024-bit string
public static String seedToString(long[] seed) {
StringBuilder sb = new StringBuilder();
for (long longV : seed) {
// Convert to 64-bit binary string per long
// Use format to keep 64-bit length, and use 0 to complete space
String binaryStr = String.format("%64s", Long.toBinaryString(longV)).replace(' ', '0');
sb.append(binaryStr);
}
return sb.toString();
}
}

View File

@@ -0,0 +1,74 @@
package su.plo.matter;
public class Hashing {
// https://en.wikipedia.org/wiki/BLAKE_(hash_function)
// https://github.com/bcgit/bc-java/blob/master/core/src/main/java/org/bouncycastle/crypto/digests/Blake2bDigest.java
private final static long[] blake2b_IV = {
0x6a09e667f3bcc908L, 0xbb67ae8584caa73bL, 0x3c6ef372fe94f82bL,
0xa54ff53a5f1d36f1L, 0x510e527fade682d1L, 0x9b05688c2b3e6c1fL,
0x1f83d9abfb41bd6bL, 0x5be0cd19137e2179L
};
private final static byte[][] blake2b_sigma = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3},
{11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4},
{7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8},
{9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13},
{2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9},
{12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11},
{13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10},
{6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5},
{10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0},
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3}
};
public static long[] hashWorldSeed(long[] worldSeed) {
long[] result = blake2b_IV.clone();
result[0] ^= 0x01010040;
hash(worldSeed, result, new long[16], 0, false);
return result;
}
public static void hash(long[] message, long[] chainValue, long[] internalState, long messageOffset, boolean isFinal) {
assert message.length == 16;
assert chainValue.length == 8;
assert internalState.length == 16;
System.arraycopy(chainValue, 0, internalState, 0, chainValue.length);
System.arraycopy(blake2b_IV, 0, internalState, chainValue.length, 4);
internalState[12] = messageOffset ^ blake2b_IV[4];
internalState[13] = blake2b_IV[5];
if (isFinal) internalState[14] = ~blake2b_IV[6];
internalState[15] = blake2b_IV[7];
for (int round = 0; round < 12; round++) {
G(message[blake2b_sigma[round][0]], message[blake2b_sigma[round][1]], 0, 4, 8, 12, internalState);
G(message[blake2b_sigma[round][2]], message[blake2b_sigma[round][3]], 1, 5, 9, 13, internalState);
G(message[blake2b_sigma[round][4]], message[blake2b_sigma[round][5]], 2, 6, 10, 14, internalState);
G(message[blake2b_sigma[round][6]], message[blake2b_sigma[round][7]], 3, 7, 11, 15, internalState);
G(message[blake2b_sigma[round][8]], message[blake2b_sigma[round][9]], 0, 5, 10, 15, internalState);
G(message[blake2b_sigma[round][10]], message[blake2b_sigma[round][11]], 1, 6, 11, 12, internalState);
G(message[blake2b_sigma[round][12]], message[blake2b_sigma[round][13]], 2, 7, 8, 13, internalState);
G(message[blake2b_sigma[round][14]], message[blake2b_sigma[round][15]], 3, 4, 9, 14, internalState);
}
for (int i = 0; i < 8; i++) {
chainValue[i] ^= internalState[i] ^ internalState[i + 8];
}
}
private static void G(long m1, long m2, int posA, int posB, int posC, int posD, long[] internalState) {
internalState[posA] = internalState[posA] + internalState[posB] + m1;
internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 32);
internalState[posC] = internalState[posC] + internalState[posD];
internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 24); // replaces 25 of BLAKE
internalState[posA] = internalState[posA] + internalState[posB] + m2;
internalState[posD] = Long.rotateRight(internalState[posD] ^ internalState[posA], 16);
internalState[posC] = internalState[posC] + internalState[posD];
internalState[posB] = Long.rotateRight(internalState[posB] ^ internalState[posC], 63); // replaces 11 of BLAKE
}
}

View File

@@ -0,0 +1,160 @@
package su.plo.matter;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.levelgen.LegacyRandomSource;
import net.minecraft.world.level.levelgen.WorldgenRandom;
import org.jetbrains.annotations.NotNull;
import java.util.Arrays;
public class WorldgenCryptoRandom extends WorldgenRandom {
// hash the world seed to guard against badly chosen world seeds
private static final long[] HASHED_ZERO_SEED = Hashing.hashWorldSeed(new long[Globals.WORLD_SEED_LONGS]);
private static final ThreadLocal<long[]> LAST_SEEN_WORLD_SEED = ThreadLocal.withInitial(() -> new long[Globals.WORLD_SEED_LONGS]);
private static final ThreadLocal<long[]> HASHED_WORLD_SEED = ThreadLocal.withInitial(() -> HASHED_ZERO_SEED);
private final long[] worldSeed = new long[Globals.WORLD_SEED_LONGS];
private final long[] randomBits = new long[8];
private int randomBitIndex;
private static final int MAX_RANDOM_BIT_INDEX = 64 * 8;
private static final int LOG2_MAX_RANDOM_BIT_INDEX = 9;
private long counter;
private final long[] message = new long[16];
private final long[] cachedInternalState = new long[16];
public WorldgenCryptoRandom(int x, int z, Globals.Salt typeSalt, long salt) {
super(org.dreeam.leaf.config.modules.opt.FastRNG.enabled ? new org.dreeam.leaf.util.math.random.FasterRandomSource(0L) : new LegacyRandomSource(0L));
if (typeSalt != null) {
this.setSecureSeed(x, z, typeSalt, salt);
}
}
public void setSecureSeed(int x, int z, Globals.Salt typeSalt, long salt) {
System.arraycopy(Globals.worldSeed, 0, this.worldSeed, 0, Globals.WORLD_SEED_LONGS);
message[0] = ((long) x << 32) | ((long) z & 0xffffffffL);
message[1] = ((long) Globals.dimension.get() << 32) | (salt & 0xffffffffL);
message[2] = typeSalt.ordinal();
message[3] = counter = 0;
randomBitIndex = MAX_RANDOM_BIT_INDEX;
}
private long[] getHashedWorldSeed() {
if (!Arrays.equals(worldSeed, LAST_SEEN_WORLD_SEED.get())) {
HASHED_WORLD_SEED.set(Hashing.hashWorldSeed(worldSeed));
System.arraycopy(worldSeed, 0, LAST_SEEN_WORLD_SEED.get(), 0, Globals.WORLD_SEED_LONGS);
}
return HASHED_WORLD_SEED.get();
}
private void moreRandomBits() {
message[3] = counter++;
System.arraycopy(getHashedWorldSeed(), 0, randomBits, 0, 8);
Hashing.hash(message, randomBits, cachedInternalState, 64, true);
}
private long getBits(int count) {
if (randomBitIndex >= MAX_RANDOM_BIT_INDEX) {
moreRandomBits();
randomBitIndex -= MAX_RANDOM_BIT_INDEX;
}
int alignment = randomBitIndex & 63;
if ((randomBitIndex >>> 6) == ((randomBitIndex + count) >>> 6)) {
long result = (randomBits[randomBitIndex >>> 6] >>> alignment) & ((1L << count) - 1);
randomBitIndex += count;
return result;
} else {
long result = (randomBits[randomBitIndex >>> 6] >>> alignment) & ((1L << (64 - alignment)) - 1);
randomBitIndex += count;
if (randomBitIndex >= MAX_RANDOM_BIT_INDEX) {
moreRandomBits();
randomBitIndex -= MAX_RANDOM_BIT_INDEX;
}
alignment = randomBitIndex & 63;
result <<= alignment;
result |= (randomBits[randomBitIndex >>> 6] >>> (64 - alignment)) & ((1L << alignment) - 1);
return result;
}
}
@Override
public @NotNull RandomSource fork() {
WorldgenCryptoRandom fork = new WorldgenCryptoRandom(0, 0, null, 0);
System.arraycopy(Globals.worldSeed, 0, fork.worldSeed, 0, Globals.WORLD_SEED_LONGS);
fork.message[0] = this.message[0];
fork.message[1] = this.message[1];
fork.message[2] = this.message[2];
fork.message[3] = this.message[3];
fork.randomBitIndex = this.randomBitIndex;
fork.counter = this.counter;
fork.nextLong();
return fork;
}
@Override
public int next(int bits) {
return (int) getBits(bits);
}
@Override
public void consumeCount(int count) {
randomBitIndex += count;
if (randomBitIndex >= MAX_RANDOM_BIT_INDEX * 2) {
randomBitIndex -= MAX_RANDOM_BIT_INDEX;
counter += randomBitIndex >>> LOG2_MAX_RANDOM_BIT_INDEX;
randomBitIndex &= MAX_RANDOM_BIT_INDEX - 1;
randomBitIndex += MAX_RANDOM_BIT_INDEX;
}
}
@Override
public int nextInt(int bound) {
int bits = Mth.ceillog2(bound);
int result;
do {
result = (int) getBits(bits);
} while (result >= bound);
return result;
}
@Override
public long nextLong() {
return getBits(64);
}
@Override
public double nextDouble() {
return getBits(53) * 0x1.0p-53;
}
@Override
public long setDecorationSeed(long worldSeed, int blockX, int blockZ) {
setSecureSeed(blockX, blockZ, Globals.Salt.POPULATION, 0);
return ((long) blockX << 32) | ((long) blockZ & 0xffffffffL);
}
@Override
public void setFeatureSeed(long populationSeed, int index, int step) {
setSecureSeed((int) (populationSeed >> 32), (int) populationSeed, Globals.Salt.DECORATION, index + 10000L * step);
}
@Override
public void setLargeFeatureSeed(long worldSeed, int chunkX, int chunkZ) {
super.setLargeFeatureSeed(worldSeed, chunkX, chunkZ);
}
@Override
public void setLargeFeatureWithSalt(long worldSeed, int regionX, int regionZ, int salt) {
super.setLargeFeatureWithSalt(worldSeed, regionX, regionZ, salt);
}
public static RandomSource seedSlimeChunk(int chunkX, int chunkZ) {
return new WorldgenCryptoRandom(chunkX, chunkZ, Globals.Salt.SLIME_CHUNK, 0);
}
}