9
0
mirror of https://github.com/Samsuik/Sakura.git synced 2025-12-30 04:09:09 +00:00

Rewrite local configuration api and expand "physics-version"

This commit is contained in:
Samsuik
2025-09-23 16:05:33 +01:00
parent 3246217d29
commit 65bc3daa96
63 changed files with 2001 additions and 1244 deletions

View File

@@ -1,7 +1,7 @@
package me.samsuik.sakura.command;
import me.samsuik.sakura.command.subcommands.*;
import me.samsuik.sakura.command.subcommands.debug.DebugLocalRegions;
import me.samsuik.sakura.command.subcommands.debug.DebugLocalConfiguration;
import me.samsuik.sakura.command.subcommands.debug.DebugRedstoneCache;
import me.samsuik.sakura.player.visibility.VisibilityTypes;
import net.minecraft.server.MinecraftServer;
@@ -30,7 +30,7 @@ public final class SakuraCommands {
// "sakura" isn't a subcommand
COMMANDS.put("sakura", new SakuraCommand("sakura"));
DEBUG_COMMANDS.add(new DebugRedstoneCache("redstone-cache"));
DEBUG_COMMANDS.add(new DebugLocalRegions("local-regions"));
DEBUG_COMMANDS.add(new DebugLocalConfiguration("local-regions"));
}
public static void registerCommands(MinecraftServer server) {

View File

@@ -0,0 +1,56 @@
package me.samsuik.sakura.command.subcommands.debug;
import me.samsuik.sakura.command.PlayerOnlySubCommand;
import me.samsuik.sakura.configuration.local.ConfigurationContainer;
import me.samsuik.sakura.configuration.local.LocalConfigurationAccessor;
import org.bukkit.Location;
import org.bukkit.entity.Player;
import org.bukkit.util.BoundingBox;
import org.jspecify.annotations.NullMarked;
import java.util.List;
@NullMarked
public final class DebugLocalConfiguration extends PlayerOnlySubCommand {
private static final int DEFAULT_REGION_SIZE = 16;
public DebugLocalConfiguration(final String name) {
super(name);
}
@Override
public void execute(final Player player, final String[] args) {
final Location location = player.getLocation();
final LocalConfigurationAccessor localConfigurationAccessor = location.getWorld().localConfig();
final BoundingBox boundingBox = localConfigurationAccessor.getAreas(location).stream()
.findAny()
.orElse(null);
if (boundingBox != null) {
player.sendRichMessage("<green>You are currently inside a area with a set local-config.");
player.sendRichMessage("<green> - %.0f %.0f %.0f".formatted(boundingBox.getMinX(), boundingBox.getMinY(), boundingBox.getMinZ()));
player.sendRichMessage("<green> - %.0f %.0f %.0f".formatted(boundingBox.getMaxX(), boundingBox.getMaxY(), boundingBox.getMaxX()));
}
if (args.length == 0) {
return;
}
if ("delete".equalsIgnoreCase(args[0]) && boundingBox != null) {
localConfigurationAccessor.remove(boundingBox);
player.sendRichMessage("<green>Removed area");
}
if ("create".equalsIgnoreCase(args[0]) && args.length > 1) {
final int size = parseInt(args, 1).orElse(DEFAULT_REGION_SIZE);
final BoundingBox area = BoundingBox.of(location, size, size, size);
localConfigurationAccessor.set(area, ConfigurationContainer.sealedContainer());
player.sendRichMessage("<green>Created a new area with size " + size);
}
}
@Override
public void tabComplete(final List<String> completions, final String[] args) throws IllegalArgumentException {
completions.addAll(List.of("create", "delete"));
}
}

View File

@@ -1,62 +0,0 @@
package me.samsuik.sakura.command.subcommands.debug;
import me.samsuik.sakura.command.PlayerOnlySubCommand;
import me.samsuik.sakura.local.LocalRegion;
import me.samsuik.sakura.local.storage.LocalStorageHandler;
import me.samsuik.sakura.local.storage.LocalValueStorage;
import org.bukkit.Location;
import org.bukkit.World;
import org.bukkit.entity.Player;
import org.jspecify.annotations.NullMarked;
import java.util.List;
import java.util.Optional;
@NullMarked
public final class DebugLocalRegions extends PlayerOnlySubCommand {
private static final int DEFAULT_REGION_SIZE = 16;
public DebugLocalRegions(String name) {
super(name);
}
@Override
public void execute(Player player, String[] args) {
if (args.length == 0) {
return;
}
final Location location = player.getLocation();
final World world = location.getWorld();
final LocalStorageHandler storageHandler = world.getStorageHandler();
final int blockX = location.getBlockX();
final int blockZ = location.getBlockZ();
final Optional<LocalRegion> currentRegion = storageHandler.locate(blockX, blockZ);
if ("create".equalsIgnoreCase(args[0]) && args.length > 1) {
final int size = parseInt(args, 1).orElse(DEFAULT_REGION_SIZE);
final LocalRegion region = LocalRegion.at(blockX, blockZ, size);
if (currentRegion.isPresent()) {
player.sendRichMessage("<red>regions cannot overlap");
} else {
storageHandler.put(region, new LocalValueStorage());
}
}
if ("get".equalsIgnoreCase(args[0])) {
player.sendRichMessage("<red>" + (currentRegion.isPresent() ? currentRegion.get() : "not inside of a region"));
}
if (currentRegion.isPresent()) {
final LocalRegion region = currentRegion.get();
if ("delete".equalsIgnoreCase(args[0])) {
storageHandler.remove(region);
}
}
}
@Override
public void tabComplete(List<String> list, String[] args) throws IllegalArgumentException {
list.addAll(List.of("create", "get", "delete"));
}
}

View File

@@ -4,6 +4,9 @@ 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 net.kyori.adventure.text.Component;
import net.kyori.adventure.text.minimessage.MiniMessage;
import net.kyori.adventure.text.minimessage.tag.resolver.Placeholder;
import org.bukkit.Material;
import org.slf4j.Logger;
import org.spongepowered.configurate.objectmapping.meta.Comment;
@@ -32,6 +35,14 @@ public final class GlobalConfiguration 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 Component durableBlockInteractionComponent(final int remaining, final int durability) {
return MiniMessage.miniMessage().deserialize(
GlobalConfiguration.get().messages.durableBlockInteraction,
Placeholder.unparsed("remaining", String.valueOf(remaining)),
Placeholder.unparsed("durability", String.valueOf(durability))
);
}
}
public Fps fps;

View File

