9
0
mirror of https://github.com/Winds-Studio/Leaf.git synced 2026-01-04 15:41:40 +00:00

Move directory

This commit is contained in:
Dreeam
2025-01-18 10:44:55 -05:00
parent 8b3bf19a92
commit 97c4794444
401 changed files with 0 additions and 483 deletions

View File

@@ -0,0 +1,133 @@
package gg.pufferfish.pufferfish.sentry;
import com.google.common.reflect.TypeToken;
import com.google.gson.Gson;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import io.sentry.SentryEvent;
import io.sentry.SentryLevel;
import io.sentry.protocol.Message;
import io.sentry.protocol.User;
import java.util.Map;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Marker;
import org.apache.logging.log4j.core.LogEvent;
import org.apache.logging.log4j.core.Logger;
import org.apache.logging.log4j.core.appender.AbstractAppender;
import org.apache.logging.log4j.core.filter.AbstractFilter;
import org.dreeam.leaf.config.modules.misc.SentryDSN;
public class PufferfishSentryAppender extends AbstractAppender {
private static final org.apache.logging.log4j.Logger logger = LogManager.getLogger(PufferfishSentryAppender.class.getSimpleName());
private static final Gson GSON = new Gson();
private final Level logLevel;
public PufferfishSentryAppender(Level logLevel) {
super("PufferfishSentryAdapter", new SentryFilter(), null);
this.logLevel = logLevel;
}
@Override
public void append(LogEvent logEvent) {
if (logEvent.getLevel().isMoreSpecificThan(logLevel) && (logEvent.getThrown() != null || !SentryDSN.onlyLogThrown)) {
try {
logException(logEvent);
} catch (Exception e) {
logger.warn("Failed to log event with sentry", e);
}
} else {
try {
logBreadcrumb(logEvent);
} catch (Exception e) {
logger.warn("Failed to log event with sentry", e);
}
}
}
private void logException(LogEvent e) {
SentryEvent event = new SentryEvent(e.getThrown());
Message sentryMessage = new Message();
sentryMessage.setMessage(e.getMessage().getFormattedMessage());
event.setThrowable(e.getThrown());
event.setLevel(getLevel(e.getLevel()));
event.setLogger(e.getLoggerName());
event.setTransaction(e.getLoggerName());
event.setExtra("thread_name", e.getThreadName());
boolean hasContext = e.getContextData() != null;
if (hasContext && e.getContextData().containsKey("pufferfishsentry_playerid")) {
User user = new User();
user.setId(e.getContextData().getValue("pufferfishsentry_playerid"));
user.setUsername(e.getContextData().getValue("pufferfishsentry_playername"));
event.setUser(user);
}
if (hasContext && e.getContextData().containsKey("pufferfishsentry_pluginname")) {
event.setExtra("plugin.name", e.getContextData().getValue("pufferfishsentry_pluginname"));
event.setExtra("plugin.version", e.getContextData().getValue("pufferfishsentry_pluginversion"));
event.setTransaction(e.getContextData().getValue("pufferfishsentry_pluginname"));
}
if (hasContext && e.getContextData().containsKey("pufferfishsentry_eventdata")) {
Map<String, String> eventFields = GSON.fromJson((String) e.getContextData().getValue("pufferfishsentry_eventdata"), new TypeToken<Map<String, String>>() {
}.getType());
if (eventFields != null) {
event.setExtra("event", eventFields);
}
}
Sentry.captureEvent(event);
}
private void logBreadcrumb(LogEvent e) {
Breadcrumb breadcrumb = new Breadcrumb();
breadcrumb.setLevel(getLevel(e.getLevel()));
breadcrumb.setCategory(e.getLoggerName());
breadcrumb.setType(e.getLoggerName());
breadcrumb.setMessage(e.getMessage().getFormattedMessage());
Sentry.addBreadcrumb(breadcrumb);
}
private SentryLevel getLevel(Level level) {
return switch (level.getStandardLevel()) {
case TRACE, DEBUG -> SentryLevel.DEBUG;
case WARN -> SentryLevel.WARNING;
case ERROR -> SentryLevel.ERROR;
case FATAL -> SentryLevel.FATAL;
default -> SentryLevel.INFO;
};
}
private static class SentryFilter extends AbstractFilter {
@Override
public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, String msg,
Object... params) {
return this.filter(logger.getName());
}
@Override
public Result filter(Logger logger, org.apache.logging.log4j.Level level, Marker marker, Object msg, Throwable t) {
return this.filter(logger.getName());
}
@Override
public Result filter(LogEvent event) {
return this.filter(event == null ? null : event.getLoggerName());
}
private Result filter(String loggerName) {
return loggerName != null && loggerName.startsWith("gg.castaway.pufferfish.sentry") ? Result.DENY
: Result.NEUTRAL;
}
}
}

View File

@@ -0,0 +1,44 @@
package gg.pufferfish.pufferfish.sentry;
import io.sentry.Sentry;
import org.apache.logging.log4j.Level;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
public class SentryManager {
private static final Logger logger = LogManager.getLogger(SentryManager.class);
private SentryManager() {
}
private static boolean initialized = false;
public static synchronized void init(Level logLevel) {
if (initialized) {
return;
}
if (logLevel == null) {
logger.error("Invalid log level, defaulting to WARN.");
logLevel = Level.WARN;
}
try {
initialized = true;
Sentry.init(options -> {
options.setDsn(org.dreeam.leaf.config.modules.misc.SentryDSN.sentryDsn);
options.setMaxBreadcrumbs(100);
});
PufferfishSentryAppender appender = new PufferfishSentryAppender(logLevel);
appender.start();
((org.apache.logging.log4j.core.Logger) LogManager.getRootLogger()).addAppender(appender);
logger.info("Sentry logging started!");
} catch (Exception e) {
logger.warn("Failed to initialize sentry!", e);
initialized = false;
}
}
}

View File

