9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-28 19:29:07 +00:00

All source patches applied and starts

This commit is contained in:
Samsuik
2025-01-16 19:32:46 +00:00
parent 24d7230079
commit 39bba67ec9
171 changed files with 4443 additions and 15 deletions

View File

@@ -0,0 +1,72 @@
package me.samsuik.sakura.command;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
import java.util.function.Function;
@DefaultQualifier(NonNull.class)
public abstract class BaseSubCommand extends Command {
public BaseSubCommand(String name) {
super(name);
this.description = "Sakura Command " + name;
this.setPermission("bukkit.command." + name);
}
public abstract void execute(CommandSender sender, String[] args);
public void tabComplete(List<String> list, String[] args) throws IllegalArgumentException {}
@Override
@Deprecated
public final boolean execute(CommandSender sender, String label, String[] args) {
if (this.testPermission(sender)) {
this.execute(sender, args);
}
return true;
}
@Override
@NotNull
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
List<String> completions = new ArrayList<>(0);
if (this.testPermissionSilent(sender)) {
this.tabComplete(completions, args);
}
return completions;
}
protected final Optional<Integer> parseInt(String[] args, int index) {
return this.parse(args, index, Integer::parseInt);
}
protected final Optional<Long> parseLong(String[] args, int index) {
return this.parse(args, index, Long::parseLong);
}
protected final Optional<Float> parseFloat(String[] args, int index) {
return this.parse(args, index, Float::parseFloat);
}
protected final Optional<Double> parseDouble(String[] args, int index) {
return this.parse(args, index, Double::parseDouble);
}
protected final <T> Optional<T> parse(String[] args, int index, Function<String, T> func) {
try {
String arg = args[index];
return Optional.of(func.apply(arg));
} catch (NumberFormatException | ArrayIndexOutOfBoundsException ignored) {
return Optional.empty();
}
}
}

View File

@@ -0,0 +1,86 @@
package me.samsuik.sakura.command;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import net.minecraft.server.MinecraftServer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.stream.Stream;
@DefaultQualifier(NonNull.class)
public final class SakuraCommand extends Command {
private static final Component HEADER_MESSAGE = MiniMessage.miniMessage().deserialize("""
<dark_purple>.</dark_purple>
<dark_purple>| <white>This is the main command for <gradient:red:light_purple:0.5>Sakura</gradient>.
<dark_purple>| <white>All exclusive commands are listed below."""
);
private static final String COMMAND_MSG = "<dark_purple>| <dark_gray>*</dark_gray> /<light_purple><command>";
public SakuraCommand(String name) {
super(name);
this.description = "";
this.usageMessage = "/sakura";
this.setPermission("bukkit.command.sakura");
}
@Override
public boolean execute(CommandSender sender, String commandLabel, String[] args) {
if (args.length > 0) {
List<Command> commands = new ArrayList<>(SakuraCommands.COMMANDS.values());
// This part is copied from the VersionCommand SubCommand in paper
Command internalVersion = MinecraftServer.getServer().server.getCommandMap().getCommand("version");
if (internalVersion != null) {
commands.add(internalVersion);
}
for (Command base : commands) {
if (base.getName().equalsIgnoreCase(args[0])) {
return base.execute(sender, commandLabel, Arrays.copyOfRange(args, 1, args.length));
}
}
}
this.sendHelpMessage(sender);
return false;
}
private void sendHelpMessage(CommandSender sender) {
sender.sendMessage(HEADER_MESSAGE);
Stream<Command> uniqueCommands = SakuraCommands.COMMANDS.values()
.stream()
.filter(command -> command != this);
uniqueCommands.forEach((command) -> {
sender.sendRichMessage(COMMAND_MSG, Placeholder.unparsed("command", command.getName()));
});
sender.sendMessage(Component.text("'", NamedTextColor.DARK_PURPLE));
}
@NotNull
@Override
public List<String> tabComplete(CommandSender sender, String alias, String[] args) throws IllegalArgumentException {
if (!this.testPermissionSilent(sender)) {
return Collections.emptyList();
}
return SakuraCommands.COMMANDS.values().stream()
.filter(command -> command != this)
.map(Command::getName)
.filter(name -> args.length <= 1 || name.startsWith(args[args.length - 1]))
.toList();
}
}

View File

@@ -0,0 +1,24 @@
package me.samsuik.sakura.command;
import me.samsuik.sakura.command.subcommands.ConfigCommand;
import me.samsuik.sakura.command.subcommands.TPSCommand;
import net.minecraft.server.MinecraftServer;
import org.bukkit.command.Command;
import java.util.HashMap;
import java.util.Map;
public final class SakuraCommands {
static final Map<String, Command> COMMANDS = new HashMap<>();
static {
COMMANDS.put("sakura", new SakuraCommand("sakura"));
COMMANDS.put("config", new ConfigCommand("config"));
COMMANDS.put("tps", new TPSCommand("tps"));
}
public static void registerCommands(MinecraftServer server) {
COMMANDS.forEach((s, command) -> {
server.server.getCommandMap().register(s, "sakura", command);
});
}
}

View File

@@ -0,0 +1,33 @@
package me.samsuik.sakura.command.subcommands;
import me.samsuik.sakura.command.BaseSubCommand;
import net.minecraft.server.MinecraftServer;
import org.bukkit.command.Command;
import org.bukkit.command.CommandSender;
import org.bukkit.craftbukkit.CraftServer;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.framework.qual.DefaultQualifier;
import static net.kyori.adventure.text.Component.text;
import static net.kyori.adventure.text.format.NamedTextColor.GREEN;
import static net.kyori.adventure.text.format.NamedTextColor.RED;
@DefaultQualifier(NonNull.class)
public final class ConfigCommand extends BaseSubCommand {
public ConfigCommand(String name) {
super(name);
this.description = "Command for reloading the sakura configuration file";
}
@Override
public void execute(CommandSender sender, String[] args) {
Command.broadcastCommandMessage(sender, text("Please note that this command is not supported and may cause issues.", RED));
Command.broadcastCommandMessage(sender, text("If you encounter any issues please use the /stop command to restart your server.", RED));
MinecraftServer server = ((CraftServer) sender.getServer()).getServer();
server.sakuraConfigurations.reloadConfigs(server);
server.server.reloadCount++;
Command.broadcastCommandMessage(sender, text("Sakura config reload complete.", GREEN));
}
}

View File

@@ -0,0 +1,92 @@
package me.samsuik.sakura.command.subcommands;
import com.google.common.base.Strings;
import com.google.common.collect.ImmutableList;
import me.samsuik.sakura.command.BaseSubCommand;
import me.samsuik.sakura.tps.ServerTickInformation;
import me.samsuik.sakura.tps.graph.BuiltComponentCanvas;
import me.samsuik.sakura.tps.graph.DetailedTPSGraph;
import me.samsuik.sakura.tps.graph.GraphComponents;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.event.ClickEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextDecoration;
import net.minecraft.server.MinecraftServer;
import org.bukkit.command.CommandSender;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class TPSCommand extends BaseSubCommand {
private static final int GRAPH_WIDTH = 71;
private static final int GRAPH_HEIGHT = 10;
private static final Style GRAY_WITH_STRIKETHROUGH = Style.style(NamedTextColor.GRAY, TextDecoration.STRIKETHROUGH);
public TPSCommand(String name) {
super(name);
this.description = "Displays the current ticks per second";
}
@Override
public void execute(CommandSender sender, String[] args) {
ServerTickInformation tickInformation = MinecraftServer.getServer().latestTickInformation();
long identifier = this.parseLong(args, 1).orElse(tickInformation.identifier());
double scale = this.parseDouble(args, 0).orElse(-1.0);
if (scale < 0.0) {
scale = this.dynamicScale(identifier);
}
ImmutableList<ServerTickInformation> tickHistory = MinecraftServer.getServer().tickHistory(identifier - GRAPH_WIDTH, identifier);
DetailedTPSGraph graph = new DetailedTPSGraph(GRAPH_WIDTH, GRAPH_HEIGHT, scale, tickHistory);
BuiltComponentCanvas canvas = graph.plot();
canvas.appendLeft(Component.text(":", NamedTextColor.BLACK));
canvas.appendRight(Component.text(":", NamedTextColor.BLACK));
canvas.header(this.createHeaderComponent(tickInformation, identifier));
canvas.footer(Component.text("*", NamedTextColor.DARK_GRAY)
.append(Component.text(Strings.repeat(" ", GRAPH_WIDTH - 1), GRAY_WITH_STRIKETHROUGH))
.append(Component.text("*")));
for (Component component : canvas.components()) {
sender.sendMessage(component);
}
}
private double dynamicScale(long identifier) {
ImmutableList<ServerTickInformation> tickHistory = MinecraftServer.getServer().tickHistory(identifier - 5, identifier);
double averageTps = tickHistory.stream()
.mapToDouble(ServerTickInformation::tps)
.average()
.orElse(0.0);
return 20 / averageTps;
}
private Component createHeaderComponent(ServerTickInformation tickInformation, long identifier) {
int scrollAmount = GRAPH_WIDTH / 3 * 2;
double memoryUsage = memoryUsage();
TextComponent.Builder builder = Component.text();
builder.color(NamedTextColor.DARK_GRAY);
builder.append(Component.text("< ")
.clickEvent(ClickEvent.runCommand("/tps -1 " + (identifier + scrollAmount))));
builder.append(Component.text(Strings.repeat(" ", 19), GRAY_WITH_STRIKETHROUGH));
builder.append(Component.text(" ( "));
builder.append(Component.text("Now: ", NamedTextColor.WHITE)
.append(Component.text("%.1f".formatted(tickInformation.tps()), tickInformation.colour())));
builder.appendSpace();
builder.append(Component.text("Mem: ", NamedTextColor.WHITE)
.append(Component.text("%.1f".formatted(memoryUsage * 100), GraphComponents.colour(1 - (float) memoryUsage))));
builder.append(Component.text("% ) "));
builder.append(Component.text(Strings.repeat(" ", 18), GRAY_WITH_STRIKETHROUGH));
builder.append(Component.text(" >")
.clickEvent(ClickEvent.runCommand("/tps -1 " + (identifier - scrollAmount))));
return builder.build();
}
private static double memoryUsage() {
Runtime runtime = Runtime.getRuntime();
double free = runtime.freeMemory();
double max = runtime.maxMemory();
double alloc = runtime.totalMemory();
return (alloc - free) / max;
}
}

