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:
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
leaf-server/src/main/java/org/dreeam/leaf/LeafBootstrap.java
Normal file
17
leaf-server/src/main/java/org/dreeam/leaf/LeafBootstrap.java
Normal 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() {
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package org.dreeam.leaf.async.path;
|
||||
|
||||
public enum PathProcessState {
|
||||
WAITING,
|
||||
PROCESSING,
|
||||
COMPLETED,
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
271
leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java
Normal file
271
leaf-server/src/main/java/org/dreeam/leaf/config/LeafConfig.java
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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 {
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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 让玩家进入已满的服务器."""));
|
||||
}
|
||||
}
|
||||
@@ -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 漏洞!!"""));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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! 警告."
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"使玩家可以击退僵尸."
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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.""",
|
||||
"""
|
||||
**实验性功能, 请及时反馈你遇到的问题!**
|
||||
是否在玩家切换世界时尝试使用 "平滑传送".
|
||||
此项要求源世界和目标世界逻辑高度相同才会生效."""
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"缓存过期时间. 单位: 分钟."
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
"玩家退出服务器时的消息"));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"移除修改无法编辑的告示牌时输出的警告."
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -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 模式即可让玩家加入后端服务器."""));
|
||||
}
|
||||
}
|
||||
@@ -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.""",
|
||||
"""
|
||||
移除原版的用户名验证,
|
||||
让所有字符均可作为玩家名."""));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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>, 显示未知命令详细信息."""));
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
""",
|
||||
"""
|
||||
是否启用聊天签名, 禁用后玩家无法进行聊天举报."""));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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的实体类型转换."""));
|
||||
}
|
||||
}
|
||||
@@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"更快的结构生成任务分段."));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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."""));
|
||||
}
|
||||
}
|
||||
@@ -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 目标选择器.
|
||||
有助于提升性能, 但对游戏有轻微影响."""));
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"是否为异步任务调度器使用虚拟线程."));
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"是否为异步聊天线程使用虚拟线程."));
|
||||
}
|
||||
}
|
||||
@@ -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.",
|
||||
"是否为用户验证器使用虚拟线程."));
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
21
leaf-server/src/main/java/org/dreeam/leaf/util/cache/CachedOrNewBitsGetter.java
vendored
Normal file
21
leaf-server/src/main/java/org/dreeam/leaf/util/cache/CachedOrNewBitsGetter.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
71
leaf-server/src/main/java/org/dreeam/leaf/util/cache/IterateOutwardsCache.java
vendored
Normal file
71
leaf-server/src/main/java/org/dreeam/leaf/util/cache/IterateOutwardsCache.java
vendored
Normal 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;
|
||||
}
|
||||
}
|
||||
46
leaf-server/src/main/java/org/dreeam/leaf/util/cache/LongList2BlockPosMutableIterable.java
vendored
Normal file
46
leaf-server/src/main/java/org/dreeam/leaf/util/cache/LongList2BlockPosMutableIterable.java
vendored
Normal 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));
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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() + '}';
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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()));
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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
Reference in New Issue
Block a user