@@ -0,0 +1,76 @@
package gg.pufferfish.pufferfish.util;
import com.google.common.collect.Queues;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.apache.logging.log4j.Marker;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.logging.Level;
public class AsyncExecutor implements Runnable {
private final Logger LOGGER = LogManager.getLogger("Leaf");
private final Queue<Runnable> jobs = Queues.newArrayDeque();
private final Lock mutex = new ReentrantLock();
private final Condition cond = mutex.newCondition();
private final Thread thread;
private volatile boolean killswitch = false;
public AsyncExecutor(String threadName) {
this.thread = new Thread(this, threadName);
}
public void start() {
thread.start();
}
public void kill() {
killswitch = true;
cond.signalAll();
}
public void submit(Runnable runnable) {
mutex.lock();
try {
jobs.offer(runnable);
cond.signalAll();
} finally {
mutex.unlock();
}
}
@Override
public void run() {
while (!killswitch) {
try {
Runnable runnable = takeRunnable();
if (runnable != null) {
runnable.run();
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (Exception e) {
LOGGER.error("Failed to execute async job for thread {}", thread.getName(), e);
}
}
}
private Runnable takeRunnable() throws InterruptedException {
mutex.lock();
try {
while (jobs.isEmpty() && !killswitch) {
cond.await();
}
if (jobs.isEmpty()) return null; // We've set killswitch
return jobs.remove();
} finally {
mutex.unlock();
}
}
}

View File

@@ -0,0 +1,20 @@
package gg.pufferfish.pufferfish.util;
import java.util.Iterator;
import org.jetbrains.annotations.NotNull;
public class IterableWrapper<T> implements Iterable<T> {
private final Iterator<T> iterator;
public IterableWrapper(Iterator<T> iterator) {
this.iterator = iterator;
}
@NotNull
@Override
public Iterator<T> iterator() {
return iterator;
}
}

View File

@@ -0,0 +1,42 @@
package gg.pufferfish.pufferfish.util;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import java.util.Map;
import org.jetbrains.annotations.Nullable;
public class Long2ObjectOpenHashMapWrapper<V> extends Long2ObjectOpenHashMap<V> {
private final Map<Long, V> backingMap;
public Long2ObjectOpenHashMapWrapper(Map<Long, V> map) {
backingMap = map;
}
@Override
public V put(Long key, V value) {
return backingMap.put(key, value);
}
@Override
public V get(Object key) {
return backingMap.get(key);
}
@Override
public V remove(Object key) {
return backingMap.remove(key);
}
@Nullable
@Override
public V putIfAbsent(Long key, V value) {
return backingMap.putIfAbsent(key, value);
}
@Override
public int size() {
return backingMap.size();
}
}

View File

@@ -0,0 +1,16 @@
package net.caffeinemc.mods.lithium.common.entity;
import net.caffeinemc.mods.lithium.common.util.change_tracking.ChangeSubscriber;
import net.minecraft.world.item.ItemStack;
public interface EquipmentEntity {
void onEquipmentReplaced(ItemStack oldStack, ItemStack newStack);
interface EquipmentTrackingEntity {
void onEquipmentChanged();
}
interface TickableEnchantmentTrackingEntity extends ChangeSubscriber.EnchantmentSubscriber<ItemStack> {
void updateHasTickableEnchantments(ItemStack oldStack, ItemStack newStack);
}
}

View File

@@ -0,0 +1,17 @@
package net.caffeinemc.mods.lithium.common.util.change_tracking;
import net.minecraft.world.item.ItemStack;
public interface ChangePublisher<T> {
void subscribe(ChangeSubscriber<T> subscriber, int subscriberData);
int unsubscribe(ChangeSubscriber<T> subscriber);
default void unsubscribeWithData(ChangeSubscriber<T> subscriber, int index) {
throw new UnsupportedOperationException("Only implemented for ItemStacks");
}
default boolean isSubscribedWithData(ChangeSubscriber<ItemStack> subscriber, int subscriberData) {
throw new UnsupportedOperationException("Only implemented for ItemStacks");
}
}

View File

@@ -0,0 +1,190 @@
package net.caffeinemc.mods.lithium.common.util.change_tracking;
import it.unimi.dsi.fastutil.ints.IntArrayList;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import net.minecraft.world.item.ItemStack;
public interface ChangeSubscriber<T> {
static <T> ChangeSubscriber<T> combine(ChangeSubscriber<T> prevSubscriber, int prevSData, @NotNull ChangeSubscriber<T> newSubscriber, int newSData) {
if (prevSubscriber == null) {
return newSubscriber;
} else if (prevSubscriber instanceof Multi) {
ArrayList<ChangeSubscriber<T>> subscribers = new ArrayList<>(((Multi<T>) prevSubscriber).subscribers);
IntArrayList subscriberDatas = new IntArrayList(((Multi<T>) prevSubscriber).subscriberDatas);
subscribers.add(newSubscriber);
subscriberDatas.add(newSData);
return new Multi<>(subscribers, subscriberDatas);
} else {
ArrayList<ChangeSubscriber<T>> subscribers = new ArrayList<>();
IntArrayList subscriberDatas = new IntArrayList();
subscribers.add(prevSubscriber);
subscriberDatas.add(prevSData);
subscribers.add(newSubscriber);
subscriberDatas.add(newSData);
return new Multi<>(subscribers, subscriberDatas);
}
}
static <T> ChangeSubscriber<T> without(ChangeSubscriber<T> prevSubscriber, ChangeSubscriber<T> removedSubscriber) {
return without(prevSubscriber, removedSubscriber, 0, false);
}
static <T> ChangeSubscriber<T> without(ChangeSubscriber<T> prevSubscriber, ChangeSubscriber<T> removedSubscriber, int removedSubscriberData, boolean matchData) {
if (prevSubscriber == removedSubscriber) {
return null;
} else if (prevSubscriber instanceof Multi<T> multi) {
int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData);
if (index != -1) {
if (multi.subscribers.size() == 2) {
return multi.subscribers.get(1 - index);
} else {
ArrayList<ChangeSubscriber<T>> subscribers = new ArrayList<>(multi.subscribers);
IntArrayList subscriberDatas = new IntArrayList(multi.subscriberDatas);
subscribers.remove(index);
subscriberDatas.removeInt(index);
return new Multi<>(subscribers, subscriberDatas);
}
} else {
return prevSubscriber;
}
} else {
return prevSubscriber;
}
}
static <T> int dataWithout(ChangeSubscriber<T> prevSubscriber, ChangeSubscriber<T> removedSubscriber, int subscriberData) {
return dataWithout(prevSubscriber, removedSubscriber, subscriberData, 0, false);
}
static <T> int dataWithout(ChangeSubscriber<T> prevSubscriber, ChangeSubscriber<T> removedSubscriber, int subscriberData, int removedSubscriberData, boolean matchData) {
if (prevSubscriber instanceof Multi<T> multi) {
int index = multi.indexOf(removedSubscriber, removedSubscriberData, matchData);
if (index != -1) {
if (multi.subscribers.size() == 2) {
return multi.subscriberDatas.getInt(1 - index);
} else {
return subscriberData;
}
} else {
return subscriberData;
}
}
return prevSubscriber == removedSubscriber ? 0 : subscriberData;
}
static int dataOf(ChangeSubscriber<?> subscribers, ChangeSubscriber<?> subscriber, int subscriberData) {
return subscribers instanceof Multi<?> multi ? multi.subscriberDatas.getInt(multi.subscribers.indexOf(subscriber)) : subscriberData;
}
static boolean containsSubscriber(ChangeSubscriber<ItemStack> subscriber, int subscriberData, ChangeSubscriber<ItemStack> subscriber1, int subscriberData1) {
if (subscriber instanceof Multi<ItemStack> multi) {
return multi.indexOf(subscriber1, subscriberData1, true) != -1;
}
return subscriber == subscriber1 && subscriberData == subscriberData1;
}
/**
* Notify the subscriber that the publisher will be changed immediately after this call.
* @param publisher The publisher that is about to change
* @param subscriberData The data associated with the subscriber, given when the subscriber was added
*/
void notify(@Nullable T publisher, int subscriberData);
/**
* Notify the subscriber about being unsubscribed from the publisher. Used when the publisher becomes invalid.
* The subscriber should not attempt to unsubscribe itself from the publisher in this method.
*
* @param publisher The publisher unsubscribed from
* @param subscriberData The data associated with the subscriber, given when the subscriber was added
*/
void forceUnsubscribe(T publisher, int subscriberData);
interface CountChangeSubscriber<T> extends ChangeSubscriber<T> {
/**
* Notify the subscriber that the publisher's count data will be changed immediately after this call.
* @param publisher The publisher that is about to change
* @param subscriberData The data associated with the subscriber, given when the subscriber was added
* @param newCount The new count of the publisher
*/
void notifyCount(T publisher, int subscriberData, int newCount);
}
interface EnchantmentSubscriber<T> extends ChangeSubscriber<T> {
/**
* Notify the subscriber that the publisher's enchantment data has been changed immediately before this call.
* @param publisher The publisher that has changed
* @param subscriberData The data associated with the subscriber, given when the subscriber was added
*/
void notifyAfterEnchantmentChange(T publisher, int subscriberData);
}
class Multi<T> implements CountChangeSubscriber<T>, EnchantmentSubscriber<T> {
private final ArrayList<ChangeSubscriber<T>> subscribers;
private final IntArrayList subscriberDatas;
public Multi(ArrayList<ChangeSubscriber<T>> subscribers, IntArrayList subscriberDatas) {
this.subscribers = subscribers;
this.subscriberDatas = subscriberDatas;
}
@Override
public void notify(T publisher, int subscriberData) {
ArrayList<ChangeSubscriber<T>> changeSubscribers = this.subscribers;
for (int i = 0; i < changeSubscribers.size(); i++) {
ChangeSubscriber<T> subscriber = changeSubscribers.get(i);
subscriber.notify(publisher, this.subscriberDatas.getInt(i));
}
}
@Override
public void forceUnsubscribe(T publisher, int subscriberData) {
ArrayList<ChangeSubscriber<T>> changeSubscribers = this.subscribers;
for (int i = 0; i < changeSubscribers.size(); i++) {
ChangeSubscriber<T> subscriber = changeSubscribers.get(i);
subscriber.forceUnsubscribe(publisher, this.subscriberDatas.getInt(i));
}
}
@Override
public void notifyCount(T publisher, int subscriberData, int newCount) {
ArrayList<ChangeSubscriber<T>> changeSubscribers = this.subscribers;
for (int i = 0; i < changeSubscribers.size(); i++) {
ChangeSubscriber<T> subscriber = changeSubscribers.get(i);
if (subscriber instanceof ChangeSubscriber.CountChangeSubscriber<T> countChangeSubscriber) {
countChangeSubscriber.notifyCount(publisher, this.subscriberDatas.getInt(i), newCount);
}
}
}
int indexOf(ChangeSubscriber<T> subscriber, int subscriberData, boolean matchData) {
if (!matchData) {
return this.subscribers.indexOf(subscriber);
} else {
for (int i = 0; i < this.subscribers.size(); i++) {
if (this.subscribers.get(i) == subscriber && this.subscriberDatas.getInt(i) == subscriberData) {
return i;
}
}
return -1;
}
}
@Override
public void notifyAfterEnchantmentChange(T publisher, int subscriberData) {
ArrayList<ChangeSubscriber<T>> changeSubscribers = this.subscribers;
for (int i = 0; i < changeSubscribers.size(); i++) {
ChangeSubscriber<T> subscriber = changeSubscribers.get(i);
if (subscriber instanceof ChangeSubscriber.EnchantmentSubscriber<T> enchantmentSubscriber) {
enchantmentSubscriber.notifyAfterEnchantmentChange(publisher, this.subscriberDatas.getInt(i));
}
}
}
}
}

View File

@@ -0,0 +1,17 @@
package org.dreeam.leaf;
import io.papermc.paper.PaperBootstrap;
import joptsimple.OptionSet;
public class LeafBootstrap {
public static final boolean enableFMA = Boolean.parseBoolean(System.getProperty("Leaf.enableFMA", "false")); // Leaf - FMA feature
public static void boot(final OptionSet options) {
runPreBootTasks();
PaperBootstrap.boot(options);
}
private static void runPreBootTasks() {
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.async;
import net.minecraft.Util;
import org.dreeam.leaf.config.modules.async.AsyncPlayerDataSave;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
public class AsyncPlayerDataSaving {
private AsyncPlayerDataSaving() {
}
public static void saveAsync(Runnable runnable) {
if (!AsyncPlayerDataSave.enabled) {
runnable.run();
return;
}
ExecutorService ioExecutor = Util.backgroundExecutor().service();
CompletableFuture.runAsync(runnable, ioExecutor);
}
}

View File

@@ -0,0 +1,164 @@
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,288 @@
package org.dreeam.leaf.async.path;
import net.minecraft.core.BlockPos;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.level.pathfinder.Node;
import net.minecraft.world.level.pathfinder.Path;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import java.util.function.Supplier;
/**
* i'll be using this to represent a path that not be processed yet!
*/
public class AsyncPath extends Path {
/**
* marks whether this async path has been processed
*/
private volatile PathProcessState processState = PathProcessState.WAITING;
/**
* runnables waiting for this to be processed
*/
private final List<Runnable> postProcessing = new ArrayList<>(0);
/**
* a list of positions that this path could path towards
*/
private final Set<BlockPos> positions;
/**
* the supplier of the real processed path
*/
private final Supplier<Path> pathSupplier;
/*
* Processed values
*/
/**
* this is a reference to the nodes list in the parent `Path` object
*/
private final List<Node> nodes;
/**
* the block we're trying to path to
* <p>
* while processing, we have no idea where this is so consumers of `Path` should check that the path is processed before checking the target block
*/
private @Nullable BlockPos target;
/**
* how far we are to the target
* <p>
* while processing, the target could be anywhere but theoretically we're always "close" to a theoretical target so default is 0
*/
private float distToTarget = 0;
/**
* whether we can reach the target
* <p>
* while processing, we can always theoretically reach the target so default is true
*/
private boolean canReach = true;
public AsyncPath(@NotNull List<Node> emptyNodeList, @NotNull Set<BlockPos> positions, @NotNull Supplier<Path> pathSupplier) {
//noinspection ConstantConditions
super(emptyNodeList, null, false);
this.nodes = emptyNodeList;
this.positions = positions;
this.pathSupplier = pathSupplier;
AsyncPathProcessor.queue(this);
}
@Override
public boolean isProcessed() {
return this.processState == PathProcessState.COMPLETED;
}
/**
* returns the future representing the processing state of this path
*/
public synchronized void postProcessing(@NotNull Runnable runnable) {
if (isProcessed()) {
runnable.run();
} else {
this.postProcessing.add(runnable);
}
}
/**
* an easy way to check if this processing path is the same as an attempted new path
*
* @param positions - the positions to compare against
* @return true if we are processing the same positions
*/
public boolean hasSameProcessingPositions(final Set<BlockPos> positions) {
if (this.positions.size() != positions.size()) {
return false;
}
return this.positions.containsAll(positions);
}
/**
* starts processing this path
*/
public synchronized void process() {
if (this.processState == PathProcessState.COMPLETED ||
this.processState == PathProcessState.PROCESSING) {
return;
}
processState = PathProcessState.PROCESSING;
final Path bestPath = this.pathSupplier.get();
this.nodes.addAll(bestPath.nodes); // we mutate this list to reuse the logic in Path
this.target = bestPath.getTarget();
this.distToTarget = bestPath.getDistToTarget();
this.canReach = bestPath.canReach();
processState = PathProcessState.COMPLETED;
for (Runnable runnable : this.postProcessing) {
runnable.run();
} // Run tasks after processing
}
/**
* if this path is accessed while it hasn't processed, just process it in-place
*/
private void checkProcessed() {
if (this.processState == PathProcessState.WAITING ||
this.processState == PathProcessState.PROCESSING) { // Block if we are on processing
this.process();
}
}
/*
* overrides we need for final fields that we cannot modify after processing
*/
@Override
public @NotNull BlockPos getTarget() {
this.checkProcessed();
return this.target;
}
@Override
public float getDistToTarget() {
this.checkProcessed();
return this.distToTarget;
}
@Override
public boolean canReach() {
this.checkProcessed();
return this.canReach;
}
/*
* overrides to ensure we're processed first
*/
@Override
public boolean isDone() {
return this.processState == PathProcessState.COMPLETED && super.isDone();
}
@Override
public void advance() {
this.checkProcessed();
super.advance();
}
@Override
public boolean notStarted() {
this.checkProcessed();
return super.notStarted();
}
@Nullable
@Override
public Node getEndNode() {
this.checkProcessed();
return super.getEndNode();
}
@Override
public Node getNode(int index) {
this.checkProcessed();
return super.getNode(index);
}
@Override
public void truncateNodes(int length) {
this.checkProcessed();
super.truncateNodes(length);
}
@Override
public void replaceNode(int index, Node node) {
this.checkProcessed();
super.replaceNode(index, node);
}
@Override
public int getNodeCount() {
this.checkProcessed();
return super.getNodeCount();
}
@Override
public int getNextNodeIndex() {
this.checkProcessed();
return super.getNextNodeIndex();
}
@Override
public void setNextNodeIndex(int nodeIndex) {
this.checkProcessed();
super.setNextNodeIndex(nodeIndex);
}
@Override
public Vec3 getEntityPosAtNode(Entity entity, int index) {
this.checkProcessed();
return super.getEntityPosAtNode(entity, index);
}
@Override
public BlockPos getNodePos(int index) {
this.checkProcessed();
return super.getNodePos(index);
}
@Override
public Vec3 getNextEntityPos(Entity entity) {
this.checkProcessed();
return super.getNextEntityPos(entity);
}
@Override
public BlockPos getNextNodePos() {
this.checkProcessed();
return super.getNextNodePos();
}
@Override
public Node getNextNode() {
this.checkProcessed();
return super.getNextNode();
}
@Nullable
@Override
public Node getPreviousNode() {
this.checkProcessed();
return super.getPreviousNode();
}
public PathProcessState getProcessState() {
return processState;
}
}

View File

@@ -0,0 +1,51 @@
package org.dreeam.leaf.async.path;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import net.minecraft.server.MinecraftServer;
import net.minecraft.world.level.pathfinder.Path;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.concurrent.*;
import java.util.function.Consumer;
/**
* used to handle the scheduling of async path processing
*/
public class AsyncPathProcessor {
private static final Executor pathProcessingExecutor = new ThreadPoolExecutor(
1,
org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingMaxThreads,
org.dreeam.leaf.config.modules.async.AsyncPathfinding.asyncPathfindingKeepalive, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setNameFormat("Leaf Async Pathfinding Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build()
);
protected static CompletableFuture<Void> queue(@NotNull AsyncPath path) {
return CompletableFuture.runAsync(path::process, pathProcessingExecutor);
}
/**
* takes a possibly unprocessed path, and waits until it is completed
* the consumer will be immediately invoked if the path is already processed
* the consumer will always be called on the main thread
*
* @param path a path to wait on
* @param afterProcessing a consumer to be called
*/
public static void awaitProcessing(@Nullable Path path, Consumer<@Nullable Path> afterProcessing) {
if (path != null && !path.isProcessed() && path instanceof AsyncPath asyncPath) {
asyncPath.postProcessing(() ->
MinecraftServer.getServer().scheduleOnMain(() -> afterProcessing.accept(path))
);
} else {
afterProcessing.accept(path);
}
}
}

View File

@@ -0,0 +1,45 @@
package org.dreeam.leaf.async.path;
import ca.spottedleaf.concurrentutil.collection.MultiThreadedQueue;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import org.apache.commons.lang.Validate;
import org.jetbrains.annotations.NotNull;
import java.util.Map;
import java.util.Queue;
import java.util.concurrent.ConcurrentHashMap;
public class NodeEvaluatorCache {
private static final Map<NodeEvaluatorFeatures, MultiThreadedQueue<NodeEvaluator>> threadLocalNodeEvaluators = new ConcurrentHashMap<>();
private static final Map<NodeEvaluator, NodeEvaluatorGenerator> nodeEvaluatorToGenerator = new ConcurrentHashMap<>();
private static @NotNull Queue<NodeEvaluator> getQueueForFeatures(@NotNull NodeEvaluatorFeatures nodeEvaluatorFeatures) {
return threadLocalNodeEvaluators.computeIfAbsent(nodeEvaluatorFeatures, key -> new MultiThreadedQueue<>());
}
public static @NotNull NodeEvaluator takeNodeEvaluator(@NotNull NodeEvaluatorGenerator generator, @NotNull NodeEvaluator localNodeEvaluator) {
final NodeEvaluatorFeatures nodeEvaluatorFeatures = NodeEvaluatorFeatures.fromNodeEvaluator(localNodeEvaluator);
NodeEvaluator nodeEvaluator = getQueueForFeatures(nodeEvaluatorFeatures).poll();
if (nodeEvaluator == null) {
nodeEvaluator = generator.generate(nodeEvaluatorFeatures);
}
nodeEvaluatorToGenerator.put(nodeEvaluator, generator);
return nodeEvaluator;
}
public static void returnNodeEvaluator(@NotNull NodeEvaluator nodeEvaluator) {
final NodeEvaluatorGenerator generator = nodeEvaluatorToGenerator.remove(nodeEvaluator);
Validate.notNull(generator, "NodeEvaluator already returned");
final NodeEvaluatorFeatures nodeEvaluatorFeatures = NodeEvaluatorFeatures.fromNodeEvaluator(nodeEvaluator);
getQueueForFeatures(nodeEvaluatorFeatures).offer(nodeEvaluator);
}
public static void removeNodeEvaluator(@NotNull NodeEvaluator nodeEvaluator) {
nodeEvaluatorToGenerator.remove(nodeEvaluator);
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import net.minecraft.world.level.pathfinder.SwimNodeEvaluator;
public record NodeEvaluatorFeatures(
NodeEvaluatorType type,
boolean canPassDoors,
boolean canFloat,
boolean canWalkOverFences,
boolean canOpenDoors,
boolean allowBreaching
) {
public static NodeEvaluatorFeatures fromNodeEvaluator(NodeEvaluator nodeEvaluator) {
NodeEvaluatorType type = NodeEvaluatorType.fromNodeEvaluator(nodeEvaluator);
boolean canPassDoors = nodeEvaluator.canPassDoors();
boolean canFloat = nodeEvaluator.canFloat();
boolean canWalkOverFences = nodeEvaluator.canWalkOverFences();
boolean canOpenDoors = nodeEvaluator.canOpenDoors();
boolean allowBreaching = nodeEvaluator instanceof SwimNodeEvaluator swimNodeEvaluator && swimNodeEvaluator.allowBreaching;
return new NodeEvaluatorFeatures(type, canPassDoors, canFloat, canWalkOverFences, canOpenDoors, allowBreaching);
}
}

View File

@@ -0,0 +1,11 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.NodeEvaluator;
import org.jetbrains.annotations.NotNull;
public interface NodeEvaluatorGenerator {
@NotNull
NodeEvaluator generate(NodeEvaluatorFeatures nodeEvaluatorFeatures);
}

View File

@@ -0,0 +1,17 @@
package org.dreeam.leaf.async.path;
import net.minecraft.world.level.pathfinder.*;
public enum NodeEvaluatorType {
WALK,
SWIM,
AMPHIBIOUS,
FLY;
public static NodeEvaluatorType fromNodeEvaluator(NodeEvaluator nodeEvaluator) {
if (nodeEvaluator instanceof SwimNodeEvaluator) return SWIM;
if (nodeEvaluator instanceof FlyNodeEvaluator) return FLY;
if (nodeEvaluator instanceof AmphibiousNodeEvaluator) return AMPHIBIOUS;
return WALK;
}
}

View File

@@ -0,0 +1,7 @@
package org.dreeam.leaf.async.path;
public enum PathProcessState {
WAITING,
PROCESSING,
COMPLETED,
}

View File

@@ -0,0 +1,140 @@
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 ca.spottedleaf.moonrise.patches.entity_tracker.EntityTrackerTrackedEntity;
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 java.util.concurrent.Executor;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MultithreadedTracker {
private static final Logger LOGGER = LogManager.getLogger("MultithreadedTracker");
public static class MultithreadedTrackerThread extends Thread {
@Override
public void run() {
super.run();
}
}
private static final Executor trackerExecutor = new ThreadPoolExecutor(
1,
org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerMaxThreads,
org.dreeam.leaf.config.modules.async.MultithreadedTracker.asyncEntityTrackerKeepalive, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(),
new ThreadFactoryBuilder()
.setThreadFactory(
r -> new MultithreadedTrackerThread() {
@Override
public void run() {
r.run();
}
}
)
.setNameFormat("Leaf Async Tracker Thread - %d")
.setPriority(Thread.NORM_PRIORITY - 2)
.build());
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;
((EntityTrackerTrackedEntity) 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;
((EntityTrackerTrackedEntity) 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();
}
}
}
}

View File

@@ -0,0 +1,57 @@
package org.dreeam.leaf.config;
import org.dreeam.leaf.config.annotations.Experimental;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Modifier;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
public abstract class ConfigModules extends LeafConfig {
private static final Set<ConfigModules> modules = new HashSet<>();
public LeafGlobalConfig config;
public ConfigModules() {
this.config = LeafConfig.config();
}
public static void initModules() throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
List<Class<?>> enabledExperimentalModules = new ArrayList<>();
for (Class<?> clazz : LeafConfig.getClasses(LeafConfig.I_CONFIG_PKG)) {
ConfigModules module = (ConfigModules) clazz.getConstructor().newInstance();
module.onLoaded();
modules.add(module);
for (Field field : getAnnotatedStaticFields(clazz, Experimental.class)) {
Object obj = field.get(null);
if (!(obj instanceof Boolean)) continue;
boolean enabled = (Boolean) obj;
if (enabled) {
enabledExperimentalModules.add(clazz);
break;
}
}
}
if (!enabledExperimentalModules.isEmpty()) {
LeafConfig.LOGGER.warn("You have following experimental module(s) enabled: {}, please report any bugs you found!", enabledExperimentalModules.stream().map(Class::getSimpleName).toList());
}
}
private static List<Field> getAnnotatedStaticFields(Class<?> clazz, Class<? extends Annotation> annotation) {
List<Field> fields = new ArrayList<>();
for (Field field : clazz.getDeclaredFields()) {
if (field.isAnnotationPresent(annotation) && Modifier.isStatic(field.getModifiers())) {
field.setAccessible(true);
fields.add(field);
}
}
return fields;
}
public abstract void onLoaded();
}

View File

@@ -0,0 +1,26 @@
package org.dreeam.leaf.config;
public enum EnumConfigCategory {
ASYNC("async"),
PERF("performance"),
FIXES("fixes"),
GAMEPLAY("gameplay-mechanisms"),
NETWORK("network"),
MISC("misc");
private final String baseKeyName;
private static final EnumConfigCategory[] VALUES = EnumConfigCategory.values();
EnumConfigCategory(String baseKeyName) {
this.baseKeyName = baseKeyName;
}
public String getBaseKeyName() {
return this.baseKeyName;
}
public static EnumConfigCategory[] getCategoryValues() {
return VALUES;
}
}

View File

@@ -0,0 +1,271 @@
package org.dreeam.leaf.config;
import io.papermc.paper.configuration.GlobalConfiguration;
import org.dreeam.leaf.config.modules.misc.SentryDSN;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.io.File;
import java.io.IOException;
import java.net.JarURLConnection;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.StandardCharsets;
import java.nio.file.FileAlreadyExistsException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.Enumeration;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import org.bukkit.Bukkit;
import org.bukkit.World;
/*
* Yoinked from: https://github.com/xGinko/AnarchyExploitFixes/ & https://github.com/LuminolMC/Luminol
* @author: @xGinko & @MrHua269
*/
public class LeafConfig {
public static final Logger LOGGER = LogManager.getLogger(LeafConfig.class.getSimpleName());
protected static final File I_CONFIG_FOLDER = new File("config");
protected static final String I_CONFIG_PKG = "org.dreeam.leaf.config.modules";
protected static final String I_GLOBAL_CONFIG_FILE = "leaf-global.yml";
protected static final String I_LEVEL_CONFIG_FILE = "leaf-world-defaults.yml"; // Leaf TODO - Per level config
private static LeafGlobalConfig leafGlobalConfig;
/* Load & Reload */
public static void reload() {
try {
long begin = System.nanoTime();
LOGGER.info("Reloading config...");
loadConfig(false);
LOGGER.info("Successfully reloaded config in {}ms.", (System.nanoTime() - begin) / 1_000_000);
} catch (Exception e) {
LOGGER.error("Failed to reload config.", e);
}
}
@Contract(" -> new")
public static @NotNull CompletableFuture<Void> reloadAsync() {
return new CompletableFuture<>();
}
public static void loadConfig() {
try {
long begin = System.nanoTime();
LOGGER.info("Loading config...");
purgeOutdated();
loadConfig(true);
LOGGER.info("Successfully loaded config in {}ms.", (System.nanoTime() - begin) / 1_000_000);
} catch (Exception e) {
LeafConfig.LOGGER.error("Failed to load config modules!", e);
}
}
/* Load Global Config */
private static void loadConfig(boolean init) throws Exception {
// Create config folder
createDirectory(LeafConfig.I_CONFIG_FOLDER);
leafGlobalConfig = new LeafGlobalConfig(init);
// Load config modules
ConfigModules.initModules();
// Save config to disk
leafGlobalConfig.saveConfig();
}
public static LeafGlobalConfig config() {
return leafGlobalConfig;
}
/* Create config folder */
protected static void createDirectory(File dir) throws IOException {
try {
Files.createDirectories(dir.toPath());
} catch (FileAlreadyExistsException e) { // Thrown if dir exists but is not a directory
if (dir.delete()) createDirectory(dir);
}
}
/* Scan classes under package */
public static @NotNull Set<Class<?>> getClasses(String pack) {
Set<Class<?>> classes = new LinkedHashSet<>();
String packageDirName = pack.replace('.', '/');
Enumeration<URL> dirs;
try {
dirs = Thread.currentThread().getContextClassLoader().getResources(packageDirName);
while (dirs.hasMoreElements()) {
URL url = dirs.nextElement();
String protocol = url.getProtocol();
if ("file".equals(protocol)) {
String filePath = URLDecoder.decode(url.getFile(), StandardCharsets.UTF_8);
findClassesInPackageByFile(pack, filePath, classes);
} else if ("jar".equals(protocol)) {
JarFile jar;
try {
jar = ((JarURLConnection) url.openConnection()).getJarFile();
Enumeration<JarEntry> entries = jar.entries();
findClassesInPackageByJar(pack, entries, packageDirName, classes);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
} catch (IOException e) {
throw new RuntimeException(e);
}
return classes;
}
private static void findClassesInPackageByFile(String packageName, String packagePath, Set<Class<?>> classes) {
File dir = new File(packagePath);
if (!dir.exists() || !dir.isDirectory()) {
return;
}
File[] dirfiles = dir.listFiles((file) -> file.isDirectory() || file.getName().endsWith(".class"));
if (dirfiles != null) {
for (File file : dirfiles) {
if (file.isDirectory()) {
findClassesInPackageByFile(packageName + "." + file.getName(), file.getAbsolutePath(), classes);
} else {
String className = file.getName().substring(0, file.getName().length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
}
private static void findClassesInPackageByJar(String packageName, Enumeration<JarEntry> entries, String packageDirName, Set<Class<?>> classes) {
while (entries.hasMoreElements()) {
JarEntry entry = entries.nextElement();
String name = entry.getName();
if (name.charAt(0) == '/') {
name = name.substring(1);
}
if (name.startsWith(packageDirName)) {
int idx = name.lastIndexOf('/');
if (idx != -1) {
packageName = name.substring(0, idx).replace('/', '.');
}
if (name.endsWith(".class") && !entry.isDirectory()) {
String className = name.substring(packageName.length() + 1, name.length() - 6);
try {
classes.add(Class.forName(packageName + '.' + className));
} catch (ClassNotFoundException e) {
throw new RuntimeException(e);
}
}
}
}
}
/* Register Spark profiler extra server configurations */
private static List<String> buildSparkExtraConfigs() {
List<String> extraConfigs = new ArrayList<>(Arrays.asList(
"config/leaf-global.yml",
"config/gale-global.yml",
"config/gale-world-defaults.yml"
));
for (World world : Bukkit.getWorlds()) {
extraConfigs.add(world.getWorldFolder().getName() + "/gale-world.yml"); // Gale world config
}
return extraConfigs;
}
private static String[] buildSparkHiddenPaths() {
return new String[]{
SentryDSN.sentryDsnConfigPath // Hide Sentry DSN key
};
}
public static void regSparkExtraConfig() {
if (GlobalConfiguration.get().spark.enabled || Bukkit.getServer().getPluginManager().getPlugin("spark") != null) {
String extraConfigs = String.join(",", buildSparkExtraConfigs());
String hiddenPaths = String.join(",", buildSparkHiddenPaths());
System.setProperty("spark.serverconfigs.extra", extraConfigs);
System.setProperty("spark.serverconfigs.hiddenpaths", hiddenPaths);
}
}
/* Purge and backup old Leaf config & Pufferfish config */
private static void purgeOutdated() {
boolean foundLegacy = false;
String pufferfishConfig = "pufferfish.yml";
String leafConfigV1 = "leaf.yml";
String leafConfigV2 = "leaf_config";
Date date = new Date();
SimpleDateFormat dateFormat = new SimpleDateFormat("yyMMddhhmmss");
String backupDir = "config/backup" + dateFormat.format(date) + "/";
File pufferfishConfigFile = new File(pufferfishConfig);
File leafConfigV1File = new File(leafConfigV1);
File leafConfigV2File = new File(leafConfigV2);
File backupDirFile = new File(backupDir);
try {
if (pufferfishConfigFile.exists() && pufferfishConfigFile.isFile()) {
createDirectory(backupDirFile);
Files.move(pufferfishConfigFile.toPath(), Path.of(backupDir + pufferfishConfig), StandardCopyOption.REPLACE_EXISTING);
foundLegacy = true;
}
if (leafConfigV1File.exists() && leafConfigV1File.isFile()) {
createDirectory(backupDirFile);
Files.move(leafConfigV1File.toPath(), Path.of(backupDir + leafConfigV1), StandardCopyOption.REPLACE_EXISTING);
foundLegacy = true;
}
if (leafConfigV2File.exists() && leafConfigV2File.isDirectory()) {
createDirectory(backupDirFile);
Files.move(leafConfigV2File.toPath(), Path.of(backupDir + leafConfigV2), StandardCopyOption.REPLACE_EXISTING);
foundLegacy = true;
}
if (foundLegacy) {
LOGGER.warn("Found legacy Leaf config files, move to backup directory: {}", backupDir);
LOGGER.warn("New Leaf config located at config/ folder, You need to transfer config to the new one manually and restart the server!");
}
} catch (IOException e) {
LOGGER.error("Failed to purge old configs.", e);
}
}
}

View File

@@ -0,0 +1,137 @@
package org.dreeam.leaf.config;
import io.github.thatsmusic99.configurationmaster.api.ConfigFile;
import io.github.thatsmusic99.configurationmaster.api.ConfigSection;
import java.io.File;
import java.util.List;
import java.util.Locale;
import java.util.Map;
public class LeafGlobalConfig {
protected static ConfigFile configFile;
private static final String CURRENT_REGION = Locale.getDefault().getCountry().toUpperCase(Locale.ROOT); // It will be in uppercase by default, just make sure
protected static final boolean isCN = CURRENT_REGION.equals("CN");
public LeafGlobalConfig(boolean init) throws Exception {
configFile = ConfigFile.loadConfig(new File(LeafConfig.I_CONFIG_FOLDER, LeafConfig.I_GLOBAL_CONFIG_FILE));
configFile.set("config-version", 3.0);
configFile.addComments("config-version", pickStringRegionBased("""
Leaf Config
GitHub Repo: https://github.com/Winds-Studio/Leaf
Discord: https://discord.com/invite/gfgAwdSEuM""",
"""
Leaf Config
GitHub Repo: https://github.com/Winds-Studio/Leaf
QQ Group: 619278377"""));
// Pre-structure to force order
structureConfig();
}
protected void structureConfig() {
for (EnumConfigCategory configCate : EnumConfigCategory.getCategoryValues()) {
createTitledSection(configCate.name(), configCate.getBaseKeyName());
}
}
public void saveConfig() throws Exception {
configFile.save();
}
// Config Utilities
public void createTitledSection(String title, String path) {
configFile.addSection(title);
configFile.addDefault(path, null);
}
public boolean getBoolean(String path, boolean def, String comment) {
configFile.addDefault(path, def, comment);
return configFile.getBoolean(path, def);
}
public boolean getBoolean(String path, boolean def) {
configFile.addDefault(path, def);
return configFile.getBoolean(path, def);
}
public String getString(String path, String def, String comment) {
configFile.addDefault(path, def, comment);
return configFile.getString(path, def);
}
public String getString(String path, String def) {
configFile.addDefault(path, def);
return configFile.getString(path, def);
}
public double getDouble(String path, double def, String comment) {
configFile.addDefault(path, def, comment);
return configFile.getDouble(path, def);
}
public double getDouble(String path, double def) {
configFile.addDefault(path, def);
return configFile.getDouble(path, def);
}
public int getInt(String path, int def, String comment) {
configFile.addDefault(path, def, comment);
return configFile.getInteger(path, def);
}
public int getInt(String path, int def) {
configFile.addDefault(path, def);
return configFile.getInteger(path, def);
}
public List<String> getList(String path, List<String> def, String comment) {
configFile.addDefault(path, def, comment);
return configFile.getStringList(path);
}
public List<String> getList(String path, List<String> def) {
configFile.addDefault(path, def);
return configFile.getStringList(path);
}
public ConfigSection getConfigSection(String path, Map<String, Object> defaultKeyValue) {
configFile.addDefault(path, null);
configFile.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> configFile.addExample(path + "." + string, object));
return configFile.getConfigSection(path);
}
public ConfigSection getConfigSection(String path, Map<String, Object> defaultKeyValue, String comment) {
configFile.addDefault(path, null, comment);
configFile.makeSectionLenient(path);
defaultKeyValue.forEach((string, object) -> configFile.addExample(path + "." + string, object));
return configFile.getConfigSection(path);
}
public void addComment(String path, String comment) {
configFile.addComment(path, comment);
}
public void addCommentIfCN(String path, String comment) {
if (isCN) {
configFile.addComment(path, comment);
}
}
public void addCommentIfNonCN(String path, String comment) {
if (!isCN) {
configFile.addComment(path, comment);
}
}
public void addCommentRegionBased(String path, String en, String cn) {
configFile.addComment(path, isCN ? cn : en);
}
public String pickStringRegionBased(String en, String cn) {
return isCN ? cn : en;
}
}

View File

@@ -0,0 +1,8 @@
package org.dreeam.leaf.config.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface DoNotLoad {
}

View File

@@ -0,0 +1,12 @@
package org.dreeam.leaf.config.annotations;
import java.lang.annotation.*;
/**
* Indicates that a feature is experimental and may be removed or changed in the future.
*/
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.FIELD})
public @interface Experimental {
}

View File

@@ -0,0 +1,8 @@
package org.dreeam.leaf.config.annotations;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
@Retention(RetentionPolicy.RUNTIME)
public @interface HotReloadUnsupported {
}

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,32 @@
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 AsyncPathfinding extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.ASYNC.getBaseKeyName() + ".async-pathfinding";
}
public static boolean enabled = false;
public static int asyncPathfindingMaxThreads = 0;
public static int asyncPathfindingKeepalive = 60;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
asyncPathfindingMaxThreads = config.getInt(getBasePath() + ".max-threads", asyncPathfindingMaxThreads);
asyncPathfindingKeepalive = config.getInt(getBasePath() + ".keepalive", asyncPathfindingKeepalive);
if (asyncPathfindingMaxThreads < 0)
asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() + asyncPathfindingMaxThreads, 1);
else if (asyncPathfindingMaxThreads == 0)
asyncPathfindingMaxThreads = Math.max(Runtime.getRuntime().availableProcessors() / 4, 1);
if (!enabled)
asyncPathfindingMaxThreads = 0;
else
LeafConfig.LOGGER.info("Using {} threads for Async Pathfinding", asyncPathfindingMaxThreads);
}
}

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,48 @@
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;
@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);
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);
}
}

