Push forward

This commit is contained in:
2026-07-14 13:29:52 +08:00
parent 969cd5c5f8
commit dd3efffa76
246 changed files with 69 additions and 69 deletions
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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);
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
@@ -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;
}
}
}
}
@@ -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
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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();
}
@@ -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 {
}
@@ -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 {
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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");
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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;
}
}
}
@@ -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);
}
}
@@ -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");
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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;
}
@@ -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;
}
}
@@ -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;
}
@@ -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;
}
@@ -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();
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
@@ -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);
}
}
}
@@ -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;
}
@@ -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;
}
@@ -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;
}
}
@@ -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
}
@@ -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;
}
}
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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;
}
}
@@ -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;
}
@@ -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))
));
}
}
@@ -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);
}
}
}
@@ -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;
}
}