@@ -14,7 +14,9 @@ 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.serializer.MinecraftMechanicsTargetSerializer;
import me.samsuik.sakura.configuration.transformation.ConfigurationTransformations;
import me.samsuik.sakura.mechanics.MinecraftMechanicsTarget;
import net.minecraft.core.RegistryAccess;
import net.minecraft.core.registries.Registries;
import net.minecraft.resources.ResourceLocation;
@@ -141,6 +143,7 @@ public final class SakuraConfigurations extends Configurations<GlobalConfigurati
.defaultOptions(options -> options
.header(contextMap.require(WORLD_NAME).equals(WORLD_DEFAULTS) ? WORLD_DEFAULTS_HEADER : WORLD_HEADER.apply(contextMap))
.serializers(serializers -> serializers
.register(new TypeToken<MinecraftMechanicsTarget>() {}, new MinecraftMechanicsTargetSerializer())
.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())

View File

@@ -10,7 +10,7 @@ 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 me.samsuik.sakura.mechanics.MinecraftMechanicsTarget;
import net.minecraft.Util;
import net.minecraft.resources.ResourceLocation;
import net.minecraft.world.entity.EntityType;
@@ -28,7 +28,7 @@ import java.util.Set;
public final class WorldConfiguration extends ConfigurationPart {
private static final Logger LOGGER = LogUtils.getClassLogger();
static final int CURRENT_VERSION = 10; // (when you change the version, change the comment, so it conflicts on rebases): rename filter bad nbt from spawn eggs
static final int CURRENT_VERSION = 11; // (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) {
@@ -115,7 +115,7 @@ public final class WorldConfiguration extends ConfigurationPart {
public TNTSpread tntSpread = TNTSpread.ALL;
public boolean tntFlowsInWater = true;
public boolean fallingBlockParity = false;
public PhysicsVersion physicsVersion = PhysicsVersion.LATEST;
public MinecraftMechanicsTarget mechanicsTarget = MinecraftMechanicsTarget.latest();
public enum TNTSpread {
ALL, Y, NONE;

View File

@@ -0,0 +1,64 @@
package me.samsuik.sakura.configuration.local;
import io.papermc.paper.configuration.WorldConfiguration;
import me.samsuik.sakura.explosion.durable.DurableMaterial;
import me.samsuik.sakura.mechanics.MinecraftMechanicsTarget;
import me.samsuik.sakura.redstone.RedstoneConfiguration;
import me.samsuik.sakura.redstone.RedstoneImplementation;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import org.bukkit.craftbukkit.block.CraftBlockType;
import org.jspecify.annotations.NullMarked;
import java.util.Map;
import java.util.stream.Collectors;
@NullMarked
public final class CachedLocalConfiguration {
public final long sectionKey;
public final MinecraftMechanicsTarget mechanicsTarget;
public final Map<Block, DurableMaterial> durableMaterials;
public final RedstoneConfiguration redstoneBehaviour;
public final boolean consistentExplosionRadius;
public final int lavaFlowSpeed;
public static CachedLocalConfiguration emptyConfiguration() {
return new CachedLocalConfiguration();
}
public CachedLocalConfiguration(final Level level, final ConfigurationContainer container, final long sectionKey) {
this.sectionKey = sectionKey;
this.mechanicsTarget = container.getOptional(ConfigurableKey.MECHANICS_TARGET)
.orElse(level.sakuraConfig().cannons.mechanics.mechanicsTarget);
this.durableMaterials = container.getOptional(ConfigurableKey.DURABLE_MATERIALS)
.map(sealedContainer -> sealedContainer.open().contents().entrySet().stream()
.map(entry -> Map.entry(CraftBlockType.bukkitToMinecraftNew(entry.getKey()), entry.getValue()))
.collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)))
.orElseGet(() -> level.sakuraConfig().cannons.explosion.durableMaterials);
this.redstoneBehaviour = container.getOptional(ConfigurableKey.REDSTONE_BEHAVIOUR)
.orElse(createDefaultRedstoneConfiguration(level));
this.consistentExplosionRadius = container.getOptional(ConfigurableKey.CONSISTENT_EXPLOSION_RADIUS)
.orElse(level.sakuraConfig().cannons.explosion.consistentRadius);
this.lavaFlowSpeed = container.getOptional(ConfigurableKey.LAVA_FLOW_SPEED)
.orElse(30);
}
private CachedLocalConfiguration() {
this.sectionKey = Long.MIN_VALUE;
this.mechanicsTarget = MinecraftMechanicsTarget.latest();
this.durableMaterials = Map.of();
this.redstoneBehaviour = new RedstoneConfiguration(RedstoneImplementation.VANILLA, false);
this.consistentExplosionRadius = false;
this.lavaFlowSpeed = 30;
}
public WorldConfiguration.Misc.RedstoneImplementation paperRedstoneImplementation() {
return WorldConfiguration.Misc.RedstoneImplementation.values()[this.redstoneBehaviour.implementation().ordinal()];
}
private static RedstoneConfiguration createDefaultRedstoneConfiguration(final Level level) {
final WorldConfiguration.Misc.RedstoneImplementation paperRedstoneImplementation = level.paperConfig().misc.redstoneImplementation;
final RedstoneImplementation sakuraRedstoneImplementation = RedstoneImplementation.values()[paperRedstoneImplementation.ordinal()];
return new RedstoneConfiguration(sakuraRedstoneImplementation, level.sakuraConfig().technical.redstone.redstoneCache);
}
}

View File

@@ -0,0 +1,66 @@
package me.samsuik.sakura.configuration.local;
import net.minecraft.util.Mth;
import org.bukkit.util.BoundingBox;
import org.jspecify.annotations.NullMarked;
import java.util.function.LongConsumer;
@NullMarked
public record ConfigurationArea(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) {
public ConfigurationArea(final BoundingBox boundingBox) {
this(
Mth.floor(boundingBox.getMinX()),
Mth.floor(boundingBox.getMinY()),
Mth.floor(boundingBox.getMinZ()),
Mth.floor(boundingBox.getMaxX()),
Mth.floor(boundingBox.getMaxY()),
Mth.floor(boundingBox.getMaxZ())
);
}
public BoundingBox asBoundingBox() {
return new BoundingBox(this.minX, this.minY, this.minZ, this.maxX, this.maxY, this.maxZ);
}
public boolean contains(final int x, final int y, final int z) {
return x >= this.minX && x < this.maxX
&& y >= this.minY && y < this.maxY
&& z >= this.minZ && z < this.maxZ;
}
public long volume() {
return this.countSections(0);
}
public long countSections(final int sectionExponent) {
final int sectionsX = (maxX - minX >> sectionExponent) + 1;
final int sectionsY = (maxY - minY >> sectionExponent) + 1;
final int sectionsZ = (maxZ - minZ >> sectionExponent) + 1;
return (long) sectionsX * (long) sectionsY * (long) sectionsZ;
}
public static long sectionKey(final int x, final int y, final int z, final int sectionExponent) {
final int sectionX = x >> sectionExponent;
final int sectionY = y >> sectionExponent;
final int sectionZ = z >> sectionExponent;
return (long) sectionX << 40 | (long) sectionY << 20 | (long) sectionZ;
}
public void forEach(final int sectionExponent, final LongConsumer sectionConsumer) {
final int minSectionX = this.minX >> sectionExponent;
final int minSectionY = this.minY >> sectionExponent;
final int minSectionZ = this.minZ >> sectionExponent;
final int maxSectionX = this.maxX >> sectionExponent;
final int maxSectionY = this.maxY >> sectionExponent;
final int maxSectionZ = this.maxZ >> sectionExponent;
for (int x = minSectionX; x <= maxSectionX; x++) {
for (int y = minSectionY; y <= maxSectionY; y++) {
for (int z = minSectionZ; z <= maxSectionZ; z++) {
sectionConsumer.accept(sectionKey(x, y, z, 0));
}
}
}
}
}

View File

@@ -1,221 +0,0 @@
package me.samsuik.sakura.configuration.local;
import ca.spottedleaf.concurrentutil.function.BiLongObjectConsumer;
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.*;
public final class LocalConfigManager implements LocalStorageHandler {
private static final long MASSIVE_REGION_SIZE = 0x10000000000L;
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 = 4;
private final Long2ObjectMap<Pair<LocalValueConfig, TickExpiry>> chunkConfigCache = new Long2ObjectOpenHashMap<>();
private final Level level;
private long expirationTick = 0L;
public LocalConfigManager(Level level) {
this.level = level;
}
private int regionChunkCoord(int n) {
return n >> this.regionExponent;
}
@Override
public synchronized @NonNull Optional<LocalRegion> locate(int x, int z) {
int regionX = this.regionChunkCoord(x);
int regionZ = this.regionChunkCoord(z);
long regionPos = ChunkPos.asLong(regionX, regionZ);
List<LocalRegion> regions = this.smallRegions.getOrDefault(regionPos, List.of());
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) {
boolean smallRegion = this.isSmallRegion(region);
this.ensureRegionIsNotOverlapping(region, smallRegion);
if (!smallRegion) {
this.largeRegions.add(region);
// The region exponent may be too small
if ((this.largeRegions.size() & 15) == 0) {
this.resizeRegions();
}
} else {
this.forEachRegionChunks(region, this::addSmallRegion);
}
this.chunkConfigCache.clear();
this.storageMap.put(region, storage);
}
@Override
public synchronized void remove(@NonNull LocalRegion region) {
this.forEachRegionChunks(region, (pos, r) -> {
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);
this.largeRegions.remove(region);
}
private void addSmallRegion(long pos, LocalRegion region) {
this.smallRegions.computeIfAbsent(pos, k -> new ArrayList<>())
.add(region);
}
private void forEachRegionChunks(LocalRegion region, BiLongObjectConsumer<LocalRegion> 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), region);
}
}
}
private void resizeRegions() {
int newExponent = this.calculateRegionExponent();
if (newExponent == this.regionExponent) {
return; // nothing has changed
}
this.regionExponent = newExponent;
this.largeRegions.clear();
this.smallRegions.clear();
for (LocalRegion region : this.storageMap.keySet()) {
if (!this.isSmallRegion(region)) {
this.largeRegions.add(region);
} else {
this.forEachRegionChunks(region, this::addSmallRegion);
}
}
}
private int calculateRegionExponent() {
long totalRegionChunks = 0;
for (LocalRegion region : this.storageMap.keySet()) {
long chunks = regionChunks(region, 0);
if (chunks >= MASSIVE_REGION_SIZE) {
continue;
}
totalRegionChunks += chunks;
}
totalRegionChunks /= this.storageMap.size();
int exponent = 4;
while (true) {
if ((totalRegionChunks >> exponent++) <= SMALL_REGION_SIZE / 2) {
return exponent;
}
}
}
private boolean isSmallRegion(LocalRegion region) {
return regionChunks(region, this.regionExponent) <= SMALL_REGION_SIZE;
}
private static long regionChunks(LocalRegion region, int exponent) {
int sizeX = region.maxX() - region.minX() >> exponent;
int sizeZ = region.maxZ() - region.minZ() >> exponent;
return (long) (sizeX + 1) * (long) (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 = gameTime - this.expirationTick;
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 -> this.createLocalChunkConfigWithExpiry(position, gameTime));
pair.value().refresh(gameTime);
return pair.key();
}
private Pair<LocalValueConfig, TickExpiry> createLocalChunkConfigWithExpiry(BlockPos position, long gameTime) {
// uses defaults from the sakura and paper config
LocalValueConfig config = new LocalValueConfig(this.level);
this.locate(position.getX(), position.getZ()).ifPresent(region -> {
config.loadFromStorage(this.storageMap.get(region));
});
TickExpiry expiry = new TickExpiry(gameTime, CONFIG_CACHE_EXPIRATION);
return Pair.of(config, expiry);
}
private void ensureRegionIsNotOverlapping(LocalRegion region, boolean smallRegion) {
Set<LocalRegion> nearbyRegions = new ReferenceOpenHashSet<>();
if (!smallRegion) {
nearbyRegions.addAll(this.storageMap.keySet());
} else {
this.forEachRegionChunks(region, (pos, r) -> {
nearbyRegions.addAll(this.smallRegions.getOrDefault(pos, List.of()));
});
}
// Throw if any of the nearby regions are overlapping
for (LocalRegion present : Iterables.concat(nearbyRegions, this.largeRegions)) {
if (present != region && present.intersects(region)) {
throw new OverlappingRegionException(present, region);
}
}
}
}