View File

@@ -0,0 +1,25 @@
package org.dreeam.leaf.config.modules.fixes;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class DontPlacePlayerIfFull extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.FIXES.getBaseKeyName();
}
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".dont-place-player-if-server-full", enabled, config.pickStringRegionBased("""
Don't let player join server if the server is full.
If enable this, you should use 'purpur.joinfullserver' permission instead of
PlayerLoginEvent#allow to let player join full server.""",
"""
服务器已满时禁止玩家加入.
开启后需使用权限 'purpur.joinfullserver' 而不是
PlayerLoginEvent#allow 让玩家进入已满的服务器."""));
}
}

View File

@@ -0,0 +1,28 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ConfigurableMaxUseItemDistance extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".player";
}
public static double maxUseItemDistance = 1.0000001D;
@Override
public void onLoaded() {
maxUseItemDistance = config.getDouble(getBasePath() + ".max-use-item-distance", maxUseItemDistance, config.pickStringRegionBased("""
The max distance of UseItem for players.
Set to -1 to disable max-distance-check.
NOTE: if set to -1 to disable the check,
players are able to use some packet modules of hack clients,
and NoCom Exploit!!""",
"""
玩家 UseItem 的最大距离.
设置为 -1 来禁用最大距离检测.
注意: 禁用此项后,
玩家可以使用作弊客户端的部分发包模块和 NoCom 漏洞!!"""));
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ConfigurableTripWireDupe extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName();
}
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".allow-tripwire-dupe", enabled);
}
}

View File

@@ -0,0 +1,22 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class DisableMovedWronglyThreshold extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".player";
}
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".disable-moved-wrongly-threshold", enabled,
config.pickStringRegionBased(
"Disable moved quickly/wrongly checks.",
"关闭 moved wrongly/too quickly! 警告."
));
}
}

View File

@@ -0,0 +1,34 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class Knockback extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".knockback";
}
public static boolean snowballCanKnockback = false;
public static boolean eggCanKnockback = false;
public static boolean canPlayerKnockbackZombie = true;
@Override
public void onLoaded() {
snowballCanKnockback = config.getBoolean(getBasePath() + ".snowball-knockback-players", snowballCanKnockback,
config.pickStringRegionBased(
"Make snowball can knockback players.",
"使雪球可以击退玩家."
));
eggCanKnockback = config.getBoolean(getBasePath() + ".egg-knockback-players", eggCanKnockback,
config.pickStringRegionBased(
"Make egg can knockback players.",
"使鸡蛋可以击退玩家."
));
canPlayerKnockbackZombie = config.getBoolean(getBasePath() + ".can-player-knockback-zombie", canPlayerKnockbackZombie,
config.pickStringRegionBased(
"Make players can knockback zombie.",
"使玩家可以击退僵尸."
));
}
}

View File

@@ -0,0 +1,27 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class MaxItemsStackCount extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".max-item-stack-count";
}
public static int maxItemStackCount = 0;
public static int maxContainerDestroyCount = 0;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(),
"Don't touch this unless you know what you are doing!",
"不要动该项, 除非你知道自己在做什么!");
maxItemStackCount = config.getInt(getBasePath() + ".max-dropped-items-stack-count", maxItemStackCount);
maxContainerDestroyCount = config.getInt(getBasePath() + ".max-container-destroy-count", maxContainerDestroyCount);
if (maxItemStackCount < 0) maxItemStackCount = 0;
if (maxContainerDestroyCount < 0) maxContainerDestroyCount = 0;
}
}

View File

@@ -0,0 +1,29 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.Experimental;
public class SmoothTeleport extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".smooth-teleport";
}
@Experimental
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath(), enabled, config.pickStringRegionBased(
"""
**Experimental feature, report any bugs you encounter!**
Whether to make a "smooth teleport" when players changing dimension.
This requires original world and target world have same logical height to work.""",
"""
**实验性功能, 请及时反馈你遇到的问题!**
是否在玩家切换世界时尝试使用 "平滑传送".
此项要求源世界和目标世界逻辑高度相同才会生效."""
));
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.gameplay;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class UseSpigotItemMergingMech extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.GAMEPLAY.getBaseKeyName() + ".use-spigot-item-merging-mechanism";
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath(), enabled);
}
}

View File

@@ -0,0 +1,29 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class Cache extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".cache";
}
public static boolean cachePlayerProfileResult = true;
public static int cachePlayerProfileResultTimeout = 1440;
@Override
public void onLoaded() {
cachePlayerProfileResult = config.getBoolean(getBasePath() + ".cache-player-profile-result", cachePlayerProfileResult, config.pickStringRegionBased("""
Cache the player profile result on they first join.
It's useful if Mojang's verification server is down.""",
"""
玩家首次加入时缓存 PlayerProfile.
正版验证服务器宕机时非常有用."""));
cachePlayerProfileResultTimeout = config.getInt(getBasePath() + ".cache-player-profile-result-timeout", cachePlayerProfileResultTimeout,
config.pickStringRegionBased(
"The timeout of the cache. Unit: Minutes.",
"缓存过期时间. 单位: 分钟."
));
}
}

View File

@@ -0,0 +1,41 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ConnectionMessage extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".connection-message";
}
public static boolean joinEnabled = true;
public static String joinMessage = "default";
public static boolean quitEnabled = true;
public static String quitMessage = "default";
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Connection message, using MiniMessage format, set to "default" to use vanilla join message.
available placeholders:
%player_name% - player name
%player_displayname% - player display name""",
"""
自定义加入 & 退出消息 (MiniMessage 格式), 设置为 'default' 将使用原版消息.
可用的内置变量:
%player_name% - 玩家名称
%player_displayname% - 玩家显示名称""");
joinEnabled = config.getBoolean(getBasePath() + ".join.enabled", joinEnabled);
joinMessage = config.getString(getBasePath() + ".join.message", joinMessage, config.pickStringRegionBased(
"Join message of player",
"玩家加入服务器时的消息"
));
quitEnabled = config.getBoolean(getBasePath() + ".quit.enabled", quitEnabled);
quitMessage = config.getString(getBasePath() + ".quit.message", quitMessage, config.pickStringRegionBased(
"Quit message of player",
"玩家退出服务器时的消息"));
}
}

View File

@@ -0,0 +1,60 @@
package org.dreeam.leaf.config.modules.misc;
import net.minecraft.core.Holder;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
public class HiddenItemComponents extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName();
}
public static List<DataComponentType<?>> hiddenItemComponentTypes = List.of();
@Override
public void onLoaded() {
List<String> list = config.getList(getBasePath() + ".hidden-item-components", new ArrayList<>(), config.pickStringRegionBased("""
Controls whether specified component information is sent to clients.
This may break resource packs and mods that rely on this information.
It needs a component type list, incorrect things will not work.
You can fill it with ["custom_data"] to hide components of CUSTOM_DATA.
Also, it can avoid some frequent client animations.
NOTICE: You must know what you're filling in and how it works! It will handle all itemStacks!""",
"""
控制哪些物品组件信息会被发送至客户端.
可能会导致依赖物品组件的资源包/模组无法正常工作.
该配置项接受一个物品组件列表, 格式不正确将不会启用.
可以填入 ["custom_data"] 来隐藏自定义数据物品组件 CUSTOM_DATA.
也可以避免一些客户端动画效果.
注意: 你必须知道你填进去的是什么, 有什么用, 该项配置会处理所有的ItemStack!"""));
List<DataComponentType<?>> types = new ArrayList<>(list.size());
for (String id : list) {
// Find and check
Optional<Holder.Reference<DataComponentType<?>>> optional = BuiltInRegistries.DATA_COMPONENT_TYPE.get(ResourceLocation.parse(id));
if (optional.isEmpty()) continue;
DataComponentType<?> type = optional.get().value();
if (type != null) {
types.add(type);
} else {
LeafConfig.LOGGER.warn("Unknown component type: {}", id);
}
}
hiddenItemComponentTypes = types;
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class Including5sIngetTPS extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".including-5s-in-get-tps", enabled);
}
}

View File

@@ -0,0 +1,29 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class LagCompensation extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".lag-compensation";
}
public static boolean enabled = false;
public static boolean enableForWater = false;
public static boolean enableForLava = false;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
This section contains lag compensation features,
which could ensure basic playing experience during a lag.""",
"""
这部分包含滞后补偿功能,
可以在卡顿情况下保障基本游戏体验.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
enableForWater = config.getBoolean(getBasePath() + ".enable-for-water", enableForWater);
enableForLava = config.getBoolean(getBasePath() + ".enable-for-lava", enableForLava);
}
}

View File

@@ -0,0 +1,62 @@
package org.dreeam.leaf.config.modules.misc;
import com.mojang.logging.LogUtils;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.annotations.DoNotLoad;
import org.slf4j.Logger;
import org.stupidcraft.linearpaper.region.EnumRegionFileExtension;
public class RegionFormatConfig extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".region-format-settings";
}
@DoNotLoad
private static final Logger logger = LogUtils.getLogger();
@DoNotLoad
public static EnumRegionFileExtension regionFormatType;
public static String regionFormatTypeName = "MCA";
public static int linearCompressionLevel = 1;
public static boolean throwOnUnknownExtension = false;
public static int linearFlushFrequency = 5;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Linear is a region file format that uses ZSTD compression instead of
ZLIB.
This format saves about 50% of disk space.
Read Documentation before using: https://github.com/xymb-endcrystalme/LinearRegionFileFormatTools
Disclaimer: This is an experimental feature, there is potential risk to lose chunk data.
So backup your server before switching to Linear.""",
"""
Linear 是一种使用 ZSTD 压缩而非 ZLIB 的区域文件格式.
该格式可节省约 50% 的磁盘空间.
使用前请阅读文档: https://github.com/xymb-endcrystalme/LinearRegionFileFormatTools
免责声明: 实验性功能,有可能导致区块数据丢失.
切换到Linear前请备份服务器.""");
regionFormatTypeName = config.getString(getBasePath() + ".region-format", regionFormatTypeName,
config.pickStringRegionBased(
"Available region formats: MCA, LINEAR",
"可用格式: MCA, LINEAR"));
linearCompressionLevel = config.getInt(getBasePath() + ".linear-compress-level", linearCompressionLevel);
throwOnUnknownExtension = config().getBoolean(getBasePath() + ".throw-on-unknown-extension-detected", throwOnUnknownExtension);
linearFlushFrequency = config.getInt(getBasePath() + ".flush-interval-seconds", linearFlushFrequency);
regionFormatType = EnumRegionFileExtension.fromName(regionFormatTypeName);
if (regionFormatType == EnumRegionFileExtension.UNKNOWN) {
logger.error("Unknown region file type {} ! Falling back to MCA format.", regionFormatTypeName);
regionFormatType = EnumRegionFileExtension.MCA;
}
if (linearCompressionLevel > 23 || linearCompressionLevel < 1) {
logger.error("Linear region compression level should be between 1 and 22 in config: {}", linearCompressionLevel);
logger.error("Falling back to compression level 1.");
linearCompressionLevel = 1;
}
}
}

View File

@@ -0,0 +1,22 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class RemoveChangeNonEditableSignWarning extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName();
}
public static boolean enabled = false;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".remove-change-non-editable-sign-warning", enabled,
config.pickStringRegionBased(
"Enable to prevent console spam.",
"移除修改无法编辑的告示牌时输出的警告."
));
}
}

View File

@@ -0,0 +1,22 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class RemoveSpigotCheckBungee extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".remove-spigot-check-bungee-config";
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath(), enabled, config.pickStringRegionBased("""
Enable player enter backend server through proxy
without backend server enabling its bungee mode.""",
"""
使服务器无需打开 bungee 模式即可让玩家加入后端服务器."""));
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class RemoveVanillaUsernameCheck extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".remove-vanilla-username-check";
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath(), enabled, config.pickStringRegionBased("""
Remove Vanilla username check,
allowing all characters as username.""",
"""
移除原版的用户名验证,
让所有字符均可作为玩家名."""));
}
}

View File

@@ -0,0 +1,24 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class SecureSeed extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".secure-seed";
}
public static boolean enabled = false;
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Once you enable secure seed, all ores and structures are generated with 1024-bit seed
instead of using 64-bit seed in vanilla, made seed cracker become impossible.""",
"""
安全种子开启后, 所有矿物与结构都将使用1024位的种子进行生成, 无法被破解.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
}
}

View File

@@ -0,0 +1,43 @@
package org.dreeam.leaf.config.modules.misc;
import org.apache.logging.log4j.Level;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class SentryDSN extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".sentry";
}
public static String sentryDsnConfigPath;
public static String sentryDsn = "";
public static String logLevel = "WARN";
public static boolean onlyLogThrown = true;
@Override
public void onLoaded() {
String sentryEnvironment = System.getenv("SENTRY_DSN");
String sentryConfig = config.getString(sentryDsnConfigPath = getBasePath() + ".dsn", sentryDsn, config.pickStringRegionBased("""
Sentry DSN for improved error logging, leave blank to disable,
Obtain from https://sentry.io/""",
"""
Sentry DSN (出现严重错误时将发送至配置的Sentry DSN地址) (留空关闭)"""));
sentryDsn = sentryEnvironment == null
? sentryConfig
: sentryEnvironment;
logLevel = config.getString(getBasePath() + ".log-level", logLevel, config.pickStringRegionBased("""
Logs with a level higher than or equal to this level will be recorded.""",
"""
大于等于该等级的日志会被记录."""));
onlyLogThrown = config.getBoolean(getBasePath() + ".only-log-thrown", onlyLogThrown, config.pickStringRegionBased("""
Only log with a Throwable will be recorded after enabling this.""",
"""
是否仅记录带有 Throwable 的日志."""));
if (sentryDsn != null && !sentryDsn.isBlank()) {
gg.pufferfish.pufferfish.sentry.SentryManager.init(Level.getLevel(logLevel));
}
}
}