View File

@@ -0,0 +1,63 @@
package me.samsuik.sakura.configuration;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.ConfigurationPart;
import io.papermc.paper.configuration.type.number.IntOr;
import org.bukkit.Material;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
@SuppressWarnings({"CanBeFinal", "FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic", "RedundantSuppression"})
public final class GlobalConfiguration extends ConfigurationPart {
private static final Logger LOGGER = LogUtils.getClassLogger();
static final int CURRENT_VERSION = 3;// (when you change the version, change the comment, so it conflicts on rebases): rename filter bad nbt from spawn eggs
private static GlobalConfiguration instance;
public static GlobalConfiguration get() {
return instance;
}
static void set(GlobalConfiguration instance) {
GlobalConfiguration.instance = instance;
}
@Setting(Configuration.VERSION_FIELD)
public int version = CURRENT_VERSION;
public Messages messages;
public class Messages extends ConfigurationPart {
public String durableBlockInteraction = "<dark_gray>(<light_purple>S</light_purple>) <white>This block has <gray><remaining></gray> of <gray><durability>";
public String fpsSettingChange = "<dark_gray>(<light_purple>S</light_purple>) <gray><state> <yellow><name>";
public boolean tpsShowEntityAndChunkCount = true;
}
public Fps fps;
public class Fps extends ConfigurationPart {
public Material material = Material.PINK_STAINED_GLASS_PANE;
}
public Players players;
public class Players extends ConfigurationPart {
public IntOr.Default bucketStackSize = IntOr.Default.USE_DEFAULT;
}
public Environment environment;
public class Environment extends ConfigurationPart {
@Comment("This is only intended for plot worlds. Will affect chunk generation on servers.")
public boolean calculateBiomeNoiseOncePerChunkSection = false;
public MobSpawnerDefaults mobSpawnerDefaults = new MobSpawnerDefaults();
public class MobSpawnerDefaults extends ConfigurationPart {
public int minSpawnDelay = 200;
public int maxSpawnDelay = 800;
public int spawnCount = 4;
public int maxNearbyEntities = 6;
public int requiredPlayerRange = 16;
public int spawnRange = 4;
}
}
}

View File

@@ -0,0 +1,228 @@
package me.samsuik.sakura.configuration;
import com.google.common.collect.Table;
import com.mojang.logging.LogUtils;
import io.leangen.geantyref.TypeToken;
import io.papermc.paper.configuration.*;
import io.papermc.paper.configuration.mapping.InnerClassFieldDiscoverer;
import io.papermc.paper.configuration.serializer.*;
import io.papermc.paper.configuration.serializer.collections.FastutilMapSerializer;
import io.papermc.paper.configuration.serializer.collections.TableSerializer;
import io.papermc.paper.configuration.serializer.registry.RegistryHolderSerializer;
import io.papermc.paper.configuration.serializer.registry.RegistryValueSerializer;
import it.unimi.dsi.fastutil.objects.Reference2IntMap;
import it.unimi.dsi.fastutil.objects.Reference2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.Reference2LongMap;
import it.unimi.dsi.fastutil.objects.Reference2LongOpenHashMap;
import me.samsuik.sakura.configuration.transformation.ConfigurationTransformations;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.levelgen.feature.ConfiguredFeature;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jspecify.annotations.NullMarked;
import org.slf4j.Logger;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.ConfigurationOptions;
import org.spongepowered.configurate.objectmapping.FieldDiscoverer;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.nio.file.Path;
import java.util.Map;
import java.util.function.Function;
import static io.leangen.geantyref.GenericTypeReflector.erase;
@NullMarked
@SuppressWarnings("Convert2Diamond")
public final class SakuraConfigurations extends Configurations<GlobalConfiguration, WorldConfiguration> {
private static final Logger LOGGER = LogUtils.getClassLogger();
static final String GLOBAL_CONFIG_FILE_NAME = "sakura-global.yml";
static final String WORLD_DEFAULTS_CONFIG_FILE_NAME = "sakura-world-defaults.yml";
static final String WORLD_CONFIG_FILE_NAME = "sakura-world.yml";
public static final String CONFIG_DIR = "config";
private static final String GLOBAL_HEADER = String.format("""
This is the global configuration file for Sakura.
As you can see, there's a lot to configure. Some options may impact gameplay, so use
with caution, and make sure you know what each option does before configuring.
The world configuration options have been moved inside
their respective world folder. The files are named %s""", WORLD_CONFIG_FILE_NAME);
private static final String WORLD_DEFAULTS_HEADER = """
This is the world defaults configuration file for Sakura.
As you can see, there's a lot to configure. Some options may impact gameplay, so use
with caution, and make sure you know what each option does before configuring.
Configuration options here apply to all worlds, unless you specify overrides inside
the world-specific config file inside each world folder.""";
private static final Function<ContextMap, String> WORLD_HEADER = map -> String.format("""
This is a world configuration file for Sakura.
This file may start empty but can be filled with settings to override ones in the %s/%s
World: %s (%s)""",
SakuraConfigurations.CONFIG_DIR,
SakuraConfigurations.WORLD_DEFAULTS_CONFIG_FILE_NAME,
map.require(WORLD_NAME),
map.require(WORLD_KEY)
);
public SakuraConfigurations(final Path globalFolder) {
super(globalFolder, GlobalConfiguration.class, WorldConfiguration.class, GLOBAL_CONFIG_FILE_NAME, WORLD_DEFAULTS_CONFIG_FILE_NAME, WORLD_CONFIG_FILE_NAME);
}
@Override
protected YamlConfigurationLoader.Builder createLoaderBuilder() {
return super.createLoaderBuilder()
.defaultOptions(PaperConfigurations::defaultOptions);
}
@Override
protected ObjectMapper.Factory.Builder createGlobalObjectMapperFactoryBuilder() {
return defaultGlobalFactoryBuilder(super.createGlobalObjectMapperFactoryBuilder());
}
private static ObjectMapper.Factory.Builder defaultGlobalFactoryBuilder(ObjectMapper.Factory.Builder builder) {
return builder.addDiscoverer(InnerClassFieldDiscoverer.globalConfig());
}
@Override
protected YamlConfigurationLoader.Builder createGlobalLoaderBuilder(RegistryAccess registryAccess) {
return super.createGlobalLoaderBuilder(registryAccess)
.defaultOptions(SakuraConfigurations::defaultGlobalOptions);
}
private static ConfigurationOptions defaultGlobalOptions(ConfigurationOptions options) {
return options
.header(GLOBAL_HEADER)
.serializers(builder -> builder
.register(new PacketClassSerializer())
);
}
@Override
public GlobalConfiguration initializeGlobalConfiguration(final RegistryAccess registryAccess) throws ConfigurateException {
GlobalConfiguration configuration = super.initializeGlobalConfiguration(registryAccess);
GlobalConfiguration.set(configuration);
return configuration;
}
@Override
protected ObjectMapper.Factory.Builder createWorldObjectMapperFactoryBuilder(final ContextMap contextMap) {
return super.createWorldObjectMapperFactoryBuilder(contextMap)
.addNodeResolver(new NestedSetting.Factory())
.addDiscoverer(createWorldConfigFieldDiscoverer(contextMap));
}
private static FieldDiscoverer<?> createWorldConfigFieldDiscoverer(final ContextMap contextMap) {
final Map<Class<?>, Object> overrides = Map.of(
WorldConfiguration.class, createWorldConfigInstance(contextMap)
);
return new InnerClassFieldDiscoverer(overrides);
}
private static WorldConfiguration createWorldConfigInstance(ContextMap contextMap) {
return new WorldConfiguration(contextMap.require(Configurations.WORLD_KEY));
}
@Override
protected YamlConfigurationLoader.Builder createWorldConfigLoaderBuilder(final ContextMap contextMap) {
final RegistryAccess access = contextMap.require(REGISTRY_ACCESS);
return super.createWorldConfigLoaderBuilder(contextMap)
.defaultOptions(options -> options
.header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap))
.serializers(serializers -> serializers
.register(new TypeToken<Reference2IntMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2IntMap<?>>(Reference2IntOpenHashMap::new, Integer.TYPE))
.register(new TypeToken<Reference2LongMap<?>>() {}, new FastutilMapSerializer.SomethingToPrimitive<Reference2LongMap<?>>(Reference2LongOpenHashMap::new, Long.TYPE))
.register(new TypeToken<Table<?, ?, ?>>() {}, new TableSerializer())
.register(StringRepresentableSerializer::isValidFor, new StringRepresentableSerializer())
.register(new RegistryValueSerializer<>(new TypeToken<EntityType<?>>() {}, access, Registries.ENTITY_TYPE, true))
.register(new RegistryValueSerializer<>(Item.class, access, Registries.ITEM, true))
.register(new RegistryValueSerializer<>(Block.class, access, Registries.BLOCK, true))
.register(new RegistryHolderSerializer<>(new TypeToken<ConfiguredFeature<?, ?>>() {}, access, Registries.CONFIGURED_FEATURE, false))
)
);
}
@Override
protected void applyWorldConfigTransformations(final ContextMap contextMap, final ConfigurationNode node, final @Nullable ConfigurationNode defaultsNode) throws ConfigurateException {
ConfigurationTransformations.worldTransformations(node);
}
@Override
protected void applyGlobalConfigTransformations(final ConfigurationNode node) throws ConfigurateException {
ConfigurationTransformations.globalTransformations(node);
}
@Override
public WorldConfiguration createWorldConfig(final ContextMap contextMap) {
final String levelName = contextMap.require(WORLD_NAME);
try {
return super.createWorldConfig(contextMap);
} catch (IOException exception) {
throw new RuntimeException("Could not create world config for " + levelName, exception);
}
}
@Override
protected boolean isConfigType(final Type type) {
return ConfigurationPart.class.isAssignableFrom(erase(type));
}
@Override
protected int globalConfigVersion() {
return GlobalConfiguration.CURRENT_VERSION;
}
@Override
protected int worldConfigVersion() {
return WorldConfiguration.CURRENT_VERSION;
}
public void reloadConfigs(MinecraftServer server) {
try {
this.initializeGlobalConfiguration(server.registryAccess(), reloader(this.globalConfigClass, GlobalConfiguration.get()));
this.initializeWorldDefaultsConfiguration(server.registryAccess());
for (ServerLevel level : server.getAllLevels()) {
this.createWorldConfig(createWorldContextMap(level), reloader(this.worldConfigClass, level.sakuraConfig()));
}
} catch (Exception ex) {
throw new RuntimeException("Could not reload sakura configuration files", ex);
}
}
private static ContextMap createWorldContextMap(ServerLevel level) {
return createWorldContextMap(level.levelStorageAccess.levelDirectory.path(), level.serverLevelData.getLevelName(), level.dimension().location(), level.registryAccess());
}
public static ContextMap createWorldContextMap(Path dir, String levelName, ResourceLocation worldKey, RegistryAccess registryAccess) {
return ContextMap.builder()
.put(WORLD_DIRECTORY, dir)
.put(WORLD_NAME, levelName)
.put(WORLD_KEY, worldKey)
.put(REGISTRY_ACCESS, registryAccess)
.build();
}
public static SakuraConfigurations setup(final Path configDir) {
try {
PaperConfigurations.createDirectoriesSymlinkAware(configDir);
return new SakuraConfigurations(configDir);
} catch (final IOException ex) {
throw new RuntimeException("Could not setup PaperConfigurations", ex);
}
}
}