View File

@@ -0,0 +1,109 @@
package me.samsuik.sakura.configuration.local;
import io.papermc.paper.util.MCUtil;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import me.samsuik.sakura.configuration.local.ConfigurationContainer.SealedConfigurationContainer;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.phys.Vec3;
import org.bukkit.util.BoundingBox;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.*;
@NullMarked
public final class LocalConfiguration implements LocalConfigurationAccessor {
private static final CachedLocalConfiguration EMPTY_CONFIGURATION = CachedLocalConfiguration.emptyConfiguration();
private final LocalConfigurationContainers containers = new LocalConfigurationContainers();
private final Long2ObjectOpenHashMap<CachedLocalConfiguration> cachedConfiguration = new Long2ObjectOpenHashMap<>();
private final CachedLocalConfiguration[] recentlyAccessed = new CachedLocalConfiguration[8];
private long lastGameTime;
private final Level level;
public LocalConfiguration(final Level level) {
this.level = level;
Arrays.fill(this.recentlyAccessed, EMPTY_CONFIGURATION);
}
@Override
public void set(final BoundingBox bb, final SealedConfigurationContainer container) {
final ConfigurationArea area = new ConfigurationArea(bb);
this.containers.add(area, container);
// As containers are immutable, we just need to clear the cache to provide an immediate update.
this.cachedConfiguration.clear();
Arrays.fill(this.recentlyAccessed, EMPTY_CONFIGURATION);
}
@Override
public @Nullable SealedConfigurationContainer remove(final BoundingBox bb) {
final ConfigurationArea area = new ConfigurationArea(bb);
final SealedConfigurationContainer container = this.containers.remove(area);
this.cachedConfiguration.clear();
Arrays.fill(this.recentlyAccessed, EMPTY_CONFIGURATION);
return container;
}
@Override
public @Nullable SealedConfigurationContainer get(final BoundingBox bb) {
return this.containers.get(new ConfigurationArea(bb));
}
@Override
public @Nullable ConfigurationContainer getContainer(final int x, final int y, final int z) {
return this.containers.getContainer(x, y, z);
}
@Override
public List<BoundingBox> getAreas(final int x, final int y, final int z) {
return this.containers.getAreas(x, y, z).stream()
.map(ConfigurationArea::asBoundingBox)
.toList();
}
public CachedLocalConfiguration at(final Vec3 vec3) {
return this.at(BlockPos.containing(vec3));
}
public CachedLocalConfiguration at(final BlockPos pos) {
// This is sometimes called off the main thread when loading/generating chunks
if (!MCUtil.isMainThread()) {
return EMPTY_CONFIGURATION;
}
final int x = pos.getX();
final int y = pos.getY();
final int z = pos.getZ();
final long sectionKey = ConfigurationArea.sectionKey(x, y, z, 2);
final int recentCacheIndex = ((x & 1) << 2) | ((y & 1) << 1) | (z & 1);
final CachedLocalConfiguration recentCache = recentlyAccessed[recentCacheIndex];
// Fast path if the local configuration was recently accessed
if (recentCache.sectionKey == sectionKey) {
return recentCache;
}
// Clear the cache every minute
final long gameTime = this.level.getGameTime();
if (gameTime - this.lastGameTime >= 60 * 20) {
this.cachedConfiguration.clear();
this.lastGameTime = gameTime;
}
// Get the local configuration from the cache, if that isn't possible then create one
CachedLocalConfiguration cache = this.cachedConfiguration.get(sectionKey);
//noinspection ConstantValue
if (cache == null) {
final ConfigurationContainer container = this.getContainer(x, y, z);
cache = new CachedLocalConfiguration(level, container, sectionKey);
this.cachedConfiguration.put(sectionKey, cache);
}
this.recentlyAccessed[recentCacheIndex] = cache;
return cache;
}
}