View File

@@ -0,0 +1,20 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ServerBrand extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".rebrand";
}
public static String serverModName = io.papermc.paper.ServerBuildInfo.buildInfo().brandName();
public static String serverGUIName = "Leaf Console";
@Override
public void onLoaded() {
serverModName = config.getString(getBasePath() + ".server-mod-name", serverModName);
serverGUIName = config.getString(getBasePath() + ".server-gui-name", serverGUIName);
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.config.modules.misc;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class UnknownCommandMessage extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.MISC.getBaseKeyName() + ".message";
}
public static String unknownCommandMessage = "<red><lang:command.unknown.command><newline><detail>";
@Override
public void onLoaded() {
unknownCommandMessage = config.getString(getBasePath() + ".unknown-command", unknownCommandMessage, config.pickStringRegionBased("""
Unknown command message, using MiniMessage format, set to "default" to use vanilla message,
placeholder: <detail>, shows detail of the unknown command information.""",
"""
发送未知命令时的消息, 使用 MiniMessage 格式, 设置为 "default" 使用原版消息.
变量: <detail>, 显示未知命令详细信息."""));
}
}

View File

@@ -0,0 +1,25 @@
package org.dreeam.leaf.config.modules.network;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ChatMessageSignature extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.NETWORK.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".chat-message-signature", enabled, config.pickStringRegionBased("""
Whether or not enable chat message signature,
disable will prevent players to report chat messages.
And also disables the popup when joining a server without
'secure chat', such as offline-mode servers.
""",
"""
是否启用聊天签名, 禁用后玩家无法进行聊天举报."""));
}
}

View File

@@ -0,0 +1,42 @@
package org.dreeam.leaf.config.modules.network;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import java.util.concurrent.ThreadLocalRandom;
public class ProtocolSupport extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.NETWORK.getBaseKeyName() + ".protocol-support";
}
public static boolean jadeProtocol = false;
public static boolean appleskinProtocol = false;
public static int appleskinSyncTickInterval = 20;
public static boolean asteorBarProtocol = false;
public static boolean chatImageProtocol = false;
public static boolean xaeroMapProtocol = false;
public static int xaeroMapServerID = ThreadLocalRandom.current().nextInt(); // Leaf - Faster Random
public static boolean syncmaticaProtocol = false;
public static boolean syncmaticaQuota = false;
public static int syncmaticaQuotaLimit = 40000000;
@Override
public void onLoaded() {
jadeProtocol = config.getBoolean(getBasePath() + ".jade-protocol", jadeProtocol);
appleskinProtocol = config.getBoolean(getBasePath() + ".appleskin-protocol", appleskinProtocol);
appleskinSyncTickInterval = config.getInt(getBasePath() + ".appleskin-protocol-sync-tick-interval", appleskinSyncTickInterval);
asteorBarProtocol = config.getBoolean(getBasePath() + ".asteorbar-protocol", asteorBarProtocol);
chatImageProtocol = config.getBoolean(getBasePath() + ".chatimage-protocol", chatImageProtocol);
xaeroMapProtocol = config.getBoolean(getBasePath() + ".xaero-map-protocol", xaeroMapProtocol);
xaeroMapServerID = config.getInt(getBasePath() + ".xaero-map-server-id", xaeroMapServerID);
syncmaticaProtocol = config.getBoolean(getBasePath() + ".syncmatica-protocol", syncmaticaProtocol);
syncmaticaQuota = config.getBoolean(getBasePath() + ".syncmatica-quota", syncmaticaQuota);
syncmaticaQuotaLimit = config.getInt(getBasePath() + ".syncmatica-quota-limit", syncmaticaQuotaLimit);
if (syncmaticaProtocol) {
org.leavesmc.leaves.protocol.syncmatica.SyncmaticaProtocol.init();
}
}
}

View File

@@ -0,0 +1,28 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class DontSaveEntity extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".dont-save-entity";
}
public static boolean dontSavePrimedTNT = false;
public static boolean dontSaveFallingBlock = false;
@Override
public void onLoaded() {
dontSavePrimedTNT = config.getBoolean(getBasePath() + ".dont-save-primed-tnt", dontSavePrimedTNT,
config.pickStringRegionBased(
"""
Disable save primed tnt on chunk unloads.
Useful for redstone/technical servers, can prevent machines from being exploded by TNT,
when player disconnected caused by Internet issue.""",
"""
区块卸载时不保存掉落的方块和激活的 TNT,
可以避免在玩家掉线时机器被炸毁."""));
dontSaveFallingBlock = config.getBoolean(getBasePath() + ".dont-save-falling-block", dontSaveFallingBlock);
}
}

View File

@@ -0,0 +1,81 @@
package org.dreeam.leaf.config.modules.opt;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
import java.util.ArrayList;
import java.util.List;
public class DynamicActivationofBrain extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".dab";
}
public static boolean enabled = true;
public static int startDistance = 12;
public static int startDistanceSquared;
public static int maximumActivationPrio = 20;
public static int activationDistanceMod = 8;
public static boolean dontEnableIfInWater = false;
public static List<String> blackedEntities = new ArrayList<>();
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Optimizes entity brains when
they're far away from the player""",
"""
根据距离动态优化生物 AI""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
dontEnableIfInWater = config.getBoolean(getBasePath() + ".dont-enable-if-in-water", dontEnableIfInWater, config.pickStringRegionBased("""
After enabling this, non-aquatic entities in the water will not be affected by DAB.
This could fix entities suffocate in the water.""",
"""
启用此项后, 在水中的非水生生物将不会被 DAB 影响.
可以避免距离玩家较远的生物在水里淹死."""));
startDistance = config.getInt(getBasePath() + ".start-distance", startDistance, config.pickStringRegionBased("""
This value determines how far away an entity has to be
from the player to start being effected by DEAR.""",
"""
生物距离玩家多少格 DAB 开始生效"""));
maximumActivationPrio = config.getInt(getBasePath() + ".max-tick-freq", maximumActivationPrio, config.pickStringRegionBased("""
This value defines how often in ticks, the furthest entity
will get their pathfinders and behaviors ticked. 20 = 1s""",
"""
最远处的实体每隔多少刻tick一次"""));
activationDistanceMod = config.getInt(getBasePath() + ".activation-dist-mod", activationDistanceMod, """
This value defines how much distance modifies an entity's
tick frequency. freq = (distanceToPlayer^2) / (2^value)",
If you want further away entities to tick less often, use 7.
If you want further away entities to tick more often, try 9.""");
blackedEntities = config.getList(getBasePath() + ".blacklisted-entities", blackedEntities,
config.pickStringRegionBased("A list of entities to ignore for activation",
"不会被 DAB 影响的实体列表"));
startDistanceSquared = startDistance * startDistance;
for (EntityType<?> entityType : BuiltInRegistries.ENTITY_TYPE) {
entityType.dabEnabled = true; // reset all, before setting the ones to true
}
final String DEFAULT_PREFIX = ResourceLocation.DEFAULT_NAMESPACE + ResourceLocation.NAMESPACE_SEPARATOR;
for (String name : blackedEntities) {
// Be compatible with both `minecraft:example` and `example` syntax
// If unknown, show user config value in the logger instead of parsed result
String typeId = name.toLowerCase().startsWith(DEFAULT_PREFIX) ? name : DEFAULT_PREFIX + name;
EntityType.byString(typeId).ifPresentOrElse(entityType ->
entityType.dabEnabled = false,
() -> LeafConfig.LOGGER.warn("Skip unknown entity {}, in {}", name, getBasePath() + ".blacklisted-entities")
);
}
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class EnableCachedMTBEntityTypeConvert extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".enable-cached-minecraft-to-bukkit-entitytype-convert", enabled, config.pickStringRegionBased("""
Whether to cache expensive CraftEntityType#minecraftToBukkit call.""",
"""
是否缓存Minecraft到Bukkit的实体类型转换."""));
}
}

View File

@@ -0,0 +1,80 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
import org.dreeam.leaf.config.LeafConfig;
import java.util.random.RandomGeneratorFactory;
public class FastRNG extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".faster-random-generator";
}
public static boolean enabled = false;
public static boolean enableForWorldgen = false;
public static String randomGenerator = "Xoroshiro128PlusPlus";
public static boolean warnForSlimeChunk = true;
public static boolean useLegacyForSlimeChunk = false;
public static boolean worldgenEnabled() { return enabled && enableForWorldgen; } // Helper function
@Override
public void onLoaded() {
config.addCommentRegionBased(getBasePath(), """
Use faster random generator?
Requires a JVM that supports RandomGenerator.
Some JREs don't support this.""",
"""
是否使用更快的随机生成器?
需要支持 RandomGenerator 的 JVM.
一些 JRE 不支持此功能.""");
enabled = config.getBoolean(getBasePath() + ".enabled", enabled);
randomGenerator = config.getString(getBasePath() + ".random-generator", randomGenerator,
config.pickStringRegionBased(
"""
Which random generator will be used?
See https://openjdk.org/jeps/356""",
"""
使用什么种类的随机生成器.
请参阅 https://openjdk.org/jeps/356"""));
enableForWorldgen = config.getBoolean(getBasePath() + ".enable-for-worldgen", enableForWorldgen,
config.pickStringRegionBased(
"""
Enable faster random generator for world generation.
WARNING: This will affect world generation!!!""",
"""
是否为世界生成启用更快的随机生成器.
警告: 此项会影响世界生成!!!"""));
warnForSlimeChunk = config.getBoolean(getBasePath() + ".warn-for-slime-chunk", warnForSlimeChunk,
config.pickStringRegionBased(
"Warn if you are not using legacy random source for slime chunk generation.",
"是否在没有为史莱姆区块使用原版随机生成器的情况下进行警告."));
useLegacyForSlimeChunk = config.getBoolean(getBasePath() + ".use-legacy-random-for-slime-chunk", useLegacyForSlimeChunk, config.pickStringRegionBased(
"""
Use legacy random source for slime chunk generation,
to follow vanilla behavior.""",
"""
是否使用原版随机生成器来生成史莱姆区块."""));
if (enabled) {
try {
RandomGeneratorFactory.of(randomGenerator);
} catch (Exception e) {
LeafConfig.LOGGER.error("Faster random generator is enabled but {} is not supported by your JVM, " +
"falling back to legacy random source.", randomGenerator);
enabled = false;
}
}
if (enabled && warnForSlimeChunk) {
LeafConfig.LOGGER.warn("You enabled faster random generator, it will offset location of slime chunk");
LeafConfig.LOGGER.warn("If your server has slime farms or facilities need vanilla slime chunk,");
LeafConfig.LOGGER.warn("set performance.faster-random-generator.use-legacy-random-for-slime-chunk " +
"to true to use LegacyRandomSource for slime chunk generation.");
LeafConfig.LOGGER.warn("Set performance.faster-random-generator.warn-for-slime-chunk to false to " +
"disable this warning.");
}
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class FasterStructureGenFutureSequencing extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".faster-structure-gen-future-sequencing", enabled,
config.pickStringRegionBased(
"May cause the inconsistent order of future compose tasks.",
"更快的结构生成任务分段."));
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ReduceUselessPackets extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".reduce-packets";
}
public static boolean reduceUselessEntityMovePackets = false;
@Override
public void onLoaded() {
reduceUselessEntityMovePackets = config.getBoolean(getBasePath() + ".reduce-entity-move-packets", reduceUselessEntityMovePackets);
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class SkipAIForNonAwareMob extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config().getBoolean(getBasePath() + ".skip-ai-for-non-aware-mob", enabled);
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class SkipMapItemDataUpdates extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".skip-map-item-data-updates-if-map-does-not-have-craftmaprenderer", enabled);
}
}

View File

@@ -0,0 +1,26 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ThrottleHopperWhenFull extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName() + ".throttle-hopper-when-full";
}
public static boolean enabled = false;
public static int skipTicks = 0;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".enabled", enabled, config.pickStringRegionBased("""
Throttles the hopper if target container is full.""",
"""
是否在目标容器已满时阻塞漏斗."""));
skipTicks = config.getInt(getBasePath() + ".skip-ticks", skipTicks, config.pickStringRegionBased("""
How many ticks to throttle when the Hopper is throttled.""",
"""
每次阻塞多少 tick."""));
}
}

View File

@@ -0,0 +1,23 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class ThrottleInactiveGoalSelectorTick extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".inactive-goal-selector-throttle", enabled, config.pickStringRegionBased("""
Throttles the AI goal selector in entity inactive ticks.
This can improve performance by a few percent, but has minor gameplay implications.""",
"""
是否在实体不活跃 tick 时阻塞 AI 目标选择器.
有助于提升性能, 但对游戏有轻微影响."""));
}
}

View File

@@ -0,0 +1,18 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class TileEntitySnapshotCreation extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".create-snapshot-on-retrieving-blockstate", enabled);
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class VT4BukkitScheduler extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".use-virtual-thread-for-async-scheduler", enabled,
config.pickStringRegionBased(
"Use the new Virtual Thread introduced in JDK 21 for CraftAsyncScheduler.",
"是否为异步任务调度器使用虚拟线程."));
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class VT4ChatExecutor extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".use-virtual-thread-for-async-chat-executor", enabled,
config.pickStringRegionBased(
"Use the new Virtual Thread introduced in JDK 21 for Async Chat Executor.",
"是否为异步聊天线程使用虚拟线程."));
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.config.modules.opt;
import org.dreeam.leaf.config.ConfigModules;
import org.dreeam.leaf.config.EnumConfigCategory;
public class VT4UserAuthenticator extends ConfigModules {
public String getBasePath() {
return EnumConfigCategory.PERF.getBaseKeyName();
}
public static boolean enabled = true;
@Override
public void onLoaded() {
enabled = config.getBoolean(getBasePath() + ".use-virtual-thread-for-user-authenticator", enabled,
config.pickStringRegionBased(
"Use the new Virtual Thread introduced in JDK 21 for User Authenticator.",
"是否为用户验证器使用虚拟线程."));
}
}

View File

@@ -0,0 +1,114 @@
package org.dreeam.leaf.misc;
import it.unimi.dsi.fastutil.doubles.DoubleArrayList;
import java.util.Collections;
import java.util.List;
public class LagCompensation {
public static float tt20(float ticks, boolean limitZero) {
float newTicks = (float) rawTT20(ticks);
if (limitZero) return newTicks > 0 ? newTicks : 1;
else return newTicks;
}
public static int tt20(int ticks, boolean limitZero) {
int newTicks = (int) Math.ceil(rawTT20(ticks));
if (limitZero) return newTicks > 0 ? newTicks : 1;
else return newTicks;
}
public static double tt20(double ticks, boolean limitZero) {
double newTicks = rawTT20(ticks);
if (limitZero) return newTicks > 0 ? newTicks : 1;
else return newTicks;
}
public static double rawTT20(double ticks) {
return ticks == 0 ? 0 : ticks * TPSCalculator.getMostAccurateTPS() / TPSCalculator.MAX_TPS;
}
public static class TPSCalculator {
public static Long lastTick;
public static Long currentTick;
private static double allMissedTicks = 0;
private static final List<Double> tpsHistory = Collections.synchronizedList(new DoubleArrayList());
private static final int historyLimit = 40;
public static final int MAX_TPS = 20;
public static final int FULL_TICK = 50;
private TPSCalculator() {}
public static void onTick() {
if (currentTick != null) {
lastTick = currentTick;
}
currentTick = System.currentTimeMillis();
addToHistory(getTPS());
clearMissedTicks();
missedTick();
}
private static void addToHistory(double tps) {
if (tpsHistory.size() >= historyLimit) {
tpsHistory.removeFirst();
}
tpsHistory.add(tps);
}
public static long getMSPT() {
return currentTick - lastTick;
}
public static double getAverageTPS() {
double sum = 0.0;
for (double value : tpsHistory) {
sum += value;
}
return tpsHistory.isEmpty() ? 0.1 : sum / tpsHistory.size();
}
public static double getTPS() {
if (lastTick == null) return -1;
if (getMSPT() <= 0) return 0.1;
double tps = 1000 / (double) getMSPT();
return tps > MAX_TPS ? MAX_TPS : tps;
}
public static void missedTick() {
if (lastTick == null) return;
long mspt = getMSPT() <= 0 ? 50 : getMSPT();
double missedTicks = (mspt / (double) FULL_TICK) - 1;
allMissedTicks += missedTicks <= 0 ? 0 : missedTicks;
}
public static double getMostAccurateTPS() {
return Math.min(getTPS(), getAverageTPS());
}
public double getAllMissedTicks() {
return allMissedTicks;
}
public static int applicableMissedTicks() {
return (int) Math.floor(allMissedTicks);
}
public static void clearMissedTicks() {
allMissedTicks -= applicableMissedTicks();
}
public void resetMissedTicks() {
allMissedTicks = 0;
}
}
}

View File