View File

@@ -0,0 +1,242 @@
package me.samsuik.sakura.configuration;
import com.mojang.logging.LogUtils;
import io.papermc.paper.configuration.Configuration;
import io.papermc.paper.configuration.ConfigurationPart;
import io.papermc.paper.configuration.NestedSetting;
import io.papermc.paper.configuration.PaperConfigurations;
import io.papermc.paper.configuration.type.number.DoubleOr;
import io.papermc.paper.configuration.type.number.IntOr;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import me.samsuik.sakura.entity.merge.MergeLevel;
import me.samsuik.sakura.explosion.durable.DurableMaterial;
import me.samsuik.sakura.physics.PhysicsVersion;
import net.minecraft.Util;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.item.Item;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.meta.Comment;
import org.spongepowered.configurate.objectmapping.meta.Setting;
import java.util.List;
import java.util.Map;
@SuppressWarnings({"FieldCanBeLocal", "FieldMayBeFinal", "NotNullFieldNotInitialized", "InnerClassMayBeStatic", "RedundantSuppression"})
public final class WorldConfiguration extends ConfigurationPart {
private static final Logger LOGGER = LogUtils.getClassLogger();
static final int CURRENT_VERSION = 6; // (when you change the version, change the comment, so it conflicts on rebases): rename filter bad nbt from spawn eggs
private transient final ResourceLocation worldKey;
WorldConfiguration(ResourceLocation worldKey) {
this.worldKey = worldKey;
}
public boolean isDefault() {
return this.worldKey.equals(PaperConfigurations.WORLD_DEFAULTS_KEY);
}
@Setting(Configuration.VERSION_FIELD)
public int version = CURRENT_VERSION;
public Cannons cannons;
public class Cannons extends ConfigurationPart {
public MergeLevel mergeLevel = MergeLevel.STRICT;
public boolean tntAndSandAffectedByBubbleColumns = true;
@NestedSetting({"treat-collidable-blocks-as-full", "while-moving"})
public boolean treatAllBlocksAsFullWhenMoving = false;
@NestedSetting({"treat-collidable-blocks-as-full", "moving-faster-than"})
public double treatAllBlocksAsFullWhenMovingFasterThan = 64.0;
public boolean loadChunks = false;
public Restrictions restrictions = new Restrictions();
public class Restrictions extends ConfigurationPart {
@Comment("The amount of blocks that can be travelled before changing direction is restricted")
public IntOr.Disabled leftShootingThreshold = IntOr.Disabled.DISABLED;
@Comment(
"Maximum amount of blocks that a cannon can adjust\n" +
"It is recommended that this value kept sane and is more than 64 blocks"
)
public IntOr.Disabled maxAdjustDistance = IntOr.Disabled.DISABLED;
}
public Tnt tnt = new Tnt();
public class Tnt extends ConfigurationPart {
public boolean forcePositionUpdates;
}
public Sand sand = new Sand();
public class Sand extends ConfigurationPart {
public boolean despawnInsideMovingPistons = true;
public boolean concreteSolidifyInWater = true;
@NestedSetting({"prevent-stacking", "against-border"})
public boolean preventAgainstBorder = false;
@NestedSetting({"prevent-stacking", "world-height"})
public boolean preventAtWorldHeight = false;
public boolean isFallingBlockInBounds(FallingBlockEntity entity) {
return (!this.preventAgainstBorder || !ca.spottedleaf.moonrise.patches.collisions.CollisionUtil.isCollidingWithBorder(entity.level().getWorldBorder(), entity.getBoundingBox().inflate(0.01)))
&& (!this.preventAtWorldHeight || entity.blockPosition().getY() < entity.level().getMaxY() - 1);
}
}
public Explosion explosion = new Explosion();
public class Explosion extends ConfigurationPart {
public boolean optimiseProtectedRegions = true;
public boolean avoidRedundantBlockSearches = false;
public Map<Block, DurableMaterial> durableMaterials = Util.make(new Reference2ObjectOpenHashMap<>(), map -> {
map.put(Blocks.OBSIDIAN, new DurableMaterial(4, Blocks.COBBLESTONE.getExplosionResistance()));
map.put(Blocks.ANVIL, new DurableMaterial(3, Blocks.END_STONE.getExplosionResistance()));
map.put(Blocks.CHIPPED_ANVIL, new DurableMaterial(3, Blocks.END_STONE.getExplosionResistance()));
map.put(Blocks.DAMAGED_ANVIL, new DurableMaterial(3, Blocks.END_STONE.getExplosionResistance()));
});
public boolean protectScaffoldingFromCreepers = false;
public boolean allowNonTntBreakingDurableBlocks = false;
public boolean destroyWaterloggedBlocks = false;
public boolean explodeLava = false;
public boolean consistentRadius = false;
public boolean explosionsHurtPlayers = true;
public boolean explosionsDropItems = true;
public boolean useBlockCacheAcrossExplosions = false;
}
public Mechanics mechanics = new Mechanics();
public class Mechanics extends ConfigurationPart {
public TNTSpread tntSpread = TNTSpread.ALL;
public boolean tntFlowsInWater = true;
public boolean fallingBlockParity = false;
public PhysicsVersion physicsVersion = PhysicsVersion.LATEST;
public enum TNTSpread {
ALL, Y, NONE;
}
}
}
public Technical technical;
public class Technical extends ConfigurationPart {
public boolean dispenserRandomItemSelection = true;
@Comment(
"Only tick hoppers when items are able to be moved\n" +
"This can cause issues with redstone contraptions that rely on DUD's to detect when hoppers fail to move items."
)
public boolean optimiseIdleHopperTicking = true;
public Redstone redstone = new Redstone();
public class Redstone extends ConfigurationPart {
public boolean redstoneCache = false;
public boolean fluidsBreakRedstone = true;
}
@Comment(
"Allow TNT duplication while `allow-piston-duplication` is disabled.\n" +
"This exists so servers can enable TNT duplication without reintroducing the other forms of piston duplication."
)
public boolean allowTNTDuplication = false;
}
public Players players;
public class Players extends ConfigurationPart {
public Combat combat = new Combat();
public class Combat extends ConfigurationPart {
public boolean legacyCombatMechanics = false;
public boolean allowSweepAttacks = true;
public boolean shieldDamageReduction = false;
public boolean oldEnchantedGoldenApple = false;
public boolean oldSoundsAndParticleEffects = false;
public boolean fastHealthRegen = true;
public IntOr.Default maxArmourDamage = IntOr.Default.USE_DEFAULT;
}
public Knockback knockback = new Knockback();
public class Knockback extends ConfigurationPart {
public DoubleOr.Default knockbackVertical = DoubleOr.Default.USE_DEFAULT;
public double knockbackVerticalLimit = 0.4;
public boolean verticalKnockbackRequireGround = true;
public double baseKnockback = 0.4;
@Comment("Knockback caused by sweeping edge")
public double sweepingEdgeKnockback = 0.4;
public Sprinting sprinting = new Sprinting();
public class Sprinting extends ConfigurationPart {
public boolean requireFullAttack = true;
public double extraKnockback = 0.5;
@Comment("Delay between extra knockback hits in milliseconds")
public IntOr.Default knockbackDelay = IntOr.Default.USE_DEFAULT;
}
@NestedSetting({"projectiles", "fishing-hooks-apply-knockback"})
public boolean fishingHooksApplyKnockback;
@Comment("Knockback resistance attribute modifier")
public double knockbackResistanceModifier = 1.0;
@Comment("Received by attacking a shielded enemy")
public double shieldHitKnockback = 0.5;
}
@Comment("Prevents players swimming using elytra or riptide to enter holes")
public boolean posesShrinkCollisionBox = true;
public boolean fishingHooksPullEntities = true;
}
public Entity entity;
public class Entity extends ConfigurationPart {
@Comment("Only modify if you know what you're doing")
public boolean disableMobAi = false;
public boolean waterSensitivity = true;
public boolean instantDeathAnimation = false;
public boolean ironGolemsTakeFalldamage = false;
public Items items = new Items();
public class Items extends ConfigurationPart {
public List<Item> explosionResistantItems = List.of();
}
@Comment("Entity travel distance limits")
public Map<EntityType<?>, Integer> chunkTravelLimit = Util.make(new Reference2ObjectOpenHashMap<>(), map -> {
map.put(EntityType.ENDER_PEARL, 8);
});
public ThrownPotion thrownPotion = new ThrownPotion();
public class ThrownPotion extends ConfigurationPart {
public double horizontalSpeed = 1.0;
public double verticalSpeed = 1.0;
public boolean allowBreakingInsideEntities = false;
}
public EnderPearl enderPearl = new EnderPearl();
public class EnderPearl extends ConfigurationPart {
public boolean useOutlineForCollision = false;
}
}
public Environment environment;
public class Environment extends ConfigurationPart {
public boolean allowWaterInTheNether = false;
public boolean disableFastNetherLava = false;
public BlockGeneration blockGeneration = new BlockGeneration();
public class BlockGeneration extends ConfigurationPart {
public boolean legacyBlockFormation = false;
}
public Crops crops = new Crops();
public class Crops extends ConfigurationPart {
public boolean useRandomChanceToGrow = false;
}
public MobSpawner mobSpawner = new MobSpawner();
public class MobSpawner extends ConfigurationPart {
public boolean checkSpawnConditions = true;
public boolean requireNearbyPlayer = true;
public boolean ignoreEntityLimit = false;
}
}
}