View File

@@ -0,0 +1,144 @@
package me.samsuik.sakura.configuration.local;
import com.google.common.collect.Iterables;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import java.util.*;
import static me.samsuik.sakura.configuration.local.ConfigurationContainer.*;
@NullMarked
public final class LocalConfigurationContainers {
private static final long MASSIVE_REGION_SIZE = 0x180000000L;
private static final int LARGE_AREA_THRESHOLD = 6 * 6 * 6;
private static final Comparator<ConfigurationArea> AREA_BY_VOLUME = Comparator.comparingLong(ConfigurationArea::volume);
private final Map<ConfigurationArea, SealedConfigurationContainer> containers = new HashMap<>();
private final List<ConfigurationArea> largeAreas = new ArrayList<>();
private final Long2ObjectOpenHashMap<List<ConfigurationArea>> smallAreas = new Long2ObjectOpenHashMap<>();
private int sectionExponent = 4;
private int changes = 0;
private boolean isLargeArea(final ConfigurationArea area) {
return area.countSections(this.sectionExponent) > LARGE_AREA_THRESHOLD;
}
public void add(final ConfigurationArea area, final SealedConfigurationContainer container) {
final ConfigurationContainer presentContainer = this.containers.put(area, container);
if (presentContainer == null) {
if (this.isLargeArea(area)) {
this.largeAreas.add(area);
} else {
this.updateSections(area, false);
}
if ((changes++ & 15) == 0) {
this.resizeSections();
}
}
}
public @Nullable SealedConfigurationContainer remove(final ConfigurationArea area) {
final SealedConfigurationContainer container = this.containers.remove(area);
if (this.isLargeArea(area)) {
this.largeAreas.remove(area);
} else if (container != null) {
this.updateSections(area, true);
}
if ((changes++ & 15) == 0) {
this.resizeSections();
}
return container;
}
public @Nullable SealedConfigurationContainer get(final ConfigurationArea area) {
return this.containers.get(area);
}
public @Nullable ConfigurationContainer getContainer(final int x, final int y, final int z) {
final ConfigurationContainer newContainer = new ConfigurationContainer();
final List<ConfigurationArea> areas = this.getAreas(x, y, z);
areas.sort(AREA_BY_VOLUME); // sorted by size
for (final ConfigurationArea area : areas) {
if (area.contains(x, y, z)) {
newContainer.fillAbsentValues(this.containers.get(area));
}
}
return newContainer.contents().isEmpty() ? null : newContainer;
}
public List<ConfigurationArea> getAreas(final int x, final int y, final int z) {
final long sectionKey = ConfigurationArea.sectionKey(x, y, z, this.sectionExponent);
final List<ConfigurationArea> nearby = this.smallAreas.getOrDefault(sectionKey, Collections.emptyList());
final List<ConfigurationArea> foundAreas = new ArrayList<>();
for (final ConfigurationArea area : Iterables.concat(nearby, this.largeAreas)) {
if (area.contains(x, y, z)) {
foundAreas.add(area);
}
}
return foundAreas;
}
private int calculateNewSectionExponent() {
long totalSectionCount = 0;
int totalAreas = 0;
for (final ConfigurationArea area : this.containers.keySet()) {
final long sections = area.countSections(4);
if (sections < MASSIVE_REGION_SIZE) {
totalSectionCount += sections;
totalAreas++;
}
}
final long averageSectionCount = totalSectionCount / Math.max(totalAreas, 1);
for (int exponent = 4;; exponent++) {
if ((averageSectionCount >> exponent) < LARGE_AREA_THRESHOLD) {
return exponent;
}
}
}
private void resizeSections() {
final int newExponent = this.calculateNewSectionExponent();
if (newExponent == this.sectionExponent) {
return; // nothing has changed
}
this.sectionExponent = newExponent;
this.smallAreas.clear();
this.largeAreas.clear();
for (final ConfigurationArea area : this.containers.keySet()) {
if (this.isLargeArea(area)) {
this.largeAreas.add(area);
} else {
this.updateSections(area, false);
}
}
}
private void updateSections(final ConfigurationArea area, final boolean remove) {
area.forEach(this.sectionExponent, sectionKey -> {
if (remove) {
final List<ConfigurationArea> areas = this.smallAreas.get(sectionKey);
//noinspection ConstantValue
if (areas != null) {
areas.remove(area);
if (areas.isEmpty()) {
this.smallAreas.remove(sectionKey);
}
}
} else {
this.smallAreas.computeIfAbsent(sectionKey, k -> new ArrayList<>()).add(area);
}
});
}
}