@@ -0,0 +1,282 @@
package org.dreeam.leaf.util;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ReferenceArrayList;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.ListIterator;
import java.util.NoSuchElementException;
/**
* Wraps a {@link List} with a hash table which provides O(1) lookups for {@link Collection#contains(Object)}. The type
* contained by this list must use reference-equality semantics.
*/
@SuppressWarnings("SuspiciousMethodCalls")
public class HashedReferenceList<T> implements List<T> {
private final ReferenceArrayList<T> list;
private final Reference2IntOpenHashMap<T> counter;
public HashedReferenceList(List<T> list) {
this.list = new ReferenceArrayList<>();
this.list.addAll(list);
this.counter = new Reference2IntOpenHashMap<>();
this.counter.defaultReturnValue(0);
for (T obj : this.list) {
this.counter.addTo(obj, 1);
}
}
@Override
public int size() {
return this.list.size();
}
@Override
public boolean isEmpty() {
return this.list.isEmpty();
}
@Override
public boolean contains(Object o) {
return this.counter.containsKey(o);
}
@Override
public Iterator<T> iterator() {
return this.listIterator();
}
@Override
public Object[] toArray() {
return this.list.toArray();
}
@SuppressWarnings("SuspiciousToArrayCall")
@Override
public <T1> T1[] toArray(T1 @NotNull [] a) {
return this.list.toArray(a);
}
@Override
public boolean add(T t) {
this.trackReferenceAdded(t);
return this.list.add(t);
}
@Override
public boolean remove(Object o) {
this.trackReferenceRemoved(o);
return this.list.remove(o);
}
@Override
public boolean containsAll(Collection<?> c) {
for (Object obj : c) {
if (!this.counter.containsKey(obj)) {
return false;
}
}
return true;
}
@Override
public boolean addAll(Collection<? extends T> c) {
for (T obj : c) {
this.trackReferenceAdded(obj);
}
return this.list.addAll(c);
}
@Override
public boolean addAll(int index, Collection<? extends T> c) {
for (T obj : c) {
this.trackReferenceAdded(obj);
}
return this.list.addAll(index, c);
}
@Override
public boolean removeAll(@NotNull Collection<?> c) {
if (this.size() >= 2 && c.size() > 4 && c instanceof List) {
//HashReferenceList uses reference equality, so using ReferenceOpenHashSet is fine
c = new ReferenceOpenHashSet<>(c);
}
this.counter.keySet().removeAll(c);
return this.list.removeAll(c);
}
@Override
public boolean retainAll(@NotNull Collection<?> c) {
this.counter.keySet().retainAll(c);
return this.list.retainAll(c);
}
@Override
public void clear() {
this.counter.clear();
this.list.clear();
}
@Override
public T get(int index) {
return this.list.get(index);
}
@Override
public T set(int index, T element) {
T prev = this.list.set(index, element);
if (prev != element) {
if (prev != null) {
this.trackReferenceRemoved(prev);
}
this.trackReferenceAdded(element);
}
return prev;
}
@Override
public void add(int index, T element) {
this.trackReferenceAdded(element);
this.list.add(index, element);
}
@Override
public T remove(int index) {
T prev = this.list.remove(index);
if (prev != null) {
this.trackReferenceRemoved(prev);
}
return prev;
}
@Override
public int indexOf(Object o) {
return this.list.indexOf(o);
}
@Override
public int lastIndexOf(Object o) {
return this.list.lastIndexOf(o);
}
@Override
public ListIterator<T> listIterator() {
return this.listIterator(0);
}
@Override
public ListIterator<T> listIterator(int index) {
return new ListIterator<>() {
private final ListIterator<T> inner = HashedReferenceList.this.list.listIterator(index);
@Override
public boolean hasNext() {
return this.inner.hasNext();
}
@Override
public T next() {
return this.inner.next();
}
@Override
public boolean hasPrevious() {
return this.inner.hasPrevious();
}
@Override
public T previous() {
return this.inner.previous();
}
@Override
public int nextIndex() {
return this.inner.nextIndex();
}
@Override
public int previousIndex() {
return this.inner.previousIndex();
}
@Override
public void remove() {
int last = this.previousIndex();
if (last == -1) {
throw new NoSuchElementException();
}
T prev = HashedReferenceList.this.get(last);
if (prev != null) {
HashedReferenceList.this.trackReferenceRemoved(prev);
}
this.inner.remove();
}
@Override
public void set(T t) {
int last = this.previousIndex();
if (last == -1) {
throw new NoSuchElementException();
}
T prev = HashedReferenceList.this.get(last);
if (prev != t) {
if (prev != null) {
HashedReferenceList.this.trackReferenceRemoved(prev);
}
HashedReferenceList.this.trackReferenceAdded(t);
}
this.inner.remove();
}
@Override
public void add(T t) {
HashedReferenceList.this.trackReferenceAdded(t);
this.inner.add(t);
}
};
}
@Override
public List<T> subList(int fromIndex, int toIndex) {
return this.list.subList(fromIndex, toIndex);
}
private void trackReferenceAdded(T t) {
this.counter.addTo(t, 1);
}
@SuppressWarnings("unchecked")
private void trackReferenceRemoved(Object o) {
if (this.counter.addTo((T) o, -1) <= 1) {
this.counter.removeInt(o);
}
}
}

View File

@@ -0,0 +1,36 @@
package org.dreeam.leaf.util.biome;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Holder;
import net.minecraft.world.level.biome.Biome;
import java.util.function.Function;
import java.util.function.Supplier;
public class PositionalBiomeGetter implements Supplier<Holder<Biome>> {
private final Function<BlockPos, Holder<Biome>> biomeGetter;
private final BlockPos.MutableBlockPos pos;
private int nextX, nextY, nextZ;
private volatile Holder<Biome> curBiome;
public PositionalBiomeGetter(Function<BlockPos, Holder<Biome>> biomeGetter, BlockPos.MutableBlockPos pos) {
this.biomeGetter = biomeGetter;
this.pos = pos;
}
public void update(int nextX, int nextY, int nextZ) {
this.nextX = nextX;
this.nextY = nextY;
this.nextZ = nextZ;
this.curBiome = null;
}
@Override
public Holder<Biome> get() {
Holder<Biome> biome = curBiome;
if (biome == null) {
curBiome = biome = biomeGetter.apply(pos.set(nextX, nextY, nextZ));
}
return biome;
}
}

View File

@@ -0,0 +1,21 @@
package org.dreeam.leaf.util.cache;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import java.util.BitSet;
import java.util.function.IntFunction;
public class CachedOrNewBitsGetter {
private static final IntFunction<BitSet> bitSetConstructor = BitSet::new;
public static ThreadLocal<Int2ObjectOpenHashMap<BitSet>> BITSETS = ThreadLocal.withInitial(Int2ObjectOpenHashMap::new);
private CachedOrNewBitsGetter() {
}
public static BitSet getCachedOrNewBitSet(int bits) {
final BitSet bitSet = BITSETS.get().computeIfAbsent(bits, bitSetConstructor);
bitSet.clear();
return bitSet;
}
}

View File

@@ -0,0 +1,71 @@
package org.dreeam.leaf.util.cache;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.longs.LongList;
import java.util.Iterator;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;
import net.minecraft.core.BlockPos;
/**
* @author 2No2Name, original implemenation by SuperCoder7979 and Gegy1000
*/
public class IterateOutwardsCache {
//POS_ZERO must not be replaced with BlockPos.ORIGIN, otherwise iterateOutwards at BlockPos.ORIGIN will not use the cache
public static final BlockPos POS_ZERO = new BlockPos(0,0,0);
private final ConcurrentHashMap<Long, LongArrayList> table;
private final int capacity;
private final Random random;
public IterateOutwardsCache(int capacity) {
this.capacity = capacity;
this.table = new ConcurrentHashMap<>(31);
this.random = new Random();
}
private void fillPositionsWithIterateOutwards(LongList entry, int xRange, int yRange, int zRange) {
// Add all positions to the cached list
for (BlockPos pos : BlockPos.withinManhattan(POS_ZERO, xRange, yRange, zRange)) {
entry.add(pos.asLong());
}
}
public LongList getOrCompute(int xRange, int yRange, int zRange) {
long key = BlockPos.asLong(xRange, yRange, zRange);
LongArrayList entry = this.table.get(key);
if (entry != null) {
return entry;
}
// Cache miss: compute and store
entry = new LongArrayList(128);
this.fillPositionsWithIterateOutwards(entry, xRange, yRange, zRange);
//decrease the array size, as of now it won't be modified anymore anyways
entry.trim();
//this might overwrite an entry as the same entry could have been computed and added during this thread's computation
//we do not use computeIfAbsent, as it can delay other threads for too long
Object previousEntry = this.table.put(key, entry);
if (previousEntry == null && this.table.size() > this.capacity) {
//prevent a memory leak by randomly removing about 1/8th of the elements when the exceed the desired capacity is exceeded
final Iterator<Long> iterator = this.table.keySet().iterator();
//prevent an unlikely infinite loop caused by another thread filling the table concurrently using counting
for (int i = -this.capacity; iterator.hasNext() && i < 5; i++) {
Long key2 = iterator.next();
//random is not threadsafe, but it doesn't matter here, because we don't need quality random numbers
if (this.random.nextInt(8) == 0 && key2 != key) {
iterator.remove();
}
}
}
return entry;
}
}

View File

@@ -0,0 +1,46 @@
package org.dreeam.leaf.util.cache;
import it.unimi.dsi.fastutil.longs.LongIterator;
import it.unimi.dsi.fastutil.longs.LongList;
import java.util.Iterator;
import net.minecraft.core.BlockPos;
/**
* @author 2No2Name
*/
public class LongList2BlockPosMutableIterable implements Iterable<BlockPos> {
private final LongList positions;
private final int xOffset, yOffset, zOffset;
public LongList2BlockPosMutableIterable(BlockPos offset, LongList posList) {
this.xOffset = offset.getX();
this.yOffset = offset.getY();
this.zOffset = offset.getZ();
this.positions = posList;
}
@Override
public Iterator<BlockPos> iterator() {
return new Iterator<BlockPos>() {
private final LongIterator it = LongList2BlockPosMutableIterable.this.positions.iterator();
private final BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos();
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public net.minecraft.core.BlockPos next() {
long nextPos = this.it.nextLong();
return this.pos.set(
LongList2BlockPosMutableIterable.this.xOffset + BlockPos.getX(nextPos),
LongList2BlockPosMutableIterable.this.yOffset + BlockPos.getY(nextPos),
LongList2BlockPosMutableIterable.this.zOffset + BlockPos.getZ(nextPos));
}
};
}
}

View File

@@ -0,0 +1,29 @@
package org.dreeam.leaf.util.item;
import net.minecraft.core.component.DataComponentType;
import net.minecraft.world.item.ItemStack;
import org.dreeam.leaf.config.modules.misc.HiddenItemComponents;
import java.util.List;
public class ItemStackObfuscator {
public static ItemStack stripMeta(final ItemStack itemStack, final boolean copyItemStack) {
if (itemStack.isEmpty() || itemStack.getComponentsPatch().isEmpty()) return itemStack;
final ItemStack copy = copyItemStack ? itemStack.copy() : itemStack;
// Get the types which need to hide
List<DataComponentType<?>> hiddenTypes = HiddenItemComponents.hiddenItemComponentTypes;
if (hiddenTypes.isEmpty()) return copy;
// Remove specified types
for (DataComponentType<?> type : hiddenTypes) {
// Only remove, no others
copy.remove(type);
}
return copy;
}
}

View File

@@ -0,0 +1,58 @@
package org.dreeam.leaf.util.map;
import com.github.benmanes.caffeine.cache.Interner;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import java.util.Map;
import java.util.function.Function;
/**
* Backed by an {@link Object2ObjectOpenHashMap}, with string keys interned to save memory.
*/
public class StringCanonizingOpenHashMap<T> extends Object2ObjectOpenHashMap<String, T> {
private static final Interner<String> KEY_INTERNER = Interner.newWeakInterner();
private static String intern(String key) {
return key != null ? KEY_INTERNER.intern(key) : null;
}
public StringCanonizingOpenHashMap() {
super();
}
public StringCanonizingOpenHashMap(int expectedSize) {
super(expectedSize);
}
public StringCanonizingOpenHashMap(int expectedSize, float loadFactor) {
super(expectedSize, loadFactor);
}
@Override
public T put(String key, T value) {
return super.put(intern(key), value);
}
@Override
public void putAll(Map<? extends String, ? extends T> m) {
if (m.isEmpty()) return;
Map<String, T> tmp = new Object2ObjectOpenHashMap<>(m.size());
m.forEach((k, v) -> tmp.put(intern(k), v));
super.putAll(tmp);
}
private void putWithoutInterning(String key, T value) {
super.put(key, value);
}
public static <T> StringCanonizingOpenHashMap<T> deepCopy(StringCanonizingOpenHashMap<T> incomingMap, Function<T, T> deepCopier) {
StringCanonizingOpenHashMap<T> newMap = new StringCanonizingOpenHashMap<>(incomingMap.size(), 0.8f);
ObjectIterator<Entry<String, T>> iterator = incomingMap.object2ObjectEntrySet().fastIterator();
while (iterator.hasNext()) {
Map.Entry<String, T> entry = iterator.next();
newMap.putWithoutInterning(entry.getKey(), deepCopier.apply(entry.getValue()));
}
return newMap;
}
}

View File

@@ -0,0 +1,90 @@
package org.dreeam.leaf.util.math;
import net.minecraft.util.Mth;
/**
* A replacement for the sine angle lookup table used in {@link Mth}, both reducing the size of LUT and improving
* the access patterns for common paired sin/cos operations.
* <p>
* sin(-x) = -sin(x)
* ... to eliminate negative angles from the LUT.
* <p>
* sin(x) = sin(pi/2 - x)
* ... to eliminate supplementary angles from the LUT.
* <p>
* Using these identities allows us to reduce the LUT from 64K entries (256 KB) to just 16K entries (64 KB), enabling
* it to better fit into the CPU's caches at the expense of some cycles on the fast path. The implementation has been
* tightly optimized to avoid branching where possible and to use very quick integer operations.
* <p>
* Generally speaking, reducing the size of a lookup table is always a good optimization, but since we need to spend
* extra CPU cycles trying to maintain parity with vanilla, there is the potential risk that this implementation ends
* up being slower than vanilla when the lookup table is able to be kept in cache memory.
* <p>
* Unlike other "fast math" implementations, the values returned by this class are *bit-for-bit identical* with those
* from {@link Mth}. Validation is performed during runtime to ensure that the table is correct.
*
* @author coderbot16 Author of the original (and very clever) implementation in Rust:
* https://gitlab.com/coderbot16/i73/-/tree/master/i73-trig/src
* @author jellysquid3 Additional optimizations, port to Java
*/
public class CompactSineLUT {
private static final int[] SINE_TABLE_INT = new int[16384 + 1];
private static final float SINE_TABLE_MIDPOINT;
static {
final float[] SINE_TABLE = Mth.SIN;
// Copy the sine table, covering to raw int bits
for (int i = 0; i < SINE_TABLE_INT.length; i++) {
SINE_TABLE_INT[i] = Float.floatToRawIntBits(SINE_TABLE[i]);
}
SINE_TABLE_MIDPOINT = SINE_TABLE[SINE_TABLE.length / 2];
// Test that the lookup table is correct during runtime
for (int i = 0; i < SINE_TABLE.length; i++) {
float expected = SINE_TABLE[i];
float value = lookup(i);
if (expected != value) {
throw new IllegalArgumentException(String.format("LUT error at index %d (expected: %s, found: %s)", i, expected, value));
}
}
}
// [VanillaCopy] MathHelper#sin(float)
public static float sin(float f) {
return lookup((int) (f * 10430.378f) & 0xFFFF);
}
// [VanillaCopy] MathHelper#cos(float)
public static float cos(float f) {
return lookup((int) (f * 10430.378f + 16384.0f) & 0xFFFF);
}
private static float lookup(int index) {
// A special case... Is there some way to eliminate this?
if (index == 32768) {
return SINE_TABLE_MIDPOINT;
}
// Trigonometric identity: sin(-x) = -sin(x)
// Given a domain of 0 <= x <= 2*pi, just negate the value if x > pi.
// This allows the sin table size to be halved.
int neg = (index & 0x8000) << 16;
// All bits set if (pi/2 <= x), none set otherwise
// Extracts the 15th bit from 'half'
int mask = (index << 17) >> 31;
// Trigonometric identity: sin(x) = sin(pi/2 - x)
int pos = (0x8001 & mask) + (index ^ mask);
// Wrap the position in the table. Moving this down to immediately before the array access
// seems to help the Hotspot compiler optimize the bit math better.
pos &= 0x7fff;
// Fetch the corresponding value from the LUT and invert the sign bit as needed
// This directly manipulate the sign bit on the float bits to simplify logic
return Float.intBitsToFloat(SINE_TABLE_INT[pos] ^ neg);
}
}

View File

@@ -0,0 +1,127 @@
package org.dreeam.leaf.util.math.random;
import com.google.common.annotations.VisibleForTesting;
import net.minecraft.util.Mth;
import net.minecraft.util.RandomSource;
import net.minecraft.world.level.levelgen.BitRandomSource;
import net.minecraft.world.level.levelgen.PositionalRandomFactory;
import org.dreeam.leaf.config.modules.opt.FastRNG;
import java.util.concurrent.ThreadLocalRandom;
import java.util.random.RandomGenerator;
import java.util.random.RandomGeneratorFactory;
public class FasterRandomSource implements BitRandomSource {
private static final int INT_BITS = 48;
private static final long SEED_MASK = 0xFFFFFFFFFFFFL;
private static final long MULTIPLIER = 25214903917L;
private static final long INCREMENT = 11L;
private static final RandomGeneratorFactory<RandomGenerator> RANDOM_GENERATOR_FACTORY = RandomGeneratorFactory.of(FastRNG.randomGenerator);
private static final boolean isSplittableGenerator = RANDOM_GENERATOR_FACTORY.isSplittable();
private long seed;
private RandomGenerator randomGenerator;
public static final FasterRandomSource SHARED_INSTANCE = new FasterRandomSource(ThreadLocalRandom.current().nextLong());
public FasterRandomSource(long seed) {
this.seed = seed;
this.randomGenerator = RANDOM_GENERATOR_FACTORY.create(seed);
}
private FasterRandomSource(long seed, RandomGenerator.SplittableGenerator randomGenerator) {
this.seed = seed;
this.randomGenerator = randomGenerator;
}
@Override
public final RandomSource fork() {
if (isSplittableGenerator) {
return new FasterRandomSource(seed, ((RandomGenerator.SplittableGenerator) this.randomGenerator).split());
}
return new FasterRandomSource(this.nextLong());
}
@Override
public final PositionalRandomFactory forkPositional() {
return new FasterRandomSourcePositionalRandomFactory(this.seed);
}
@Override
public final void setSeed(long seed) {
this.seed = seed;
this.randomGenerator = RANDOM_GENERATOR_FACTORY.create(seed);
}
@Override
public final int next(int bits) {
// >>> instead of Mojang's >> fixes MC-239059
return (int) ((seed * MULTIPLIER + INCREMENT & SEED_MASK) >>> INT_BITS - bits);
}
public static class FasterRandomSourcePositionalRandomFactory implements PositionalRandomFactory {
private final long seed;
public FasterRandomSourcePositionalRandomFactory(long seed) {
this.seed = seed;
}
@Override
public RandomSource at(int x, int y, int z) {
long l = Mth.getSeed(x, y, z);
long m = l ^ this.seed;
return new FasterRandomSource(m);
}
@Override
public RandomSource fromHashOf(String seed) {
int i = seed.hashCode();
return new FasterRandomSource((long)i ^ this.seed);
}
@Override
public RandomSource fromSeed(long seed) {
return new FasterRandomSource(seed);
}
@VisibleForTesting
@Override
public void parityConfigString(StringBuilder info) {
info.append("FasterRandomSourcePositionalRandomFactory{").append(this.seed).append("}");
}
}
@Override
public final int nextInt() {
return randomGenerator.nextInt();
}
@Override
public final int nextInt(int bound) {
return randomGenerator.nextInt(bound);
}
@Override
public final long nextLong() {
return randomGenerator.nextLong();
}
@Override
public final boolean nextBoolean() {
return randomGenerator.nextBoolean();
}
@Override
public final float nextFloat() {
return randomGenerator.nextFloat();
}
@Override
public final double nextDouble() {
return randomGenerator.nextDouble();
}
@Override
public final double nextGaussian() {
return randomGenerator.nextGaussian();
}
}

