diff --git a/src/main/java/dev/galacticraft/api/accessor/ChunkOxygenAccessor.java b/src/main/java/dev/galacticraft/api/accessor/ChunkOxygenAccessor.java new file mode 100644 index 0000000000..221f156c25 --- /dev/null +++ b/src/main/java/dev/galacticraft/api/accessor/ChunkOxygenAccessor.java @@ -0,0 +1,56 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.api.accessor; + +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import net.minecraft.core.BlockPos; + +import java.util.Iterator; + +public interface ChunkOxygenAccessor { + /** + * {@return the {@link AtmosphereProvider atmosphere providers} that service the chunk section at the given y-position} + * @param y the block y-height of the section to check. + */ + Iterator galacticraft$getProviders(int y); + + /** + * {@return the positions of the {@link AtmosphereProvider atmosphere providers} that service the chunk section at the given y-position} + * @param y the block y-height of the section to check. + */ + Iterator galacticraft$getProviderPositions(int y); + + /** + * Links the given atmosphere provider to the chunk section at the given index. + * @param sectionIndex the index of the chunk section being provided to. + * @param provider the location of the atmosphere provider being linked. + */ + void galacticraft$addAtmosphereProvider(int sectionIndex, BlockPos provider); + + /** + * Unlinks the given atmosphere provider to the chunk section at the given index. + * @param sectionIndex the index of the chunk section that is no longer being provided to. + * @param provider the location of the atmosphere provider being unlinked. + */ + void galacticraft$removeAtmosphereProvider(int sectionIndex, BlockPos provider); +} diff --git a/src/main/java/dev/galacticraft/api/accessor/GCBlockExtensions.java b/src/main/java/dev/galacticraft/api/accessor/GCBlockExtensions.java new file mode 100644 index 0000000000..cf49f459cd --- /dev/null +++ b/src/main/java/dev/galacticraft/api/accessor/GCBlockExtensions.java @@ -0,0 +1,85 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.api.accessor; + +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.api.oxygen.OxygenUtil; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.state.BlockState; + +import java.util.Iterator; + +public interface GCBlockExtensions { + /** + * {@return whether this block will transform on placement due to a lack of atmosphere} + * This should only be implemented if it is not possible to handle the transformation elsewhere. + * @param state the state being placed + */ + default boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return false; + } + + /** + * Extinguishes the given state due to a lack of atmosphere. + * @param pos the position of the block in-world + * @param state the state being placed + * @return the transformed block state. + */ + default BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + return state; + } + + /** + * {@return whether the given block state needs to be alerted to changes in atmospheric composition} + * @param state the state being checked + */ + default boolean galacticraft$hasAtmosphereListener(BlockState state) { + return false; + } + + /** + * Called when the atmospheric composition changes. + * @param level the current level + * @param pos the position of the block + * @param state the current state of the block + * @param iterator all atmosphere providers at the given position + * @see #galacticraft$onAtmosphereChange(ServerLevel, BlockPos, BlockState, boolean) + */ + default void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, Iterator iterator) { + this.galacticraft$onAtmosphereChange(level, pos, state, OxygenUtil.isBreathable(pos, iterator)); + } + + + /** + * Called when the atmospheric composition changes. + * For blocks that just need to know if the position is breathable or not. Should NOT be called outside this class. + * @param level the current level + * @param pos the position of the block + * @param state the current state of the block + * @param breathable whether the given position is currently breathable + * @see #galacticraft$onAtmosphereChange(ServerLevel, BlockPos, BlockState, Iterator) + */ + default void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + } +} diff --git a/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessor.java b/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessor.java index 9437ae0407..337eaaf0db 100644 --- a/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessor.java +++ b/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessor.java @@ -23,44 +23,34 @@ package dev.galacticraft.api.accessor; import net.minecraft.core.BlockPos; -import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.block.state.BlockState; -public interface LevelOxygenAccessor { +/** + * @see LevelOxygenAccessorRO + */ +public interface LevelOxygenAccessor extends LevelOxygenAccessorRO { /** - * Returns whether the supplied position in this world is breathable for entities - * - * @param pos the position to test - * @return whether the supplied position in the chunk is breathable for entities + * Adds an atmosphere provider to the chunk section at the given position + * @param sectionX the section x-position + * @param sectionY the section y-position + * @param sectionZ the section z-position + * @param provider the position of the provider to add */ - default boolean isBreathable(BlockPos pos) { - return this.isBreathable(pos.getX(), pos.getY(), pos.getZ()); - } - - default boolean isBreathableChunk(LevelChunk chunk, int x, int y, int z) { - throw new RuntimeException("This should be overridden by mixin!"); - } - - default boolean isBreathable(int x, int y, int z) { - throw new RuntimeException("This should be overridden by mixin!"); - } - - default void setBreathable(BlockPos pos, boolean value) { - this.setBreathable(pos.getX(), pos.getY(), pos.getZ(), value); - } - - default void setBreathable(int x, int y, int z, boolean value) { - throw new RuntimeException("This should be overridden by mixin!"); - } + void galacticraft$addAtmosphereProvider(int sectionX, int sectionY, int sectionZ, BlockPos provider); - default void setBreathableChunk(LevelChunk chunk, int x, int y, int z, boolean value) { - throw new RuntimeException("This should be overridden by mixin!"); - } - - default boolean getDefaultBreathable() { - throw new RuntimeException("This should be overridden by mixin!"); - } + /** + * Removes an atmosphere provider from the chunk section at the given position + * @param sectionX the section x-position + * @param sectionY the section y-position + * @param sectionZ the section z-position + * @param provider the position of the provider to remove + */ + void galacticraft$removeAtmosphereProvider(int sectionX, int sectionY, int sectionZ, BlockPos provider); - default void setDefaultBreathable(boolean breathable) { - throw new RuntimeException("This should be overridden by mixin!"); - } + /** + * Notifies the block at the given position that the atmosphere has changed. + * @param pos the position where the atmosphere changed + * @param state the current block state of the block where the atmosphere changed + */ + default void galacticraft$notifyAtmosphereChange(BlockPos pos, BlockState state) {} } diff --git a/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessorRO.java b/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessorRO.java new file mode 100644 index 0000000000..eb28edf299 --- /dev/null +++ b/src/main/java/dev/galacticraft/api/accessor/LevelOxygenAccessorRO.java @@ -0,0 +1,87 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.api.accessor; + +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import net.minecraft.core.BlockPos; +import net.minecraft.world.phys.Vec3; + +import java.util.Iterator; + +public interface LevelOxygenAccessorRO { + /** + * {@return the atmosphere providers for the given block position} + */ + default Iterator galacticraft$getAtmosphereProviders(int x, int y, int z) { + throw new RuntimeException("This should be overridden by mixin!"); + } + + /** + * {@return the positions of the atmosphere providers for the given block position} + */ + default Iterator galacticraft$getAtmosphereProviderLocations(int x, int y, int z) { + throw new RuntimeException("This should be overridden by mixin!"); + } + + /** + * {@return whether the given point in the level is breathable} + */ + default boolean galacticraft$isBreathable(double x, double y, double z) { + throw new RuntimeException("This should be overridden by mixin!"); + } + + /** + * {@return whether the block position in the level is breathable} + * It is undefined whether partially breathable block positions are breathable or not. + */ + default boolean galacticraft$isBreathable(int x, int y, int z) { + throw new RuntimeException("This should be overridden by mixin!"); + } + + /** + * {@return whether the block position in the level is breathable} + * It is undefined whether partially breathable block positions are breathable or not. + */ + default boolean galacticraft$isBreathable(BlockPos pos) { + throw new RuntimeException("This should be overridden by mixin!"); + } + + /** + * {@return whether this has a breathable atmosphere} + */ + default boolean galacticraft$isBreathable() { + throw new RuntimeException("This should be overridden by mixin!"); + } + + default boolean galacticraft$isBreathable(Vec3 point) { + return this.galacticraft$isBreathable(point.x, point.y, point.z); + } + + default Iterator galacticraft$getAtmosphereProviderLocations(BlockPos pos) { + return this.galacticraft$getAtmosphereProviderLocations(pos.getX(), pos.getY(), pos.getZ()); + } + + default Iterator galacticraft$getAtmosphereProviders(BlockPos pos) { + return this.galacticraft$getAtmosphereProviders(pos.getX(), pos.getY(), pos.getZ()); + } +} diff --git a/src/main/java/dev/galacticraft/api/block/entity/AtmosphereProvider.java b/src/main/java/dev/galacticraft/api/block/entity/AtmosphereProvider.java new file mode 100644 index 0000000000..cb9bb95d63 --- /dev/null +++ b/src/main/java/dev/galacticraft/api/block/entity/AtmosphereProvider.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.api.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.state.BlockState; + +/** + * Describes a {@link BlockEntity} that generates an atmosphere somewhere in the world. + * + * @see dev.galacticraft.api.accessor.LevelOxygenAccessor + * @see dev.galacticraft.api.accessor.ChunkOxygenAccessor + */ +public interface AtmosphereProvider { + /** + * {@return whether the given point is breathable} + * @param x the x-coordinate to check + * @param y the y-coordinate to check + * @param z the z-coordinate to check + */ + boolean canBreathe(double x, double y, double z); + + /** + * {@return whether the given block position is breathable} + * It is not defined where within the block is (or is not) breathable. + * @param x the x-position to check + * @param y the y-position to check + * @param z the z-position to check + */ + default boolean canBreathe(int x, int y, int z) { + return this.canBreathe(new BlockPos(x, y, z)); + } + + /** + * {@return whether the given block position is breathable} + * It is not defined where within the block is (or is not) breathable. + * @param pos the position to check + */ + boolean canBreathe(BlockPos pos); + + /** + * Called when a block state is updated within a chunk that has this atmosphere provider attached. + * @param pos the position of block that was changed + * @param newState the new state being placed at the position. + */ + void notifyStateChange(BlockPos pos, BlockState newState); + + /** + * Helper method to get a block entity instance. + * @return {@code this} + */ + default BlockEntity be() { + return (BlockEntity) this; + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/accessor/InternalLevelOxygenAccessor.java b/src/main/java/dev/galacticraft/api/block/entity/SpaceFillingAtmosphereProvider.java similarity index 81% rename from src/main/java/dev/galacticraft/impl/internal/accessor/InternalLevelOxygenAccessor.java rename to src/main/java/dev/galacticraft/api/block/entity/SpaceFillingAtmosphereProvider.java index deace84101..55e9fc2ea2 100644 --- a/src/main/java/dev/galacticraft/impl/internal/accessor/InternalLevelOxygenAccessor.java +++ b/src/main/java/dev/galacticraft/api/block/entity/SpaceFillingAtmosphereProvider.java @@ -20,13 +20,7 @@ * SOFTWARE. */ -package dev.galacticraft.impl.internal.accessor; +package dev.galacticraft.api.block.entity; -import org.jetbrains.annotations.ApiStatus; - -@ApiStatus.Internal -public interface InternalLevelOxygenAccessor { - boolean getDefaultBreathable(); - - void setDefaultBreathable(boolean breathable); +public interface SpaceFillingAtmosphereProvider extends AtmosphereProvider { } diff --git a/src/main/java/dev/galacticraft/mod/mixin/LevelMixin.java b/src/main/java/dev/galacticraft/api/oxygen/OxygenUtil.java similarity index 67% rename from src/main/java/dev/galacticraft/mod/mixin/LevelMixin.java rename to src/main/java/dev/galacticraft/api/oxygen/OxygenUtil.java index d1779d191a..b97713d5d2 100644 --- a/src/main/java/dev/galacticraft/mod/mixin/LevelMixin.java +++ b/src/main/java/dev/galacticraft/api/oxygen/OxygenUtil.java @@ -20,21 +20,18 @@ * SOFTWARE. */ -package dev.galacticraft.mod.mixin; +package dev.galacticraft.api.oxygen; -import dev.galacticraft.mod.accessor.GCLevelAccessor; -import dev.galacticraft.mod.machine.SealerManager; -import net.minecraft.world.level.Level; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import net.minecraft.core.BlockPos; -@Mixin(Level.class) -public class LevelMixin implements GCLevelAccessor { - @Unique - private final SealerManager sealerManager = new SealerManager((Level) (Object) this); +import java.util.Iterator; - @Override - public SealerManager galacticraft$getSealerManager() { - return sealerManager; +public class OxygenUtil { + public static boolean isBreathable(BlockPos pos, Iterator iterator) { + while (iterator.hasNext()) { + if (iterator.next().canBreathe(pos)) return true; + } + return false; } } diff --git a/src/main/java/dev/galacticraft/api/registry/ExtinguishableBlockRegistry.java b/src/main/java/dev/galacticraft/api/registry/ExtinguishableBlockRegistry.java deleted file mode 100644 index a8bb3301b0..0000000000 --- a/src/main/java/dev/galacticraft/api/registry/ExtinguishableBlockRegistry.java +++ /dev/null @@ -1,113 +0,0 @@ -/* - * Copyright (c) 2019-2025 Team Galacticraft - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.galacticraft.api.registry; - -import dev.galacticraft.impl.internal.registry.ExtinguishableBlockRegistryImpl; -import net.fabricmc.fabric.api.util.Block2ObjectMap; -import net.minecraft.core.BlockPos; -import net.minecraft.tags.TagKey; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.block.state.BlockState; -import org.jetbrains.annotations.Nullable; - -import java.util.function.Consumer; -import java.util.function.Function; - -public interface ExtinguishableBlockRegistry extends Block2ObjectMap { - ExtinguishableBlockRegistry INSTANCE = new ExtinguishableBlockRegistryImpl(); - - default void add(Block block, BlockState transform) { - this.add(block, state -> transform); - } - - default void add(Block block, Function transform) { - this.add(block, new Entry(transform)); - } - - default void add(Block block, BlockState transform, Consumer callback) { - this.add(block, state -> transform, callback); - } - - default void add(Block block, Function transform, Consumer callback) { - this.add(block, new Entry(transform, callback)); - } - - default void add(TagKey tag, BlockState transform) { - this.add(tag, state -> transform); - } - - default void add(TagKey tag, Function transform) { - this.add(tag, new Entry(transform)); - } - - default void add(TagKey tag, BlockState transform, Consumer callback) { - this.add(tag, state -> transform, callback); - } - - default void add(TagKey tag, Function transform, Consumer callback) { - this.add(tag, new Entry(transform, callback)); - } - - default @Nullable BlockState transform(BlockState oldState) { - Entry entry = this.get(oldState.getBlock()); - if (entry != null) { - BlockState newState = entry.transform(oldState); - return oldState != newState ? newState : null; - } - return null; - } - - final class Entry { - private final Function transform; - private final Consumer callback; - - public Entry(Function transform) { - this.transform = transform; - this.callback = context -> {}; - } - - public Entry(Function transform, Consumer callback) { - this.transform = transform; - this.callback = callback; - } - - public Function getTransform() { - return this.transform; - } - - public BlockState transform(BlockState state) { - return this.transform.apply(state); - } - - public Consumer getCallback() { - return this.callback; - } - - public void callback(Context context) { - this.callback.accept(context); - } - } - - public record Context(Level level, BlockPos pos, BlockState state) {} -} diff --git a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenSyncer.java b/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessorInternal.java similarity index 77% rename from src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenSyncer.java rename to src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessorInternal.java index c29c700f2e..f527953ece 100644 --- a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenSyncer.java +++ b/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessorInternal.java @@ -22,15 +22,16 @@ package dev.galacticraft.impl.internal.accessor; +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; -import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.ApiStatus; import org.jetbrains.annotations.Nullable; -public interface ChunkOxygenSyncer { - default @Nullable OxygenUpdatePayload.OxygenData[] galacticraft$syncOxygenPacketsToClient() { - return null; - } - default void galacticraft$readOxygenUpdate(@NotNull OxygenUpdatePayload.OxygenData[] data) { - } +public interface ChunkOxygenAccessorInternal extends ChunkOxygenAccessor { + @ApiStatus.Internal + void galacticraft$markSectionDirty(int sectionIndex); + + @ApiStatus.Internal + OxygenUpdatePayload.OxygenData @Nullable [] galacticraft$getPendingOxygenChanges(); } diff --git a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkSectionOxygenAccessor.java b/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkSectionOxygenAccessor.java index e3d4bd4d88..e54ca875e8 100644 --- a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkSectionOxygenAccessor.java +++ b/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkSectionOxygenAccessor.java @@ -22,34 +22,28 @@ package dev.galacticraft.impl.internal.accessor; -import net.minecraft.network.FriendlyByteBuf; +import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import org.jetbrains.annotations.ApiStatus; -import org.jetbrains.annotations.NotNull; -import org.jetbrains.annotations.Nullable; -import java.util.BitSet; +import java.util.ArrayList; @ApiStatus.Internal public interface ChunkSectionOxygenAccessor { - default boolean galacticraft$isInverted(int x, int y, int z) { - return this.galacticraft$isInverted(x + (y << 4) + (z << 8)); - } - - boolean galacticraft$isInverted(int pos); - - default void galacticraft$setInverted(int x, int y, int z, boolean inverted) { - this.galacticraft$setInverted(x + (y << 4) + (z << 8), inverted); - } - - void galacticraft$setInverted(int pos, boolean value); + boolean galacticraft$hasProvider(BlockPos pos); + boolean galacticraft$addProvider(BlockPos pos); + boolean galacticraft$removeProvider(BlockPos pos); boolean galacticraft$isEmpty(); - @Nullable BitSet galacticraft$getBits(); + void galacticraft$writeTag(CompoundTag apiTag); + void galacticraft$readTag(CompoundTag apiTag); - void galacticraft$setBits(@Nullable BitSet set); + OxygenUpdatePayload.OxygenSectionData galacticraft$updatePayload(); - void galacticraft$writeOxygenPacket(@NotNull FriendlyByteBuf buf); + void galacticraft$loadData(OxygenUpdatePayload.OxygenSectionData data); - void galacticraft$readOxygenPacket(@NotNull FriendlyByteBuf buf); + @ApiStatus.Internal + ArrayList galacticraft$getRawProviders(); } diff --git a/src/main/java/dev/galacticraft/impl/internal/command/GCApiCommands.java b/src/main/java/dev/galacticraft/impl/internal/command/GCApiCommands.java index adf4756b4b..23c433bf25 100644 --- a/src/main/java/dev/galacticraft/impl/internal/command/GCApiCommands.java +++ b/src/main/java/dev/galacticraft/impl/internal/command/GCApiCommands.java @@ -23,7 +23,6 @@ package dev.galacticraft.impl.internal.command; import com.mojang.brigadier.arguments.ArgumentType; -import com.mojang.brigadier.arguments.BoolArgumentType; import com.mojang.brigadier.arguments.IntegerArgumentType; import com.mojang.brigadier.builder.LiteralArgumentBuilder; import com.mojang.brigadier.context.CommandContext; @@ -90,41 +89,13 @@ public static void register() { commandDispatcher.register(builder); builder = Commands.literal(Constant.MOD_ID + ":oxygen").requires(source -> source.hasPermission(3)); builder.then(Commands.literal("get").then(Commands.argument("start_pos", BlockPosArgument.blockPos()).executes(GCApiCommands::getOxygen).then(Commands.argument("end_pos", BlockPosArgument.blockPos()).executes(GCApiCommands::getOxygenArea)))); - builder.then(Commands.literal("set").requires(source -> source.hasPermission(4)).then(Commands.argument("start_pos", BlockPosArgument.blockPos()).then(Commands.argument("oxygen", BoolArgumentType.bool()).executes(GCApiCommands::setOxygen)).then(Commands.argument("end_pos", BlockPosArgument.blockPos()).then(Commands.argument("oxygen", BoolArgumentType.bool()).executes(GCApiCommands::setOxygenArea))))); commandDispatcher.register(builder); }); } - private static int setOxygen(CommandContext context) throws CommandSyntaxException { - BlockPos pos = BlockPosArgument.getLoadedBlockPos(context, "start_pos"); - boolean b = BoolArgumentType.getBool(context, "oxygen"); - context.getSource().getLevel().setBreathable(pos, b); - context.getSource().sendSuccess(() -> Component.translatable(Translations.SetOxygen.SUCCESS_SINGLE), true); - return 1; - } - - private static int setOxygenArea(CommandContext context) throws CommandSyntaxException { - BlockPos startPos = BlockPosArgument.getLoadedBlockPos(context, "start_pos"); - BlockPos endPos = BlockPosArgument.getLoadedBlockPos(context, "end_pos"); - LevelOxygenAccessor accessor = context.getSource().getLevel(); - BoundingBox box = BoundingBox.fromCorners(startPos, endPos); - boolean b = BoolArgumentType.getBool(context, "oxygen"); - BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); - for (int x = box.minX(); x <= box.maxX(); x++) { - for (int y = box.minX(); y <= box.maxX(); y++) { - for (int z = box.minX(); z <= box.maxX(); z++) { - accessor.setBreathable(mutable.set(x, y, z), b); - } - } - } - - context.getSource().sendSuccess(() -> Component.translatable(Translations.SetOxygen.SUCCESS_MULTIPLE), true); - return 1; - } - private static int getOxygen(CommandContext context) throws CommandSyntaxException { BlockPos pos = BlockPosArgument.getLoadedBlockPos(context, "start_pos"); - if (context.getSource().getLevel().isBreathable(pos)) { + if (context.getSource().getLevel().galacticraft$isBreathable(pos)) { context.getSource().sendSuccess(() -> Component.translatable(Translations.SetOxygen.OXYGEN_EXISTS), false); } else { context.getSource().sendSuccess(() -> Component.translatable(Translations.SetOxygen.NO_OXYGEN_EXISTS), false); @@ -144,7 +115,7 @@ private static int getOxygenArea(CommandContext context) thr for (int x = box.minX(); x <= box.maxX(); x++) { for (int y = box.minX(); y <= box.maxX(); y++) { for (int z = box.minX(); z <= box.maxX(); z++) { - breathable = accessor.isBreathable(mutable.set(x, y, z)); + breathable = accessor.galacticraft$isBreathable(mutable.set(x, y, z)); hasSomeOxygen |= breathable; allOxygen = allOxygen && breathable; } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/gear/LivingEntityMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/gear/LivingEntityMixin.java index 4b70194a71..a99f72ae48 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/gear/LivingEntityMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/gear/LivingEntityMixin.java @@ -47,7 +47,6 @@ import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; -import net.minecraft.core.Direction; import net.minecraft.core.registries.Registries; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.syncher.EntityDataAccessor; @@ -146,7 +145,7 @@ private void galacticraft_oxygenCheck(CallbackInfo ci) { LivingEntity entity = ((LivingEntity) (Object) this); if (entity.galacticraft$oxygenConsumptionRate() == 0) return; AttributeInstance attribute = entity.getAttribute(GcApiEntityAttributes.CAN_BREATHE_IN_SPACE); - if (!entity.level().isBreathable(entity.blockPosition().relative(Direction.UP, (int) Math.floor(entity.getEyeHeight(entity.getPose())))) && !(attribute != null && attribute.getValue() >= 0.99D)) { + if (!entity.level().galacticraft$isBreathable(entity.getEyePosition()) && !(attribute != null && attribute.getValue() >= 0.99D)) { if (!entity.isEyeInFluid(GCFluidTags.NON_BREATHABLE) && !(entity instanceof Player player && player.getAbilities().invulnerable)) { entity.setAirSupply(this.decreaseAirSupply(entity.getAirSupply())); if (entity.getAirSupply() == -20) { @@ -174,7 +173,7 @@ private boolean galacticraft_testForBreathability(boolean original) { this.lastHurtBySuffocationTimestamp = this.tickCount; return false; } - return original || this.isEyeInFluid(GCFluidTags.NON_BREATHABLE) || !entity.level().isBreathable(entity.blockPosition().relative(Direction.UP, (int) Math.floor(this.getEyeHeight(entity.getPose())))); + return original || this.isEyeInFluid(GCFluidTags.NON_BREATHABLE) || !entity.level().galacticraft$isBreathable(entity.getEyePosition()); } @ModifyExpressionValue(method = "baseTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/LivingEntity;canBreatheUnderwater()Z")) @@ -191,7 +190,7 @@ private void galacticraft_modifyAirLevel(int air, CallbackInfoReturnable= 0.99D) || - entity.level().isBreathable(entity.blockPosition().relative(Direction.UP, (int) Math.floor(entity.getEyeHeight(entity.getPose())))) + entity.level().galacticraft$isBreathable(entity.getEyePosition()) )) { this.lastHurtBySuffocationTimestamp = this.tickCount; cir.setReturnValue(this.increaseAirSupply(air)); diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/gear/ServerPlayerMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/gear/ServerPlayerMixin.java index b5256690ee..a7b11a5801 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/gear/ServerPlayerMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/gear/ServerPlayerMixin.java @@ -35,7 +35,6 @@ import net.fabricmc.fabric.api.transfer.v1.item.InventoryStorage; import net.fabricmc.fabric.api.transfer.v1.storage.Storage; import net.fabricmc.fabric.api.transfer.v1.transaction.Transaction; -import net.minecraft.core.Direction; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.server.level.ServerLevel; @@ -73,7 +72,7 @@ private void galacticraft_parrotOxygenCheck(CallbackInfo ci) { CompoundTag leftParrot = this.getShoulderEntityLeft(); CompoundTag rightParrot = this.getShoulderEntityRight(); if ((!leftParrot.isEmpty() || !rightParrot.isEmpty()) - && !this.level().isBreathable(this.blockPosition().relative(Direction.UP, (int) Math.floor(this.getEyeHeight(this.getPose()))))) { + && !this.level().galacticraft$isBreathable(this.getEyePosition())) { long rate = Galacticraft.CONFIG.parrotOxygenConsumptionRate(); if (!leftParrot.isEmpty()) { SimpleContainer inv = new GearInventory(); diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkAccessMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkAccessMixin.java new file mode 100644 index 0000000000..131ed4dff9 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkAccessMixin.java @@ -0,0 +1,117 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen; + +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; +import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; +import dev.galacticraft.impl.internal.oxygen.ApPosIterator; +import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.Unique; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; + +@Mixin(ChunkAccess.class) +public abstract class ChunkAccessMixin implements ChunkOxygenAccessorInternal { + @Shadow + @Final + protected LevelChunkSection[] sections; + + @Shadow + @Final + protected LevelHeightAccessor levelHeightAccessor; + + @Shadow + public abstract void setUnsaved(boolean needsSaving); + + @Unique + private short dirtySections = 0; + + @Override + public Iterator galacticraft$getProviders(int y) { + return Collections.emptyIterator(); + } + + @Override + public Iterator galacticraft$getProviderPositions(int y) { + int sectionIndex = this.levelHeightAccessor.getSectionIndex(y); + ArrayList positions = ((ChunkSectionOxygenAccessor) this.sections[sectionIndex]).galacticraft$getRawProviders(); + if (positions == null) return Collections.emptyIterator(); + return new ApPosIterator((ChunkAccess) (Object) this, positions.iterator(), sectionIndex); + } + + @Override + public void galacticraft$markSectionDirty(int sectionIndex) { + this.dirtySections |= (short) (0b1 << sectionIndex); + this.setUnsaved(true); + } + + @Override + public void galacticraft$addAtmosphereProvider(int sectionIndex, BlockPos provider) { + if (((ChunkSectionOxygenAccessor) this.sections[sectionIndex]).galacticraft$addProvider(provider)) { + this.galacticraft$markSectionDirty(sectionIndex); + } + } + + @Override + public void galacticraft$removeAtmosphereProvider(int sectionIndex, BlockPos provider) { + if (((ChunkSectionOxygenAccessor) this.sections[sectionIndex]).galacticraft$removeProvider(provider)) { + this.galacticraft$markSectionDirty(sectionIndex); + } + } + + @Override + public @Nullable OxygenUpdatePayload.OxygenData[] galacticraft$getPendingOxygenChanges() { + if (this.dirtySections != 0) { + int count = 0; + for (int i = 0; i < this.sections.length; i++) { + if ((this.dirtySections & (0b1 << i)) != 0) { + count++; + } + } + + OxygenUpdatePayload.OxygenData[] data = new OxygenUpdatePayload.OxygenData[count]; + + int idx = 0; + for (byte i = 0; i < this.sections.length; i++) { + if ((this.dirtySections & (0b1 << i)) != 0) { + data[idx++] = new OxygenUpdatePayload.OxygenData(i, ((ChunkSectionOxygenAccessor) this.sections[i]).galacticraft$updatePayload()); + } + } + this.dirtySections = 0; + return data; + } + return null; + } + +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkHolderMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkHolderMixin.java index 7f40a50626..e106f0e4ce 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkHolderMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkHolderMixin.java @@ -22,14 +22,12 @@ package dev.galacticraft.impl.internal.mixin.oxygen; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenSyncer; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.network.protocol.Packet; import net.minecraft.server.level.ChunkHolder; -import net.minecraft.server.level.GenerationChunkHolder; import net.minecraft.server.level.ServerPlayer; -import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.chunk.LevelChunk; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; @@ -49,12 +47,11 @@ public abstract class ChunkHolderMixin { @Final private ChunkHolder.PlayerProvider playerProvider; - @Inject(method = "broadcastChanges", at = @At("HEAD")) - private void galacticraft_flushOxygenPackets(LevelChunk chunk, CallbackInfo ci) { - OxygenUpdatePayload.OxygenData[] data = ((ChunkOxygenSyncer) chunk).galacticraft$syncOxygenPacketsToClient(); + @Inject(method = "broadcastChanges", at = @At("RETURN")) + private void flushOxygenPackets(LevelChunk chunk, CallbackInfo ci) { + OxygenUpdatePayload.OxygenData[] data = ((ChunkOxygenAccessorInternal) chunk).galacticraft$getPendingOxygenChanges(); if (data != null) { - ChunkPos pos = ((GenerationChunkHolder) (Object) this).getPos(); - this.broadcast(this.playerProvider.getPlayers(pos, false), ServerPlayNetworking.createS2CPacket(new OxygenUpdatePayload(pos.toLong(), data))); + this.broadcast(this.playerProvider.getPlayers(chunk.getPos(), false), ServerPlayNetworking.createS2CPacket(new OxygenUpdatePayload(chunk.getPos().toLong(), data))); } } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkSerializerMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkSerializerMixin.java index 43c27149ef..900c432443 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkSerializerMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ChunkSerializerMixin.java @@ -22,53 +22,35 @@ package dev.galacticraft.impl.internal.mixin.oxygen; -import com.mojang.serialization.Codec; +import com.llamalad7.mixinextras.sugar.Local; import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; import dev.galacticraft.mod.Constant; -import net.minecraft.core.Holder; -import net.minecraft.core.Registry; -import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.ListTag; -import net.minecraft.nbt.Tag; import net.minecraft.server.level.ServerLevel; import net.minecraft.world.entity.ai.village.poi.PoiManager; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.level.chunk.*; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.ProtoChunk; import net.minecraft.world.level.chunk.storage.ChunkSerializer; import net.minecraft.world.level.chunk.storage.RegionStorageInfo; -import net.minecraft.world.level.levelgen.BelowZeroRetrogen; -import net.minecraft.world.level.levelgen.blending.BlendingData; -import net.minecraft.world.level.lighting.LevelLightEngine; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; -import java.util.BitSet; -import java.util.Objects; @Mixin(ChunkSerializer.class) public abstract class ChunkSerializerMixin { - @Inject(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunkSection;getStates()Lnet/minecraft/world/level/chunk/PalettedContainer;"), locals = LocalCapture.CAPTURE_FAILHARD) - private static void galacticraft_serializeOxygen(ServerLevel world, ChunkAccess chunk, CallbackInfoReturnable cir, ChunkPos chunkPos, CompoundTag nbtCompound, BlendingData blendingData, BelowZeroRetrogen belowZeroRetrogen, UpgradeData upgradeData, LevelChunkSection[] chunkSections, ListTag nbtList, LevelLightEngine lightingProvider, Registry registry, Codec>> codec, boolean bl, int i, int j, boolean bl2, DataLayer chunkNibbleArray, DataLayer chunkNibbleArray2, CompoundTag nbtCompound2, LevelChunkSection section) { - var accessor = (ChunkSectionOxygenAccessor) section; - if (!accessor.galacticraft$isEmpty()) { - CompoundTag nbt = new CompoundTag(); - nbt.putByteArray(Constant.Nbt.OXYGEN, Objects.requireNonNull(accessor.galacticraft$getBits()).toByteArray()); - nbtCompound2.put(Constant.Nbt.GC_API, nbt); - } + @Inject(method = "write", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/chunk/LevelChunkSection;getStates()Lnet/minecraft/world/level/chunk/PalettedContainer;")) + private static void serializeOxygen(ServerLevel world, ChunkAccess chunk, CallbackInfoReturnable cir, @Local(ordinal = 1) CompoundTag sectionTag, @Local LevelChunkSection section) { + CompoundTag apiTag = new CompoundTag(); + ((ChunkSectionOxygenAccessor) section).galacticraft$writeTag(apiTag); + if (!apiTag.isEmpty()) sectionTag.put(Constant.Nbt.GC_API, apiTag); } - @Inject(method = "read", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;checkConsistencyWithBlocks(Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/LevelChunkSection;)V"), locals = LocalCapture.CAPTURE_FAILHARD) - private static void galacticraft_deserializeOxygen(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos chunkPos, CompoundTag nbt, CallbackInfoReturnable cir, ChunkPos chunkPos2, UpgradeData upgradeData, boolean bl, ListTag listTag, int i, LevelChunkSection[] levelChunkSections, boolean bl2, ChunkSource chunkSource, LevelLightEngine levelLightEngine, Registry registry, Codec codec, boolean bl3, int j, CompoundTag compoundTag, int k, int l, PalettedContainer palettedContainer, PalettedContainerRO palettedContainerRO, LevelChunkSection levelChunkSection, SectionPos sectionPos) { - CompoundTag apiCompound = compoundTag.getCompound(Constant.Nbt.GC_API); - if (apiCompound.contains(Constant.Nbt.OXYGEN, Tag.TAG_BYTE_ARRAY)) { - ((ChunkSectionOxygenAccessor) levelChunkSection).galacticraft$setBits(BitSet.valueOf(apiCompound.getByteArray(Constant.Nbt.OXYGEN))); - } else { - ((ChunkSectionOxygenAccessor) levelChunkSection).galacticraft$setBits(null); - } + @Inject(method = "read", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/entity/ai/village/poi/PoiManager;checkConsistencyWithBlocks(Lnet/minecraft/core/SectionPos;Lnet/minecraft/world/level/chunk/LevelChunkSection;)V")) + private static void deserializeOxygen(ServerLevel world, PoiManager poiStorage, RegionStorageInfo key, ChunkPos chunkPos, CompoundTag nbt, CallbackInfoReturnable cir, @Local(ordinal = 1) CompoundTag sectionTag, @Local LevelChunkSection section) { + ((ChunkSectionOxygenAccessor) section).galacticraft$readTag(sectionTag.getCompound(Constant.Nbt.GC_API)); } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/EmptyLevelChunkMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/EmptyLevelChunkMixin.java index b85b319cfa..ec0cc4fea9 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/EmptyLevelChunkMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/EmptyLevelChunkMixin.java @@ -22,19 +22,43 @@ package dev.galacticraft.impl.internal.mixin.oxygen; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessor; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenSyncer; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; +import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; +import net.minecraft.core.BlockPos; import net.minecraft.world.level.chunk.EmptyLevelChunk; +import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; +import java.util.Collections; +import java.util.Iterator; + @Mixin(EmptyLevelChunk.class) -public abstract class EmptyLevelChunkMixin implements ChunkOxygenSyncer, ChunkOxygenAccessor { +public abstract class EmptyLevelChunkMixin implements ChunkOxygenAccessorInternal { + @Override + public @Nullable OxygenUpdatePayload.OxygenData[] galacticraft$getPendingOxygenChanges() { + return null; + } + + @Override + public void galacticraft$markSectionDirty(int sectionIndex) { + } + + @Override + public Iterator galacticraft$getProviders(int y) { + return Collections.emptyIterator(); + } + + @Override + public Iterator galacticraft$getProviderPositions(int y) { + return Collections.emptyIterator(); + } + @Override - public boolean galacticraft$isInverted(int x, int y, int z) { - return false; + public void galacticraft$addAtmosphereProvider(int sectionIndex, BlockPos provider) { } @Override - public void galacticraft$setInverted(int x, int y, int z, boolean inverted) { + public void galacticraft$removeAtmosphereProvider(int sectionIndex, BlockPos provider) { } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ImposterProtoChunkMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ImposterProtoChunkMixin.java index f3882cba1c..81c0091e31 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ImposterProtoChunkMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ImposterProtoChunkMixin.java @@ -22,15 +22,22 @@ package dev.galacticraft.impl.internal.mixin.oxygen; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessor; +import com.google.common.collect.Iterators; +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; +import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; +import net.minecraft.core.BlockPos; import net.minecraft.world.level.chunk.ImposterProtoChunk; import net.minecraft.world.level.chunk.LevelChunk; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; +import java.util.Iterator; + @Mixin(ImposterProtoChunk.class) -public abstract class ImposterProtoChunkMixin implements ChunkOxygenAccessor { +public abstract class ImposterProtoChunkMixin implements ChunkOxygenAccessorInternal { @Shadow @Final private boolean allowWrites; @@ -39,14 +46,40 @@ public abstract class ImposterProtoChunkMixin implements ChunkOxygenAccessor { private LevelChunk wrapped; @Override - public boolean galacticraft$isInverted(int x, int y, int z) { - return ((ChunkOxygenAccessor) this.wrapped).galacticraft$isInverted(x, y, z); + public Iterator galacticraft$getProviders(int y) { + Iterator iterator = ((ChunkOxygenAccessor) this.wrapped).galacticraft$getProviders(y); + return this.allowWrites ? iterator : Iterators.unmodifiableIterator(iterator); //todo: removal based on loading can still occur with blocked writes + } + + @Override + public Iterator galacticraft$getProviderPositions(int y) { + Iterator iterator = ((ChunkOxygenAccessor) this.wrapped).galacticraft$getProviderPositions(y); + return this.allowWrites ? iterator : Iterators.unmodifiableIterator(iterator); + } + + @Override + public void galacticraft$markSectionDirty(int sectionIndex) { + if (this.allowWrites) { + ((ChunkOxygenAccessorInternal) this.wrapped).galacticraft$markSectionDirty(sectionIndex); + } + } + + @Override + public OxygenUpdatePayload.OxygenData[] galacticraft$getPendingOxygenChanges() { + return ((ChunkOxygenAccessorInternal) this.wrapped).galacticraft$getPendingOxygenChanges(); + } + + @Override + public void galacticraft$addAtmosphereProvider(int sectionIndex, BlockPos provider) { + if (this.allowWrites) { + ((ChunkOxygenAccessor) this.wrapped).galacticraft$addAtmosphereProvider(sectionIndex, provider); + } } @Override - public void galacticraft$setInverted(int x, int y, int z, boolean inverted) { + public void galacticraft$removeAtmosphereProvider(int sectionIndex, BlockPos provider) { if (this.allowWrites) { - ((ChunkOxygenAccessor) this.wrapped).galacticraft$setInverted(x, y, z, inverted); + ((ChunkOxygenAccessor) this.wrapped).galacticraft$removeAtmosphereProvider(sectionIndex, provider); } } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelAccessorMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelAccessorMixin.java new file mode 100644 index 0000000000..e5721667ca --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelAccessorMixin.java @@ -0,0 +1,45 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen; + +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; +import dev.galacticraft.api.accessor.LevelOxygenAccessor; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.LevelAccessor; +import org.spongepowered.asm.mixin.Mixin; + +// all base classes for LevelAccessor +@Mixin({Level.class, WorldGenRegion.class}) +public abstract class LevelAccessorMixin implements LevelAccessor, LevelOxygenAccessor { + @Override + public void galacticraft$removeAtmosphereProvider(int sectionX, int sectionY, int sectionZ, BlockPos provider) { + ((ChunkOxygenAccessor) this.getChunk(sectionX, sectionZ)).galacticraft$removeAtmosphereProvider(sectionY, provider); + } + + @Override + public void galacticraft$addAtmosphereProvider(int sectionX, int sectionY, int sectionZ, BlockPos provider) { + ((ChunkOxygenAccessor) this.getChunk(sectionX, sectionZ)).galacticraft$addAtmosphereProvider(sectionY, provider); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkMixin.java index ddc563c8f0..0b4478566f 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkMixin.java @@ -22,110 +22,69 @@ package dev.galacticraft.impl.internal.mixin.oxygen; -import com.llamalad7.mixinextras.injector.wrapoperation.Operation; -import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessor; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenSyncer; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; -import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; -import dev.galacticraft.mod.events.GCEventHandlers; +import dev.galacticraft.impl.internal.oxygen.ProviderIterator; import net.minecraft.core.BlockPos; import net.minecraft.core.Registry; import net.minecraft.world.level.ChunkPos; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.biome.Biome; +import net.minecraft.world.level.block.entity.BlockEntity; import net.minecraft.world.level.block.state.BlockState; import net.minecraft.world.level.chunk.ChunkAccess; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.chunk.LevelChunkSection; import net.minecraft.world.level.chunk.UpgradeData; import net.minecraft.world.level.levelgen.blending.BlendingData; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.BitSet; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; @Mixin(LevelChunk.class) -public abstract class LevelChunkMixin extends ChunkAccess implements ChunkOxygenAccessor, ChunkOxygenSyncer { +public abstract class LevelChunkMixin extends ChunkAccess implements ChunkOxygenAccessorInternal { @Shadow @Final Level level; - private @Unique short dirtySections = 0b0; - private LevelChunkMixin(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biome, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable BlendingData blendingData) { - super(pos, upgradeData, heightLimitView, biome, inhabitedTime, sectionArrayInitializer, blendingData); + public LevelChunkMixin(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biomeRegistry, long inhabitedTime, @Nullable LevelChunkSection[] sectionArray, @Nullable BlendingData blendingData) { + super(pos, upgradeData, heightLimitView, biomeRegistry, inhabitedTime, sectionArray, blendingData); } @Override - public boolean galacticraft$isInverted(int x, int y, int z) { - return ((ChunkSectionOxygenAccessor) this.sections[this.getSectionIndex(y)]).galacticraft$isInverted(x, y & 15, z); + public Iterator galacticraft$getProviders(int y) { + int sectionIndex = this.levelHeightAccessor.getSectionIndex(y); + ArrayList positions = ((ChunkSectionOxygenAccessor) this.sections[sectionIndex]).galacticraft$getRawProviders(); + if (positions == null) return Collections.emptyIterator(); + return new ProviderIterator(this.level, this, positions.listIterator(), sectionIndex); } - @Override - public void galacticraft$setInverted(int x, int y, int z, boolean inverted) { - var accessor = ((ChunkSectionOxygenAccessor) this.sections[this.getSectionIndex(y)]); - if (inverted != accessor.galacticraft$isInverted(x, y & 15, z)) { - if (!this.level.isClientSide) { - this.unsaved = true; - this.dirtySections |= (short) (0b1 << this.getSectionIndex(y)); - } - accessor.galacticraft$setInverted(x, y & 15, z, inverted); - } - } - - @Override - public @Nullable OxygenUpdatePayload.OxygenData[] galacticraft$syncOxygenPacketsToClient() { - assert !this.level.isClientSide; - if (this.dirtySections != 0b0) { - int count = 0; - for (int i = 0; i < this.sections.length; i++) { - if ((this.dirtySections & (0b1 << i)) != 0) { - count++; - } - } - - OxygenUpdatePayload.OxygenData[] data = new OxygenUpdatePayload.OxygenData[count]; + @Inject(method = "setBlockState", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;", ordinal = 0)) + private void notifyAPsOnBlockChange(BlockPos pos, BlockState blockState, boolean bl, CallbackInfoReturnable cir) { + if (this.level.isClientSide) return; - int idx = 0; - for (byte i = 0; i < this.sections.length; i++) { - if ((this.dirtySections & (0b1 << i)) != 0) { - BitSet data1 = ((ChunkSectionOxygenAccessor) this.sections[i]).galacticraft$getBits(); - data[idx++] = new OxygenUpdatePayload.OxygenData(i, data1 == null ? new BitSet(0) : data1); + Iterator iterator = this.galacticraft$getProviderPositions(pos.getY()); + while (iterator.hasNext()) { + BlockPos atPos = iterator.next(); + BlockEntity blockEntity = this.level.getBlockEntity(atPos); + if (blockEntity instanceof AtmosphereProvider provider) { + provider.notifyStateChange(pos, blockState); + } else { + for (int i = 0; i < this.sections.length; i++) { + ((ChunkSectionOxygenAccessor) this.sections[i]).galacticraft$removeProvider(pos); + this.galacticraft$markSectionDirty(i); } } - this.dirtySections = 0; - return data; - } - return null; - } - - @Override - public void galacticraft$readOxygenUpdate(@NotNull OxygenUpdatePayload.OxygenData[] buf) { - for (OxygenUpdatePayload.@NotNull OxygenData oxygenData : buf) { - ((ChunkSectionOxygenAccessor) this.sections[oxygenData.section()]).galacticraft$setBits(oxygenData.data()); - } - } - - @Inject(method = "setBlockState", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;getBlock()Lnet/minecraft/world/level/block/Block;", ordinal = 0)) - private void resetAirOnBlockChange(BlockPos pos, BlockState blockState, boolean bl, CallbackInfoReturnable cir) { - // TODO: Better system for checking whether the oxygen should be reset - if (blockState.isSolidRender(this.level, pos)) { - this.galacticraft$setInverted(pos.getX() & 15, pos.getY(), pos.getZ() & 15, false); - } - } - - @WrapOperation(method = "setBlockState", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;onPlace(Lnet/minecraft/world/level/Level;Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;Z)V", ordinal = 0)) - private void extinguishFire(BlockState newState, Level level, BlockPos pos, BlockState oldState, boolean bl, Operation original) { - if (level.isBreathable(pos) || !GCEventHandlers.extinguishBlock(level, pos, newState)) { - original.call(newState, level, pos, oldState, bl); } } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkSectionMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkSectionMixin.java index 90678af285..f0d04ff69c 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkSectionMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelChunkSectionMixin.java @@ -22,98 +22,146 @@ package dev.galacticraft.impl.internal.mixin.oxygen; +import com.google.common.collect.Lists; +import com.llamalad7.mixinextras.injector.ModifyReturnValue; import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; +import dev.galacticraft.impl.network.s2c.OxygenUpdatePayload; +import dev.galacticraft.mod.Constant; +import net.minecraft.core.BlockPos; +import net.minecraft.nbt.CompoundTag; import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.VarInt; import net.minecraft.world.level.chunk.LevelChunkSection; -import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import java.util.BitSet; +import java.util.ArrayList; @Mixin(LevelChunkSection.class) public abstract class LevelChunkSectionMixin implements ChunkSectionOxygenAccessor { - private @Unique - @Nullable BitSet bits = null; + private @Unique @Nullable ArrayList providers = null; @Override - public boolean galacticraft$isInverted(int pos) { - return this.bits != null && this.bits.get(pos); + public boolean galacticraft$hasProvider(BlockPos pos) { + if (this.providers == null) return false; + return this.providers.contains(pos); } @Override - public void galacticraft$setInverted(int pos, boolean value) { - if (value) { - if (this.bits == null) this.bits = new BitSet(pos); - this.bits.set(pos); - } else if (this.bits != null) { - this.bits.clear(pos); + public boolean galacticraft$addProvider(BlockPos pos) { + if (this.providers == null) { + this.providers = Lists.newArrayList(pos); + return true; + } else if (!this.galacticraft$hasProvider(pos)) { + this.providers.add(pos); + return true; } + return false; } - @Inject(method = "getSerializedSize", at = @At("RETURN"), cancellable = true) - private void increaseChunkPacketSize(CallbackInfoReturnable cir) { - if (this.bits == null) { - cir.setReturnValue(cir.getReturnValueI() + VarInt.getByteSize(0)); - } else { - byte[] byteArray = this.bits.toByteArray(); - cir.setReturnValue(cir.getReturnValueI() + (VarInt.getByteSize(byteArray.length) + byteArray.length)); + @Override + public boolean galacticraft$removeProvider(BlockPos pos) { + if (this.providers == null) return false; + if (this.providers.size() == 1) { + if (this.providers.getFirst().equals(pos)) { + this.providers = null; + return true; + } + return false; } - } - @Inject(method = "hasOnlyAir()Z", at = @At("RETURN"), cancellable = true) - private void verifyOxygenEmpty(CallbackInfoReturnable cir) { - cir.setReturnValue(cir.getReturnValueZ() && this.galacticraft$isEmpty()); + return this.providers.remove(pos); } - @Inject(method = "write", at = @At("RETURN")) - private void writeOxygenDataToPacket(FriendlyByteBuf buf, CallbackInfo ci) { - this.galacticraft$writeOxygenPacket(buf); + @Override + public boolean galacticraft$isEmpty() { + return this.providers == null; } - @Inject(method = "read", at = @At("RETURN")) - private void galacticraft_fromPacket(FriendlyByteBuf buf, CallbackInfo ci) { - this.galacticraft$readOxygenPacket(buf); + @Override + public void galacticraft$writeTag(CompoundTag apiTag) { + if (this.providers != null) { + long[] serialized = new long[this.providers.size()]; + for (int i = 0; i < this.providers.size(); i++) { + serialized[i] = this.providers.get(i).asLong(); + } + apiTag.putLongArray(Constant.Nbt.SRC, serialized); + } } @Override - public boolean galacticraft$isEmpty() { - return this.bits == null || this.bits.isEmpty(); + public void galacticraft$readTag(CompoundTag apiTag) { + long[] serialized = apiTag.getLongArray(Constant.Nbt.SRC); + if (serialized.length > 0) { + this.providers = new ArrayList<>(serialized.length); + for (long l : serialized) { + this.providers.add(BlockPos.of(l)); + } + } else { + this.providers = null; + } } @Override - public BitSet galacticraft$getBits() { - return this.bits; + public OxygenUpdatePayload.OxygenSectionData galacticraft$updatePayload() { + return new OxygenUpdatePayload.OxygenSectionData(this.providers != null ? this.providers.toArray(BlockPos[]::new) : new BlockPos[0]); } @Override - public void galacticraft$setBits(@Nullable BitSet set) { - this.bits = set; + public void galacticraft$loadData(OxygenUpdatePayload.OxygenSectionData data) { + BlockPos[] positions = data.positions(); + if (positions.length == 0) { + this.providers = null; + } else { + this.providers = Lists.newArrayList(positions); + } } @Override - public void galacticraft$writeOxygenPacket(@NotNull FriendlyByteBuf buf) { - if (this.bits != null) { - byte[] bytes = this.bits.toByteArray(); - buf.writeByteArray(bytes); + public ArrayList galacticraft$getRawProviders() { + return this.providers; + } + + @ModifyReturnValue(method = "getSerializedSize", at = @At("RETURN")) + private int increaseChunkPacketSize(int original) { + if (this.providers != null) { + return original + VarInt.getByteSize(this.providers.size()) + Long.BYTES * this.providers.size(); + } else { + return original + VarInt.getByteSize(0); + } + } + + @ModifyReturnValue(method = "hasOnlyAir()Z", at = @At("RETURN")) + private boolean verifyOxygenEmpty(boolean original) { + return original && this.galacticraft$isEmpty(); + } + + @Inject(method = "write", at = @At("RETURN")) + private void writeOxygenDataToPacket(FriendlyByteBuf buf, CallbackInfo ci) { + if (this.providers != null) { + buf.writeVarInt(this.providers.size()); + for (BlockPos provider : this.providers) { + buf.writeLong(provider.asLong()); + } } else { buf.writeVarInt(0); } } - @Override - public void galacticraft$readOxygenPacket(@NotNull FriendlyByteBuf buf) { - byte[] bytes = buf.readByteArray(); - if (bytes.length != 0) { - this.bits = BitSet.valueOf(bytes); + @Inject(method = "read", at = @At("RETURN")) + private void readOxygenFromPacket(FriendlyByteBuf buf, CallbackInfo ci) { + int size = buf.readVarInt(); + if (size == 0) { + this.providers = null; } else { - this.bits = null; + this.providers = new ArrayList<>(size); + for (int i = 0; i < size; i++) { + this.providers.add(BlockPos.of(buf.readLong())); + } } } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelMixin.java index eb060709c0..e342e5fac5 100644 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelMixin.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/LevelMixin.java @@ -22,23 +22,23 @@ package dev.galacticraft.impl.internal.mixin.oxygen; +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; import dev.galacticraft.api.accessor.LevelBodyAccessor; import dev.galacticraft.api.accessor.LevelOxygenAccessor; +import dev.galacticraft.api.block.entity.AtmosphereProvider; import dev.galacticraft.api.universe.celestialbody.CelestialBody; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessor; -import dev.galacticraft.impl.internal.accessor.InternalLevelOxygenAccessor; -import dev.galacticraft.mod.events.GCEventHandlers; import net.minecraft.core.BlockPos; import net.minecraft.core.Holder; import net.minecraft.core.RegistryAccess; import net.minecraft.core.SectionPos; import net.minecraft.resources.ResourceKey; +import net.minecraft.util.Mth; import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; +import net.minecraft.world.level.LevelHeightAccessor; import net.minecraft.world.level.chunk.LevelChunk; import net.minecraft.world.level.storage.WritableLevelData; import org.jetbrains.annotations.NotNull; -import org.spongepowered.asm.mixin.Final; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; import org.spongepowered.asm.mixin.Unique; @@ -46,82 +46,72 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.Iterator; import java.util.function.Supplier; @Mixin(Level.class) -public abstract class LevelMixin implements LevelOxygenAccessor, InternalLevelOxygenAccessor, LevelAccessor { - private @Unique boolean breathable = true; +public abstract class LevelMixin implements LevelOxygenAccessor, LevelAccessor, LevelHeightAccessor { + @Unique + private boolean breathable = true; @Shadow public abstract @NotNull LevelChunk getChunk(int i, int j); - @Shadow - @Final - private ResourceKey dimension; - @Inject(method = "", at = @At("RETURN")) - private void initializeOxygenValues(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i, CallbackInfo ci) { - Holder> holder1 = ((LevelBodyAccessor) this).galacticraft$getCelestialBody(); - this.setDefaultBreathable(holder1 != null ? holder1.value().atmosphere().breathable() : this.breathable); + private void initializeOxygenValues(WritableLevelData writableLevelData, ResourceKey resourceKey, RegistryAccess registryAccess, Holder holder, Supplier supplier, boolean bl, boolean bl2, long l, int i, CallbackInfo ci) { + Holder> body = ((LevelBodyAccessor) this).galacticraft$getCelestialBody(); + this.breathable = body == null || body.value().atmosphere().breathable(); } @Override - public boolean isBreathable(int x, int y, int z) { - if (this.validPosition(x, y, z)) { - return this.isBreathableChunk(this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)), x & 15, y, z & 15); - } - return this.breathable/* && y < this.getMaxBuildHeight() * 2*/; + public Iterator galacticraft$getAtmosphereProviders(int x, int y, int z) { + if (y < this.getMinBuildHeight()) y = this.getMinBuildHeight(); + if (y >= this.getMaxBuildHeight()) y = this.getMaxBuildHeight() - 1; + return ((ChunkOxygenAccessor) this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z))).galacticraft$getProviders(y); } @Override - public boolean isBreathableChunk(LevelChunk chunk, int x, int y, int z) { - assert x >= 0 && x < 16 && z >= 0 && z < 16; - if (this.withinBuildHeight(y)) { - return this.breathable ^ ((ChunkOxygenAccessor) chunk).galacticraft$isInverted(x, y, z); - } - return this.breathable/* && y < this.getMaxBuildHeight() * 2*/; + public Iterator galacticraft$getAtmosphereProviderLocations(int x, int y, int z) { + if (y < this.getMinBuildHeight()) y = this.getMinBuildHeight(); + if (y >= this.getMaxBuildHeight()) y = this.getMaxBuildHeight() - 1; + return ((ChunkOxygenAccessor) this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z))).galacticraft$getProviderPositions(y); } @Override - public void setBreathable(int x, int y, int z, boolean value) { - if (withinWorldSize(x, z) && y >= this.getMinBuildHeight() && y < this.getMaxBuildHeight()) { - this.setBreathableChunk(this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)), x & 15, y, z & 15, value); + public boolean galacticraft$isBreathable(double x, double y, double z) { + if (this.breathable) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(Mth.floor(x), Mth.floor(y), Mth.floor(z)); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(x, y, z)) return true; } + return false; } @Override - public void setBreathableChunk(LevelChunk chunk, int x, int y, int z, boolean value) { - assert x >= 0 && x < 16 && z >= 0 && z < 16; - if (y < this.getMinBuildHeight() || y >= this.getMaxBuildHeight()) return; - ((ChunkOxygenAccessor) chunk).galacticraft$setInverted(x, y, z, this.breathable ^ value); - if (!value) { - BlockPos blockPos = chunk.getPos().getBlockAt(x, y, z); - GCEventHandlers.extinguishBlock((Level) (Object) this, blockPos, this.getBlockState(blockPos)); + public boolean galacticraft$isBreathable(int x, int y, int z) { + if (this.breathable) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(x, y, z); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(x, y, z)) return true; } + return false; } @Override - public boolean getDefaultBreathable() { - return this.breathable; + public boolean galacticraft$isBreathable(BlockPos pos) { + if (this.breathable) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(pos.getX(), pos.getY(), pos.getZ()); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(pos)) return true; + } + return false; } @Override - public void setDefaultBreathable(boolean breathable) { - this.breathable = breathable; - } - - @Unique - private boolean withinBuildHeight(int y) { - return y >= this.getMinBuildHeight() && y < this.getMaxBuildHeight(); - } - - @Unique - private static boolean withinWorldSize(int x, int z) { - return x >= -Level.MAX_LEVEL_SIZE && z >= -Level.MAX_LEVEL_SIZE && x < Level.MAX_LEVEL_SIZE && z < Level.MAX_LEVEL_SIZE; - } - - @Unique - private boolean validPosition(int x, int y, int z) { - return this.withinBuildHeight(y) && withinWorldSize(x, z); + public boolean galacticraft$isBreathable() { + return this.breathable; } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ProtoChunkMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ProtoChunkMixin.java deleted file mode 100644 index 5f8617b583..0000000000 --- a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ProtoChunkMixin.java +++ /dev/null @@ -1,55 +0,0 @@ -/* - * Copyright (c) 2019-2025 Team Galacticraft - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.galacticraft.impl.internal.mixin.oxygen; - -import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessor; -import dev.galacticraft.impl.internal.accessor.ChunkOxygenSyncer; -import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; -import net.minecraft.core.Registry; -import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.LevelHeightAccessor; -import net.minecraft.world.level.biome.Biome; -import net.minecraft.world.level.chunk.ChunkAccess; -import net.minecraft.world.level.chunk.LevelChunkSection; -import net.minecraft.world.level.chunk.ProtoChunk; -import net.minecraft.world.level.chunk.UpgradeData; -import net.minecraft.world.level.levelgen.blending.BlendingData; -import org.jetbrains.annotations.Nullable; -import org.spongepowered.asm.mixin.Mixin; - -@Mixin(ProtoChunk.class) -public abstract class ProtoChunkMixin extends ChunkAccess implements ChunkOxygenAccessor, ChunkOxygenSyncer { - private ProtoChunkMixin(ChunkPos pos, UpgradeData upgradeData, LevelHeightAccessor heightLimitView, Registry biome, long inhabitedTime, @Nullable LevelChunkSection[] sectionArrayInitializer, @Nullable BlendingData blendingData) { - super(pos, upgradeData, heightLimitView, biome, inhabitedTime, sectionArrayInitializer, blendingData); - } - - @Override - public boolean galacticraft$isInverted(int x, int y, int z) { - return ((ChunkSectionOxygenAccessor) this.sections[this.getSectionIndex(y)]).galacticraft$isInverted(x, y & 15, z); - } - - @Override - public void galacticraft$setInverted(int x, int y, int z, boolean inverted) { - ((ChunkSectionOxygenAccessor) this.sections[this.getSectionIndex(y)]).galacticraft$setInverted(x, y & 15, z, inverted); - } -} diff --git a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessor.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ServerLevelMixin.java similarity index 51% rename from src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessor.java rename to src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ServerLevelMixin.java index c1734a6042..00fe6de512 100644 --- a/src/main/java/dev/galacticraft/impl/internal/accessor/ChunkOxygenAccessor.java +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/ServerLevelMixin.java @@ -20,26 +20,18 @@ * SOFTWARE. */ -package dev.galacticraft.impl.internal.accessor; +package dev.galacticraft.impl.internal.mixin.oxygen; -public interface ChunkOxygenAccessor { - /** - * Returns whether the supplied position in the chunk is breathable for entities - * - * @param x the position to test on the X-axis, normalized from 0 to 15 - * @param y the position to test on the Y-axis, must be within world height - * @param z the position to test on the Z-axis, normalized from 0 to 15 - * @return whether the supplied position in the chunk is breathable for entities - */ - boolean galacticraft$isInverted(int x, int y, int z); +import dev.galacticraft.api.accessor.LevelOxygenAccessor; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; - /** - * Sets the breathable state for entities for the supplied position - * - * @param x the position to test on the X-axis, normalized from 0 to 15 - * @param y the position to test on the Y-axis, must be within world height - * @param z the position to test on the Z-axis, normalized from 0 to 15 - * @param inverted whether the supplied position is breathable - */ - void galacticraft$setInverted(int x, int y, int z, boolean inverted); +@Mixin(ServerLevel.class) +public abstract class ServerLevelMixin implements LevelOxygenAccessor { + @Override + public void galacticraft$notifyAtmosphereChange(BlockPos pos, BlockState state) { + state.getBlock().galacticraft$onAtmosphereChange((ServerLevel) (Object) this, pos, state, this.galacticraft$getAtmosphereProviders(pos)); + } } diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/WorldGenRegionMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/WorldGenRegionMixin.java new file mode 100644 index 0000000000..e6a4f0d5e7 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/WorldGenRegionMixin.java @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen; + +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; +import dev.galacticraft.api.accessor.LevelOxygenAccessor; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; +import dev.galacticraft.impl.internal.oxygen.ProviderIterator; +import net.minecraft.core.BlockPos; +import net.minecraft.core.SectionPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.util.Mth; +import net.minecraft.world.level.LevelHeightAccessor; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.ChunkAccess; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; + +@Mixin(WorldGenRegion.class) +public abstract class WorldGenRegionMixin implements LevelOxygenAccessor, LevelHeightAccessor { + @Shadow @Final private ServerLevel level; + + @Shadow public abstract ChunkAccess getChunk(int chunkX, int chunkZ); + + @Override + public Iterator galacticraft$getAtmosphereProviders(int x, int y, int z) { + if (this.isOutsideBuildHeight(y)) return Collections.emptyIterator(); + ChunkAccess chunk = this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z)); + int sectionIndex = chunk.getSectionIndex(y); + ArrayList positions = ((ChunkSectionOxygenAccessor) chunk.getSections()[sectionIndex]).galacticraft$getRawProviders(); + if (positions == null) return Collections.emptyIterator(); + return new ProviderIterator(this.level, chunk, positions.listIterator(), sectionIndex); + } + + @Override + public Iterator galacticraft$getAtmosphereProviderLocations(int x, int y, int z) { + if (this.isOutsideBuildHeight(y)) return Collections.emptyIterator(); + return ((ChunkOxygenAccessor) this.getChunk(SectionPos.blockToSectionCoord(x), SectionPos.blockToSectionCoord(z))).galacticraft$getProviderPositions(y); + } + + @Override + public void galacticraft$notifyAtmosphereChange(BlockPos pos, BlockState state) { + state.getBlock().galacticraft$onAtmosphereChange(this.level, pos, state, this.galacticraft$getAtmosphereProviders(pos)); + } + + @Override + public boolean galacticraft$isBreathable(double x, double y, double z) { + if (this.level.galacticraft$isBreathable()) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(Mth.floor(x), Mth.floor(y), Mth.floor(z)); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(x, y, z)) return true; + } + return false; + } + + @Override + public boolean galacticraft$isBreathable(int x, int y, int z) { + if (this.level.galacticraft$isBreathable()) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(x, y, z); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(x, y, z)) return true; + } + return false; + } + + @Override + public boolean galacticraft$isBreathable(BlockPos pos) { + if (this.level.galacticraft$isBreathable()) return true; + Iterator iter = this.galacticraft$getAtmosphereProviders(pos.getX(), pos.getY(), pos.getZ()); + while (iter.hasNext()) { + AtmosphereProvider next = iter.next(); + if (next.canBreathe(pos)) return true; + } + return false; + } + + @Override + public boolean galacticraft$isBreathable() { + return this.level.galacticraft$isBreathable(); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/AbstractCandleBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/AbstractCandleBlockMixin.java new file mode 100644 index 0000000000..9e57de03c4 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/AbstractCandleBlockMixin.java @@ -0,0 +1,75 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import dev.galacticraft.api.accessor.GCBlockExtensions; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.block.AbstractCandleBlock; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(AbstractCandleBlock.class) +public abstract class AbstractCandleBlockMixin implements GCBlockExtensions { + @Shadow + protected abstract Iterable getParticleOffsets(BlockState state); + + @Shadow + @Final + public static BooleanProperty LIT; + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return state.getValue(LIT); + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + return state.setValue(LIT, false); + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return state.getValue(LIT); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + this.getParticleOffsets(state).forEach(vec3 -> + level.sendParticles(ParticleTypes.SMOKE, (double) pos.getX() + vec3.x(), (double) pos.getY() + vec3.y(), (double) pos.getZ() + vec3.z(), 0, 0.0D, 0.1D, 0.0D, 0.0D) + ); + level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); + + level.setBlock(pos, state.setValue(LIT, false), Block.UPDATE_ALL_IMMEDIATE); + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/CampfireBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/CampfireBlockMixin.java new file mode 100644 index 0000000000..14a8f2dbcf --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/CampfireBlockMixin.java @@ -0,0 +1,80 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation; +import dev.galacticraft.api.accessor.GCBlockExtensions; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.CampfireBlock; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.state.properties.BooleanProperty; +import net.minecraft.world.level.block.state.properties.Property; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(CampfireBlock.class) +public class CampfireBlockMixin implements GCBlockExtensions { + @Shadow + @Final + public static BooleanProperty LIT; + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return state.getValue(LIT); + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + return state.setValue(LIT, false); + } + + @WrapOperation(method = "getStateForPlacement", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/state/BlockState;setValue(Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;)Ljava/lang/Object;")) + private Object preventFireInNoAtmosphere(BlockState instance, Property property, Comparable comparable, Operation original, BlockPlaceContext ctx) { + if (property == LIT) { + original.call(instance, property, ((Boolean) comparable) && ctx.getLevel().galacticraft$isBreathable(ctx.getClickedPos())); + } + return original.call(instance, property, comparable); + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return state.getValue(LIT); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + level.playSound(null, pos, SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 0.2F, 1.0F); + + level.setBlock(pos, state.setValue(CampfireBlock.LIT, false), Block.UPDATE_ALL_IMMEDIATE); + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/EntityMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/EntityMixin.java new file mode 100644 index 0000000000..131b733ccb --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/EntityMixin.java @@ -0,0 +1,54 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.Level; +import net.minecraft.world.phys.Vec3; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +@Mixin(Entity.class) +public abstract class EntityMixin { + @Shadow + private Level level; + + @Shadow + private Vec3 position; + + @Shadow + private int remainingFireTicks; + + @Shadow + public abstract void extinguishFire(); + + @Inject(method = "baseTick", at = @At("RETURN")) + private void extinguishFireNoAir(CallbackInfo ci) { + if (this.remainingFireTicks > 0 && !this.level.galacticraft$isBreathable(this.position)) { + this.extinguishFire(); + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/FireBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/FireBlockMixin.java new file mode 100644 index 0000000000..56930c08b0 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/FireBlockMixin.java @@ -0,0 +1,51 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import dev.galacticraft.api.accessor.GCBlockExtensions; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.FireBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(FireBlock.class) +public class FireBlockMixin implements GCBlockExtensions { + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return true; + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + return Blocks.AIR.defaultBlockState(); + } + + @ModifyReturnValue(method = "canSurvive", at = @At("RETURN")) + public boolean canSurvive(boolean original, BlockState state, LevelReader level, BlockPos pos) { + return original && level.galacticraft$isBreathable(pos); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LanternBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LanternBlockMixin.java new file mode 100644 index 0000000000..a9c24b0e35 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LanternBlockMixin.java @@ -0,0 +1,88 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import dev.galacticraft.api.accessor.GCBlockExtensions; +import dev.galacticraft.mod.content.GCBlocks; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.item.context.BlockPlaceContext; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.LanternBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(LanternBlock.class) +public class LanternBlockMixin implements GCBlockExtensions { + @ModifyReturnValue(method = "getStateForPlacement", at = @At("RETURN")) + private BlockState extinguishNoAir(BlockState original, BlockPlaceContext ctx) { + if (original != null && !ctx.getLevel().galacticraft$isBreathable(ctx.getClickedPos())) { + return this.galacticraft$extinguishBlockPlace(ctx.getClickedPos(), original); + } + return original; + } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return state.getBlock() == Blocks.LANTERN || state.getBlock() == Blocks.SOUL_LANTERN; + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + if (state.getBlock() == Blocks.LANTERN) { + return GCBlocks.UNLIT_LANTERN.withPropertiesOf(state); + } else if (state.getBlock() == Blocks.SOUL_LANTERN) { + return GCBlocks.UNLIT_SOUL_LANTERN.withPropertiesOf(state); + } + return state; + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return this.galacticraft$hasLegacyExtinguishTransform(state); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + if (this.galacticraft$hasAtmosphereListener(state)) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + (state.getValue(LanternBlock.HANGING) ? 0.25D : 0.1875D); + double z = pos.getZ() + 0.5D; + + for (Direction direction : Direction.Plane.HORIZONTAL) { + level.sendParticles(ParticleTypes.SMOKE, x + 0.27 * (double) direction.getStepX(), y, z + 0.27 * (double) direction.getStepZ(), 0, 0.0D, 0.0D, 0.0D, 0.0D); + } + level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); + level.setBlock(pos, this.galacticraft$extinguishBlockPlace(pos, state), Block.UPDATE_ALL_IMMEDIATE); + } + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LevelImplMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LevelImplMixin.java new file mode 100644 index 0000000000..436b25e26b --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/LevelImplMixin.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import dev.galacticraft.api.accessor.LevelOxygenAccessor; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.WorldGenRegion; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +// all base classes for LevelAccessor +@Mixin({Level.class, WorldGenRegion.class}) +public abstract class LevelImplMixin implements LevelOxygenAccessor { + @WrapMethod(method = "setBlock(Lnet/minecraft/core/BlockPos;Lnet/minecraft/world/level/block/state/BlockState;II)Z") + private boolean extinguishFire(BlockPos pos, BlockState state, int flags, int maxUpdateDepth, Operation original) { + return original.call(pos, state.getBlock().galacticraft$hasLegacyExtinguishTransform(state) && !this.galacticraft$isBreathable(pos) ? state.getBlock().galacticraft$extinguishBlockPlace(pos, state) : state, flags, maxUpdateDepth); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/SoulFireBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/SoulFireBlockMixin.java new file mode 100644 index 0000000000..6554c3a412 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/SoulFireBlockMixin.java @@ -0,0 +1,68 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import dev.galacticraft.api.accessor.GCBlockExtensions; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.LevelReader; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.SoulFireBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(SoulFireBlock.class) +public class SoulFireBlockMixin implements GCBlockExtensions { + @ModifyReturnValue(method = "canSurvive", at = @At("RETURN")) + public boolean canSurvive(boolean original, BlockState state, LevelReader level, BlockPos pos) { + return original && level.galacticraft$isBreathable(pos); + } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return true; + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + return Blocks.AIR.defaultBlockState(); + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return true; + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + level.setBlock(pos, Blocks.AIR.defaultBlockState(), Block.UPDATE_ALL_IMMEDIATE); + level.playSound(null, pos, SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.25F, 2.6F + (level.random.nextFloat() - level.random.nextFloat()) * 0.8F); + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/TorchBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/TorchBlockMixin.java new file mode 100644 index 0000000000..55ca559d2c --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/TorchBlockMixin.java @@ -0,0 +1,76 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import dev.galacticraft.api.accessor.GCBlockExtensions; +import dev.galacticraft.mod.content.GCBlocks; +import net.minecraft.core.BlockPos; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.TorchBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + + +@Mixin(TorchBlock.class) +public class TorchBlockMixin implements GCBlockExtensions { + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return state.getBlock() == Blocks.TORCH || state.getBlock() == Blocks.SOUL_TORCH; + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + if (state.getBlock() == Blocks.TORCH) { + return GCBlocks.UNLIT_TORCH.withPropertiesOf(state); + } else if (state.getBlock() == Blocks.SOUL_TORCH) { + return GCBlocks.UNLIT_SOUL_TORCH.withPropertiesOf(state); + } + return state; + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return this.galacticraft$hasLegacyExtinguishTransform(state); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + if (this.galacticraft$hasLegacyExtinguishTransform(state)) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + 0.92D; + double z = pos.getZ() + 0.5D; + + level.sendParticles(ParticleTypes.SMOKE, x, y, z, 0, 0.0D, 0.0D, 0.0D, 0.0D); + level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); + + level.setBlock(pos, this.galacticraft$extinguishBlockPlace(pos, state), Block.UPDATE_ALL_IMMEDIATE); + } + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WallTorchBlockMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WallTorchBlockMixin.java new file mode 100644 index 0000000000..76f31ff95a --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WallTorchBlockMixin.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import dev.galacticraft.api.accessor.GCBlockExtensions; +import dev.galacticraft.mod.content.GCBlocks; +import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.particles.ParticleTypes; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.sounds.SoundEvents; +import net.minecraft.sounds.SoundSource; +import net.minecraft.world.level.block.Block; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.WallTorchBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + + +@Mixin(WallTorchBlock.class) +public class WallTorchBlockMixin implements GCBlockExtensions { + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return state.getBlock() == Blocks.WALL_TORCH || state.getBlock() == Blocks.SOUL_WALL_TORCH; + } + + @Override + public BlockState galacticraft$extinguishBlockPlace(BlockPos pos, BlockState state) { + if (state.getBlock() == Blocks.WALL_TORCH) { + return GCBlocks.UNLIT_WALL_TORCH.withPropertiesOf(state); + } else if (state.getBlock() == Blocks.SOUL_WALL_TORCH) { + return GCBlocks.UNLIT_SOUL_WALL_TORCH.withPropertiesOf(state); + } + return state; + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return this.galacticraft$hasLegacyExtinguishTransform(state); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, boolean breathable) { + if (!breathable) { + if (this.galacticraft$hasLegacyExtinguishTransform(state)) { + double x = pos.getX() + 0.5D; + double y = pos.getY() + 0.92D; + double z = pos.getZ() + 0.5D; + + Direction direction = state.getValue(WallTorchBlock.FACING).getOpposite(); + level.sendParticles(ParticleTypes.SMOKE, x + 0.27 * (double) direction.getStepX(), y, z + 0.27 * (double) direction.getStepZ(), 0, 0.0D, 0.0D, 0.0D, 0.0D); + level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); + + level.setBlock(pos, this.galacticraft$extinguishBlockPlace(pos, state), Block.UPDATE_ALL_IMMEDIATE); + } + } + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WeatheringCopperBlocksMixin.java b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WeatheringCopperBlocksMixin.java new file mode 100644 index 0000000000..12facbfb81 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/mixin/oxygen/extinguish/WeatheringCopperBlocksMixin.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.mixin.oxygen.extinguish; + +import com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod; +import com.llamalad7.mixinextras.injector.wrapoperation.Operation; +import net.minecraft.core.BlockPos; +import net.minecraft.server.level.ServerLevel; +import net.minecraft.util.RandomSource; +import net.minecraft.world.level.block.*; +import net.minecraft.world.level.block.state.BlockState; +import org.spongepowered.asm.mixin.Mixin; + +@Mixin({WeatheringCopperBulbBlock.class, WeatheringCopperDoorBlock.class, WeatheringCopperFullBlock.class, + WeatheringCopperGrateBlock.class, WeatheringCopperSlabBlock.class, WeatheringCopperStairBlock.class, + WeatheringCopperTrapDoorBlock.class}) +public class WeatheringCopperBlocksMixin { + @WrapMethod(method = "randomTick") + private void checkOxidizable(BlockState state, ServerLevel level, BlockPos pos, RandomSource randomSource, Operation original) { + if (level.galacticraft$isBreathable(pos)) original.call(state, level, pos, randomSource); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/oxygen/ApPosIterator.java b/src/main/java/dev/galacticraft/impl/internal/oxygen/ApPosIterator.java new file mode 100644 index 0000000000..b2d187e4d4 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/oxygen/ApPosIterator.java @@ -0,0 +1,47 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.oxygen; + +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.chunk.ChunkAccess; + +import java.util.Iterator; + +public record ApPosIterator(ChunkAccess chunkAccess, Iterator positions, int y) implements Iterator { + @Override + public boolean hasNext() { + return this.positions.hasNext(); + } + + @Override + public BlockPos next() { + return this.positions.next(); + } + + @Override + public void remove() { + this.positions.remove(); + ((ChunkOxygenAccessorInternal) this.chunkAccess).galacticraft$markSectionDirty(this.y); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/oxygen/ProviderIterator.java b/src/main/java/dev/galacticraft/impl/internal/oxygen/ProviderIterator.java new file mode 100644 index 0000000000..efd369faee --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/oxygen/ProviderIterator.java @@ -0,0 +1,86 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.oxygen; + +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.impl.internal.accessor.ChunkOxygenAccessorInternal; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.BlockGetter; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.chunk.ChunkAccess; + +import java.util.Iterator; +import java.util.ListIterator; +import java.util.NoSuchElementException; + +public class ProviderIterator implements Iterator { + private final BlockGetter level; + private final ChunkAccess access; + private final ListIterator positions; + private final int y; + private AtmosphereProvider next; + + public ProviderIterator(BlockGetter level, ChunkAccess access, ListIterator positions, int y) { + this.level = level; + this.access = access; + this.positions = positions; + this.y = y; + this.next = this.computeNext(); + } + + private AtmosphereProvider computeNext() { + if (this.positions.hasNext()) { + BlockEntity blockEntity = this.level.getBlockEntity(this.positions.next()); + if (blockEntity instanceof AtmosphereProvider provider) { + return provider; + } else { + this.positions.remove(); + } + return this.computeNext(); + } + return null; + } + + + @Override + public boolean hasNext() { + return this.next != null; + } + + @Override + public AtmosphereProvider next() { + if (this.next == null) { + throw new NoSuchElementException(); + } + AtmosphereProvider next1 = this.next; + this.next = this.computeNext(); + return next1; + } + + @Override + public void remove() { + this.positions.previous(); + this.positions.remove(); + ((ChunkOxygenAccessorInternal) this.access).galacticraft$markSectionDirty(this.y); + } +} diff --git a/src/main/java/dev/galacticraft/impl/internal/oxygen/ReverseSortedPosList.java b/src/main/java/dev/galacticraft/impl/internal/oxygen/ReverseSortedPosList.java new file mode 100644 index 0000000000..e24cc24002 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/oxygen/ReverseSortedPosList.java @@ -0,0 +1,65 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.oxygen; + +import net.minecraft.core.BlockPos; + +public class ReverseSortedPosList extends SortedPosList { + public ReverseSortedPosList(BlockPos center) { + super(center); + } + + @Override + protected int binarySearch(double distance) { + return binarySearch0(this.distances, 0, this.size, distance); + } + + // Arrays#binarySearch0 + private static int binarySearch0(double[] a, int fromIndex, int toIndex, double key) { + int low = fromIndex; + int high = toIndex - 1; + + while (low <= high) { + int mid = (low + high) >>> 1; + double midVal = a[mid]; + + if (midVal > key) { + low = mid + 1; + } else if (midVal < key) { + high = mid - 1; + } else { + long midBits = Double.doubleToLongBits(midVal); + long keyBits = Double.doubleToLongBits(key); + if (midBits == keyBits) { + return mid; + } else if (midBits > keyBits) { + low = mid + 1; + } else { + high = mid - 1; + } + } + } + return -(low + 1); // key not found. + } + +} diff --git a/src/main/java/dev/galacticraft/impl/internal/oxygen/SortedPosList.java b/src/main/java/dev/galacticraft/impl/internal/oxygen/SortedPosList.java new file mode 100644 index 0000000000..7ff0b8a466 --- /dev/null +++ b/src/main/java/dev/galacticraft/impl/internal/oxygen/SortedPosList.java @@ -0,0 +1,181 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.impl.internal.oxygen; + +import it.unimi.dsi.fastutil.objects.ObjectArrays; +import net.minecraft.core.BlockPos; + +import java.util.Arrays; + +public class SortedPosList { + private final BlockPos center; + double[] distances; // alt: don't cache and just calculate from pos each time? + private BlockPos[] positions; + int size = 0; + + public SortedPosList(BlockPos center) { + this.center = center; + this.distances = new double[0]; + this.positions = new BlockPos[0]; + } + + // returns the index of the given position OR the index of where the position would be inserted (-ve) + private int findIndex(BlockPos pos, double distance) { + int i = binarySearch(distance); + // negative: not found || equal: found immediately! + if (i < 0 || this.positions[i].equals(pos)) return i; + // many positions can have the same distance from the center, so check for the pos + + // search left + for (int j = i - 1; j >= 0; j--) { + if (this.distances[j] == distance) { + if (this.positions[j].equals(pos)) { + return j; + } + } else { + // now in a different distance group - no need to search further + break; + } + } + + // search right + for (int j = i + 1; j < this.size; j++) { + if (this.distances[j] == distance) { + if (this.positions[j].equals(pos)) { + return j; + } + } else { + // now in a different distance group - no need to search further + break; + } + } + return -(i + 1); + } + + protected int binarySearch(double distance) { + return Arrays.binarySearch(this.distances, 0, this.size, distance); + } + + public boolean add(BlockPos pos, double distance) { + int i = this.findIndex(pos, distance); + if (i >= 0) { + return false; + } else { + int j = getInsertionPosition(i); + this.addInternal(pos, distance, j); + return true; + } + } + + public boolean add(BlockPos pos) { + return this.add(pos, this.calculateDistanceSq(pos)); + } + + public BlockPos get(int index) { + return this.positions[index]; + } + + public double getDistance(int index) { + return this.distances[index]; + } + + public boolean remove(BlockPos pos) { + return this.remove(pos, this.calculateDistanceSq(pos)); + } + + public boolean remove(BlockPos pos, double distance) { + int i = this.findIndex(pos, distance); + if (i >= 0) { + this.remove(i); + return true; + } else { + return false; + } + } + + public boolean contains(BlockPos pos) { + return this.contains(pos, this.calculateDistanceSq(pos)); + } + + public boolean contains(BlockPos pos, double distance) { + return this.findIndex(pos, distance) >= 0; + } + + public int size() { + return this.size; + } + + public double calculateDistanceSq(BlockPos object) { + return this.center.distToLowCornerSqr(object.getX(), object.getY() - 0.5, object.getZ()); + } + + private static int getInsertionPosition(int binarySearchResult) { + return -binarySearchResult - 1; + } + + private void grow(int minCapacity) { + if (minCapacity > this.positions.length) { + if (this.positions != ObjectArrays.DEFAULT_EMPTY_ARRAY) { + // grow at least ~ 1.5x each time. + minCapacity = Math.max(Math.max(this.positions.length + (this.positions.length >> 1), minCapacity), 16); + } + + double[] distances = new double[minCapacity]; + System.arraycopy(this.distances, 0, distances, 0, this.size); + this.distances = distances; + + BlockPos[] positions = new BlockPos[minCapacity]; + System.arraycopy(this.positions, 0, positions, 0, this.size); + this.positions = positions; + } + } + + private void addInternal(BlockPos object, double distance, int index) { + this.grow(this.size + 1); + if (index != this.size) { + System.arraycopy(this.distances, index, this.distances, index + 1, this.size - index); + System.arraycopy(this.positions, index, this.positions, index + 1, this.size - index); + } + + this.distances[index] = distance; + this.positions[index] = object; + this.size++; + } + + public void remove(int index) { + this.size--; + if (index != this.size) { + System.arraycopy(this.distances, index + 1, this.distances, index, this.size - index); + System.arraycopy(this.positions, index + 1, this.positions, index, this.size - index); + } + + this.distances[this.size] = 0.0; + this.positions[this.size] = null; + } + + public void clear() { + this.size = 0; + Arrays.fill(this.distances, 0.0); + Arrays.fill(this.positions, null); + } +} \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/impl/internal/registry/ExtinguishableBlockRegistryImpl.java b/src/main/java/dev/galacticraft/impl/internal/registry/ExtinguishableBlockRegistryImpl.java deleted file mode 100644 index 6547e33a06..0000000000 --- a/src/main/java/dev/galacticraft/impl/internal/registry/ExtinguishableBlockRegistryImpl.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Copyright (c) 2019-2025 Team Galacticraft - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.galacticraft.impl.internal.registry; - -import dev.galacticraft.api.registry.ExtinguishableBlockRegistry; -import net.fabricmc.fabric.api.event.lifecycle.v1.CommonLifecycleEvents; -import net.minecraft.core.Holder; -import net.minecraft.core.registries.BuiltInRegistries; -import net.minecraft.tags.TagKey; -import net.minecraft.world.level.block.Block; - -import java.util.HashMap; -import java.util.IdentityHashMap; -import java.util.Map; - -public class ExtinguishableBlockRegistryImpl implements ExtinguishableBlockRegistry { - private final Map registeredEntriesBlock = new IdentityHashMap<>(); - private final Map, ExtinguishableBlockRegistry.Entry> registeredEntriesTag = new HashMap<>(); - private volatile Map computedEntries = null; - - public ExtinguishableBlockRegistryImpl() { - // Reset computed values after tags change since they depend on tags. - CommonLifecycleEvents.TAGS_LOADED.register((registries, client) -> { - this.computedEntries = null; - }); - } - - private Map getEntryMap() { - Map map = this.computedEntries; - - if (map == null) { - map = new IdentityHashMap<>(); - - // tags take precedence over blocks - for (TagKey tag : this.registeredEntriesTag.keySet()) { - ExtinguishableBlockRegistry.Entry entry = this.registeredEntriesTag.get(tag); - - for (Holder block : BuiltInRegistries.BLOCK.getTagOrEmpty(tag)) { - map.put(block.value(), entry); - } - } - - map.putAll(this.registeredEntriesBlock); - this.computedEntries = map; - } - - return map; - } - - @Override - public Entry get(Block block) { - return this.getEntryMap().get(block); - } - - @Override - public void add(Block block, Entry value) { - this.registeredEntriesBlock.put(block, value); - this.computedEntries = null; - } - - @Override - public void add(TagKey tag, Entry value) { - this.registeredEntriesTag.put(tag, value); - this.computedEntries = null; - } - - @Override - public void remove(Block block) { - this.clear(block); - } - - @Override - public void remove(TagKey tag) { - this.clear(tag); - } - - @Override - public void clear(Block block) { - this.registeredEntriesBlock.remove(block); - this.computedEntries = null; - } - - @Override - public void clear(TagKey tag) { - this.registeredEntriesTag.remove(tag); - this.computedEntries = null; - } -} diff --git a/src/main/java/dev/galacticraft/impl/network/s2c/OxygenUpdatePayload.java b/src/main/java/dev/galacticraft/impl/network/s2c/OxygenUpdatePayload.java index f416e627fd..a14a038776 100644 --- a/src/main/java/dev/galacticraft/impl/network/s2c/OxygenUpdatePayload.java +++ b/src/main/java/dev/galacticraft/impl/network/s2c/OxygenUpdatePayload.java @@ -25,22 +25,23 @@ import dev.galacticraft.impl.internal.accessor.ChunkSectionOxygenAccessor; import dev.galacticraft.mod.Constant; import dev.galacticraft.mod.util.StreamCodecs; -import io.netty.buffer.ByteBuf; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; import net.minecraft.network.codec.ByteBufCodecs; import net.minecraft.network.codec.StreamCodec; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.resources.ResourceLocation; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.status.ChunkStatus; import org.jetbrains.annotations.NotNull; -import java.util.BitSet; public record OxygenUpdatePayload(long chunk, OxygenData[] data) implements S2CPayload { public static final ResourceLocation ID = Constant.id("oxygen_update"); public static final Type TYPE = new Type<>(ID); - public static final StreamCodec CODEC = StreamCodec.composite( + public static final StreamCodec CODEC = StreamCodec.composite( StreamCodecs.LONG, d -> d.chunk, StreamCodecs.array(OxygenData.CODEC, OxygenData[]::new), @@ -56,22 +57,45 @@ public record OxygenUpdatePayload(long chunk, OxygenData[] data) implements S2CP @Override public Runnable handle(ClientPlayNetworking.@NotNull Context context) { return () -> { - LevelChunk chunk = context.client().level.getChunk(ChunkPos.getX(this.chunk), ChunkPos.getZ(this.chunk)); - for (OxygenData datum : this.data) { - ChunkSectionOxygenAccessor accessor = (ChunkSectionOxygenAccessor) chunk.getSection(datum.section); - accessor.galacticraft$setBits(datum.data); + ChunkAccess chunk = context.client().level.getChunk(ChunkPos.getX(this.chunk), ChunkPos.getZ(this.chunk), ChunkStatus.FULL, false); + if (chunk != null) { + for (OxygenData datum : this.data) { + ((ChunkSectionOxygenAccessor) chunk.getSection(datum.section)).galacticraft$loadData(datum.data); + } } }; } - public record OxygenData(byte section, @NotNull BitSet data) { - private static final StreamCodec BIT_SET_CODEC = ByteBufCodecs.BYTE_ARRAY.map(BitSet::valueOf, BitSet::toByteArray); - public static final StreamCodec CODEC = StreamCodec.composite( + public record OxygenData(byte section, OxygenSectionData data) { + public static final StreamCodec CODEC = StreamCodec.composite( ByteBufCodecs.BYTE, d -> d.section, - BIT_SET_CODEC, + OxygenSectionData.CODEC, d -> d.data, OxygenData::new ); } + + public record OxygenSectionData(BlockPos[] positions) { + public static final StreamCodec CODEC = new StreamCodec<>() { + @Override + public @NotNull OxygenSectionData decode(FriendlyByteBuf buf) { + int count = buf.readVarInt(); + + BlockPos[] positions = new BlockPos[count]; + for (int i = 0; i < count; i++) { + positions[i] = buf.readBlockPos(); + } + return new OxygenSectionData(positions); + } + + @Override + public void encode(FriendlyByteBuf buf, OxygenSectionData section) { + buf.writeVarInt(section.positions.length); + for (int i = 0; i < section.positions.length; i++) { + buf.writeBlockPos(section.positions[i]); + } + } + }; + } } diff --git a/src/main/java/dev/galacticraft/mod/Constant.java b/src/main/java/dev/galacticraft/mod/Constant.java index a6df436adc..e2d27667ca 100644 --- a/src/main/java/dev/galacticraft/mod/Constant.java +++ b/src/main/java/dev/galacticraft/mod/Constant.java @@ -1040,7 +1040,7 @@ static Style getCoolingStyle(double scale) { } interface Nbt { - String GC_API = "GCApi"; + String GC_API = "GCAPI"; String CHANGE_COUNT = "Modified"; String OXYGEN = "Inversion"; String GEAR_INV = "GearInv"; @@ -1085,6 +1085,12 @@ interface Nbt { String DOCKED_UUID = "DockedUuid"; String CAN_CONTENTS = "CanContents"; String CAN_COUNT = "CanCount"; + String SEALED = "Sealed"; + String SOLID = "Solid"; + String LISTENERS = "Listeners"; + String CONTENDED = "Contended"; + String UNSEALED = "Unsealed"; + String SRC = "Src"; } interface Chunk { diff --git a/src/main/java/dev/galacticraft/mod/Galacticraft.java b/src/main/java/dev/galacticraft/mod/Galacticraft.java index 78b09e5c15..2db940110b 100644 --- a/src/main/java/dev/galacticraft/mod/Galacticraft.java +++ b/src/main/java/dev/galacticraft/mod/Galacticraft.java @@ -33,7 +33,6 @@ import dev.galacticraft.mod.data.OxygenBlockDataManager; import dev.galacticraft.mod.events.GCCauldronInteraction; import dev.galacticraft.mod.events.GCEventHandlers; -import dev.galacticraft.mod.events.GCExtinguishable; import dev.galacticraft.mod.events.GCSulfuricAcidHandlers; import dev.galacticraft.mod.lookup.GCApiLookupProviders; import dev.galacticraft.mod.machine.GCMachineStatuses; @@ -109,7 +108,6 @@ public void onInitialize() { GCCelestialHandlers.register(); GCEventHandlers.init(); GCCauldronInteraction.init(); - GCExtinguishable.register(); GCSulfuricAcidHandlers.register(); ResourceManagerHelper.get(PackType.SERVER_DATA).registerReloadListener(OxygenBlockDataManager.INSTANCE); Constant.LOGGER.info("Initialization complete. (Took {}ms).", System.currentTimeMillis() - startInitTime); diff --git a/src/main/java/dev/galacticraft/mod/accessor/GCLevelAccessor.java b/src/main/java/dev/galacticraft/mod/accessor/GCLevelAccessor.java index fac48b2123..083548f1e1 100644 --- a/src/main/java/dev/galacticraft/mod/accessor/GCLevelAccessor.java +++ b/src/main/java/dev/galacticraft/mod/accessor/GCLevelAccessor.java @@ -22,12 +22,9 @@ package dev.galacticraft.mod.accessor; -import dev.galacticraft.mod.machine.SealerManager; import dev.galacticraft.mod.misc.footprint.FootprintManager; public interface GCLevelAccessor { - SealerManager galacticraft$getSealerManager(); - default FootprintManager galacticraft$getFootprintManager() { throw new RuntimeException("This should be overridden by mixin!"); // Hopefully this doesn't cause issues with mods with fake worlds } diff --git a/src/main/java/dev/galacticraft/mod/client/gui/overlay/OxygenOverlay.java b/src/main/java/dev/galacticraft/mod/client/gui/overlay/OxygenOverlay.java index f68d1501be..646bd372b9 100644 --- a/src/main/java/dev/galacticraft/mod/client/gui/overlay/OxygenOverlay.java +++ b/src/main/java/dev/galacticraft/mod/client/gui/overlay/OxygenOverlay.java @@ -52,7 +52,7 @@ public static void onHudRender(GuiGraphics graphics, DeltaTracker delta) { return; } - boolean nonBreathable = !mc.level.getDefaultBreathable(); + boolean nonBreathable = !mc.level.galacticraft$isBreathable(); boolean hasMaskAndGear = mc.player.galacticraft$hasMaskAndGear(); if (nonBreathable || hasMaskAndGear) { boolean hasOxygen = false; @@ -77,7 +77,7 @@ public static void onHudRender(GuiGraphics graphics, DeltaTracker delta) { DrawableUtil.drawOxygenBuffer(graphics.pose(), x, y, amount, capacity); } - if (nonBreathable && !((hasMaskAndGear && hasOxygen) || mc.level.isBreathable(mc.player.blockPosition().above()))) { + if (nonBreathable && !((hasMaskAndGear && hasOxygen) || mc.level.galacticraft$isBreathable(mc.player.getEyePosition(delta.getGameTimeDeltaPartialTick(false))))) { final Window scaledresolution = mc.getWindow(); final int width = scaledresolution.getGuiScaledWidth(); final int height = scaledresolution.getGuiScaledHeight(); diff --git a/src/main/java/dev/galacticraft/mod/client/gui/screen/ingame/OxygenBubbleDistributorScreen.java b/src/main/java/dev/galacticraft/mod/client/gui/screen/ingame/OxygenBubbleDistributorScreen.java index c3f4fc2050..2545d0ba17 100644 --- a/src/main/java/dev/galacticraft/mod/client/gui/screen/ingame/OxygenBubbleDistributorScreen.java +++ b/src/main/java/dev/galacticraft/mod/client/gui/screen/ingame/OxygenBubbleDistributorScreen.java @@ -33,44 +33,48 @@ import dev.galacticraft.mod.util.Translations; import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; import net.minecraft.ChatFormatting; -import net.minecraft.client.Minecraft; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.components.EditBox; import net.minecraft.network.chat.Component; import net.minecraft.world.entity.player.Inventory; +import org.apache.commons.lang3.function.Consumers; +import org.apache.commons.lang3.math.NumberUtils; import java.text.DecimalFormat; public class OxygenBubbleDistributorScreen extends MachineScreen { private static final DecimalFormat FORMAT = new DecimalFormat(); - private final EditBox textField; + private EditBox textField; public OxygenBubbleDistributorScreen(OxygenBubbleDistributorMenu handler, Inventory inv, Component title) { super(handler, title, Constant.ScreenTexture.BUBBLE_DISTRIBUTOR_SCREEN); - this.textField = new EditBox(Minecraft.getInstance().font, this.leftPos + 132, this.topPos + 59, 26, 20, Component.literal(String.valueOf(this.menu.size))); - this.textField.setResponder((s -> { - try { - if (Byte.parseByte(s) < 1) { - textField.setValue(String.valueOf(this.menu.targetSize)); - } - } catch (NumberFormatException ignore) { - textField.setValue(String.valueOf(this.menu.targetSize)); - } - })); - - this.textField.setFilter((s -> { - try { - return Byte.parseByte(s) >= 1; - } catch (NumberFormatException ignore) { - return false; - } - })); } @Override protected void init() { super.init(); this.titleLabelX += 20; + + this.textField = new EditBox(this.font, this.leftPos + 132, this.topPos + 59, 26, 20, Component.literal(String.valueOf(this.menu.targetSize))); + this.textField.setValue(String.valueOf(this.menu.targetSize)); + this.textField.setResponder(this::updateFromText); + + this.textField.setFilter((s -> { + int value = NumberUtils.toInt(s, -1); + return s.isBlank() || value >= 0 && value <= OxygenBubbleDistributorBlockEntity.MAX_SIZE; + })); + + this.addRenderableWidget(this.textField); + } + + @Override + protected void containerTick() { + super.containerTick(); + if (!this.textField.getValue().isBlank() && NumberUtils.toInt(this.textField.getValue(), -1) != this.menu.targetSize) { + this.textField.setResponder(Consumers.nop()); + this.textField.setValue(String.valueOf(this.menu.targetSize)); + this.textField.setResponder(this::updateFromText); + } } @SuppressWarnings("DataFlowIssue") @@ -109,16 +113,10 @@ protected void renderMachineBackground(GuiGraphics graphics, int mouseX, int mou @Override protected void renderForeground(GuiGraphics graphics, int mouseX, int mouseY, float delta) { super.renderForeground(graphics, mouseX, mouseY, delta); - textField.setValue(String.valueOf(this.menu.targetSize)); MachineStatus status = this.menu.state.getStatus(); graphics.drawString(this.font, Component.translatable(Translations.Ui.MACHINE_STATUS, status != null ? status.getText() : Component.empty()), this.leftPos + 60, this.topPos + 30, ChatFormatting.DARK_GRAY.getColor(), false); - this.textField.render(graphics, mouseX, mouseY, delta); - - this.textField.setX(this.leftPos + 132); - this.textField.setY(this.topPos + 59); - if (this.menu.state.isActive()) { graphics.drawString(this.font, Component.translatable(Translations.Ui.BUBBLE_CURRENT_SIZE, FORMAT.format(this.menu.size)).setStyle(Constant.Text.DARK_GRAY_STYLE), this.leftPos + 60, this.topPos + 42, ChatFormatting.DARK_GRAY.getColor(), false); } @@ -126,17 +124,7 @@ protected void renderForeground(GuiGraphics graphics, int mouseX, int mouseY, fl @Override public boolean mouseClicked(double mouseX, double mouseY, int button) { - return super.mouseClicked(mouseX, mouseY, button) | checkClick(mouseX, mouseY, button) | textField.mouseClicked(mouseX, mouseY, button); - } - - @Override - public boolean keyPressed(int keyCode, int scanCode, int modifiers) { - return super.keyPressed(keyCode, scanCode, modifiers) | textField.keyPressed(keyCode, scanCode, modifiers); - } - - @Override - public boolean keyReleased(int keyCode, int scanCode, int modifiers) { - return super.keyReleased(keyCode, scanCode, modifiers) | textField.keyReleased(keyCode, scanCode, modifiers); + return super.mouseClicked(mouseX, mouseY, button) | this.checkClick(mouseX, mouseY, button); } private boolean checkClick(double mouseX, double mouseY, int button) { @@ -149,20 +137,18 @@ private boolean checkClick(double mouseX, double mouseY, int button) { } if (DrawableUtil.isWithin(mouseX, mouseY, this.leftPos + 158, this.topPos + 59, Constant.TextureCoordinate.ARROW_VERTICAL_WIDTH, Constant.TextureCoordinate.ARROW_VERTICAL_HEIGHT)) { - if (this.menu.targetSize != Byte.MAX_VALUE) { - this.menu.targetSize = ((byte) (this.menu.targetSize + 1)); - textField.setValue(String.valueOf(this.menu.targetSize)); - ClientPlayNetworking.send(new BubbleMaxPayload(this.menu.targetSize)); + if (this.menu.targetSize < OxygenBubbleDistributorBlockEntity.MAX_SIZE) { + this.menu.targetSize = this.menu.targetSize + 1; + this.textField.setValue(String.valueOf(this.menu.targetSize)); this.playButtonSound(); return true; } } if (DrawableUtil.isWithin(mouseX, mouseY, this.leftPos + 158, this.topPos + 69, Constant.TextureCoordinate.ARROW_VERTICAL_WIDTH, Constant.TextureCoordinate.ARROW_VERTICAL_HEIGHT)) { - if (this.menu.targetSize > 1) { - this.menu.targetSize = (byte) (this.menu.targetSize - 1); - textField.setValue(String.valueOf(this.menu.targetSize)); - ClientPlayNetworking.send(new BubbleMaxPayload(this.menu.targetSize)); + if (this.menu.targetSize > 0) { + this.menu.targetSize = this.menu.targetSize - 1; + this.textField.setValue(String.valueOf(this.menu.targetSize)); this.playButtonSound(); return true; } @@ -170,4 +156,13 @@ private boolean checkClick(double mouseX, double mouseY, int button) { } return false; } + + private void updateFromText(String s) { + if (s.isBlank()) return; + int value = NumberUtils.toInt(s, -1); + if (value >= 0 && value <= OxygenBubbleDistributorBlockEntity.MAX_SIZE) { + this.menu.targetSize = value; + ClientPlayNetworking.send(new BubbleMaxPayload(value)); + } + } } diff --git a/src/main/java/dev/galacticraft/mod/client/network/GCClientPacketReceiver.java b/src/main/java/dev/galacticraft/mod/client/network/GCClientPacketReceiver.java index 7f3f21a56e..e0a3242348 100644 --- a/src/main/java/dev/galacticraft/mod/client/network/GCClientPacketReceiver.java +++ b/src/main/java/dev/galacticraft/mod/client/network/GCClientPacketReceiver.java @@ -32,6 +32,7 @@ public static void register() { register(BubbleSizePayload.TYPE); register(BubbleUpdatePayload.TYPE); register(OpenCelestialScreenPayload.TYPE); + register(OxygenSealerUpdatePayload.TYPE); register(FootprintPacket.TYPE); register(FootprintRemovedPacket.TYPE); register(ResetPerspectivePacket.TYPE); diff --git a/src/main/java/dev/galacticraft/mod/client/render/block/entity/GCBlockEntityRenderer.java b/src/main/java/dev/galacticraft/mod/client/render/block/entity/GCBlockEntityRenderer.java index 9e50768174..4a3cb9ee21 100644 --- a/src/main/java/dev/galacticraft/mod/client/render/block/entity/GCBlockEntityRenderer.java +++ b/src/main/java/dev/galacticraft/mod/client/render/block/entity/GCBlockEntityRenderer.java @@ -30,7 +30,7 @@ public static void register() { BlockEntityRenderers.register(GCBlockEntityTypes.BASIC_SOLAR_PANEL, SolarPanelBlockEntityRenderer::new); BlockEntityRenderers.register(GCBlockEntityTypes.ADVANCED_SOLAR_PANEL, SolarPanelBlockEntityRenderer::new); BlockEntityRenderers.register(GCBlockEntityTypes.GLASS_FLUID_PIPE, FluidPipeBlockEntityRenderer::new); - BlockEntityRenderers.register(GCBlockEntityTypes.OXYGEN_BUBBLE_DISTRIBUTOR, BubbleDistributorRenderer::new); + BlockEntityRenderers.register(GCBlockEntityTypes.OXYGEN_BUBBLE_DISTRIBUTOR, OxygenBubbleDistributorRenderer::new); BlockEntityRenderers.register(GCBlockEntityTypes.ROCKET_WORKBENCH, RocketWorkbenchBlockEntityRenderer::new); //BlockEntityRenderers.register(GCBlockEntityTypes.CANNED_FOOD, CannedFoodBlockEntityRenderer::new); } diff --git a/src/main/java/dev/galacticraft/mod/client/render/block/entity/BubbleDistributorRenderer.java b/src/main/java/dev/galacticraft/mod/client/render/block/entity/OxygenBubbleDistributorRenderer.java similarity index 87% rename from src/main/java/dev/galacticraft/mod/client/render/block/entity/BubbleDistributorRenderer.java rename to src/main/java/dev/galacticraft/mod/client/render/block/entity/OxygenBubbleDistributorRenderer.java index 1b4377c794..c6241be557 100644 --- a/src/main/java/dev/galacticraft/mod/client/render/block/entity/BubbleDistributorRenderer.java +++ b/src/main/java/dev/galacticraft/mod/client/render/block/entity/OxygenBubbleDistributorRenderer.java @@ -34,11 +34,11 @@ import net.minecraft.client.renderer.texture.OverlayTexture; import net.minecraft.resources.ResourceLocation; -public class BubbleDistributorRenderer implements BlockEntityRenderer { +public class OxygenBubbleDistributorRenderer implements BlockEntityRenderer { public static final ResourceLocation MODEL = Constant.id("models/misc/sphere.json"); public final GCModel bubbleModel; - public BubbleDistributorRenderer(BlockEntityRendererProvider.Context context) { + public OxygenBubbleDistributorRenderer(BlockEntityRendererProvider.Context context) { this.bubbleModel = GCModelLoader.INSTANCE.getModel(MODEL); if (bubbleModel == null) { @@ -52,11 +52,12 @@ public void render(OxygenBubbleDistributorBlockEntity machine, float tickDelta, if (machine.isDisabled() || !machine.isBubbleVisible()) { return; } - double size = machine.getSize(); +// float size = (float) Mth.lerp(tickDelta, machine.getPrevSize(), machine.getSize()); //todo lerp size + float size = (float) machine.getSize(); matrices.pushPose(); matrices.translate(0.5F, 1.0F, 0.5F); - matrices.scale((float) size, (float) size, (float) size); + matrices.scale(size, size, size); bubbleModel.render(matrices, null, vertexConsumers.getBuffer(GCRenderTypes.bubble(GCRenderTypes.OBJ_ATLAS)), light, OverlayTexture.NO_OVERLAY); diff --git a/src/main/java/dev/galacticraft/mod/client/render/entity/BubbleEntityRenderer.java b/src/main/java/dev/galacticraft/mod/client/render/entity/BubbleEntityRenderer.java index b7ce48c3b0..480a66de6a 100644 --- a/src/main/java/dev/galacticraft/mod/client/render/entity/BubbleEntityRenderer.java +++ b/src/main/java/dev/galacticraft/mod/client/render/entity/BubbleEntityRenderer.java @@ -28,7 +28,7 @@ import dev.galacticraft.mod.client.model.GCModel; import dev.galacticraft.mod.client.model.GCModelLoader; import dev.galacticraft.mod.client.model.GCRenderTypes; -import dev.galacticraft.mod.client.render.block.entity.BubbleDistributorRenderer; +import dev.galacticraft.mod.client.render.block.entity.OxygenBubbleDistributorRenderer; import dev.galacticraft.mod.content.entity.BubbleEntity; import net.fabricmc.api.EnvType; import net.fabricmc.api.Environment; @@ -51,7 +51,7 @@ public BubbleEntityRenderer(EntityRendererProvider.Context context) { @Override public void render(BubbleEntity entity, float yaw, float tickDelta, PoseStack matrices, MultiBufferSource vertexConsumers, int light) { if (bubbleModel == null) { - bubbleModel = GCModelLoader.INSTANCE.getModel(BubbleDistributorRenderer.MODEL); + bubbleModel = GCModelLoader.INSTANCE.getModel(OxygenBubbleDistributorRenderer.MODEL); assert bubbleModel != null; } float size = entity.getSize(); diff --git a/src/main/java/dev/galacticraft/mod/content/GCBlocks.java b/src/main/java/dev/galacticraft/mod/content/GCBlocks.java index e72d2bd993..59a7b1084d 100644 --- a/src/main/java/dev/galacticraft/mod/content/GCBlocks.java +++ b/src/main/java/dev/galacticraft/mod/content/GCBlocks.java @@ -354,8 +354,8 @@ public class GCBlocks { public static final Block REFINERY = BLOCKS.registerWithItem(Constant.Block.REFINERY, new RefineryBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops())); public static final Block FUEL_LOADER = BLOCKS.registerWithItem(Constant.Block.FUEL_LOADER, new FuelLoaderBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops())); public static final Block OXYGEN_COLLECTOR = BLOCKS.registerWithItem(Constant.Block.OXYGEN_COLLECTOR, new OxygenCollectorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops())); - public static final Block OXYGEN_SEALER = BLOCKS.registerWithItem(Constant.Block.OXYGEN_SEALER, new SimpleMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops(), Constant.id(Constant.Block.OXYGEN_SEALER))); - public static final Block OXYGEN_BUBBLE_DISTRIBUTOR = BLOCKS.registerWithItem(Constant.Block.OXYGEN_BUBBLE_DISTRIBUTOR, new SimpleMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops(), Constant.id(Constant.Block.OXYGEN_BUBBLE_DISTRIBUTOR))); + public static final Block OXYGEN_SEALER = BLOCKS.registerWithItem(Constant.Block.OXYGEN_SEALER, new OxygenSealerBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops())); + public static final Block OXYGEN_BUBBLE_DISTRIBUTOR = BLOCKS.registerWithItem(Constant.Block.OXYGEN_BUBBLE_DISTRIBUTOR, new OxygenBubbleDistributorBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops())); public static final Block OXYGEN_DECOMPRESSOR = BLOCKS.registerWithItem(Constant.Block.OXYGEN_DECOMPRESSOR, new SimpleMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops(), Constant.id(Constant.Block.OXYGEN_DECOMPRESSOR))); public static final Block OXYGEN_COMPRESSOR = BLOCKS.registerWithItem(Constant.Block.OXYGEN_COMPRESSOR, new SimpleMachineBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops(), Constant.id(Constant.Block.OXYGEN_COMPRESSOR))); public static final Block OXYGEN_STORAGE_MODULE = BLOCKS.registerWithItem(Constant.Block.OXYGEN_STORAGE_MODULE, new ResourceStorageBlock(BlockBehaviour.Properties.of().mapColor(MapColor.COLOR_GRAY).strength(3.0F, 5.0F).sound(SoundType.METAL).requiresCorrectToolForDrops(), Constant.id(Constant.Block.OXYGEN_STORAGE_MODULE))); diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/AirlockControllerBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/AirlockControllerBlockEntity.java index e854da4d63..ed6ee2ddc0 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/AirlockControllerBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/AirlockControllerBlockEntity.java @@ -233,7 +233,7 @@ public void unsealAirLock() { for (x = this.protocol.minX + 1; x <= this.protocol.maxX - 1; x++) { for (z = this.protocol.minZ + 1; z <= this.protocol.maxZ - 1; z++) { pos = new BlockPos(x, y, z); - breathable = this.level.isBreathable(pos.above()); + breathable = this.level.galacticraft$isBreathable(pos.above()); if (breathable) { if (this.level.getBlockState(pos).getBlock() == GCBlocks.AIR_LOCK_SEAL) { sealedSide = true; @@ -241,7 +241,7 @@ public void unsealAirLock() { } continue; } - breathable = this.level.isBreathable(pos.below()); + breathable = this.level.galacticraft$isBreathable(pos.below()); if (breathable) { if (this.level.getBlockState(pos).getBlock() == GCBlocks.AIR_LOCK_SEAL) { sealedSide = true; @@ -271,7 +271,7 @@ public void unsealAirLock() { for (x = this.lastProtocol.minX + 1; x <= this.lastProtocol.maxX - 1; x++) { for (y = this.lastProtocol.minY + 1; y <= this.lastProtocol.maxY - 1; y++) { pos = new BlockPos(x, y, z); - breathable = this.level.isBreathable(pos.north()); + breathable = this.level.galacticraft$isBreathable(pos.north()); if (breathable) { if (this.level.getBlockState(pos).is(GCBlocks.AIR_LOCK_SEAL)) { sealedSide = true; @@ -279,7 +279,7 @@ public void unsealAirLock() { } continue; } - breathable = this.level.isBreathable(pos.south()); + breathable = this.level.galacticraft$isBreathable(pos.south()); if (breathable) { if (this.level.getBlockState(pos).is(GCBlocks.AIR_LOCK_SEAL)) { sealedSide = true; @@ -307,7 +307,7 @@ public void unsealAirLock() { for (z = this.lastProtocol.minZ + 1; z <= this.lastProtocol.maxZ - 1; z++) { for (y = this.lastProtocol.minY + 1; y <= this.lastProtocol.maxY - 1; y++) { pos = new BlockPos(x, y, z); - breathable = this.level.isBreathable(pos.west()); + breathable = this.level.galacticraft$isBreathable(pos.west()); if (breathable) { if (this.level.getBlockState(pos).is(GCBlocks.AIR_LOCK_SEAL)) { sealedSide = true; @@ -315,7 +315,7 @@ public void unsealAirLock() { } continue; } - breathable = this.level.isBreathable(pos.east()); + breathable = this.level.galacticraft$isBreathable(pos.east()); if (breathable) { if (this.level.getBlockState(pos).is(GCBlocks.AIR_LOCK_SEAL)) { sealedSide = true; diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CoalGeneratorBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CoalGeneratorBlockEntity.java index c27c7e910a..bf1379ce16 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CoalGeneratorBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CoalGeneratorBlockEntity.java @@ -61,7 +61,6 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.properties.BlockStateProperties; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -195,8 +194,7 @@ public void updateActiveState(Level level, BlockPos pos, BlockState state, boole } private boolean shouldExtinguish(ServerLevel level, BlockPos pos, BlockState state) { - return !level.isBreathable(pos.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING))) - && !level.isBreathable(pos); + return !level.galacticraft$isBreathable(pos); } private MachineStatus consumeFuel(ServerLevel level, BlockPos pos, BlockState state) { diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CompressorBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CompressorBlockEntity.java index ab5c995c6e..2692a7f110 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CompressorBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/CompressorBlockEntity.java @@ -53,7 +53,6 @@ import net.minecraft.world.item.crafting.RecipeHolder; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.properties.BlockStateProperties; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -180,8 +179,7 @@ public void updateActiveState(Level level, BlockPos pos, BlockState state, boole } private boolean shouldExtinguish(Level level, BlockPos pos, BlockState state) { - return !level.isBreathable(pos.relative(state.getValue(BlockStateProperties.HORIZONTAL_FACING))) - && !level.isBreathable(pos); + return !level.galacticraft$isBreathable(pos); } @Override diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenBubbleDistributorBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenBubbleDistributorBlockEntity.java index 81381f6689..9f3e861f41 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenBubbleDistributorBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenBubbleDistributorBlockEntity.java @@ -23,7 +23,11 @@ package dev.galacticraft.mod.content.block.entity.machine; import com.mojang.datafixers.util.Pair; +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; +import dev.galacticraft.api.block.entity.AtmosphereProvider; import dev.galacticraft.api.gas.Gases; +import dev.galacticraft.impl.internal.oxygen.ReverseSortedPosList; +import dev.galacticraft.impl.internal.oxygen.SortedPosList; import dev.galacticraft.machinelib.api.block.entity.MachineBlockEntity; import dev.galacticraft.machinelib.api.filter.ResourceFilters; import dev.galacticraft.machinelib.api.machine.MachineStatus; @@ -47,6 +51,7 @@ import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.core.BlockPos; import net.minecraft.core.HolderLookup; +import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerLevel; @@ -59,10 +64,14 @@ import net.minecraft.world.level.Level; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.chunk.LevelChunk; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class OxygenBubbleDistributorBlockEntity extends MachineBlockEntity { + +public class OxygenBubbleDistributorBlockEntity extends MachineBlockEntity implements AtmosphereProvider { + public static final int MAX_SIZE = 12; + public static final int CHARGE_SLOT = 0; public static final int OXYGEN_INPUT_SLOT = 1; // REVIEW: should this be 0 or 1? public static final int OXYGEN_TANK = 0; @@ -96,26 +105,23 @@ public class OxygenBubbleDistributorBlockEntity extends MachineBlockEntity { private boolean bubbleVisible = true; private double size = 0; - private byte targetSize = 1; + private int targetSize = 0; private int players = 0; private double prevSize; - private boolean oxygenUnloaded = true; - private boolean oxygenWorld = true; + + // todo: drop reverse list, track indices instead. + private final SortedPosList sealedListeners; + private final ReverseSortedPosList unsealedListeners; public OxygenBubbleDistributorBlockEntity(BlockPos pos, BlockState state) { super(GCBlockEntityTypes.OXYGEN_BUBBLE_DISTRIBUTOR, pos, state, SPEC); - } - - @Override - public void setLevel(Level level) { - super.setLevel(level); - this.oxygenWorld = level.getDefaultBreathable(); + this.sealedListeners = new SortedPosList(pos); + this.unsealedListeners = new ReverseSortedPosList(pos); } @Override protected void tickConstant(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ProfilerFiller profiler) { super.tickConstant(level, pos, state, profiler); - this.oxygenUnloaded = false; profiler.push("extract_resources"); this.chargeFromSlot(CHARGE_SLOT); this.takeFluidFromSlot(OXYGEN_INPUT_SLOT, OXYGEN_TANK, Gases.OXYGEN); @@ -130,25 +136,24 @@ protected void tickConstant(@NotNull ServerLevel level, @NotNull BlockPos pos, @ if (this.energyStorage().canExtract(Galacticraft.CONFIG.oxygenCollectorEnergyConsumptionRate())) { //todo: config profiler.push("bubble"); if (this.size > this.targetSize) { - this.setSize(Math.max(this.size - 0.1F, this.targetSize)); + this.setSizeAndUpdate(Math.max(this.size - 0.025F, this.targetSize)); //todo: change rate based on SA or volume } profiler.pop(); - this.trySyncSize(level, pos, profiler); + this.trySyncSize(level, pos); profiler.push("bubbler_distributor_transfer"); - long oxygenRequired = Math.max((long) ((4.0 / 3.0) * Math.PI * this.size * this.size * this.size), 1); + long oxygenRequired = Math.max((long) ((4.0 / 3.0) * Math.PI * this.size * this.size * this.size), 1); //todo: balance values FluidResourceSlot slot = this.fluidStorage().slot(OXYGEN_TANK); if (slot.canExtract(oxygenRequired)) { slot.extract(oxygenRequired); this.energyStorage().extract(Galacticraft.CONFIG.oxygenCollectorEnergyConsumptionRate()); if (this.size < this.targetSize) { - this.setSize(this.size + 0.05D); + this.setSizeAndUpdate(Math.min(this.targetSize, this.size + 0.05D)); //todo: change rate based on SA or volume } profiler.pop(); - this.distributeOxygenToArea(this.size, true); return GCMachineStatuses.DISTRIBUTING; } else { status = GCMachineStatuses.NOT_ENOUGH_OXYGEN; @@ -162,91 +167,126 @@ protected void tickConstant(@NotNull ServerLevel level, @NotNull BlockPos pos, @ } profiler.push("size"); - if (this.size > 0) { - this.setSize(this.size - 0.2D); - this.trySyncSize(level, pos, profiler); - this.distributeOxygenToArea(this.size, true); // technically this oxygen is being created from thin air + if (this.size != 0.0) { + this.setSizeAndUpdate(0.0); + this.trySyncSize(level, pos); } - if (this.size < 0) { - this.setSize(0); - } profiler.pop(); return status; } @Override - protected void tickDisabled(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ProfilerFiller profiler) { - if (this.size > 0) { - this.distributeOxygenToArea(this.size, this.oxygenWorld); - this.setSize(0); - } - this.trySyncSize(level, pos, profiler); - - super.tickDisabled(level, pos, state, profiler); - } - - @Override - public void setRemoved() { - if (!this.oxygenUnloaded) { - this.oxygenUnloaded = true; - this.distributeOxygenToArea(this.size, this.oxygenWorld); + protected void updateActiveState(Level level, BlockPos pos, BlockState state, boolean active) { + super.updateActiveState(level, pos, state, active); + if (!active && !level.isClientSide) { + if (this.size > 0) { + this.setSizeAndUpdate(0.0); + this.trySyncSize((ServerLevel) level, pos); + } } - super.setRemoved(); } - private void trySyncSize(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull ProfilerFiller profiler) { + private void trySyncSize(@NotNull ServerLevel level, @NotNull BlockPos pos) { // Could maybe get away with running this 1 in 10 ticks to reduce network traffic if (this.prevSize != this.size || this.players != level.players().size()) { this.players = level.players().size(); this.prevSize = this.size; - profiler.push("network"); + if (this.size < 0) this.size = 0; + BubbleSizePayload payload = new BubbleSizePayload(pos, this.size); for (ServerPlayer player : level.players()) { - if (this.size < 0) this.size = 0; - ServerPlayNetworking.send(player, new BubbleSizePayload(pos, this.size)); + ServerPlayNetworking.send(player, payload); + } + } + } + + // recalculate section claims and block subscribers based on new target size + public void distributeOxygenToArea(double targetSize, double prevSize) { + if (targetSize < prevSize) return; + + this.handleAllocation(targetSize, true); + assert this.level != null; + + int ceilSize = Mth.ceil(targetSize); + double prevSizeSq = Math.floor(prevSize * prevSize); + double curSizeSq = Math.floor(this.size * this.size); + double ceilSq = Math.ceil((targetSize + 1) * (targetSize + 1)); + for (BlockPos pos : BlockPos.betweenClosed(this.worldPosition.getX() - ceilSize, this.worldPosition.getY() - ceilSize, this.worldPosition.getZ() - ceilSize, + this.worldPosition.getX() + ceilSize, this.worldPosition.getY() + ceilSize, this.worldPosition.getZ() + ceilSize)) { + double distance = this.calculateDistanceSq(pos); + if (distance > prevSizeSq && distance <= ceilSq) { + BlockPos immutable = pos.immutable(); + BlockState state = this.level.getBlockState(pos); + if (state.getBlock().galacticraft$hasAtmosphereListener(state)) { + if (distance <= curSizeSq) { + this.sealedListeners.add(immutable, distance); + } else { + this.unsealedListeners.add(immutable, distance); + } + } else { + this.sealedListeners.remove(immutable, distance); + this.unsealedListeners.remove(immutable, distance); + } } - profiler.pop(); } } - public int getDistanceFromDistributor(int par1, int par3, int par5) { - final int d3 = this.getBlockPos().getX() - par1; - final int d4 = this.getBlockPos().getY() - par3; - final int d5 = this.getBlockPos().getZ() - par5; - return d3 * d3 + d4 * d4 + d5 * d5; + public double calculateDistanceSq(BlockPos object) { + return this.worldPosition.distToLowCornerSqr(object.getX(), object.getY() - 0.5, object.getZ()); } - public void distributeOxygenToArea(double size, boolean oxygenated) { - int radius = Mth.floor(Math.max(size, this.prevSize)) + 4; - int bubbleR2 = (int) (size * size); - BlockPos.MutableBlockPos pos = new BlockPos.MutableBlockPos(); - for (int x = this.getBlockPos().getX() - radius; x <= this.getBlockPos().getX() + radius; x++) { - for (int y = this.getBlockPos().getY() - radius; y <= this.getBlockPos().getY() + radius; y++) { - for (int z = this.getBlockPos().getZ() - radius; z <= this.getBlockPos().getZ() + radius; z++) { - if (this.getDistanceFromDistributor(x, y, z) <= bubbleR2) { - this.level.setBreathable(pos.set(x, y, z), oxygenated); + // mark all surrounding chunk sections within size as provided for (or no longer provided for, if allocate=false) + private void handleAllocation(double size, boolean allocate) { + int ceilSize = Mth.ceil(size) + 1; + int minX = SectionPos.blockToSectionCoord(this.worldPosition.getX() - ceilSize); + int minZ = SectionPos.blockToSectionCoord(this.worldPosition.getZ() - ceilSize); + int maxX = SectionPos.blockToSectionCoord(this.worldPosition.getX() + ceilSize); + int maxZ = SectionPos.blockToSectionCoord(this.worldPosition.getZ() + ceilSize); + int minSection = Math.max(0, this.level.getSectionIndex(this.worldPosition.getY() - ceilSize)); + int maxSection = Math.min(this.level.getMaxSection() - 1, this.level.getSectionIndex(this.worldPosition.getY() + ceilSize)); + + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + LevelChunk chunk = this.level.getChunk(x, z); + for (int sectionIndex = minSection; sectionIndex <= maxSection; sectionIndex++) { + if (allocate) { + ((ChunkOxygenAccessor) chunk).galacticraft$addAtmosphereProvider(sectionIndex, this.worldPosition); } else { - this.level.setBreathable(pos.set(x, y, z), this.oxygenWorld); + ((ChunkOxygenAccessor) chunk).galacticraft$removeAtmosphereProvider(sectionIndex, this.worldPosition); } } } } } - public byte getTargetSize() { + public int getTargetSize() { return this.targetSize; } - public void setTargetSize(byte targetSize) { + public void setTargetSize(int targetSize) { + this.distributeOxygenToArea(targetSize, this.targetSize); this.targetSize = targetSize; + this.setChanged(); } @Override protected void saveAdditional(CompoundTag tag, HolderLookup.Provider lookup) { super.saveAdditional(tag, lookup); - tag.putByte(Constant.Nbt.MAX_SIZE, this.targetSize); + tag.putInt(Constant.Nbt.MAX_SIZE, this.targetSize); tag.putDouble(Constant.Nbt.SIZE, this.size); tag.putBoolean(Constant.Nbt.VISIBLE, this.bubbleVisible); + + long[] sealedListeners = new long[this.sealedListeners.size()]; + long[] unsealedListeners = new long[this.unsealedListeners.size()]; + + for (int i = 0; i < this.sealedListeners.size(); i++) { + sealedListeners[i] = this.sealedListeners.get(i).asLong(); + } + for (int i = 0; i < this.unsealedListeners.size(); i++) { + unsealedListeners[i] = this.unsealedListeners.get(i).asLong(); + } + tag.putLongArray(Constant.Nbt.SEALED, sealedListeners); + tag.putLongArray(Constant.Nbt.UNSEALED, unsealedListeners); } @Override @@ -254,16 +294,89 @@ public void loadAdditional(CompoundTag tag, HolderLookup.Provider lookup) { super.loadAdditional(tag, lookup); this.size = tag.getDouble(Constant.Nbt.SIZE); if (this.size < 0) this.size = 0; - this.targetSize = tag.getByte(Constant.Nbt.MAX_SIZE); - if (this.targetSize < 1) this.targetSize = 1; + this.targetSize = tag.getInt(Constant.Nbt.MAX_SIZE); + if (this.targetSize < 0 || this.targetSize > MAX_SIZE) this.targetSize = 0; this.bubbleVisible = tag.getBoolean(Constant.Nbt.VISIBLE); + + this.sealedListeners.clear(); + this.unsealedListeners.clear(); + for (long value : tag.getLongArray(Constant.Nbt.SEALED)) { + this.sealedListeners.add(BlockPos.of(value)); + } + for (long l : tag.getLongArray(Constant.Nbt.UNSEALED)) { + this.unsealedListeners.add(BlockPos.of(l)); + } } public double getSize() { return this.size; } + public double getPrevSize() { + return prevSize; + } + + public void setSizeAndUpdate(double size) { + this.prevSize = this.size; + this.size = size; + + if (this.prevSize > this.size) { // shrink + if (this.sealedListeners.size() > 0) { + double sizeSq = this.size * this.size; + double maxDist = this.sealedListeners.getDistance(this.sealedListeners.size() - 1); + if (maxDist > sizeSq) { // check if furthest listener is now uncovered (newly unsealed) + for (int i = this.sealedListeners.size() - 1; i >= 0; i--) { + if (this.sealedListeners.size() == 0) break; + double distance = this.sealedListeners.getDistance(i); + if (distance > sizeSq) { + BlockPos pos = this.sealedListeners.get(i); + BlockState state = this.level.getBlockState(pos); + state.getBlock().galacticraft$onAtmosphereChange((ServerLevel) level, pos, state, level.galacticraft$getAtmosphereProviders(pos)); + if (this.sealedListeners.contains(pos, distance)) { + this.unsealedListeners.add(pos, distance); + if (i < this.sealedListeners.size() && this.sealedListeners.get(i) == pos) { + this.sealedListeners.remove(i); + } else { + this.sealedListeners.remove(pos, distance); + } + } + } + } + } + } + } else if (this.prevSize < this.size) { // grow + if (this.unsealedListeners.size() > 0) { + double sizeSq = this.size * this.size; + double minDist = this.unsealedListeners.getDistance(this.unsealedListeners.size() - 1); + if (minDist <= sizeSq) { // check if closest listener is now covered (newly sealed) + for (int i = this.unsealedListeners.size() - 1; i >= 0; i--) { + if (this.unsealedListeners.size() == 0) break; + double distance = this.unsealedListeners.getDistance(i); + if (distance <= sizeSq) { + BlockPos pos = this.unsealedListeners.get(i); + BlockState state = this.level.getBlockState(pos); + state.getBlock().galacticraft$onAtmosphereChange((ServerLevel) level, pos, state, level.galacticraft$getAtmosphereProviders(pos)); + if (this.unsealedListeners.contains(pos, distance)) { + this.sealedListeners.add(pos, distance); + if (i < this.unsealedListeners.size() && this.unsealedListeners.get(i) == pos) { + this.unsealedListeners.remove(i); + } else { + this.unsealedListeners.remove(pos, distance); + } + } + } + } + } + } + } + + //todo: deallocate sections on full shrink + + this.setChanged(); + } + public void setSize(double size) { + this.prevSize = this.size; this.size = size; } @@ -293,4 +406,51 @@ public void setBubbleVisible(boolean bubbleVisible) { this.populateUpdateTag(tag); return tag; } + + @Override + public boolean canBreathe(double x, double y, double z) { // -0.5 as bubble is on top of block + return this.worldPosition.distToCenterSqr(x, y - 0.5, z) <= this.size * this.size; + } + + @Override + public boolean canBreathe(int x, int y, int z) { + return this.worldPosition.distToLowCornerSqr(x, y - 0.5, z) <= this.size * this.size; + } + + @Override + public boolean canBreathe(BlockPos pos) { + return this.worldPosition.distToLowCornerSqr(pos.getX(), pos.getY() - 0.5, pos.getZ()) <= this.size * this.size; + } + + @Override + public void notifyStateChange(BlockPos pos, BlockState newState) { + double dist = this.calculateDistanceSq(pos); + if (newState.getBlock().galacticraft$hasAtmosphereListener(newState)) { + if (dist <= this.size * this.size) { + this.sealedListeners.add(pos.immutable(), dist); + this.unsealedListeners.remove(pos, dist); + } else { + this.unsealedListeners.add(pos.immutable(), dist); + this.sealedListeners.remove(pos, dist); + } + } else { + this.sealedListeners.remove(pos, dist); + this.unsealedListeners.remove(pos, dist); + } + } + + public void onBroken() { + this.handleAllocation(MAX_SIZE + 1, false); + this.size = 0; + int size = this.sealedListeners.size(); + for (int i = 0; i < size; i++) { + BlockPos pos = this.sealedListeners.get(i); + BlockState state = this.level.getBlockState(pos); + if (state.getBlock().galacticraft$hasAtmosphereListener(state)) { + state.getBlock().galacticraft$onAtmosphereChange((ServerLevel) this.level, pos, state, this.level.galacticraft$getAtmosphereProviders(pos)); + } + } + this.sealedListeners.clear(); + this.unsealedListeners.clear(); + } } \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenCollectorBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenCollectorBlockEntity.java index 18ead0babe..7f211bbfda 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenCollectorBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenCollectorBlockEntity.java @@ -93,7 +93,7 @@ public OxygenCollectorBlockEntity(BlockPos pos, BlockState state) { @Override public void setLevel(Level level) { super.setLevel(level); - this.oxygenWorld = level.getDefaultBreathable(); + this.oxygenWorld = level.galacticraft$isBreathable(); } private int collectOxygen(@NotNull ServerLevel level, @NotNull BlockPos pos) { diff --git a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenSealerBlockEntity.java b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenSealerBlockEntity.java index 2b2ef32318..f99b662122 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenSealerBlockEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/block/entity/machine/OxygenSealerBlockEntity.java @@ -23,6 +23,8 @@ package dev.galacticraft.mod.content.block.entity.machine; import com.mojang.datafixers.util.Pair; +import dev.galacticraft.api.accessor.ChunkOxygenAccessor; +import dev.galacticraft.api.block.entity.SpaceFillingAtmosphereProvider; import dev.galacticraft.api.gas.Gases; import dev.galacticraft.machinelib.api.block.entity.MachineBlockEntity; import dev.galacticraft.machinelib.api.filter.ResourceFilters; @@ -40,31 +42,44 @@ import dev.galacticraft.mod.Galacticraft; import dev.galacticraft.mod.content.GCBlockEntityTypes; import dev.galacticraft.mod.machine.GCMachineStatuses; +import dev.galacticraft.mod.network.s2c.OxygenSealerUpdatePayload; import dev.galacticraft.mod.screen.OxygenSealerMenu; +import dev.galacticraft.mod.tag.GCBlockTags; import dev.galacticraft.mod.util.FluidUtil; +import it.unimi.dsi.fastutil.objects.*; +import net.fabricmc.fabric.api.networking.v1.PlayerLookup; +import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; import net.minecraft.core.BlockPos; +import net.minecraft.core.Direction; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.SectionPos; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; import net.minecraft.server.level.ServerLevel; +import net.minecraft.server.level.ServerPlayer; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.player.Inventory; import net.minecraft.world.entity.player.Player; import net.minecraft.world.inventory.InventoryMenu; 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.level.chunk.LevelChunkSection; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; -public class OxygenSealerBlockEntity extends MachineBlockEntity { +import java.util.BitSet; + +public class OxygenSealerBlockEntity extends MachineBlockEntity implements SpaceFillingAtmosphereProvider { public static final int CHARGE_SLOT = 0; public static final int OXYGEN_INPUT_SLOT = 1; public static final int OXYGEN_TANK = 0; public static final long MAX_OXYGEN = FluidUtil.bucketsToDroplets(20); - public static final int SEAL_CHECK_TIME = 20; + public static final int SEAL_CHECK_TIME = 40; + public static final int MAX_SEALER_VOLUME = 1024; //2048 - private boolean isSealed = false; - private boolean hasEnergy = false; - private boolean hasOxygen = false; - private boolean blocked = false; + private int sealCheckTime = SEAL_CHECK_TIME; private static final StorageSpec SPEC = StorageSpec.of( MachineItemStorage.spec( @@ -92,19 +107,24 @@ public class OxygenSealerBlockEntity extends MachineBlockEntity { ) ); + /** + * Positions of all blocks that are sealed. This includes SOLID WALLS of the sealed space. + * Maps block position -> "is this block solid?" + */ + private final Object2BooleanOpenHashMap sealedPositions = new Object2BooleanOpenHashMap<>(); + /** + * Blocks that need to be notified if we seal/unseal them + */ + private final ObjectSet atmosphereAwareBlocks = new ObjectOpenHashSet<>(); + /** + * Whether some other block is currently sealing the space. + */ + private boolean contended; public OxygenSealerBlockEntity(BlockPos pos, BlockState state) { super(GCBlockEntityTypes.OXYGEN_SEALER, pos, state, SPEC); } - @Override - public void setLevel(Level level) { - super.setLevel(level); - if (!level.isClientSide) { - level.galacticraft$getSealerManager().addSealer(this); - } - } - @Override protected void tickConstant(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ProfilerFiller profiler) { super.tickConstant(level, pos, state, profiler); @@ -116,50 +136,151 @@ protected void tickConstant(@NotNull ServerLevel level, @NotNull BlockPos pos, @ @Override protected @NotNull MachineStatus tick(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ProfilerFiller profiler) { - // Check if the machine has enough energy if (!this.energyStorage().canExtract(Galacticraft.CONFIG.oxygenSealerEnergyConsumptionRate())) { - this.hasEnergy = false; return MachineStatuses.NOT_ENOUGH_ENERGY; } - this.hasEnergy = true; - // Check if the oxygen tank is empty if (this.fluidStorage().slot(OXYGEN_TANK).isEmpty()) { - this.hasOxygen = false; return GCMachineStatuses.NOT_ENOUGH_OXYGEN; } - this.hasOxygen = true; - if (!level.getBlockState(pos.offset(0, 1, 0)).isAir()) { - this.blocked = true; - return GCMachineStatuses.BLOCKED; - } - this.blocked = false; + if (this.contended || this.level.galacticraft$isBreathable()) { + // allow for instant swap if the other sealer fails for some reason. + if (this.sealCheckTime > 1) this.sealCheckTime--; - if (this.hasEnergy) { - this.consumeEnergy(); + return GCMachineStatuses.ALREADY_SEALED; } - if (this.isSealed) { - // Consume oxygen if sealed - this.consumeOxygen(); + if (!this.sealedPositions.isEmpty()) { // we have an active, valid seal. + this.energyStorage().extract(Galacticraft.CONFIG.oxygenSealerEnergyConsumptionRate()); + this.fluidStorage().slot(OXYGEN_TANK).extract(Galacticraft.CONFIG.oxygenSealerOxygenConsumptionRate()); return GCMachineStatuses.SEALED; - } else { - return GCMachineStatuses.AREA_TOO_LARGE; + } else if (--this.sealCheckTime == 0) { + this.sealCheckTime = SEAL_CHECK_TIME; + + // CALCULATE SEALED SPACE: + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + Object2BooleanOpenHashMap visited = new Object2BooleanOpenHashMap<>(); + ObjectArrayFIFOQueue queue = new ObjectArrayFIFOQueue<>(32); + + visited.put(pos, true); + visited.put(pos.relative(Direction.UP), false); + queue.enqueue(new SourcedPos(pos.relative(Direction.UP), Direction.DOWN)); + + while (!queue.isEmpty() && (queue.size() + visited.size() <= MAX_SEALER_VOLUME)) { + SourcedPos current = queue.dequeue(); + if (level.isOutsideBuildHeight(current.pos.getY())) { + if (queue.isEmpty()) queue.enqueue(new SourcedPos(null, Direction.DOWN)); // ensure no accidental passes! + break; + } + for (Direction direction : Direction.values()) { + if (direction == current.direction) continue; + mutable.setWithOffset(current.pos, direction); + if (!visited.containsKey(mutable)) { + BlockState bs = level.getBlockState(mutable); + boolean full = isSealable(mutable, bs); + BlockPos ps = mutable.immutable(); + visited.put(ps, full); + if (!full) { + queue.enqueue(new SourcedPos(ps, direction.getOpposite())); + } + } + } + } + + if (queue.isEmpty()) { + // found area - seal it! + this.sealBlocks(visited); + } // no area, no change. + } + return GCMachineStatuses.AREA_TOO_LARGE; + } + + @Override + protected void updateActiveState(Level level, BlockPos pos, BlockState state, boolean b) { + super.updateActiveState(level, pos, state, b); + + for (BlockPos pos1 : this.atmosphereAwareBlocks) { + this.notifyBlockOfSealChange(pos1); + } + } + + public boolean isSealed() { + return this.isActive(); + } + + public int getSealTickTime() { + return this.sealCheckTime; + } + + private void sealBlocks(Object2BooleanOpenHashMap visited) { + if (visited.isEmpty()) return; + Object2ObjectOpenHashMap visitedSections = new Object2ObjectOpenHashMap<>(); + BlockPos.MutableBlockPos section = new BlockPos.MutableBlockPos(); + + this.sealedPositions.ensureCapacity(visited.size() + this.sealedPositions.size()); + ObjectIterator> iterator = visited.object2BooleanEntrySet().fastIterator(); + while (iterator.hasNext()) { + Object2BooleanMap.Entry e = iterator.next(); + BlockPos pos = e.getKey(); + this.sealedPositions.put(pos, e.getBooleanValue()); + + section.set(SectionPos.blockToSectionCoord(pos.getX()), this.level.getSectionIndex(pos.getY()), SectionPos.blockToSectionCoord(pos.getZ())); + // we only need to 'claim' each section once. might as well cache the sections too. + if (!visitedSections.containsKey(section)) { + LevelChunk chunk = this.level.getChunk(section.getX(), section.getZ()); + LevelChunkSection section1 = chunk.getSection(section.getY()); + visitedSections.put(section.immutable(), section1); + ((ChunkOxygenAccessor) chunk).galacticraft$addAtmosphereProvider(section.getY(), this.worldPosition); + } + + LevelChunkSection chunkSection = visitedSections.get(section); + BlockState blockState = chunkSection.getBlockState(pos.getX() & 15, pos.getY() & 15, pos.getZ() & 15); + // if we sealed an atmospheric subscriber, then we need to let it know of the changes (now and future). + if (blockState.getBlock().galacticraft$hasAtmosphereListener(blockState)) { + if (this.isSealed()) { + this.level.galacticraft$notifyAtmosphereChange(pos, blockState); + } + this.atmosphereAwareBlocks.add(pos); + } } + + this.markChanged(); } @Override - protected void tickDisabled(@NotNull ServerLevel level, @NotNull BlockPos pos, @NotNull BlockState state, @NotNull ProfilerFiller profiler) { - super.tickDisabled(level, pos, state, profiler); + protected void saveAdditional(CompoundTag tag, HolderLookup.Provider lookup) { + super.saveAdditional(tag, lookup); + long[] positions = new long[this.sealedPositions.size()]; + BitSet walls = new BitSet(this.sealedPositions.size()); + long[] listeners = new long[this.atmosphereAwareBlocks.size()]; + int i = 0; + for (Object2BooleanMap.Entry e : this.sealedPositions.object2BooleanEntrySet()) { + positions[i] = e.getKey().asLong(); + walls.set(i++, e.getBooleanValue()); + } + i = 0; + for (BlockPos pos : this.atmosphereAwareBlocks) { + listeners[i] = pos.asLong(); + } + tag.putLongArray(Constant.Nbt.SEALED, positions); + tag.putLongArray(Constant.Nbt.SOLID, walls.toLongArray()); + tag.putLongArray(Constant.Nbt.LISTENERS, listeners); + tag.putBoolean(Constant.Nbt.CONTENDED, this.contended); } @Override - public void setRemoved() { - super.setRemoved(); - if (this.level != null && !this.level.isClientSide) { - this.level.galacticraft$getSealerManager().removeSealer(this); + public void loadAdditional(CompoundTag tag, HolderLookup.Provider lookup) { + super.loadAdditional(tag, lookup); + long[] sealed = tag.getLongArray(Constant.Nbt.SEALED); + BitSet solid = BitSet.valueOf(tag.getLongArray(Constant.Nbt.SOLID)); + for (int i = 0; i < sealed.length; i++) { + this.sealedPositions.put(BlockPos.of(sealed[i]), solid.get(i)); } + for (long listener : tag.getLongArray(Constant.Nbt.LISTENERS)) { + this.atmosphereAwareBlocks.add(BlockPos.of(listener)); + } + this.contended = tag.getBoolean(Constant.Nbt.CONTENDED); } @Nullable @@ -168,36 +289,282 @@ public MachineMenu createMenu(int syncId, Inventor return new OxygenSealerMenu(syncId, player, this); } - private void consumeOxygen() { - this.fluidStorage().slot(OXYGEN_TANK).extract(Galacticraft.CONFIG.oxygenSealerOxygenConsumptionRate()); + @Override + public boolean canBreathe(double x, double y, double z) { + return this.isSealed() && this.sealedPositions.containsKey(BlockPos.containing(x, y, z)); } - private void consumeEnergy() { - this.energyStorage().extract(Galacticraft.CONFIG.oxygenSealerEnergyConsumptionRate()); + @Override + public boolean canBreathe(BlockPos pos) { + return this.isSealed() && this.sealedPositions.containsKey(pos); } - public boolean isSealed() { - return this.isSealed; + @Override + public void notifyStateChange(BlockPos pos, BlockState newState) { + if (pos.equals(this.worldPosition) && !newState.is(this.getBlockState().getBlock())) { + this.destroySeal(); + return; + } + // we only care about state changes to sealed blocks + if (this.sealedPositions.containsKey(pos)) { + if (newState.getBlock().galacticraft$hasAtmosphereListener(newState)) { + this.atmosphereAwareBlocks.add(pos); + } else { + this.atmosphereAwareBlocks.remove(pos); + } + + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + if (this.sealedPositions.getBoolean(pos)) { + if (!isSealable(pos, newState)) { + // was sealed -> now not. check for leaks + this.sealedPositions.put(pos, false); + + Object2BooleanOpenHashMap visited = new Object2BooleanOpenHashMap<>(); + boolean sealable = true; + + for (Direction direction : Direction.values()) { + mutable.setWithOffset(pos, direction); + if (!this.sealedPositions.containsKey(mutable) && !visited.containsKey(mutable)) { + if (isSealable(mutable, level.getBlockState(mutable))) { + visited.put(mutable.immutable(), true); // newly uncovered WALL. + if (this.sealedPositions.size() == MAX_SEALER_VOLUME) { + sealable = false; + break; + } + } else if (!this.trySeal(pos, direction, visited) || visited.size() + this.sealedPositions.size() > MAX_SEALER_VOLUME) { // newly made unsealed hole - try to seal it. + sealable = false; + break; + } + } + } + if (sealable) { + // it's still sealed - update! + this.sealBlocks(visited); + } else { + // no longer sealed - destroy. + this.destroySeal(); + } + } + } else if (isSealable(pos, newState)) { + // was not sealed -> now is. check if it blocks others. + this.sealedPositions.put(pos, true); + + // can use pure virtual search; no leaks are possible since a new solid block was placed. + ObjectOpenHashSet visited = new ObjectOpenHashSet<>(); + ObjectOpenHashSet visitedNonSolid = new ObjectOpenHashSet<>(); + // SSSSS need to navigate back to sealer +1y to confirm connection as the seal is emitted from above + // WWMWW + // UUUUU <- if we only checked WP, then newly disconnected pockets below the sealer would be considered connected + BlockPos target = this.worldPosition.relative(Direction.UP); + + // special case: block placed on top of sealer + if (target.equals(pos)) { + this.destroySeal(); + Object2BooleanOpenHashMap sealed = new Object2BooleanOpenHashMap<>(); + sealed.put(pos, true); + this.sealBlocks(sealed); + this.markChanged(); + return; + } + + // check that all surrounding blocks are still somehow connected to the sealed space. + boolean anyPass = false; + for (Direction direction : Direction.values()) { + mutable.setWithOffset(pos, direction); + if (this.sealedPositions.containsKey(mutable)) { + visited.clear(); + visitedNonSolid.clear(); + if (!this.tryNavigateTo(mutable, target, visited, visitedNonSolid)) { + this.destroySubSeal(visited, visitedNonSolid); + } else { + anyPass = true; + } + } + } + if (!anyPass) { + // This should not happen??? + Constant.LOGGER.warn("Block placement resulted in total seal destruction?"); + this.destroySeal(); + } + } + + this.markChanged(); + } } - public void setSealed(boolean sealed) { - this.isSealed = sealed; + // removes the given positons from the seal, updating the blocks accordingly. + private void destroySubSeal(ObjectOpenHashSet visited, ObjectOpenHashSet visitedNonSolid) { + // all non-solid blocks can be removed immediately + for (BlockPos pos : visitedNonSolid) { + this.sealedPositions.removeBoolean(pos); + visited.remove(pos); + if (this.atmosphereAwareBlocks.remove(pos)) { + this.notifyBlockOfSealChange(pos); + } + } + + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + // solid blocks are more complicated - must check for adjacent sealed non-solid blocks before removing + // SSWUU <- given this pattern (sealed, wall, unsealed) we need the walls to remain sealed even though + // SSWUU they're connected to the newly unsealed section + for (BlockPos pos : visited) { + boolean anyPath = false; + for (Direction direction : Direction.values()) { + mutable.setWithOffset(pos, direction); + if (!this.sealedPositions.getOrDefault(mutable, true)) { + anyPath = true; + break; + } + } + if (!anyPath) { + // all surrounding blocks either SOLID (not transitive), or otherwise not breathable - this block is not sealed anymore + this.sealedPositions.removeBoolean(pos); + if (this.atmosphereAwareBlocks.remove(pos)) { + this.notifyBlockOfSealChange(pos); + } + } + } } - public boolean hasEnergy() { - return this.hasEnergy; + // attempts to navigate (via BFS) to the target position + private boolean tryNavigateTo(BlockPos.MutableBlockPos mutable, BlockPos target, ObjectOpenHashSet visited, ObjectOpenHashSet visitedNonSolid) { + if (this.sealedPositions.getOrDefault(mutable, false) || mutable.equals(target)) { + visited.add(mutable.immutable()); + return mutable.equals(target); + } + + BlockPos start = mutable.immutable(); + ObjectArrayFIFOQueue queue = new ObjectArrayFIFOQueue<>(32); + queue.enqueue(start); + visited.add(start); + + while (!queue.isEmpty()) { + BlockPos current = queue.dequeue(); + for (Direction direction : Direction.values()) { + mutable.setWithOffset(current, direction); + if (target.equals(mutable)) return true; + if (this.sealedPositions.containsKey(mutable)) { + if (!visited.contains(mutable)) { + BlockPos pos = mutable.immutable(); + visited.add(pos); + if (!this.sealedPositions.getBoolean(mutable)) { + visitedNonSolid.add(pos); + queue.enqueue(pos); + } + } + } + } + } + return false; } - public boolean hasOxygen() { - return this.hasOxygen; + // attempts to from a seal starting from the given block & direction + private boolean trySeal(BlockPos from, Direction fromDir, Object2BooleanMap visited) { + BlockPos.MutableBlockPos mutable = new BlockPos.MutableBlockPos(); + ObjectArrayFIFOQueue queue = new ObjectArrayFIFOQueue<>(32); + + visited.put(from, false); + visited.put(from.relative(fromDir), false); // newly uncovered AIR. + queue.enqueue(new SourcedPos(from.relative(fromDir), fromDir.getOpposite())); + + while (!queue.isEmpty() && (this.sealedPositions.size() + queue.size() + visited.size() <= MAX_SEALER_VOLUME)) { + SourcedPos current = queue.dequeue(); + if (this.level.isOutsideBuildHeight(current.pos.getY())) return false; + + for (Direction direction : Direction.values()) { + if (direction == current.direction) continue; + mutable.setWithOffset(current.pos, direction); + // if NOT sealed and NOT already visited, it's a new spot. + if (!this.sealedPositions.containsKey(mutable) && !visited.containsKey(mutable)) { + BlockState bs = this.level.getBlockState(mutable); + boolean full = isSealable(mutable, bs); + BlockPos ps = mutable.immutable(); + visited.put(ps, full); + if (!full) { + queue.enqueue(new SourcedPos(ps, direction.getOpposite())); + } + } + } + } + + return queue.isEmpty(); } - public boolean isBlocked() { - return this.blocked; + public void destroySeal() { + assert this.level != null; + for (BlockPos pos : this.sealedPositions.keySet()) { + // todo: make more efficient + ((ChunkOxygenAccessor) this.level.getChunkAt(pos)).galacticraft$removeAtmosphereProvider(this.level.getSectionIndex(pos.getY()), this.worldPosition); + } + this.sealedPositions.clear(); + this.markChanged(); + + // was sealed, notify blocks of loss + if (this.isSealed()) { + for (BlockPos pos : this.atmosphereAwareBlocks) { + this.notifyBlockOfSealChange(pos); + } + } } - public int getSealTickTime() { - if (this.level == null) return 0; - return SEAL_CHECK_TIME - (int) (this.level.getGameTime() % SEAL_CHECK_TIME); + private void notifyBlockOfSealChange(BlockPos pos) { + BlockState state = this.level.getBlockState(pos); + if (state.getBlock().galacticraft$hasAtmosphereListener(state)) { + state.getBlock().galacticraft$onAtmosphereChange(((ServerLevel) this.level), pos, state, level.galacticraft$getAtmosphereProviders(pos)); + } + } + + private void markChanged() { + this.setChanged(); + CustomPacketPayload updatePayload = this.createUpdatePayload(); + for (ServerPlayer player : PlayerLookup.tracking(this)) { + ServerPlayNetworking.send(player, updatePayload); + } + } + + private boolean isSealable(BlockPos pos, BlockState newState) { + return newState.isCollisionShapeFullBlock(this.level, pos) || newState.is(GCBlockTags.SEALABLE); + } + + @Override + public @NotNull CustomPacketPayload createUpdatePayload() { + BlockPos[] positions = this.sealedPositions.keySet().toArray(new BlockPos[0]); + BitSet values = new BitSet(this.sealedPositions.size()); + int i = 0; + for (boolean value : this.sealedPositions.values()) { + if (value) values.set(i); + i++; + } + return new OxygenSealerUpdatePayload(this.worldPosition, positions, values); } + + @Override + public void populateUpdateTag(CompoundTag tag) { + super.populateUpdateTag(tag); + long[] positions = new long[this.sealedPositions.size()]; + BitSet set = new BitSet(this.sealedPositions.size()); + int i = 0; + for (Object2BooleanMap.Entry e : this.sealedPositions.object2BooleanEntrySet()) { + positions[i] = e.getKey().asLong(); + set.set(i++, e.getBooleanValue()); + } + tag.putLongArray(Constant.Nbt.SEALED, positions); + tag.putLongArray(Constant.Nbt.SOLID, set.toLongArray()); + } + + public void handleUpdate(OxygenSealerUpdatePayload payload) { + this.sealedPositions.clear(); + BlockPos[] positions = payload.positions(); + BitSet set = payload.set(); + for (int i = 0; i < positions.length; i++) { + this.sealedPositions.put(positions[i], set.get(i)); + } + } + + public void markContended(boolean contended) { + assert this.isActive() != contended; + this.contended = contended; + } + + private record SourcedPos(BlockPos pos, Direction direction) {} } diff --git a/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneLanternBlock.java b/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneLanternBlock.java index 5162dff24a..82c4553ee6 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneLanternBlock.java +++ b/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneLanternBlock.java @@ -29,6 +29,7 @@ import net.minecraft.world.item.ItemStack; import net.minecraft.world.item.TooltipFlag; import net.minecraft.world.level.block.LanternBlock; +import net.minecraft.world.level.block.state.BlockState; import java.util.List; @@ -42,4 +43,14 @@ public void appendHoverText(ItemStack stack, Item.TooltipContext context, List codec() { return CODEC; } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return false; // overrides the mixin + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return false; // overrides the mixin + } } diff --git a/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneWallTorchBlock.java b/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneWallTorchBlock.java index ed195f18f8..d704122fc4 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneWallTorchBlock.java +++ b/src/main/java/dev/galacticraft/mod/content/block/environment/GlowstoneWallTorchBlock.java @@ -53,4 +53,14 @@ public void appendHoverText(ItemStack stack, Item.TooltipContext context, List CODEC = RecordCodecBuilder.mapCodec(instance -> instance.group( Block.CODEC.fieldOf("lit_block").forGetter(unlitLantern -> unlitLantern.litBlock), propertiesCodec() @@ -74,4 +75,14 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev return super.useItemOn(stack, state, level, pos, player, hand, hit); } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return false; // overrides the mixin + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return false; // overrides the mixin + } } diff --git a/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitTorchBlock.java b/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitTorchBlock.java index 5f47b439c6..e0b4926e51 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitTorchBlock.java +++ b/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitTorchBlock.java @@ -85,4 +85,14 @@ public void animateTick(BlockState blockState, Level level, BlockPos blockPos, R public MapCodec codec() { return CODEC; } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return false; // overrides the mixin + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return false; // overrides the mixin + } } \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitWallTorchBlock.java b/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitWallTorchBlock.java index 3542f51888..267f524e28 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitWallTorchBlock.java +++ b/src/main/java/dev/galacticraft/mod/content/block/environment/UnlitWallTorchBlock.java @@ -80,4 +80,14 @@ protected ItemInteractionResult useItemOn(ItemStack stack, BlockState state, Lev public void animateTick(BlockState blockState, Level world, BlockPos blockPos, RandomSource random) { // stop particles from spawning } + + @Override + public boolean galacticraft$hasLegacyExtinguishTransform(BlockState state) { + return false; // overrides the mixin + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return false; // overrides the mixin + } } \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenBubbleDistributorBlock.java b/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenBubbleDistributorBlock.java new file mode 100644 index 0000000000..312d39b8ec --- /dev/null +++ b/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenBubbleDistributorBlock.java @@ -0,0 +1,61 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.mod.content.block.machine; + +import com.mojang.serialization.MapCodec; +import dev.galacticraft.machinelib.api.block.MachineBlock; +import dev.galacticraft.machinelib.api.block.entity.MachineBlockEntity; +import dev.galacticraft.mod.content.block.entity.machine.OxygenBubbleDistributorBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.BaseEntityBlock; +import net.minecraft.world.level.block.state.BlockState; +import org.jetbrains.annotations.NotNull; + +public class OxygenBubbleDistributorBlock extends MachineBlock { + private static final MapCodec CODEC = simpleCodec(OxygenBubbleDistributorBlock::new); + + public OxygenBubbleDistributorBlock(Properties settings) { + super(settings); + } + + @Override + protected @NotNull MapCodec codec() { + return CODEC; + } + + @Override + public @NotNull MachineBlockEntity newBlockEntity(BlockPos pos, BlockState state) { + return new OxygenBubbleDistributorBlockEntity(pos, state); + } + + @Override + protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { + if (newState.getBlock() != this) { + if (!level.isClientSide && level.getBlockEntity(pos) instanceof OxygenBubbleDistributorBlockEntity be) { + be.onBroken(); + } + } + super.onRemove(state, level, pos, newState, moved); + } +} \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenSealerBlock.java b/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenSealerBlock.java index d90ebc1cf4..de5323a362 100644 --- a/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenSealerBlock.java +++ b/src/main/java/dev/galacticraft/mod/content/block/machine/OxygenSealerBlock.java @@ -23,17 +23,21 @@ package dev.galacticraft.mod.content.block.machine; import com.mojang.serialization.MapCodec; +import dev.galacticraft.api.block.entity.AtmosphereProvider; +import dev.galacticraft.api.block.entity.SpaceFillingAtmosphereProvider; import dev.galacticraft.machinelib.api.block.MachineBlock; import dev.galacticraft.machinelib.api.block.entity.MachineBlockEntity; import dev.galacticraft.mod.content.block.entity.machine.OxygenSealerBlockEntity; import net.minecraft.core.BlockPos; -import net.minecraft.util.RandomSource; +import net.minecraft.server.level.ServerLevel; import net.minecraft.world.level.Level; import net.minecraft.world.level.block.BaseEntityBlock; import net.minecraft.world.level.block.state.BlockState; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.util.Iterator; + public class OxygenSealerBlock extends MachineBlock { private static final MapCodec CODEC = simpleCodec(OxygenSealerBlock::new); @@ -52,7 +56,55 @@ public OxygenSealerBlock(Properties settings) { } @Override - public void animateTick(BlockState state, Level world, BlockPos pos, RandomSource random) { + protected void onRemove(BlockState state, Level level, BlockPos pos, BlockState newState, boolean moved) { + super.onRemove(state, level, pos, newState, moved); + if (!newState.is(this)) { + if (level.getBlockEntity(pos) instanceof OxygenSealerBlockEntity be) { + be.destroySeal(); + } + } + } + + @Override + public boolean galacticraft$hasAtmosphereListener(BlockState state) { + return true; + } + + @Override + public void onPlace(BlockState state, Level level, BlockPos pos, BlockState blockState2, boolean bl) { + super.onPlace(state, level, pos, blockState2, bl); + OxygenSealerBlockEntity be = (OxygenSealerBlockEntity) level.getBlockEntity(pos); + Iterator iterator = level.galacticraft$getAtmosphereProviders(pos); + assert be != null; + while (iterator.hasNext()) { + AtmosphereProvider next = iterator.next(); + if (!pos.equals(next.be().getBlockPos()) && next instanceof SpaceFillingAtmosphereProvider) { + if (next.canBreathe(pos)) { + be.markContended(true); + return; + } + } + } + be.markContended(false); + } + + @Override + public void galacticraft$onAtmosphereChange(ServerLevel level, BlockPos pos, BlockState state, Iterator iterator) { + checkContention(level, pos, iterator); + } + private static void checkContention(ServerLevel level, BlockPos pos, Iterator iterator) { + OxygenSealerBlockEntity be = (OxygenSealerBlockEntity) level.getBlockEntity(pos); + assert be != null; + while (iterator.hasNext()) { + AtmosphereProvider next = iterator.next(); + if (!pos.equals(next.be().getBlockPos()) && next instanceof SpaceFillingAtmosphereProvider) { + if (next.canBreathe(pos)) { + be.markContended(true); + return; + } + } + } + be.markContended(false); } } \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/content/entity/vehicle/RocketEntity.java b/src/main/java/dev/galacticraft/mod/content/entity/vehicle/RocketEntity.java index 80bceae84b..bc7f9022d3 100644 --- a/src/main/java/dev/galacticraft/mod/content/entity/vehicle/RocketEntity.java +++ b/src/main/java/dev/galacticraft/mod/content/entity/vehicle/RocketEntity.java @@ -513,7 +513,7 @@ && level().getBlockState(dockPos).getValue(AbstractLaunchPad.PART) != AbstractLa if (getLaunchStage().ordinal() >= LaunchStage.LAUNCHED.ordinal()) { if (ticksSinceJump > 1000 && this.onGround()) { - boolean createFire = this.level().getDefaultBreathable(); + boolean createFire = this.level().galacticraft$isBreathable(); for (Entity entity : this.getPassengers()) { if (entity instanceof ServerPlayer player) { diff --git a/src/main/java/dev/galacticraft/mod/events/GCEventHandlers.java b/src/main/java/dev/galacticraft/mod/events/GCEventHandlers.java index d8878b0e2d..cd2b86b703 100644 --- a/src/main/java/dev/galacticraft/mod/events/GCEventHandlers.java +++ b/src/main/java/dev/galacticraft/mod/events/GCEventHandlers.java @@ -22,7 +22,6 @@ package dev.galacticraft.mod.events; -import dev.galacticraft.api.registry.ExtinguishableBlockRegistry; import dev.galacticraft.api.registry.AcidTransformItemRegistry; import dev.galacticraft.api.universe.celestialbody.CelestialBody; import dev.galacticraft.api.universe.celestialbody.landable.Landable; @@ -37,7 +36,6 @@ import net.fabricmc.fabric.api.event.lifecycle.v1.ServerTickEvents; import net.fabricmc.fabric.api.networking.v1.PlayerLookup; import net.fabricmc.fabric.api.networking.v1.ServerPlayNetworking; -import net.minecraft.core.BlockPos; import net.minecraft.core.GlobalPos; import net.minecraft.core.Holder; import net.minecraft.network.chat.Component; @@ -49,8 +47,6 @@ import net.minecraft.world.entity.player.Player; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.ChunkPos; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; import static dev.galacticraft.mod.Constant.CelestialBody.EARTH; @@ -85,7 +81,6 @@ public static void onWorldTick(ServerLevel level) { footprintManager.footprintBlockChanges.clear(); } - level.galacticraft$getSealerManager().tick(); } public static void onServerTick(MinecraftServer server) { @@ -132,16 +127,6 @@ private static void throwMeteor(MinecraftServer server, ServerLevel level, Playe level.addFreshEntity(meteor); } - public static boolean extinguishBlock(Level level, BlockPos pos, BlockState oldState) { - ExtinguishableBlockRegistry.Entry entry = ExtinguishableBlockRegistry.INSTANCE.get(oldState.getBlock()); - if (entry == null) return false; - BlockState newState = entry.transform(oldState); - if (newState == null) return false; - level.setBlockAndUpdate(pos, newState); - entry.callback(new ExtinguishableBlockRegistry.Context(level, pos, oldState)); - return true; - } - public static boolean sulfuricAcidTransformItem(ItemEntity itemEntity, ItemStack original) { AcidTransformItemRegistry.Entry entry = AcidTransformItemRegistry.INSTANCE.get(original.getItem()); if (entry == null) return false; diff --git a/src/main/java/dev/galacticraft/mod/events/GCExtinguishable.java b/src/main/java/dev/galacticraft/mod/events/GCExtinguishable.java deleted file mode 100644 index 03aee2d61d..0000000000 --- a/src/main/java/dev/galacticraft/mod/events/GCExtinguishable.java +++ /dev/null @@ -1,149 +0,0 @@ -/* - * Copyright (c) 2019-2025 Team Galacticraft - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.galacticraft.mod.events; - -import dev.galacticraft.api.registry.ExtinguishableBlockRegistry; -import dev.galacticraft.mod.content.GCBlocks; -import dev.galacticraft.mod.mixin.AbstractCandleBlockInvoker; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.core.particles.ParticleTypes; -import net.minecraft.server.level.ServerLevel; -import net.minecraft.sounds.SoundEvents; -import net.minecraft.sounds.SoundSource; -import net.minecraft.tags.BlockTags; -import net.minecraft.util.RandomSource; -import net.minecraft.world.level.block.AbstractCandleBlock; -import net.minecraft.world.level.block.Blocks; -import net.minecraft.world.level.block.LanternBlock; -import net.minecraft.world.level.block.WallTorchBlock; -import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.level.block.state.properties.BlockStateProperties; - -public class GCExtinguishable { - public static void register() { - // Normal Fire - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.LANTERN, - state -> GCBlocks.UNLIT_LANTERN.defaultBlockState() - .setValue(LanternBlock.HANGING, state.getValue(LanternBlock.HANGING)) - .setValue(LanternBlock.WATERLOGGED, state.getValue(LanternBlock.WATERLOGGED)), - GCExtinguishable::extinguishLantern - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.TORCH, - state -> GCBlocks.UNLIT_TORCH.defaultBlockState(), - GCExtinguishable::extinguishTorch - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.WALL_TORCH, - state -> GCBlocks.UNLIT_WALL_TORCH.defaultBlockState() - .setValue(WallTorchBlock.FACING, state.getValue(WallTorchBlock.FACING)), - GCExtinguishable::extinguishTorch - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.CAMPFIRE, - state -> state.getValue(BlockStateProperties.LIT) ? state.setValue(BlockStateProperties.LIT, false) : null, - GCExtinguishable::extinguishCampfire - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.FIRE, Blocks.AIR.defaultBlockState(), GCExtinguishable::extinguishFire); - - // Soul Fire - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.SOUL_LANTERN, - state -> GCBlocks.UNLIT_SOUL_LANTERN.defaultBlockState() - .setValue(LanternBlock.HANGING, state.getValue(LanternBlock.HANGING)) - .setValue(LanternBlock.WATERLOGGED, state.getValue(LanternBlock.WATERLOGGED)), - GCExtinguishable::extinguishLantern - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.SOUL_TORCH, - state -> GCBlocks.UNLIT_SOUL_TORCH.defaultBlockState(), - GCExtinguishable::extinguishTorch - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.SOUL_WALL_TORCH, - state -> GCBlocks.UNLIT_SOUL_WALL_TORCH.defaultBlockState() - .setValue(WallTorchBlock.FACING, state.getValue(WallTorchBlock.FACING)), - GCExtinguishable::extinguishTorch - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.SOUL_CAMPFIRE, - state -> state.getValue(BlockStateProperties.LIT) ? state.setValue(BlockStateProperties.LIT, false) : null, - GCExtinguishable::extinguishCampfire - ); - ExtinguishableBlockRegistry.INSTANCE.add(Blocks.SOUL_FIRE, Blocks.AIR.defaultBlockState(), GCExtinguishable::extinguishFire); - - // Candles - ExtinguishableBlockRegistry.INSTANCE.add(BlockTags.CANDLES, - state -> state.getValue(BlockStateProperties.LIT) ? state.setValue(BlockStateProperties.LIT, false) : null, - GCExtinguishable::extinguishCandle - ); - ExtinguishableBlockRegistry.INSTANCE.add(BlockTags.CANDLE_CAKES, - state -> state.getValue(BlockStateProperties.LIT) ? state.setValue(BlockStateProperties.LIT, false) : null, - GCExtinguishable::extinguishCandle - ); - } - - public static void extinguishLantern(ExtinguishableBlockRegistry.Context context) { - if (context.level() instanceof ServerLevel level) { - BlockPos pos = context.pos(); - BlockState state = context.state(); - double x = pos.getX() + 0.5D; - double y = pos.getY() + (state.getValue(LanternBlock.HANGING) ? 0.25D : 0.1875D); - double z = pos.getZ() + 0.5D; - - for (Direction direction : Direction.Plane.HORIZONTAL) { - level.sendParticles(ParticleTypes.SMOKE, x + 0.27 * (double) direction.getStepX(), y, z + 0.27 * (double) direction.getStepZ(), 0, 0.0D, 0.0D, 0.0D, 0.0D); - } - level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); - } - } - - public static void extinguishTorch(ExtinguishableBlockRegistry.Context context) { - if (context.level() instanceof ServerLevel level) { - BlockPos pos = context.pos(); - double x = pos.getX() + 0.5D; - double y = pos.getY() + 0.92D; - double z = pos.getZ() + 0.5D; - - BlockState state = context.state(); - Direction direction = state.getBlock() instanceof WallTorchBlock ? state.getValue(WallTorchBlock.FACING).getOpposite() : Direction.UP; - level.sendParticles(ParticleTypes.SMOKE, x + 0.27 * (double) direction.getStepX(), y, z + 0.27 * (double) direction.getStepZ(), 0, 0.0D, 0.0D, 0.0D, 0.0D); - level.playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); - } - } - - public static void extinguishCandle(ExtinguishableBlockRegistry.Context context) { - if (context.state().getBlock() instanceof AbstractCandleBlock candle) { - BlockPos pos = context.pos(); - if (context.level() instanceof ServerLevel level) { - ((AbstractCandleBlockInvoker) candle).callGetParticleOffsets(context.state()).forEach(vec3 -> - level.sendParticles(ParticleTypes.SMOKE, (double) pos.getX() + vec3.x(), (double) pos.getY() + vec3.y(), (double) pos.getZ() + vec3.z(), 0, 0.0D, 0.1D, 0.0D, 0.0D) - ); - } - context.level().playSound(null, pos, SoundEvents.CANDLE_EXTINGUISH, SoundSource.BLOCKS, 1.0F, 1.0F); - } - } - - public static void extinguishCampfire(ExtinguishableBlockRegistry.Context context) { - context.level().playSound(null, context.pos(), SoundEvents.GENERIC_EXTINGUISH_FIRE, SoundSource.BLOCKS, 0.2F, 1.0F); - } - - public static void extinguishFire(ExtinguishableBlockRegistry.Context context) { - RandomSource randomSource = context.level().getRandom(); - context.level().playSound(null, context.pos(), SoundEvents.FIRE_EXTINGUISH, SoundSource.BLOCKS, 0.25F, 2.6F + (randomSource.nextFloat() - randomSource.nextFloat()) * 0.8F); - } -} diff --git a/src/main/java/dev/galacticraft/mod/events/GCInteractionEventHandlers.java b/src/main/java/dev/galacticraft/mod/events/GCInteractionEventHandlers.java index 5d12be0102..a3270da211 100644 --- a/src/main/java/dev/galacticraft/mod/events/GCInteractionEventHandlers.java +++ b/src/main/java/dev/galacticraft/mod/events/GCInteractionEventHandlers.java @@ -31,7 +31,6 @@ import dev.galacticraft.mod.util.Translations; import net.fabricmc.fabric.api.event.player.UseEntityCallback; import net.fabricmc.fabric.api.event.player.UseItemCallback; -import net.minecraft.core.BlockPos; import net.minecraft.core.component.DataComponents; import net.minecraft.network.chat.Component; import net.minecraft.stats.Stats; @@ -45,14 +44,13 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.LivingEntity; import net.minecraft.world.entity.TamableAnimal; -import net.minecraft.world.entity.animal.Wolf; import net.minecraft.world.entity.animal.Parrot; +import net.minecraft.world.entity.animal.Wolf; import net.minecraft.world.entity.player.Player; import net.minecraft.world.food.FoodProperties; import net.minecraft.world.item.ItemStack; import net.minecraft.world.level.Level; import net.minecraft.world.phys.EntityHitResult; -import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.Nullable; public class GCInteractionEventHandlers { @@ -175,8 +173,7 @@ public static InteractionResult feedParrot(Player player, Level level, Parrot pa } private static boolean cannotEatInNoAtmosphere(Player player, Level level) { - Vec3 eyePos = player.getEyePosition(); - if (Galacticraft.CONFIG.cannotEatInNoAtmosphere() && !level.getDefaultBreathable() && !level.isBreathable(new BlockPos((int) Math.floor(eyePos.x), (int) Math.floor(eyePos.y), (int) Math.floor(eyePos.z)))) { + if (Galacticraft.CONFIG.cannotEatInNoAtmosphere() && !level.galacticraft$isBreathable() && !level.galacticraft$isBreathable(player.getEyePosition())) { player.displayClientMessage(Component.translatable(Translations.Chat.CANNOT_EAT_IN_NO_ATMOSPHERE).withStyle(Constant.Text.RED_STYLE), true); return true; } @@ -184,8 +181,7 @@ private static boolean cannotEatInNoAtmosphere(Player player, Level level) { } private static boolean cannotFeedInNoAtmosphere(Player player, Level level, LivingEntity entity) { - Vec3 eyePos = entity.getEyePosition(); - if (Galacticraft.CONFIG.cannotEatInNoAtmosphere() && !level.getDefaultBreathable() && !level.isBreathable(new BlockPos((int) Math.floor(eyePos.x), (int) Math.floor(eyePos.y), (int) Math.floor(eyePos.z)))) { + if (Galacticraft.CONFIG.cannotEatInNoAtmosphere() && !level.galacticraft$isBreathable() && !level.galacticraft$isBreathable(player.getEyePosition())) { player.displayClientMessage(Component.translatable(Translations.Chat.CANNOT_FEED_IN_NO_ATMOSPHERE).withStyle(Constant.Text.RED_STYLE), true); return true; } diff --git a/src/main/java/dev/galacticraft/mod/events/GCSleepEventHandlers.java b/src/main/java/dev/galacticraft/mod/events/GCSleepEventHandlers.java index 18aa0f656c..a6716911d6 100644 --- a/src/main/java/dev/galacticraft/mod/events/GCSleepEventHandlers.java +++ b/src/main/java/dev/galacticraft/mod/events/GCSleepEventHandlers.java @@ -60,7 +60,7 @@ public static InteractionResult allowCryogenicSleep(LivingEntity entity, BlockPo public static boolean allowSettingSpawn(Player player, BlockPos sleepingPos) { Level level = player.level(); - return level.isBreathable(sleepingPos) || level.getBlockState(sleepingPos).getBlock() instanceof CryogenicChamberBlock; + return level.galacticraft$isBreathable(sleepingPos) || level.getBlockState(sleepingPos).getBlock() instanceof CryogenicChamberBlock; } public static Direction changeSleepPosition(LivingEntity entity, BlockPos sleepingPos, @Nullable Direction sleepingDirection) { @@ -75,7 +75,7 @@ public static Direction changeSleepPosition(LivingEntity entity, BlockPos sleepi public static Player.BedSleepingProblem sleepInSpace(Player player, BlockPos sleepingPos) { Level level = player.level(); - if (!level.isBreathable(sleepingPos) && level.getBlockState(sleepingPos).getBlock() instanceof BedBlock) { + if (!level.galacticraft$isBreathable(sleepingPos) && level.getBlockState(sleepingPos).getBlock() instanceof BedBlock) { player.sendSystemMessage(Component.translatable(Translations.Chat.BED_FAIL)); return Player.BedSleepingProblem.NOT_POSSIBLE_HERE; } diff --git a/src/main/java/dev/galacticraft/mod/machine/SealerManager.java b/src/main/java/dev/galacticraft/mod/machine/SealerManager.java deleted file mode 100644 index 23f4ed9f72..0000000000 --- a/src/main/java/dev/galacticraft/mod/machine/SealerManager.java +++ /dev/null @@ -1,154 +0,0 @@ -/* - * Copyright (c) 2019-2025 Team Galacticraft - * - * Permission is hereby granted, free of charge, to any person obtaining a copy - * of this software and associated documentation files (the "Software"), to deal - * in the Software without restriction, including without limitation the rights - * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell - * copies of the Software, and to permit persons to whom the Software is - * furnished to do so, subject to the following conditions: - * - * The above copyright notice and this permission notice shall be included in all - * copies or substantial portions of the Software. - * - * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR - * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, - * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE - * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER - * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, - * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE - * SOFTWARE. - */ - -package dev.galacticraft.mod.machine; - -import dev.galacticraft.mod.Constant; -import dev.galacticraft.mod.content.block.entity.machine.OxygenSealerBlockEntity; -import dev.galacticraft.mod.tag.GCBlockTags; -import net.minecraft.core.BlockPos; -import net.minecraft.core.Direction; -import net.minecraft.world.level.Level; -import net.minecraft.world.level.block.state.BlockState; - -import java.util.*; - -import static dev.galacticraft.mod.content.block.entity.machine.OxygenSealerBlockEntity.SEAL_CHECK_TIME; - -public class SealerManager { - - private static class SpaceToSeal { - - private final List sealers = new ArrayList<>(); - private final Set blocksToSeal = new HashSet<>(); - private final Deque floodFillQueue = new ArrayDeque<>(); - - public SpaceToSeal(OxygenSealerBlockEntity sealer) { - sealers.add(sealer); - floodFillQueue.add(sealer.getBlockPos().offset(0, 1, 0)); - } - - public boolean willSealSucceed() { - return floodFillQueue.isEmpty(); - } - - } - - private final Level level; - private final Map sealers = new HashMap<>(); - private final Set sealedBlocks = new HashSet<>(); - private int sealCheckTimer = SEAL_CHECK_TIME; - - public SealerManager(Level level) { - this.level = level; - } - - private static final double MAX_SEALER_VOLUME = 1024; - - public void tick() { - // Update sealing status periodically - if (this.level.getGameTime() % SEAL_CHECK_TIME == 0) { - updateSealedBlocks(); - } - } - - public void updateSealedBlocks() { - if (level.isClientSide) return; - - // If the level is breathable oxygen sealers don't change anything - if (level.getDefaultBreathable()) return; - - if (level.getServer() != null && !level.getServer().isReady()) { - Constant.LOGGER.info("World is not fully loaded, skipping sealing calculation"); - return; - } - - // Reset all sealed blocks from the previous update to not be breathable - for (BlockPos pos : sealedBlocks) level.setBreathable(pos, false); - sealedBlocks.clear(); - - Set spacesToSeal = new HashSet<>(); - for (Map.Entry entry : this.sealers.entrySet()) { - OxygenSealerBlockEntity sealer = entry.getValue(); - if (!sealer.hasEnergy()) continue; - if (!sealer.hasOxygen()) continue; - if (sealer.isBlocked()) continue; - - // Flood fill to find all blocks this sealer is trying to seal - SpaceToSeal spaceToSeal = new SpaceToSeal(sealer); - while (!spaceToSeal.floodFillQueue.isEmpty()) { - BlockPos pos = spaceToSeal.floodFillQueue.pollFirst(); - if (spaceToSeal.blocksToSeal.contains(pos)) continue; - // TODO: Better check to account for non-full blocks - BlockState blockState = this.level.getBlockState(pos); - if (blockState.is(GCBlockTags.SEALABLE)) continue; - if (blockState.isCollisionShapeFullBlock(this.level, pos) && !blockState.is(GCBlockTags.UNSEALABLE)) continue; - - spaceToSeal.blocksToSeal.add(pos); - for (Direction direction : Direction.values()) spaceToSeal.floodFillQueue.add(pos.relative(direction)); - - boolean willAlreadySeal = false; - for (SpaceToSeal otherSpace : spacesToSeal) { - if (spaceToSeal == otherSpace) continue; - if (!otherSpace.blocksToSeal.contains(pos)) continue; - // We have encountered a block that another sealer is trying to seal, - // so both sealers must be within the same space, so we combine them. - spaceToSeal.sealers.addAll(otherSpace.sealers); - spaceToSeal.blocksToSeal.addAll(otherSpace.blocksToSeal); - spaceToSeal.floodFillQueue.addAll(otherSpace.floodFillQueue); - willAlreadySeal = otherSpace.willSealSucceed(); - spacesToSeal.remove(otherSpace); - break; - } - // If we just combined with a space that was already filled, - // we must be in the same space, so we don't need to continue flood fill - if (willAlreadySeal) break; - - // If the space has become too large to fill, stop performing flood fill - if (spaceToSeal.blocksToSeal.size() > spaceToSeal.sealers.size() * MAX_SEALER_VOLUME) break; - } - spacesToSeal.add(spaceToSeal); - } - - for (SpaceToSeal spaceToSeal : spacesToSeal) { - for (OxygenSealerBlockEntity sealer : spaceToSeal.sealers) sealer.setSealed(spaceToSeal.willSealSucceed()); - if (!spaceToSeal.willSealSucceed()) continue; - for (BlockPos pos : spaceToSeal.blocksToSeal) { - sealedBlocks.add(pos); - level.setBreathable(pos, true); - } - } - } - - public void addSealer(OxygenSealerBlockEntity sealer) { - BlockPos pos = sealer.getBlockPos(); - Constant.LOGGER.info("Adding sealer at {} in dimension {}", pos, level.dimension().location()); - this.sealers.put(pos, sealer); - } - - public void removeSealer(OxygenSealerBlockEntity sealer) { - BlockPos pos = sealer.getBlockPos(); - Constant.LOGGER.info("Removing sealer at {} in dimension {}", pos, level.dimension().location()); - this.sealers.remove(pos); - } - -} diff --git a/src/main/java/dev/galacticraft/mod/mixin/AbstractFurnaceBlockEntityMixin.java b/src/main/java/dev/galacticraft/mod/mixin/AbstractFurnaceBlockEntityMixin.java index 208add95c3..b01de9d0f0 100644 --- a/src/main/java/dev/galacticraft/mod/mixin/AbstractFurnaceBlockEntityMixin.java +++ b/src/main/java/dev/galacticraft/mod/mixin/AbstractFurnaceBlockEntityMixin.java @@ -46,8 +46,8 @@ public abstract class AbstractFurnaceBlockEntityMixin extends BlockEntity { } @Inject(method = "serverTick", at = @At("HEAD")) - private static void gc$extinguishFurnace(Level level, BlockPos blockPos, BlockState blockState, AbstractFurnaceBlockEntity be, CallbackInfo ci) { - if (be.litTime > 0 && gc$shouldExtinguish(level, blockPos, blockState)) { + private static void extinguishFurnace(Level level, BlockPos blockPos, BlockState blockState, AbstractFurnaceBlockEntity be, CallbackInfo ci) { + if (be.litTime > 0 && shouldExtinguish(level, blockPos, blockState)) { be.litTime = 0; blockState = blockState.setValue(AbstractFurnaceBlock.LIT, false); level.setBlock(blockPos, blockState, Block.UPDATE_ALL); @@ -58,16 +58,15 @@ public abstract class AbstractFurnaceBlockEntityMixin extends BlockEntity { } @ModifyExpressionValue(method = "serverTick", at = @At(value = "INVOKE", target = "Lnet/minecraft/world/level/block/entity/AbstractFurnaceBlockEntity;canBurn(Lnet/minecraft/core/RegistryAccess;Lnet/minecraft/world/item/crafting/RecipeHolder;Lnet/minecraft/core/NonNullList;I)Z")) - private static boolean gc$canBurn(boolean original, Level level, BlockPos blockPos, BlockState blockState, AbstractFurnaceBlockEntity be) { - if (gc$shouldExtinguish(level, blockPos, blockState)) { + private static boolean canBurn(boolean original, Level level, BlockPos blockPos, BlockState blockState, AbstractFurnaceBlockEntity be) { + if (shouldExtinguish(level, blockPos, blockState)) { return false; } return original; } @Unique - private static boolean gc$shouldExtinguish(Level level, BlockPos blockPos, BlockState blockState) { - return !level.isBreathable(blockPos.relative(blockState.getValue(AbstractFurnaceBlock.FACING))) - && !level.isBreathable(blockPos); + private static boolean shouldExtinguish(Level level, BlockPos blockPos, BlockState blockState) { + return !level.galacticraft$isBreathable(blockPos); } } \ No newline at end of file diff --git a/src/main/java/dev/galacticraft/mod/mixin/CakeBlockMixin.java b/src/main/java/dev/galacticraft/mod/mixin/CakeBlockMixin.java index 4754d192bd..be7d01e156 100644 --- a/src/main/java/dev/galacticraft/mod/mixin/CakeBlockMixin.java +++ b/src/main/java/dev/galacticraft/mod/mixin/CakeBlockMixin.java @@ -29,12 +29,10 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.InteractionResult; import net.minecraft.world.entity.player.Player; -import net.minecraft.world.level.Level; import net.minecraft.world.level.LevelAccessor; import net.minecraft.world.level.block.Block; import net.minecraft.world.level.block.CakeBlock; import net.minecraft.world.level.block.state.BlockState; -import net.minecraft.world.phys.Vec3; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; @@ -47,18 +45,14 @@ public CakeBlockMixin(Properties settings) { } @Inject(method = "eat", at = @At("HEAD"), cancellable = true) - private static void galacticraft$eatCake(LevelAccessor levelAccessor, BlockPos blockPos, BlockState blockState, Player player, CallbackInfoReturnable cir) { - Level level = (Level) levelAccessor; - Vec3 playerEyePos = player.getEyePosition(); - + private static void checkEdible(LevelAccessor level, BlockPos blockPos, BlockState blockState, Player player, CallbackInfoReturnable cir) { if (player.galacticraft$hasMask()) { if (Galacticraft.CONFIG.cannotEatWithMask()) { player.displayClientMessage(Component.translatable(Translations.Chat.CANNOT_EAT_WITH_MASK).withStyle(Constant.Text.RED_STYLE), true); cir.setReturnValue(InteractionResult.FAIL); } } else if (Galacticraft.CONFIG.cannotEatInNoAtmosphere() - && !level.getDefaultBreathable() - && !level.isBreathable(new BlockPos((int) Math.floor(playerEyePos.x), (int) Math.floor(playerEyePos.y), (int) Math.floor(playerEyePos.z))) + && !level.galacticraft$isBreathable(player.getEyePosition()) ) { player.displayClientMessage(Component.translatable(Translations.Chat.CANNOT_EAT_IN_NO_ATMOSPHERE).withStyle(Constant.Text.RED_STYLE), true); cir.setReturnValue(InteractionResult.FAIL); diff --git a/src/main/java/dev/galacticraft/mod/network/GCPackets.java b/src/main/java/dev/galacticraft/mod/network/GCPackets.java index 38c08e54b4..b6b0b317cc 100644 --- a/src/main/java/dev/galacticraft/mod/network/GCPackets.java +++ b/src/main/java/dev/galacticraft/mod/network/GCPackets.java @@ -33,6 +33,7 @@ public static void register() { PayloadTypeRegistry.playS2C().register(FootprintPacket.TYPE, FootprintPacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(FootprintRemovedPacket.TYPE, FootprintRemovedPacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(OpenCelestialScreenPayload.TYPE, OpenCelestialScreenPayload.STREAM_CODEC); + PayloadTypeRegistry.playS2C().register(OxygenSealerUpdatePayload.TYPE, OxygenSealerUpdatePayload.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(ResetPerspectivePacket.TYPE, ResetPerspectivePacket.STREAM_CODEC); PayloadTypeRegistry.playS2C().register(CapeAssignmentsPacket.TYPE, CapeAssignmentsPacket.STREAM_CODEC); diff --git a/src/main/java/dev/galacticraft/mod/network/c2s/BubbleMaxPayload.java b/src/main/java/dev/galacticraft/mod/network/c2s/BubbleMaxPayload.java index e06cf849a0..392ae76d09 100644 --- a/src/main/java/dev/galacticraft/mod/network/c2s/BubbleMaxPayload.java +++ b/src/main/java/dev/galacticraft/mod/network/c2s/BubbleMaxPayload.java @@ -34,8 +34,8 @@ import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; -public record BubbleMaxPayload(byte max) implements C2SPayload { - public static final StreamCodec STREAM_CODEC = ByteBufCodecs.BYTE.map(BubbleMaxPayload::new, packet -> packet.max); +public record BubbleMaxPayload(int max) implements C2SPayload { + public static final StreamCodec STREAM_CODEC = ByteBufCodecs.BYTE.map(BubbleMaxPayload::new, packet -> (byte)packet.max); public static final ResourceLocation ID = Constant.id("bubble_max"); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(ID); @@ -45,8 +45,8 @@ public void handle(ServerPlayNetworking.@NotNull Context context) { if (context.player().containerMenu instanceof OxygenBubbleDistributorMenu menu) { OxygenBubbleDistributorBlockEntity machine = menu.be; if (machine.getSecurity().hasAccess(context.player())) { - if (max > 0) { - machine.setTargetSize(max); + if (this.max >= 0 && this.max <= OxygenBubbleDistributorBlockEntity.MAX_SIZE) { + machine.setTargetSize(this.max); } } } diff --git a/src/main/java/dev/galacticraft/mod/network/s2c/BubbleUpdatePayload.java b/src/main/java/dev/galacticraft/mod/network/s2c/BubbleUpdatePayload.java index 18baa2ccd1..61ca440fd6 100644 --- a/src/main/java/dev/galacticraft/mod/network/s2c/BubbleUpdatePayload.java +++ b/src/main/java/dev/galacticraft/mod/network/s2c/BubbleUpdatePayload.java @@ -34,13 +34,13 @@ import net.minecraft.resources.ResourceLocation; import org.jetbrains.annotations.NotNull; -public record BubbleUpdatePayload(BlockPos pos, byte maxSize, double size, boolean visible) implements S2CPayload { +public record BubbleUpdatePayload(BlockPos pos, int maxSize, double size, boolean visible) implements S2CPayload { public static final ResourceLocation ID = Constant.id("bubble_update"); public static final CustomPacketPayload.Type TYPE = new CustomPacketPayload.Type<>(ID); public static final StreamCodec STREAM_CODEC = StreamCodec.composite( BlockPos.STREAM_CODEC, p -> p.pos, - ByteBufCodecs.BYTE, + ByteBufCodecs.INT, p -> p.maxSize, ByteBufCodecs.DOUBLE, p -> p.size, diff --git a/src/main/java/dev/galacticraft/mod/network/s2c/OxygenSealerUpdatePayload.java b/src/main/java/dev/galacticraft/mod/network/s2c/OxygenSealerUpdatePayload.java new file mode 100644 index 0000000000..436a040039 --- /dev/null +++ b/src/main/java/dev/galacticraft/mod/network/s2c/OxygenSealerUpdatePayload.java @@ -0,0 +1,79 @@ +/* + * Copyright (c) 2019-2025 Team Galacticraft + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +package dev.galacticraft.mod.network.s2c; + +import dev.galacticraft.impl.network.s2c.S2CPayload; +import dev.galacticraft.mod.Constant; +import dev.galacticraft.mod.content.block.entity.machine.OxygenSealerBlockEntity; +import net.fabricmc.fabric.api.client.networking.v1.ClientPlayNetworking; +import net.minecraft.core.BlockPos; +import net.minecraft.network.FriendlyByteBuf; +import net.minecraft.network.codec.StreamCodec; +import net.minecraft.network.protocol.common.custom.CustomPacketPayload; +import net.minecraft.resources.ResourceLocation; +import org.jetbrains.annotations.NotNull; + +import java.util.BitSet; + +public record OxygenSealerUpdatePayload(BlockPos pos, BlockPos[] positions, BitSet set) implements S2CPayload { + public static final ResourceLocation ID = Constant.id("sealer_update"); + public static final Type TYPE = new Type<>(ID); + public static final StreamCodec STREAM_CODEC = new StreamCodec<>() { + @Override + public void encode(FriendlyByteBuf buf, OxygenSealerUpdatePayload object2) { + buf.writeBlockPos(object2.pos); + buf.writeVarInt(object2.positions.length); + for (BlockPos pos : object2.positions) { + buf.writeLong(pos.asLong()); + } + buf.writeLongArray(object2.set.toLongArray()); + } + + @Override + public OxygenSealerUpdatePayload decode(FriendlyByteBuf buf) { + BlockPos pos1 = buf.readBlockPos(); + int len = buf.readVarInt(); + BlockPos[] positions = new BlockPos[len]; + for (int i = 0; i < len; i++) { + positions[i] = BlockPos.of(buf.readLong()); + } + long[] longs = buf.readLongArray(); + BitSet set = BitSet.valueOf(longs); + return new OxygenSealerUpdatePayload(pos1, positions, set); + } + }; + + @Override + public Runnable handle(ClientPlayNetworking.@NotNull Context context) { + return () -> { + if (context.player().level().getBlockEntity(this.pos) instanceof OxygenSealerBlockEntity machine) { + machine.handleUpdate(this); + } + }; + } + + @Override + public @NotNull Type type() { + return TYPE; + } +} diff --git a/src/main/java/dev/galacticraft/mod/screen/OxygenBubbleDistributorMenu.java b/src/main/java/dev/galacticraft/mod/screen/OxygenBubbleDistributorMenu.java index 92f345420e..058fb349e0 100644 --- a/src/main/java/dev/galacticraft/mod/screen/OxygenBubbleDistributorMenu.java +++ b/src/main/java/dev/galacticraft/mod/screen/OxygenBubbleDistributorMenu.java @@ -32,7 +32,7 @@ public class OxygenBubbleDistributorMenu extends MachineMenu { public boolean bubbleVisible; - public byte targetSize; + public int targetSize; public double size; public OxygenBubbleDistributorMenu(int syncId, Player player, OxygenBubbleDistributorBlockEntity machine) { @@ -49,7 +49,7 @@ public OxygenBubbleDistributorMenu(int syncId, Inventory inv, BlockPos pos) { @Override public void registerData(@NotNull MenuData data) { super.registerData(data); - data.registerByte(this.be::getTargetSize, b -> this.targetSize = (byte) b); + data.registerInt(this.be::getTargetSize, b -> this.targetSize = b); data.registerDouble(this.be::getSize, d -> this.size = d); data.registerBoolean(this.be::isBubbleVisible, b -> this.bubbleVisible = b); } diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json index b8adee434e..c1f8170cb8 100644 --- a/src/main/resources/fabric.mod.json +++ b/src/main/resources/fabric.mod.json @@ -293,16 +293,15 @@ "custom": { "loom:injected_interfaces": { "net/minecraft/class_3222": [ - "dev/galacticraft/api/accessor/GearInventoryProvider", "dev/galacticraft/mod/accessor/ServerPlayerAccessor" ], - "net/minecraft/class_742": [ - "dev/galacticraft/api/accessor/GearInventoryProvider" - ], "net/minecraft/class_1309": [ "dev/galacticraft/api/accessor/GearInventoryProvider", "dev/galacticraft/mod/accessor/CryogenicAccessor" ], + "net/minecraft/class_4538": [ + "dev/galacticraft/api/accessor/LevelOxygenAccessorRO" + ], "net/minecraft/class_1937": [ "dev/galacticraft/api/accessor/LevelBodyAccessor", "dev/galacticraft/api/accessor/LevelOxygenAccessor", @@ -313,6 +312,9 @@ ], "net/minecraft/class_1297": [ "dev/galacticraft/mod/accessor/EntityAccessor" + ], + "net/minecraft/class_2248": [ + "dev/galacticraft/api/accessor/GCBlockExtensions" ] } }, diff --git a/src/main/resources/galacticraft-api.mixins.json b/src/main/resources/galacticraft-api.mixins.json index 2aefb14ebc..e8a3bc77a3 100644 --- a/src/main/resources/galacticraft-api.mixins.json +++ b/src/main/resources/galacticraft-api.mixins.json @@ -11,14 +11,27 @@ "gear.ServerPlayerMixin", "gear.TamableAnimalMixin", "gravity.EntityGravityMixin", + "oxygen.ChunkAccessMixin", "oxygen.ChunkHolderMixin", "oxygen.ChunkSerializerMixin", "oxygen.EmptyLevelChunkMixin", "oxygen.ImposterProtoChunkMixin", + "oxygen.LevelAccessorMixin", "oxygen.LevelChunkMixin", "oxygen.LevelChunkSectionMixin", "oxygen.LevelMixin", - "oxygen.ProtoChunkMixin", + "oxygen.ServerLevelMixin", + "oxygen.WorldGenRegionMixin", + "oxygen.extinguish.AbstractCandleBlockMixin", + "oxygen.extinguish.CampfireBlockMixin", + "oxygen.extinguish.EntityMixin", + "oxygen.extinguish.FireBlockMixin", + "oxygen.extinguish.LanternBlockMixin", + "oxygen.extinguish.LevelImplMixin", + "oxygen.extinguish.SoulFireBlockMixin", + "oxygen.extinguish.TorchBlockMixin", + "oxygen.extinguish.WallTorchBlockMixin", + "oxygen.extinguish.WeatheringCopperBlocksMixin", "research.AdvancementRewardsMixin", "research.ServerPlayerMixin" ], diff --git a/src/main/resources/galacticraft.mixins.json b/src/main/resources/galacticraft.mixins.json index 4410f06c0c..aca47aaa9f 100644 --- a/src/main/resources/galacticraft.mixins.json +++ b/src/main/resources/galacticraft.mixins.json @@ -21,7 +21,6 @@ "FlowingFluidMixin", "ItemEntityMixin", "ItemStackMixin", - "LevelMixin", "LivingEntityMixin", "ModelProviderMixin", "MultiNoiseBiomeSourceParameterListPresetAccessor",