View File

@@ -1,48 +0,0 @@
package me.samsuik.sakura.configuration.local;
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);
// temp, will be updated later
DurableMaterial durableMaterial = new DurableMaterial(materialProperties.getKey(), materialProperties.getValue(), false);
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

@@ -1,9 +0,0 @@
package me.samsuik.sakura.configuration.local;
import me.samsuik.sakura.local.LocalRegion;
public final class OverlappingRegionException extends RuntimeException {
public OverlappingRegionException(LocalRegion presentRegion, LocalRegion region) {
super("overlapping region (%s, %s)".formatted(presentRegion, region));
}
}

View File

@@ -0,0 +1,50 @@
package me.samsuik.sakura.configuration.serializer;
import io.leangen.geantyref.TypeToken;
import me.samsuik.sakura.mechanics.MechanicVersion;
import me.samsuik.sakura.mechanics.MinecraftMechanicsTarget;
import me.samsuik.sakura.mechanics.MinecraftVersionEncoding;
import me.samsuik.sakura.mechanics.ServerType;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.jspecify.annotations.NullMarked;
import org.spongepowered.configurate.ConfigurationNode;
import org.spongepowered.configurate.NodePath;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.serialize.TypeSerializer;
import java.lang.reflect.Type;
import java.util.function.Predicate;
@NullMarked
public final class MinecraftMechanicsTargetSerializer implements TypeSerializer<MinecraftMechanicsTarget> {
private static final NodePath MECHANIC_VERSION = NodePath.path("mechanic-version");
private static final NodePath SERVER_TYPE = NodePath.path("server-type");
@Override
public MinecraftMechanicsTarget deserialize(final Type type, final ConfigurationNode root) throws SerializationException {
final String mechanicVersion = root.node(MECHANIC_VERSION).getString();
final String serverType = root.node(SERVER_TYPE).getString();
final MinecraftMechanicsTarget mechanicsTarget = MinecraftMechanicsTarget.fromString("%s+%s".formatted(mechanicVersion, serverType));
if (mechanicsTarget == null) {
throw new IllegalArgumentException("Unable to deserialize MinecraftMechanicsTarget (" + mechanicVersion + ", " + serverType + ")");
}
return mechanicsTarget;
}
@Override
public void serialize(
final Type type,
@Nullable MinecraftMechanicsTarget mechanicsTarget,
final ConfigurationNode root
) throws SerializationException {
if (mechanicsTarget == null) {
mechanicsTarget = MinecraftMechanicsTarget.latest();
}
root.node(MECHANIC_VERSION).set(MechanicVersion.name(mechanicsTarget.mechanicVersion()));
root.node(SERVER_TYPE).set(ServerType.name(mechanicsTarget.serverType()));
}
}

View File

@@ -29,6 +29,7 @@ public final class ConfigurationTransformations {
V8_RenameExplosionResistantItems.apply(versionedBuilder);
V9_RenameAllowNonTntBreakingDurableBlocks.apply(versionedBuilder);
V10_DurableMaterialOnlyDamagedByTnt.apply(versionedBuilder);
V11_RemovePhysicsVersion.apply(versionedBuilder);
// ADD FUTURE VERSIONED TRANSFORMS TO versionedBuilder HERE
versionedBuilder.build().apply(node);
}

View File

@@ -0,0 +1,18 @@
package me.samsuik.sakura.configuration.transformation.world;
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 V11_RemovePhysicsVersion {
private static final int VERSION = 11;
private static final NodePath PATH = path("cannons", "mechanics", "physics-version");
public static void apply(final ConfigurationTransformation.VersionedBuilder builder) {
builder.addVersion(VERSION, ConfigurationTransformation.builder()
.addAction(PATH, TransformAction.remove())
.build());
}
}

View File