View File

@@ -0,0 +1,17 @@
package org.dreeam.leaf.version;
import org.galemc.gale.version.AbstractPaperVersionFetcher;
public class LeafVersionFetcher extends AbstractPaperVersionFetcher {
public LeafVersionFetcher() {
super(
"ver/1.21.3",
"https://github.com/Winds-Studio/Leaf",
"Winds-Studio",
"Leaf",
"Winds-Studio",
"Leaf"
);
}
}

View File

@@ -0,0 +1,24 @@
package org.leavesmc.leaves;
import org.bukkit.Bukkit;
import java.util.logging.Level;
import java.util.logging.Logger;
public class LeavesLogger extends Logger {
public static final LeavesLogger LOGGER = new LeavesLogger();
private LeavesLogger() {
super("Leaves", null);
setParent(Bukkit.getLogger());
setLevel(Level.ALL);
}
public void severe(String msg, Exception exception) {
this.log(Level.SEVERE, msg, exception);
}
public void warning(String msg, Exception exception) {
this.log(Level.WARNING, msg, exception);
}
}

View File

@@ -0,0 +1,38 @@
package org.leavesmc.leaves.bot;
import com.mojang.datafixers.DataFixer;
import net.minecraft.server.MinecraftServer;
import net.minecraft.stats.ServerStatsCounter;
import net.minecraft.stats.Stat;
import net.minecraft.world.entity.player.Player;
import java.io.File;
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);
}
@Override
public void save() {
}
@Override
public void setValue(Player player, Stat<?> stat, int value) {
}
@Override
public void parseLocal(DataFixer dataFixer, String json) {
}
@Override
public int getValue(Stat<?> stat) {
return 0;
}
}

View File

@@ -0,0 +1,73 @@
package org.leavesmc.leaves.entity;
import net.minecraft.server.level.ServerPlayer;
import org.bukkit.craftbukkit.CraftServer;
import org.bukkit.craftbukkit.entity.CraftPlayer;
import org.bukkit.entity.Player;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.replay.ServerPhotographer;
import java.io.File;
public class CraftPhotographer extends CraftPlayer implements Photographer {
public CraftPhotographer(CraftServer server, ServerPhotographer entity) {
super(server, entity);
}
@Override
public void stopRecording() {
this.stopRecording(true);
}
@Override
public void stopRecording(boolean async) {
this.stopRecording(async, true);
}
@Override
public void stopRecording(boolean async, boolean save) {
this.getHandle().remove(async, save);
}
@Override
public void pauseRecording() {
this.getHandle().pauseRecording();
}
@Override
public void resumeRecording() {
this.getHandle().resumeRecording();
}
@Override
public void setRecordFile(@NotNull File file) {
this.getHandle().setSaveFile(file);
}
@Override
public void setFollowPlayer(@Nullable Player player) {
ServerPlayer serverPlayer = player != null ? ((CraftPlayer) player).getHandle() : null;
this.getHandle().setFollowPlayer(serverPlayer);
}
@Override
public @NotNull String getId() {
return this.getHandle().createState.id;
}
@Override
public ServerPhotographer getHandle() {
return (ServerPhotographer) entity;
}
public void setHandle(final ServerPhotographer entity) {
super.setHandle(entity);
}
@Override
public String toString() {
return "CraftPhotographer{" + "name=" + getName() + '}';
}
}

View File

@@ -0,0 +1,82 @@
package org.leavesmc.leaves.entity;
import com.google.common.collect.Lists;
import org.bukkit.Location;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.leavesmc.leaves.replay.BukkitRecorderOption;
import org.leavesmc.leaves.replay.RecorderOption;
import org.leavesmc.leaves.replay.ServerPhotographer;
import java.util.Collection;
import java.util.Collections;
import java.util.UUID;
public class CraftPhotographerManager implements PhotographerManager {
private final Collection<Photographer> photographerViews = Collections.unmodifiableList(Lists.transform(ServerPhotographer.getPhotographers(), ServerPhotographer::getBukkitPlayer));
@Override
public @Nullable Photographer getPhotographer(@NotNull UUID uuid) {
ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
if (photographer != null) {
return photographer.getBukkitPlayer();
}
return null;
}
@Override
public @Nullable Photographer getPhotographer(@NotNull String id) {
ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
if (photographer != null) {
return photographer.getBukkitPlayer();
}
return null;
}
@Override
public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location) {
ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createDefaultOption()).createSync();
if (photographer != null) {
return photographer.getBukkitPlayer();
}
return null;
}
@Override
public @Nullable Photographer createPhotographer(@NotNull String id, @NotNull Location location, @NotNull BukkitRecorderOption recorderOption) {
ServerPhotographer photographer = new ServerPhotographer.PhotographerCreateState(location, id, RecorderOption.createFromBukkit(recorderOption)).createSync();
if (photographer != null) {
return photographer.getBukkitPlayer();
}
return null;
}
@Override
public void removePhotographer(@NotNull String id) {
ServerPhotographer photographer = ServerPhotographer.getPhotographer(id);
if (photographer != null) {
photographer.remove(true);
}
}
@Override
public void removePhotographer(@NotNull UUID uuid) {
ServerPhotographer photographer = ServerPhotographer.getPhotographer(uuid);
if (photographer != null) {
photographer.remove(true);
}
}
@Override
public void removeAllPhotographers() {
for (ServerPhotographer photographer : ServerPhotographer.getPhotographers()) {
photographer.remove(true);
}
}
@Override
public Collection<Photographer> getPhotographers() {
return photographerViews;
}
}

View File

@@ -0,0 +1,124 @@
package org.leavesmc.leaves.protocol;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.food.FoodData;
import net.minecraft.world.level.GameRules;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
@LeavesProtocol(namespace = "appleskin")
public class AppleSkinProtocol {
public static final String PROTOCOL_ID = "appleskin";
private static final ResourceLocation SATURATION_KEY = id("saturation");
private static final ResourceLocation EXHAUSTION_KEY = id("exhaustion");
private static final ResourceLocation NATURAL_REGENERATION_KEY = id("natural_regeneration");
private static final float MINIMUM_EXHAUSTION_CHANGE_THRESHOLD = 0.01F;
private static final Map<ServerPlayer, Float> previousSaturationLevels = new HashMap<>();
private static final Map<ServerPlayer, Float> previousExhaustionLevels = new HashMap<>();
private static final Map<ServerPlayer, Boolean> previousNaturalRegeneration = new HashMap<>();
private static final Map<ServerPlayer, Set<String>> subscribedChannels = new HashMap<>();
public static boolean shouldEnable() {
return org.dreeam.leaf.config.modules.network.ProtocolSupport.appleskinProtocol;
}
@Contract("_ -> new")
public static @NotNull ResourceLocation id(String path) {
return new ResourceLocation(PROTOCOL_ID, path);
}
@ProtocolHandler.PlayerJoin
public static void onPlayerLoggedIn(@NotNull ServerPlayer player) {
resetPlayerData(player);
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLoggedOut(@NotNull ServerPlayer player) {
subscribedChannels.remove(player);
resetPlayerData(player);
}
@ProtocolHandler.MinecraftRegister(ignoreId = true)
public static void onPlayerSubscribed(@NotNull ServerPlayer player, String channel) {
if (org.dreeam.leaf.config.modules.network.ProtocolSupport.appleskinProtocol) {
subscribedChannels.computeIfAbsent(player, k -> new HashSet<>()).add(channel);
}
}
@ProtocolHandler.Ticker
public static void tick() {
if (MinecraftServer.getServer().getTickCount() % org.dreeam.leaf.config.modules.network.ProtocolSupport.appleskinSyncTickInterval != 0) {
return;
}
for (Map.Entry<ServerPlayer, Set<String>> entry : subscribedChannels.entrySet()) {
ServerPlayer player = entry.getKey();
FoodData data = player.getFoodData();
for (String channel : entry.getValue()) {
switch (channel) {
case "saturation" -> {
float saturation = data.getSaturationLevel();
Float previousSaturation = previousSaturationLevels.get(player);
if (previousSaturation == null || saturation != previousSaturation) {
ProtocolUtils.sendPayloadPacket(player, SATURATION_KEY, buf -> buf.writeFloat(saturation));
previousSaturationLevels.put(player, saturation);
}
}
case "exhaustion" -> {
float exhaustion = data.exhaustionLevel;
Float previousExhaustion = previousExhaustionLevels.get(player);
if (previousExhaustion == null || Math.abs(exhaustion - previousExhaustion) >= MINIMUM_EXHAUSTION_CHANGE_THRESHOLD) {
ProtocolUtils.sendPayloadPacket(player, EXHAUSTION_KEY, buf -> buf.writeFloat(exhaustion));
previousExhaustionLevels.put(player, exhaustion);
}
}
case "natural_regeneration" -> {
boolean regeneration = player.serverLevel().getGameRules().getBoolean(GameRules.RULE_NATURAL_REGENERATION);
Boolean previousRegeneration = previousNaturalRegeneration.get(player);
if (previousRegeneration == null || regeneration != previousRegeneration) {
ProtocolUtils.sendPayloadPacket(player, NATURAL_REGENERATION_KEY, buf -> buf.writeBoolean(regeneration));
previousNaturalRegeneration.put(player, regeneration);
}
}
}
}
}
}
@ProtocolHandler.ReloadServer
public static void onServerReload() {
if (!org.dreeam.leaf.config.modules.network.ProtocolSupport.appleskinProtocol) {
disableAllPlayer();
}
}
public static void disableAllPlayer() {
for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) {
onPlayerLoggedOut(player);
}
}
private static void resetPlayerData(@NotNull ServerPlayer player) {
previousExhaustionLevels.remove(player);
previousSaturationLevels.remove(player);
previousNaturalRegeneration.remove(player);
}
}

View File

@@ -0,0 +1,102 @@
package org.leavesmc.leaves.protocol;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.food.FoodData;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
@LeavesProtocol(namespace = "asteorbar")
public class AsteorBarProtocol {
public static final String PROTOCOL_ID = "asteorbar";
private static final ResourceLocation NETWORK_KEY = id("network");
private static final Map<UUID, Float> previousSaturationLevels = new HashMap<>();
private static final Map<UUID, Float> previousExhaustionLevels = new HashMap<>();
private static final float THRESHOLD = 0.01F;
private static final Set<ServerPlayer> players = new HashSet<>();
public static boolean shouldEnable() {
return org.dreeam.leaf.config.modules.network.ProtocolSupport.asteorBarProtocol;
}
@Contract("_ -> new")
public static @NotNull ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(PROTOCOL_ID, path);
}
@ProtocolHandler.PlayerJoin
public static void onPlayerLoggedIn(@NotNull ServerPlayer player) {
resetPlayerData(player);
}
@ProtocolHandler.PlayerLeave
public static void onPlayerLoggedOut(@NotNull ServerPlayer player) {
players.remove(player);
resetPlayerData(player);
}
@ProtocolHandler.MinecraftRegister(ignoreId = true)
public static void onPlayerSubscribed(@NotNull ServerPlayer player) {
players.add(player);
}
@ProtocolHandler.Ticker
public static void tick() {
for (ServerPlayer player : players) {
FoodData data = player.getFoodData();
float saturation = data.getSaturationLevel();
Float previousSaturation = previousSaturationLevels.get(player.getUUID());
if (previousSaturation == null || saturation != previousSaturation) {
ProtocolUtils.sendPayloadPacket(player, NETWORK_KEY, buf -> {
buf.writeByte(1);
buf.writeFloat(saturation);
});
previousSaturationLevels.put(player.getUUID(), saturation);
}
float exhaustion = data.exhaustionLevel;
Float previousExhaustion = previousExhaustionLevels.get(player.getUUID());
if (previousExhaustion == null || Math.abs(exhaustion - previousExhaustion) >= THRESHOLD) {
ProtocolUtils.sendPayloadPacket(player, NETWORK_KEY, buf -> {
buf.writeByte(0);
buf.writeFloat(exhaustion);
});
previousExhaustionLevels.put(player.getUUID(), exhaustion);
}
}
}
@ProtocolHandler.ReloadServer
public static void onServerReload() {
if (!org.dreeam.leaf.config.modules.network.ProtocolSupport.asteorBarProtocol) {
disableAllPlayer();
}
}
public static void disableAllPlayer() {
for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().getPlayers()) {
onPlayerLoggedOut(player);
}
}
private static void resetPlayerData(@NotNull ServerPlayer player) {
previousExhaustionLevels.remove(player.getUUID());
previousSaturationLevels.remove(player.getUUID());
}
}

View File

@@ -0,0 +1,147 @@
package org.leavesmc.leaves.protocol;
import com.google.common.collect.Lists;
import com.google.gson.Gson;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.Packet;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.dreeam.leaf.config.modules.network.ProtocolSupport;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.chatimage.ChatImageIndex;
import org.leavesmc.leaves.protocol.core.LeavesCustomPayload;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.LeavesProtocolManager;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;
@LeavesProtocol(namespace = "chatimage")
public class ChatImageProtocol {
public static final String PROTOCOL_ID = "chatimage";
private static final Map<String, HashMap<Integer, String>> SERVER_BLOCK_CACHE = new HashMap<>();
private static final Map<String, Integer> FILE_COUNT_MAP = new HashMap<>();
private static final Map<String, List<String>> USER_CACHE_MAP = new HashMap<>();
public static int MAX_STRING = 532767;
private static final Gson gson = new Gson();
public static boolean shouldEnable() {
return org.dreeam.leaf.config.modules.network.ProtocolSupport.chatImageProtocol;
}
public record FileInfoChannelPacket(
String message) implements LeavesCustomPayload<LeavesProtocolManager.LeavesPayload> {
private static final ResourceLocation FILE_INFO = ChatImageProtocol.id("file_info");
@New
public FileInfoChannelPacket(ResourceLocation id, FriendlyByteBuf buffer) {
this(buffer.readUtf(MAX_STRING));
}
@Override
public void write(final FriendlyByteBuf buffer) {
buffer.writeUtf(message(), MAX_STRING);
}
@Override
public @NotNull ResourceLocation id() {
return FILE_INFO;
}
}
public record DownloadFileChannelPacket(
String message) implements LeavesCustomPayload<LeavesProtocolManager.LeavesPayload> {
private static final ResourceLocation DOWNLOAD_FILE_CHANNEL = ChatImageProtocol.id("download_file_channel");
@New
public DownloadFileChannelPacket(ResourceLocation id, FriendlyByteBuf buffer) {
this(buffer.readUtf(MAX_STRING));
}
@Override
public void write(final FriendlyByteBuf buffer) {
buffer.writeUtf(message(), MAX_STRING);
}
@Override
public @NotNull ResourceLocation id() {
return DOWNLOAD_FILE_CHANNEL;
}
}
public record FileChannelPacket(
String message) implements LeavesCustomPayload<LeavesProtocolManager.LeavesPayload> {
private static final ResourceLocation FILE_CHANNEL = ChatImageProtocol.id("file_channel");
@New
public FileChannelPacket(ResourceLocation id, FriendlyByteBuf buffer) {
this(buffer.readUtf(MAX_STRING));
}
@Override
public void write(final FriendlyByteBuf buffer) {
buffer.writeUtf(message(), MAX_STRING);
}
@Override
public @NotNull ResourceLocation id() {
return FILE_CHANNEL;
}
}
@ProtocolHandler.PayloadReceiver(payload = FileChannelPacket.class, payloadId = "file_channel")
public static void serverFileChannelReceived(ServerPlayer player, String res) {
ChatImageIndex title = gson.fromJson(res, ChatImageIndex.class);
HashMap<Integer, String> blocks = SERVER_BLOCK_CACHE.containsKey(title.url) ? SERVER_BLOCK_CACHE.get(title.url) : new HashMap<>();
blocks.put(title.index, res);
SERVER_BLOCK_CACHE.put(title.url, blocks);
FILE_COUNT_MAP.put(title.url, title.total);
if (title.total == blocks.size()) {
if (USER_CACHE_MAP.containsKey(title.url)) {
List<String> names = USER_CACHE_MAP.get(title.url);
for (String uuid : names) {
ServerPlayer serverPlayer = player.server.getPlayerList().getPlayer(UUID.fromString(uuid));
if (serverPlayer != null) {
sendToPlayer(new FileInfoChannelPacket("true->" + title.url), serverPlayer);
}
}
USER_CACHE_MAP.put(title.url, Lists.newArrayList());
}
}
}
@ProtocolHandler.PayloadReceiver(payload = FileInfoChannelPacket.class, payloadId = "file_info")
public static void serverFileInfoChannelReceived(ServerPlayer player, String url) {
if (SERVER_BLOCK_CACHE.containsKey(url) && FILE_COUNT_MAP.containsKey(url)) {
HashMap<Integer, String> list = SERVER_BLOCK_CACHE.get(url);
Integer total = FILE_COUNT_MAP.get(url);
if (total == list.size()) {
for (Map.Entry<Integer, String> entry : list.entrySet()) {
sendToPlayer(new DownloadFileChannelPacket(entry.getValue()), player);
}
return;
}
}
sendToPlayer(new FileInfoChannelPacket("null->" + url), player);
List<String> names = USER_CACHE_MAP.containsKey(url) ? USER_CACHE_MAP.get(url) : Lists.newArrayList();
names.add(player.getStringUUID());
USER_CACHE_MAP.put(url, names);
}
@Contract("_ -> new")
public static @NotNull ResourceLocation id(String path) {
return ResourceLocation.fromNamespaceAndPath(PROTOCOL_ID, path);
}
public static void sendToPlayer(CustomPacketPayload payload, ServerPlayer player) {
player.connection.send((Packet<?>) payload);
}
}

View File

@@ -0,0 +1,45 @@
package org.leavesmc.leaves.protocol;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
@LeavesProtocol(namespace = {"xaerominimap", "xaeroworldmap"})
public class XaeroMapProtocol {
public static final String PROTOCOL_ID_MINI = "xaerominimap";
public static final String PROTOCOL_ID_WORLD = "xaeroworldmap";
private static final ResourceLocation MINIMAP_KEY = idMini("main");
private static final ResourceLocation WORLDMAP_KEY = idWorld("main");
public static boolean shouldEnable() {
return org.dreeam.leaf.config.modules.network.ProtocolSupport.xaeroMapProtocol;
}
@Contract("_ -> new")
public static @NotNull ResourceLocation idMini(String path) {
return new ResourceLocation(PROTOCOL_ID_MINI, path);
}
@Contract("_ -> new")
public static @NotNull ResourceLocation idWorld(String path) {
return new ResourceLocation(PROTOCOL_ID_WORLD, path);
}
public static void onSendWorldInfo(@NotNull ServerPlayer player) {
if (shouldEnable()) {
ProtocolUtils.sendPayloadPacket(player, MINIMAP_KEY, buf -> {
buf.writeByte(0);
buf.writeInt(org.dreeam.leaf.config.modules.network.ProtocolSupport.xaeroMapServerID);
});
ProtocolUtils.sendPayloadPacket(player, WORLDMAP_KEY, buf -> {
buf.writeByte(0);
buf.writeInt(org.dreeam.leaf.config.modules.network.ProtocolSupport.xaeroMapServerID);
});
}
}
}