View File

@@ -0,0 +1,46 @@
package me.samsuik.sakura.configuration.transformation;
import io.papermc.paper.configuration.transformation.Transformations;
import me.samsuik.sakura.configuration.transformation.global.V1_RelocateMessages;
import me.samsuik.sakura.configuration.transformation.global.V2_ConvertIconToMaterial;
import me.samsuik.sakura.configuration.transformation.world.*;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import java.util.List;
public final class ConfigurationTransformations {
private static final List<NodePath> REMOVED_GLOBAL_PATHS = List.of(
NodePath.path("cannons")
);
public static void worldTransformations(final ConfigurationNode node) throws ConfigurateException {
final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
V2_VerticalKnockbackUseDefault.apply(versionedBuilder);
V3_RenameKnockback.apply(versionedBuilder);
V4_RenameNonStrictMergeLevel.apply(versionedBuilder);
V5_CombineLoadChunksOptions.apply(versionedBuilder);
V6_FixIncorrectExtraKnockback.apply(versionedBuilder);
// ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
versionedBuilder.build().apply(node);
}
public static void globalTransformations(final ConfigurationNode node) throws ConfigurateException {
final ConfigurationTransformation.Builder builder = ConfigurationTransformation.builder();
for (final NodePath path : REMOVED_GLOBAL_PATHS) {
builder.addAction(path, TransformAction.remove());
}
builder.build().apply(node);
final ConfigurationTransformation.VersionedBuilder versionedBuilder = Transformations.versionedBuilder();
V1_RelocateMessages.apply(versionedBuilder);
V2_ConvertIconToMaterial.apply(versionedBuilder);
// ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
versionedBuilder.build().apply(node);
}
private ConfigurationTransformations() {}
}

View File

@@ -0,0 +1,31 @@
package me.samsuik.sakura.configuration.transformation.global;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import java.util.Map;
import static org.spongepowered.configurate.NodePath.path;
public final class V1_RelocateMessages {
private static final int VERSION = 2; // targeted version is always ahead by one
private static final Map<NodePath, NodePath> RELOCATION = Map.of(
path("fps", "message"), path("messages", "fps-setting-change"),
path("players", "potato-message"), path("messages", "durable-block-interaction")
);
private V1_RelocateMessages() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
ConfigurationTransformation.Builder transformationBuilder = ConfigurationTransformation.builder();
for (Map.Entry<NodePath, NodePath> entry : RELOCATION.entrySet()) {
transformationBuilder.addAction(entry.getKey(), relocate(entry.getValue()));
}
builder.addVersion(VERSION, transformationBuilder.build());
}
private static TransformAction relocate(NodePath path) {
return (node, object) -> path.array();
}
}

View File

@@ -0,0 +1,28 @@
package me.samsuik.sakura.configuration.transformation.global;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
public final class V2_ConvertIconToMaterial implements TransformAction {
private static final int VERSION = 3; // targeted version is always ahead by one
private static final NodePath PATH = NodePath.path("fps", "material");
private static final V2_ConvertIconToMaterial INSTANCE = new V2_ConvertIconToMaterial();
private V2_ConvertIconToMaterial() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
}
@Override
public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
if (value.raw() instanceof String stringValue) {
value.raw(stringValue.toUpperCase());
}
return null;
}
}

View File

@@ -0,0 +1,31 @@
package me.samsuik.sakura.configuration.transformation.world;
import io.papermc.paper.configuration.type.number.DoubleOr;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static org.spongepowered.configurate.NodePath.path;
public final class V2_VerticalKnockbackUseDefault implements TransformAction {
private static final int VERSION = 2;
private static final NodePath PATH = path("players", "knockback", "knockback-vertical");
private static final V2_VerticalKnockbackUseDefault INSTANCE = new V2_VerticalKnockbackUseDefault();
private V2_VerticalKnockbackUseDefault() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
}
@Override
public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
if (value.getDouble() == 0.4) {
value.set(DoubleOr.Default.USE_DEFAULT);
}
return null;
}
}

View File

@@ -0,0 +1,27 @@
package me.samsuik.sakura.configuration.transformation.world;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import java.util.Map;
import static org.spongepowered.configurate.NodePath.path;
import static org.spongepowered.configurate.transformation.TransformAction.*;
public final class V3_RenameKnockback {
private static final int VERSION = 3;
private static final Map<NodePath, String> RENAME = Map.of(
path("players", "knockback", "vertical-limit-require-ground"), "vertical-knockback-require-ground",
path("players", "knockback", "knockback-horizontal"), "base-knockback"
);
private V3_RenameKnockback() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
ConfigurationTransformation.Builder transformationBuilder = ConfigurationTransformation.builder();
for (Map.Entry<NodePath, String> entry : RENAME.entrySet()) {
transformationBuilder.addAction(entry.getKey(), rename(entry.getValue()));
}
builder.addVersion(VERSION, transformationBuilder.build());
}
}

View File

@@ -0,0 +1,35 @@
package me.samsuik.sakura.configuration.transformation.world;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import java.util.Locale;
import static org.spongepowered.configurate.NodePath.path;
public final class V4_RenameNonStrictMergeLevel implements TransformAction {
private static final int VERSION = 4;
private static final String OLD_LEVEL_NAME = "NON_STRICT";
private static final String NEW_LEVEL_NAME = "LENIENT";
private static final NodePath PATH = path("cannons", "merge-level");
private static final V4_RenameNonStrictMergeLevel INSTANCE = new V4_RenameNonStrictMergeLevel();
private V4_RenameNonStrictMergeLevel() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
}
@Override
public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
String level = value.getString();
if (level != null && OLD_LEVEL_NAME.equals(level.toUpperCase(Locale.ENGLISH))) {
value.set(NEW_LEVEL_NAME);
}
return null;
}
}

View File

@@ -0,0 +1,44 @@
package me.samsuik.sakura.configuration.transformation.world;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import java.util.List;
import static org.spongepowered.configurate.NodePath.path;
public final class V5_CombineLoadChunksOptions implements TransformAction {
private static final int VERSION = 5;
private static final List<String> ENTITY_PATHS = List.of("tnt", "sand");
private static final String OLD_NAME = "loads-chunks";
private static final String NAME = "load-chunks";
private static final NodePath PATH = path("cannons");
private static final V5_CombineLoadChunksOptions INSTANCE = new V5_CombineLoadChunksOptions();
private V5_CombineLoadChunksOptions() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
}
@Override
public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
boolean shouldLoadChunks = false;
for (String entity : ENTITY_PATHS) {
NodePath entityPath = NodePath.path(entity, OLD_NAME);
if (value.hasChild(entityPath)) {
ConfigurationNode node = value.node(entityPath);
shouldLoadChunks |= node.getBoolean();
node.raw(null);
}
}
value.node(NAME).set(shouldLoadChunks);
return null;
}
}

View File

@@ -0,0 +1,30 @@
package me.samsuik.sakura.configuration.transformation.world;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.spongepowered.configurate.ConfigurateException;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.transformation.ConfigurationTransformation;
import org.spongepowered.configurate.transformation.TransformAction;
import static org.spongepowered.configurate.NodePath.path;
public final class V6_FixIncorrectExtraKnockback implements TransformAction {
private static final int VERSION = 6;
private static final NodePath PATH = path("players", "knockback", "sprinting", "extra-knockback");
private static final V6_FixIncorrectExtraKnockback INSTANCE = new V6_FixIncorrectExtraKnockback();
private V6_FixIncorrectExtraKnockback() {}
public static void apply(ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder().addAction(PATH, INSTANCE).build());
}
@Override
public Object @Nullable [] visitPath(NodePath path, ConfigurationNode value) throws ConfigurateException {
if (value.getDouble() == 1.0) {
value.set(0.5);
}
return null;
}
}