@@ -1,179 +0,0 @@
package me.samsuik.sakura.explosion;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class LegacyExplosionClipping {
public static BlockHitResult.Type clipLegacy(Level level, Vec3 from, Vec3 to) {
int toX = Mth.floor(to.x);
int toY = Mth.floor(to.y);
int toZ = Mth.floor(to.z);
int fromX = Mth.floor(from.x);
int fromY = Mth.floor(from.y);
int fromZ = Mth.floor(from.z);
BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos(fromX, fromY, fromZ);
LevelChunk chunk = level.getChunkIfLoaded(fromX >> 4, fromZ >> 4);
if (chunk == null) {
return BlockHitResult.Type.MISS;
}
BlockState state = chunk.getBlockState(mutableBlockPos);
VoxelShape shape = state.getShape(level, mutableBlockPos);
for (AABB bb : shape.toAabbs()) {
if (clip(bb, mutableBlockPos, from, to)) {
return BlockHitResult.Type.BLOCK;
}
}
for (int steps = 0; steps < 16; ++steps) {
if (fromX == toX && fromY == toY && fromZ == toZ) {
return BlockHitResult.Type.MISS;
}
boolean moveX = true;
boolean moveY = true;
boolean moveZ = true;
double d0 = 999.0D;
double d1 = 999.0D;
double d2 = 999.0D;
if (toX > fromX) {
d0 = (double) fromX + 1.0D;
} else if (toX < fromX) {
d0 = (double) fromX + 0.0D;
} else {
moveX = false;
}
if (toY > fromY) {
d1 = (double) fromY + 1.0D;
} else if (toY < fromY) {
d1 = (double) fromY + 0.0D;
} else {
moveY = false;
}
if (toZ > fromZ) {
d2 = (double) fromZ + 1.0D;
} else if (toZ < fromZ) {
d2 = (double) fromZ + 0.0D;
} else {
moveZ = false;
}
double d3 = 999.0D;
double d4 = 999.0D;
double d5 = 999.0D;
double d6 = to.x - from.x;
double d7 = to.y - from.y;
double d8 = to.z - from.z;
if (moveX) d3 = (d0 - from.x) / d6;
if (moveY) d4 = (d1 - from.y) / d7;
if (moveZ) d5 = (d2 - from.z) / d8;
if (d3 == -0.0D) d3 = -1.0E-4D;
if (d4 == -0.0D) d4 = -1.0E-4D;
if (d5 == -0.0D) d5 = -1.0E-4D;
Direction moveDir;
if (d3 < d4 && d3 < d5) {
moveDir = toX > fromX ? Direction.WEST : Direction.EAST;
from = new Vec3(d0, from.y + d7 * d3, from.z + d8 * d3);
} else if (d4 < d5) {
moveDir = toY > fromY ? Direction.DOWN : Direction.UP;
from = new Vec3(from.x + d6 * d4, d1, from.z + d8 * d4);
} else {
moveDir = toZ > fromZ ? Direction.NORTH : Direction.SOUTH;
from = new Vec3(from.x + d6 * d5, from.y + d7 * d5, d2);
}
fromX = Mth.floor(from.x) - (moveDir == Direction.EAST ? 1 : 0);
fromY = Mth.floor(from.y) - (moveDir == Direction.UP ? 1 : 0);
fromZ = Mth.floor(from.z) - (moveDir == Direction.SOUTH ? 1 : 0);
mutableBlockPos.set(fromX, fromY, fromZ);
int chunkX = fromX >> 4;
int chunkZ = fromZ >> 4;
if (chunkX != chunk.locX || chunkZ != chunk.locZ) {
chunk = level.getChunkIfLoaded(chunkX, chunkZ);
}
if (chunk == null) {
return BlockHitResult.Type.MISS;
}
state = chunk.getBlockState(mutableBlockPos);
shape = state.getShape(level, mutableBlockPos);
for (AABB bb : shape.toAabbs()) {
if (clip(bb, mutableBlockPos, from, to)) {
return BlockHitResult.Type.BLOCK;
}
}
}
return BlockHitResult.Type.MISS;
}
private static boolean clip(AABB bb, BlockPos pos, Vec3 from, Vec3 to) {
from = from.subtract(pos.getX(), pos.getY(), pos.getZ());
to = to.subtract(pos.getX(), pos.getY(), pos.getZ());
double x = to.x - from.x;
double y = to.y - from.y;
double z = to.z - from.z;
double minXd = clip(bb.minX, x, from.x);
double minYd = clip(bb.minY, y, from.y);
double minZd = clip(bb.minZ, z, from.z);
double maxXd = clip(bb.maxX, x, from.x);
double maxYd = clip(bb.maxY, y, from.y);
double maxZd = clip(bb.maxZ, z, from.z);
return clipX(from, bb, minXd, y, z) || clipY(from, bb, minYd, x, z) || clipZ(from, bb, minZd, x, y)
|| clipX(from, bb, maxXd, y, z) || clipY(from, bb, maxYd, x, z) || clipZ(from, bb, maxZd, x, y);
}
private static double clip(double bound, double axisD, double axisN) {
if (axisD * axisD < 1.0000000116860974E-7D) {
return -1.0;
}
return (bound - axisN) / axisD;
}
private static boolean clipX(Vec3 from, AABB bb, double n, double y, double z) {
if (n < 0.0 || n > 1.0) {
return false;
}
y = from.y + y * n;
z = from.z + z * n;
return y >= bb.minY && y <= bb.maxY && z >= bb.minZ && z <= bb.maxZ;
}
private static boolean clipY(Vec3 from, AABB bb, double n, double x, double z) {
if (n < 0.0 || n > 1.0) {
return false;
}
x = from.x + x * n;
z = from.z + z * n;
return x >= bb.minX && x <= bb.maxX && z >= bb.minZ && z <= bb.maxZ;
}
private static boolean clipZ(Vec3 from, AABB bb, double n, double x, double y) {
if (n < 0.0 || n > 1.0) {
return false;
}
x = from.x + x * n;
y = from.y + y * n;
return x >= bb.minX && x <= bb.maxX && y >= bb.minY && y <= bb.maxY;
}
}

View File