View File

@@ -0,0 +1,16 @@
package org.leavesmc.leaves.protocol.chatimage;
public class ChatImageIndex {
public int index;
public int total;
public String url;
public String bytes;
public ChatImageIndex(int index, int total, String url, String bytes) {
this.index = index;
this.total = total;
this.url = url;
this.bytes = bytes;
}
}

View File

@@ -0,0 +1,29 @@
package org.leavesmc.leaves.protocol.core;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import org.jetbrains.annotations.NotNull;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public interface LeavesCustomPayload<T extends LeavesCustomPayload<T>> extends CustomPacketPayload {
@Target({ElementType.METHOD, ElementType.CONSTRUCTOR})
@Retention(RetentionPolicy.RUNTIME)
@interface New {
}
void write(FriendlyByteBuf buf);
ResourceLocation id();
@Override
@NotNull
default Type<T> type() {
return new CustomPacketPayload.Type<>(id());
}
}

View File

@@ -0,0 +1,12 @@
package org.leavesmc.leaves.protocol.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
public @interface LeavesProtocol {
String[] namespace();
}

View File

@@ -0,0 +1,379 @@
package org.leavesmc.leaves.protocol.core;
import com.google.common.collect.ImmutableSet;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.chat.Component;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.level.ServerPlayer;
import org.apache.commons.lang.ArrayUtils;
import org.bukkit.event.player.PlayerKickEvent;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.LeavesLogger;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
public class LeavesProtocolManager {
private static final Class<?>[] PAYLOAD_PARAMETER_TYPES = {ResourceLocation.class, FriendlyByteBuf.class};
private static final LeavesLogger LOGGER = LeavesLogger.LOGGER;
private static final Map<LeavesProtocol, Map<ProtocolHandler.PayloadReceiver, Executable>> KNOWN_TYPES = new HashMap<>();
private static final Map<LeavesProtocol, Map<ProtocolHandler.PayloadReceiver, Method>> KNOW_RECEIVERS = new HashMap<>();
private static Set<ResourceLocation> ALL_KNOWN_ID = new HashSet<>();
private static final List<Method> TICKERS = new ArrayList<>();
private static final List<Method> PLAYER_JOIN = new ArrayList<>();
private static final List<Method> PLAYER_LEAVE = new ArrayList<>();
private static final List<Method> RELOAD_SERVER = new ArrayList<>();
private static final Map<LeavesProtocol, Map<ProtocolHandler.MinecraftRegister, Method>> MINECRAFT_REGISTER = new HashMap<>();
public static void reload() {
handleServerReload();
cleanProtocols(); // Do cleanup
init();
}
public static void init() {
boolean shouldEnable;
for (Class<?> clazz : org.dreeam.leaf.config.LeafConfig.getClasses("org.leavesmc.leaves.protocol")) {
final LeavesProtocol protocol = clazz.getAnnotation(LeavesProtocol.class);
if (protocol != null) {
Set<Method> methods;
try {
Method[] publicMethods = clazz.getMethods();
Method[] privateMethods = clazz.getDeclaredMethods();
methods = new HashSet<>(publicMethods.length + privateMethods.length, 1.0f);
Collections.addAll(methods, publicMethods);
Collections.addAll(methods, privateMethods);
Object instance = clazz.getConstructor().newInstance();
Method method = clazz.getMethod("shouldEnable");
shouldEnable = (boolean) method.invoke(instance);
} catch (NoClassDefFoundError | InvocationTargetException | InstantiationException |
IllegalAccessException | NoSuchMethodException error) {
LOGGER.severe("Failed to load class " + clazz.getName() + " due to missing dependencies, " + error.getCause() + ": " + error.getMessage());
return;
}
Map<ProtocolHandler.PayloadReceiver, Executable> map = KNOWN_TYPES.getOrDefault(protocol, new HashMap<>());
for (final Method method : methods) {
if (method.isBridge() || method.isSynthetic() || !Modifier.isStatic(method.getModifiers())) {
continue;
}
method.setAccessible(true);
final ProtocolHandler.ReloadServer reloadServer = method.getAnnotation(ProtocolHandler.ReloadServer.class);
if (reloadServer != null) {
RELOAD_SERVER.add(method);
continue;
}
if (!shouldEnable) {
continue;
}
final ProtocolHandler.Init init = method.getAnnotation(ProtocolHandler.Init.class);
if (init != null) {
try {
method.invoke(null);
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.severe("Failed to invoke init method " + method.getName() + " in " + clazz.getName() + ", " + exception.getCause() + ": " + exception.getMessage());
}
continue;
}
final ProtocolHandler.PayloadReceiver receiver = method.getAnnotation(ProtocolHandler.PayloadReceiver.class);
if (receiver != null) {
try {
boolean found = false;
for (Method payloadMethod : receiver.payload().getDeclaredMethods()) {
if (payloadMethod.isAnnotationPresent(LeavesCustomPayload.New.class)) {
if (Arrays.equals(payloadMethod.getParameterTypes(), PAYLOAD_PARAMETER_TYPES) && payloadMethod.getReturnType() == receiver.payload() && Modifier.isStatic(payloadMethod.getModifiers())) {
payloadMethod.setAccessible(true);
map.put(receiver, payloadMethod);
found = true;
break;
}
}
}
if (!found) {
Constructor<? extends LeavesCustomPayload<?>> constructor = receiver.payload().getConstructor(PAYLOAD_PARAMETER_TYPES);
if (constructor.isAnnotationPresent(LeavesCustomPayload.New.class)) {
constructor.setAccessible(true);
map.put(receiver, constructor);
} else {
throw new NoSuchMethodException();
}
}
} catch (NoSuchMethodException exception) {
LOGGER.severe("Failed to find constructor for " + receiver.payload().getName() + ", " + exception.getCause() + ": " + exception.getMessage());
continue;
}
if (!KNOW_RECEIVERS.containsKey(protocol)) {
KNOW_RECEIVERS.put(protocol, new HashMap<>());
}
KNOW_RECEIVERS.get(protocol).put(receiver, method);
continue;
}
final ProtocolHandler.Ticker ticker = method.getAnnotation(ProtocolHandler.Ticker.class);
if (ticker != null) {
TICKERS.add(method);
continue;
}
final ProtocolHandler.PlayerJoin playerJoin = method.getAnnotation(ProtocolHandler.PlayerJoin.class);
if (playerJoin != null) {
PLAYER_JOIN.add(method);
continue;
}
final ProtocolHandler.PlayerLeave playerLeave = method.getAnnotation(ProtocolHandler.PlayerLeave.class);
if (playerLeave != null) {
PLAYER_LEAVE.add(method);
continue;
}
final ProtocolHandler.MinecraftRegister minecraftRegister = method.getAnnotation(ProtocolHandler.MinecraftRegister.class);
if (minecraftRegister != null) {
if (!MINECRAFT_REGISTER.containsKey(protocol)) {
MINECRAFT_REGISTER.put(protocol, new HashMap<>());
}
MINECRAFT_REGISTER.get(protocol).put(minecraftRegister, method);
}
}
KNOWN_TYPES.put(protocol, map);
}
}
for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) {
Map<ProtocolHandler.PayloadReceiver, Executable> map = KNOWN_TYPES.get(protocol);
for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) {
if (receiver.sendFabricRegister() && !receiver.ignoreId()) {
for (String payloadId : receiver.payloadId()) {
for (String namespace : protocol.namespace()) {
ALL_KNOWN_ID.add(new ResourceLocation(namespace, payloadId));
}
}
}
}
}
ALL_KNOWN_ID = ImmutableSet.copyOf(ALL_KNOWN_ID);
}
private static void cleanProtocols() {
KNOWN_TYPES.clear();
KNOW_RECEIVERS.clear();
//ALL_KNOWN_ID.clear(); // No need
TICKERS.clear();
PLAYER_JOIN.clear();
PLAYER_LEAVE.clear();
//RELOAD_SERVER.clear(); // No need
MINECRAFT_REGISTER.clear();
}
public static LeavesCustomPayload<?> decode(ResourceLocation id, FriendlyByteBuf buf) {
for (LeavesProtocol protocol : KNOWN_TYPES.keySet()) {
if (!ArrayUtils.contains(protocol.namespace(), id.getNamespace())) {
continue;
}
Map<ProtocolHandler.PayloadReceiver, Executable> map = KNOWN_TYPES.get(protocol);
for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) {
if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), id.getPath())) {
try {
if (map.get(receiver) instanceof Constructor<?> constructor) {
return (LeavesCustomPayload<?>) constructor.newInstance(id, buf);
} else if (map.get(receiver) instanceof Method method) {
return (LeavesCustomPayload<?>) method.invoke(null, id, buf);
}
} catch (InvocationTargetException | InstantiationException | IllegalAccessException exception) {
LOGGER.warning("Failed to create payload for " + id + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage());
buf.readBytes(buf.readableBytes());
return new ErrorPayload(id, protocol.namespace(), receiver.payloadId());
}
}
}
}
return null;
}
public static void handlePayload(ServerPlayer player, LeavesCustomPayload<?> payload) {
if (payload instanceof ErrorPayload errorPayload) {
player.connection.disconnect(Component.literal("Payload " + Arrays.toString(errorPayload.packetID) + " from " + Arrays.toString(errorPayload.protocolID) + " error"), PlayerKickEvent.Cause.INVALID_PAYLOAD);
return;
}
for (LeavesProtocol protocol : KNOW_RECEIVERS.keySet()) {
if (!ArrayUtils.contains(protocol.namespace(), payload.type().id().getNamespace())) {
continue;
}
Map<ProtocolHandler.PayloadReceiver, Method> map = KNOW_RECEIVERS.get(protocol);
for (ProtocolHandler.PayloadReceiver receiver : map.keySet()) {
if (payload.getClass() == receiver.payload()) {
if (receiver.ignoreId() || ArrayUtils.contains(receiver.payloadId(), payload.type().id().getPath())) {
try {
map.get(receiver).invoke(null, player, payload);
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to handle payload " + payload.type().id() + " in " + ArrayUtils.toString(protocol.namespace()) + ", " + exception.getCause() + ": " + exception.getMessage());
}
}
}
}
}
}
public static void handleTick() {
if (!TICKERS.isEmpty()) {
try {
for (Method method : TICKERS) {
method.invoke(null);
}
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to tick, " + exception.getCause() + ": " + exception.getMessage());
}
}
}
public static void handlePlayerJoin(ServerPlayer player) {
if (!PLAYER_JOIN.isEmpty()) {
try {
for (Method method : PLAYER_JOIN) {
method.invoke(null, player);
}
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to handle player join, " + exception.getCause() + ": " + exception.getMessage());
}
}
ProtocolUtils.sendPayloadPacket(player, new FabricRegisterPayload(ALL_KNOWN_ID));
}
public static void handlePlayerLeave(ServerPlayer player) {
if (!PLAYER_LEAVE.isEmpty()) {
try {
for (Method method : PLAYER_LEAVE) {
method.invoke(null, player);
}
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to handle player leave, " + exception.getCause() + ": " + exception.getMessage());
}
}
}
public static void handleServerReload() {
if (!RELOAD_SERVER.isEmpty()) {
try {
for (Method method : RELOAD_SERVER) {
method.invoke(null);
}
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to handle server reload, " + exception.getCause() + ": " + exception.getMessage());
}
}
}
public static void handleMinecraftRegister(String channelId, ServerPlayer player) {
for (LeavesProtocol protocol : MINECRAFT_REGISTER.keySet()) {
String[] channel = channelId.split(":");
if (!ArrayUtils.contains(protocol.namespace(), channel[0])) {
continue;
}
Map<ProtocolHandler.MinecraftRegister, Method> map = MINECRAFT_REGISTER.get(protocol);
for (ProtocolHandler.MinecraftRegister register : map.keySet()) {
if (register.ignoreId() || ArrayUtils.contains(register.channelId(), channel[1])) {
try {
map.get(register).invoke(null, player, channel[1]);
} catch (InvocationTargetException | IllegalAccessException exception) {
LOGGER.warning("Failed to handle minecraft register, " + exception.getCause() + ": " + exception.getMessage());
}
}
}
}
}
public record ErrorPayload(ResourceLocation id, String[] protocolID, String[] packetID) implements LeavesCustomPayload<ErrorPayload> {
@Override
public void write(@NotNull FriendlyByteBuf buf) {
}
}
public record EmptyPayload(ResourceLocation id) implements LeavesCustomPayload<EmptyPayload> {
@New
public EmptyPayload(ResourceLocation location, FriendlyByteBuf buf) {
this(location);
}
@Override
public void write(@NotNull FriendlyByteBuf buf) {
}
}
public record LeavesPayload(FriendlyByteBuf data, ResourceLocation id) implements LeavesCustomPayload<LeavesPayload> {
@New
public LeavesPayload(ResourceLocation location, FriendlyByteBuf buf) {
this(new FriendlyByteBuf(buf.readBytes(buf.readableBytes())), location);
}
@Override
public void write(FriendlyByteBuf buf) {
buf.writeBytes(data);
}
}
public record FabricRegisterPayload(
Set<ResourceLocation> channels) implements LeavesCustomPayload<FabricRegisterPayload> {
public static final ResourceLocation CHANNEL = ResourceLocation.withDefaultNamespace("register");
@New
public FabricRegisterPayload(ResourceLocation location, FriendlyByteBuf buf) {
this(buf.readCollection(HashSet::new, FriendlyByteBuf::readResourceLocation));
}
@Override
public void write(FriendlyByteBuf buf) {
boolean first = true;
ResourceLocation channel;
for (Iterator<ResourceLocation> var3 = this.channels.iterator(); var3.hasNext(); buf.writeBytes(channel.toString().getBytes(StandardCharsets.US_ASCII))) {
channel = var3.next();
if (first) {
first = false;
} else {
buf.writeByte(0);
}
}
}
@Override
public ResourceLocation id() {
return CHANNEL;
}
}
}

View File