View File

@@ -0,0 +1,7 @@
package me.samsuik.sakura.explosion.durable;
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
@ConfigSerializable
public record DurableMaterial(int durability, float resistance) {
}

View File

@@ -0,0 +1,203 @@
package me.samsuik.sakura.local.config;
import com.google.common.collect.Iterables;
import it.unimi.dsi.fastutil.Pair;
import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet;
import me.samsuik.sakura.local.LocalRegion;
import me.samsuik.sakura.local.storage.LocalStorageHandler;
import me.samsuik.sakura.local.storage.LocalValueStorage;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.ChunkPos;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import java.util.*;
import java.util.function.LongConsumer;
public final class LocalConfigManager implements LocalStorageHandler {
private static final int SMALL_REGION_SIZE = 12;
private static final int CONFIG_CACHE_EXPIRATION = 600;
private final Map<LocalRegion, LocalValueStorage> storageMap = new Object2ObjectOpenHashMap<>();
private final List<LocalRegion> largeRegions = new ObjectArrayList<>();
private final Long2ObjectMap<List<LocalRegion>> smallRegions = new Long2ObjectOpenHashMap<>();
private int regionExponent = 0;
private final Long2ObjectMap<Pair<LocalValueConfig, TickExpiry>> chunkConfigCache = new Long2ObjectOpenHashMap<>();
private final Level level;
private long expirationTick = 0L;
public LocalConfigManager(Level level) {
this.level = level;
}
@Override
public synchronized @NonNull Optional<LocalRegion> locate(int x, int z) {
int regionX = x >> this.regionExponent;
int regionZ = z >> this.regionExponent;
long regionPos = ChunkPos.asLong(regionX, regionZ);
List<LocalRegion> regions = this.smallRegions.get(regionPos);
for (LocalRegion region : Iterables.concat(regions, this.largeRegions)) {
if (region.contains(x, z)) {
return Optional.of(region);
}
}
return Optional.empty();
}
@Override
public synchronized @Nullable LocalValueStorage get(@NonNull LocalRegion region) {
return this.storageMap.get(region);
}
@Override
public synchronized boolean has(@NonNull LocalRegion region) {
return this.storageMap.containsKey(region);
}
@Override
public synchronized void put(@NonNull LocalRegion region, @NonNull LocalValueStorage storage) {
this.ensureNotOverlapping(region);
int shift = this.regionExponent;
int regionChunks = regionChunks(region, shift);
if (regionChunks <= SMALL_REGION_SIZE) {
this.forEachRegionChunks(region, pos -> {
this.smallRegions.computeIfAbsent(pos, k -> new ArrayList<>())
.add(region);
});
} else {
this.largeRegions.add(region);
// The region exponent might be too small
if (this.largeRegions.size() % 24 == 0) {
this.resizeRegions();
}
}
this.chunkConfigCache.clear();
this.storageMap.put(region, storage);
}
@Override
public synchronized void remove(@NonNull LocalRegion region) {
this.forEachRegionChunks(region, pos -> {
List<LocalRegion> regions = this.smallRegions.get(pos);
if (regions != null) {
regions.remove(region);
if (regions.isEmpty()) {
this.smallRegions.remove(pos);
}
}
});
this.chunkConfigCache.clear();
this.storageMap.remove(region);
}
private void forEachRegionChunks(LocalRegion region, LongConsumer chunkConsumer) {
int exponent = this.regionExponent;
int minX = region.minX() >> exponent;
int minZ = region.minZ() >> exponent;
int maxX = region.maxX() >> exponent;
int maxZ = region.maxZ() >> exponent;
for (int x = minX; x <= maxX; ++x) {
for (int z = minZ; z <= maxZ; ++z) {
chunkConsumer.accept(ChunkPos.asLong(x, z));
}
}
}
private void resizeRegions() {
List<LocalRegion> regions = this.regions();
int newExponent = this.calculateRegionExponent(regions);
if (newExponent == this.regionExponent) {
return; // nothing has changed
}
this.regionExponent = newExponent;
this.largeRegions.clear();
this.smallRegions.clear();
for (LocalRegion region : regions) {
int regionChunks = regionChunks(region, newExponent);
if (regionChunks <= SMALL_REGION_SIZE) {
this.forEachRegionChunks(region, pos -> {
this.smallRegions.computeIfAbsent(pos, k -> new ArrayList<>())
.add(region);
});
} else {
this.largeRegions.add(region);
}
}
}
private int calculateRegionExponent(List<LocalRegion> regions) {
int regionChunks = 0;
for (LocalRegion region : regions) {
regionChunks += regionChunks(region, 0);
}
regionChunks /= regions.size();
int exponent = 4;
while (true) {
if ((regionChunks >> exponent++) <= SMALL_REGION_SIZE / 2) {
return exponent;
}
}
}
private static int regionChunks(LocalRegion region, int exponent) {
int sizeX = region.maxX() - region.minX() >> exponent;
int sizeZ = region.maxZ() - region.minZ() >> exponent;
return (sizeX + 1) * (sizeZ + 1);
}
@Override
public synchronized @NonNull List<LocalRegion> regions() {
return new ArrayList<>(this.storageMap.keySet());
}
public synchronized LocalValueConfig config(BlockPos position) {
long gameTime = this.level.getGameTime();
long ticks = this.expirationTick - gameTime;
if (ticks >= CONFIG_CACHE_EXPIRATION / 3) {
this.chunkConfigCache.values().removeIf(pair -> pair.value().isExpired(gameTime));
this.expirationTick = gameTime;
}
long chunkKey = ChunkPos.asLong(position.getX() >> 4, position.getZ() >> 4);
Pair<LocalValueConfig, TickExpiry> pair = this.chunkConfigCache.computeIfAbsent(chunkKey, k -> {
return Pair.of(this.createLocalChunkConfig(position), new TickExpiry(gameTime, CONFIG_CACHE_EXPIRATION));
});
pair.value().refresh(gameTime);
return pair.key();
}
private LocalValueConfig createLocalChunkConfig(BlockPos position) {
// uses defaults from the sakura config
LocalValueConfig config = new LocalValueConfig(this.level);
this.locate(position.getX(), position.getZ()).ifPresent(region -> {
config.loadFromStorage(this.storageMap.get(region));
});
return config;
}
private void ensureNotOverlapping(LocalRegion region) {
Set<LocalRegion> nearbyRegions = new ReferenceOpenHashSet<>();
this.forEachRegionChunks(region, pos -> {
nearbyRegions.addAll(this.smallRegions.getOrDefault(pos, List.of()));
});
for (LocalRegion present : Iterables.concat(nearbyRegions, this.largeRegions)) {
if (present != region && present.intersects(region)) {
throw new UnsupportedOperationException("overlapping region (%s, %s)".formatted(present, region));
}
}
}
}

View File

@@ -0,0 +1,47 @@
package me.samsuik.sakura.local.config;
import io.papermc.paper.configuration.WorldConfiguration.Misc.RedstoneImplementation;
import it.unimi.dsi.fastutil.objects.Reference2ObjectOpenHashMap;
import me.samsuik.sakura.explosion.durable.DurableMaterial;
import me.samsuik.sakura.local.LocalValueKeys;
import me.samsuik.sakura.local.storage.LocalValueStorage;
import me.samsuik.sakura.physics.PhysicsVersion;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import org.bukkit.craftbukkit.util.CraftMagicNumbers;
import java.util.Map;
public final class LocalValueConfig {
public Map<Block, DurableMaterial> durableMaterials;
public RedstoneImplementation redstoneImplementation;
public PhysicsVersion physicsVersion;
public boolean consistentRadius;
public boolean redstoneCache;
public int lavaFlowSpeed = -1;
LocalValueConfig(Level level) {
this.durableMaterials = new Reference2ObjectOpenHashMap<>(level.sakuraConfig().cannons.explosion.durableMaterials);
this.redstoneImplementation = level.paperConfig().misc.redstoneImplementation;
this.physicsVersion = level.sakuraConfig().cannons.mechanics.physicsVersion;
this.consistentRadius = level.sakuraConfig().cannons.explosion.consistentRadius;
this.redstoneCache = level.sakuraConfig().technical.redstone.redstoneCache;
}
void loadFromStorage(LocalValueStorage storage) {
storage.get(LocalValueKeys.DURABLE_MATERIALS).ifPresent(materials -> {
materials.forEach((materialType, materialProperties) -> {
Block nmsBlock = CraftMagicNumbers.getBlock(materialType);
DurableMaterial durableMaterial = new DurableMaterial(materialProperties.getKey(), materialProperties.getValue());
this.durableMaterials.put(nmsBlock, durableMaterial);
});
});
storage.get(LocalValueKeys.REDSTONE_IMPLEMENTATION).ifPresent(implementation -> {
this.redstoneImplementation = RedstoneImplementation.values()[implementation.ordinal()];
});
this.physicsVersion = storage.getOrDefault(LocalValueKeys.PHYSICS_VERSION, this.physicsVersion);
this.consistentRadius = storage.getOrDefault(LocalValueKeys.CONSISTENT_EXPLOSION_RADIUS, this.consistentRadius);
this.redstoneCache = storage.getOrDefault(LocalValueKeys.REDSTONE_CACHE, this.redstoneCache);
this.lavaFlowSpeed = storage.getOrDefault(LocalValueKeys.LAVA_FLOW_SPEED, this.lavaFlowSpeed);
}
}

View File