@@ -4,6 +4,7 @@ import ca.spottedleaf.moonrise.common.util.WorldUtil;
import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.ChunkEntitySlices;
import ca.spottedleaf.moonrise.patches.chunk_system.level.entity.EntityLookup;
import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.samsuik.sakura.mechanics.MechanicVersion;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.util.Mth;
@@ -159,7 +160,7 @@ public abstract class SpecialisedExplosion<T extends Entity> extends ServerExplo
double z = entity.getZ() - pos.z;
double distance = Math.sqrt(x * x + y * y + z * z);
// Sakura start - configure cannon physics
if (this.physics.before(1_17_0)) {
if (this.mechanicsTarget.before(MechanicVersion.v1_17)) {
distanceFromBottom = (float) distanceFromBottom;
distance = (float) distance;
}

View File

@@ -5,6 +5,7 @@ import it.unimi.dsi.fastutil.objects.ObjectArrayList;
import me.samsuik.sakura.entity.EntityState;
import me.samsuik.sakura.entity.merge.MergeLevel;
import me.samsuik.sakura.entity.merge.MergeableEntity;
import me.samsuik.sakura.mechanics.MechanicVersion;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.server.level.ServerLevel;
@@ -38,7 +39,7 @@ public final class TntExplosion extends SpecialisedExplosion<PrimedTnt> {
// Sakura start - configure cannon physics
@Override
protected double getExplosionOffset() {
return this.physics.before(1_10_0) ? (double) 0.49f : super.getExplosionOffset();
return this.mechanicsTarget.before(MechanicVersion.v1_10) ? (double) 0.49f : super.getExplosionOffset();
}
// Sakura end - configure cannon physics

View File

@@ -3,9 +3,11 @@ package me.samsuik.sakura.explosion.durable;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
import net.minecraft.core.BlockPos;
import org.jspecify.annotations.NullMarked;
import java.util.concurrent.TimeUnit;
@NullMarked
public final class DurableBlockManager {
private final Cache<BlockPos, DurableBlock> durableBlocks = CacheBuilder.newBuilder()
.expireAfterAccess(1, TimeUnit.MINUTES)

View File

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

View File

@@ -0,0 +1,52 @@
package me.samsuik.sakura.mechanics;
import net.minecraft.core.Direction;
import net.minecraft.world.entity.Entity;
import net.minecraft.world.phys.Vec3;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
@NullMarked
public final class EntityBehaviour {
public static void pre1_21_6$changeEntityPosition(
final Entity entity,
final Vec3 position,
final Vec3 relativeMovement,
final MinecraftMechanicsTarget mechanicsTarget
) {
final Vec3 newPosition = position.add(relativeMovement);
final Vec3 newEntityPosition;
if (mechanicsTarget.is(MechanicVersion.v1_21_5)) {
newEntityPosition = manglePosition(position, relativeMovement);
entity.addMovementThisTick(new Entity.Movement(position, newPosition, true));
} else {
newEntityPosition = newPosition;
}
entity.setPos(newEntityPosition);
}
private static Vec3 manglePosition(final Vec3 position, final Vec3 relativeMovement) {
Vec3 newPosition = position;
for (final Direction.Axis axis : Entity.axisStepOrder(relativeMovement)) {
final double movement = relativeMovement.get(axis);
if (movement != 0.0) {
newPosition = newPosition.relative(axis.getPositive(), movement);
}
}
return newPosition;
}
public static boolean canMoveEntity(final double relativeMovementSqr, final Vec3 movement, final MinecraftMechanicsTarget mechanicsTarget) {
return relativeMovementSqr > 1.0E-7
|| mechanicsTarget.atLeast(MechanicVersion.v1_21_2) && movement.lengthSqr() - relativeMovementSqr < 1.0E-7
|| mechanicsTarget.before(MechanicVersion.v1_14);
}
public static boolean prioritiseXFirst(final double x, final double z, final @Nullable MinecraftMechanicsTarget mechanicsTarget) {
return mechanicsTarget == null || mechanicsTarget.atLeast(MechanicVersion.v1_14)
? Math.abs(x) < Math.abs(z)
: mechanicsTarget.isLegacy() && Math.abs(x) > Math.abs(z);
}
}

View File

@@ -0,0 +1,44 @@
package me.samsuik.sakura.mechanics;
import net.minecraft.core.BlockPos;
import net.minecraft.server.level.ServerLevel;
import net.minecraft.world.entity.item.FallingBlockEntity;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.Block;
import net.minecraft.world.level.block.Blocks;
import net.minecraft.world.level.block.FallingBlock;
import net.minecraft.world.level.block.state.BlockState;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class FallingBlockBehaviour {
public static boolean isAbleToStackOnBlock(final FallingBlockEntity fallingBlock, final MinecraftMechanicsTarget mechanicsTarget) {
if (!mechanicsTarget.between(me.samsuik.sakura.mechanics.MechanicVersion.v1_9, me.samsuik.sakura.mechanics.MechanicVersion.v1_14)) {
return true;
}
// This is patched by default on Paper.
if (mechanicsTarget.isServerType(me.samsuik.sakura.mechanics.ServerType.PAPER)) {
return true;
}
// todo: Entity#getOnPos might be a good alternative to this
final BlockPos blockPos = BlockPos.containing(fallingBlock.getX(), fallingBlock.getY() - 0.001f, fallingBlock.getZ());
final BlockState state = fallingBlock.level().getBlockState(blockPos);
return !FallingBlock.isFree(state);
}
public static void removeBlockOnFall(final FallingBlockEntity fallingBlock, final Block block) {
final Level level = fallingBlock.level();
final BlockPos blockPos = fallingBlock.blockPosition();
final BlockState state = level.getBlockState(blockPos);
// todo: Do we need to call the event here? This event is already called in the fall method that spawns the falling block entity.
if (state.is(block) && org.bukkit.craftbukkit.event.CraftEventFactory.callEntityChangeBlockEvent(fallingBlock, blockPos, Blocks.AIR.defaultBlockState())) {
level.removeBlock(blockPos, false);
} else {
if (state.is(block)) {
((ServerLevel) level).getChunkSource().blockChanged(blockPos);
}
fallingBlock.discard(org.bukkit.event.entity.EntityRemoveEvent.Cause.DESPAWN);
}
}
}

View File

@@ -0,0 +1,182 @@
package me.samsuik.sakura.mechanics;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
import net.minecraft.util.Mth;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.block.state.BlockState;
import net.minecraft.world.level.chunk.LevelChunk;
import net.minecraft.world.phys.AABB;
import net.minecraft.world.phys.BlockHitResult;
import net.minecraft.world.phys.HitResult;
import net.minecraft.world.phys.Vec3;
import net.minecraft.world.phys.shapes.VoxelShape;
import org.jspecify.annotations.NullMarked;
/**
* A replica of the explosion raytrace code before it was replaced in Minecraft 1.14.
*/
@NullMarked
public final class LegacyExplosionBlockClipping {
private static final double EPSILON = 1.0e-7f; // the precision loss is intentional
private Vec3 currentPos;
private final Vec3 endPos;
private final int toX;
private final int toY;
private final int toZ;
private LegacyExplosionBlockClipping(final Vec3 currentPos, final Vec3 endPos) {
this.currentPos = currentPos;
this.endPos = endPos;
this.toX = Mth.floor(endPos.x);
this.toY = Mth.floor(endPos.y);
this.toZ = Mth.floor(endPos.z);
}
public static BlockHitResult.Type clip(final Level level, final Vec3 currentPos, final Vec3 endPos) {
final LegacyExplosionBlockClipping clipDetection = new LegacyExplosionBlockClipping(currentPos, endPos);
final BlockPos.MutableBlockPos mutableBlockPos = new BlockPos.MutableBlockPos();
LevelChunk chunk = null;
int steps = 0;
do {
final int chunkX = Mth.floor(currentPos.x) >> 4;
final int chunkZ = Mth.floor(currentPos.z) >> 4;
if (chunk == null || chunkX != chunk.locX || chunkZ != chunk.locZ) {
chunk = level.getChunkIfLoaded(chunkX, chunkZ);
if (chunk == null) break;
}
final BlockState state = chunk.getBlockState(mutableBlockPos);
final VoxelShape shape = state.getShape(level, mutableBlockPos);
for (final AABB shapeBB : shape.toAabbs()) {
if (clip(shapeBB, mutableBlockPos, currentPos, endPos)) {
return HitResult.Type.BLOCK;
}
}
} while (++steps < 16 && clipDetection.next(mutableBlockPos));
return HitResult.Type.MISS;
}
private boolean next(final BlockPos.MutableBlockPos mutableBlockPos) {
final int currX = mutableBlockPos.getX();
final int currY = mutableBlockPos.getY();
final int currZ = mutableBlockPos.getZ();
final int toX = this.toX;
final int toY = this.toY;
final int toZ = this.toZ;
if (currX == toX && currY == toY && currZ == toZ) {
return false;
}
boolean moveX = true;
boolean moveY = true;
boolean moveZ = true;
double d0 = 999.0D;
double d1 = 999.0D;
double d2 = 999.0D;
if (toX > currX) {
d0 = (double) currX + 1.0D;
} else if (toX < currX) {
d0 = (double) currX + 0.0D;
} else {
moveX = false;
}
if (toY > currY) {
d1 = (double) currY + 1.0D;
} else if (toY < currY) {
d1 = (double) currY + 0.0D;
} else {
moveY = false;
}
if (toZ > currZ) {
d2 = (double) currZ + 1.0D;
} else if (toZ < currZ) {
d2 = (double) currZ + 0.0D;
} else {
moveZ = false;
}
double d3 = 999.0D;
double d4 = 999.0D;
double d5 = 999.0D;
final Vec3 currPos = this.currentPos;
final Vec3 endPos = this.endPos;
final double d6 = endPos.x - currPos.x;
final double d7 = endPos.y - currPos.y;
final double d8 = endPos.z - currPos.z;
if (moveX) d3 = (d0 - currPos.x) / d6;
if (moveY) d4 = (d1 - currPos.y) / d7;
if (moveZ) d5 = (d2 - currPos.z) / d8;
if (d3 == -0.0D) d3 = -1.0E-4D;
if (d4 == -0.0D) d4 = -1.0E-4D;
if (d5 == -0.0D) d5 = -1.0E-4D;
final Direction moveDir;
final Vec3 newCurrentPos;
if (d3 < d4 && d3 < d5) {
moveDir = toX > currX ? Direction.WEST : Direction.EAST;
newCurrentPos = new Vec3(d0, currPos.y + d7 * d3, currPos.z + d8 * d3);
} else if (d4 < d5) {
moveDir = toY > currY ? Direction.DOWN : Direction.UP;
newCurrentPos = new Vec3(currPos.x + d6 * d4, d1, currPos.z + d8 * d4);
} else {
moveDir = toZ > currZ ? Direction.NORTH : Direction.SOUTH;
newCurrentPos = new Vec3(currPos.x + d6 * d5, currPos.y + d7 * d5, d2);
}
mutableBlockPos.set(
Mth.floor(currPos.x) - (moveDir == Direction.EAST ? 1 : 0),
Mth.floor(currPos.y) - (moveDir == Direction.UP ? 1 : 0),
Mth.floor(currPos.z) - (moveDir == Direction.SOUTH ? 1 : 0)
);
this.currentPos = newCurrentPos;
return true;
}
private static boolean clip(final AABB bb, final BlockPos pos, final Vec3 from, final Vec3 to) {
final Vec3 origin = from.subtract(pos.getX(), pos.getY(), pos.getZ());
final Vec3 direction = to.subtract(pos.getX(), pos.getY(), pos.getZ()).subtract(origin);
double tmin = Double.NEGATIVE_INFINITY;
double tmax = Double.POSITIVE_INFINITY;
if (direction.x * direction.x >= EPSILON) {
final double t1 = (bb.minX - origin.x) / direction.x;
final double t2 = (bb.maxX - origin.x) / direction.x;
tmin = Math.max(tmin, Math.min(t1, t2));
tmax = Math.min(tmax, Math.max(t1, t2));
} else if (origin.x < bb.minX || origin.x > bb.maxX) {
return false;
}
if (direction.y * direction.y >= EPSILON) {
final double t1 = (bb.minY - origin.y) / direction.y;
final double t2 = (bb.maxY - origin.y) / direction.y;
tmin = Math.max(tmin, Math.min(t1, t2));
tmax = Math.min(tmax, Math.max(t1, t2));
} else if (origin.y < bb.minY || origin.y > bb.maxY) {
return false;
}
if (direction.z * direction.z >= EPSILON) {
double t1 = (bb.minZ - origin.z) / direction.z;
double t2 = (bb.maxZ - origin.z) / direction.z;
tmin = Math.max(tmin, Math.min(t1, t2));
tmax = Math.min(tmax, Math.max(t1, t2));
} else if (origin.z < bb.minZ || origin.z > bb.maxZ) {
return false;
}
return tmax >= tmin && tmax >= 0.0 && tmin <= 1.0;
}
}

View File

@@ -0,0 +1,34 @@
package me.samsuik.sakura.mechanics;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.material.FlowingFluid;
import net.minecraft.world.level.material.FluidState;
import org.jspecify.annotations.NullMarked;
@NullMarked
public final class LiquidBehaviour {
public static boolean canLiquidSolidify(
final Level level,
final BlockPos pos,
final FluidState fluidState,
final MinecraftMechanicsTarget mechanicsTarget
) {
// In legacy-paper and versions since 1.16, liquids should always solidify.
if (mechanicsTarget.atLeast(MechanicVersion.v1_16) || mechanicsTarget.before(MechanicVersion.v1_10) && mechanicsTarget.isServerType(ServerType.PAPER)) {
return true;
}
// In 1.13 and later, liquids can only solidify if they occupy at least half of the block.
if (mechanicsTarget.atLeast(MechanicVersion.v1_13) && fluidState.getHeight(level, pos) >= 0.44444445f) {
return true;
}
// todo: not sure if this is necessary, this looks identical to the condition above.
if (mechanicsTarget.before(MechanicVersion.v1_13)) {
return FlowingFluid.getLegacyLevel(fluidState) < 4;
}
return true;
}
}

View File

@@ -1,7 +1,7 @@
package me.samsuik.sakura.redstone;
import io.papermc.paper.configuration.WorldConfiguration;
import me.samsuik.sakura.configuration.local.LocalValueConfig;
import me.samsuik.sakura.configuration.local.CachedLocalConfiguration;
import net.minecraft.core.BlockPos;
import net.minecraft.world.level.Level;
import net.minecraft.world.level.redstone.Orientation;
@@ -13,10 +13,9 @@ public record RedstoneNetworkSource(WorldConfiguration.Misc.RedstoneImplementati
BlockPos position, @Nullable Orientation orientation,
int updateDepth, int newPower, int oldPower) {
public static RedstoneNetworkSource createNetworkSource(Level level, LocalValueConfig localConfig, BlockPos pos,
public static RedstoneNetworkSource createNetworkSource(Level level, CachedLocalConfiguration localConfiguration, BlockPos pos,
@Nullable Orientation orientation, int newPower, int oldPower) {
WorldConfiguration.Misc.RedstoneImplementation redstoneImplementation = localConfig.redstoneImplementation;
int updateDepth = level.neighborUpdater.getUpdateDepth();
return new RedstoneNetworkSource(redstoneImplementation, pos, orientation, updateDepth, newPower, oldPower);
return new RedstoneNetworkSource(localConfiguration.paperRedstoneImplementation(), pos, orientation, updateDepth, newPower, oldPower);
}
}

View File

@@ -1,7 +1,7 @@
package me.samsuik.sakura.redstone;
import it.unimi.dsi.fastutil.objects.*;
import me.samsuik.sakura.configuration.local.LocalValueConfig;
import me.samsuik.sakura.configuration.local.CachedLocalConfiguration;
import me.samsuik.sakura.utils.TickExpiry;
import net.minecraft.core.BlockPos;
import net.minecraft.core.Direction;
@@ -41,8 +41,8 @@ public final class RedstoneWireCache {
}
public boolean tryApplyFromCache(BlockPos pos, @Nullable Orientation orientation, int newPower, int oldPower) {
LocalValueConfig localConfig = this.level.localConfig().config(pos);
if (!localConfig.redstoneCache || this.isTrackingWireUpdates()) {
final CachedLocalConfiguration localConfiguration = this.level.localConfig().at(pos);
if (!localConfiguration.redstoneBehaviour.cache() || this.isTrackingWireUpdates()) {
return false;
}
@@ -51,7 +51,7 @@ public final class RedstoneWireCache {
return true;
}
RedstoneNetworkSource networkSource = RedstoneNetworkSource.createNetworkSource(this.level, localConfig, pos, orientation, newPower, oldPower);
RedstoneNetworkSource networkSource = RedstoneNetworkSource.createNetworkSource(this.level, localConfiguration, pos, orientation, newPower, oldPower);
RedstoneNetwork network = this.networkCache.get(networkSource);
if (network != null) {
try {