@@ -0,0 +1,56 @@
package org.leavesmc.leaves.protocol.core;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
public class ProtocolHandler {
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Init {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PayloadReceiver {
Class<? extends LeavesCustomPayload<?>> payload();
String[] payloadId() default "";
boolean ignoreId() default false;
boolean sendFabricRegister() default true;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Ticker {
int delay() default 0;
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlayerJoin {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PlayerLeave {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface ReloadServer {
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MinecraftRegister {
String[] channelId() default "";
boolean ignoreId() default false;
}
}

View File

@@ -0,0 +1,52 @@
package org.leavesmc.leaves.protocol.core;
import io.netty.buffer.ByteBuf;
import io.papermc.paper.ServerBuildInfo;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket;
import net.minecraft.network.protocol.common.custom.CustomPacketPayload;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import org.jetbrains.annotations.NotNull;
import java.util.function.Consumer;
import java.util.function.Function;
public class ProtocolUtils {
private static final Function<ByteBuf, RegistryFriendlyByteBuf> bufDecorator = RegistryFriendlyByteBuf.decorator(MinecraftServer.getServer().registryAccess());
public static String buildProtocolVersion(String protocol) {
return protocol + "-leaves-" + ServerBuildInfo.buildInfo().asString(ServerBuildInfo.StringRepresentation.VERSION_SIMPLE);
}
public static void sendEmptyPayloadPacket(ServerPlayer player, ResourceLocation id) {
player.connection.send(new ClientboundCustomPayloadPacket(new LeavesProtocolManager.EmptyPayload(id)));
}
@SuppressWarnings("all")
public static void sendPayloadPacket(@NotNull ServerPlayer player, ResourceLocation id, Consumer<FriendlyByteBuf> consumer) {
player.connection.send(new ClientboundCustomPayloadPacket(new LeavesCustomPayload() {
@Override
public void write(@NotNull FriendlyByteBuf buf) {
consumer.accept(buf);
}
@Override
@NotNull
public ResourceLocation id() {
return id;
}
}));
}
public static void sendPayloadPacket(ServerPlayer player, CustomPacketPayload payload) {
player.connection.send(new ClientboundCustomPayloadPacket(payload));
}
public static RegistryFriendlyByteBuf decorate(ByteBuf buf) {
return bufDecorator.apply(buf);
}
}

View File

@@ -0,0 +1,271 @@
package org.leavesmc.leaves.protocol.jade;
import net.minecraft.core.BlockPos;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.core.registries.Registries;
import net.minecraft.nbt.CompoundTag;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.util.Mth;
import net.minecraft.world.entity.AgeableMob;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.animal.Animal;
import net.minecraft.world.entity.animal.Chicken;
import net.minecraft.world.entity.animal.allay.Allay;
import net.minecraft.world.entity.animal.armadillo.Armadillo;
import net.minecraft.world.entity.animal.frog.Tadpole;
import net.minecraft.world.entity.monster.ZombieVillager;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.Items;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.CampfireBlock;
import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity;
import net.minecraft.world.level.block.entity.BeehiveBlockEntity;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.entity.BrewingStandBlockEntity;
import net.minecraft.world.level.block.entity.CalibratedSculkSensorBlockEntity;
import net.minecraft.world.level.block.entity.ChiseledBookShelfBlockEntity;
import net.minecraft.world.level.block.entity.CommandBlockEntity;
import net.minecraft.world.level.block.entity.ComparatorBlockEntity;
import net.minecraft.world.level.block.entity.HopperBlockEntity;
import net.minecraft.world.level.block.entity.JukeboxBlockEntity;
import net.minecraft.world.level.block.entity.LecternBlockEntity;
import net.minecraft.world.level.block.entity.TrialSpawnerBlockEntity;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.LeavesLogger;
import org.leavesmc.leaves.protocol.core.LeavesProtocol;
import org.leavesmc.leaves.protocol.core.ProtocolHandler;
import org.leavesmc.leaves.protocol.core.ProtocolUtils;
import org.leavesmc.leaves.protocol.jade.accessor.BlockAccessor;
import org.leavesmc.leaves.protocol.jade.accessor.EntityAccessor;
import org.leavesmc.leaves.protocol.jade.payload.ReceiveDataPayload;
import org.leavesmc.leaves.protocol.jade.payload.RequestBlockPayload;
import org.leavesmc.leaves.protocol.jade.payload.RequestEntityPayload;
import org.leavesmc.leaves.protocol.jade.payload.ServerPingPayload;
import org.leavesmc.leaves.protocol.jade.provider.IJadeProvider;
import org.leavesmc.leaves.protocol.jade.provider.IServerDataProvider;
import org.leavesmc.leaves.protocol.jade.provider.IServerExtensionProvider;
import org.leavesmc.leaves.protocol.jade.provider.ItemStorageExtensionProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.BeehiveProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.BrewingStandProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.CampfireProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.ChiseledBookshelfProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.CommandBlockProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.FurnaceProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.HopperLockProvider;
import org.leavesmc.leaves.protocol.jade.provider.ItemStorageProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.JukeboxProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.LecternProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.MobSpawnerCooldownProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.ObjectNameProvider;
import org.leavesmc.leaves.protocol.jade.provider.block.RedstoneProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.AnimalOwnerProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.MobBreedingProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.MobGrowthProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.NextEntityDropProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.StatusEffectsProvider;
import org.leavesmc.leaves.protocol.jade.provider.entity.ZombieVillagerProvider;
import org.leavesmc.leaves.protocol.jade.util.HierarchyLookup;
import org.leavesmc.leaves.protocol.jade.util.LootTableMineableCollector;
import org.leavesmc.leaves.protocol.jade.util.PairHierarchyLookup;
import org.leavesmc.leaves.protocol.jade.util.PriorityStore;
import org.leavesmc.leaves.protocol.jade.util.WrappedHierarchyLookup;
import java.util.Collections;
import java.util.List;
@LeavesProtocol(namespace = "jade")
public class JadeProtocol {
public static PriorityStore<ResourceLocation, IJadeProvider> priorities;
private static List<Block> shearableBlocks = null;
public static final String PROTOCOL_ID = "jade";
public static final HierarchyLookup<IServerDataProvider<EntityAccessor>> entityDataProviders = new HierarchyLookup<>(Entity.class);
public static final PairHierarchyLookup<IServerDataProvider<BlockAccessor>> blockDataProviders = new PairHierarchyLookup<>(new HierarchyLookup<>(Block.class), new HierarchyLookup<>(BlockEntity.class));
public static final WrappedHierarchyLookup<IServerExtensionProvider<ItemStack>> itemStorageProviders = WrappedHierarchyLookup.forAccessor();
public static boolean shouldEnable() {
return org.dreeam.leaf.config.modules.network.ProtocolSupport.jadeProtocol;
}
@Contract("_ -> new")
public static @NotNull ResourceLocation id(String path) {
return new ResourceLocation(PROTOCOL_ID, path);
}
@Contract("_ -> new")
public static @NotNull ResourceLocation mc_id(String path) {
return ResourceLocation.withDefaultNamespace(path);
}
@ProtocolHandler.Init
public static void init() {
priorities = new PriorityStore<>(IJadeProvider::getDefaultPriority, IJadeProvider::getUid);
// core plugin
blockDataProviders.register(BlockEntity.class, ObjectNameProvider.ForBlock.INSTANCE);
// universal plugin
entityDataProviders.register(Entity.class, ItemStorageProvider.getEntity());
blockDataProviders.register(Block.class, ItemStorageProvider.getBlock());
itemStorageProviders.register(Object.class, ItemStorageExtensionProvider.INSTANCE);
itemStorageProviders.register(Block.class, ItemStorageExtensionProvider.INSTANCE);
// vanilla plugin
entityDataProviders.register(Entity.class, AnimalOwnerProvider.INSTANCE);
entityDataProviders.register(LivingEntity.class, StatusEffectsProvider.INSTANCE);
entityDataProviders.register(AgeableMob.class, MobGrowthProvider.INSTANCE);
entityDataProviders.register(Tadpole.class, MobGrowthProvider.INSTANCE);
entityDataProviders.register(Animal.class, MobBreedingProvider.INSTANCE);
entityDataProviders.register(Allay.class, MobBreedingProvider.INSTANCE);
entityDataProviders.register(Chicken.class, NextEntityDropProvider.INSTANCE);
entityDataProviders.register(Armadillo.class, NextEntityDropProvider.INSTANCE);
entityDataProviders.register(ZombieVillager.class, ZombieVillagerProvider.INSTANCE);
blockDataProviders.register(BrewingStandBlockEntity.class, BrewingStandProvider.INSTANCE);
blockDataProviders.register(BeehiveBlockEntity.class, BeehiveProvider.INSTANCE);
blockDataProviders.register(CommandBlockEntity.class, CommandBlockProvider.INSTANCE);
blockDataProviders.register(JukeboxBlockEntity.class, JukeboxProvider.INSTANCE);
blockDataProviders.register(LecternBlockEntity.class, LecternProvider.INSTANCE);
blockDataProviders.register(ComparatorBlockEntity.class, RedstoneProvider.INSTANCE);
blockDataProviders.register(HopperBlockEntity.class, HopperLockProvider.INSTANCE);
blockDataProviders.register(CalibratedSculkSensorBlockEntity.class, RedstoneProvider.INSTANCE);
blockDataProviders.register(AbstractFurnaceBlockEntity.class, FurnaceProvider.INSTANCE);
blockDataProviders.register(ChiseledBookShelfBlockEntity.class, ChiseledBookshelfProvider.INSTANCE);
blockDataProviders.register(TrialSpawnerBlockEntity.class, MobSpawnerCooldownProvider.INSTANCE);
blockDataProviders.idMapped();
entityDataProviders.idMapped();
itemStorageProviders.register(CampfireBlock.class, CampfireProvider.INSTANCE);
blockDataProviders.loadComplete(priorities);
entityDataProviders.loadComplete(priorities);
itemStorageProviders.loadComplete(priorities);
try {
shearableBlocks = Collections.unmodifiableList(LootTableMineableCollector.execute(
MinecraftServer.getServer().reloadableRegistries().lookup().lookupOrThrow(Registries.LOOT_TABLE),
Items.SHEARS.getDefaultInstance()
));
} catch (Throwable ignore) {
shearableBlocks = List.of();
LeavesLogger.LOGGER.severe("Failed to collect shearable blocks");
}
}
@ProtocolHandler.PlayerJoin
public static void onPlayerJoin(ServerPlayer player) {
sendPingPacket(player);
}
@ProtocolHandler.PayloadReceiver(payload = RequestEntityPayload.class, payloadId = "request_entity")
public static void requestEntityData(ServerPlayer player, RequestEntityPayload payload) {
MinecraftServer.getServer().execute(() -> {
EntityAccessor accessor = payload.data().unpack(player);
if (accessor == null) {
return;
}
Entity entity = accessor.getEntity();
double maxDistance = Mth.square(player.entityInteractionRange() + 21);
if (entity == null || player.distanceToSqr(entity) > maxDistance) {
return;
}
List<IServerDataProvider<EntityAccessor>> providers = entityDataProviders.get(entity);
if (providers.isEmpty()) {
return;
}
CompoundTag tag = new CompoundTag();
for (IServerDataProvider<EntityAccessor> provider : providers) {
if (!payload.dataProviders().contains(provider)) {
continue;
}
try {
provider.appendServerData(tag, accessor);
} catch (Exception e) {
LeavesLogger.LOGGER.warning("Error while saving data for entity " + entity);
}
}
tag.putInt("EntityId", entity.getId());
ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag));
});
}
@ProtocolHandler.PayloadReceiver(payload = RequestBlockPayload.class, payloadId = "request_block")
public static void requestBlockData(ServerPlayer player, RequestBlockPayload payload) {
MinecraftServer server = MinecraftServer.getServer();
server.execute(() -> {
BlockAccessor accessor = payload.data().unpack(player);
if (accessor == null) {
return;
}
BlockPos pos = accessor.getPosition();
Block block = accessor.getBlock();
BlockEntity blockEntity = accessor.getBlockEntity();
double maxDistance = Mth.square(player.blockInteractionRange() + 21);
if (pos.distSqr(player.blockPosition()) > maxDistance || !accessor.getLevel().isLoaded(pos)) {
return;
}
List<IServerDataProvider<BlockAccessor>> providers;
if (blockEntity != null) {
providers = blockDataProviders.getMerged(block, blockEntity);
} else {
providers = blockDataProviders.first.get(block);
}
if (providers.isEmpty()) {
return;
}
CompoundTag tag = new CompoundTag();
for (IServerDataProvider<BlockAccessor> provider : providers) {
if (!payload.dataProviders().contains(provider)) {
continue;
}
try {
provider.appendServerData(tag, accessor);
} catch (Exception e) {
LeavesLogger.LOGGER.warning("Error while saving data for block " + accessor.getBlockState());
}
}
tag.putInt("x", pos.getX());
tag.putInt("y", pos.getY());
tag.putInt("z", pos.getZ());
tag.putString("BlockId", BuiltInRegistries.BLOCK.getKey(block).toString());
ProtocolUtils.sendPayloadPacket(player, new ReceiveDataPayload(tag));
});
}
@ProtocolHandler.ReloadServer
public static void onServerReload() {
if (org.dreeam.leaf.config.modules.network.ProtocolSupport.jadeProtocol) {
enableAllPlayer();
}
}
public static void enableAllPlayer() {
for (ServerPlayer player : MinecraftServer.getServer().getPlayerList().players) {
sendPingPacket(player);
}
}
public static void sendPingPacket(ServerPlayer player) {
ProtocolUtils.sendPayloadPacket(player, new ServerPingPayload(Collections.emptyMap(), shearableBlocks, blockDataProviders.mappedIds(), entityDataProviders.mappedIds()));
}
}

View File

@@ -0,0 +1,32 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import net.minecraft.nbt.Tag;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamEncoder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
import org.jetbrains.annotations.Nullable;
public interface Accessor<T extends HitResult> {
Level getLevel();
Player getPlayer();
<D> Tag encodeAsNbt(StreamEncoder<RegistryFriendlyByteBuf, D> codec, D value);
T getHitResult();
/**
* @return {@code true} if the dedicated server has Jade installed.
*/
boolean isServerConnected();
boolean showDetails();
@Nullable
Object getTarget();
float tickRate();
}

View File

@@ -0,0 +1,83 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import java.util.function.Supplier;
import org.apache.commons.lang3.ArrayUtils;
import io.netty.buffer.Unpooled;
import net.minecraft.nbt.ByteArrayTag;
import net.minecraft.nbt.Tag;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.StreamEncoder;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.HitResult;
public abstract class AccessorImpl<T extends HitResult> implements Accessor<T> {
private final Level level;
private final Player player;
private final Supplier<T> hit;
private final boolean serverConnected;
private final boolean showDetails;
protected boolean verify;
private RegistryFriendlyByteBuf buffer;
public AccessorImpl(Level level, Player player, Supplier<T> hit, boolean serverConnected, boolean showDetails) {
this.level = level;
this.player = player;
this.hit = hit;
this.serverConnected = serverConnected;
this.showDetails = showDetails;
}
@Override
public Level getLevel() {
return level;
}
@Override
public Player getPlayer() {
return player;
}
private RegistryFriendlyByteBuf buffer() {
if (buffer == null) {
buffer = new RegistryFriendlyByteBuf(Unpooled.buffer(), level.registryAccess());
}
buffer.clear();
return buffer;
}
@Override
public <D> Tag encodeAsNbt(StreamEncoder<RegistryFriendlyByteBuf, D> streamCodec, D value) {
RegistryFriendlyByteBuf buffer = buffer();
streamCodec.encode(buffer, value);
ByteArrayTag tag = new ByteArrayTag(ArrayUtils.subarray(buffer.array(), 0, buffer.readableBytes()));
buffer.clear();
return tag;
}
@Override
public T getHitResult() {
return hit.get();
}
/**
* Returns true if dedicated server has Jade installed.
*/
@Override
public boolean isServerConnected() {
return serverConnected;
}
@Override
public boolean showDetails() {
return showDetails;
}
@Override
public float tickRate() {
return getLevel().tickRateManager().tickrate();
}
}

View File

@@ -0,0 +1,50 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
import org.jetbrains.annotations.ApiStatus;
import java.util.function.Supplier;
public interface BlockAccessor extends Accessor<BlockHitResult> {
Block getBlock();
BlockState getBlockState();
BlockEntity getBlockEntity();
BlockPos getPosition();
Direction getSide();
@ApiStatus.NonExtendable
interface Builder {
Builder level(Level level);
Builder player(Player player);
Builder showDetails(boolean showDetails);
Builder hit(BlockHitResult hit);
Builder blockState(BlockState state);
default Builder blockEntity(BlockEntity blockEntity) {
return blockEntity(() -> blockEntity);
}
Builder blockEntity(Supplier<BlockEntity> blockEntity);
Builder from(BlockAccessor accessor);
BlockAccessor build();
}
}

View File

@@ -0,0 +1,163 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import java.util.function.Supplier;
import org.jetbrains.annotations.Nullable;
import com.google.common.base.Suppliers;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.network.FriendlyByteBuf;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.entity.BlockEntity;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.phys.BlockHitResult;
/**
* Class to get information of block target and context.
*/
public class BlockAccessorImpl extends AccessorImpl<BlockHitResult> implements BlockAccessor {
private final BlockState blockState;
@Nullable
private final Supplier<BlockEntity> blockEntity;
private BlockAccessorImpl(Builder builder) {
super(builder.level, builder.player, Suppliers.ofInstance(builder.hit), builder.connected, builder.showDetails);
blockState = builder.blockState;
blockEntity = builder.blockEntity;
}
@Override
public Block getBlock() {
return getBlockState().getBlock();
}
@Override
public BlockState getBlockState() {
return blockState;
}
@Override
public BlockEntity getBlockEntity() {
return blockEntity == null ? null : blockEntity.get();
}
@Override
public BlockPos getPosition() {
return getHitResult().getBlockPos();
}
@Override
public Direction getSide() {
return getHitResult().getDirection();
}
@Nullable
@Override
public Object getTarget() {
return getBlockEntity();
}
public static class Builder implements BlockAccessor.Builder {
private Level level;
private Player player;
private boolean connected;
private boolean showDetails;
private BlockHitResult hit;
private BlockState blockState = Blocks.AIR.defaultBlockState();
private Supplier<BlockEntity> blockEntity;
@Override
public Builder level(Level level) {
this.level = level;
return this;
}
@Override
public Builder player(Player player) {
this.player = player;
return this;
}
@Override
public Builder showDetails(boolean showDetails) {
this.showDetails = showDetails;
return this;
}
@Override
public Builder hit(BlockHitResult hit) {
this.hit = hit;
return this;
}
@Override
public Builder blockState(BlockState blockState) {
this.blockState = blockState;
return this;
}
@Override
public Builder blockEntity(Supplier<BlockEntity> blockEntity) {
this.blockEntity = blockEntity;
return this;
}
@Override
public Builder from(BlockAccessor accessor) {
level = accessor.getLevel();
player = accessor.getPlayer();
connected = accessor.isServerConnected();
showDetails = accessor.showDetails();
hit = accessor.getHitResult();
blockEntity = accessor::getBlockEntity;
blockState = accessor.getBlockState();
return this;
}
@Override
public BlockAccessor build() {
return new BlockAccessorImpl(this);
}
}
public record SyncData(boolean showDetails, BlockHitResult hit, BlockState blockState, ItemStack fakeBlock) {
public static final StreamCodec<RegistryFriendlyByteBuf, SyncData> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL,
SyncData::showDetails,
StreamCodec.of(FriendlyByteBuf::writeBlockHitResult, FriendlyByteBuf::readBlockHitResult),
SyncData::hit,
ByteBufCodecs.idMapper(Block.BLOCK_STATE_REGISTRY),
SyncData::blockState,
ItemStack.OPTIONAL_STREAM_CODEC,
SyncData::fakeBlock,
SyncData::new
);
public BlockAccessor unpack(ServerPlayer player) {
Supplier<BlockEntity> blockEntity = null;
if (blockState.hasBlockEntity()) {
blockEntity = Suppliers.memoize(() -> player.level().getBlockEntity(hit.getBlockPos()));
}
return new Builder()
.level(player.level())
.player(player)
.showDetails(showDetails)
.hit(hit)
.blockState(blockState)
.blockEntity(blockEntity)
.build();
}
}
}

View File

@@ -0,0 +1,44 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.EntityHitResult;
import org.jetbrains.annotations.ApiStatus;
import java.util.function.Supplier;
public interface EntityAccessor extends Accessor<EntityHitResult> {
Entity getEntity();
/**
* For part entity like ender dragon's, getEntity() will return the parent entity.
*/
Entity getRawEntity();
@ApiStatus.NonExtendable
interface Builder {
Builder level(Level level);
Builder player(Player player);
Builder showDetails(boolean showDetails);
default Builder hit(EntityHitResult hit) {
return hit(() -> hit);
}
Builder hit(Supplier<EntityHitResult> hit);
default Builder entity(Entity entity) {
return entity(() -> entity);
}
Builder entity(Supplier<Entity> entity);
Builder from(EntityAccessor accessor);
EntityAccessor build();
}
}

View File

@@ -0,0 +1,123 @@
package org.leavesmc.leaves.protocol.jade.accessor;
import com.google.common.base.Suppliers;
import net.minecraft.network.RegistryFriendlyByteBuf;
import net.minecraft.network.codec.ByteBufCodecs;
import net.minecraft.network.codec.StreamCodec;
import net.minecraft.server.level.ServerPlayer;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.EntityHitResult;
import net.minecraft.world.phys.Vec3;
import org.jetbrains.annotations.NotNull;
import org.leavesmc.leaves.protocol.jade.util.CommonUtil;
import java.util.function.Supplier;
public class EntityAccessorImpl extends AccessorImpl<EntityHitResult> implements EntityAccessor {
private final Supplier<Entity> entity;
public EntityAccessorImpl(Builder builder) {
super(builder.level, builder.player, builder.hit, builder.connected, builder.showDetails);
entity = builder.entity;
}
@Override
public Entity getEntity() {
return CommonUtil.wrapPartEntityParent(getRawEntity());
}
@Override
public Entity getRawEntity() {
return entity.get();
}
@NotNull
@Override
public Object getTarget() {
return getEntity();
}
public static class Builder implements EntityAccessor.Builder {
public boolean showDetails;
private Level level;
private Player player;
private boolean connected;
private Supplier<EntityHitResult> hit;
private Supplier<Entity> entity;
@Override
public Builder level(Level level) {
this.level = level;
return this;
}
@Override
public Builder player(Player player) {
this.player = player;
return this;
}
@Override
public Builder showDetails(boolean showDetails) {
this.showDetails = showDetails;
return this;
}
@Override
public Builder hit(Supplier<EntityHitResult> hit) {
this.hit = hit;
return this;
}
@Override
public Builder entity(Supplier<Entity> entity) {
this.entity = entity;
return this;
}
@Override
public Builder from(EntityAccessor accessor) {
level = accessor.getLevel();
player = accessor.getPlayer();
connected = accessor.isServerConnected();
showDetails = accessor.showDetails();
hit = accessor::getHitResult;
entity = accessor::getEntity;
return this;
}
@Override
public EntityAccessor build() {
return new EntityAccessorImpl(this);
}
}
public record SyncData(boolean showDetails, int id, int partIndex, Vec3 hitVec) {
public static final StreamCodec<RegistryFriendlyByteBuf, SyncData> STREAM_CODEC = StreamCodec.composite(
ByteBufCodecs.BOOL,
SyncData::showDetails,
ByteBufCodecs.VAR_INT,
SyncData::id,
ByteBufCodecs.VAR_INT,
SyncData::partIndex,
ByteBufCodecs.VECTOR3F.map(Vec3::new, Vec3::toVector3f),
SyncData::hitVec,
SyncData::new
);
public EntityAccessor unpack(ServerPlayer player) {
Supplier<Entity> entity = Suppliers.memoize(() -> CommonUtil.getPartEntity(player.level().getEntity(id), partIndex));
return new EntityAccessorImpl.Builder()
.level(player.level())
.player(player)
.showDetails(showDetails)
.entity(entity)
.hit(Suppliers.memoize(() -> new EntityHitResult(entity.get(), hitVec)))
.build();
}
}
}

Some files were not shown because too many files have changed in this diff Show More