@@ -0,0 +1,64 @@
package me.samsuik.sakura.player.combat;
import net.minecraft.core.Holder;
import net.minecraft.core.HolderLookup;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceKey;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.damagesource.DamageSource;
import net.minecraft.world.entity.EquipmentSlot;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.ai.attributes.AttributeModifier;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.ItemAttributeModifiers;
import net.minecraft.world.item.enchantment.Enchantment;
import net.minecraft.world.item.enchantment.Enchantments;
import net.minecraft.world.item.enchantment.ItemEnchantments;
import org.apache.commons.lang3.mutable.MutableFloat;
import java.util.OptionalDouble;
public final class CombatUtil {
public static double getLegacyAttackDifference(ItemStack itemstack) {
ItemAttributeModifiers defaultModifiers = itemstack.getItem().components().get(DataComponents.ATTRIBUTE_MODIFIERS);
if (defaultModifiers != null && !defaultModifiers.modifiers().isEmpty()) { // exists
double baseAttack = 0.0;
for (ItemAttributeModifiers.Entry entry : defaultModifiers.modifiers()) {
if (!entry.slot().test(EquipmentSlot.MAINHAND) || !entry.attribute().is(Attributes.ATTACK_DAMAGE))
continue;
if (entry.modifier().operation() != AttributeModifier.Operation.ADD_VALUE)
return 0;
baseAttack += entry.modifier().amount();
}
OptionalDouble legacyAttack = LegacyDamageMapping.itemAttackDamage(itemstack.getItem());
if (baseAttack != 0.0 && legacyAttack.isPresent()) {
return legacyAttack.getAsDouble() - baseAttack;
}
}
return 0;
}
public static float calculateLegacySharpnessDamage(LivingEntity entity, ItemStack itemstack, DamageSource damageSource) {
Holder<Enchantment> enchantment = getEnchantmentHolder(Enchantments.SHARPNESS);
ItemEnchantments itemEnchantments = itemstack.getEnchantments();
int enchantmentLevel = itemEnchantments.getLevel(enchantment);
MutableFloat damage = new MutableFloat();
if (entity.level() instanceof ServerLevel level) {
enchantment.value().modifyDamage(level, enchantmentLevel, itemstack, entity, damageSource, damage);
}
// legacy - modern
return enchantmentLevel * 1.25F - damage.getValue();
}
private static Holder<Enchantment> getEnchantmentHolder(ResourceKey<Enchantment> enchantmentKey) {
RegistryAccess registryAccess = MinecraftServer.getServer().registryAccess();
HolderLookup.RegistryLookup<Enchantment> enchantments = registryAccess.lookupOrThrow(Registries.ENCHANTMENT);
return enchantments.getOrThrow(enchantmentKey);
}
}

View File

@@ -0,0 +1,64 @@
package me.samsuik.sakura.player.combat;
import net.minecraft.core.component.DataComponents;
import net.minecraft.world.InteractionHand;
import net.minecraft.world.InteractionResult;
import net.minecraft.world.effect.MobEffectInstance;
import net.minecraft.world.effect.MobEffects;
import net.minecraft.world.entity.LivingEntity;
import net.minecraft.world.entity.player.Player;
import net.minecraft.world.item.Item;
import net.minecraft.world.item.ItemStack;
import net.minecraft.world.item.component.Consumable;
import net.minecraft.world.item.component.Consumables;
import net.minecraft.world.item.consume_effects.ApplyStatusEffectsConsumeEffect;
import net.minecraft.world.level.Level;
import org.jspecify.annotations.NullMarked;
import java.util.List;
import java.util.Optional;
@NullMarked
@SuppressWarnings("OptionalAssignedToNull")
public final class CustomGoldenApple extends Item {
private static final Consumable LEGACY_ENCHANTED_GOLDEN_APPLE = Consumables.defaultFood()
.onConsume(
new ApplyStatusEffectsConsumeEffect(
List.of(
new MobEffectInstance(MobEffects.REGENERATION, 600, 4),
new MobEffectInstance(MobEffects.DAMAGE_RESISTANCE, 6000, 0),
new MobEffectInstance(MobEffects.FIRE_RESISTANCE, 6000, 0),
new MobEffectInstance(MobEffects.ABSORPTION, 2400, 0)
)
)
)
.build();
public CustomGoldenApple(Properties settings) {
super(settings);
}
@Override
public InteractionResult use(Level level, Player player, InteractionHand hand) {
ItemStack stack = player.getItemInHand(hand);
if (this.itemHasConsumableComponent(stack, level)) {
return super.use(level, player, hand);
} else {
return LEGACY_ENCHANTED_GOLDEN_APPLE.startConsuming(player, stack, hand);
}
}
@Override
public ItemStack finishUsingItem(ItemStack stack, Level level, LivingEntity entity) {
if (this.itemHasConsumableComponent(stack, level)) {
return super.finishUsingItem(stack, level, entity);
} else {
return LEGACY_ENCHANTED_GOLDEN_APPLE.onConsume(level, entity, stack);
}
}
private boolean itemHasConsumableComponent(ItemStack stack, Level level) {
Optional<?> consumable = stack.getComponentsPatch().get(DataComponents.CONSUMABLE);
return consumable != null || !level.sakuraConfig().players.combat.oldEnchantedGoldenApple;
}
}

View File

@@ -0,0 +1,64 @@
package me.samsuik.sakura.player.combat;
import it.unimi.dsi.fastutil.objects.Reference2DoubleMap;
import it.unimi.dsi.fastutil.objects.Reference2DoubleOpenHashMap;
import net.minecraft.core.component.DataComponents;
import net.minecraft.core.registries.BuiltInRegistries;
import net.minecraft.world.entity.ai.attributes.Attributes;
import net.minecraft.world.item.*;
import net.minecraft.world.item.component.ItemAttributeModifiers;
import java.util.OptionalDouble;
public final class LegacyDamageMapping {
private static final Reference2DoubleMap<Item> LEGACY_ITEM_DAMAGE_MAP = new Reference2DoubleOpenHashMap<>();
public static OptionalDouble itemAttackDamage(Item item) {
double result = LEGACY_ITEM_DAMAGE_MAP.getDouble(item);
return result == Double.MIN_VALUE ? OptionalDouble.empty() : OptionalDouble.of(result);
}
private static double adjustDamageForItem(Item item, double attackDamage) {
return switch (item) {
case SwordItem i -> 1.0;
case PickaxeItem i -> 1.0;
case ShovelItem i -> -0.5;
case HoeItem i -> -attackDamage;
case null, default -> 0.0;
};
}
static {
LEGACY_ITEM_DAMAGE_MAP.defaultReturnValue(Double.MIN_VALUE);
// tool material is no longer exposed
LEGACY_ITEM_DAMAGE_MAP.put(Items.WOODEN_AXE, 3.0);
LEGACY_ITEM_DAMAGE_MAP.put(Items.GOLDEN_AXE, 3.0);
LEGACY_ITEM_DAMAGE_MAP.put(Items.STONE_AXE, 4.0);
LEGACY_ITEM_DAMAGE_MAP.put(Items.IRON_AXE, 5.0);
LEGACY_ITEM_DAMAGE_MAP.put(Items.DIAMOND_AXE, 6.0);
LEGACY_ITEM_DAMAGE_MAP.put(Items.NETHERITE_AXE, 7.0);
for (Item item : BuiltInRegistries.ITEM) {
ItemAttributeModifiers modifiers = item.components().get(DataComponents.ATTRIBUTE_MODIFIERS);
if (modifiers == null || LEGACY_ITEM_DAMAGE_MAP.containsKey(item)) {
continue;
}
assert item instanceof AxeItem : "missing axe mapping";
double attackDamage = modifiers.modifiers().stream()
.filter(e -> e.attribute().is(Attributes.ATTACK_DAMAGE))
.mapToDouble(e -> e.modifier().amount())
.sum();
if (attackDamage > 0.0) {
double adjustment = adjustDamageForItem(item, attackDamage);
LEGACY_ITEM_DAMAGE_MAP.put(item, attackDamage + adjustment);
}
}
}
private LegacyDamageMapping() {}
}

View File

@@ -0,0 +1,36 @@
package me.samsuik.sakura.tps;
import me.samsuik.sakura.configuration.GlobalConfiguration;
import me.samsuik.sakura.tps.graph.GraphComponents;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.TextComponent;
import net.kyori.adventure.text.format.TextColor;
import org.jspecify.annotations.NullMarked;
@NullMarked
public record ServerTickInformation(long identifier, double tps, double averageTick, long longestTick, float targetTickRate, int chunks, int entities) {
public static final ServerTickInformation FILLER = new ServerTickInformation(0, 0.0, 0.0, 0, 0.0f, 0, 0);
public TextColor colour() {
float lag = (float) this.tps / this.targetTickRate;
return GraphComponents.colour(lag);
}
public Component hoverComponent(TextColor colour) {
TextComponent.Builder builder = Component.text();
builder.append(Component.text("TPS: ")
.append(Component.text("%.1f".formatted(this.tps), colour)));
builder.appendNewline();
builder.append(Component.text("MSPT: ")
.append(Component.text("%.1f".formatted(this.averageTick), colour))
.append(Component.text("/"))
.append(Component.text(this.longestTick, colour)));
if (GlobalConfiguration.get().messages.tpsShowEntityAndChunkCount) {
builder.appendNewline();
builder.append(Component.text("Entities: " + this.entities));
builder.appendNewline();
builder.append(Component.text("Chunks: " + this.chunks));
}
return builder.build();
}
}

View File

