Push forward
This commit is contained in:
@@ -0,0 +1,621 @@
|
||||
package abomination;
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.io.MoonriseRegionFileIO;
|
||||
import com.github.luben.zstd.ZstdInputStream;
|
||||
import com.github.luben.zstd.ZstdOutputStream;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import io.nanachiyo0721.shiroha.data.RegionFile;
|
||||
import net.jpountz.lz4.LZ4Compressor;
|
||||
import net.jpountz.lz4.LZ4Factory;
|
||||
import net.jpountz.lz4.LZ4FastDecompressor;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFileVersion;
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
import net.openhft.hashing.LongHashFunction;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import javax.annotation.Nullable;
|
||||
import java.io.*;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.StandardCopyOption;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.locks.LockSupport;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
// LinearRegionFile_implementation_version_0_5byXymb
|
||||
// Just gonna use this string to inform other forks about updates ;-)
|
||||
public class LinearRegionFile implements RegionFile {
|
||||
private static final long SUPERBLOCK = 0xc3ff13183cca9d9aL;
|
||||
private static final byte VERSION = 3;
|
||||
private static final int HEADER_SIZE = 27;
|
||||
private static final int FOOTER_SIZE = 8;
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
private byte[][] bucketBuffers;
|
||||
private final byte[][] buffer = new byte[1024][];
|
||||
private final int[] bufferUncompressedSize = new int[1024];
|
||||
|
||||
private final long[] chunkTimestamps = new long[1024];
|
||||
private final Object markedToSaveLock = new Object();
|
||||
|
||||
private final LZ4Compressor compressor;
|
||||
private final LZ4FastDecompressor decompressor;
|
||||
|
||||
private boolean markedToSave = false;
|
||||
private boolean close = false;
|
||||
|
||||
public final ReentrantLock fileLock = new ReentrantLock(true);
|
||||
public Path regionFile;
|
||||
|
||||
private final int compressionLevel;
|
||||
private int gridSize = 8;
|
||||
private int bucketSize = 4;
|
||||
private final Thread bindThread;
|
||||
|
||||
public Path getRegionFile() {
|
||||
return this.regionFile;
|
||||
}
|
||||
|
||||
public ReentrantLock getFileLock() {
|
||||
return this.fileLock;
|
||||
}
|
||||
|
||||
private int chunkToBucketIdx(int chunkX, int chunkZ) {
|
||||
int bx = chunkX / bucketSize, bz = chunkZ / bucketSize;
|
||||
return bx * gridSize + bz;
|
||||
}
|
||||
|
||||
private void openBucket(int chunkX, int chunkZ) {
|
||||
chunkX = Math.floorMod(chunkX, 32);
|
||||
chunkZ = Math.floorMod(chunkZ, 32);
|
||||
int idx = chunkToBucketIdx(chunkX, chunkZ);
|
||||
|
||||
if (bucketBuffers == null) return;
|
||||
if (bucketBuffers[idx] != null) {
|
||||
try {
|
||||
ByteArrayInputStream bucketByteStream = new ByteArrayInputStream(bucketBuffers[idx]);
|
||||
ZstdInputStream zstdStream = new ZstdInputStream(bucketByteStream);
|
||||
ByteBuffer bucketBuffer = ByteBuffer.wrap(zstdStream.readAllBytes());
|
||||
|
||||
int bx = chunkX / bucketSize, bz = chunkZ / bucketSize;
|
||||
|
||||
for (int cx = 0; cx < 32 / gridSize; cx++) {
|
||||
for (int cz = 0; cz < 32 / gridSize; cz++) {
|
||||
int chunkIndex = (bx * (32 / gridSize) + cx) + (bz * (32 / gridSize) + cz) * 32;
|
||||
|
||||
int chunkSize = bucketBuffer.getInt();
|
||||
long timestamp = bucketBuffer.getLong();
|
||||
this.chunkTimestamps[chunkIndex] = timestamp;
|
||||
|
||||
if (chunkSize > 0) {
|
||||
byte[] chunkData = new byte[chunkSize - 8];
|
||||
bucketBuffer.get(chunkData);
|
||||
|
||||
int maxCompressedLength = this.compressor.maxCompressedLength(chunkData.length);
|
||||
byte[] compressed = new byte[maxCompressedLength];
|
||||
int compressedLength = this.compressor.compress(chunkData, 0, chunkData.length, compressed, 0, maxCompressedLength);
|
||||
byte[] finalCompressed = new byte[compressedLength];
|
||||
System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength);
|
||||
|
||||
// TODO: Optimization - return the requested chunk immediately to save on one LZ4 decompression
|
||||
this.buffer[chunkIndex] = finalCompressed;
|
||||
this.bufferUncompressedSize[chunkIndex] = chunkData.length;
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (IOException ex) {
|
||||
throw new RuntimeException("Region file corrupted: " + regionFile + " bucket: " + idx);
|
||||
// TODO: Make sure the server crashes instead of corrupting the world
|
||||
}
|
||||
bucketBuffers[idx] = null;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean regionFileOpen = false;
|
||||
|
||||
private synchronized void openRegionFile() {
|
||||
if (regionFileOpen) return;
|
||||
regionFileOpen = true;
|
||||
|
||||
File regionFile = new File(this.regionFile.toString());
|
||||
|
||||
if (!regionFile.canRead()) {
|
||||
this.bindThread.start();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] fileContent = Files.readAllBytes(this.regionFile);
|
||||
ByteBuffer buffer = ByteBuffer.wrap(fileContent);
|
||||
|
||||
long superBlock = buffer.getLong();
|
||||
if (superBlock != SUPERBLOCK)
|
||||
throw new RuntimeException("Invalid superblock: " + superBlock + " file " + this.regionFile);
|
||||
|
||||
byte version = buffer.get();
|
||||
if (version == 1 || version == 2) {
|
||||
parseLinearV1(buffer);
|
||||
} else if (version == 3) {
|
||||
parseLinearV2(buffer);
|
||||
} else {
|
||||
throw new RuntimeException("Invalid version: " + version + " file " + this.regionFile);
|
||||
}
|
||||
|
||||
this.bindThread.start();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to open region file " + this.regionFile, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseLinearV1(ByteBuffer buffer) throws IOException {
|
||||
final int HEADER_SIZE = 32;
|
||||
final int FOOTER_SIZE = 8;
|
||||
|
||||
// Skip newestTimestamp (Long) + Compression level (Byte) + Chunk count (Short): Unused.
|
||||
buffer.position(buffer.position() + 11);
|
||||
|
||||
int dataCount = buffer.getInt();
|
||||
long fileLength = this.regionFile.toFile().length();
|
||||
if (fileLength != HEADER_SIZE + dataCount + FOOTER_SIZE) {
|
||||
throw new IOException("Invalid file length: " + this.regionFile + " " + fileLength + " " + (HEADER_SIZE + dataCount + FOOTER_SIZE));
|
||||
}
|
||||
|
||||
buffer.position(buffer.position() + 8); // Skip data hash (Long): Unused.
|
||||
|
||||
byte[] rawCompressed = new byte[dataCount];
|
||||
buffer.get(rawCompressed);
|
||||
|
||||
ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(rawCompressed);
|
||||
ZstdInputStream zstdInputStream = new ZstdInputStream(byteArrayInputStream);
|
||||
ByteBuffer decompressedBuffer = ByteBuffer.wrap(zstdInputStream.readAllBytes());
|
||||
|
||||
int[] starts = new int[1024];
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
starts[i] = decompressedBuffer.getInt();
|
||||
decompressedBuffer.getInt(); // Skip timestamps (Int): Unused.
|
||||
}
|
||||
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
if (starts[i] > 0) {
|
||||
int size = starts[i];
|
||||
byte[] chunkData = new byte[size];
|
||||
decompressedBuffer.get(chunkData);
|
||||
|
||||
int maxCompressedLength = this.compressor.maxCompressedLength(size);
|
||||
byte[] compressed = new byte[maxCompressedLength];
|
||||
int compressedLength = this.compressor.compress(chunkData, 0, size, compressed, 0, maxCompressedLength);
|
||||
byte[] finalCompressed = new byte[compressedLength];
|
||||
System.arraycopy(compressed, 0, finalCompressed, 0, compressedLength);
|
||||
|
||||
this.buffer[i] = finalCompressed;
|
||||
this.bufferUncompressedSize[i] = size;
|
||||
this.chunkTimestamps[i] = getTimestamp(); // Use current timestamp as we don't have the original
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void parseLinearV2(ByteBuffer buffer) throws IOException {
|
||||
buffer.getLong(); // Skip newestTimestamp (Long)
|
||||
gridSize = buffer.get();
|
||||
if (gridSize != 1 && gridSize != 2 && gridSize != 4 && gridSize != 8 && gridSize != 16 && gridSize != 32)
|
||||
throw new RuntimeException("Invalid grid size: " + gridSize + " file " + this.regionFile);
|
||||
bucketSize = 32 / gridSize;
|
||||
|
||||
buffer.getInt(); // Skip region_x (Int)
|
||||
buffer.getInt(); // Skip region_z (Int)
|
||||
|
||||
boolean[] chunkExistenceBitmap = deserializeExistenceBitmap(buffer);
|
||||
|
||||
while (true) {
|
||||
byte featureNameLength = buffer.get();
|
||||
if (featureNameLength == 0) break;
|
||||
byte[] featureNameBytes = new byte[featureNameLength];
|
||||
buffer.get(featureNameBytes);
|
||||
String featureName = new String(featureNameBytes);
|
||||
int featureValue = buffer.getInt();
|
||||
// System.out.println("NBT Feature: " + featureName + " = " + featureValue);
|
||||
}
|
||||
|
||||
int[] bucketSizes = new int[gridSize * gridSize];
|
||||
byte[] bucketCompressionLevels = new byte[gridSize * gridSize];
|
||||
long[] bucketHashes = new long[gridSize * gridSize];
|
||||
for (int i = 0; i < gridSize * gridSize; i++) {
|
||||
bucketSizes[i] = buffer.getInt();
|
||||
bucketCompressionLevels[i] = buffer.get();
|
||||
bucketHashes[i] = buffer.getLong();
|
||||
}
|
||||
|
||||
bucketBuffers = new byte[gridSize * gridSize][];
|
||||
for (int i = 0; i < gridSize * gridSize; i++) {
|
||||
if (bucketSizes[i] > 0) {
|
||||
bucketBuffers[i] = new byte[bucketSizes[i]];
|
||||
buffer.get(bucketBuffers[i]);
|
||||
long rawHash = LongHashFunction.xx().hashBytes(bucketBuffers[i]);
|
||||
if (rawHash != bucketHashes[i]) throw new IOException("Region file hash incorrect " + this.regionFile);
|
||||
}
|
||||
}
|
||||
|
||||
long footerSuperBlock = buffer.getLong();
|
||||
if (footerSuperBlock != SUPERBLOCK)
|
||||
throw new IOException("Footer superblock invalid " + this.regionFile);
|
||||
}
|
||||
|
||||
public LinearRegionFile(RegionStorageInfo storageKey, Path directory, Path path, boolean dsync, int compressionLevel) throws IOException {
|
||||
this(storageKey, directory, path, RegionFileVersion.getSelected(), dsync, compressionLevel);
|
||||
}
|
||||
|
||||
public LinearRegionFile(RegionStorageInfo storageKey, Path path, Path directory, RegionFileVersion compressionFormat, boolean dsync, int compressionLevel) throws IOException {
|
||||
Runnable flushCheck = () -> {
|
||||
while (!close) {
|
||||
synchronized (saveLock) {
|
||||
if (markedToSave && activeSaveThreads < SAVE_THREAD_MAX_COUNT) {
|
||||
activeSaveThreads++;
|
||||
Runnable flushOperation = () -> {
|
||||
try {
|
||||
flush();
|
||||
} catch (IOException ex) {
|
||||
LOGGER.error("Region file {} flush failed", this.regionFile.toAbsolutePath(), ex);
|
||||
} finally {
|
||||
synchronized (saveLock) {
|
||||
activeSaveThreads--;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Thread saveThread = USE_VIRTUAL_THREAD ?
|
||||
Thread.ofVirtual().name("Linear IO - " + LinearRegionFile.this.hashCode()).unstarted(flushOperation) :
|
||||
Thread.ofPlatform().name("Linear IO - " + LinearRegionFile.this.hashCode()).unstarted(flushOperation);
|
||||
saveThread.setPriority(Thread.NORM_PRIORITY - 3);
|
||||
saveThread.start();
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(SAVE_DELAY_MS));
|
||||
}
|
||||
};
|
||||
this.bindThread = USE_VIRTUAL_THREAD ? Thread.ofVirtual().unstarted(flushCheck) : Thread.ofPlatform().unstarted(flushCheck);
|
||||
this.bindThread.setName("Linear IO Schedule - " + this.hashCode());
|
||||
this.regionFile = path;
|
||||
this.compressionLevel = compressionLevel;
|
||||
|
||||
this.compressor = LZ4Factory.fastestInstance().fastCompressor();
|
||||
this.decompressor = LZ4Factory.fastestInstance().fastDecompressor();
|
||||
}
|
||||
|
||||
private synchronized void markToSave() {
|
||||
synchronized (markedToSaveLock) {
|
||||
markedToSave = true;
|
||||
}
|
||||
}
|
||||
|
||||
private synchronized boolean isMarkedToSave() {
|
||||
synchronized (markedToSaveLock) {
|
||||
if (markedToSave) {
|
||||
markedToSave = false;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
public static int SAVE_THREAD_MAX_COUNT = 6;
|
||||
public static int SAVE_DELAY_MS = 100;
|
||||
public static boolean USE_VIRTUAL_THREAD = true;
|
||||
private static final Object saveLock = new Object();
|
||||
private static int activeSaveThreads = 0;
|
||||
|
||||
/*public void run() {
|
||||
while (!close) {
|
||||
synchronized (saveLock) {
|
||||
if (markedToSave && activeSaveThreads < SAVE_THREAD_MAX_COUNT) {
|
||||
activeSaveThreads++;
|
||||
Thread saveThread = new Thread(() -> {
|
||||
try {
|
||||
flush();
|
||||
} catch (IOException ex) {
|
||||
LOGGER.error("Region file " + this.regionFile.toAbsolutePath() + " flush failed", ex);
|
||||
} finally {
|
||||
synchronized (saveLock) {
|
||||
activeSaveThreads--;
|
||||
}
|
||||
}
|
||||
}, "RegionFileFlush");
|
||||
saveThread.setPriority(Thread.NORM_PRIORITY - 3);
|
||||
saveThread.start();
|
||||
}
|
||||
}
|
||||
LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(SAVE_DELAY_MS));
|
||||
}
|
||||
}*/
|
||||
|
||||
public synchronized boolean doesChunkExist(ChunkPos pos) throws Exception {
|
||||
openRegionFile();
|
||||
throw new Exception("doesChunkExist is a stub");
|
||||
}
|
||||
|
||||
public synchronized void flush() throws IOException {
|
||||
if (!isMarkedToSave()) return;
|
||||
|
||||
openRegionFile();
|
||||
|
||||
long timestamp = getTimestamp();
|
||||
|
||||
long writeStart = System.nanoTime();
|
||||
File tempFile = new File(regionFile.toString() + ".tmp");
|
||||
FileOutputStream fileStream = new FileOutputStream(tempFile);
|
||||
DataOutputStream dataStream = new DataOutputStream(fileStream);
|
||||
|
||||
dataStream.writeLong(SUPERBLOCK);
|
||||
dataStream.writeByte(VERSION);
|
||||
dataStream.writeLong(timestamp);
|
||||
dataStream.writeByte(gridSize);
|
||||
|
||||
String fileName = regionFile.getFileName().toString();
|
||||
String[] parts = fileName.split("\\.");
|
||||
int regionX = 0;
|
||||
int regionZ = 0;
|
||||
try {
|
||||
if (parts.length >= 4) {
|
||||
regionX = Integer.parseInt(parts[1]);
|
||||
regionZ = Integer.parseInt(parts[2]);
|
||||
} else {
|
||||
LOGGER.warn("Unexpected file name format: " + fileName);
|
||||
}
|
||||
} catch (NumberFormatException e) {
|
||||
LOGGER.error("Failed to parse region coordinates from file name: " + fileName, e);
|
||||
}
|
||||
|
||||
dataStream.writeInt(regionX);
|
||||
dataStream.writeInt(regionZ);
|
||||
|
||||
boolean[] chunkExistenceBitmap = new boolean[1024];
|
||||
for (int i = 0; i < 1024; i++) {
|
||||
chunkExistenceBitmap[i] = (this.bufferUncompressedSize[i] > 0);
|
||||
}
|
||||
writeSerializedExistenceBitmap(dataStream, chunkExistenceBitmap);
|
||||
|
||||
writeNBTFeatures(dataStream);
|
||||
|
||||
int bucketMisses = 0;
|
||||
byte[][] buckets = new byte[gridSize * gridSize][];
|
||||
for (int bx = 0; bx < gridSize; bx++) {
|
||||
for (int bz = 0; bz < gridSize; bz++) {
|
||||
if (bucketBuffers != null && bucketBuffers[bx * gridSize + bz] != null) {
|
||||
buckets[bx * gridSize + bz] = bucketBuffers[bx * gridSize + bz];
|
||||
continue;
|
||||
}
|
||||
bucketMisses++;
|
||||
|
||||
ByteArrayOutputStream bucketStream = new ByteArrayOutputStream();
|
||||
ZstdOutputStream zstdStream = new ZstdOutputStream(bucketStream, this.compressionLevel);
|
||||
DataOutputStream bucketDataStream = new DataOutputStream(zstdStream);
|
||||
|
||||
boolean hasData = false;
|
||||
for (int cx = 0; cx < 32 / gridSize; cx++) {
|
||||
for (int cz = 0; cz < 32 / gridSize; cz++) {
|
||||
int chunkIndex = (bx * 32 / gridSize + cx) + (bz * 32 / gridSize + cz) * 32;
|
||||
if (this.bufferUncompressedSize[chunkIndex] > 0) {
|
||||
hasData = true;
|
||||
byte[] chunkData = new byte[this.bufferUncompressedSize[chunkIndex]];
|
||||
this.decompressor.decompress(this.buffer[chunkIndex], 0, chunkData, 0, this.bufferUncompressedSize[chunkIndex]);
|
||||
bucketDataStream.writeInt(chunkData.length + 8);
|
||||
bucketDataStream.writeLong(this.chunkTimestamps[chunkIndex]);
|
||||
bucketDataStream.write(chunkData);
|
||||
} else {
|
||||
bucketDataStream.writeInt(0);
|
||||
bucketDataStream.writeLong(this.chunkTimestamps[chunkIndex]);
|
||||
}
|
||||
}
|
||||
}
|
||||
bucketDataStream.close();
|
||||
|
||||
if (hasData) {
|
||||
buckets[bx * gridSize + bz] = bucketStream.toByteArray();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < gridSize * gridSize; i++) {
|
||||
dataStream.writeInt(buckets[i] != null ? buckets[i].length : 0);
|
||||
dataStream.writeByte(this.compressionLevel);
|
||||
long rawHash = 0;
|
||||
if (buckets[i] != null) {
|
||||
rawHash = LongHashFunction.xx().hashBytes(buckets[i]);
|
||||
}
|
||||
dataStream.writeLong(rawHash);
|
||||
}
|
||||
|
||||
for (int i = 0; i < gridSize * gridSize; i++) {
|
||||
if (buckets[i] != null) {
|
||||
dataStream.write(buckets[i]);
|
||||
}
|
||||
}
|
||||
|
||||
dataStream.writeLong(SUPERBLOCK);
|
||||
|
||||
dataStream.flush();
|
||||
fileStream.getFD().sync();
|
||||
fileStream.getChannel().force(true); // Ensure atomicity on Btrfs
|
||||
dataStream.close();
|
||||
|
||||
fileStream.close();
|
||||
Files.move(tempFile.toPath(), this.regionFile, StandardCopyOption.REPLACE_EXISTING);
|
||||
//System.out.println("writeStart REGION FILE FLUSH " + (System.nanoTime() - writeStart) + " misses: " + bucketMisses);
|
||||
}
|
||||
|
||||
private void writeNBTFeatures(DataOutputStream dataStream) throws IOException {
|
||||
// writeNBTFeature(dataStream, "example", 1);
|
||||
dataStream.writeByte(0); // End of NBT features
|
||||
}
|
||||
|
||||
private void writeNBTFeature(DataOutputStream dataStream, String featureName, int featureValue) throws IOException {
|
||||
byte[] featureNameBytes = featureName.getBytes();
|
||||
dataStream.writeByte(featureNameBytes.length);
|
||||
dataStream.write(featureNameBytes);
|
||||
dataStream.writeInt(featureValue);
|
||||
}
|
||||
|
||||
public static final int MAX_CHUNK_SIZE = 500 * 1024 * 1024; // Abomination - prevent chunk dupe
|
||||
|
||||
public synchronized void write(ChunkPos pos, ByteBuffer buffer) {
|
||||
openRegionFile();
|
||||
openBucket(pos.x(), pos.z());
|
||||
try {
|
||||
byte[] b = toByteArray(new ByteArrayInputStream(buffer.array()));
|
||||
int uncompressedSize = b.length;
|
||||
|
||||
if (uncompressedSize > MAX_CHUNK_SIZE) {
|
||||
LOGGER.error("Chunk dupe attempt " + this.regionFile);
|
||||
clear(pos);
|
||||
} else {
|
||||
int maxCompressedLength = this.compressor.maxCompressedLength(b.length);
|
||||
byte[] compressed = new byte[maxCompressedLength];
|
||||
int compressedLength = this.compressor.compress(b, 0, b.length, compressed, 0, maxCompressedLength);
|
||||
b = new byte[compressedLength];
|
||||
System.arraycopy(compressed, 0, b, 0, compressedLength);
|
||||
|
||||
int index = getChunkIndex(pos.x(), pos.z());
|
||||
this.buffer[index] = b;
|
||||
this.chunkTimestamps[index] = getTimestamp();
|
||||
this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] = uncompressedSize;
|
||||
}
|
||||
} catch (IOException e) {
|
||||
LOGGER.error("Chunk write IOException " + e + " " + this.regionFile);
|
||||
}
|
||||
markToSave();
|
||||
}
|
||||
|
||||
public DataOutputStream getChunkDataOutputStream(ChunkPos pos) {
|
||||
openRegionFile();
|
||||
openBucket(pos.x(), pos.z());
|
||||
return new DataOutputStream(new BufferedOutputStream(new ChunkBuffer(pos)));
|
||||
}
|
||||
|
||||
@Override
|
||||
public MoonriseRegionFileIO.RegionDataController.WriteData moonrise$startWrite(CompoundTag data, ChunkPos pos) throws IOException {
|
||||
final DataOutputStream out = this.getChunkDataOutputStream(pos);
|
||||
|
||||
return new MoonriseRegionFileIO.RegionDataController.WriteData(
|
||||
data, MoonriseRegionFileIO.RegionDataController.WriteData.WriteResult.WRITE,
|
||||
out, regionFile -> out.close()
|
||||
);
|
||||
}
|
||||
|
||||
private class ChunkBuffer extends ByteArrayOutputStream {
|
||||
|
||||
private final ChunkPos pos;
|
||||
|
||||
public ChunkBuffer(ChunkPos chunkcoordintpair) {
|
||||
super();
|
||||
this.pos = chunkcoordintpair;
|
||||
}
|
||||
|
||||
public void close() throws IOException {
|
||||
ByteBuffer bytebuffer = ByteBuffer.wrap(this.buf, 0, this.count);
|
||||
LinearRegionFile.this.write(this.pos, bytebuffer);
|
||||
}
|
||||
}
|
||||
|
||||
private byte[] toByteArray(InputStream in) throws IOException {
|
||||
ByteArrayOutputStream out = new ByteArrayOutputStream();
|
||||
byte[] tempBuffer = new byte[4096];
|
||||
|
||||
int length;
|
||||
while ((length = in.read(tempBuffer)) >= 0) {
|
||||
out.write(tempBuffer, 0, length);
|
||||
}
|
||||
|
||||
return out.toByteArray();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public synchronized DataInputStream getChunkDataInputStream(ChunkPos pos) {
|
||||
openRegionFile();
|
||||
openBucket(pos.x(), pos.z());
|
||||
|
||||
if (this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] != 0) {
|
||||
byte[] content = new byte[bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())]];
|
||||
this.decompressor.decompress(this.buffer[getChunkIndex(pos.x(), pos.z())], 0, content, 0, bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())]);
|
||||
return new DataInputStream(new ByteArrayInputStream(content));
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public synchronized void clear(ChunkPos pos) {
|
||||
openRegionFile();
|
||||
openBucket(pos.x(), pos.z());
|
||||
int i = getChunkIndex(pos.x(), pos.z());
|
||||
this.buffer[i] = null;
|
||||
this.bufferUncompressedSize[i] = 0;
|
||||
this.chunkTimestamps[i] = 0;
|
||||
markToSave();
|
||||
}
|
||||
|
||||
public synchronized boolean hasChunk(ChunkPos pos) {
|
||||
openRegionFile();
|
||||
openBucket(pos.x(), pos.z());
|
||||
return this.bufferUncompressedSize[getChunkIndex(pos.x(), pos.z())] > 0;
|
||||
}
|
||||
|
||||
public synchronized void close() throws IOException {
|
||||
openRegionFile();
|
||||
close = true;
|
||||
try {
|
||||
flush();
|
||||
} catch (IOException e) {
|
||||
throw new IOException("Region flush IOException " + e + " " + this.regionFile);
|
||||
}
|
||||
}
|
||||
|
||||
private static int getChunkIndex(int x, int z) {
|
||||
return (x & 31) + ((z & 31) << 5);
|
||||
}
|
||||
|
||||
private static int getTimestamp() {
|
||||
return (int) (System.currentTimeMillis() / 1000L);
|
||||
}
|
||||
|
||||
public boolean recalculateHeader() {
|
||||
return false;
|
||||
}
|
||||
|
||||
public void setOversized(int x, int z, boolean something) {
|
||||
}
|
||||
|
||||
public CompoundTag getOversizedData(int x, int z) throws IOException {
|
||||
throw new IOException("getOversizedData is a stub " + this.regionFile);
|
||||
}
|
||||
|
||||
public boolean isOversized(int x, int z) {
|
||||
return false;
|
||||
}
|
||||
|
||||
public Path getPath() {
|
||||
return this.regionFile;
|
||||
}
|
||||
|
||||
private boolean[] deserializeExistenceBitmap(ByteBuffer buffer) {
|
||||
boolean[] result = new boolean[1024];
|
||||
for (int i = 0; i < 128; i++) {
|
||||
byte b = buffer.get();
|
||||
for (int j = 0; j < 8; j++) {
|
||||
result[i * 8 + j] = ((b >> (7 - j)) & 1) == 1;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
private void writeSerializedExistenceBitmap(DataOutputStream out, boolean[] bitmap) throws IOException {
|
||||
for (int i = 0; i < 128; i++) {
|
||||
byte b = 0;
|
||||
for (int j = 0; j < 8; j++) {
|
||||
if (bitmap[i * 8 + j]) {
|
||||
b |= (1 << (7 - j));
|
||||
}
|
||||
}
|
||||
out.writeByte(b);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package com.kiocg;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class ChunkHot {
|
||||
// 热度统计总区间数量
|
||||
private static final int TIMES_LENGTH = 10;
|
||||
// 当前统计区间下标
|
||||
private int index = -1;
|
||||
|
||||
// 热度统计区间
|
||||
private final long[] times = new long[TIMES_LENGTH];
|
||||
// 存放临时的区间数值
|
||||
// 用于修正正在统计的当前区间热度没有计入总值的问题
|
||||
private long temp;
|
||||
// 所有区间的热度总值
|
||||
private long total;
|
||||
|
||||
// 用于每个具体统计的计算
|
||||
private long nanos;
|
||||
// 当前统计是否进行中
|
||||
private volatile boolean started = false;
|
||||
|
||||
/**
|
||||
* 更新区间下标
|
||||
*/
|
||||
public void nextTick() {
|
||||
this.index = ++this.index % TIMES_LENGTH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始统计一个新区间
|
||||
*/
|
||||
public void start() {
|
||||
started = true;
|
||||
temp = times[this.index];
|
||||
times[this.index] = 0L;
|
||||
}
|
||||
|
||||
public boolean isStarted() {
|
||||
return this.started;
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束当前区间的统计
|
||||
* 将统计值更新入热度总值
|
||||
*/
|
||||
public void stop() {
|
||||
started = false;
|
||||
total -= temp;
|
||||
total += times[this.index];
|
||||
}
|
||||
|
||||
/**
|
||||
* 开始一个具体统计
|
||||
*/
|
||||
public void startTicking() {
|
||||
if (!started) return;
|
||||
nanos = System.nanoTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* 结束一个具体统计
|
||||
* 将统计值计入当前热度区间
|
||||
*/
|
||||
public void stopTickingAndCount() {
|
||||
if (!started) return;
|
||||
// 定义一个具体统计的最大值为 1,000,000
|
||||
// 有时候某个具体统计的计算值会在某1刻飙升,可能是由于保存数据到磁盘?
|
||||
times[this.index] += Math.min(System.nanoTime() - nanos, 1000000L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 清空统计 (当区块卸载时)
|
||||
*/
|
||||
public void clear() {
|
||||
started = false;
|
||||
Arrays.fill(times, 0L);
|
||||
temp = 0L;
|
||||
total = 0L;
|
||||
nanos = 0L;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return 获取区块热度平均值
|
||||
*/
|
||||
public long getAverage() {
|
||||
return total / ((long) TIMES_LENGTH * 20L);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,158 @@
|
||||
/*
|
||||
* This file is part of Kaiiju (https://github.com/KaiijuMC/Kaiiju)
|
||||
*
|
||||
* Kaiiju is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Kaiiju is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Kaiiju. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package dev.kaiijumc.kaiiju;
|
||||
|
||||
import com.google.common.base.Throwables;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import io.github.classgraph.ClassGraph;
|
||||
import io.github.classgraph.ClassInfo;
|
||||
import io.github.classgraph.ScanResult;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.configuration.InvalidConfigurationException;
|
||||
import org.bukkit.configuration.file.YamlConfiguration;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.logging.Level;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
public class KaiijuEntityLimits {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
private static final File CONFIG_FOLDER = new File("luminol_config");
|
||||
|
||||
protected static final String HEADER =
|
||||
"Per region entity limits for Kaiiju.\n"
|
||||
+ "If there are more of particular entity type in a region than limit, entity ticking will be throttled.\n"
|
||||
+ "Example: for Wither limit 100 & 300 Withers in a region -> 100 Withers tick every tick & every Wither ticks every 3 ticks.\n"
|
||||
+ "Available entities: GlowSquid, Ambient, Bat, Animal, Bee, Cat, Chicken, Cod, Cow, Dolphin, Fish, FishSchool, Fox, Golem, IronGolem, "
|
||||
+ "MushroomCow, Ocelot, Panda, Parrot, Perchable, Pig, PolarBear, PufferFish, Rabbit, Salmon, Sheep, Snowman, Squid, TropicalFish, Turtle, "
|
||||
+ "WaterAnimal, Wolf, Allay, Axolotl, Camel, Frog, Tadpole, Goat, Horse, HorseAbstract, HorseChestedAbstract, HorseDonkey, HorseMule, "
|
||||
+ "HorseSkeleton, HorseZombie, Llama, LlamaTrader, Sniffer, EnderCrystal, EnderDragon, Wither, ArmorStand, Hanging, ItemFrame, Leash, "
|
||||
+ "Painting, GlowItemFrame, FallingBlock, Item, TNTPrimed, Blaze, CaveSpider, Creeper, Drowned, Enderman, Endermite, Evoker, Ghast, "
|
||||
+ "GiantZombie, Guardian, GuardianElder, IllagerAbstract, IllagerIllusioner, IllagerWizard, MagmaCube, Monster, MonsterPatrolling, Phantom, "
|
||||
+ "ZombifiedPiglin, Pillager, Ravager, Shulker, Silverfish, Skeleton, SkeletonAbstract, SkeletonStray, SkeletonWither, Slime, Spider, Strider, Vex, "
|
||||
+ "Vindicator, Witch, Zoglin, Zombie, ZombieHusk, ZombieVillager, Hoglin, Piglin, PiglinAbstract, PiglinBrute, Warden, Villager, "
|
||||
+ "VillagerTrader, Arrow, DragonFireball, Egg, EnderPearl, EnderSignal, EvokerFangs, Fireball, FireballFireball, Fireworks, FishingHook, "
|
||||
+ "LargeFireball, LlamaSpit, Potion, Projectile, ProjectileThrowable, ShulkerBullet, SmallFireball, Snowball, SpectralArrow, ThrownExpBottle, "
|
||||
+ "ThrownTrident, TippedArrow, WitherSkull, Raider, ChestBoat, Boat, MinecartAbstract, MinecartChest, MinecartCommandBlock, MinecartContainer, "
|
||||
+ "MinecartFurnace, MinecartHopper, MinecartMobSpawner, MinecartRideable, MinecartTNT\n";
|
||||
protected static final File ENTITY_LIMITS_FILE = new File(CONFIG_FOLDER, "kaiiju_entity_limits.yml");
|
||||
public static YamlConfiguration entityLimitsConfig;
|
||||
public static boolean enabled = false;
|
||||
|
||||
protected static Map<Class<? extends Entity>, EntityLimit> entityLimits;
|
||||
|
||||
static final String ENTITY_PREFIX = "Entity";
|
||||
|
||||
public static void init() {
|
||||
init(true);
|
||||
}
|
||||
|
||||
private static void init(boolean setup) {
|
||||
entityLimitsConfig = new YamlConfiguration();
|
||||
|
||||
if (ENTITY_LIMITS_FILE.exists()) {
|
||||
try {
|
||||
entityLimitsConfig.load(ENTITY_LIMITS_FILE);
|
||||
} catch (InvalidConfigurationException ex) {
|
||||
Bukkit.getLogger().log(Level.SEVERE, "Could not load kaiiju_entity_limits.yml, please correct your syntax errors", ex);
|
||||
throw Throwables.propagate(ex);
|
||||
} catch (IOException ignore) {
|
||||
}
|
||||
} else {
|
||||
if (setup) {
|
||||
entityLimitsConfig.options().header(HEADER);
|
||||
entityLimitsConfig.options().copyDefaults(true);
|
||||
entityLimitsConfig.set("enabled", enabled);
|
||||
entityLimitsConfig.set("Axolotl.limit", 1000);
|
||||
entityLimitsConfig.set("Axolotl.removal", 2000);
|
||||
try {
|
||||
entityLimitsConfig.save(ENTITY_LIMITS_FILE);
|
||||
} catch (IOException ex) {
|
||||
Bukkit.getLogger().log(Level.SEVERE, "Could not save " + ENTITY_LIMITS_FILE, ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enabled = entityLimitsConfig.getBoolean("enabled");
|
||||
|
||||
entityLimits = new Object2ObjectOpenHashMap<>();
|
||||
try (ScanResult scanResult = new ClassGraph().enableAllInfo().acceptPackages("net.minecraft.world.entity").scan()) {
|
||||
Map<String, ClassInfo> entityClasses = new HashMap<>();
|
||||
for (ClassInfo classInfo : scanResult.getAllClasses()) {
|
||||
Class<?> entityClass = Class.forName(classInfo.getName());
|
||||
if (Entity.class.isAssignableFrom(entityClass)) {
|
||||
String entityName = extractEntityName(entityClass.getSimpleName());
|
||||
entityClasses.put(entityName, classInfo);
|
||||
}
|
||||
}
|
||||
|
||||
for (String key : entityLimitsConfig.getKeys(false)) {
|
||||
if (key.equals("enabled")) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!entityClasses.containsKey(key)) {
|
||||
LOGGER.error("Unknown entity '" + key + "' in kaiiju-entity-limits.yml, skipping");
|
||||
continue;
|
||||
}
|
||||
int limit = entityLimitsConfig.getInt(key + ".limit");
|
||||
int removal = entityLimitsConfig.getInt(key + ".removal");
|
||||
|
||||
if (limit < 1) {
|
||||
LOGGER.error(key + " has a limit less than the minimum of 1, ignoring");
|
||||
continue;
|
||||
}
|
||||
if (removal <= limit && removal != -1) {
|
||||
LOGGER.error(key + " has a removal limit that is less than or equal to its limit, setting removal to limit * 10");
|
||||
removal = limit * 10;
|
||||
}
|
||||
|
||||
entityLimits.put((Class<? extends Entity>) Class.forName(entityClasses.get(key).getName()), new EntityLimit(limit, removal));
|
||||
}
|
||||
} catch (ClassNotFoundException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
|
||||
public static EntityLimit getEntityLimit(Entity entity) {
|
||||
return entityLimits.get(entity.getClass());
|
||||
}
|
||||
|
||||
private static String extractEntityName(String input) {
|
||||
int prefixLength = ENTITY_PREFIX.length();
|
||||
|
||||
if (input.length() <= prefixLength || !input.startsWith(ENTITY_PREFIX)) {
|
||||
return input;
|
||||
} else {
|
||||
return input.substring(prefixLength);
|
||||
}
|
||||
}
|
||||
|
||||
public record EntityLimit(int limit, int removal) {
|
||||
@Override
|
||||
public String toString() {
|
||||
return "EntityLimit{limit=" + limit + ", removal=" + removal + "}";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* This file is part of Kaiiju (https://github.com/KaiijuMC/Kaiiju)
|
||||
*
|
||||
* Kaiiju is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Kaiiju is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Kaiiju. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package dev.kaiijumc.kaiiju;
|
||||
|
||||
import io.papermc.paper.threadedregions.RegionizedWorldData;
|
||||
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
public class KaiijuEntityThrottler {
|
||||
private static class TickInfo {
|
||||
int currentTick;
|
||||
int continueFrom;
|
||||
int toTick;
|
||||
int toRemove;
|
||||
}
|
||||
|
||||
public static class EntityThrottlerReturn {
|
||||
public boolean skip;
|
||||
public boolean remove;
|
||||
}
|
||||
|
||||
private final Object2ObjectOpenHashMap<KaiijuEntityLimits.EntityLimit, TickInfo> entityLimitTickInfoMap = new Object2ObjectOpenHashMap<>();
|
||||
|
||||
public void tickLimiterStart() {
|
||||
for (TickInfo tickInfo : entityLimitTickInfoMap.values()) {
|
||||
tickInfo.currentTick = 0;
|
||||
}
|
||||
}
|
||||
|
||||
public EntityThrottlerReturn tickLimiterShouldSkip(Entity entity) {
|
||||
EntityThrottlerReturn retVal = new EntityThrottlerReturn();
|
||||
if (entity.isRemoved()) return retVal;
|
||||
KaiijuEntityLimits.EntityLimit entityLimit = KaiijuEntityLimits.getEntityLimit(entity);
|
||||
|
||||
if (entityLimit != null) {
|
||||
TickInfo tickInfo = entityLimitTickInfoMap.computeIfAbsent(entityLimit, el -> {
|
||||
TickInfo newTickInfo = new TickInfo();
|
||||
newTickInfo.toTick = entityLimit.limit();
|
||||
return newTickInfo;
|
||||
});
|
||||
|
||||
tickInfo.currentTick++;
|
||||
if (tickInfo.currentTick <= tickInfo.toRemove && entityLimit.removal() > 0) {
|
||||
retVal.skip = false;
|
||||
retVal.remove = true;
|
||||
return retVal;
|
||||
}
|
||||
|
||||
if (tickInfo.currentTick < tickInfo.continueFrom) {
|
||||
retVal.skip = true;
|
||||
return retVal;
|
||||
}
|
||||
if (tickInfo.currentTick - tickInfo.continueFrom < tickInfo.toTick) {
|
||||
retVal.skip = false;
|
||||
return retVal;
|
||||
}
|
||||
retVal.skip = true;
|
||||
return retVal;
|
||||
} else {
|
||||
retVal.skip = false;
|
||||
return retVal;
|
||||
}
|
||||
}
|
||||
|
||||
public void tickLimiterFinish(RegionizedWorldData regionizedWorldData) {
|
||||
for (var entry : entityLimitTickInfoMap.entrySet()) {
|
||||
KaiijuEntityLimits.EntityLimit entityLimit = entry.getKey();
|
||||
TickInfo tickInfo = entry.getValue();
|
||||
|
||||
int additionals = 0;
|
||||
int nextContinueFrom = tickInfo.continueFrom + tickInfo.toTick;
|
||||
if (nextContinueFrom >= tickInfo.currentTick) {
|
||||
additionals = entityLimit.limit() - (tickInfo.currentTick - tickInfo.continueFrom);
|
||||
nextContinueFrom = 0;
|
||||
}
|
||||
tickInfo.continueFrom = nextContinueFrom;
|
||||
tickInfo.toTick = entityLimit.limit() + additionals;
|
||||
|
||||
if (tickInfo.toRemove == 0 && tickInfo.currentTick > entityLimit.removal()) {
|
||||
tickInfo.toRemove = tickInfo.currentTick - entityLimit.removal();
|
||||
} else if (tickInfo.toRemove != 0) {
|
||||
tickInfo.toRemove = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.commands;
|
||||
|
||||
import io.nanachiyo0721.shiroha.commands.bar.BarCommand;
|
||||
|
||||
public class CommandRegister {
|
||||
/**
|
||||
* Register commands after config loading
|
||||
* This method is called after system configuration is fully loaded,
|
||||
* used to register commands that depend on complete configuration
|
||||
*/
|
||||
public static void register() {
|
||||
new BarCommand().register();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package io.nanachiyo0721.shiroha.commands.bar;
|
||||
|
||||
import io.nanachiyo0721.shiroha.commands.bar.sub.ToggleCommand;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.RootNode;
|
||||
|
||||
public class BarCommand extends RootNode {
|
||||
private static final String PERM_BASE = "luminol.commands.bar";
|
||||
|
||||
public BarCommand() {
|
||||
super("bar", PERM_BASE);
|
||||
children(
|
||||
new BarSubcommand(EnumBarType.TPS),
|
||||
new BarSubcommand(EnumBarType.MEMORY),
|
||||
new BarSubcommand(EnumBarType.REGION)
|
||||
);
|
||||
}
|
||||
|
||||
public static boolean hasPermission(@NotNull CommandSender sender, String... subcommand) {
|
||||
return hasPermission(PERM_BASE, sender, subcommand);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void register() {
|
||||
super.register();
|
||||
children.forEach(child -> {
|
||||
if (child instanceof BarSubcommand barSubcommand) {
|
||||
barSubcommand.getChildren().forEach(subChild -> {
|
||||
if (subChild instanceof ToggleCommand toggleCommand) {
|
||||
toggleCommand.register();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregister() {
|
||||
super.unregister();
|
||||
children.forEach(child -> {
|
||||
if (child instanceof BarSubcommand barSubcommand) {
|
||||
barSubcommand.getChildren().forEach(subChild -> {
|
||||
if (subChild instanceof ToggleCommand toggleCommand) {
|
||||
toggleCommand.unregister();
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package io.nanachiyo0721.shiroha.commands.bar;
|
||||
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.nanachiyo0721.shiroha.commands.bar.sub.ConfigEditCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.bar.sub.ToggleCommand;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.CommandNode;
|
||||
import org.leavesmc.leaves.command.LiteralNode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class BarSubcommand extends LiteralNode {
|
||||
public BarSubcommand(EnumBarType barType) {
|
||||
super(barType.getCommandName());
|
||||
children(
|
||||
new ToggleCommand(barType),
|
||||
new ConfigEditCommand(barType)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requires(@NotNull CommandSourceStack source) {
|
||||
return hasPermission(source.getSender());
|
||||
}
|
||||
|
||||
protected boolean hasPermission(CommandSender sender) {
|
||||
return BarCommand.hasPermission(sender, this.name);
|
||||
}
|
||||
|
||||
public List<CommandNode> getChildren() {
|
||||
return children;
|
||||
}
|
||||
}
|
||||
+74
@@ -0,0 +1,74 @@
|
||||
package io.nanachiyo0721.shiroha.commands.bar.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.BoolArgumentType;
|
||||
import io.nanachiyo0721.shiroha.api.config.ShirohaConfigsInstance;
|
||||
import io.nanachiyo0721.shiroha.config.ConfigManager;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.MembarConfig;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.RegionBarConfig;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.TpsBarConfig;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBarList;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.command.LiteralNode;
|
||||
|
||||
public class ConfigEditCommand extends LiteralNode {
|
||||
private final EnumBarType barType;
|
||||
|
||||
public ConfigEditCommand(EnumBarType barType) {
|
||||
super("config");
|
||||
this.barType = barType;
|
||||
children(
|
||||
BooleanArgument::new
|
||||
);
|
||||
}
|
||||
|
||||
private class BooleanArgument extends ArgumentNode<Boolean> {
|
||||
protected BooleanArgument() {
|
||||
super("boolean", BoolArgumentType.bool());
|
||||
}
|
||||
|
||||
// TODO
|
||||
@Contract(pure = true)
|
||||
private static boolean isEnabledInGlobal(@NonNull EnumBarType type) {
|
||||
return switch (type) {
|
||||
case TPS -> TpsBarConfig.tpsbarEnabled;
|
||||
case MEMORY -> MembarConfig.memoryBarEnabled;
|
||||
case REGION -> RegionBarConfig.regionbarEnabled;
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
boolean enabled = isEnabledInGlobal(barType);
|
||||
|
||||
boolean value = context.getArgument(BooleanArgument.class);
|
||||
if (value == enabled) {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Bar type with " + barType.getName() + " was already " + (value ? "enabled" : "disabled") + "!")
|
||||
.color(TextColor.color(255, 0, 0)));
|
||||
} else {
|
||||
ShirohaConfigsInstance config = ConfigManager.getConfigs(barType.getConfigOrigin());
|
||||
if (config.setConfig(barType.getConfigPath(), value)) {
|
||||
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Bar type with " + barType.getName() + (value ? " enabled" : " disabled") + " successfully!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
);
|
||||
|
||||
config.reloadAsync(true).thenAccept(_ -> {
|
||||
TickableStatusBarList.raiseGlobalReload();
|
||||
});
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package io.nanachiyo0721.shiroha.commands.bar.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.Commands;
|
||||
import io.papermc.paper.command.brigadier.PaperCommands;
|
||||
import io.nanachiyo0721.shiroha.commands.bar.BarCommand;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBarList;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.bukkit.entity.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
import org.leavesmc.leaves.command.LiteralNode;
|
||||
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ToggleCommand extends LiteralNode {
|
||||
private final EnumBarType barType;
|
||||
|
||||
public ToggleCommand(EnumBarType barType) {
|
||||
super("toggle");
|
||||
this.barType = barType;
|
||||
children(
|
||||
PlayerArg::new
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
if (!(context.getSender() instanceof Player player)) {
|
||||
context.getSender().sendMessage(Component.text("Only player can display bars!").color(TextColor.color(255, 0, 0)));
|
||||
return true;
|
||||
}
|
||||
return execute0(context, player);
|
||||
}
|
||||
|
||||
public boolean execute0(@NotNull CommandContext context, Player player) {
|
||||
final TickableStatusBarList barList = ((CraftPlayer) player).getHandle().statusBarList;
|
||||
|
||||
boolean enabled = barList.isEnabled(this.barType);
|
||||
|
||||
if (!enabled) {
|
||||
context.getSender().sendMessage(Component.text("Bar type with " + this.barType.getName() + " was already disabled!").color(TextColor.color(255, 0, 0)));
|
||||
return true;
|
||||
}
|
||||
|
||||
if (barList.isVisible(this.barType)) {
|
||||
context.getSender().sendMessage(Component.text("Disabled Bar type with " + this.barType.getName() + " for " + player.getName()).color(TextColor.color(0, 255, 0)));
|
||||
barList.setVisible(this.barType, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
context.getSender().sendMessage(Component.text("Enabled Bar type with " + this.barType.getName() + " for " + player.getName()).color(TextColor.color(0, 255, 0)));
|
||||
barList.setVisible(this.barType, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
private class PlayerArg extends ArgumentNode<String> {
|
||||
protected PlayerArg() {
|
||||
super("player", StringArgumentType.string());
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
Bukkit.getServer().getOnlinePlayers().forEach(player -> builder.suggest(player.getName()));
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String name = context.getArgument(PlayerArg.class);
|
||||
Player player = Bukkit.getServer().getPlayer(name);
|
||||
if (player == null) {
|
||||
player = Bukkit.getServer().getPlayer(UUID.fromString(name));
|
||||
if (player == null) {
|
||||
context.getSender().sendMessage(Component.text("Player " + name + " was not found!").color(TextColor.color(255, 0, 0)));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return execute0(context, player);
|
||||
}
|
||||
}
|
||||
|
||||
protected ArgumentBuilder<CommandSourceStack, ?> compile0() {
|
||||
ArgumentBuilder<CommandSourceStack, ?> builder = Commands.literal(this.barType.getCommandName()).requires(this::requires);
|
||||
|
||||
if (canExecute()) {
|
||||
builder = builder.executes(mojangCtx -> {
|
||||
CommandContext ctx = new CommandContext(mojangCtx);
|
||||
return execute(ctx) ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requires(@NotNull CommandSourceStack source) {
|
||||
return BarCommand.hasPermission(source.getSender(), this.barType.getName(), this.name);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void register() { // register for old version command
|
||||
PaperCommands.INSTANCE.setValid();
|
||||
PaperCommands.INSTANCE.getDispatcher().register((LiteralArgumentBuilder<CommandSourceStack>) compile0());
|
||||
PaperCommands.INSTANCE.invalidate();
|
||||
Bukkit.getOnlinePlayers().forEach(Player::updateCommands);
|
||||
}
|
||||
|
||||
public void unregister() { // unregister for old version command
|
||||
PaperCommands.INSTANCE.setValid();
|
||||
PaperCommands.INSTANCE.getDispatcher().getRoot().removeCommand(this.barType.getCommandName());
|
||||
PaperCommands.INSTANCE.invalidate();
|
||||
Bukkit.getOnlinePlayers().forEach(Player::updateCommands);
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config;
|
||||
|
||||
import io.nanachiyo0721.shiroha.commands.config.sub.*;
|
||||
import io.nanachiyo0721.shiroha.config.ConfigsInstance;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.RootNode;
|
||||
|
||||
public class ConfigCommand extends RootNode {
|
||||
public final ConfigsInstance config;
|
||||
public final String name;
|
||||
private final String PERM_BASE;
|
||||
|
||||
public ConfigCommand(String name, String commandName, ConfigsInstance config) {
|
||||
super(commandName, name + ".commands." + name + "config");
|
||||
this.name = name;
|
||||
this.PERM_BASE = name + ".commands." + name + "config";
|
||||
this.config = config;
|
||||
children(
|
||||
new ReloadCommand(this),
|
||||
new SetCommand(this),
|
||||
new ResetCommand(this),
|
||||
new OpenGuiCommand(this),
|
||||
new SubmitCommand(this),
|
||||
new CleanCommand(this),
|
||||
new ResetCommentsCommand(this)
|
||||
);
|
||||
}
|
||||
|
||||
public boolean hasPermission(@NotNull CommandSender sender, String... subcommand) {
|
||||
return hasPermission(PERM_BASE, sender, subcommand);
|
||||
}
|
||||
|
||||
public String getCommandName() {
|
||||
return super.name;
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config;
|
||||
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.LiteralNode;
|
||||
|
||||
public abstract class ConfigSubcommand extends LiteralNode {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
protected ConfigSubcommand(String name, ConfigCommand parent) {
|
||||
super(name);
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requires(@NotNull CommandSourceStack source) {
|
||||
return hasPermission(source.getSender());
|
||||
}
|
||||
|
||||
protected boolean hasPermission(CommandSender sender) {
|
||||
return parent.hasPermission(sender, this.name);
|
||||
}
|
||||
}
|
||||
+69
@@ -0,0 +1,69 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class CleanCommand extends ConfigSubcommand {
|
||||
public CleanCommand(ConfigCommand parent) {
|
||||
super("clean", parent);
|
||||
children(
|
||||
new PathArgument(parent)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("If you want to clean up useless items in the configuration file, please use /" + parent.getCommandName() + " clean confirm")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("confirm", StringArgumentType.string());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
builder.suggest("confirm");
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
String confirm = context.getArgument(PathArgument.class);
|
||||
if (!"confirm".equals(confirm)) {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Please use /" + parent.getCommandName() + " clean confirm to confirm!")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
parent.config.clean();
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Clean up in the configuration file successfully!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+83
@@ -0,0 +1,83 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import io.nanachiyo0721.shiroha.utils.dialog.ConfigCommandDialog;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast;
|
||||
|
||||
public class OpenGuiCommand extends ConfigSubcommand {
|
||||
public OpenGuiCommand(ConfigCommand parent) {
|
||||
super("open-gui", parent);
|
||||
children(
|
||||
new PathArgument(parent)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
if (context.getSender() instanceof CraftPlayer cPlayer) {
|
||||
final Player player = cPlayer.getHandle();
|
||||
ConfigCommandDialog.openGui(player, parent.getCommandName(), parent.config);
|
||||
} else {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Only player can use this command!")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("path", StringArgumentType.string());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgumentOrDefault(PathArgument.class, "");
|
||||
int dotIndex = path.lastIndexOf(".");
|
||||
builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2);
|
||||
if (dotIndex == -1) builder.suggest("full");
|
||||
for (String s : getListClosestMatchingLast(
|
||||
path.substring(dotIndex + 1),
|
||||
parent.config.completeConfigPath(path)
|
||||
)) {
|
||||
builder.suggest(s.substring(path.lastIndexOf('.') + 1));
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
if (context.getSender() instanceof CraftPlayer cPlayer) {
|
||||
final Player player = cPlayer.getHandle();
|
||||
ConfigCommandDialog.openGui(player, parent.getCommandName(), parent.config, context.getArgument(PathArgument.class));
|
||||
} else {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Only player can use this command!")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
public class ReloadCommand extends ConfigSubcommand {
|
||||
public ReloadCommand(ConfigCommand parent) {
|
||||
super("reload", parent);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Reloaded config file!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast;
|
||||
|
||||
public class ResetCommand extends ConfigSubcommand {
|
||||
public ResetCommand(ConfigCommand parent) {
|
||||
super("reset", parent);
|
||||
children(new PathArgument(parent));
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("path", StringArgumentType.string());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgumentOrDefault(PathArgument.class, "");
|
||||
int dotIndex = path.lastIndexOf(".");
|
||||
builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2);
|
||||
for (String s : getListClosestMatchingLast(
|
||||
path.substring(dotIndex + 1),
|
||||
parent.config.completeConfigPath(path)
|
||||
)) {
|
||||
builder.suggest(s.substring(path.lastIndexOf('.') + 1));
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String path = context.getArgumentOrDefault(PathArgument.class, "");
|
||||
parent.config.resetConfig(path);
|
||||
parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Reset Config " + path + " to " + parent.config.getConfig(path) + " successfully!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+68
@@ -0,0 +1,68 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public class ResetCommentsCommand extends ConfigSubcommand {
|
||||
public ResetCommentsCommand(ConfigCommand parent) {
|
||||
super("reset-comments", parent);
|
||||
children(
|
||||
new PathArgument(parent)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("If you want to reset comments to default in the configuration file, please use /" + parent.getCommandName() + " reset-comments confirm")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("confirm", StringArgumentType.string());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
builder.suggest("confirm");
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) throws CommandSyntaxException {
|
||||
String confirm = context.getArgument(CleanCommand.PathArgument.class);
|
||||
if (!confirm.equals("confirm")) {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Please use /" + parent.getCommandName() + " reset-comments confirm to confirm!")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
parent.config.reloadAsync(false).thenAccept(nullValue -> context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Reset comments to default in the configuration file!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
import static org.leavesmc.leaves.command.CommandUtils.getListClosestMatchingLast;
|
||||
|
||||
public class SetCommand extends ConfigSubcommand {
|
||||
public SetCommand(ConfigCommand parent) {
|
||||
super("set", parent);
|
||||
children(new PathArgument(parent));
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("path", StringArgumentType.string());
|
||||
this.parent = parent;
|
||||
children(
|
||||
new ValueArgument(parent)
|
||||
);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgumentOrDefault(PathArgument.class, "");
|
||||
int dotIndex = path.lastIndexOf(".");
|
||||
builder = builder.createOffset(builder.getInput().lastIndexOf(' ') + dotIndex + 2);
|
||||
for (String s : getListClosestMatchingLast(
|
||||
path.substring(dotIndex + 1),
|
||||
parent.config.completeConfigPath(path)
|
||||
)) {
|
||||
builder.suggest(s.substring(path.lastIndexOf('.') + 1));
|
||||
}
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String path = context.getArgumentOrDefault(PathArgument.class, "");
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Config " + path + " is " + parent.config.getConfig(path) + "!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
);
|
||||
return true;
|
||||
}
|
||||
|
||||
private class ValueArgument extends ArgumentNode<String> {
|
||||
private final ConfigCommand parent;
|
||||
|
||||
private ValueArgument(ConfigCommand parent) {
|
||||
super("value", StringArgumentType.greedyString());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected CompletableFuture<Suggestions> getSuggestions(@NotNull CommandContext context, @NotNull SuggestionsBuilder builder) {
|
||||
String path = context.getArgument(PathArgument.class);
|
||||
if (!parent.config.getAllConfigPaths("").contains(path)) {
|
||||
return builder
|
||||
.suggest("<ERROR CONFIG>", net.minecraft.network.chat.Component.literal("This config path does not exist."))
|
||||
.buildFuture();
|
||||
}
|
||||
Object value = parent.config.getConfigOrigin(path);
|
||||
String[] suggestions = parent.config.getConfigSuggestions(path);
|
||||
builder.suggest(value.toString(), net.minecraft.network.chat.Component.literal("Default value")
|
||||
.withStyle(style -> style.withColor(net.minecraft.network.chat.TextColor.fromLegacyFormat(net.minecraft.ChatFormatting.GRAY))));
|
||||
if (suggestions == null) {
|
||||
if (value instanceof Boolean) {
|
||||
builder.suggest(String.valueOf(!(Boolean) value));
|
||||
} else if (value instanceof Enum<?> enumValue) {
|
||||
Enum<?>[] values = enumValue.getClass().getEnumConstants();
|
||||
for (Enum<?> enumValue1 : values) {
|
||||
if (enumValue1 == value) continue;
|
||||
builder.suggest(enumValue1.name());
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (String s : suggestions) {
|
||||
if (!Objects.equals(s, value.toString())) {
|
||||
builder.suggest(s);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return builder.buildFuture();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String path = context.getArgument(PathArgument.class);
|
||||
String value = context.getArgument(ValueArgument.class);
|
||||
if (parent.config.setConfig(path, value)) {
|
||||
parent.config.reloadAsync(true).thenAccept(nullValue -> context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Set Config " + path + " to " + value + " successfully!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
));
|
||||
} else {
|
||||
context.getSender().sendMessage(
|
||||
Component
|
||||
.text("Failed to set config " + path + " to " + value + "!")
|
||||
.color(TextColor.color(255, 0, 0))
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
package io.nanachiyo0721.shiroha.commands.config.sub;
|
||||
|
||||
import com.mojang.brigadier.arguments.StringArgumentType;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigCommand;
|
||||
import io.nanachiyo0721.shiroha.commands.config.ConfigSubcommand;
|
||||
import io.nanachiyo0721.shiroha.utils.dialog.ConfigCommandDialog;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.leavesmc.leaves.command.ArgumentNode;
|
||||
import org.leavesmc.leaves.command.CommandContext;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
public class SubmitCommand extends ConfigSubcommand {
|
||||
public SubmitCommand(ConfigCommand parent) {
|
||||
super("submit", parent);
|
||||
children(
|
||||
new PathArgument(parent)
|
||||
);
|
||||
}
|
||||
|
||||
static class PathArgument extends ArgumentNode<String> {
|
||||
protected final ConfigCommand parent;
|
||||
|
||||
PathArgument(ConfigCommand parent) {
|
||||
super("path", StringArgumentType.greedyString());
|
||||
this.parent = parent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean execute(@NotNull CommandContext context) {
|
||||
String content = context.getRange().get(context.getInput());
|
||||
String[] args = org.apache.commons.lang3.StringUtils.split(content, ' ');
|
||||
ConfigCommandDialog.processSubmit(context.getSender(), parent.config, Arrays.copyOfRange(args, 2, args.length));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
package io.nanachiyo0721.shiroha.config;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import io.nanachiyo0721.shiroha.commands.CommandRegister;
|
||||
import io.nanachiyo0721.shiroha.config.flags.TransformedConfig;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.*;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.ConcurrentLinkedQueue;
|
||||
|
||||
public class ConfigManager {
|
||||
private static boolean initialized = false;
|
||||
private static final ConfigsInstanceBuilder builder = new ConfigsInstanceBuilder();
|
||||
private static final Map<String, ConfigsInstance> configfiles = new HashMap<>();
|
||||
private static final Collection<Runnable> runnableBeforeFinalLoad = new ConcurrentLinkedQueue<>();
|
||||
private static final Map<TransformedConfig, String[]> needTransformedConfigs = new ConcurrentHashMap<>();
|
||||
// String[]:
|
||||
// 0 -> origin key
|
||||
// 1 -> target key
|
||||
// 2 -> origin full path
|
||||
// 3 -> target full path
|
||||
|
||||
public static void initConfigs() {
|
||||
registerConfig("shiroha", builder.of("shiroha", "io.nanachiyo0721.shiroha.config.modules"));
|
||||
preLoad();
|
||||
}
|
||||
|
||||
public static void registerConfig(String name, ConfigsInstance config) {
|
||||
configfiles.put(name, config);
|
||||
}
|
||||
|
||||
public static void preLoad() {
|
||||
CompletableFuture<?>[] futures = configfiles.values().stream()
|
||||
.map(config -> CompletableFuture.runAsync(() -> {
|
||||
try {
|
||||
config.preLoadConfig();
|
||||
} catch (IOException e) {
|
||||
throw new RuntimeException("Failed to preload config", e);
|
||||
}
|
||||
}))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures).join();
|
||||
acceptTransformedConfigs();
|
||||
}
|
||||
|
||||
public static void loadConfigFiles() {
|
||||
runTaskBeforeFinalLoad();
|
||||
// Finalize loading
|
||||
CompletableFuture<?>[] futures = configfiles.values().stream()
|
||||
.map(config -> CompletableFuture.runAsync(config::finalizeLoadConfig))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures).join();
|
||||
CommandRegister.register(); // register command after config loaded to enable some command didn't depend on config files
|
||||
initialized = true;
|
||||
}
|
||||
|
||||
public static void registerRunnableBeforeFinalLoad(Runnable runnable) {
|
||||
if (initialized) return;
|
||||
runnableBeforeFinalLoad.add(runnable);
|
||||
}
|
||||
|
||||
public static void registerTransformedConfig(@NotNull String origin, @NotNull String target, @NotNull String originKey, @NotNull String targetKey, TransformedConfig transformedConfig) {
|
||||
if (initialized) return;
|
||||
needTransformedConfigs.put(transformedConfig, new String[]{origin, target, originKey, targetKey});
|
||||
}
|
||||
|
||||
public static ConfigsInstance getConfigs(String name) {
|
||||
return configfiles.get(name);
|
||||
}
|
||||
|
||||
public static ConfigsInstanceBuilder getBuilder() {
|
||||
return builder;
|
||||
}
|
||||
|
||||
static void runTaskBeforeFinalLoad() {
|
||||
runnableBeforeFinalLoad.forEach(Runnable::run);
|
||||
runnableBeforeFinalLoad.clear();
|
||||
}
|
||||
|
||||
public static void reApplyStagedConfigs() {
|
||||
CompletableFuture<?>[] futures = configfiles.values().stream()
|
||||
.map(config -> CompletableFuture.runAsync(config::reApplyStagedConfigs))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures).join();
|
||||
}
|
||||
|
||||
public static void saveConfigs() {
|
||||
saveConfigs(true);
|
||||
}
|
||||
|
||||
public static void saveConfigs(boolean async) {
|
||||
if (async) {
|
||||
CompletableFuture<?>[] futures = configfiles.values().stream()
|
||||
.map(config -> CompletableFuture.runAsync(config::saveConfigs))
|
||||
.toArray(CompletableFuture[]::new);
|
||||
CompletableFuture.allOf(futures).join();
|
||||
} else {
|
||||
configfiles.values().forEach(ConfigsInstance::saveConfigs);
|
||||
}
|
||||
}
|
||||
|
||||
public static void acceptTransformedConfigs() {
|
||||
Set<ConfigsInstance> toReload = new HashSet<>();
|
||||
for (Map.Entry<TransformedConfig, String[]> entry : needTransformedConfigs.entrySet()) {
|
||||
String[] config = entry.getValue();
|
||||
TransformedConfig transformedConfig = entry.getKey();
|
||||
ConfigsInstance origin = getConfigs(config[0]);
|
||||
ConfigsInstance target = getConfigs(config[1]);
|
||||
if (origin == null || target == null) continue;
|
||||
CommentedFileConfig originConfig = origin.getFileInstance();
|
||||
CommentedFileConfig targetConfig = target.getFileInstance();
|
||||
|
||||
final String oldConfigKeyName = config[2];
|
||||
final String newConfigKeyName = config[3];
|
||||
Object oldValue = originConfig.get(oldConfigKeyName);
|
||||
if (oldValue != null) {
|
||||
boolean success = true;
|
||||
if (transformedConfig.transform()) {
|
||||
try {
|
||||
for (Class<? extends DefaultTransformLogic> logic : transformedConfig.transformLogic()) {
|
||||
oldValue = logic.getDeclaredConstructor().newInstance().transform(oldValue);
|
||||
}
|
||||
oldValue = new DefaultTransformLogic().transform(oldValue);
|
||||
targetConfig.set(newConfigKeyName, oldValue);
|
||||
if (transformedConfig.transformComments()) {
|
||||
targetConfig.setComment(newConfigKeyName, originConfig.getComment(oldConfigKeyName));
|
||||
}
|
||||
} catch (Exception e) {
|
||||
success = false;
|
||||
target.logger.error("Failed to transform removed config {}!", transformedConfig.name());
|
||||
}
|
||||
}
|
||||
|
||||
if (success) origin.removeConfig(oldConfigKeyName, transformedConfig.directory());
|
||||
}
|
||||
toReload.add(target);
|
||||
toReload.add(origin);
|
||||
}
|
||||
toReload.forEach(ConfigsInstance::saveConfigs);
|
||||
needTransformedConfigs.clear(); // free space when all done
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+82
@@ -0,0 +1,82 @@
|
||||
package io.nanachiyo0721.shiroha.config;
|
||||
|
||||
import io.nanachiyo0721.shiroha.api.config.ShirohaConfigBuilder;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
public class ConfigsInstanceBuilder implements ShirohaConfigBuilder {
|
||||
// Factory methods for creating ConfigsInstance objects
|
||||
public ConfigsInstance of(
|
||||
@NotNull ClassLoader loader,
|
||||
@NotNull String name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(loader, new File(name + "_config"), name, pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull String name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(MinecraftServer.class.getClassLoader(), name, pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull ClassLoader loader,
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(loader, base, name, name + "_global_config.toml", pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(MinecraftServer.class.getClassLoader(), base, name, pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull ClassLoader loader,
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String file_name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(loader, base, name, file_name, name + "config", pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String file_name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return this.of(MinecraftServer.class.getClassLoader(), base, name, file_name, pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull ClassLoader loader,
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String file_name,
|
||||
@NotNull String command_name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return new ConfigsInstance(loader, base, name, file_name, command_name, pack);
|
||||
}
|
||||
|
||||
public ConfigsInstance of(
|
||||
@NotNull File base,
|
||||
@NotNull String name,
|
||||
@NotNull String file_name,
|
||||
@NotNull String command_name,
|
||||
@NotNull String pack
|
||||
) {
|
||||
return new ConfigsInstance(MinecraftServer.class.getClassLoader(), base, name, file_name, command_name, pack);
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.nanachiyo0721.shiroha.config;
|
||||
|
||||
public class DefaultTransformLogic {
|
||||
public Object transform(Object obj) {
|
||||
return obj;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package io.nanachiyo0721.shiroha.config;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
public interface IConfigModule {
|
||||
default void beforeFinalLoad() {
|
||||
}
|
||||
|
||||
default void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
}
|
||||
|
||||
default void onUnloaded(CommentedFileConfig configInstance) {
|
||||
}
|
||||
|
||||
default <T> T get(String keyName, T defaultValue, @NotNull CommentedFileConfig config) {
|
||||
if (!config.contains(keyName)) {
|
||||
config.set(keyName, defaultValue);
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return config.get(keyName);
|
||||
}
|
||||
}
|
||||
+24
@@ -0,0 +1,24 @@
|
||||
package io.nanachiyo0721.shiroha.config;
|
||||
|
||||
import java.util.IllegalFormatConversionException;
|
||||
|
||||
public class IllegalFormatConversionExceptionWithOrigin extends IllegalFormatConversionException {
|
||||
private final Object origin;
|
||||
|
||||
/**
|
||||
* Constructs an instance of this class with the mismatched conversion and
|
||||
* the corresponding argument class.
|
||||
*
|
||||
* @param c Inapplicable conversion
|
||||
* @param arg Class of the mismatched argument
|
||||
* @param originalValue The original value
|
||||
*/
|
||||
public IllegalFormatConversionExceptionWithOrigin(char c, Class<?> arg, Object originalValue) {
|
||||
super(c, arg);
|
||||
origin = originalValue;
|
||||
}
|
||||
|
||||
public Object getOrigin() {
|
||||
return origin;
|
||||
}
|
||||
}
|
||||
+9
@@ -0,0 +1,9 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface CommandSuggestions {
|
||||
String[] suggest();
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigClassInfo {
|
||||
EnumConfigCategory category();
|
||||
|
||||
String name();
|
||||
|
||||
String[] directory() default {};
|
||||
|
||||
String comments() default "";
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface ConfigInfo {
|
||||
String name();
|
||||
|
||||
String[] directory() default {};
|
||||
|
||||
String comments() default "";
|
||||
|
||||
boolean allowAutoReset() default true;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface DoNotLoad {
|
||||
}
|
||||
+8
@@ -0,0 +1,8 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
public @interface HotReloadUnsupported {
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
package io.nanachiyo0721.shiroha.config.flags;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.DefaultTransformLogic;
|
||||
|
||||
import java.lang.annotation.Repeatable;
|
||||
import java.lang.annotation.Retention;
|
||||
import java.lang.annotation.RetentionPolicy;
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@Repeatable(TransformedConfig.List.class)
|
||||
public @interface TransformedConfig {
|
||||
String name();
|
||||
|
||||
String[] directory();
|
||||
|
||||
String originInstance() default "";
|
||||
|
||||
boolean transform() default true;
|
||||
|
||||
boolean transformComments() default true;
|
||||
|
||||
Class<? extends DefaultTransformLogic>[] transformLogic() default {};
|
||||
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@interface List {
|
||||
TransformedConfig[] value();
|
||||
}
|
||||
}
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.experiment;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "command")
|
||||
public class CommandConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enable_data_command")
|
||||
@HotReloadUnsupported
|
||||
public static boolean data = false;
|
||||
@ConfigInfo(name = "enable_command_block", comments = """
|
||||
Force to enable command blocks.
|
||||
ATTENTION: WOULD CAUSE SERVER CRASHING AS SOME THREADING ISSUE!!!
|
||||
DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!!
|
||||
""")
|
||||
public static boolean commandBlock = false;
|
||||
@ConfigInfo(name = "enable_tick_command", comments = """
|
||||
Only freeze/unfreeze/step/query command is allowed if you enabled it.
|
||||
WARN: This should disabled in production environment!
|
||||
""")
|
||||
public static boolean tick = false;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.experiment;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "disable_async_catchers")
|
||||
public class DisableAsyncCatcherConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Disable async catcher to prevent some crashes caused by some plugins which supports folia but has issuable logics.
|
||||
ATTENTION: Would cause region deadlock when getChunkAt was incorrectly called!
|
||||
See: https://github.com/PaperMC/Folia/issues/280 which is resolved in folia(https://github.com/PaperMC/Folia/commit/2e7bc0721af95196c85500c7bb136aeea0bc12ce)
|
||||
DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!!
|
||||
""")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.experiment;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.EXPERIMENT, name = "disable_entity_exception_catchers")
|
||||
public class DisableEntityCatchConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
If this config enabled, the server will crash directly when entity ticking has some errors instead of removing the entity to keep server running.
|
||||
It could prevent entity disappearing but may cause more server crashes.
|
||||
DO NOT ENABLE UNLESS YOU KNOW WHAT YOU ARE DOING!!!""")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+22
@@ -0,0 +1,22 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.CommandSuggestions;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumCollisionBehaviorMode;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "collision_behavior")
|
||||
public class CollisionBehaviorConfig implements IConfigModule {
|
||||
@CommandSuggestions(suggest = {"VANILLA", "BLOCK_SHAPE_VANILLA", "PAPER"})
|
||||
@ConfigInfo(name = "mode", comments =
|
||||
"""
|
||||
Decides which collision logics will be used(Moonrise and Paper modified this for optimization but would also break some vanilla behaviours at the same time).
|
||||
Would be useful for fixing improper behaviours of some huge redstone machines
|
||||
Available Value:
|
||||
VANILLA
|
||||
BLOCK_SHAPE_VANILLA
|
||||
PAPER""")
|
||||
public static EnumCollisionBehaviorMode behaviorMode = EnumCollisionBehaviorMode.BLOCK_SHAPE_VANILLA;
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "fix_high_velocity_issue")
|
||||
public class FoliaEntityMovingFixConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments =
|
||||
"""
|
||||
A simple fix of an issue on folia\s
|
||||
(Sometimes the entity would\s
|
||||
have a large moment that cross the\s
|
||||
different tick regions, and it would\s
|
||||
make the server crashed) but sometimes it might doesn't work""")
|
||||
public static boolean enabled = false;
|
||||
|
||||
@ConfigInfo(name = "warn_on_detected")
|
||||
public static boolean warnOnDetected = false;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "force_cleanup_drop_non_owned_entity_memory_module", comments = "This config is a temporary fix for those incorrect owned data in the memory of each mob, for more you can see https://github.com/PaperMC/Folia/issues/203")
|
||||
public class ForceCleanupEntityBrainMemoryConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled_for_entity", comments = "When enabled, the entity's brain will clean the memory which is typed of entity and not belong to current tickregion")
|
||||
public static boolean enabledForEntity = false;
|
||||
|
||||
@ConfigInfo(name = "enabled_for_block_pos", comments = "When enabled, the entity's brain will clean the memory which is typed of block_pos and not belong to current tickregion")
|
||||
public static boolean enabledForBlockPos = false;
|
||||
|
||||
@ConfigInfo(name = "enabled_for_position_tracker", comments = "When enabled, the entity's brain will clean the memory which is typed of position_tracker and not belong to current tickregion")
|
||||
public static boolean enabledForPositionTracker = false;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "item_multitask")
|
||||
public class ItemMultitaskConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Prevent the server from interrupting the state of items
|
||||
during block interactions or hotbar slot changes.""")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "long_command_support")
|
||||
public class LongCommandSupportConfig {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Some long commands can be run through the dialog command,
|
||||
but paper has prohibited it.
|
||||
Enable this to fix this problem.""")
|
||||
public static boolean enabled = true;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(name = "poi_range_fixes", category = EnumConfigCategory.FIXES)
|
||||
public class POIRangeFixes implements IConfigModule {
|
||||
@ConfigInfo(name = "do_not_compete_poi_if_unloaded", comments = """
|
||||
Do not compete POI if it's unloaded
|
||||
Related with https://github.com/PaperMC/Folia/issues/292
|
||||
""")
|
||||
public static boolean doNotCompetePOIIfUnloaded = false;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "pathfinding_fixes")
|
||||
public class PathfindingFixesConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "break_down_pathfinding_when_out_of_region", comments = "Recompute path or stop pathfinding when it's touching the blocks out of current tick region")
|
||||
public static boolean breakDownPathfindingWhenOutOfRegion = false;
|
||||
@ConfigInfo(name = "do_not_pathfind_to_not_owned_targets", comments = "Skip pathfinding target when it's out of current tick region")
|
||||
public static boolean doNotPathfindToNotOwnedTargets = false;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.fixes;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FIXES, name = "allow_unsafe_teleportation")
|
||||
public class UnsafeTeleportationConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Allow non player entities enter end portals if enabled.
|
||||
If you want to use sand duping,please turn on this.
|
||||
Warning: This would cause some unsafe issues, you could learn more on : https://github.com/PaperMC/Folia/issues/297""")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBarList;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "membar")
|
||||
public class MembarConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean memoryBarEnabled = false;
|
||||
@ConfigInfo(name = "format")
|
||||
public static String memBarFormat = "<gray>Memory usage <yellow>:</yellow> <used>MB<yellow>/</yellow><available>MB";
|
||||
@ConfigInfo(name = "bar_color_list")
|
||||
public static List<BossBar.Color> barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE);
|
||||
@ConfigInfo(name = "memory_color_list")
|
||||
public static List<String> memColors = List.of("<gradient:#55ff55:#00aa00><text></gradient>", "<gradient:#ffff55:#ffaa00><text></gradient>", "<gradient:#ff5555:#aa0000><text></gradient>", "<gradient:#55ff55:#00aa00><text></gradient>");
|
||||
@ConfigInfo(name = "update_interval_ticks")
|
||||
public static int updateInterval = 15;
|
||||
@ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST")
|
||||
public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR;
|
||||
|
||||
@DoNotLoad
|
||||
private static boolean inited = false;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
TickableStatusBarList.raiseGlobalReload(EnumBarType.MEMORY);
|
||||
|
||||
if (!inited) { // command has moved to CommandRegister
|
||||
inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnloaded(CommentedFileConfig configInstance) {
|
||||
Bukkit.getCommandMap().getKnownCommands().remove("luminol:membar");
|
||||
}
|
||||
}
|
||||
+66
@@ -0,0 +1,66 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import net.objecthunter.exp4j.Expression;
|
||||
import net.objecthunter.exp4j.ExpressionBuilder;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
@ConfigClassInfo(name = "portal_rate_limit", category = EnumConfigCategory.FUNCTION)
|
||||
public class PortalRateLimiterConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enable", comments = "Whether or not to limit the portal rate when entity goes into portals")
|
||||
@HotReloadUnsupported
|
||||
public static boolean enabled = false;
|
||||
|
||||
@ConfigInfo(name = "maximum_portal_teleports_per_tick", comments = """
|
||||
Decides how much portal teleportation should be handled within a tick in a single tick region,when exceed,
|
||||
the portal teleportation will be pushed into the next tick
|
||||
|
||||
Note: set to -1 to use custom expressions""")
|
||||
@HotReloadUnsupported
|
||||
public static int maxPortalTeleportsPerTick = 200;
|
||||
|
||||
@ConfigInfo(name = "maximum_portal_teleports_per_tick_expression", comments = """
|
||||
If the fixed limit is not enough for use, you could define your own expression to dynamically limit the
|
||||
portal rate.
|
||||
|
||||
Available variables(all is of current tickregion): e (ticking_entity_count)
|
||||
c (ticking_chunk_count)
|
||||
p (player_count)
|
||||
Example: 50 * (1 + sqrt(x/1000) + c/200 + p/5)
|
||||
""")
|
||||
@HotReloadUnsupported
|
||||
public static String maxPortalTeleportsExpression = "50 * (1 + sqrt(e/1000) + c/200 + p/5)";
|
||||
|
||||
// use this to prevent reallocation
|
||||
private static final String VARIABLE_TICKING_ENTITY_CONT = "e";
|
||||
private static final String VARIABLE_TICKING_CHUNK_CONT = "c";
|
||||
private static final String VARIABLE_PLAYER_CONT = "p";
|
||||
|
||||
@Nullable
|
||||
public static Expression getExpressionIfConfigured() {
|
||||
if (maxPortalTeleportsPerTick != -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return new ExpressionBuilder(maxPortalTeleportsExpression)
|
||||
.variables(
|
||||
VARIABLE_PLAYER_CONT,
|
||||
VARIABLE_TICKING_CHUNK_CONT,
|
||||
VARIABLE_TICKING_ENTITY_CONT
|
||||
)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static int computeExpression(@NotNull Expression expression, int entityCount, int chunkCount, int playerCount) {
|
||||
expression.setVariable(VARIABLE_TICKING_ENTITY_CONT, entityCount);
|
||||
expression.setVariable(VARIABLE_TICKING_CHUNK_CONT, chunkCount);
|
||||
expression.setVariable(VARIABLE_PLAYER_CONT, playerCount);
|
||||
|
||||
return (int) expression.evaluate();
|
||||
}
|
||||
}
|
||||
+50
@@ -0,0 +1,50 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBarList;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "regionbar")
|
||||
public class RegionBarConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean regionbarEnabled = false;
|
||||
@ConfigInfo(name = "format")
|
||||
public static String regionBarFormat = "<gray>Util<yellow>:</yellow> <util> Chunks<yellow>:</yellow> <green><chunks></green> Players<yellow>:</yellow> <green><players></green> Entities<yellow>:</yellow> <green><entities></green>";
|
||||
@ConfigInfo(name = "bar_color_list")
|
||||
public static List<BossBar.Color> barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE);
|
||||
@ConfigInfo(name = "util_color_list")
|
||||
public static List<String> utilColors = List.of("<gradient:#55ff55:#00aa00><text></gradient>", "<gradient:#ffff55:#ffaa00><text></gradient>", "<gradient:#ff5555:#aa0000><text></gradient>", "<gradient:#55ff55:#00aa00><text></gradient>");
|
||||
@ConfigInfo(name = "update_interval_ticks")
|
||||
public static int updateInterval = 15;
|
||||
@ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST")
|
||||
public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR;
|
||||
|
||||
@DoNotLoad
|
||||
private static boolean inited = false;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
TickableStatusBarList.raiseGlobalReload(EnumBarType.REGION);
|
||||
|
||||
if (!inited) { // command has moved to CommandRegister
|
||||
inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnloaded(CommentedFileConfig configInstance) {
|
||||
Bukkit.getCommandMap().getKnownCommands().remove("luminol:regionbar");
|
||||
}
|
||||
}
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import abomination.LinearRegionFile;
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.IllegalFormatConversionExceptionWithOrigin;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumRegionFormat;
|
||||
import io.nanachiyo0721.shiroha.utils.BufferedLinearRegionFileFlusher;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "region_format")
|
||||
public class RegionFormatConfig implements IConfigModule {
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "format", allowAutoReset = false, comments = "Available choices: MCA, B_LINEAR, LINEAR_V2")
|
||||
public static EnumRegionFormat regionFormat = EnumRegionFormat.MCA;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "linear_compression_level", comments = "Decides the compression level of the region file(Only works for LINEAR_V2 and B_LINEAR)")
|
||||
public static int linearCompressionLevel = 1;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "linear_io_thread_count", comments = "Decides the worker thread count of linear(Only works for LINEAR_V2)")
|
||||
public static int linearIoThreadCount = 6;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "linear_io_flush_delay_ms", comments = "Decides when it will be flushed to the region file when it has been marked to save for n(default is 100) milliseconds(Only works for LINEAR_V2)")
|
||||
public static int linearIoFlushDelayMs = 100;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "blinear_io_flush_delay_ms", comments = "Decides when it will be flushed to the region file when there has been no write operations for n(default is 3000) milliseconds(Only works for B_LINEAR)")
|
||||
public static int blinearIoFlushDelayMs = 3000;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "blinear_io_thread_count", comments = "Decides the worker thread count of buffered linear(Only works for B_LINEAR)")
|
||||
public static int blinearIoThreadCount = 6;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "linear_use_virtual_thread", comments = "Decides if it could use virtual threads for linear format(Only works for LINEAR_V2)")
|
||||
public static boolean linearUseVirtualThread = true;
|
||||
|
||||
@DoNotLoad
|
||||
public static BufferedLinearRegionFileFlusher blinearFlusher = null;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> exs) {
|
||||
if (exs != null) {
|
||||
for (Exception e : exs) {
|
||||
if (e instanceof IllegalFormatConversionExceptionWithOrigin) {
|
||||
throw new RuntimeException("Invalid region format: " + ((IllegalFormatConversionExceptionWithOrigin) e).getOrigin().toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (regionFormat == EnumRegionFormat.LINEAR_V2) {
|
||||
checkCompressionLevel();
|
||||
|
||||
LinearRegionFile.SAVE_DELAY_MS = linearIoFlushDelayMs;
|
||||
LinearRegionFile.SAVE_THREAD_MAX_COUNT = linearIoThreadCount;
|
||||
LinearRegionFile.USE_VIRTUAL_THREAD = linearUseVirtualThread;
|
||||
}
|
||||
|
||||
if (regionFormat == EnumRegionFormat.B_LINEAR) {
|
||||
blinearFlusher = new BufferedLinearRegionFileFlusher(blinearIoThreadCount, 20, blinearIoFlushDelayMs);
|
||||
|
||||
checkCompressionLevel();
|
||||
|
||||
// we don't need to consider that it will be reloaded more than once as this config is unreloadable
|
||||
Runtime.getRuntime().addShutdownHook(new Thread(() -> blinearFlusher.shutdown()));
|
||||
}
|
||||
}
|
||||
|
||||
private static void checkCompressionLevel() {
|
||||
if (RegionFormatConfig.linearCompressionLevel > 23 || RegionFormatConfig.linearCompressionLevel < 1) {
|
||||
MinecraftServer.LOGGER.error("Linear or BufferedLinear region compression level should be between 1 and 22 in config: {}", RegionFormatConfig.linearCompressionLevel);
|
||||
MinecraftServer.LOGGER.error("Falling back to compression level 1.");
|
||||
RegionFormatConfig.linearCompressionLevel = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
+41
@@ -0,0 +1,41 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
import java.security.SecureRandom;
|
||||
import java.util.Base64;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "secure_seed")
|
||||
public class SecureSeedConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Once you enable secure seed, all ores and structures are generated with 1024-bit seed
|
||||
instead of using 64-bit seed in vanilla, making traditional seed cracking impossible.
|
||||
Note: If you use V1 it will be vulnerable to terrain elevation attacks.
|
||||
***** WARN: You need keep it enabled if your old world are also using secure seed! Or it will kill your save *****""")
|
||||
@HotReloadUnsupported
|
||||
public static boolean enabled = false;
|
||||
|
||||
@ConfigInfo(name = "version", comments = """
|
||||
Version 1: Blake2b (insecure, reversible with a GPU/ASIC cluster in minutes with enough entropy)
|
||||
Version 2: Blake3 with salt key derivation (recommended, irreversible)
|
||||
***** WARN: Switching versions will cause chunk errors! *****""")
|
||||
@HotReloadUnsupported
|
||||
public static int version = 1;
|
||||
|
||||
@ConfigInfo(name = "salt", comments = """
|
||||
Auto-generated 256-bit salt for V2 cryptographic operations.
|
||||
Generated once on first startup - DO NOT SHARE THIS OR MODIFY (MODIFYING THIS WILL CAUSE CHUNK ERRORS)!
|
||||
Used with Blake3 keyed hash to make seed irreversible.""")
|
||||
@HotReloadUnsupported
|
||||
public static String salt = generateSalt();
|
||||
|
||||
private static String generateSalt() {
|
||||
byte[] saltBytes = new byte[32];
|
||||
new SecureRandom().nextBytes(saltBytes);
|
||||
return Base64.getEncoder().encodeToString(saltBytes);
|
||||
}
|
||||
}
|
||||
+58
@@ -0,0 +1,58 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBarList;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "tpsbar")
|
||||
public class TpsBarConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean tpsbarEnabled = false;
|
||||
@ConfigInfo(name = "format")
|
||||
public static String tpsBarFormat = "<gray>TPS<yellow>:</yellow> <tps> MSPT<yellow>:</yellow> <mspt> Ping<yellow>:</yellow> <ping>ms ChunkHot<yellow>:</yellow> <chunkhot>";
|
||||
@ConfigInfo(name = "bar_color_list")
|
||||
public static List<BossBar.Color> barColors = List.of(BossBar.Color.GREEN, BossBar.Color.YELLOW, BossBar.Color.RED, BossBar.Color.PURPLE);
|
||||
@ConfigInfo(name = "tps_color_list")
|
||||
public static List<String> tpsColors = List.of("<gradient:#55ff55:#00aa00><text></gradient>", "<gradient:#ffff55:#ffaa00><text></gradient>", "<gradient:#ff5555:#aa0000><text></gradient>", "<gradient:#55ff55:#00aa00><text></gradient>");
|
||||
@ConfigInfo(name = "ping_color_list")
|
||||
public static List<String> pingColors = List.of("<gradient:#55ff55:#00aa00><text></gradient>", "<gradient:#ffff55:#ffaa00><text></gradient>", "<gradient:#ff5555:#aa0000><text></gradient>", "<gradient:#55ff55:#00aa00><text></gradient>");
|
||||
@ConfigInfo(name = "chunkhot_color_list")
|
||||
public static List<String> chunkHotColors = List.of("<gradient:#55ff55:#00aa00><text></gradient>", "<gradient:#ffff55:#ffaa00><text></gradient>", "<gradient:#ff5555:#aa0000><text></gradient>", "<gradient:#55ff55:#00aa00><text></gradient>");
|
||||
@ConfigInfo(name = "update_interval_ticks")
|
||||
public static int updateInterval = 15;
|
||||
@ConfigInfo(name = "precision_of_tps_value", comments = "Example(if tps is 20.00000000)(value -> result): 2 -> 20.00, 1 -> 20.0")
|
||||
public static int precisionOfTPS = 2;
|
||||
@ConfigInfo(name = "precision_of_mspt_value", comments = "Example(if mspt is 20.00000000)(value -> result): 2 -> 20.00, 1 -> 20.0")
|
||||
public static int precisionOfMSPT = 2;
|
||||
@ConfigInfo(name = "display", comments = "Available displays: BOSS_BAR, ACTION_BAR, TAB_LIST")
|
||||
public static EnumStatusBarDisplay display = EnumStatusBarDisplay.BOSS_BAR;
|
||||
|
||||
@DoNotLoad
|
||||
private static boolean inited = false;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
TickableStatusBarList.raiseGlobalReload(EnumBarType.TPS);
|
||||
|
||||
if (!inited) { // command has moved to CommandRegister
|
||||
inited = true;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUnloaded(CommentedFileConfig configInstance) {
|
||||
Bukkit.getCommandMap().getKnownCommands().remove("luminol:tpsbar");
|
||||
}
|
||||
}
|
||||
+21
@@ -0,0 +1,21 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.function;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumTripwireBehavior;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.FUNCTION, name = "tripwire_dupe")
|
||||
public class TripwireBehaviorConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean enabled = false;
|
||||
|
||||
@ConfigInfo(name = "behavior_mode", comments =
|
||||
"""
|
||||
Available Value:
|
||||
VANILLA20
|
||||
VANILLA21
|
||||
MIXED""")
|
||||
public static EnumTripwireBehavior behaviorMode = EnumTripwireBehavior.VANILLA21;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "disable_warning")
|
||||
public class DisableWarningConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "disable_heightmap_warning", comments =
|
||||
"""
|
||||
Disable heightmap-check's warning""")
|
||||
public static boolean disableHeightmapWarning = false;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "folia_watchdog")
|
||||
public class FoliaWatchogConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "tick_region_time_out_ms", comments = "Decides the interval of the watchdog prints the threads dumps of tickregions in stuck")
|
||||
public static int tickRegionTimeOutMs = 5000;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "force_disable_packet_limiter_of_paper", comments =
|
||||
"Force and fully disable all packet limiters of Paper, which is used to prevent from kicking by using some quick crafting mods but \n" +
|
||||
"has negative impacts on security"
|
||||
)
|
||||
public class PaperPacketLimiterConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "force_disable")
|
||||
public static boolean forceDisable = false;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "save_portal_tickets")
|
||||
public class SavePortalTicketsConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "do_save", comments = "whether or not to save the portal tickets when server stopping," +
|
||||
" this would make it acts like mc before 1.21.5," +
|
||||
" and won't auto active the portal chunk loader when server started again.")
|
||||
public static boolean doSave = true;
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "server_mod_name")
|
||||
public class ServerModNameConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "name", comments = "Decides the server mod name shown in your F3 debug screen.")
|
||||
public static String serverModName = "Shiroha";
|
||||
|
||||
@ConfigInfo(name = "vanilla_spoof", comments = "Ignore any plugin's modification and server mod name set in this config block, only force sending brand name of vanilla")
|
||||
public static boolean fakeVanilla = false;
|
||||
}
|
||||
+63
@@ -0,0 +1,63 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.misc;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.Set;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.MISC, name = "username_checks")
|
||||
public class UsernameCheckConfig implements IConfigModule {
|
||||
@DoNotLoad
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
@ConfigInfo(name = "enabled", comments = "Decide whether the username checks are enabled, \n" +
|
||||
" you could disable it if your players are using Chinese username but also notification any security impacts caused by disabling it")
|
||||
public static boolean enabled = true;
|
||||
@ConfigInfo(name = "enforce_skull_validation", comments = """
|
||||
Enforce skull validation, preventing skulls with invalid names from disconnecting the client.
|
||||
""")
|
||||
public static boolean enforceSkullValidation = true;
|
||||
@ConfigInfo(name = "allow_old_player_join", comments = """
|
||||
Allow old players to join the server after the username regex is changed,
|
||||
even if their names don't meet the new requirements.
|
||||
""")
|
||||
public static boolean allowOldPlayersJoin = false;
|
||||
|
||||
@DoNotLoad
|
||||
private static final String defaultUsernameCheckRegex = "^[a-zA-Z0-9_.]*$";
|
||||
@ConfigInfo(name = "username_check_regex", comments = """
|
||||
Use username regex to validate usernames,
|
||||
allowing only characters specified in the regex.
|
||||
""")
|
||||
public static final String usernameCheckRegex = defaultUsernameCheckRegex;
|
||||
|
||||
@DoNotLoad
|
||||
public static Pattern usernameRegex;
|
||||
|
||||
public static boolean useCustomUsernameRegex() {
|
||||
return !usernameCheckRegex.equals(defaultUsernameCheckRegex);
|
||||
}
|
||||
|
||||
public static boolean shouldSkipNonPlayerNameCheck() { // helper
|
||||
return !enabled || !usernameCheckRegex.equals(defaultUsernameCheckRegex);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
try {
|
||||
usernameRegex = Pattern.compile(usernameCheckRegex);
|
||||
} catch (Exception ex) {
|
||||
LOGGER.error("Failed to parse regex! Falling back to default", ex);
|
||||
|
||||
usernameRegex = Pattern.compile(defaultUsernameCheckRegex);
|
||||
}
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "use_async_protocol_switching")
|
||||
public class AsyncProtocolChangeConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Uses async protocol preparation for mc.
|
||||
Warn: Due to the packet sequence was changed by this optimization, it might be\s
|
||||
uncompatible with some plugins(ViaVersion etc.)""")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+142
@@ -0,0 +1,142 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import io.nanachiyo0721.shiroha.utils.AffinityRunnableWrapper;
|
||||
import net.openhft.affinity.Affinity;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.BitSet;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "cpu_affinity")
|
||||
public class CpuAffinityConfig implements IConfigModule {
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "enabled_for_tickregion", comments = "Using this you could pin the threads of tick region scheduler(Following are the same) to cpu cores listed in the config 'tickregion_affinity' following, \n" +
|
||||
"which is useful for those CPU with P and E cores (such as 12/13/14 gen Intel Core CPUs and so on.)")
|
||||
public static boolean enabledForTickRegion = false;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "enable_for_chunksystem_worker")
|
||||
public static boolean enabledForChunkSystemWorker = false;
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "enable_for_chunksystem_io")
|
||||
public static boolean enabledForChunkSystemIo = false;
|
||||
@ConfigInfo(name = "enable_for_netty_io")
|
||||
public static boolean enableForNettyIo = false;
|
||||
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "tickregion_affinity", comments = "The core number you want the tick region threads to bind on")
|
||||
public static List<String> tickRegionAffinity = Affinity.getAffinity()
|
||||
.stream()
|
||||
.mapToObj(String::valueOf)
|
||||
.toList();
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "chunksystem_worker_affinity")
|
||||
public static List<String> chunkSystemWorkerAffinity = Affinity.getAffinity()
|
||||
.stream()
|
||||
.mapToObj(String::valueOf)
|
||||
.toList();
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "chunksystem_io_affinity")
|
||||
public static List<String> chunkSystemIoAffinity = Affinity.getAffinity()
|
||||
.stream()
|
||||
.mapToObj(String::valueOf)
|
||||
.toList();
|
||||
@HotReloadUnsupported
|
||||
@ConfigInfo(name = "netty_io_affinity")
|
||||
public static List<String> nettyIoAffinity = Affinity.getAffinity()
|
||||
.stream()
|
||||
.mapToObj(String::valueOf)
|
||||
.toList();
|
||||
|
||||
@DoNotLoad
|
||||
private static boolean inited = false;
|
||||
@DoNotLoad
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
@DoNotLoad
|
||||
public static AffinityRunnableWrapper tickRegionRunnableWrapper;
|
||||
@DoNotLoad
|
||||
public static AffinityRunnableWrapper chunkSystemWorkerRunnableWrapper;
|
||||
@DoNotLoad
|
||||
public static AffinityRunnableWrapper chunkSystemIoRunnableWrapper;
|
||||
@DoNotLoad
|
||||
public static AffinityRunnableWrapper nettyIoRunnableWrapper;
|
||||
|
||||
public static Runnable wrapForTickRegion(Runnable in) {
|
||||
return tickRegionRunnableWrapper == null ? in : tickRegionRunnableWrapper.wrap(in);
|
||||
}
|
||||
|
||||
public static Runnable wrapForChunkSystemWorker(Runnable in) {
|
||||
return chunkSystemWorkerRunnableWrapper == null ? in : chunkSystemWorkerRunnableWrapper.wrap(in);
|
||||
}
|
||||
|
||||
public static Runnable wrapForChunkSystemIo(Runnable in) {
|
||||
return chunkSystemIoRunnableWrapper == null ? in : chunkSystemIoRunnableWrapper.wrap(in);
|
||||
}
|
||||
|
||||
public static Runnable wrapForNettyIo(Runnable in) {
|
||||
return nettyIoRunnableWrapper == null ? in : nettyIoRunnableWrapper.wrap(in);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
if (inited) {
|
||||
return;
|
||||
}
|
||||
inited = true;
|
||||
|
||||
if (enabledForTickRegion) {
|
||||
tickRegionRunnableWrapper = new AffinityRunnableWrapper("tick_region", parseAffinity(tickRegionAffinity));
|
||||
LOGGER.info("Tick region thread now bound to: {}", tickRegionRunnableWrapper.getAffinity());
|
||||
}
|
||||
|
||||
if (enabledForChunkSystemIo) {
|
||||
chunkSystemIoRunnableWrapper = new AffinityRunnableWrapper("chunk_system_io", parseAffinity(chunkSystemIoAffinity));
|
||||
LOGGER.info("Chunk system I/O thread now bound to: {}", chunkSystemIoRunnableWrapper.getAffinity());
|
||||
}
|
||||
|
||||
if (enabledForChunkSystemWorker) {
|
||||
chunkSystemWorkerRunnableWrapper = new AffinityRunnableWrapper("chunk_system_worker", parseAffinity(chunkSystemWorkerAffinity));
|
||||
LOGGER.info("Chunk system worker thread now bound to: {}", chunkSystemIoRunnableWrapper.getAffinity());
|
||||
}
|
||||
|
||||
if (enableForNettyIo) {
|
||||
nettyIoRunnableWrapper = new AffinityRunnableWrapper("netty_io", parseAffinity(nettyIoAffinity));
|
||||
LOGGER.info("Netty I/O thread now bound to: {}", nettyIoRunnableWrapper.getAffinity());
|
||||
}
|
||||
}
|
||||
|
||||
private @NonNull BitSet parseAffinity(@NonNull List<String> affinity) {
|
||||
int maxAvailable = Runtime.getRuntime().availableProcessors();
|
||||
BitSet affinitySet = new BitSet(affinity.size());
|
||||
affinity.stream()
|
||||
.mapToInt(str -> {
|
||||
try {
|
||||
return Integer.parseInt(str);
|
||||
} catch (NumberFormatException ignored) {
|
||||
LOGGER.warn("Unable to parse cpu id {} to a valid number, falling back to 0.", str);
|
||||
return 0;
|
||||
}
|
||||
})
|
||||
.distinct()
|
||||
.filter(cpuId -> {
|
||||
if (cpuId >= 0 && cpuId < maxAvailable) {
|
||||
return true;
|
||||
} else {
|
||||
LOGGER.warn("Invalid cpu id {}, ignoring.", cpuId);
|
||||
return false;
|
||||
}
|
||||
})
|
||||
.forEach(affinitySet::set);
|
||||
return affinitySet;
|
||||
}
|
||||
}
|
||||
+15
@@ -0,0 +1,15 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "throttle_goal_selector_tick_in_inactive_tick", comments =
|
||||
"Throttles the AI goal selector in entity inactive ticks. \n" +
|
||||
"This can improve performance by a few percent, but has minor gameplay implications."
|
||||
)
|
||||
public class EntityGoalSelectorInactiveTickConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean enabled = false;
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "variable_entity_waking_up")
|
||||
public class GaleVariableEntityWakeupConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "entity_wakeup_duration_ratio_standard_deviation", comments = """
|
||||
If this value is set to any value > 0, waking up inactive entities happens spread over time, instead of many entities at once. This makes entities feel and behave more natural.
|
||||
This setting is the coefficient of variation, or σ / μ (the ratio of the standard deviation to the mean) of the inactivity duration.
|
||||
|
||||
In other words, this setting is the value σ, so that the regular inactivity duration will be multiplied by a factor normal_distribution(μ = 1, σ).
|
||||
If a value ≤ 0 is given, variable entity wake-up is disabled.""")
|
||||
public static double entityWakeUpDurationRatioStandardDeviation = 0.2;
|
||||
}
|
||||
+18
@@ -0,0 +1,18 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import dev.kaiijumc.kaiiju.KaiijuEntityLimits;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "kaiiju_entity_limiter")
|
||||
public class KaiijuEntityLimiterConfig implements IConfigModule {
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
KaiijuEntityLimits.init();
|
||||
}
|
||||
}
|
||||
+17
@@ -0,0 +1,17 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.HotReloadUnsupported;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "lithium_sleeping_block_entity")
|
||||
public class LeavesSleepingBlockEntityConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled", comments = """
|
||||
Use sleeping blocking optimizations from lithium,\s
|
||||
on luminol the hopper optimizations of paper were totally removed and replaced by those of lithium\s
|
||||
and it's turned on by default""")
|
||||
@HotReloadUnsupported
|
||||
public static boolean enabled = true;
|
||||
}
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "lobotomize_villager", comments = "Lobotomizes the villager if it cannot move (Does not disable trading)")
|
||||
public class LobotomizeVillageConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean villagerLobotomizeEnabled = false;
|
||||
@ConfigInfo(name = "check_interval", comments = "The interval in ticks to check if a villager is lobotomized ")
|
||||
public static int villagerLobotomizeCheckInterval = 100;
|
||||
@ConfigInfo(name = "wait_until_trade_locked", comments = "Wait until a villager has been traded with before lobotomizing")
|
||||
public static boolean villagerLobotomizeWaitUntilTradeLocked = false;
|
||||
}
|
||||
+12
@@ -0,0 +1,12 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "end_dragon")
|
||||
public class OptimizedDragonRespawnConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "optimized_dragon_respawn")
|
||||
public static boolean optimizedRespawn = false;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "reduce_sensor_work", comments = "When it is enabled, it will delete the line of sight cache less often and use a faster nearby comparison.")
|
||||
public class PetalReduceSensorWorkConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean enabled = true;
|
||||
@ConfigInfo(name = "delay_ticks", comments = "The interval of each entity to drop the cache(in ticks)")
|
||||
public static int delayTicks = 10;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "projectile")
|
||||
public class ProjectileChunkReduceConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "max-loads-per-tick", comments = "Controls how many chunks are allowed to be sync loaded by projectiles in a tick.")
|
||||
public static int maxProjectileLoadsPerTick;
|
||||
@ConfigInfo(name = "max-loads-per-projectile", comments = "Controls how many chunks a projectile can load in its lifetime before it gets automatically removed.")
|
||||
public static int maxProjectileLoadsPerProjectile;
|
||||
}
|
||||
+45
@@ -0,0 +1,45 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.optimizations;
|
||||
|
||||
import com.electronwill.nightconfig.core.file.CommentedFileConfig;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import gg.pufferfish.pufferfish.simd.SIMDDetection;
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.DoNotLoad;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.Set;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.OPTIMIZATIONS, name = "use_simd")
|
||||
public class SIMDConfig implements IConfigModule {
|
||||
@DoNotLoad
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
@ConfigInfo(name = "enabled")
|
||||
public static boolean enabled = true;
|
||||
|
||||
@Override
|
||||
public void onLoaded(CommentedFileConfig configInstance, @Nullable Set<Exception> e) {
|
||||
if (!enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Attempt to detect vectorization
|
||||
try {
|
||||
SIMDDetection.isEnabled = SIMDDetection.canEnable(LOGGER);
|
||||
} catch (NoClassDefFoundError | Exception ignored) {
|
||||
ignored.printStackTrace();
|
||||
}
|
||||
|
||||
if (SIMDDetection.isEnabled) {
|
||||
LOGGER.info("SIMD operations detected as functional. Will replace some operations with faster versions.");
|
||||
} else {
|
||||
LOGGER.warn("SIMD operations are available for your server, but are not configured!");
|
||||
LOGGER.warn("To enable additional optimizations, add \"--add-modules=jdk.incubator.vector\" to your startup flags, BEFORE the \"-jar\".");
|
||||
LOGGER.warn("If you have already added this flag, then SIMD operations are not supported on your JVM or CPU.");
|
||||
LOGGER.warn("Debug: Java: {}, test run: {}", System.getProperty("java.version"), SIMDDetection.testRun);
|
||||
}
|
||||
}
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.removed;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.REMOVED, name = "removed_config")
|
||||
public class RemovedConfig implements IConfigModule {
|
||||
@ConfigInfo(name = "removed", comments =
|
||||
"""
|
||||
RemovedConfig redirect to here, no any function.""")
|
||||
public static boolean enabled = true;
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
package io.nanachiyo0721.shiroha.config.modules.unsupported;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.IConfigModule;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigClassInfo;
|
||||
import io.nanachiyo0721.shiroha.config.flags.ConfigInfo;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumConfigCategory;
|
||||
|
||||
@ConfigClassInfo(category = EnumConfigCategory.UNSUPPORTED, name = "disable_check_for_folia_supported")
|
||||
public class DisableCheckForFoliaSupported implements IConfigModule {
|
||||
@ConfigInfo(name = "disable_for_paper", comments = """
|
||||
Disable check for folia-supported for spigot/bukkit/paper plugin.
|
||||
ATTENTION: No support will be provided if you enabled this.""")
|
||||
public static boolean disableForPaper = false;
|
||||
}
|
||||
+1441
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
||||
package io.nanachiyo0721.shiroha.data;
|
||||
|
||||
|
||||
import ca.spottedleaf.moonrise.patches.chunk_system.storage.ChunkSystemRegionFile;
|
||||
import net.minecraft.nbt.CompoundTag;
|
||||
import net.minecraft.world.level.ChunkPos;
|
||||
|
||||
import java.io.DataInputStream;
|
||||
import java.io.DataOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.nio.ByteBuffer;
|
||||
import java.nio.file.Path;
|
||||
|
||||
public interface RegionFile extends ChunkSystemRegionFile, AutoCloseable {
|
||||
Path getPath();
|
||||
|
||||
DataInputStream getChunkDataInputStream(ChunkPos pos) throws IOException;
|
||||
|
||||
boolean doesChunkExist(ChunkPos pos) throws Exception;
|
||||
|
||||
DataOutputStream getChunkDataOutputStream(ChunkPos pos) throws IOException;
|
||||
|
||||
void flush() throws IOException;
|
||||
|
||||
void clear(ChunkPos pos) throws IOException;
|
||||
|
||||
boolean hasChunk(ChunkPos pos);
|
||||
|
||||
void close() throws IOException;
|
||||
|
||||
void write(ChunkPos pos, ByteBuffer buf) throws IOException;
|
||||
|
||||
CompoundTag getOversizedData(int x, int z) throws IOException;
|
||||
|
||||
boolean isOversized(int x, int z);
|
||||
|
||||
boolean recalculateHeader() throws IOException;
|
||||
|
||||
void setOversized(int x, int z, boolean oversized) throws IOException;
|
||||
|
||||
default int getRecalculateCount() {
|
||||
return 0;
|
||||
} // Luminol - Configurable region file format
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBar;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.impl.Membar;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.impl.RegionBar;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.impl.TpsBar;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
public enum EnumBarType {
|
||||
TPS(
|
||||
TpsBar.class,
|
||||
"tps",
|
||||
"function.tpsbar.enabled",
|
||||
TpsBar::buildSettings
|
||||
),
|
||||
MEMORY(
|
||||
Membar.class,
|
||||
"memory",
|
||||
"membar",
|
||||
"function.membar.enabled",
|
||||
Membar::buildSettings
|
||||
),
|
||||
REGION(
|
||||
RegionBar.class,
|
||||
"region",
|
||||
"function.regionbar.enabled",
|
||||
RegionBar::buildSettings
|
||||
);
|
||||
|
||||
private final Class<? extends TickableStatusBar> clazz;
|
||||
private final String name;
|
||||
private final String commandName;
|
||||
private final String configPath;
|
||||
private final String configOrigin;
|
||||
private final Supplier<Map<String, Object>> settingsProvider;
|
||||
|
||||
EnumBarType(Class<? extends TickableStatusBar> clazz, String name, String configPath, Supplier<Map<String, Object>> settingsProvider) {
|
||||
this(clazz, name, name + "bar", configPath, settingsProvider);
|
||||
}
|
||||
|
||||
EnumBarType(Class<? extends TickableStatusBar> clazz, String name, Pair<String, String> configPath, Supplier<Map<String, Object>> settingsProvider) {
|
||||
this(clazz, name, name + "bar", configPath, settingsProvider);
|
||||
}
|
||||
|
||||
EnumBarType(Class<? extends TickableStatusBar> clazz, String name, String commandName, String configPath, Supplier<Map<String, Object>> settingsProvider) {
|
||||
this(clazz, name, commandName, new Pair<>("luminol", configPath), settingsProvider);
|
||||
}
|
||||
|
||||
EnumBarType(Class<? extends TickableStatusBar> clazz, String name, String commandName, @NonNull Pair<String, String> configPath, Supplier<Map<String, Object>> settingsProvider) {
|
||||
this.clazz = clazz;
|
||||
this.name = name;
|
||||
this.commandName = commandName;
|
||||
this.configPath = configPath.getSecond();
|
||||
this.configOrigin = configPath.getFirst();
|
||||
this.settingsProvider = settingsProvider;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public TickableStatusBar newBar(Player player) {
|
||||
try {
|
||||
return this.clazz.getConstructor(Player.class).newInstance(player);
|
||||
} catch (Exception e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
public Map<String, Object> getSettings() {
|
||||
return this.settingsProvider.get();
|
||||
}
|
||||
|
||||
public String getCommandName() {
|
||||
return this.commandName;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public String getConfigOrigin() {
|
||||
return this.configOrigin;
|
||||
}
|
||||
|
||||
public String getConfigPath() {
|
||||
return this.configPath;
|
||||
}
|
||||
}
|
||||
+7
@@ -0,0 +1,7 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
public enum EnumCollisionBehaviorMode {
|
||||
VANILLA,
|
||||
BLOCK_SHAPE_VANILLA,
|
||||
PAPER
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
public enum EnumConfigCategory {
|
||||
OPTIMIZATIONS("optimizations"), // optimize performance feature
|
||||
FIXES("fixes"), // fix not vanilla or other bugs caused by folia/paper
|
||||
MISC("misc"), // unknown classify features
|
||||
FUNCTION("function"), // new functions
|
||||
EXPERIMENT("experiment"), // experimental features
|
||||
UNSUPPORTED("unsupported"), // features we do not want anyone to use
|
||||
REMOVED("removed"), // removed config
|
||||
ROOT(null);
|
||||
|
||||
private final String baseKeyName;
|
||||
private final String keyComment;
|
||||
|
||||
EnumConfigCategory(String baseKeyName, String keyComment) {
|
||||
this.baseKeyName = baseKeyName;
|
||||
this.keyComment = keyComment;
|
||||
}
|
||||
|
||||
EnumConfigCategory(String baseKeyName) {
|
||||
this.baseKeyName = baseKeyName;
|
||||
this.keyComment = null;
|
||||
}
|
||||
|
||||
public String getBaseKeyName() {
|
||||
return this.baseKeyName;
|
||||
}
|
||||
|
||||
public String getKeyComment() {
|
||||
return this.keyComment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
import abomination.LinearRegionFile;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.RegionFormatConfig;
|
||||
import io.nanachiyo0721.shiroha.data.BufferedLinearRegionFile;
|
||||
import io.nanachiyo0721.shiroha.utils.RegionFileFactory;
|
||||
import net.minecraft.world.level.chunk.storage.RegionFile;
|
||||
|
||||
public enum EnumRegionFormat {
|
||||
MCA("mca", (info) -> new RegionFile(info.info(), info.filePath(), info.folder(), info.sync())),
|
||||
LINEAR_V2("linear", (info) -> new LinearRegionFile(info.info(), info.filePath(), info.folder(), info.sync(), RegionFormatConfig.linearCompressionLevel)),
|
||||
B_LINEAR("b_linear", (info) -> new BufferedLinearRegionFile(info.filePath(), RegionFormatConfig.linearCompressionLevel, RegionFormatConfig.blinearFlusher));
|
||||
|
||||
private final String argument;
|
||||
private final RegionFileFactory creator;
|
||||
|
||||
EnumRegionFormat(String argument, RegionFileFactory creator) {
|
||||
this.argument = argument;
|
||||
this.creator = creator;
|
||||
}
|
||||
|
||||
public RegionFileFactory getCreator() {
|
||||
return this.creator;
|
||||
}
|
||||
|
||||
public String getArgument() {
|
||||
return this.argument;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
public enum EnumStatusBarDisplay {
|
||||
BOSS_BAR,
|
||||
ACTION_BAR,
|
||||
TAB_LIST;
|
||||
|
||||
@Contract(pure = true)
|
||||
public static @Nullable EnumStatusBarDisplay fromOrdinal(int ordinal) {
|
||||
EnumStatusBarDisplay[] values = values();
|
||||
|
||||
if (ordinal < 0 || ordinal >= values.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return values[ordinal];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package io.nanachiyo0721.shiroha.enums;
|
||||
|
||||
public enum EnumTripwireBehavior {
|
||||
VANILLA20,
|
||||
VANILLA21,
|
||||
MIXED
|
||||
}
|
||||
+194
@@ -0,0 +1,194 @@
|
||||
package io.nanachiyo0721.shiroha.functions.bars;
|
||||
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
public abstract class TickableStatusBar {
|
||||
public static final String SETTING_KEY_UPDATE_INTERVALS = "update_intervals";
|
||||
public static final String SETTING_KEY_ENABLED = "enabled";
|
||||
public static final String SETTING_DISPLAY = "display";
|
||||
public static final String SETTING_ALLOW_PLAYER_DISPLAY_SWITCH = "allow_player_display_switch";
|
||||
|
||||
protected final Player player;
|
||||
private BossBar bar = null;
|
||||
|
||||
private long tickedCount = 0;
|
||||
private boolean lastIsVisible = false;
|
||||
private EnumStatusBarDisplay lastDisplay;
|
||||
|
||||
private boolean visible = false;
|
||||
private boolean enabled = false;
|
||||
private boolean allowPlayerDisplaySwitch = false;
|
||||
|
||||
private int updateIntervalInTicks;
|
||||
private EnumStatusBarDisplay display;
|
||||
private EnumStatusBarDisplay storedDisplay;
|
||||
|
||||
public TickableStatusBar(Player player) {
|
||||
this.player = player;
|
||||
}
|
||||
|
||||
public EnumStatusBarDisplay getDisplay() {
|
||||
return this.display;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle the update of the status bar display update
|
||||
*
|
||||
* @param bar the bossbar instance if the display mode is BOSS_BAR, else it's null
|
||||
* @param owner the player that is displayed for
|
||||
* @see EnumStatusBarDisplay
|
||||
*/
|
||||
public abstract void updateDisplay(@Nullable BossBar bar, Player owner);
|
||||
|
||||
public void handleDisplayUpdate(Player owner, EnumStatusBarDisplay old, EnumStatusBarDisplay newDisplay) {
|
||||
if (old == EnumStatusBarDisplay.TAB_LIST && newDisplay != EnumStatusBarDisplay.TAB_LIST) {
|
||||
final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity();
|
||||
|
||||
// reset the display
|
||||
apiOwner.sendPlayerListFooter(Component.empty());
|
||||
}
|
||||
}
|
||||
|
||||
public BossBar newBar() {
|
||||
return BossBar.bossBar(Component.text(""), 0.0F, BossBar.Color.PURPLE, BossBar.Overlay.NOTCHED_20);
|
||||
}
|
||||
|
||||
public void initSettings(@NotNull Map<String, Object> settings) {
|
||||
this.applySettings(settings);
|
||||
|
||||
// actual we need sync it here
|
||||
this.lastDisplay = this.display;
|
||||
}
|
||||
|
||||
public void applySettings(@NotNull Map<String, Object> settings) {
|
||||
this.updateIntervalInTicks = (int) settings.getOrDefault(SETTING_KEY_UPDATE_INTERVALS, 20);
|
||||
this.enabled = (boolean) settings.getOrDefault(SETTING_KEY_ENABLED, false);
|
||||
this.allowPlayerDisplaySwitch = (boolean) settings.getOrDefault(SETTING_ALLOW_PLAYER_DISPLAY_SWITCH, false);
|
||||
|
||||
// pre init(the value might not be initialized if it's a new player)
|
||||
if (this.storedDisplay == null) {
|
||||
this.storedDisplay = (EnumStatusBarDisplay) settings.getOrDefault(SETTING_DISPLAY, EnumStatusBarDisplay.BOSS_BAR);
|
||||
}
|
||||
|
||||
// pre init(the value might not be initialized if it's a new player)
|
||||
// also force update when custom switch is not allowed
|
||||
if (this.display == null || !this.allowPlayerDisplaySwitch) {
|
||||
this.display = (EnumStatusBarDisplay) settings.getOrDefault(SETTING_DISPLAY, EnumStatusBarDisplay.BOSS_BAR);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return this.enabled;
|
||||
}
|
||||
|
||||
protected void tick() {
|
||||
final CraftPlayer apiPlayer = (CraftPlayer) this.player.getBukkitEntity();
|
||||
boolean barUpdateRequired = this.tickedCount % this.updateIntervalInTicks == 0;
|
||||
|
||||
final boolean isActualVisible = this.visible && this.enabled;
|
||||
|
||||
final boolean usesBossbarBefore = this.lastDisplay == EnumStatusBarDisplay.BOSS_BAR;
|
||||
final boolean usesBossbar = this.display == EnumStatusBarDisplay.BOSS_BAR;
|
||||
|
||||
// reduce allocations
|
||||
if (this.bar == null && usesBossbar) {
|
||||
this.bar = this.newBar();
|
||||
}
|
||||
|
||||
// handle display updates
|
||||
// bossbar -> other
|
||||
if (usesBossbarBefore && !usesBossbar) {
|
||||
apiPlayer.hideBossBar(this.bar);
|
||||
}
|
||||
// other -> bossbar
|
||||
if (!usesBossbarBefore && usesBossbar) {
|
||||
apiPlayer.showBossBar(this.bar);
|
||||
}
|
||||
// sync display state
|
||||
if (this.lastDisplay != this.display) {
|
||||
this.handleDisplayUpdate(this.player, this.lastDisplay, this.display);
|
||||
|
||||
this.lastDisplay = this.display;
|
||||
}
|
||||
|
||||
// handle visible state upgrade / downgrade
|
||||
// visible -> invisible
|
||||
if (this.lastIsVisible && !isActualVisible) {
|
||||
if (usesBossbar) {
|
||||
// go hide
|
||||
apiPlayer.hideBossBar(this.bar);
|
||||
}
|
||||
}
|
||||
|
||||
// invisible -> visible
|
||||
if (!this.lastIsVisible && isActualVisible) {
|
||||
if (usesBossbar) {
|
||||
// make it visible then
|
||||
apiPlayer.showBossBar(this.bar);
|
||||
}
|
||||
|
||||
// force update once
|
||||
this.updateDisplay(this.bar, this.player);
|
||||
// skip unnecessary updates
|
||||
barUpdateRequired = false;
|
||||
}
|
||||
|
||||
// refresh the old value after sync
|
||||
if (this.lastIsVisible != isActualVisible) {
|
||||
this.lastIsVisible = isActualVisible;
|
||||
}
|
||||
|
||||
// we only updates the displayed value when it's visible
|
||||
if (isActualVisible) {
|
||||
if (barUpdateRequired) {
|
||||
this.updateDisplay(this.bar, this.player);
|
||||
}
|
||||
}
|
||||
|
||||
this.tickedCount++;
|
||||
}
|
||||
|
||||
// note: the visible update is performed by the tick logic, we don't actively update it
|
||||
public void setVisible(boolean visible) {
|
||||
this.visible = visible;
|
||||
}
|
||||
|
||||
public boolean isVisible() {
|
||||
return this.visible;
|
||||
}
|
||||
|
||||
public void store(@NotNull ValueOutput output) {
|
||||
output.putBoolean("visible", this.visible);
|
||||
output.putByte("display", (byte) this.storedDisplay.ordinal());
|
||||
}
|
||||
|
||||
public void load(@NotNull ValueInput input) {
|
||||
this.visible = input.getBooleanOr("visible", false);
|
||||
|
||||
EnumStatusBarDisplay display = EnumStatusBarDisplay.fromOrdinal(input.getByteOr("display", (byte) 0));
|
||||
// null -> not found
|
||||
// also we only change it when custom switch is enabled
|
||||
if (display == null) {
|
||||
// init (by default it's that configured value)
|
||||
this.storedDisplay = this.display;
|
||||
} else {
|
||||
// value is present, sync
|
||||
this.storedDisplay = display;
|
||||
|
||||
// then sync to mainline if custom switch is allowed
|
||||
if (this.allowPlayerDisplaySwitch) {
|
||||
this.display = display;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+171
@@ -0,0 +1,171 @@
|
||||
package io.nanachiyo0721.shiroha.functions.bars;
|
||||
|
||||
import com.mojang.logging.LogUtils;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumBarType;
|
||||
import net.minecraft.server.MinecraftServer;
|
||||
import net.minecraft.server.players.PlayerList;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import net.minecraft.world.level.storage.ValueInput;
|
||||
import net.minecraft.world.level.storage.ValueOutput;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.util.EnumMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class TickableStatusBarList {
|
||||
private static final Logger LOGGER = LogUtils.getLogger();
|
||||
|
||||
private final EnumMap<EnumBarType, TickableStatusBar> managedBars = new EnumMap<>(EnumBarType.class);
|
||||
private final Player player;
|
||||
|
||||
public TickableStatusBarList(Player player) {
|
||||
this.player = player;
|
||||
|
||||
for (EnumBarType type : EnumBarType.values()) {
|
||||
final TickableStatusBar bar = type.newBar(this.player);
|
||||
|
||||
bar.initSettings(type.getSettings());
|
||||
|
||||
this.managedBars.put(type, bar);
|
||||
}
|
||||
}
|
||||
|
||||
public static void raiseGlobalReload(EnumBarType type) {
|
||||
final MinecraftServer server = MinecraftServer.getServer();
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PlayerList playerList = server.getPlayerList();
|
||||
if (playerList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Player playerInList : playerList.getPlayers()) {
|
||||
playerInList.getBukkitEntity().taskScheduler.scheduleOrExecute(_ -> playerInList.statusBarList.reload(type));
|
||||
}
|
||||
}
|
||||
|
||||
public static void raiseGlobalReload() {
|
||||
final MinecraftServer server = MinecraftServer.getServer();
|
||||
if (server == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
final PlayerList playerList = server.getPlayerList();
|
||||
if (playerList == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Player playerInList : playerList.getPlayers()) {
|
||||
playerInList.getBukkitEntity().taskScheduler.scheduleOrExecute(_ -> playerInList.statusBarList.reloadAll());
|
||||
}
|
||||
}
|
||||
|
||||
public void reloadAll() {
|
||||
for (EnumBarType type : EnumBarType.values()) {
|
||||
this.reload(type);
|
||||
}
|
||||
}
|
||||
|
||||
public void reload(EnumBarType type) {
|
||||
final TickableStatusBar bar = this.managedBars.get(type);
|
||||
if (bar == null) {
|
||||
LOGGER.warn("Reloading a non-existed bar {} !", type);
|
||||
return;
|
||||
}
|
||||
|
||||
bar.applySettings(type.getSettings());
|
||||
}
|
||||
|
||||
public void tick() {
|
||||
for (TickableStatusBar bar : this.managedBars.values()) {
|
||||
bar.tick();
|
||||
}
|
||||
}
|
||||
|
||||
public void load(@NonNull ValueInput input) {
|
||||
final ValueInput statusBarsInput = input
|
||||
.child("luminol")
|
||||
.flatMap(luminol -> luminol.child("status_bars"))
|
||||
.orElse(null); // I hate these optionals (x)
|
||||
|
||||
// does not have this data
|
||||
if (statusBarsInput == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (Map.Entry<EnumBarType, TickableStatusBar> barEntry : this.managedBars.entrySet()) {
|
||||
final EnumBarType type = barEntry.getKey();
|
||||
final TickableStatusBar bar = barEntry.getValue();
|
||||
final String categoryName = type.getName();
|
||||
|
||||
if (bar != null) {
|
||||
final ValueInput inputOfThisBar = statusBarsInput.child(categoryName).orElse(null);
|
||||
|
||||
// does not have this data
|
||||
if (inputOfThisBar == null) {
|
||||
continue;
|
||||
}
|
||||
|
||||
bar.load(inputOfThisBar);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.warn("Skipping loading null status bar {} !", categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
public void save(@NonNull ValueOutput output) {
|
||||
final ValueOutput statusBarsOutput = output
|
||||
.child("luminol")
|
||||
.child("status_bars");
|
||||
|
||||
for (Map.Entry<EnumBarType, TickableStatusBar> barEntry : this.managedBars.entrySet()) {
|
||||
final EnumBarType type = barEntry.getKey();
|
||||
final TickableStatusBar bar = barEntry.getValue();
|
||||
final String categoryName = type.getName();
|
||||
|
||||
if (bar != null) {
|
||||
final ValueOutput outOfThisBar = statusBarsOutput.child(categoryName);
|
||||
|
||||
bar.store(outOfThisBar);
|
||||
continue;
|
||||
}
|
||||
|
||||
LOGGER.warn("Skipping storing null status bar {} !", categoryName);
|
||||
}
|
||||
}
|
||||
|
||||
public void setVisible(EnumBarType type, boolean visible) {
|
||||
final TickableStatusBar bar = this.managedBars.get(type);
|
||||
|
||||
if (bar == null) {
|
||||
LOGGER.warn("Bar with type {} does not exist! Skipping visibility updates.", type);
|
||||
return;
|
||||
}
|
||||
|
||||
bar.setVisible(visible);
|
||||
}
|
||||
|
||||
public boolean isVisible(EnumBarType type) {
|
||||
final TickableStatusBar bar = this.managedBars.get(type);
|
||||
|
||||
if (bar == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return bar.isVisible();
|
||||
}
|
||||
|
||||
public boolean isEnabled(EnumBarType type) {
|
||||
final TickableStatusBar bar = this.managedBars.get(type);
|
||||
|
||||
if (bar == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return bar.isEnabled();
|
||||
}
|
||||
}
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
package io.nanachiyo0721.shiroha.functions.bars.impl;
|
||||
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.MembarConfig;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBar;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.lang.management.ManagementFactory;
|
||||
import java.lang.management.MemoryUsage;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class Membar extends TickableStatusBar {
|
||||
public Membar(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public static @NonNull @UnmodifiableView Map<String, Object> buildSettings() {
|
||||
final HashMap<String, Object> ret = new HashMap<>();
|
||||
|
||||
ret.put(TickableStatusBar.SETTING_KEY_ENABLED, MembarConfig.memoryBarEnabled);
|
||||
ret.put(TickableStatusBar.SETTING_DISPLAY, MembarConfig.display);
|
||||
ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, MembarConfig.updateInterval);
|
||||
|
||||
return Collections.unmodifiableMap(ret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplay(@Nullable BossBar bar, @NonNull Player owner) {
|
||||
final EnumStatusBarDisplay display = this.getDisplay();
|
||||
final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity();
|
||||
|
||||
final MemoryUsage heap = ManagementFactory.getMemoryMXBean().getHeapMemoryUsage();
|
||||
long used = heap.getUsed();
|
||||
long xmx = heap.getMax();
|
||||
|
||||
double percent = Math.clamp((float) used / xmx, 0.0F, 1.0F);
|
||||
final Component message = MiniMessage.miniMessage().deserialize(
|
||||
MembarConfig.memBarFormat,
|
||||
Placeholder.component("used", getMemoryComponent(used, xmx)),
|
||||
Placeholder.component("available", getMaxMemComponent(xmx))
|
||||
);
|
||||
|
||||
switch (display) {
|
||||
case BOSS_BAR -> bar.name(message).color(barColorForMemory(percent)).progress((float) percent);
|
||||
|
||||
case ACTION_BAR -> apiOwner.sendActionBar(message);
|
||||
|
||||
case TAB_LIST -> apiOwner.sendPlayerListFooter(message);
|
||||
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull Component getMaxMemComponent(double max) {
|
||||
final BossBar.Color colorBukkit = BossBar.Color.GREEN;
|
||||
final String colorString = colorBukkit.name();
|
||||
|
||||
final String content = "<%s><text></%s>";
|
||||
final String replaced = String.format(content, colorString, colorString);
|
||||
|
||||
return MiniMessage.miniMessage().deserialize(replaced, Placeholder.parsed("text", String.format("%.2f", max / (1024 * 1024))));
|
||||
}
|
||||
|
||||
private static @NotNull Component getMemoryComponent(long used, long max) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForMemory(Math.clamp((float) used / max, 0.0F, 1.0F)), Placeholder.parsed("text", String.format("%.2f", (double) used / (1024 * 1024))));
|
||||
}
|
||||
|
||||
private static BossBar.Color barColorForMemory(double memPercent) {
|
||||
if (memPercent == -1) {
|
||||
return MembarConfig.barColors.get(3);
|
||||
}
|
||||
|
||||
if (memPercent <= 50) {
|
||||
return MembarConfig.barColors.get(0);
|
||||
}
|
||||
|
||||
if (memPercent <= 70) {
|
||||
return MembarConfig.barColors.get(1);
|
||||
}
|
||||
|
||||
return MembarConfig.barColors.get(2);
|
||||
}
|
||||
|
||||
private static String textPlaceholderForMemory(double memPercent) {
|
||||
if (memPercent == -1) {
|
||||
return MembarConfig.memColors.get(3);
|
||||
}
|
||||
|
||||
if (memPercent <= 50) {
|
||||
return MembarConfig.memColors.get(0);
|
||||
}
|
||||
|
||||
if (memPercent <= 70) {
|
||||
return MembarConfig.memColors.get(1);
|
||||
}
|
||||
|
||||
return MembarConfig.memColors.get(2);
|
||||
}
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
package io.nanachiyo0721.shiroha.functions.bars.impl;
|
||||
|
||||
import ca.spottedleaf.common.time.TickData;
|
||||
import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
||||
import io.papermc.paper.threadedregions.TickRegionScheduler;
|
||||
import io.papermc.paper.threadedregions.TickRegions;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.RegionBarConfig;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBar;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class RegionBar extends TickableStatusBar {
|
||||
private final ThreadLocal<DecimalFormat> ONE_DECIMAL_PLACES = ThreadLocal.withInitial(() -> new DecimalFormat("#,##0.0"));
|
||||
|
||||
public RegionBar(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public static @NonNull @UnmodifiableView Map<String, Object> buildSettings() {
|
||||
final HashMap<String, Object> ret = new HashMap<>();
|
||||
|
||||
ret.put(TickableStatusBar.SETTING_KEY_ENABLED, RegionBarConfig.regionbarEnabled);
|
||||
ret.put(TickableStatusBar.SETTING_DISPLAY, RegionBarConfig.display);
|
||||
ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, RegionBarConfig.updateInterval);
|
||||
|
||||
return Collections.unmodifiableMap(ret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplay(@Nullable BossBar bar, @NonNull Player owner) {
|
||||
final EnumStatusBarDisplay display = this.getDisplay();
|
||||
final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity();
|
||||
|
||||
final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region = TickRegionScheduler.getCurrentRegion();
|
||||
final TickData.TickReportData reportData = region.getData().getRegionSchedulingHandle().getTickReport5s(System.nanoTime());
|
||||
final TickRegions.RegionStats regionStats = region.getData().getRegionStats();
|
||||
|
||||
final double utilisation = reportData.utilisation();
|
||||
final int chunkCount = regionStats.getChunkCount();
|
||||
final int playerCount = regionStats.getPlayerCount();
|
||||
final int entityCount = regionStats.getEntityCount();
|
||||
|
||||
final double utilisationPercent = utilisation * 100.0;
|
||||
final String formattedUtil = ONE_DECIMAL_PLACES.get().format(utilisationPercent);
|
||||
final Component message = MiniMessage.miniMessage().deserialize(
|
||||
RegionBarConfig.regionBarFormat,
|
||||
Placeholder.component("util", getUtilComponent(formattedUtil)),
|
||||
Placeholder.component("chunks", getChunksComponent(chunkCount)),
|
||||
Placeholder.component("players", getPlayersComponent(playerCount)),
|
||||
Placeholder.component("entities", getEntitiesComponent(entityCount))
|
||||
);
|
||||
|
||||
switch (display) {
|
||||
case ACTION_BAR -> apiOwner.sendActionBar(message);
|
||||
|
||||
case BOSS_BAR ->
|
||||
bar.name(message).color(barColorForUtil(utilisationPercent)).progress((float) Math.clamp(utilisation, 0, 1.0));
|
||||
|
||||
case TAB_LIST -> apiOwner.sendPlayerListFooter(message);
|
||||
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull Component getEntitiesComponent(int entities) {
|
||||
final String content = "<text>";
|
||||
return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(entities)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getPlayersComponent(int players) {
|
||||
final String content = "<text>";
|
||||
return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(players)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getChunksComponent(int chunks) {
|
||||
final String content = "<text>";
|
||||
return MiniMessage.miniMessage().deserialize(content, Placeholder.parsed("text", String.valueOf(chunks)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getUtilComponent(String formattedUtil) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForUtil(Double.parseDouble(formattedUtil)), Placeholder.parsed("text", formattedUtil + "%"));
|
||||
}
|
||||
|
||||
private static BossBar.Color barColorForUtil(double util) {
|
||||
if (util > 100) {
|
||||
return RegionBarConfig.barColors.get(3);
|
||||
}
|
||||
|
||||
if (util >= 70) {
|
||||
return RegionBarConfig.barColors.get(2);
|
||||
}
|
||||
|
||||
if (util >= 50) {
|
||||
return RegionBarConfig.barColors.get(1);
|
||||
}
|
||||
|
||||
return RegionBarConfig.barColors.get(0);
|
||||
}
|
||||
|
||||
private static String textPlaceholderForUtil(double util) {
|
||||
if (util > 100) {
|
||||
return RegionBarConfig.utilColors.get(3);
|
||||
}
|
||||
|
||||
if (util >= 70) {
|
||||
return RegionBarConfig.utilColors.get(2);
|
||||
}
|
||||
|
||||
if (util >= 50) {
|
||||
return RegionBarConfig.utilColors.get(1);
|
||||
}
|
||||
|
||||
return RegionBarConfig.utilColors.get(0);
|
||||
}
|
||||
}
|
||||
+167
@@ -0,0 +1,167 @@
|
||||
package io.nanachiyo0721.shiroha.functions.bars.impl;
|
||||
|
||||
import ca.spottedleaf.common.time.TickData;
|
||||
import io.papermc.paper.threadedregions.ThreadedRegionizer;
|
||||
import io.papermc.paper.threadedregions.TickRegionScheduler;
|
||||
import io.papermc.paper.threadedregions.TickRegions;
|
||||
import io.nanachiyo0721.shiroha.config.modules.function.TpsBarConfig;
|
||||
import io.nanachiyo0721.shiroha.enums.EnumStatusBarDisplay;
|
||||
import io.nanachiyo0721.shiroha.functions.bars.TickableStatusBar;
|
||||
import net.kyori.adventure.bossbar.BossBar;
|
||||
import net.kyori.adventure.text.Component;
|
||||
import net.kyori.adventure.text.minimessage.MiniMessage;
|
||||
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.bukkit.craftbukkit.entity.CraftPlayer;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
import org.jetbrains.annotations.UnmodifiableView;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
public class TpsBar extends TickableStatusBar {
|
||||
public TpsBar(Player player) {
|
||||
super(player);
|
||||
}
|
||||
|
||||
public static @NonNull @UnmodifiableView Map<String, Object> buildSettings() {
|
||||
final HashMap<String, Object> ret = new HashMap<>();
|
||||
|
||||
ret.put(TickableStatusBar.SETTING_KEY_ENABLED, TpsBarConfig.tpsbarEnabled);
|
||||
ret.put(TickableStatusBar.SETTING_DISPLAY, TpsBarConfig.display);
|
||||
ret.put(TickableStatusBar.SETTING_KEY_UPDATE_INTERVALS, TpsBarConfig.updateInterval);
|
||||
|
||||
return Collections.unmodifiableMap(ret);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDisplay(@Nullable BossBar bar, @NotNull Player owner) {
|
||||
final EnumStatusBarDisplay display = this.getDisplay();
|
||||
final CraftPlayer apiOwner = (CraftPlayer) owner.getBukkitEntity();
|
||||
|
||||
final ThreadedRegionizer.ThreadedRegion<TickRegions.TickRegionData, TickRegions.TickRegionSectionData> region = TickRegionScheduler.getCurrentRegion();
|
||||
final TickData.TickReportData reportData = region.getData().getRegionSchedulingHandle().getTickReport5s(System.nanoTime());
|
||||
final TickData.SegmentData tpsData = reportData.tpsData().segmentAll();
|
||||
|
||||
final double tps = tpsData.average();
|
||||
final double mspt = reportData.timePerTickData().segmentAll().average() / 1.0E6;
|
||||
|
||||
final Component message = MiniMessage.miniMessage().deserialize(
|
||||
TpsBarConfig.tpsBarFormat,
|
||||
Placeholder.component("tps", getTpsComponent(tps)),
|
||||
Placeholder.component("mspt", getMsptComponent(mspt)),
|
||||
Placeholder.component("ping", getPingComponent(apiOwner.getPing())),
|
||||
Placeholder.component("chunkhot", getChunkHotComponent(apiOwner.getNearbyChunkHot()))
|
||||
);
|
||||
|
||||
switch (display) {
|
||||
case ACTION_BAR -> apiOwner.sendActionBar(message);
|
||||
|
||||
case BOSS_BAR ->
|
||||
bar.name(message).color(barColorForTps(tps)).progress((float) Math.clamp(mspt / 50, 0, (float) 1));
|
||||
|
||||
case TAB_LIST -> apiOwner.sendPlayerListFooter(message);
|
||||
|
||||
default -> throw new IllegalStateException();
|
||||
}
|
||||
}
|
||||
|
||||
private static @NotNull Component getPingComponent(int ping) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForPing(ping), Placeholder.parsed("text", String.valueOf(ping)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getMsptComponent(double mspt) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForMspt(mspt), Placeholder.parsed("text", String.format("%." + TpsBarConfig.precisionOfMSPT + "f", mspt)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getChunkHotComponent(long chunkHot) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForChunkHot(chunkHot), Placeholder.parsed("text", String.valueOf(chunkHot)));
|
||||
}
|
||||
|
||||
private static @NotNull Component getTpsComponent(double tps) {
|
||||
return MiniMessage.miniMessage().deserialize(textPlaceholderForTps(tps), Placeholder.parsed("text", String.format("%." + TpsBarConfig.precisionOfTPS + "f", tps)));
|
||||
}
|
||||
|
||||
private static String textPlaceholderForPing(int ping) {
|
||||
if (ping == -1) {
|
||||
return TpsBarConfig.pingColors.get(3);
|
||||
}
|
||||
|
||||
if (ping <= 80) {
|
||||
return TpsBarConfig.pingColors.get(0);
|
||||
}
|
||||
|
||||
if (ping <= 160) {
|
||||
return TpsBarConfig.pingColors.get(1);
|
||||
}
|
||||
|
||||
return TpsBarConfig.pingColors.get(2);
|
||||
}
|
||||
|
||||
private static String textPlaceholderForChunkHot(long chunkHot) {
|
||||
if (chunkHot == -1) {
|
||||
return TpsBarConfig.chunkHotColors.get(3);
|
||||
}
|
||||
|
||||
if (chunkHot <= 300000L) {
|
||||
return TpsBarConfig.chunkHotColors.get(0);
|
||||
}
|
||||
|
||||
if (chunkHot <= 500000L) {
|
||||
return TpsBarConfig.chunkHotColors.get(1);
|
||||
}
|
||||
|
||||
return TpsBarConfig.chunkHotColors.get(2);
|
||||
}
|
||||
|
||||
private static String textPlaceholderForMspt(double mspt) {
|
||||
if (mspt == -1) {
|
||||
return TpsBarConfig.tpsColors.get(3);
|
||||
}
|
||||
|
||||
if (mspt <= 25) {
|
||||
return TpsBarConfig.tpsColors.get(0);
|
||||
}
|
||||
|
||||
if (mspt <= 50) {
|
||||
return TpsBarConfig.tpsColors.get(1);
|
||||
}
|
||||
|
||||
return TpsBarConfig.tpsColors.get(2);
|
||||
}
|
||||
|
||||
private static BossBar.Color barColorForTps(double tps) {
|
||||
if (tps == -1) {
|
||||
return TpsBarConfig.barColors.get(3);
|
||||
}
|
||||
|
||||
if (tps >= 19) {
|
||||
return TpsBarConfig.barColors.get(0);
|
||||
}
|
||||
|
||||
if (tps >= 15) {
|
||||
return TpsBarConfig.barColors.get(1);
|
||||
}
|
||||
|
||||
return TpsBarConfig.barColors.get(2);
|
||||
}
|
||||
|
||||
private static String textPlaceholderForTps(double tps) {
|
||||
if (tps == -1) {
|
||||
return TpsBarConfig.tpsColors.get(3);
|
||||
}
|
||||
|
||||
if (tps >= 19) {
|
||||
return TpsBarConfig.tpsColors.get(0);
|
||||
}
|
||||
|
||||
if (tps >= 15) {
|
||||
return TpsBarConfig.tpsColors.get(1);
|
||||
}
|
||||
|
||||
return TpsBarConfig.tpsColors.get(2);
|
||||
}
|
||||
}
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import net.openhft.affinity.Affinity;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.BitSet;
|
||||
|
||||
public class AffinityRunnableWrapper {
|
||||
private final BitSet affinity;
|
||||
private final String name;
|
||||
|
||||
public AffinityRunnableWrapper(String name, BitSet affinity) {
|
||||
this.name = name;
|
||||
this.affinity = affinity;
|
||||
}
|
||||
|
||||
public Runnable wrap(Runnable original) {
|
||||
return () -> {
|
||||
Affinity.setAffinity(this.affinity);
|
||||
|
||||
original.run();
|
||||
};
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
@NotNull
|
||||
public BitSet getAffinity() {
|
||||
return this.affinity;
|
||||
}
|
||||
}
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import com.google.common.util.concurrent.ThreadFactoryBuilder;
|
||||
import com.mojang.logging.LogUtils;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
|
||||
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
|
||||
import io.nanachiyo0721.shiroha.data.BufferedLinearRegionFile;
|
||||
import org.apache.commons.lang3.Validate;
|
||||
import org.slf4j.Logger;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import java.util.concurrent.*;
|
||||
|
||||
public class BufferedLinearRegionFileFlusher implements Runnable {
|
||||
private static final Logger logger = LogUtils.getLogger();
|
||||
|
||||
private final Set<BufferedLinearRegionFile> inManagement = new ObjectArraySet<>();
|
||||
private final ScheduledFuture<?> flusherChecker;
|
||||
private final Executor ioWorkerPool;
|
||||
private final long flushOfWriteTimeoutMs;
|
||||
|
||||
public BufferedLinearRegionFileFlusher(int nIoThreads, long checkIntervalMs, long flushOfWriteTimeoutMs) {
|
||||
Validate.isTrue(nIoThreads > 0, "Number of I/O threads must > 0!");
|
||||
Validate.isTrue(checkIntervalMs > 0, "Check interval must > 0");
|
||||
Validate.isTrue(flushOfWriteTimeoutMs > 0, "Flush of write timeout must > 0");
|
||||
|
||||
this.ioWorkerPool = Executors.newFixedThreadPool(nIoThreads, new ThreadFactoryBuilder()
|
||||
.setNameFormat("BufferedLinearRegionFile I/O Worker %d")
|
||||
.setDaemon(true)
|
||||
.build()
|
||||
);
|
||||
this.flusherChecker = Executors.newSingleThreadScheduledExecutor(new ThreadFactoryBuilder()
|
||||
.setNameFormat("BufferedLinearRegionFile Flusher Checker")
|
||||
.setDaemon(true)
|
||||
.build())
|
||||
.scheduleWithFixedDelay(this, checkIntervalMs, checkIntervalMs, TimeUnit.MILLISECONDS);
|
||||
this.flushOfWriteTimeoutMs = flushOfWriteTimeoutMs;
|
||||
}
|
||||
|
||||
public void shutdown() {
|
||||
this.flusherChecker.cancel(false);
|
||||
|
||||
((ExecutorService) this.ioWorkerPool).shutdown();
|
||||
for (; ; ) {
|
||||
try {
|
||||
if (((ExecutorService) this.ioWorkerPool).awaitTermination(100, TimeUnit.MILLISECONDS)) {
|
||||
break;
|
||||
}
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
final long currentNanos = System.nanoTime();
|
||||
final BufferedLinearRegionFile[] copied;
|
||||
|
||||
synchronized (this) {
|
||||
copied = this.inManagement.toArray(new BufferedLinearRegionFile[0]);
|
||||
}
|
||||
|
||||
final List<BufferedLinearRegionFile> toRemove = new ObjectArrayList<>();
|
||||
for (BufferedLinearRegionFile file : copied) {
|
||||
// try acquiring the read lock
|
||||
if (!file.softReadLock()) {
|
||||
// if the read lock is unacquirable, it might mean there is another operations is processing(might be a writing operation)
|
||||
continue;
|
||||
}
|
||||
|
||||
boolean closed;
|
||||
|
||||
try {
|
||||
// check if the file is closed
|
||||
closed = file.isClosedRaw();
|
||||
} finally {
|
||||
file.releaseReadLock();
|
||||
}
|
||||
|
||||
if (closed) {
|
||||
// add to pending remove list so that we could clean the closed file correctly
|
||||
toRemove.add(file);
|
||||
continue;
|
||||
}
|
||||
|
||||
// skip non sync-required files
|
||||
if (!file.shouldSync()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
final long lastWriteNanos = file.getLastWritten();
|
||||
final long timeElapsed = (currentNanos - lastWriteNanos) / 1_000_000; // Convert to milliseconds
|
||||
|
||||
// if deadline(timeout) reached
|
||||
if (timeElapsed >= this.flushOfWriteTimeoutMs) {
|
||||
// already marked to flush
|
||||
if (!file.markAsBeingSynced()) {
|
||||
continue;
|
||||
}
|
||||
|
||||
this.ioWorkerPool.execute(() -> {
|
||||
try {
|
||||
file.syncIfNeeded();
|
||||
} catch (IOException e) {
|
||||
logger.error("Failed to sync master file: ", e);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
synchronized (this) {
|
||||
// clean closed files
|
||||
for (BufferedLinearRegionFile file : toRemove) {
|
||||
this.inManagement.remove(file);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void removeFile(BufferedLinearRegionFile fileToRemove) {
|
||||
synchronized (this) {
|
||||
this.inManagement.remove(fileToRemove);
|
||||
}
|
||||
}
|
||||
|
||||
public void addFile(BufferedLinearRegionFile fileToAdd) {
|
||||
synchronized (this) {
|
||||
this.inManagement.add(fileToAdd);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
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.util.Collection;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashSet;
|
||||
import java.util.Set;
|
||||
import java.util.jar.JarEntry;
|
||||
import java.util.jar.JarFile;
|
||||
|
||||
public class ClassLoadUtil {
|
||||
public static @NotNull Collection<Class<?>> getClasses(String pack, ClassLoader loader) {
|
||||
Set<Class<?>> classes = new HashSet<>();
|
||||
String packageDirName = pack.replace('.', '/');
|
||||
Enumeration<URL> dirs;
|
||||
|
||||
try {
|
||||
dirs = loader.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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,152 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import org.bukkit.Server;
|
||||
import org.bukkit.command.Command;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.bukkit.configuration.file.FileConfiguration;
|
||||
import org.bukkit.generator.BiomeProvider;
|
||||
import org.bukkit.generator.ChunkGenerator;
|
||||
import org.bukkit.plugin.PluginBase;
|
||||
import org.bukkit.plugin.PluginDescriptionFile;
|
||||
import org.bukkit.plugin.PluginLoader;
|
||||
import org.bukkit.plugin.PluginLogger;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.InputStream;
|
||||
import java.util.List;
|
||||
|
||||
public class NullPlugin extends PluginBase {
|
||||
private final String pluginName;
|
||||
private boolean enabled = true;
|
||||
private PluginDescriptionFile pdf;
|
||||
|
||||
public NullPlugin() {
|
||||
this.pluginName = "Minecraft";
|
||||
pdf = new PluginDescriptionFile(pluginName, "1.0", "nms");
|
||||
}
|
||||
|
||||
@Override
|
||||
public File getDataFolder() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginDescriptionFile getDescription() {
|
||||
return pdf;
|
||||
}
|
||||
|
||||
// Paper start
|
||||
@Override
|
||||
public io.papermc.paper.plugin.configuration.PluginMeta getPluginMeta() {
|
||||
return pdf;
|
||||
}
|
||||
|
||||
@Override
|
||||
public FileConfiguration getConfig() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
// Paper end
|
||||
|
||||
@Override
|
||||
public InputStream getResource(String filename) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveConfig() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveDefaultConfig() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void saveResource(String resourcePath, boolean replace) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reloadConfig() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginLogger getLogger() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public PluginLoader getPluginLoader() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Server getServer() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDisable() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoad() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onEnable() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isNaggable() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNaggable(boolean canNag) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ChunkGenerator getDefaultWorldGenerator(String worldName, String id) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public @Nullable BiomeProvider getDefaultBiomeProvider(@NotNull String worldName, @Nullable String id) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCommand(CommandSender sender, Command command, String label, String[] args) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> onTabComplete(CommandSender sender, Command command, String alias, String[] args) {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
|
||||
// Paper start - lifecycle events
|
||||
@Override
|
||||
public @NotNull io.papermc.paper.plugin.lifecycle.event.LifecycleEventManager<org.bukkit.plugin.Plugin> getLifecycleManager() {
|
||||
throw new UnsupportedOperationException("Not supported.");
|
||||
}
|
||||
// Paper end - lifecycle events
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
public class RateThrottler {
|
||||
private static final byte STATE_NOT_BEGIN = 0;
|
||||
private static final byte STATE_RECORDING = 1;
|
||||
private static final byte STATE_DESTROYED = 2;
|
||||
|
||||
private byte status;
|
||||
private int recordedThisTick = 0;
|
||||
|
||||
private int totallyTicked = 0;
|
||||
private int totalCount = 0;
|
||||
|
||||
private void checkDestroyed() {
|
||||
if (this.status == STATE_DESTROYED) {
|
||||
throw new IllegalStateException("Already destroyed!");
|
||||
}
|
||||
}
|
||||
|
||||
public void increase() {
|
||||
this.recordedThisTick++;
|
||||
}
|
||||
|
||||
public void mergeWith(@NotNull RateThrottler other) {
|
||||
this.checkDestroyed();
|
||||
other.checkDestroyed();
|
||||
|
||||
this.totallyTicked += other.totallyTicked;
|
||||
this.totalCount += other.totalCount;
|
||||
}
|
||||
|
||||
public void splitInto(@NotNull RateThrottler other) {
|
||||
this.checkDestroyed();
|
||||
other.checkDestroyed();
|
||||
|
||||
other.totalCount = this.totalCount;
|
||||
other.totallyTicked = this.totallyTicked;
|
||||
}
|
||||
|
||||
public double getAvgCount() {
|
||||
return (double) this.totalCount / Math.min(this.totallyTicked, 1);
|
||||
}
|
||||
|
||||
public int getCountThisTick() {
|
||||
return this.recordedThisTick;
|
||||
}
|
||||
|
||||
public boolean isOutOfRate(int expected) {
|
||||
return this.recordedThisTick >= expected;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (this.status == STATE_DESTROYED) {
|
||||
throw new IllegalStateException("Already destroyed!");
|
||||
}
|
||||
|
||||
this.status = STATE_DESTROYED;
|
||||
}
|
||||
|
||||
public void begin() {
|
||||
this.checkDestroyed();
|
||||
|
||||
if (this.status == STATE_RECORDING) {
|
||||
throw new IllegalStateException("Attempt to begin a already recording throttler!");
|
||||
}
|
||||
|
||||
this.status = STATE_RECORDING;
|
||||
}
|
||||
|
||||
public void done() {
|
||||
this.checkDestroyed();
|
||||
|
||||
if (this.status == STATE_NOT_BEGIN) {
|
||||
throw new IllegalStateException("Attempt to done a already done or new throttler!");
|
||||
|
||||
}
|
||||
|
||||
this.status = STATE_NOT_BEGIN;
|
||||
|
||||
this.totallyTicked++;
|
||||
this.totalCount += this.recordedThisTick;
|
||||
|
||||
this.recordedThisTick = 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import net.minecraft.world.level.chunk.storage.RegionStorageInfo;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
public record RegionCreatorInfo(RegionStorageInfo info, Path filePath, Path folder, boolean sync) {
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
package io.nanachiyo0721.shiroha.utils;
|
||||
|
||||
import io.nanachiyo0721.shiroha.data.RegionFile;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
@FunctionalInterface
|
||||
public interface RegionFileFactory {
|
||||
RegionFile newFile(RegionCreatorInfo info) throws IOException;
|
||||
}
|
||||
+117
@@ -0,0 +1,117 @@
|
||||
package io.nanachiyo0721.shiroha.utils.dialog;
|
||||
|
||||
import com.google.common.reflect.TypeToken;
|
||||
import com.google.gson.Gson;
|
||||
import io.nanachiyo0721.shiroha.config.ConfigsInstance;
|
||||
import net.kyori.adventure.text.format.TextColor;
|
||||
import net.minecraft.commands.functions.StringTemplate;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.dialog.action.CommandTemplate;
|
||||
import net.minecraft.server.dialog.action.ParsedTemplate;
|
||||
import net.minecraft.world.entity.player.Player;
|
||||
import org.bukkit.command.CommandSender;
|
||||
|
||||
import java.lang.reflect.Type;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
|
||||
public class ConfigCommandDialog {
|
||||
public static void openGui(Player player, String name, ConfigsInstance config) {
|
||||
openGui(player, name, config, "");
|
||||
}
|
||||
|
||||
public static void openGui(Player player, String name, ConfigsInstance config, String[] args) {
|
||||
openGui(player, name, config, args.length == 1 ? "" : args[1]);
|
||||
}
|
||||
|
||||
public static void openGui(Player player, String name, ConfigsInstance config, String prefix) {
|
||||
if (prefix.equals("full")) {
|
||||
player.openDialog(
|
||||
ConfigDialogUtil.createHolder(
|
||||
name,
|
||||
config.getAllDataFull(),
|
||||
name + " submit "
|
||||
));
|
||||
return;
|
||||
}
|
||||
|
||||
// Get all possible paths at current level
|
||||
List<String> keyList = config.completeConfigPath(prefix.isEmpty() ? prefix : prefix + ".");
|
||||
List<String> keySingleConfigs = config.getSingleConfig(prefix);
|
||||
keyList.removeAll(keySingleConfigs);
|
||||
DialogUtil.DialogBuilder builder = new DialogUtil.DialogBuilder();
|
||||
|
||||
// Add navigation buttons for each sub-path
|
||||
for (String key : keyList) {
|
||||
// Check if this key has children or is a valid config node
|
||||
List<String> childPaths = config.completeConfigPath(key + ".");
|
||||
List<String> childKeySingleConfigs = config.getSingleConfig(key);
|
||||
|
||||
// Always create button if there are child paths or if it's a valid config node
|
||||
if (!childPaths.isEmpty() || !childKeySingleConfigs.isEmpty()) {
|
||||
String raw = name + " open-gui " + key + "$(missing)";
|
||||
StringTemplate template = StringTemplate.fromString(raw);
|
||||
CommandTemplate commandTemplate = new CommandTemplate(new ParsedTemplate(raw, template));
|
||||
builder.addButton(
|
||||
DialogUtil.createButton(
|
||||
Component.translatable(key),
|
||||
300,
|
||||
Optional.of(commandTemplate)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
ConfigDialogUtil.addInputs(
|
||||
config.getDataFull(keySingleConfigs),
|
||||
name + " submit ",
|
||||
builder
|
||||
);
|
||||
|
||||
// Add "Show all configs" button at root level
|
||||
if (prefix.isEmpty()) {
|
||||
String raw = name + " open-gui full$(missing)";
|
||||
StringTemplate template = StringTemplate.fromString(raw);
|
||||
CommandTemplate commandTemplate = new CommandTemplate(new ParsedTemplate(raw, template));
|
||||
builder.addButton(
|
||||
DialogUtil.createButton(
|
||||
Component.translatable("Show all configs"),
|
||||
300,
|
||||
Optional.of(commandTemplate)
|
||||
));
|
||||
}
|
||||
|
||||
if (builder.getInputCount() == 0) {
|
||||
builder.addButton(
|
||||
DialogUtil.createButton(
|
||||
Component.translatable("Close"),
|
||||
300,
|
||||
Optional.empty()
|
||||
));
|
||||
}
|
||||
|
||||
builder.setTitle(name)
|
||||
.setPause(false)
|
||||
.setColumns(1);
|
||||
player.openDialog(
|
||||
DialogUtil.transformToHolder(
|
||||
builder.build()
|
||||
));
|
||||
}
|
||||
|
||||
public static void processSubmit(CommandSender sender, ConfigsInstance config, String[] args) {
|
||||
String fullText = String.join(" ", args);
|
||||
Gson gson = new Gson();
|
||||
Type type = new TypeToken<Map<String, String>>() {
|
||||
}.getType();
|
||||
Map<String, String> map = gson.fromJson(fullText, type);
|
||||
for (Map.Entry<String, String> entry : map.entrySet()) {
|
||||
config.setConfig(entry.getKey(), entry.getValue());
|
||||
}
|
||||
config.reloadAsync(true).thenAccept(nullValue -> sender.sendMessage(
|
||||
net.kyori.adventure.text.Component
|
||||
.text("Apply config update successfully!")
|
||||
.color(TextColor.color(0, 255, 0))
|
||||
));
|
||||
}
|
||||
}
|
||||
+55
@@ -0,0 +1,55 @@
|
||||
package io.nanachiyo0721.shiroha.utils.dialog;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import io.nanachiyo0721.shiroha.api.config.ConfigDataPair;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.server.dialog.Dialog;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jspecify.annotations.NonNull;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
|
||||
public class ConfigDialogUtil {
|
||||
public static Holder<Dialog> createHolder(String title, Set<ConfigDataPair> configs, String commandPrefix) {
|
||||
return DialogUtil.createHolder(title, generateConfigMap(configs), commandPrefix);
|
||||
}
|
||||
|
||||
public static DialogUtil.DialogBuilder addInputs(Set<ConfigDataPair> configs, String commandPrefix, @NotNull DialogUtil.DialogBuilder builder) {
|
||||
return DialogUtil.addInputs(generateConfigMap(configs), commandPrefix, builder);
|
||||
}
|
||||
|
||||
private static @NonNull Map<String, Pair<Object, String>> generateConfigMap(Set<ConfigDataPair> configs) {
|
||||
Map<String, Pair<Object, String>> map = new TreeMap<>();
|
||||
for (ConfigDataPair config : configs) {
|
||||
String key = config.key();
|
||||
Object value = config.value();
|
||||
String[] suggestions = config.suggestions();
|
||||
String comment = config.comment();
|
||||
String addition1 = "";
|
||||
if (comment != null && !comment.isEmpty()) {
|
||||
addition1 = "Comments: " + comment;
|
||||
}
|
||||
|
||||
String addition2 = "";
|
||||
|
||||
if (suggestions != null && suggestions.length > 0) {
|
||||
StringBuilder addition = new StringBuilder("Suggestions: ");
|
||||
boolean first = true;
|
||||
for (String suggestion : suggestions) {
|
||||
if (!first) {
|
||||
addition.append(", ");
|
||||
} else {
|
||||
first = false;
|
||||
}
|
||||
addition.append(suggestion);
|
||||
}
|
||||
addition2 = addition.toString();
|
||||
}
|
||||
String addition = addition1.isEmpty() ? addition2 : addition2.isEmpty() ? addition1 : addition1 + "\n" + addition2;
|
||||
map.put(key, Pair.of(value, addition));
|
||||
}
|
||||
return map;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,472 @@
|
||||
package io.nanachiyo0721.shiroha.utils.dialog;
|
||||
|
||||
import com.mojang.datafixers.util.Pair;
|
||||
import net.minecraft.commands.functions.StringTemplate;
|
||||
import net.minecraft.core.Holder;
|
||||
import net.minecraft.network.chat.Component;
|
||||
import net.minecraft.server.dialog.*;
|
||||
import net.minecraft.server.dialog.action.Action;
|
||||
import net.minecraft.server.dialog.action.CommandTemplate;
|
||||
import net.minecraft.server.dialog.action.ParsedTemplate;
|
||||
import net.minecraft.server.dialog.body.DialogBody;
|
||||
import net.minecraft.server.dialog.input.BooleanInput;
|
||||
import net.minecraft.server.dialog.input.NumberRangeInput;
|
||||
import net.minecraft.server.dialog.input.TextInput;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.json.simple.JSONObject;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class DialogUtil {
|
||||
public static Holder<Dialog> createHolder(String title, Map<String, Pair<Object, String>> map, String commandPrefix) {
|
||||
return transformToHolder(
|
||||
createDialog(title, map, commandPrefix)
|
||||
);
|
||||
}
|
||||
|
||||
public static Holder<Dialog> createHolder(String title, List<String> list) {
|
||||
return transformToHolder(
|
||||
createDialog(title, list)
|
||||
);
|
||||
}
|
||||
|
||||
public static Holder<Dialog> transformToHolder(Dialog dialog) {
|
||||
return Holder.direct(dialog);
|
||||
}
|
||||
|
||||
public static MultiActionDialog createDialog(String title, List<String> options) {
|
||||
DialogBuilder builder = new DialogBuilder();
|
||||
for (String option : options) {
|
||||
builder.addButton(
|
||||
createButton(
|
||||
Component.translatable(option),
|
||||
300,
|
||||
Optional.empty()
|
||||
));
|
||||
}
|
||||
|
||||
builder.setTitle(title)
|
||||
.setPause(false)
|
||||
.setColumns(1);
|
||||
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
public static MultiActionDialog createDialog(String title, Map<String, Pair<Object, String>> map, String commandPrefix) {
|
||||
return addInputs(map, commandPrefix, new DialogBuilder())
|
||||
.setTitle(title)
|
||||
.setPause(false)
|
||||
.setColumns(1)
|
||||
.build();
|
||||
}
|
||||
|
||||
public static DialogBuilder addInputs(Map<String, Pair<Object, String>> map, String commandPrefix, @NotNull DialogBuilder builder) {
|
||||
boolean hasInput = false;
|
||||
JSONObject valueBuilder = new JSONObject();
|
||||
Set<String> usedKeys = new HashSet<>();
|
||||
int keyCounter = 0;
|
||||
|
||||
for (Map.Entry<String, Pair<Object, String>> entry : map.entrySet()) {
|
||||
Object value = entry.getValue().getFirst();
|
||||
String label = entry.getKey();
|
||||
String key = sanitizeKey(label);
|
||||
String comment = entry.getValue().getSecond();
|
||||
|
||||
String originalKey = key;
|
||||
while (usedKeys.contains(key)) {
|
||||
key = originalKey + "_" + (++keyCounter);
|
||||
}
|
||||
usedKeys.add(key);
|
||||
|
||||
valueBuilder.put(label, "$(" + key + ")");
|
||||
|
||||
if (comment != null && !comment.isEmpty()) {
|
||||
String addition = "Any edit in this text input will not save to file.\n" + comment;
|
||||
String _label = "Additional information of " + label;
|
||||
String _key = sanitizeKey(_label);
|
||||
String _originalKey = _key;
|
||||
while (usedKeys.contains(_key)) {
|
||||
_key = _originalKey + "_" + (++keyCounter);
|
||||
}
|
||||
usedKeys.add(_key);
|
||||
builder.addInput(
|
||||
createTextInput(
|
||||
_label,
|
||||
_key,
|
||||
addition,
|
||||
300,
|
||||
true,
|
||||
2147483647,
|
||||
new TextInput.MultilineOptions(
|
||||
Optional.of(1000),
|
||||
Optional.of(
|
||||
(int) (20 * (addition.lines().count() + 1)
|
||||
)
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
switch (value) {
|
||||
case Boolean boolValue -> {
|
||||
Input checkbox = createCheckbox(label, key, boolValue, "true", "false");
|
||||
builder.addInput(checkbox);
|
||||
}
|
||||
case String stringValue -> {
|
||||
Input textbox = createTextInput(label, key, stringValue, 300, true, 2147483647, null);
|
||||
builder.addInput(textbox);
|
||||
}
|
||||
case Number numberValue -> {
|
||||
Input numberInput = createTextInput(label, key, numberValue.toString(), 300, true, 2147483647, null);
|
||||
builder.addInput(numberInput);
|
||||
}
|
||||
default -> {
|
||||
}
|
||||
}
|
||||
|
||||
hasInput = true;
|
||||
}
|
||||
String raw = commandPrefix + valueBuilder.toJSONString() + "$(missing)";
|
||||
StringTemplate template = StringTemplate.fromString(raw);
|
||||
CommandTemplate confirmTemplate = new CommandTemplate(new ParsedTemplate(raw, template));
|
||||
if (hasInput) {
|
||||
builder.addButton(createButton(
|
||||
Component.translatable("Confirm"),
|
||||
300,
|
||||
Optional.of(confirmTemplate)
|
||||
))
|
||||
.addButton(createButton(
|
||||
Component.translatable("Cancel"),
|
||||
300,
|
||||
Optional.empty()
|
||||
));
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
public static Input createCheckbox(String label, String key, boolean value, String trueText, String falseText) {
|
||||
return new Input(key, new BooleanInput(
|
||||
Component.translatable(label),
|
||||
value,
|
||||
trueText,
|
||||
falseText
|
||||
));
|
||||
}
|
||||
|
||||
public static Input createTextInput(String label, String key, String value, int width, boolean labelVisible, int maxLength, TextInput.MultilineOptions multilineOptions) {
|
||||
return new Input(key, new TextInput(
|
||||
width,
|
||||
Component.translatable(label),
|
||||
labelVisible,
|
||||
value,
|
||||
maxLength,
|
||||
Optional.ofNullable(multilineOptions)
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: number input is not work now
|
||||
public static Input createNumberInput(String label, String key, Number value, NumberRangeInput.RangeInfo rangeInfo) {
|
||||
return new Input(key, new NumberRangeInput(
|
||||
300,
|
||||
Component.translatable(label),
|
||||
value.getClass().getName(),
|
||||
rangeInfo
|
||||
));
|
||||
}
|
||||
|
||||
public static ActionButton createButton(Component key, int width, Optional<Action> action) {
|
||||
CommonButtonData buttonData = new CommonButtonData(
|
||||
key,
|
||||
width
|
||||
);
|
||||
return new ActionButton(buttonData, action);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the display width of a string (Chinese characters have width 2, English characters have width 1)
|
||||
*
|
||||
* @param text The text to calculate width for
|
||||
* @return The display width
|
||||
*/
|
||||
public static int getTextDisplayWidth(String text) {
|
||||
int width = 0;
|
||||
for (char c : text.toCharArray()) {
|
||||
// Chinese character range
|
||||
if (c >= '\u4e00' && c <= '\u9fff') {
|
||||
width += 2;
|
||||
} else {
|
||||
width += 1;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Split text into words, preserving spaces and line breaks
|
||||
*
|
||||
* @param text The text to split
|
||||
* @return List of words with their separators
|
||||
*/
|
||||
private static List<String> splitIntoWords(String text) {
|
||||
List<String> words = new ArrayList<>();
|
||||
StringBuilder currentWord = new StringBuilder();
|
||||
|
||||
for (int i = 0; i < text.length(); i++) {
|
||||
char c = text.charAt(i);
|
||||
|
||||
if (Character.isWhitespace(c)) {
|
||||
// If we have accumulated a word, add it to list
|
||||
if (!currentWord.isEmpty()) {
|
||||
words.add(currentWord.toString());
|
||||
currentWord = new StringBuilder();
|
||||
}
|
||||
// Add whitespace as separate "word"
|
||||
words.add(String.valueOf(c));
|
||||
} else {
|
||||
// Accumulate non-whitespace characters
|
||||
currentWord.append(c);
|
||||
}
|
||||
}
|
||||
|
||||
// Add the last word if exists
|
||||
if (!currentWord.isEmpty()) {
|
||||
words.add(currentWord.toString());
|
||||
}
|
||||
|
||||
return words;
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically wrap text by words based on display width
|
||||
*
|
||||
* @param text Original text
|
||||
* @param maxWidth Maximum display width per line
|
||||
* @return Processed text with appropriate line breaks
|
||||
*/
|
||||
public static String wrapTextByWordsAndWidth(String text, int maxWidth) {
|
||||
// If text is null or width is within limit, return as is
|
||||
if (text == null || getTextDisplayWidth(text) <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
List<String> words = splitIntoWords(text);
|
||||
StringBuilder wrappedText = new StringBuilder();
|
||||
StringBuilder currentLine = new StringBuilder();
|
||||
int currentWidth = 0;
|
||||
|
||||
for (String word : words) {
|
||||
int wordWidth = getTextDisplayWidth(word);
|
||||
|
||||
// Handle explicit line breaks
|
||||
if (word.contains("\n")) {
|
||||
// Add current line content
|
||||
wrappedText.append(currentLine);
|
||||
// Add the word containing newline
|
||||
wrappedText.append(word);
|
||||
// Reset for next line
|
||||
currentLine = new StringBuilder();
|
||||
currentWidth = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle whitespace characters
|
||||
if (word.matches("\\s+")) {
|
||||
// If adding space would exceed limit, wrap to next line
|
||||
if (currentWidth + wordWidth > maxWidth && !currentLine.toString().trim().isEmpty()) {
|
||||
wrappedText.append(currentLine.toString().trim()).append("\n");
|
||||
currentLine = new StringBuilder();
|
||||
currentWidth = 0;
|
||||
// Only add space if it's not leading whitespace on new line
|
||||
if (!word.equals(" ")) {
|
||||
currentLine.append(word);
|
||||
currentWidth = wordWidth;
|
||||
}
|
||||
} else {
|
||||
currentLine.append(word);
|
||||
currentWidth += wordWidth;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Handle regular words
|
||||
// If adding word would exceed limit, wrap to next line
|
||||
if (currentWidth + wordWidth > maxWidth && !currentLine.toString().trim().isEmpty()) {
|
||||
wrappedText.append(currentLine.toString().trim()).append("\n");
|
||||
currentLine = new StringBuilder();
|
||||
currentWidth = 0;
|
||||
}
|
||||
|
||||
currentLine.append(word);
|
||||
currentWidth += wordWidth;
|
||||
}
|
||||
|
||||
// Append the last line if not empty
|
||||
if (!currentLine.toString().trim().isEmpty()) {
|
||||
wrappedText.append(currentLine.toString().trim());
|
||||
}
|
||||
|
||||
return wrappedText.toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* Automatically wrap text based on display width
|
||||
*
|
||||
* @param text Original text
|
||||
* @param maxWidth Maximum display width
|
||||
* @return Processed text with line breaks
|
||||
*/
|
||||
public static String wrapTextByDisplayWidth(String text, int maxWidth) {
|
||||
// If text is null or width is within limit, return as is
|
||||
if (text == null || getTextDisplayWidth(text) <= maxWidth) {
|
||||
return text;
|
||||
}
|
||||
|
||||
StringBuilder wrappedText = new StringBuilder();
|
||||
StringBuilder currentLine = new StringBuilder();
|
||||
int currentWidth = 0;
|
||||
|
||||
for (char c : text.toCharArray()) {
|
||||
// Determine character width (2 for Chinese, 1 for others)
|
||||
int charWidth = (c >= '\u4e00' && c <= '\u9fff') ? 2 : 1;
|
||||
|
||||
if (currentWidth + charWidth > maxWidth && !currentLine.isEmpty()) {
|
||||
wrappedText.append(currentLine.toString().trim()).append("\n");
|
||||
currentLine = new StringBuilder();
|
||||
currentWidth = 0;
|
||||
}
|
||||
|
||||
currentLine.append(c);
|
||||
currentWidth += charWidth;
|
||||
}
|
||||
|
||||
if (!currentLine.isEmpty()) {
|
||||
wrappedText.append(currentLine.toString().trim());
|
||||
}
|
||||
|
||||
return wrappedText.toString();
|
||||
}
|
||||
|
||||
private static String sanitizeKey(String originalKey) {
|
||||
if (originalKey == null || originalKey.isEmpty()) {
|
||||
return "key_" + System.currentTimeMillis(); // generate a unique key
|
||||
}
|
||||
|
||||
StringBuilder sanitized = new StringBuilder();
|
||||
|
||||
char firstChar = originalKey.charAt(0);
|
||||
if (Character.isDigit(firstChar)) {
|
||||
sanitized.append("_").append(firstChar);
|
||||
} else if (isValidKeyChar(firstChar)) {
|
||||
sanitized.append(firstChar);
|
||||
} else {
|
||||
sanitized.append("_");
|
||||
}
|
||||
|
||||
for (int i = 1; i < originalKey.length(); i++) {
|
||||
char c = originalKey.charAt(i);
|
||||
if (isValidKeyChar(c)) {
|
||||
sanitized.append(c);
|
||||
} else {
|
||||
sanitized.append("_");
|
||||
}
|
||||
}
|
||||
|
||||
String result = sanitized.toString();
|
||||
if (result.isEmpty() || Character.isDigit(result.charAt(0))) {
|
||||
result = "key_" + result;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static boolean isValidKeyChar(char c) {
|
||||
return Character.isLetterOrDigit(c) || c == '_';
|
||||
}
|
||||
|
||||
public static class DialogBuilder {
|
||||
String title = "";
|
||||
Optional<Component> externalTitle = Optional.empty();
|
||||
boolean canCloseWithEscape = true;
|
||||
boolean pause = true;
|
||||
int actionClose = 0;
|
||||
int columns = 2;
|
||||
List<ActionButton> buttons = new ArrayList<>();
|
||||
List<Input> inputs = new ArrayList<>();
|
||||
List<DialogBody> bodies = new ArrayList<>();
|
||||
Optional<ActionButton> exitButton = Optional.empty();
|
||||
|
||||
public DialogBuilder setTitle(String title) {
|
||||
this.title = title;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder setExternalTitle(Component externalTitle) {
|
||||
this.externalTitle = Optional.of(externalTitle);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder setCanCloseWithEscape(boolean canCloseWithEscape) {
|
||||
this.canCloseWithEscape = canCloseWithEscape;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder setPause(boolean pause) {
|
||||
this.pause = pause;
|
||||
return this;
|
||||
}
|
||||
|
||||
// 0 for close, 1 for none, 2 for wait
|
||||
public DialogBuilder setActionClose(int actionClose) {
|
||||
this.actionClose = actionClose;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder setColumns(int columns) {
|
||||
this.columns = columns;
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder addButton(ActionButton button) {
|
||||
this.buttons.add(button);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getButtonCount() {
|
||||
return this.buttons.size();
|
||||
}
|
||||
|
||||
public DialogBuilder addInput(Input input) {
|
||||
this.inputs.add(input);
|
||||
return this;
|
||||
}
|
||||
|
||||
public int getInputCount() {
|
||||
return this.inputs.size();
|
||||
}
|
||||
|
||||
public DialogBuilder addBody(DialogBody body) {
|
||||
this.bodies.add(body);
|
||||
return this;
|
||||
}
|
||||
|
||||
public DialogBuilder setExitButton(ActionButton exitButton) {
|
||||
this.exitButton = Optional.of(exitButton);
|
||||
return this;
|
||||
}
|
||||
|
||||
public MultiActionDialog build() {
|
||||
CommonDialogData data = new CommonDialogData(
|
||||
Component.translatable(title),
|
||||
externalTitle,
|
||||
canCloseWithEscape,
|
||||
pause,
|
||||
DialogAction.values()[actionClose], // if paused you must do something
|
||||
bodies,
|
||||
inputs
|
||||
);
|
||||
return new MultiActionDialog(data, buttons, exitButton, columns);
|
||||
}
|
||||
}
|
||||
}
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
package io.nanachiyo0721.shiroha.utils.entity;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
import net.minecraft.world.entity.MoverType;
|
||||
import net.minecraft.world.phys.Vec3;
|
||||
|
||||
public class EntityMoveOutOfRegionException extends RuntimeException {
|
||||
private final Entity entity;
|
||||
private final Vec3 movement;
|
||||
private final MoverType moverType;
|
||||
|
||||
public EntityMoveOutOfRegionException(Entity entity, Vec3 movement, MoverType moverType) {
|
||||
this.entity = entity;
|
||||
this.movement = movement;
|
||||
this.moverType = moverType;
|
||||
}
|
||||
|
||||
|
||||
public Entity getEntity() {
|
||||
return this.entity;
|
||||
}
|
||||
|
||||
public Vec3 getMovement() {
|
||||
return this.movement;
|
||||
}
|
||||
|
||||
public MoverType getMoverType() {
|
||||
return this.moverType;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
// This is a part from Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/26.2/leaf-server/src/main/java/me/titaniumtown/ArrayConstants.java)
|
||||
// Original License: https://github.com/Winds-Studio/Leaf/blob/ver/26.2/LICENSE.md
|
||||
|
||||
// Gale - JettPack - reduce array allocations
|
||||
|
||||
package me.titaniumtown;
|
||||
|
||||
public final class ArrayConstants {
|
||||
|
||||
public static final Object[] emptyObjectArray = new Object[0];
|
||||
public static final short[] emptyShortArray = new short[0];
|
||||
public static final int[] emptyIntArray = new int[0];
|
||||
public static final int[] zeroSingletonIntArray = new int[]{0};
|
||||
public static final byte[] emptyByteArray = new byte[0];
|
||||
public static final String[] emptyStringArray = new String[0];
|
||||
public static final long[] emptyLongArray = new long[0];
|
||||
public static final org.bukkit.entity.Entity[] emptyBukkitEntityArray = new org.bukkit.entity.Entity[0];
|
||||
public static final net.minecraft.world.entity.Entity[] emptyEntityArray = new net.minecraft.world.entity.Entity[0];
|
||||
//public static final net.minecraft.server.level.ServerLevel[] emptyServerLevelArray = new net.minecraft.server.level.ServerLevel[0];
|
||||
public static final net.minecraft.tags.TagKey[] emptyTagKeyArray = new net.minecraft.tags.TagKey[0]; // Leaf - Optimize isEyeInFluid
|
||||
}
|
||||
@@ -0,0 +1,104 @@
|
||||
/**
|
||||
* This file is a part of Leaf(
|
||||
* * This file is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/1.21.11/leaf-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java))
|
||||
* Original license: https://github.com/Winds-Studio/Leaf/blob/ver/1.21.11/LICENSE.md
|
||||
*/
|
||||
package org.dreeam.leaf.util;
|
||||
|
||||
import net.minecraft.world.entity.Entity;
|
||||
|
||||
public final class FastBitRadixSort {
|
||||
|
||||
private static final int SMALL_ARRAY_THRESHOLD = 6;
|
||||
private static final long[] LONGS = new long[0];
|
||||
private long[] bitsBuffer = LONGS;
|
||||
|
||||
public void sort(Object[] entities, int size, net.minecraft.core.Position target) {
|
||||
if (size <= 1) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.bitsBuffer.length < size) {
|
||||
this.bitsBuffer = new long[size];
|
||||
}
|
||||
double tx = target.x();
|
||||
double ty = target.y();
|
||||
double tz = target.z();
|
||||
for (int i = 0; i < size; i++) {
|
||||
this.bitsBuffer[i] = Double.doubleToRawLongBits(((Entity) entities[i]).distanceToSqr(tx, ty, tz));
|
||||
}
|
||||
|
||||
fastRadixSort(entities, this.bitsBuffer, 0, size - 1, 62);
|
||||
}
|
||||
|
||||
private static void fastRadixSort(
|
||||
Object[] ents,
|
||||
long[] bits,
|
||||
int low,
|
||||
int high,
|
||||
int bit
|
||||
) {
|
||||
if (bit < 0 || low >= high) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (high - low <= SMALL_ARRAY_THRESHOLD) {
|
||||
insertionSort(ents, bits, low, high);
|
||||
return;
|
||||
}
|
||||
|
||||
int i = low;
|
||||
int j = high;
|
||||
final long mask = 1L << bit;
|
||||
|
||||
while (i <= j) {
|
||||
while (i <= j && (bits[i] & mask) == 0) {
|
||||
i++;
|
||||
}
|
||||
while (i <= j && (bits[j] & mask) != 0) {
|
||||
j--;
|
||||
}
|
||||
if (i < j) {
|
||||
swap(ents, bits, i++, j--);
|
||||
}
|
||||
}
|
||||
|
||||
if (low < j) {
|
||||
fastRadixSort(ents, bits, low, j, bit - 1);
|
||||
}
|
||||
if (i < high) {
|
||||
fastRadixSort(ents, bits, i, high, bit - 1);
|
||||
}
|
||||
}
|
||||
|
||||
private static void insertionSort(
|
||||
Object[] ents,
|
||||
long[] bits,
|
||||
int low,
|
||||
int high
|
||||
) {
|
||||
for (int i = low + 1; i <= high; i++) {
|
||||
int j = i;
|
||||
Object currentEntity = ents[j];
|
||||
long currentBits = bits[j];
|
||||
|
||||
while (j > low && bits[j - 1] > currentBits) {
|
||||
ents[j] = ents[j - 1];
|
||||
bits[j] = bits[j - 1];
|
||||
j--;
|
||||
}
|
||||
ents[j] = currentEntity;
|
||||
bits[j] = currentBits;
|
||||
}
|
||||
}
|
||||
|
||||
private static void swap(Object[] ents, long[] bits, int a, int b) {
|
||||
Object tempEntity = ents[a];
|
||||
ents[a] = ents[b];
|
||||
ents[b] = tempEntity;
|
||||
|
||||
long tempBits = bits[a];
|
||||
bits[a] = bits[b];
|
||||
bits[b] = tempBits;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* This file is a part of Leaf(https://github.com/Winds-Studio/Leaf/blob/ver/1.21.8/leaf-server/src/main/java/org/dreeam/leaf/world/biome/PositionalBiomeGetter.java)
|
||||
* Original license: https://github.com/Winds-Studio/Leaf/blob/ver/1.21.8/LICENSE.md
|
||||
*/
|
||||
package org.dreeam.leaf.world.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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.mojang.brigadier.arguments.ArgumentType;
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.RequiredArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import com.mojang.brigadier.suggestion.Suggestions;
|
||||
import com.mojang.brigadier.suggestion.SuggestionsBuilder;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.Commands;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletableFuture;
|
||||
|
||||
public abstract class ArgumentNode<T> extends CommandNode {
|
||||
protected final ArgumentType<T> argumentType;
|
||||
|
||||
protected ArgumentNode(String name, ArgumentType<T> argumentType) {
|
||||
super(name);
|
||||
this.argumentType = argumentType;
|
||||
}
|
||||
|
||||
@SuppressWarnings({"unused", "RedundantThrows"})
|
||||
protected CompletableFuture<Suggestions> getSuggestions(final CommandContext context, final SuggestionsBuilder builder) throws CommandSyntaxException {
|
||||
return Suggestions.empty();
|
||||
}
|
||||
|
||||
protected boolean overrideSuggestions() {
|
||||
return isMethodOverridden("getSuggestions", ArgumentNode.class);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ArgumentBuilder<CommandSourceStack, ?> compileBase() {
|
||||
RequiredArgumentBuilder<CommandSourceStack, T> argumentBuilder = Commands.argument(name, argumentType);
|
||||
|
||||
if (overrideSuggestions()) {
|
||||
argumentBuilder.suggests(
|
||||
(context, builder) -> getSuggestions(new CommandContext(context), builder)
|
||||
);
|
||||
}
|
||||
|
||||
return argumentBuilder;
|
||||
}
|
||||
|
||||
public static class ArgumentSuggestions {
|
||||
@Contract(pure = true)
|
||||
public static WrappedArgument.@NotNull SuggestionApplier strings(String... values) {
|
||||
return (context, builder) -> {
|
||||
for (String s : values) {
|
||||
builder.suggest(s);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Contract(pure = true)
|
||||
public static WrappedArgument.@NotNull SuggestionApplier strings(List<String> values) {
|
||||
return (context, builder) -> {
|
||||
for (String s : values) {
|
||||
builder.suggest(s);
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.mojang.brigadier.Command;
|
||||
import com.mojang.brigadier.RedirectModifier;
|
||||
import com.mojang.brigadier.context.ParsedCommandNode;
|
||||
import com.mojang.brigadier.context.StringRange;
|
||||
import com.mojang.brigadier.tree.CommandNode;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import static org.leavesmc.leaves.command.CommandNode.getNameForNode;
|
||||
|
||||
@SuppressWarnings({"ClassCanBeRecord", "unused"})
|
||||
public class CommandContext {
|
||||
private final com.mojang.brigadier.context.CommandContext<CommandSourceStack> source;
|
||||
|
||||
public CommandContext(com.mojang.brigadier.context.CommandContext<CommandSourceStack> source) {
|
||||
this.source = source;
|
||||
}
|
||||
|
||||
public com.mojang.brigadier.context.CommandContext<CommandSourceStack> getChild() {
|
||||
return source.getChild();
|
||||
}
|
||||
|
||||
public com.mojang.brigadier.context.CommandContext<CommandSourceStack> getLastChild() {
|
||||
return source.getLastChild();
|
||||
}
|
||||
|
||||
public Command<CommandSourceStack> getCommand() {
|
||||
return source.getCommand();
|
||||
}
|
||||
|
||||
public CommandSourceStack getSource() {
|
||||
return source.getSource();
|
||||
}
|
||||
|
||||
public CommandSender getSender() {
|
||||
return source.getSource().getSender();
|
||||
}
|
||||
|
||||
public <V> @NotNull V getArgument(final String name, final Class<V> clazz) {
|
||||
return source.getArgument(name, clazz);
|
||||
}
|
||||
|
||||
public int getInteger(final String name) {
|
||||
return source.getArgument(name, Integer.class);
|
||||
}
|
||||
|
||||
public boolean getBoolean(final String name) {
|
||||
return source.getArgument(name, Boolean.class);
|
||||
}
|
||||
|
||||
public String getString(final String name) {
|
||||
return source.getArgument(name, String.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public <V> @NotNull V getArgument(final Class<? extends ArgumentNode<V>> nodeClass) {
|
||||
String name = getNameForNode(nodeClass);
|
||||
return (V) source.getArgument(name, Object.class);
|
||||
}
|
||||
|
||||
public <V> V getArgumentOrDefault(final Class<? extends ArgumentNode<V>> nodeClass, final V defaultValue) {
|
||||
try {
|
||||
return getArgument(nodeClass);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public <V> V getArgumentOrDefault(final String name, final Class<V> clazz, final V defaultValue) {
|
||||
try {
|
||||
return source.getArgument(name, clazz);
|
||||
} catch (IllegalArgumentException e) {
|
||||
return defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
public String getStringOrDefault(final String name, final String defaultValue) {
|
||||
return getArgumentOrDefault(name, String.class, defaultValue);
|
||||
}
|
||||
|
||||
public int getIntegerOrDefault(final String name, final int defaultValue) {
|
||||
return getArgumentOrDefault(name, Integer.class, defaultValue);
|
||||
}
|
||||
|
||||
public float getFloatOrDefault(final String name, final float defaultValue) {
|
||||
return getArgumentOrDefault(name, Float.class, defaultValue);
|
||||
}
|
||||
|
||||
public RedirectModifier<CommandSourceStack> getRedirectModifier() {
|
||||
return source.getRedirectModifier();
|
||||
}
|
||||
|
||||
public StringRange getRange() {
|
||||
return source.getRange();
|
||||
}
|
||||
|
||||
public String getInput() {
|
||||
return source.getInput();
|
||||
}
|
||||
|
||||
public CommandNode<CommandSourceStack> getRootNode() {
|
||||
return source.getRootNode();
|
||||
}
|
||||
|
||||
public List<ParsedCommandNode<CommandSourceStack>> getNodes() {
|
||||
return source.getNodes();
|
||||
}
|
||||
|
||||
public boolean hasNodes() {
|
||||
return source.hasNodes();
|
||||
}
|
||||
|
||||
public boolean isForked() {
|
||||
return source.isForked();
|
||||
}
|
||||
|
||||
public com.mojang.brigadier.context.CommandContext<CommandSourceStack> getMojangContext() {
|
||||
return source;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.exceptions.CommandSyntaxException;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.function.Supplier;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
public abstract class CommandNode {
|
||||
private static final Map<Class<? extends CommandNode>, String> class2NameMap = new HashMap<>();
|
||||
|
||||
protected final String name;
|
||||
protected final List<CommandNode> children = new ArrayList<>();
|
||||
|
||||
protected CommandNode(String name) {
|
||||
this.name = name;
|
||||
class2NameMap.put(getClass(), name);
|
||||
}
|
||||
|
||||
@SafeVarargs
|
||||
protected final void children(Supplier<? extends CommandNode>... childrenClasses) {
|
||||
this.children.addAll(Stream.of(childrenClasses).map(Supplier::get).toList());
|
||||
}
|
||||
|
||||
protected final void children(CommandNode... childrenClasses) {
|
||||
this.children.addAll(List.of(childrenClasses));
|
||||
}
|
||||
|
||||
protected abstract ArgumentBuilder<CommandSourceStack, ?> compileBase();
|
||||
|
||||
protected boolean execute(CommandContext context) throws CommandSyntaxException {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean requires(CommandSourceStack source) {
|
||||
return true;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
protected ArgumentBuilder<CommandSourceStack, ?> compile() {
|
||||
ArgumentBuilder<CommandSourceStack, ?> builder = compileBase().requires(this::requires);
|
||||
|
||||
for (CommandNode child : children) {
|
||||
builder = builder.then(child.compile());
|
||||
}
|
||||
|
||||
if (canExecute()) {
|
||||
builder = builder.executes(mojangCtx -> {
|
||||
CommandContext ctx = new CommandContext(mojangCtx);
|
||||
return execute(ctx) ? 1 : 0;
|
||||
});
|
||||
}
|
||||
|
||||
return builder;
|
||||
}
|
||||
|
||||
protected boolean canExecute() {
|
||||
return isMethodOverridden("execute", CommandNode.class);
|
||||
}
|
||||
|
||||
protected boolean isMethodOverridden(String methodName, @NotNull Class<?> baseClass) {
|
||||
for (Method method : getClass().getDeclaredMethods()) {
|
||||
if (method.getName().equals(methodName)) {
|
||||
return method.getDeclaringClass() != baseClass;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static String getNameForNode(Class<? extends CommandNode> nodeClass) {
|
||||
return class2NameMap.get(nodeClass);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.google.common.base.Functions;
|
||||
import com.google.common.collect.Iterables;
|
||||
import com.google.common.collect.Lists;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.permissions.Permission;
|
||||
import org.bukkit.permissions.PermissionDefault;
|
||||
import org.bukkit.plugin.PluginManager;
|
||||
import org.checkerframework.checker.nullness.qual.NonNull;
|
||||
import org.checkerframework.framework.qual.DefaultQualifier;
|
||||
import org.jetbrains.annotations.Contract;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
import org.jetbrains.annotations.Nullable;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
public class CommandUtils {
|
||||
|
||||
public static void registerPermissions(String base, @NotNull List<? extends CommandNode> children) {
|
||||
List<String> permissions = new ArrayList<>();
|
||||
permissions.add(base);
|
||||
permissions.addAll(children.stream().map((it) -> base + "." + it.getName()).toList());
|
||||
registerPermissions(permissions);
|
||||
}
|
||||
|
||||
public static void registerPermissions(@NotNull List<String> permissions) {
|
||||
PluginManager pluginManager = Bukkit.getServer().getPluginManager();
|
||||
for (String perm : permissions) {
|
||||
if (pluginManager.getPermission(perm) == null) {
|
||||
pluginManager.addPermission(new Permission(perm, PermissionDefault.OP));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@DefaultQualifier(NonNull.class)
|
||||
public static List<String> getListClosestMatchingLast(final String last, final Collection<?> collection) {
|
||||
if (collection.isEmpty()) {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
ArrayList<Candidate> candidates = Lists.newArrayList();
|
||||
String lastLower = last.toLowerCase();
|
||||
for (String item : Iterables.transform(collection, Functions.toStringFunction())) {
|
||||
String itemLower = item.toLowerCase();
|
||||
if (itemLower.startsWith(lastLower)) {
|
||||
candidates.add(Candidate.of(item, 0));
|
||||
} else if (itemLower.contains(lastLower)) {
|
||||
candidates.add(Candidate.of(item, damerauLevenshteinDistance(lastLower, itemLower)));
|
||||
}
|
||||
}
|
||||
candidates.sort(Comparator.comparingInt(Candidate::score));
|
||||
|
||||
List<String> results = new ArrayList<>(candidates.size());
|
||||
for (Candidate candidate : candidates) {
|
||||
results.add(candidate.item);
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes the Dameraur-Levenshtein Distance between two strings. Adapted
|
||||
* from the algorithm at <a href="http://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance">Wikipedia: Damerau–Levenshtein distance</a>
|
||||
*
|
||||
* @param s1 The first string being compared.
|
||||
* @param s2 The second string being compared.
|
||||
* @return The number of substitutions, deletions, insertions, and
|
||||
* transpositions required to get from s1 to s2.
|
||||
*/
|
||||
@SuppressWarnings("DuplicatedCode")
|
||||
private static int damerauLevenshteinDistance(@Nullable String s1, @Nullable String s2) {
|
||||
if (s1 == null && s2 == null) {
|
||||
return 0;
|
||||
}
|
||||
if (s1 != null && s2 == null) {
|
||||
return s1.length();
|
||||
}
|
||||
if (s1 == null) {
|
||||
return s2.length();
|
||||
}
|
||||
|
||||
int s1Len = s1.length();
|
||||
int s2Len = s2.length();
|
||||
int[][] H = new int[s1Len + 2][s2Len + 2];
|
||||
|
||||
int INF = s1Len + s2Len;
|
||||
H[0][0] = INF;
|
||||
for (int i = 0; i <= s1Len; i++) {
|
||||
H[i + 1][1] = i;
|
||||
H[i + 1][0] = INF;
|
||||
}
|
||||
for (int j = 0; j <= s2Len; j++) {
|
||||
H[1][j + 1] = j;
|
||||
H[0][j + 1] = INF;
|
||||
}
|
||||
|
||||
Map<Character, Integer> sd = new HashMap<>();
|
||||
for (char Letter : (s1 + s2).toCharArray()) {
|
||||
if (!sd.containsKey(Letter)) {
|
||||
sd.put(Letter, 0);
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 1; i <= s1Len; i++) {
|
||||
int DB = 0;
|
||||
for (int j = 1; j <= s2Len; j++) {
|
||||
int i1 = sd.get(s2.charAt(j - 1));
|
||||
int j1 = DB;
|
||||
|
||||
if (s1.charAt(i - 1) == s2.charAt(j - 1)) {
|
||||
H[i + 1][j + 1] = H[i][j];
|
||||
DB = j;
|
||||
} else {
|
||||
H[i + 1][j + 1] = Math.min(H[i][j], Math.min(H[i + 1][j], H[i][j + 1])) + 1;
|
||||
}
|
||||
|
||||
H[i + 1][j + 1] = Math.min(H[i + 1][j + 1], H[i1][j1] + (i - i1 - 1) + 1 + (j - j1 - 1));
|
||||
}
|
||||
sd.put(s1.charAt(i - 1), i);
|
||||
}
|
||||
|
||||
return H[s1Len + 1][s2Len + 1];
|
||||
}
|
||||
|
||||
private record Candidate(String item, int score) {
|
||||
@Contract("_, _ -> new")
|
||||
private static @NotNull Candidate of(String item, int score) {
|
||||
return new Candidate(item, score);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.Commands;
|
||||
|
||||
public class LiteralNode extends CommandNode {
|
||||
|
||||
protected LiteralNode(String name) {
|
||||
super(name);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ArgumentBuilder<CommandSourceStack, ?> compileBase() {
|
||||
return Commands.literal(name);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
/*
|
||||
* This file is part of Leaves (https://github.com/LeavesMC/Leaves)
|
||||
*
|
||||
* Leaves is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Lesser General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* Leaves is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Lesser General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Lesser General Public License
|
||||
* along with Leaves. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
package org.leavesmc.leaves.command;
|
||||
|
||||
import com.mojang.brigadier.builder.ArgumentBuilder;
|
||||
import com.mojang.brigadier.builder.LiteralArgumentBuilder;
|
||||
import io.papermc.paper.command.brigadier.CommandSourceStack;
|
||||
import io.papermc.paper.command.brigadier.PaperCommands;
|
||||
import org.bukkit.Bukkit;
|
||||
import org.bukkit.command.CommandSender;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import static org.leavesmc.leaves.command.CommandUtils.registerPermissions;
|
||||
|
||||
public abstract class RootNode extends LiteralNode {
|
||||
private final String permissionBase;
|
||||
|
||||
public RootNode(String name, String permissionBase) {
|
||||
super(name);
|
||||
this.permissionBase = permissionBase;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ArgumentBuilder<CommandSourceStack, ?> compile() {
|
||||
registerPermissions(permissionBase, this.children);
|
||||
return super.compile();
|
||||
}
|
||||
|
||||
public static boolean hasPermission(String permissionBase, @NotNull CommandSender sender, String... subcommand) {
|
||||
if (sender.hasPermission(permissionBase)) return true;
|
||||
String currentPermission = permissionBase;
|
||||
for (String sub : subcommand) {
|
||||
currentPermission += "." + sub;
|
||||
if (sender.hasPermission(currentPermission)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean requires(@NotNull CommandSourceStack source) {
|
||||
return children.stream().anyMatch(child -> child.requires(source));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
public void register() {
|
||||
PaperCommands.INSTANCE.setValid();
|
||||
PaperCommands.INSTANCE.getDispatcher().register((LiteralArgumentBuilder<CommandSourceStack>) compile());
|
||||
PaperCommands.INSTANCE.invalidate();
|
||||
Bukkit.getOnlinePlayers().forEach(org.bukkit.entity.Player::updateCommands);
|
||||
}
|
||||
|
||||
public void unregister() {
|
||||
PaperCommands.INSTANCE.setValid();
|
||||
PaperCommands.INSTANCE.getDispatcher().getRoot().removeCommand(name);
|
||||
PaperCommands.INSTANCE.invalidate();
|
||||
Bukkit.getOnlinePlayers().forEach(org.bukkit.entity.Player::updateCommands);
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user