@@ -0,0 +1,71 @@
package me.samsuik.sakura.tps;
import com.google.common.collect.ImmutableList;
import it.unimi.dsi.fastutil.longs.LongArrayList;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.minecraft.server.MinecraftServer;
import net.minecraft.server.level.ServerLevel;
import org.jspecify.annotations.NullMarked;
import java.util.Collection;
import java.util.List;
@NullMarked
public final class TickInformationCollector {
private static final int TEN_MINUTES = 10 * 60;
private final ObjectArrayList<ServerTickInformation> collectedInformation = new ObjectArrayList<>();
private final LongArrayList tickSamples = new LongArrayList();
private long identifier = 0;
public ServerTickInformation latestTickInformation() {
return this.collectedInformation.getLast();
}
public void levelData(Collection<ServerLevel> levels, double tps) {
int chunks = 0;
int entities = 0;
for (ServerLevel level : levels) {
chunks += level.chunkSource.getFullChunksCount();
entities += level.entityTickList.entities.size();
}
double averageTick = this.tickSamples.longStream()
.average()
.orElse(0.0);
long longestTick = this.tickSamples.longStream()
.max()
.orElse(0);
float targetTickRate = MinecraftServer.getServer().tickRateManager().tickrate();
ServerTickInformation tickInformation = new ServerTickInformation(
this.identifier++, tps, averageTick, longestTick, targetTickRate, chunks, entities
);
this.collectedInformation.add(tickInformation);
this.tickSamples.clear();
if (this.collectedInformation.size() > TEN_MINUTES) {
this.collectedInformation.subList(0, 60).clear();
}
}
public void tickDuration(long timeTaken) {
this.tickSamples.add(timeTaken);
}
public ImmutableList<ServerTickInformation> collect(long from, long to) {
List<ServerTickInformation> collected = new ObjectArrayList<>();
for (ServerTickInformation tickInformation : this.collectedInformation.reversed()) {
if (tickInformation.identifier() >= from && tickInformation.identifier() < to) {
collected.add(tickInformation);
}
}
long ahead = to - this.identifier;
long missing = to - from - collected.size();
for (int i = 0; i < missing; ++i) {
int ind = (i < ahead) ? 0 : collected.size();
collected.add(ind, ServerTickInformation.FILLER);
}
return ImmutableList.copyOf(collected);
}
}

View File

@@ -0,0 +1,36 @@
package me.samsuik.sakura.tps.graph;
import com.google.common.collect.ImmutableList;
import net.kyori.adventure.text.Component;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@NullMarked
public final class BuiltComponentCanvas {
private final List<Component> components;
BuiltComponentCanvas(List<Component> components) {
this.components = components;
}
public void appendLeft(Component component) {
this.components.replaceAll(component::append);
}
public void appendRight(Component component) {
this.components.replaceAll(row -> row.append(component));
}
public void header(Component component) {
this.components.addFirst(component);
}
public void footer(Component component) {
this.components.add(component);
}
public ImmutableList<Component> components() {
return ImmutableList.copyOf(this.components);
}
}

View File

@@ -0,0 +1,63 @@
package me.samsuik.sakura.tps.graph;
import com.google.common.base.Preconditions;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.JoinConfiguration;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@NullMarked
public final class ComponentCanvas {
private final int width;
private final int height;
private final Component[][] components;
public ComponentCanvas(int width, int height) {
this.width = width;
this.height = height;
// [x, y] is flipped as it makes converting the components into a list easier
this.components = new Component[height][width];
}
public void flip() {
for (int y = 0; y < this.height; ++y) {
if (y >= this.height / 2) {
Component[] row = this.components[y];
int relocatingRow = this.height - 1 - y;
this.components[y] = this.components[relocatingRow];
this.components[relocatingRow] = row;
}
}
}
public void fill(Component component) {
for (int x = 0; x < this.width; ++x) {
for (int y = 0; y < this.height; ++y) {
this.set(x, y, component);
}
}
}
public Component get(int x, int y) {
Component component = this.components[y][x];
return Preconditions.checkNotNull(component, "missing component at x:{} y:{}", x, y);
}
public void set(int x, int y, Component component) {
this.components[y][x] = component;
}
public BuiltComponentCanvas build() {
return new BuiltComponentCanvas(this.joinComponents());
}
private List<Component> joinComponents() {
List<Component> componentList = new ObjectArrayList<>(this.height);
for (Component[] row : this.components) {
componentList.add(Component.join(JoinConfiguration.noSeparators(), row));
}
return componentList;
}
}

View File

@@ -0,0 +1,102 @@
package me.samsuik.sakura.tps.graph;
import me.samsuik.sakura.tps.ServerTickInformation;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@NullMarked
public final class DetailedTPSGraph extends TPSGraph {
public DetailedTPSGraph(int width, int height, double scale, List<ServerTickInformation> tickInformation) {
super(width, height, scale, tickInformation);
}
@Override
public BuiltComponentCanvas plot() {
ComponentCanvas canvas = new ComponentCanvas(this.width, this.height);
canvas.fill(GraphComponents.BACKGROUND);
this.basicOutline(canvas);
this.prettifyOutline(canvas);
this.addColourAndHoverInformation(canvas);
canvas.flip();
return canvas.build();
}
private void basicOutline(ComponentCanvas canvas) {
for (int x = 0; x < this.width; ++x) {
int row = this.rowFromColumn(x);
int nextRow = this.rowFromColumn(x + 1);
int minRow = Math.min(row, nextRow);
int maxRow = Math.max(row, nextRow);
if (maxRow - minRow >= 2) {
canvas.set(x, minRow, GraphComponents.TOP_DOTTED_LINE);
canvas.set(x, maxRow, GraphComponents.BOTTOM_DOTTED_LINE);
for (int y = minRow + 1; y < maxRow; ++y) {
canvas.set(x, y, GraphComponents.VERTICAL_LINE);
}
} else {
canvas.set(x, row, GraphComponents.HORIZONTAL_LINE);
}
}
}
private void prettifyOutline(ComponentCanvas canvas) {
for (int x = 0; x < this.width; ++x) {
int row = this.rowFromColumn(x);
int nextRow = this.rowFromColumn(x + 1);
int prevRow = this.rowFromColumn(x - 1);
int minRow = Math.min(row, nextRow);
int maxRow = Math.max(row, nextRow);
if (maxRow - minRow >= 2) {
this.prettifyVerticalOutline(canvas, x, row, nextRow, prevRow, minRow, maxRow);
} else {
this.prettifySlopes(canvas, x, row, nextRow, prevRow);
}
}
}
private void prettifyVerticalOutline(ComponentCanvas canvas, int x, int row, int nextRow, int prevRow, int minRow, int maxRow) {
if (minRow == nextRow) {
canvas.set(x, minRow, GraphComponents.CONE_BOTTOM_LEFT);
} else if (prevRow <= minRow) {
canvas.set(x, minRow, GraphComponents.CONE_BOTTOM_RIGHT);
}
if (prevRow == row + 1 && nextRow < row) {
canvas.set(x, maxRow, GraphComponents.CONE_TOP_RIGHT);
}
if (maxRow == row && Math.abs(nextRow - maxRow) > 1 && Math.abs(prevRow - maxRow) > 1 && prevRow < maxRow) {
canvas.set(x - 1, maxRow, GraphComponents.CONE_TOP_LEFT);
canvas.set(x, maxRow, GraphComponents.CONE_TOP_RIGHT);
}
if (minRow == row && Math.abs(nextRow - minRow) > 1 && Math.abs(prevRow - minRow) > 1 && prevRow > minRow) {
canvas.set(x - 1, minRow, GraphComponents.CONE_BOTTOM_LEFT);
canvas.set(x, minRow, GraphComponents.CONE_BOTTOM_RIGHT);
}
}
private void prettifySlopes(ComponentCanvas canvas, int x, int row, int nextRow, int prevRow) {
int slopeDirection = nextRow - prevRow;
int slopeChange = Math.abs(slopeDirection);
if (slopeChange >= 2 && Math.max(nextRow, prevRow) == row + 1) {
canvas.set(x, row, slopeDirection < 0 ? GraphComponents.TL_TO_BR : GraphComponents.BL_TO_TR);
} else if (Math.abs(row - nextRow) == 1 || slopeDirection == 0) {
if (row < nextRow) {
canvas.set(x, row, GraphComponents.TOP_DOTTED_LINE);
} else if (row > nextRow) {
canvas.set(x, row, GraphComponents.BOTTOM_DOTTED_LINE);
}
} else if (Math.abs(row - prevRow) == 1) {
if (prevRow > row) {
canvas.set(x, row, GraphComponents.TOP_DOTTED_LINE);
} else if (prevRow < row) {
canvas.set(x, row, GraphComponents.BOTTOM_DOTTED_LINE);
}
}
}
}

View File

@@ -0,0 +1,42 @@
package me.samsuik.sakura.tps.graph;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.Style;
import net.kyori.adventure.text.format.TextColor;
import net.kyori.adventure.text.format.TextDecoration;
import java.util.List;
public final class GraphComponents {
private static final Style STRIKE_THROUGH_STYLE = Style.style(TextDecoration.STRIKETHROUGH);
private static final Style REMOVE_STRIKE_THROUGH_STYLE = Style.style(TextDecoration.STRIKETHROUGH.withState(false));
public static final Component BACKGROUND = Component.text("::");
public static final Component HORIZONTAL_LINE = Component.text(" ", STRIKE_THROUGH_STYLE);
public static final Component VERTICAL_LINE = Component.text("||");
public static final Component TOP_DOTTED_LINE = Component.text("''");
public static final Component BOTTOM_DOTTED_LINE = Component.text("..");
public static final Component BL_TO_TR = Component.text(".", STRIKE_THROUGH_STYLE).append(Component.text("'", REMOVE_STRIKE_THROUGH_STYLE));
public static final Component TL_TO_BR = Component.text("'").append(Component.text(".", STRIKE_THROUGH_STYLE));
public static final Component CONE_TOP_LEFT = Component.text(".!");
public static final Component CONE_TOP_RIGHT = Component.text("!.");
public static final Component CONE_BOTTOM_LEFT = Component.text("'!");
public static final Component CONE_BOTTOM_RIGHT = Component.text("!'");
private static final List<TextColor> COLOURS = List.of(
NamedTextColor.GREEN, NamedTextColor.YELLOW, NamedTextColor.GOLD,
NamedTextColor.RED, NamedTextColor.DARK_GRAY, TextColor.color(40, 40, 40)
);
public static TextColor colour(float num) {
float segment = 1.0f / COLOURS.size();
float a = (1.0f - num) / segment;
float t = a % 1.0f;
int startIndex = Math.clamp((int) a, 0, COLOURS.size() - 2);
int endIndex = startIndex + 1;
TextColor startColour = COLOURS.get(startIndex);
TextColor endColour = COLOURS.get(endIndex);
return TextColor.lerp(t, startColour, endColour);
}
}

View File

@@ -0,0 +1,60 @@
package me.samsuik.sakura.tps.graph;
import com.google.common.base.Preconditions;
import me.samsuik.sakura.tps.ServerTickInformation;
import net.kyori.adventure.text.Component;
import net.kyori.adventure.text.event.HoverEvent;
import net.kyori.adventure.text.format.NamedTextColor;
import net.kyori.adventure.text.format.TextColor;
import net.minecraft.util.Mth;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@NullMarked
public abstract class TPSGraph {
protected final List<ServerTickInformation> tickInformation;
protected final int width;
protected final int height;
protected final double scale;
public TPSGraph(int width, int height, double scale, List<ServerTickInformation> tickInformation) {
Preconditions.checkArgument(tickInformation.size() == width);
this.width = width;
this.height = height;
this.scale = scale;
this.tickInformation = tickInformation;
}
public abstract BuiltComponentCanvas plot();
protected final int rowFromColumn(int x) {
int clamped = Math.clamp(x, 0, this.width - 1);
ServerTickInformation tickInformation = this.tickInformation.get(clamped);
return this.rowFromTPS(tickInformation.tps());
}
protected final int rowFromTPS(double tps) {
int row = Mth.floor((tps / 3) * this.scale);
return Mth.clamp(row, 0, this.height - 1);
}
protected final void addColourAndHoverInformation(ComponentCanvas canvas) {
for (int x = 0; x < this.width; ++x) {
ServerTickInformation tickInformation = this.tickInformation.get(x);
TextColor colourFromTPS = tickInformation.colour();
Component hoverComponent = tickInformation.hoverComponent(colourFromTPS);
HoverEvent<Component> hoverEvent = HoverEvent.showText(hoverComponent);
for (int y = 0; y < this.height; ++y) {
Component component = canvas.get(x, y);
if (component == GraphComponents.BACKGROUND) {
component = component.color(NamedTextColor.BLACK);
} else {
component = component.color(colourFromTPS);
}
canvas.set(x, y, component.hoverEvent(hoverEvent));
}
}
}
}

View File

@@ -0,0 +1,60 @@
package me.samsuik.sakura.utils;
import com.google.common.collect.AbstractIterator;
import net.minecraft.core.BlockPos;
import net.minecraft.core.BlockPos.MutableBlockPos;
import net.minecraft.util.Mth;
import net.minecraft.world.phys.AABB;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public final class BlockPosIterator extends AbstractIterator<BlockPos> {
private final int startX;
private final int startY;
private final int startZ;
private final int endX;
private final int endY;
private final int endZ;
private @Nullable MutableBlockPos pos = null;
public static Iterable<BlockPos> iterable(AABB bb) {
return () -> new BlockPosIterator(bb);
}
public BlockPosIterator(AABB bb) {
this.startX = Mth.floor(bb.minX);
this.startY = Mth.floor(bb.minY);
this.startZ = Mth.floor(bb.minZ);
this.endX = Mth.floor(bb.maxX);
this.endY = Mth.floor(bb.maxY);
this.endZ = Mth.floor(bb.maxZ);
}
@Override
protected BlockPos computeNext() {
MutableBlockPos pos = this.pos;
if (pos == null) {
return this.pos = new MutableBlockPos(this.startX, this.startY, this.startZ);
} else {
int x = pos.getX();
int y = pos.getY();
int z = pos.getZ();
if (y < this.endY) {
y += 1;
} else if (x < this.endX) {
x += 1;
y = this.startY;
} else if (z < this.endZ) {
z += 1;
x = this.startX;
} else {
return this.endOfData();
}
pos.set(x, y, z);
return pos;
}
}
}

View File

@@ -0,0 +1,24 @@
package me.samsuik.sakura.utils;
import com.google.common.base.Preconditions;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class TickExpiry {
private long tick;
private final int expiration;
public TickExpiry(long tick, int expiration) {
Preconditions.checkArgument(expiration > 0, "expiration must be greater than 0");
this.tick = tick;
this.expiration = expiration;
}
public void refresh(long tick) {
this.tick = tick;
}
public boolean isExpired(long tick) {
return this.tick >= tick - this.expiration;
}
}

View File

@@ -0,0 +1,54 @@
package me.samsuik.sakura.utils.collections;
import it.unimi.dsi.fastutil.HashCommon;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.function.ToIntFunction;
public final class FixedSizeCustomObjectTable<T> {
private final ToIntFunction<T> keyFunction;
private final T[] contents;
private final int mask;
public FixedSizeCustomObjectTable(int size, @NotNull ToIntFunction<T> keyFunction) {
if (size < 0) {
throw new IllegalArgumentException("Table size cannot be negative");
} else {
int n = HashCommon.nextPowerOfTwo(size - 1);
this.keyFunction = keyFunction;
this.contents = (T[]) new Object[n];
this.mask = (n - 1);
}
}
private int key(T value) {
return this.keyFunction.applyAsInt(value);
}
public @Nullable T get(T value) {
return this.get(this.key(value));
}
public @Nullable T get(int key) {
return this.contents[key & this.mask];
}
public void write(int key, T value) {
this.contents[key & this.mask] = value;
}
public @Nullable T getAndWrite(T value) {
int key = this.key(value);
T found = this.get(key);
this.write(key, value);
return found;
}
public void clear() {
int size = this.contents.length;
for (int i = 0; i < size; ++i) {
this.contents[i] = null;
}
}
}

View File

@@ -0,0 +1,49 @@
package me.samsuik.sakura.utils.collections;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import java.util.Arrays;
import java.util.Comparator;
public final class OrderedComparatorList<T> extends ObjectArrayList<T> {
private final Comparator<T> comparator;
private boolean binarySearch = true;
public OrderedComparatorList(int capacity, Comparator<T> comparator) {
super(capacity);
this.comparator = Comparator.nullsLast(comparator);
}
public OrderedComparatorList(Comparator<T> comparator) {
this(DEFAULT_INITIAL_CAPACITY, comparator);
}
private void validateBounds(int index, T t, boolean up) {
if (index != 0 && this.comparator.compare(get(index - 1), t) > 0) {
this.binarySearch = false;
} else if (up && index < size() - 1 && this.comparator.compare(get(index + 1), t) < 0) {
this.binarySearch = false;
}
}
@Override
public boolean add(T t) {
this.validateBounds(size(), t, false);
return super.add(t);
}
@Override
public void add(int index, T t) {
this.validateBounds(index, t, true);
super.add(index, t);
}
@Override
public int indexOf(final Object k) {
if (this.binarySearch) {
return Math.max(Arrays.binarySearch(this.a, (T) k, this.comparator), -1);
} else {
return super.indexOf(k);
}
}
}

View File

@@ -0,0 +1,32 @@
package me.samsuik.sakura.utils.collections;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import it.unimi.dsi.fastutil.objects.ObjectCollection;
import net.minecraft.server.level.ChunkMap;
public final class TrackedEntityChunkMap extends Int2ObjectOpenHashMap<ChunkMap.TrackedEntity> {
private final ObjectArrayList<ChunkMap.TrackedEntity> entityList = new UnorderedIndexedList<>();
@Override
public ChunkMap.TrackedEntity put(int k, ChunkMap.TrackedEntity trackedEntity) {
ChunkMap.TrackedEntity tracked = super.put(k, trackedEntity);
if (tracked != null) {
this.entityList.remove(trackedEntity);
}
this.entityList.add(trackedEntity);
return tracked;
}
@Override
public ChunkMap.TrackedEntity remove(int k) {
ChunkMap.TrackedEntity tracked = super.remove(k);
this.entityList.remove(tracked);
return tracked;
}
@Override
public ObjectCollection<ChunkMap.TrackedEntity> values() {
return this.entityList;
}
}

View File

@@ -0,0 +1,61 @@
package me.samsuik.sakura.utils.collections;
import it.unimi.dsi.fastutil.ints.Int2IntOpenHashMap;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
public final class UnorderedIndexedList<T> extends ObjectArrayList<T> {
private final Int2IntOpenHashMap elementToIndex;
public UnorderedIndexedList() {
this(DEFAULT_INITIAL_CAPACITY);
}
public UnorderedIndexedList(int capacity) {
super(capacity);
this.elementToIndex = new Int2IntOpenHashMap();
this.elementToIndex.defaultReturnValue(-1);
}
@Override
public boolean add(final T t) {
this.elementToIndex.put(t.hashCode(), size());
return super.add(t);
}
@Override
public T remove(final int index) {
final int tail = size() - 1;
final T at = a[index];
if (index != tail) {
final T tailObj = a[tail];
if (tailObj != null)
this.elementToIndex.put(tailObj.hashCode(), index);
this.a[index] = tailObj;
}
if (at != null)
this.elementToIndex.remove(at.hashCode());
this.a[tail] = null;
this.size = tail;
return at;
}
@Override
public void clear() {
this.elementToIndex.clear();
super.clear();
}
@Override
public int indexOf(final Object k) {
if (k == null) return -1;
// entities uses their id as a hashcode
return this.elementToIndex.get(k.hashCode());
}
@Override
public void add(final int index, final T t) {
throw new UnsupportedOperationException();
}
}