diff --git a/gradle.properties b/gradle.properties index 272f9e8..6ef13b5 100755 --- a/gradle.properties +++ b/gradle.properties @@ -1,5 +1,5 @@ group=net.gensokyoreimagined.nitori -version=1.2-SNAPSHOT +version=1.3-SNAPSHOT description=Converting patches into mixins, for the Ignite Framework org.gradle.parallel=true diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/BlockStatePathingCache.java b/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/BlockStatePathingCache.java new file mode 100644 index 0000000..054a8b9 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/BlockStatePathingCache.java @@ -0,0 +1,4 @@ +package net.gensokyoreimagined.nitori.common.ai.pathing; + +public class BlockStatePathingCache { +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/PathNodeCache.java b/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/PathNodeCache.java new file mode 100644 index 0000000..c6884ac --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/ai/pathing/PathNodeCache.java @@ -0,0 +1,134 @@ +package net.gensokyoreimagined.nitori.common.ai.pathing; + +import net.gensokyoreimagined.nitori.common.block.BlockCountingSection; +import net.gensokyoreimagined.nitori.common.block.BlockStateFlags; +import net.gensokyoreimagined.nitori.common.util.Pos; +import net.gensokyoreimagined.nitori.common.world.ChunkView; +import net.gensokyoreimagined.nitori.common.world.WorldHelper; +import me.jellysquid.mods.lithium.mixin.ai.pathing.PathContextAccessor; +import net.minecraft.world.level.block.state.BlockBehaviour; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.pathfinder.WalkNodeEvaluator; +import net.minecraft.world.level.pathfinder.BinaryHeap; +import net.minecraft.entity.ai.pathing.PathNodeType; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.BlockView; +import net.minecraft.world.chunk.Chunk; +import net.minecraft.world.chunk.ChunkSection; + +public abstract class PathNodeCache { + private static boolean isChunkSectionDangerousNeighbor(ChunkSection section) { + return section.getBlockStateContainer() + .hasAny(state -> getNeighborPathNodeType(state) != PathNodeType.OPEN); + } + + public static PathNodeType getPathNodeType(BlockState state) { + return ((BlockStatePathingCache) state).lithium$getPathNodeType(); + } + + public static PathNodeType getNeighborPathNodeType(BlockBehaviour.AbstractBlockState state) { + return ((BlockStatePathingCache) state).lithium$getNeighborPathNodeType(); + } + + /** + * Returns whether a chunk section is free of dangers. This makes use of a caching layer to greatly + * accelerate neighbor danger checks when path-finding. + * + * @param section The chunk section to test for dangers + * @return True if this neighboring section is free of any dangers, otherwise false if it could + * potentially contain dangers + */ + public static boolean isSectionSafeAsNeighbor(ChunkSection section) { + // Empty sections can never contribute a danger + if (section.isEmpty()) { + return true; + } + + if (BlockStateFlags.ENABLED) { + return !((BlockCountingSection) section).lithium$mayContainAny(BlockStateFlags.PATH_NOT_OPEN); + } + return !isChunkSectionDangerousNeighbor(section); + } + + + public static PathNodeType getNodeTypeFromNeighbors(BinaryHeap context, int x, int y, int z, PathNodeType fallback) { + BlockView world = context.getWorld(); + + ChunkSection section = null; + + // Check that all the block's neighbors are within the same chunk column. If so, we can isolate all our block + // reads to just one chunk and avoid hits against the server chunk manager. + if (world instanceof ChunkView chunkView && WorldHelper.areNeighborsWithinSameChunkSection(x, y, z)) { + // If the y-coordinate is within bounds, we can cache the chunk section. Otherwise, the if statement to check + // if the cached chunk section was initialized will early-exit. + if (!world.isOutOfHeightLimit(y)) { + Chunk chunk = chunkView.lithium$getLoadedChunk(Pos.ChunkCoord.fromBlockCoord(x), Pos.ChunkCoord.fromBlockCoord(z)); + + // If the chunk is absent, the cached section above will remain null, as there is no chunk section anyway. + // An empty chunk or section will never pose any danger sources, which will be caught later. + if (chunk != null) { + section = chunk.getSectionArray()[Pos.SectionYIndex.fromBlockCoord(world, y)]; + } + } + + // If we can guarantee that blocks won't be modified while the cache is active, try to see if the chunk + // section is empty or contains any dangerous blocks within the palette. If not, we can assume any checks + // against this chunk section will always fail, allowing us to fast-exit. + if (section == null || PathNodeCache.isSectionSafeAsNeighbor(section)) { + return fallback; //TODO side effects of vanilla's path node caching + } + } + + int xStart = x - 1; + int yStart = y - 1; + int zStart = z - 1; + + int xEnd = x + 1; + int yEnd = y + 1; + int zEnd = z + 1; + + // Vanilla iteration order is XYZ + for (int adjX = xStart; adjX <= xEnd; adjX++) { + for (int adjY = yStart; adjY <= yEnd; adjY++) { + for (int adjZ = zStart; adjZ <= zEnd; adjZ++) { + // Skip the vertical column of the origin block + if (adjX == x && adjZ == z) { + continue; + } + + BlockState state; + + // If we're not accessing blocks outside a given section, we can greatly accelerate block state + // retrieval by calling upon the cached chunk directly. + if (section != null) { + state = section.getBlockState(adjX & 15, adjY & 15, adjZ & 15); + } else { + BlockPos.Mutable pos = ((PathContextAccessor) context).getLastNodePos().set(adjX, adjY, adjZ); + state = world.getBlockState(pos); + } + + if (state.isAir()) { + continue; + } + + PathNodeType neighborType = PathNodeCache.getNeighborPathNodeType(state); + + if (neighborType == null) { //Here null means that no path node type is cached (uninitialized or dynamic) + //Passing null as previous node type to the method signals to other lithium mixins that we only want the neighbor behavior of this block and not its neighbors + neighborType = WalkNodeEvaluator.getNodeTypeFromNeighbors(context, adjX + 1, adjY + 1, adjZ + 1, null); + //Here null means that the path node type is not changed by the block! + if (neighborType == null) { + neighborType = PathNodeType.OPEN; + } + } + if (neighborType != PathNodeType.OPEN) { + return neighborType; + } + } + } + } + + return fallback; + } + +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockListeningSection.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockListeningSection.java new file mode 100644 index 0000000..41c22a7 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockListeningSection.java @@ -0,0 +1,14 @@ +package net.gensokyoreimagined.nitori.common.block; + +import net.gensokyoreimagined.nitori.common.entity.block_tracking.SectionedBlockChangeTracker; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; + +public interface BlockListeningSection { + + void lithium$addToCallback(ListeningBlockStatePredicate blockGroup, SectionedBlockChangeTracker tracker, long sectionPos, Level world); + + void lithium$removeFromCallback(ListeningBlockStatePredicate blockGroup, SectionedBlockChangeTracker tracker); + + void lithium$invalidateListeningSection(SectionPos sectionPos); +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockStateFlags.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockStateFlags.java new file mode 100644 index 0000000..92e4ae9 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/BlockStateFlags.java @@ -0,0 +1,123 @@ +package net.gensokyoreimagined.nitori.common.block; + +import it.unimi.dsi.fastutil.objects.Reference2BooleanArrayMap; +import net.gensokyoreimagined.nitori.common.ai.pathing.BlockStatePathingCache; +import net.gensokyoreimagined.nitori.common.ai.pathing.PathNodeCache; +import net.gensokyoreimagined.nitori.common.entity.FluidCachingEntity; +import net.gensokyoreimagined.nitori.common.reflection.ReflectionUtil; +import net.minecraft.block.AbstractBlock; +import net.minecraft.block.BlockState; +import net.minecraft.entity.Entity; +import net.minecraft.entity.ai.pathing.PathNodeType; +import net.minecraft.registry.tag.FluidTags; +import net.minecraft.world.chunk.ChunkSection; + +import java.util.ArrayList; + +public class BlockStateFlags { + public static final boolean ENABLED = BlockCountingSection.class.isAssignableFrom(ChunkSection.class); + + public static final int NUM_LISTENING_FLAGS; + public static final ListeningBlockStatePredicate[] LISTENING_FLAGS; + public static final int LISTENING_MASK_OR; + + //Listening Flag + public static final ListeningBlockStatePredicate ANY; + + public static final int NUM_TRACKED_FLAGS; + public static final TrackedBlockStatePredicate[] TRACKED_FLAGS; + + //Counting flags + public static final TrackedBlockStatePredicate OVERSIZED_SHAPE; + public static final TrackedBlockStatePredicate PATH_NOT_OPEN; + public static final TrackedBlockStatePredicate WATER; + public static final TrackedBlockStatePredicate LAVA; + + public static final TrackedBlockStatePredicate[] FLAGS; + + //Non counting flags + public static final TrackedBlockStatePredicate ENTITY_TOUCHABLE; + + static { + Reference2BooleanArrayMap listeningFlags = new Reference2BooleanArrayMap<>(); + + ANY = new ListeningBlockStatePredicate(listeningFlags.size()) { + @Override + public boolean test(BlockState operand) { + return true; + } + }; + //false -> we listen to changes of all blocks that pass the predicate test. + //true -> we only listen to changes of the predicate test result + listeningFlags.put(ANY, false); + + NUM_LISTENING_FLAGS = listeningFlags.size(); + int listenMaskOR = 0; + int iteration = 0; + for (var entry : listeningFlags.reference2BooleanEntrySet()) { + boolean listenOnlyXOR = entry.getBooleanValue(); + listenMaskOR |= listenOnlyXOR ? 0 : 1 << iteration; + } + LISTENING_MASK_OR = listenMaskOR; + LISTENING_FLAGS = listeningFlags.keySet().toArray(new ListeningBlockStatePredicate[NUM_LISTENING_FLAGS]); + + + ArrayList countingFlags = new ArrayList<>(listeningFlags.keySet()); + + OVERSIZED_SHAPE = new TrackedBlockStatePredicate(countingFlags.size()) { + @Override + public boolean test(BlockState operand) { + return operand.exceedsCube(); + } + }; + countingFlags.add(OVERSIZED_SHAPE); + + if (FluidCachingEntity.class.isAssignableFrom(Entity.class)) { + WATER = new TrackedBlockStatePredicate(countingFlags.size()) { + @Override + public boolean test(BlockState operand) { + return operand.getFluidState().getFluid().isIn(FluidTags.WATER); + } + }; + countingFlags.add(WATER); + + LAVA = new TrackedBlockStatePredicate(countingFlags.size()) { + @Override + public boolean test(BlockState operand) { + return operand.getFluidState().getFluid().isIn(FluidTags.LAVA); + } + }; + countingFlags.add(LAVA); + } else { + WATER = null; + LAVA = null; + } + + if (BlockStatePathingCache.class.isAssignableFrom(AbstractBlock.AbstractBlockState.class)) { + PATH_NOT_OPEN = new TrackedBlockStatePredicate(countingFlags.size()) { + @Override + public boolean test(BlockState operand) { + return PathNodeCache.getNeighborPathNodeType(operand) != PathNodeType.OPEN; + } + }; + countingFlags.add(PATH_NOT_OPEN); + } else { + PATH_NOT_OPEN = null; + } + + NUM_TRACKED_FLAGS = countingFlags.size(); + TRACKED_FLAGS = countingFlags.toArray(new TrackedBlockStatePredicate[NUM_TRACKED_FLAGS]); + + ArrayList flags = new ArrayList<>(countingFlags); + + ENTITY_TOUCHABLE = new TrackedBlockStatePredicate(countingFlags.size()) { + @Override + public boolean test(BlockState operand) { + return ReflectionUtil.isBlockStateEntityTouchable(operand); + } + }; + flags.add(ENTITY_TOUCHABLE); + + FLAGS = flags.toArray(new TrackedBlockStatePredicate[0]); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/ListeningBlockStatePredicate.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/ListeningBlockStatePredicate.java new file mode 100644 index 0000000..5484f71 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/ListeningBlockStatePredicate.java @@ -0,0 +1,10 @@ +package net.gensokyoreimagined.nitori.common.block; + +public abstract class ListeningBlockStatePredicate extends TrackedBlockStatePredicate { + public static int LISTENING_MASK; + + protected ListeningBlockStatePredicate(int index) { + super(index); + LISTENING_MASK |= (1 << this.getIndex()); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java new file mode 100644 index 0000000..8384639 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepUntilTimeBlockEntityTickInvoker.java @@ -0,0 +1,37 @@ +package net.gensokyoreimagined.nitori.common.block.entity; + +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.jetbrains.annotations.NotNull; + +public record SleepUntilTimeBlockEntityTickInvoker(BlockEntity sleepingBlockEntity, long sleepUntilTickExclusive, + TickingBlockEntity delegate) implements TickingBlockEntity { + + @Override + public void tick() { + //noinspection ConstantConditions + long tickTime = this.sleepingBlockEntity.getLevel().getGameTime(); + if (tickTime >= this.sleepUntilTickExclusive) { + ((SleepingBlockEntity) this.sleepingBlockEntity).setTicker(this.delegate); + this.delegate.tick(); + } + } + + @Override + public boolean isRemoved() { + return this.sleepingBlockEntity.isRemoved(); + } + + @Override + public @NotNull BlockPos getPos() { + return this.sleepingBlockEntity.getBlockPos(); + } + + @Override + public String getType() { + //noinspection ConstantConditions + return BlockEntityType.getKey(this.sleepingBlockEntity.getType()).toString(); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepingBlockEntity.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepingBlockEntity.java new file mode 100644 index 0000000..ec27378 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/SleepingBlockEntity.java @@ -0,0 +1,80 @@ +package net.gensokyoreimagined.nitori.common.block.entity; + +import net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.WrappedBlockEntityTickInvokerAccessor; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.TickingBlockEntity; + +public interface SleepingBlockEntity { + TickingBlockEntity SLEEPING_BLOCK_ENTITY_TICKER = new TickingBlockEntity() { + public void tick() { + } + + public boolean isRemoved() { + return false; + } + + public BlockPos getPos() { + return null; + } + + public String getType() { + return ""; + } + }; + + WrappedBlockEntityTickInvokerAccessor lithium$getTickWrapper(); + + void lithium$setTickWrapper(WrappedBlockEntityTickInvokerAccessor tickWrapper); + + TickingBlockEntity lithium$getSleepingTicker(); + + void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker); + + default boolean nitori$startSleeping() { + if (this.isSleeping()) { + return false; + } + + WrappedBlockEntityTickInvokerAccessor tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return false; + } + this.lithium$setSleepingTicker(tickWrapper.getTicker()); + tickWrapper.callRebind(SleepingBlockEntity.SLEEPING_BLOCK_ENTITY_TICKER); + return true; + } + + default void sleepOnlyCurrentTick() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + WrappedBlockEntityTickInvokerAccessor tickWrapper = this.lithium$getTickWrapper(); + if (sleepingTicker == null) { + sleepingTicker = tickWrapper.getTicker(); + } + Level world = ((BlockEntity) this).getLevel(); + tickWrapper.callRebind(new SleepUntilTimeBlockEntityTickInvoker((BlockEntity) this, world.getGameTime() + 1, sleepingTicker)); + this.lithium$setSleepingTicker(null); + } + + default void wakeUpNow() { + TickingBlockEntity sleepingTicker = this.lithium$getSleepingTicker(); + if (sleepingTicker == null) { + return; + } + this.setTicker(sleepingTicker); + this.lithium$setSleepingTicker(null); + } + + default void setTicker(TickingBlockEntity delegate) { + WrappedBlockEntityTickInvokerAccessor tickWrapper = this.lithium$getTickWrapper(); + if (tickWrapper == null) { + return; + } + tickWrapper.callRebind(delegate); + } + + default boolean isSleeping() { + return this.lithium$getSleepingTicker() != null; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/movement_tracker/SectionedEntityMovementTracker.java b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/movement_tracker/SectionedEntityMovementTracker.java new file mode 100644 index 0000000..5a846a1 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/block/entity/movement_tracker/SectionedEntityMovementTracker.java @@ -0,0 +1,4 @@ +package net.gensokyoreimagined.nitori.common.block.entity.movement_tracker; + +public class SectionedEntityMovementTracker { +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/ChunkSectionChangeCallback.java b/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/ChunkSectionChangeCallback.java new file mode 100644 index 0000000..747622c --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/ChunkSectionChangeCallback.java @@ -0,0 +1,104 @@ +package net.gensokyoreimagined.nitori.common.entity.block_tracking; + +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import net.gensokyoreimagined.nitori.common.block.BlockListeningSection; +import net.gensokyoreimagined.nitori.common.block.BlockStateFlags; +import net.gensokyoreimagined.nitori.common.block.ListeningBlockStatePredicate; +import net.gensokyoreimagined.nitori.common.util.Pos; +import net.gensokyoreimagined.nitori.common.world.LithiumData; +import net.gensokyoreimagined.nitori.common.world.chunk.ChunkStatusTracker; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunkSection; + +import java.util.ArrayList; + +public final class ChunkSectionChangeCallback { + private final ArrayList[] trackers; + private short listeningMask; + + static { + if (BlockListeningSection.class.isAssignableFrom(LevelChunkSection.class)) { + ChunkStatusTracker.registerUnloadCallback((serverWorld, chunkPos) -> { + Long2ReferenceOpenHashMap changeCallbacks = ((LithiumData) serverWorld).lithium$getData().chunkSectionChangeCallbacks(); + int x = chunkPos.x; + int z = chunkPos.z; + for (int y = Pos.SectionYCoord.getMinYSection(serverWorld); y <= Pos.SectionYCoord.getMaxYSectionInclusive(serverWorld); y++) { + SectionPos chunkSectionPos = SectionPos.of(x, y, z); + ChunkSectionChangeCallback chunkSectionChangeCallback = changeCallbacks.remove(chunkSectionPos.asLong()); + if (chunkSectionChangeCallback != null) { + chunkSectionChangeCallback.onChunkSectionInvalidated(chunkSectionPos); + } + } + }); + } + } + + public ChunkSectionChangeCallback() { + //noinspection unchecked + this.trackers = new ArrayList[BlockStateFlags.NUM_LISTENING_FLAGS]; + this.listeningMask = 0; + } + + public static ChunkSectionChangeCallback create(long sectionPos, Level world) { + ChunkSectionChangeCallback chunkSectionChangeCallback = new ChunkSectionChangeCallback(); + Long2ReferenceOpenHashMap changeCallbacks = ((LithiumData) world).lithium$getData().chunkSectionChangeCallbacks(); + ChunkSectionChangeCallback previous = changeCallbacks.put(sectionPos, chunkSectionChangeCallback); + if (previous != null) { + previous.onChunkSectionInvalidated(SectionPos.from(sectionPos)); + } + return chunkSectionChangeCallback; + } + + public short onBlockChange(int blockGroupIndex, BlockListeningSection section) { + ArrayList sectionedBlockChangeTrackers = this.trackers[blockGroupIndex]; + this.trackers[blockGroupIndex] = null; + if (sectionedBlockChangeTrackers != null) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < sectionedBlockChangeTrackers.size(); i++) { + sectionedBlockChangeTrackers.get(i).setChanged(section); + } + } + this.listeningMask &= (short) ~(1 << blockGroupIndex); + + return this.listeningMask; + } + + public short addTracker(SectionedBlockChangeTracker tracker, ListeningBlockStatePredicate blockGroup) { + int blockGroupIndex = blockGroup.getIndex(); + ArrayList sectionedBlockChangeTrackers = this.trackers[blockGroupIndex]; + if (sectionedBlockChangeTrackers == null) { + this.trackers[blockGroupIndex] = (sectionedBlockChangeTrackers = new ArrayList<>()); + } + sectionedBlockChangeTrackers.add(tracker); + + this.listeningMask |= (short) (1 << blockGroupIndex); + return this.listeningMask; + } + + public short removeTracker(SectionedBlockChangeTracker tracker, ListeningBlockStatePredicate blockGroup) { + int blockGroupIndex = blockGroup.getIndex(); + ArrayList sectionedBlockChangeTrackers = this.trackers[blockGroupIndex]; + if (sectionedBlockChangeTrackers != null) { + sectionedBlockChangeTrackers.remove(tracker); + if (sectionedBlockChangeTrackers.isEmpty()) { + this.listeningMask &= (short) ~(1 << blockGroup.getIndex()); + } + } + return this.listeningMask; + } + + public void onChunkSectionInvalidated(SectionPos sectionPos) { + for (int flagIndex = 0; flagIndex < this.trackers.length; flagIndex++) { + ArrayList sectionedBlockChangeTrackers = this.trackers[flagIndex]; + this.trackers[flagIndex] = null; + if (sectionedBlockChangeTrackers != null) { + //noinspection ForLoopReplaceableByForEach + for (int i = 0; i < sectionedBlockChangeTrackers.size(); i++) { + sectionedBlockChangeTrackers.get(i).onChunkSectionInvalidated(sectionPos); + } + } + } + this.listeningMask = 0; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/SectionedBlockChangeTracker.java b/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/SectionedBlockChangeTracker.java new file mode 100644 index 0000000..4d8ce4c --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/entity/block_tracking/SectionedBlockChangeTracker.java @@ -0,0 +1,211 @@ +package net.gensokyoreimagined.nitori.common.entity.block_tracking; + +import net.gensokyoreimagined.nitori.common.block.BlockListeningSection; +import net.gensokyoreimagined.nitori.common.block.ListeningBlockStatePredicate; +import net.gensokyoreimagined.nitori.common.util.Pos; +import net.gensokyoreimagined.nitori.common.util.deduplication.LithiumInterner; +import net.gensokyoreimagined.nitori.common.util.tuples.WorldSectionBox; +import net.gensokyoreimagined.nitori.common.world.LithiumData; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.ChunkAccess; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.status.ChunkStatus; + +import java.util.ArrayList; +import java.util.Objects; + +public class SectionedBlockChangeTracker { + public final WorldSectionBox trackedWorldSections; + public final ListeningBlockStatePredicate blockGroup; + + private long maxChangeTime; + + private int timesRegistered; + //Some sections may not exist / be unloaded. We have to be aware of those. //TODO Invalidation when sections / chunks unload (but the entity does not (?), not sure whether this is possible) + boolean isListeningToAll = false; + private ArrayList sectionsNotListeningTo = null; + private ArrayList sectionsUnsubscribed = null; + + public SectionedBlockChangeTracker(WorldSectionBox trackedWorldSections, ListeningBlockStatePredicate blockGroup) { + this.trackedWorldSections = trackedWorldSections; + this.blockGroup = blockGroup; + + this.maxChangeTime = 0; + } + + public boolean matchesMovedBox(AABB box) { + return this.trackedWorldSections.matchesRelevantBlocksBox(box); + } + + public static SectionedBlockChangeTracker registerAt(Level world, AABB entityBoundingBox, ListeningBlockStatePredicate blockGroup) { + WorldSectionBox worldSectionBox = WorldSectionBox.relevantExpandedBlocksBox(world, entityBoundingBox); + SectionedBlockChangeTracker tracker = new SectionedBlockChangeTracker(worldSectionBox, blockGroup); + + LithiumInterner blockChangeTrackers = ((LithiumData) world).lithium$getData().blockChangeTrackers(); + tracker = blockChangeTrackers.getCanonical(tracker); + + tracker.register(); + return tracker; + } + + long getWorldTime() { + return this.trackedWorldSections.world().getGameTime(); + } + + public void register() { + if (this.timesRegistered == 0) { + WorldSectionBox trackedSections = this.trackedWorldSections; + for (int x = trackedSections.chunkX1(); x < trackedSections.chunkX2(); x++) { + for (int z = trackedSections.chunkZ1(); z < trackedSections.chunkZ2(); z++) { + Level world = trackedSections.world(); + ChunkAccess chunk = world.getChunk(x, z, ChunkStatus.FULL, false); + LevelChunkSection[] sectionArray = chunk == null ? null : chunk.getSections(); + for (int y = trackedSections.chunkY1(); y < trackedSections.chunkY2(); y++) { + if (Pos.SectionYCoord.getMinYSection(world) > y || Pos.SectionYCoord.getMaxYSectionExclusive(world) <= y) { + continue; + } + SectionPos sectionPos = SectionPos.of(x, y, z); + if (sectionArray == null) { + if (this.sectionsNotListeningTo == null) { + this.sectionsNotListeningTo = new ArrayList<>(); + } + this.sectionsNotListeningTo.add(sectionPos); + continue; + } + LevelChunkSection section = sectionArray[Pos.SectionYIndex.fromSectionCoord(world, y)]; + + BlockListeningSection blockListeningSection = (BlockListeningSection) section; + blockListeningSection.lithium$addToCallback(this.blockGroup, this, SectionPos.asLong(x, y, z), world); + } + } + } + this.isListeningToAll = (this.sectionsNotListeningTo == null || this.sectionsNotListeningTo.isEmpty()) + && (this.sectionsUnsubscribed == null || this.sectionsUnsubscribed.isEmpty()); + this.setChanged(this.getWorldTime()); + } + this.timesRegistered++; + } + + public void unregister() { + if (--this.timesRegistered > 0) { + return; + } + WorldSectionBox trackedSections = this.trackedWorldSections; + Level world = trackedSections.world(); + for (int x = trackedSections.chunkX1(); x < trackedSections.chunkX2(); x++) { + for (int z = trackedSections.chunkZ1(); z < trackedSections.chunkZ2(); z++) { + ChunkAccess chunk = world.getChunk(x, z, ChunkStatus.FULL, false); + LevelChunkSection[] sectionArray = chunk == null ? null : chunk.getSections(); + for (int y = trackedSections.chunkY1(); y < trackedSections.chunkY2(); y++) { + + if (sectionArray == null) { + continue; + } + if (Pos.SectionYCoord.getMinYSection(world) > y || Pos.SectionYCoord.getMaxYSectionExclusive(world) <= y) { + continue; + } + LevelChunkSection section = sectionArray[Pos.SectionYIndex.fromSectionCoord(world, y)]; + + BlockListeningSection blockListeningSection = (BlockListeningSection) section; + blockListeningSection.lithium$removeFromCallback(this.blockGroup, this); + } + } + } + this.sectionsNotListeningTo = null; + LithiumInterner blockChangeTrackers = ((LithiumData) world).lithium$getData().blockChangeTrackers(); + blockChangeTrackers.deleteCanonical(this); + } + + public void listenToAllSections() { + boolean changed = false; + ArrayList notListeningTo = this.sectionsNotListeningTo; + if (notListeningTo != null) { + for (int i = notListeningTo.size() - 1; i >= 0; i--) { + changed = true; + SectionPos chunkSectionPos = notListeningTo.get(i); + Level world = this.trackedWorldSections.world(); + ChunkAccess chunk = world.getChunk(chunkSectionPos.getX(), chunkSectionPos.getZ(), ChunkStatus.FULL, false); + if (chunk != null) { + notListeningTo.remove(i); + } else { + //Chunk not loaded, cannot listen to all sections. + return; + } + LevelChunkSection section = chunk.getSections()[Pos.SectionYIndex.fromSectionCoord(world, chunkSectionPos.getY())]; + BlockListeningSection blockListeningSection = (BlockListeningSection) section; + blockListeningSection.lithium$addToCallback(this.blockGroup, this, chunkSectionPos.asLong(), world); + } + } + if (this.sectionsUnsubscribed != null) { + ArrayList unsubscribed = this.sectionsUnsubscribed; + for (int i = unsubscribed.size() - 1; i >= 0; i--) { + changed = true; + BlockListeningSection blockListeningSection = unsubscribed.remove(i); + blockListeningSection.lithium$addToCallback(this.blockGroup, this, Long.MIN_VALUE, null); + } + } + this.isListeningToAll = true; + if (changed) { + this.setChanged(this.getWorldTime()); + } + } + + public void setChanged(BlockListeningSection section) { + if (this.sectionsUnsubscribed == null) { + this.sectionsUnsubscribed = new ArrayList<>(); + } + this.sectionsUnsubscribed.add(section); + this.setChanged(this.getWorldTime()); + this.isListeningToAll = false; + } + + public void setChanged(long atTime) { + if (atTime > this.maxChangeTime) { + this.maxChangeTime = atTime; + } + } + + /** + * Method to quickly check whether any relevant blocks changed inside the relevant chunk sections after + * the last test. + * + * @param lastCheckedTime time of the last interaction attempt + * @return whether any relevant entity moved in the tracked area + */ + public boolean isUnchangedSince(long lastCheckedTime) { + if (lastCheckedTime <= this.maxChangeTime) { + return false; + } + if (!this.isListeningToAll) { + this.listenToAllSections(); + return this.isListeningToAll && lastCheckedTime > this.maxChangeTime; + } + return true; + } + + //Do not modify, used for deduplication of instances + @Override + public boolean equals(Object obj) { + if (obj == this) return true; + if (obj == null || obj.getClass() != this.getClass()) return false; + var that = (SectionedBlockChangeTracker) obj; + return Objects.equals(this.trackedWorldSections, that.trackedWorldSections) && + Objects.equals(this.blockGroup, that.blockGroup); + } + //Do not modify, used for deduplication of instances + @Override + public int hashCode() { + return this.getClass().hashCode() ^ this.trackedWorldSections.hashCode() ^ this.blockGroup.hashCode(); + } + + public void onChunkSectionInvalidated(SectionPos sectionPos) { + if (this.sectionsNotListeningTo == null) { + this.sectionsNotListeningTo = new ArrayList<>(); + } + this.sectionsNotListeningTo.add(sectionPos); + this.setChanged(this.getWorldTime()); + this.isListeningToAll = false; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/util/deduplication/LithiumInterner.java b/src/main/java/net/gensokyoreimagined/nitori/common/util/deduplication/LithiumInterner.java new file mode 100644 index 0000000..ef1d871 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/util/deduplication/LithiumInterner.java @@ -0,0 +1,16 @@ +package net.gensokyoreimagined.nitori.common.util.deduplication; + +import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; + +public class LithiumInterner { + private final ObjectOpenHashSet canonicalStorage = new ObjectOpenHashSet<>(); + + public S getCanonical(S value) { + //noinspection unchecked + return (S) this.canonicalStorage.addOrGet(value); + } + + public void deleteCanonical(T value) { + this.canonicalStorage.remove(value); + } +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/util/tuples/WorldSectionBox.java b/src/main/java/net/gensokyoreimagined/nitori/common/util/tuples/WorldSectionBox.java new file mode 100644 index 0000000..59b84a5 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/util/tuples/WorldSectionBox.java @@ -0,0 +1,57 @@ +package net.gensokyoreimagined.nitori.common.util.tuples; + +import net.minecraft.world.phys.AABB; +import net.minecraft.core.SectionPos; +import net.minecraft.util.Mth; +import net.minecraft.world.level.Level; + +//Y values use coordinates, not indices (y=0 -> chunkY=0) +//upper bounds are EXCLUSIVE +public record WorldSectionBox(Level world, int chunkX1, int chunkY1, int chunkZ1, int chunkX2, int chunkY2, + int chunkZ2) { + public static WorldSectionBox entityAccessBox(Level world, AABB box) { + int minX = SectionPos.blockToSectionCoord(box.minX - 2.0D); + int minY = SectionPos.blockToSectionCoord(box.minY - 4.0D); + int minZ = SectionPos.blockToSectionCoord(box.minZ - 2.0D); + int maxX = SectionPos.blockToSectionCoord(box.maxX + 2.0D) + 1; + int maxY = SectionPos.blockToSectionCoord(box.maxY) + 1; + int maxZ = SectionPos.blockToSectionCoord(box.maxZ + 2.0D) + 1; + return new WorldSectionBox(world, minX, minY, minZ, maxX, maxY, maxZ); + } + + //Relevant block box: Entity hitbox expanded to all blocks it touches. Then expand the resulting box by 1 block in each direction. + //Include all chunk sections that contain blocks inside the expanded box. + public static WorldSectionBox relevantExpandedBlocksBox(Level world, AABB box) { + int minX = SectionPos.blockToSectionCoord(Mth.floor(box.minX) - 1); + int minY = SectionPos.blockToSectionCoord(Mth.floor(box.minY) - 1); + int minZ = SectionPos.blockToSectionCoord(Mth.floor(box.minZ) - 1); + int maxX = SectionPos.blockToSectionCoord(Mth.floor(box.maxX) + 1) + 1; + int maxY = SectionPos.blockToSectionCoord(Mth.floor(box.maxY) + 1) + 1; + int maxZ = SectionPos.blockToSectionCoord(Mth.floor(box.maxZ) + 1) + 1; + return new WorldSectionBox(world, minX, minY, minZ, maxX, maxY, maxZ); + } + //Like relevant blocks, but not expanded, because fluids never exceed the 1x1x1 volume of a block + public static WorldSectionBox relevantFluidBox(Level world, AABB box) { + int minX = SectionPos.blockToSectionCoord(Mth.floor(box.minX)); + int minY = SectionPos.blockToSectionCoord(Mth.floor(box.minY)); + int minZ = SectionPos.blockToSectionCoord(Mth.floor(box.minZ)); + int maxX = SectionPos.blockToSectionCoord(Mth.floor(box.maxX)) + 1; + int maxY = SectionPos.blockToSectionCoord(Mth.floor(box.maxY)) + 1; + int maxZ = SectionPos.blockToSectionCoord(Mth.floor(box.maxZ)) + 1; + return new WorldSectionBox(world, minX, minY, minZ, maxX, maxY, maxZ); + } + + public int numSections() { + return (this.chunkX2 - this.chunkX1) * (this.chunkY2 - this.chunkY1) * (this.chunkZ2 - this.chunkZ1); + } + + public boolean matchesRelevantBlocksBox(AABB box) { + return SectionPos.blockToSectionCoord(Mth.floor(box.minX) - 1) == this.chunkX1 && + SectionPos.blockToSectionCoord(Mth.floor(box.minY) - 1) == this.chunkY1 && + SectionPos.blockToSectionCoord(Mth.floor(box.minZ) - 1) == this.chunkZ1 && + SectionPos.blockToSectionCoord(Mth.ceil(box.maxX) + 1) + 1 == this.chunkX2 && + SectionPos.blockToSectionCoord(Mth.ceil(box.maxY) + 1) + 1 == this.chunkY2 && + SectionPos.blockToSectionCoord(Mth.ceil(box.maxZ) + 1) + 1 == this.chunkZ2; + } + +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/common/world/LithiumData.java b/src/main/java/net/gensokyoreimagined/nitori/common/world/LithiumData.java new file mode 100644 index 0000000..5181ce3 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/common/world/LithiumData.java @@ -0,0 +1,55 @@ +package net.gensokyoreimagined.nitori.common.world; + +import it.unimi.dsi.fastutil.ints.Int2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ReferenceOpenHashMap; +import it.unimi.dsi.fastutil.objects.ReferenceOpenHashSet; +import net.gensokyoreimagined.nitori.common.entity.block_tracking.ChunkSectionChangeCallback; +import net.gensokyoreimagined.nitori.common.entity.block_tracking.SectionedBlockChangeTracker; +import net.gensokyoreimagined.nitori.common.block.entity.movement_tracker.SectionedEntityMovementTracker; +import net.gensokyoreimagined.nitori.common.util.deduplication.LithiumInterner; +import net.minecraft.world.entity.ai.navigation.PathNavigation; +import net.minecraft.world.item.ItemStack; +import net.minecraft.core.registries.Registries; +import net.minecraft.world.entity.raid.Raid; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.gameevent.GameEventDispatcher; +import net.minecraft.world.level.gameevent.GameEventListenerRegistry; + +public interface LithiumData { + + record Data( + // Map of chunk position -> y section -> game event dispatcher + // This should be faster than the chunk lookup, since there are usually a lot more chunks than + // chunk with game event dispatchers (we only initialize them when non-empty set of listeners) + // All Int2ObjectMap objects are also stored in a field of the corresponding WorldChunk. + Long2ReferenceOpenHashMap> gameEventDispatchersByChunk, + + // Cached ominous banner, must not be mutated. + ItemStack ominousBanner, + + // Set of active mob navigations (active = have a path) + ReferenceOpenHashSet activeNavigations, + + // Block change tracker deduplication + LithiumInterner blockChangeTrackers, + + // Entity movement tracker deduplication + LithiumInterner> entityMovementTrackers, + + // Block ChunkSection listeners + Long2ReferenceOpenHashMap chunkSectionChangeCallbacks + ) { + public Data(Level world) { + this( + new Long2ReferenceOpenHashMap<>(), + world.registryAccess().getOptionalWrapper(Registries.BANNER_PATTERN).map(Raid::getOminousBanner).orElse(null), + new ReferenceOpenHashSet<>(), + new LithiumInterner<>(), + new LithiumInterner<>(), + new Long2ReferenceOpenHashMap<>() + ); + } + } + + LithiumData.Data lithium$getData(); +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/MixinDirection.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/MixinDirection.java deleted file mode 100644 index 6f58661..0000000 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/MixinDirection.java +++ /dev/null @@ -1,46 +0,0 @@ -// Nitori Copyright (C) 2024 Gensokyo Reimagined -// -// This program is free software: you can redistribute it and/or modify -// it under the terms of the GNU General Public License as published by -// the Free Software Foundation, either version 3 of the License, or -// (at your option) any later version. -// -// This program is distributed in the hope that it will be useful, -// but WITHOUT ANY WARRANTY; without even the implied warranty of -// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -// GNU General Public License for more details. -// -// You should have received a copy of the GNU General Public License -// along with this program. If not, see . -package net.gensokyoreimagined.nitori.mixin; - -import net.minecraft.core.Direction; -import net.minecraft.util.RandomSource; -import org.spongepowered.asm.mixin.Final; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Overwrite; -import org.spongepowered.asm.mixin.Shadow; - -@Mixin(Direction.class) -public class MixinDirection { - @Shadow @Final private static Direction[] VALUES; - @Shadow @Final private int oppositeIndex; - - /** - * @author DoggySazHi - * @reason Implementation of 0005-lithium-fast-util.patch, requires a overwrite to avoid calling `from3DDataValue` - */ - @Overwrite - public Direction getOpposite() { - return VALUES[this.oppositeIndex]; - } - - /** - * @author DoggySazHi - * @reason Implementation of 0005-lithium-fast-util.patch, requires a overwrite to avoid calling `Util.getRandom` - */ - @Overwrite - public static Direction getRandom(RandomSource random) { - return VALUES[random.nextInt(VALUES.length)]; - } -} diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/alloc/composter/ComposterMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/alloc/composter/ComposterMixin.java index ed91a02..5e334ea 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/alloc/composter/ComposterMixin.java +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/alloc/composter/ComposterMixin.java @@ -1,48 +1,46 @@ package net.gensokyoreimagined.nitori.mixin.alloc.composter; -//import net.gensokyoreimagined.nitori.common.util.ArrayConstants; -//import net.minecraft.world.WorldlyContainer; -//import net.minecraft.core.Direction; -//import org.spongepowered.asm.mixin.Mixin; -//import org.spongepowered.asm.mixin.Overwrite; -// -//public class ComposterMixin { -// -// @Mixin(targets = "net.minecraft.block.ComposterBlock$ComposterInventory") -// static abstract class ComposterBlockComposterInventoryMixin implements WorldlyContainer { -// /** -// * @author 2No2Name -// * @reason avoid allocation -// */ -// @Overwrite -// public int[] getSlotsForFace(Direction side) { -// return side == Direction.UP ? ArrayConstants.ZERO : ArrayConstants.EMPTY; -// } -// } -// -// @Mixin(targets = "net.minecraft.block.ComposterBlock$DummyInventory") -// static abstract class ComposterBlockDummyInventoryMixin implements WorldlyContainer { -// /** -// * @author 2No2Name -// * @reason avoid allocation -// */ -// @Overwrite -// public int[] getSlotsForFace(Direction side) { -// return ArrayConstants.EMPTY; -// } -// } -// -// @Mixin(targets = "net.minecraft.block.ComposterBlock$FullComposterInventory") -// static abstract class ComposterBlockFullComposterInventoryMixin implements WorldlyContainer { -// /** -// * @author 2No2Name -// * @reason avoid allocation -// */ -// @Overwrite -// public int[] getSlotsForFace(Direction side) { -// return side == Direction.DOWN ? ArrayConstants.ZERO : ArrayConstants.EMPTY; -// } -// } -//} +import net.gensokyoreimagined.nitori.common.util.ArrayConstants; +import net.minecraft.world.WorldlyContainer; +import net.minecraft.core.Direction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; -//TODO: Mixins are not getting dettected for some reason even if they are in the mixins.core??? \ No newline at end of file +public class ComposterMixin { + + @Mixin(targets = "net.minecraft.world.level.block.ComposterBlock$InputContainer") + static abstract class ComposterBlockComposterInventoryMixin implements WorldlyContainer { + /** + * @author 2No2Name + * @reason avoid allocation + */ + @Overwrite + public int[] getSlotsForFace(Direction side) { + return side == Direction.UP ? ArrayConstants.ZERO : ArrayConstants.EMPTY; + } + } + + @Mixin(targets = "net.minecraft.world.level.block.ComposterBlock$EmptyContainer") + static abstract class ComposterBlockDummyInventoryMixin implements WorldlyContainer { + /** + * @author 2No2Name + * @reason avoid allocation + */ + @Overwrite + public int[] getSlotsForFace(Direction side) { + return ArrayConstants.EMPTY; + } + } + + @Mixin(targets = "net.minecraft.world.level.block.ComposterBlock$OutputContainer") + static abstract class ComposterBlockFullComposterInventoryMixin implements WorldlyContainer { + /** + * @author 2No2Name + * @reason avoid allocation + */ + @Overwrite + public int[] getSlotsForFace(Direction side) { + return side == Direction.DOWN ? ArrayConstants.ZERO : ArrayConstants.EMPTY; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/block_entity_tickers/WorldChunkMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/block_entity_tickers/WorldChunkMixin.java new file mode 100644 index 0000000..2cfa23b --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/block_entity_tickers/WorldChunkMixin.java @@ -0,0 +1,38 @@ +package net.gensokyoreimagined.nitori.mixin.collections.block_entity_tickers; + +import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.ChunkPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.UpgradeData; +import net.minecraft.world.level.chunk.LevelChunk; +import net.minecraft.world.level.levelgen.blending.BlendingData; +import net.minecraft.world.ticks.LevelChunkTicks; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +import org.spongepowered.asm.mixin.Shadow; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Coerce; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@Mixin(LevelChunk.class) +public class WorldChunkMixin { + @Mutable + @Shadow + @Final + private Map tickersInLevel; + + @Inject( + method = "(Lnet/minecraft/world/level/Level;Lnet/minecraft/world/level/ChunkPos;Lnet/minecraft/world/level/chunk/UpgradeData;Lnet/minecraft/world/ticks/LevelChunkTicks;Lnet/minecraft/world/ticks/LevelChunkTicks;J[Lnet/minecraft/world/level/chunk/LevelChunkSection;Lnet/minecraft/world/level/chunk/LevelChunk$PostLoadProcessor;Lnet/minecraft/world/level/levelgen/blending/BlendingData;)V", + at = @At("TAIL") + ) + @Coerce + private void createFastUtilMap(Level world, ChunkPos pos, UpgradeData upgradeData, LevelChunkTicks blockTickScheduler, LevelChunkTicks fluidTickScheduler, long inhabitedTime, LevelChunkSection[] sectionArrayInitializer, LevelChunk.PostLoadProcessor entityLoader, BlendingData blendingData, CallbackInfo ci) { + this.tickersInLevel = new Object2ObjectOpenHashMap<>(); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/brain/BrainMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/brain/BrainMixin.java new file mode 100644 index 0000000..614d6ef --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/brain/BrainMixin.java @@ -0,0 +1,41 @@ +package net.gensokyoreimagined.nitori.mixin.collections.brain; + +import com.google.common.collect.ImmutableList; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceLinkedOpenHashMap; +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.world.entity.ai.Brain; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +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; + +import java.util.Collection; +import java.util.Map; +import java.util.function.Supplier; + +@Mixin(Brain.class) +public class BrainMixin { + + @Mutable + @Shadow + @Final + private Map memories; + + @Mutable + @Shadow + @Final + private Map sensors; + + @Inject( + method = "", + at = @At("RETURN") + ) + private void reinitializeBrainCollections(Collection memories, Collection sensors, ImmutableList memoryEntries, Supplier codecSupplier, CallbackInfo ci) { + this.memories = new Reference2ReferenceOpenHashMap<>(this.memories); + this.sensors = new Reference2ReferenceLinkedOpenHashMap<>(this.sensors); + } + +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_by_type/TypeFilterableListMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_by_type/TypeFilterableListMixin.java new file mode 100644 index 0000000..2b55fe6 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_by_type/TypeFilterableListMixin.java @@ -0,0 +1,28 @@ +package net.gensokyoreimagined.nitori.mixin.collections.entity_by_type; + +import it.unimi.dsi.fastutil.objects.Reference2ReferenceOpenHashMap; +import net.minecraft.util.ClassInstanceMultiMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Mutable; +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; + +import java.util.List; +import java.util.Map; + +@Mixin(ClassInstanceMultiMap.class) +public class TypeFilterableListMixin { + + @Mutable + @Shadow + @Final + private Map, List> byClass; + + @Inject(method = "", at = @At("RETURN")) + private void init(Class elementType, CallbackInfo ci) { + this.byClass = new Reference2ReferenceOpenHashMap<>(this.byClass); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_filtering/TypeFilterableListMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_filtering/TypeFilterableListMixin.java new file mode 100644 index 0000000..c92d560 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/collections/entity_filtering/TypeFilterableListMixin.java @@ -0,0 +1,55 @@ +package net.gensokyoreimagined.nitori.mixin.collections.entity_filtering; + +import net.minecraft.util.ClassInstanceMultiMap; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +import java.util.*; + +/** + * Patches {@link ClassInstanceMultiMap} to improve performance when entities are being queried in the world. + */ +@Mixin(ClassInstanceMultiMap.class) +public class TypeFilterableListMixin { + + @Shadow + @Final + private Map, List> byClass; + + @Shadow + @Final + private List allInstances; + + /** + * @reason Only perform the slow Class#isAssignableFrom(Class) if a list doesn't exist for the type, otherwise + * we can assume it's already valid. The slow-path code is moved to a separate method to help the JVM inline this. + * @author JellySquid + */ + @SuppressWarnings("unchecked") + @Overwrite + public Collection find(Class type) { + Collection collection = this.byClass.get(type); + + if (collection == null) { + collection = this.createAllOfType(type); + } + + return (Collection) Collections.unmodifiableCollection(collection); + } + + private Collection createAllOfType(Class type) { + List list = new ArrayList<>(); + + for (T allElement : this.allInstances) { + if (type.isInstance(allElement)) { + list.add(allElement); + } + } + + this.byClass.put(type, list); + + return list; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/entity/fast_retrieval/SectionedEntityCacheMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/entity/fast_retrieval/SectionedEntityCacheMixin.java new file mode 100644 index 0000000..04a17db --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/entity/fast_retrieval/SectionedEntityCacheMixin.java @@ -0,0 +1,96 @@ +package net.gensokyoreimagined.nitori.mixin.entity.fast_retrieval; + +import net.minecraft.util.AbortableIterationConsumer; +import net.minecraft.world.phys.AABB; +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.entity.EntityAccess; +import net.minecraft.world.level.entity.EntitySection; +import net.minecraft.world.level.entity.EntitySectionStorage; +import org.jetbrains.annotations.Nullable; +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; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(EntitySectionStorage.class) +public abstract class SectionedEntityCacheMixin { + @Shadow + @Nullable + public abstract EntitySection getSection(long sectionPos); + + /** + * @author 2No2Name + * @reason avoid iterating through LongAVLTreeSet, possibly iterating over hundreds of irrelevant longs to save up to 8 hash set gets + */ + @Inject( + method = "forEachAccessibleNonEmptySection", + at = @At( + value = "INVOKE_ASSIGN", + shift = At.Shift.AFTER, + target = "Lnet/minecraft/core/SectionPos;posToSectionCoord(D)I", + ordinal = 5 + ), + locals = LocalCapture.CAPTURE_FAILHARD, + cancellable = true + ) + public void forEachInBox(AABB box, AbortableIterationConsumer> action, CallbackInfo ci, int i, int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { + if (maxX >= minX + 4 || maxZ >= minZ + 4) { + return; // Vanilla is likely more optimized when shooting entities with TNT cannons over huge distances. + // Choosing a cutoff of 4 chunk size, as it becomes more likely that these entity sections do not exist when + // they are far away from the shot entity (player despawn range, position maybe not on the ground, etc) + } + ci.cancel(); + + // Vanilla order of the AVL long set is sorting by ascending long value. The x, y, z positions are packed into + // a long with the x position's lowest 22 bits placed at the MSB. + // Therefore the long is negative iff the 22th bit of the x position is set, which happens iff the x position + // is negative. A positive x position will never have its 22th bit set, as these big coordinates are far outside + // the world. y and z positions are treated as unsigned when sorting by ascending long value, as their sign bits + // are placed somewhere inside the packed long + + for (int x = minX; x <= maxX; x++) { + for (int z = Math.max(minZ, 0); z <= maxZ; z++) { + if (this.forEachInColumn(x, minY, maxY, z, action).shouldAbort()) { + return; + } + } + + int bound = Math.min(-1, maxZ); + for (int z = minZ; z <= bound; z++) { + if (this.forEachInColumn(x, minY, maxY, z, action).shouldAbort()) { + return; + } + } + } + } + + private AbortableIterationConsumer.Continuation forEachInColumn(int x, int minY, int maxY, int z, AbortableIterationConsumer> action) { + AbortableIterationConsumer.Continuation ret = AbortableIterationConsumer.Continuation.CONTINUE; + //y from negative to positive, but y is treated as unsigned + for (int y = Math.max(minY, 0); y <= maxY; y++) { + if ((ret = this.consumeSection(SectionPos.asLong(x, y, z), action)).shouldAbort()) { + return ret; + } + } + int bound = Math.min(-1, maxY); + for (int y = minY; y <= bound; y++) { + if ((ret = this.consumeSection(SectionPos.asLong(x, y, z), action)).shouldAbort()) { + return ret; + } + } + return ret; + } + + private AbortableIterationConsumer.Continuation consumeSection(long pos, AbortableIterationConsumer> action) { + EntitySection section = this.getSection(pos); + //noinspection SizeReplaceableByIsEmpty + if (section != null && + 0 != section.size() /* util.entity_movement_tracking mixins modify isEmpty to include listener objects */ + && section.getStatus().isAccessible()) { + return action.accept(section); + } + return AbortableIterationConsumer.Continuation.CONTINUE; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/math/fast_util/DirectionMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/math/fast_util/DirectionMixin.java index d1d9486..943d543 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/math/fast_util/DirectionMixin.java +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/math/fast_util/DirectionMixin.java @@ -1,38 +1,37 @@ package net.gensokyoreimagined.nitori.mixin.math.fast_util; -//import net.minecraft.core.Direction; -//import net.minecraft.core.Direction.Axis; -//import net.minecraft.util.RandomSource; -//import org.spongepowered.asm.mixin.Final; -//import org.spongepowered.asm.mixin.Mixin; -//import org.spongepowered.asm.mixin.Overwrite; -//import org.spongepowered.asm.mixin.Shadow; -// -//@Mixin(Direction.class) -//public class DirectionMixin { -// @Shadow -// @Final -// private static Direction[] ALL; -// -// @Shadow -// @Final -// private int idOpposite; -// -// /** -// * @reason Avoid the modulo/abs operations -// * @author JellySquid -// */ -// @Overwrite -// public Direction getOpposite() { -// return ALL[this.idOpposite]; -// } -// -// /** -// * @reason Do not allocate an excessive number of Direction arrays -// * @author JellySquid -// */ -// @Overwrite -// public static Direction getRandom(RandomSource rand) { -// return Direction.ALL[rand.nextInt(ALL.length)]; -// } -//} \ No newline at end of file +import net.minecraft.core.Direction; +import net.minecraft.util.RandomSource; +import org.spongepowered.asm.mixin.Final; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Overwrite; +import org.spongepowered.asm.mixin.Shadow; + +@Mixin(Direction.class) +public class DirectionMixin { + @Shadow + @Final + private static Direction[] VALUES; + + @Shadow + @Final + private int oppositeIndex; + + /** + * @reason Avoid the modulo/abs operations + * @author JellySquid + */ + @Overwrite + public Direction getOpposite() { + return VALUES[this.oppositeIndex]; + } + + /** + * @reason Do not allocate an excessive number of Direction arrays + * @author JellySquid + */ + @Overwrite + public static Direction getRandom(RandomSource rand) { + return VALUES[rand.nextInt(VALUES.length)]; + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/MixinMth.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/math/sine_lut/MixinMth.java similarity index 98% rename from src/main/java/net/gensokyoreimagined/nitori/mixin/MixinMth.java rename to src/main/java/net/gensokyoreimagined/nitori/mixin/math/sine_lut/MixinMth.java index 78820bd..1bd55cd 100644 --- a/src/main/java/net/gensokyoreimagined/nitori/mixin/MixinMth.java +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/math/sine_lut/MixinMth.java @@ -12,7 +12,7 @@ // // You should have received a copy of the GNU General Public License // along with this program. If not, see . -package net.gensokyoreimagined.nitori.mixin; +package net.gensokyoreimagined.nitori.mixin.math.sine_lut; import net.minecraft.util.Mth; import org.spongepowered.asm.mixin.Mixin; diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/shapes/lazy_shape_context/EntityShapeContextMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/shapes/lazy_shape_context/EntityShapeContextMixin.java new file mode 100644 index 0000000..8312240 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/shapes/lazy_shape_context/EntityShapeContextMixin.java @@ -0,0 +1,110 @@ +package net.gensokyoreimagined.nitori.mixin.shapes.lazy_shape_context; + +import net.minecraft.world.phys.shapes.EntityCollisionContext; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.level.material.FluidState; +import net.minecraft.world.item.Item; +import net.minecraft.world.item.ItemStack; +import org.jetbrains.annotations.Nullable; +import org.spongepowered.asm.mixin.*; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Constant; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.ModifyConstant; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +import java.util.function.Predicate; + +@Mixin(EntityCollisionContext.class) +public class EntityShapeContextMixin { + @Mutable + @Shadow + @Final + private ItemStack heldItem; + + @Mutable + @Shadow + @Final + private Predicate canStandOnFluid; + + @Shadow + @Final + @Nullable + private Entity entity; + + /** + * Mixin the instanceof to always return false to avoid the expensive inventory access. + * No need to use Opcodes.INSTANCEOF or similar. + */ + @SuppressWarnings("InvalidInjectorMethodSignature") + @ModifyConstant( + method = "(Lnet/minecraft/world/entity/Entity;)V", + constant = @Constant(classValue = LivingEntity.class, ordinal = 0) + ) + private static boolean redirectInstanceOf(Object ignored, Class constant) { + return false; + } + + @SuppressWarnings("InvalidInjectorMethodSignature") + @ModifyConstant( + method = "(Lnet/minecraft/world/entity/Entity;)V", + constant = @Constant(classValue = LivingEntity.class, ordinal = 2) + ) + private static boolean redirectInstanceOf2(Object ignored, Class constant) { + return false; + } + + @Inject( + method = "(Lnet/minecraft/world/entity/Entity;)V", + at = @At("TAIL") + ) + private void initFields(Entity entity, CallbackInfo ci) { + this.heldItem = null; + this.canStandOnFluid = null; + } + + @Inject( + method = "isHoldingItem", + at = @At("HEAD") + ) + public void isHolding(Item item, CallbackInfoReturnable cir) { + this.nitori$initHeldItem(); + } + + @Intrinsic + public ItemStack getHeldItem() { + return this.heldItem; + } + + @SuppressWarnings({"UnresolvedMixinReference", "MixinAnnotationTarget"}) + @Inject( + method = "getHeldItem", + at = @At("HEAD") + ) + private void nitori$initHeldItem(CallbackInfoReturnable callbackInfoReturnable) { + this.nitori$initHeldItem(); + } + + @Unique + private void nitori$initHeldItem() { + if (this.heldItem == null) { + this.heldItem = this.entity instanceof LivingEntity ? ((LivingEntity) this.entity).getMainHandItem() : ItemStack.EMPTY; + } + } + + @Inject( + method = "canStandOnFluid", + at = @At("HEAD") + ) + public void canWalkOnFluid(FluidState state, FluidState fluidState, CallbackInfoReturnable cir) { + if (this.canStandOnFluid == null) { + if (this.entity instanceof LivingEntity livingEntity) { + this.canStandOnFluid = livingEntity::canStandOnFluid; + } else { + this.canStandOnFluid = (liquid) -> false; + } + } + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/util/block_tracking/ChunkSectionMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/util/block_tracking/ChunkSectionMixin.java new file mode 100644 index 0000000..2b8f3d9 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/util/block_tracking/ChunkSectionMixin.java @@ -0,0 +1,173 @@ +package net.gensokyoreimagined.nitori.mixin.util.block_tracking; + +import me.jellysquid.mods.lithium.common.block.*; +import net.gensokyoreimagined.nitori.common.entity.block_tracking.ChunkSectionChangeCallback; +import net.gensokyoreimagined.nitori.common.entity.block_tracking.SectionedBlockChangeTracker; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.core.SectionPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.chunk.LevelChunkSection; +import net.minecraft.world.level.chunk.PalettedContainer; +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.Redirect; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +/** + * Keep track of how many blocks that meet certain criteria are in this chunk section. + * E.g. if no over-sized blocks are there, collision code can skip a few blocks. + * + * @author 2No2Name + */ +@Mixin(LevelChunkSection.class) +public abstract class ChunkSectionMixin implements BlockCountingSection, BlockListeningSection { + + @Shadow + @Final + private PalettedContainer blockStateContainer; + + @Unique + private short[] countsByFlag = null; + @Unique + private ChunkSectionChangeCallback changeListener; + @Unique + private short listeningMask; + + @Unique + private static void addToFlagCount(short[] countsByFlag, BlockState state, short change) { + int flags = ((BlockStateFlagHolder) state).lithium$getAllFlags(); + int i; + while ((i = Integer.numberOfTrailingZeros(flags)) < 32 && i < countsByFlag.length) { + //either count up by one (prevFlag not set) or down by one (prevFlag set) + countsByFlag[i] += change; + flags &= ~(1 << i); + } + } + + @Override + public boolean lithium$mayContainAny(TrackedBlockStatePredicate trackedBlockStatePredicate) { + if (this.countsByFlag == null) { + fastInitClientCounts(); + } + return this.countsByFlag[trackedBlockStatePredicate.getIndex()] != (short) 0; + } + + @Unique + private void fastInitClientCounts() { + this.countsByFlag = new short[BlockStateFlags.NUM_TRACKED_FLAGS]; + for (TrackedBlockStatePredicate trackedBlockStatePredicate : BlockStateFlags.TRACKED_FLAGS) { + if (this.blockStateContainer.hasAny(trackedBlockStatePredicate)) { + //We haven't counted, so we just set the count so high that it never incorrectly reaches 0. + //For most situations, this overestimation does not hurt client performance compared to correct counting, + this.countsByFlag[trackedBlockStatePredicate.getIndex()] = 16 * 16 * 16; + } + } + } + + @Redirect( + method = "calculateCounts()V", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/world/chunk/PalettedContainer;count(Lnet/minecraft/world/chunk/PalettedContainer$Counter;)V" + ) + ) + private void initFlagCounters(PalettedContainer palettedContainer, PalettedContainer.Counter consumer) { + palettedContainer.count((state, count) -> { + consumer.accept(state, count); + addToFlagCount(this.countsByFlag, state, (short) count); + }); + } + + @Inject(method = "calculateCounts()V", at = @At("HEAD")) + private void createFlagCounters(CallbackInfo ci) { + this.countsByFlag = new short[BlockStateFlags.NUM_TRACKED_FLAGS]; + } + + @Inject( + method = "readDataPacket", + at = @At(value = "HEAD") + ) + private void resetData(FriendlyByteBuf buf, CallbackInfo ci) { + this.countsByFlag = null; + } + + @Inject( + method = "setBlockState(IIILnet/minecraft/block/BlockState;Z)Lnet/minecraft/block/BlockState;", + at = @At( + value = "INVOKE", + target = "Lnet/minecraft/block/BlockState;getFluidState()Lnet/minecraft/fluid/FluidState;", + ordinal = 0, + shift = At.Shift.BEFORE + ), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private void updateFlagCounters(int x, int y, int z, BlockState newState, boolean lock, CallbackInfoReturnable cir, BlockState oldState) { + this.lithium$trackBlockStateChange(newState, oldState); + } + + @Override + public void lithium$trackBlockStateChange(BlockState newState, BlockState oldState) { + short[] countsByFlag = this.countsByFlag; + if (countsByFlag == null) { + return; + } + int prevFlags = ((BlockStateFlagHolder) oldState).lithium$getAllFlags(); + int flags = ((BlockStateFlagHolder) newState).lithium$getAllFlags(); + + int flagsXOR = prevFlags ^ flags; + //we need to iterate over indices that changed or are in the listeningMask + //Some Listening Flags are sensitive to both the previous and the new block. Others are only sensitive to + //blocks that are different according to the predicate (XOR). For XOR, the block counting needs to be updated + //as well. + int iterateFlags = (~BlockStateFlags.LISTENING_MASK_OR & flagsXOR) | + (BlockStateFlags.LISTENING_MASK_OR & this.listeningMask & (prevFlags | flags)); + int flagIndex; + + while ((flagIndex = Integer.numberOfTrailingZeros(iterateFlags)) < 32 && flagIndex < countsByFlag.length) { + int flagBit = 1 << flagIndex; + //either count up by one (prevFlag not set) or down by one (prevFlag set) + if ((flagsXOR & flagBit) != 0) { + countsByFlag[flagIndex] += (short) (1 - (((prevFlags >>> flagIndex) & 1) << 1)); + } + if ((this.listeningMask & flagBit) != 0) { + this.listeningMask = this.changeListener.onBlockChange(flagIndex, this); + } + iterateFlags &= ~flagBit; + } + } + + @Override + public void lithium$addToCallback(ListeningBlockStatePredicate blockGroup, SectionedBlockChangeTracker tracker, long sectionPos, Level world) { + if (this.changeListener == null) { + if (sectionPos == Long.MIN_VALUE || world == null) { + throw new IllegalArgumentException("Expected world and section pos during intialization!"); + } + this.changeListener = ChunkSectionChangeCallback.create(sectionPos, world); + } + + this.listeningMask = this.changeListener.addTracker(tracker, blockGroup); + } + + @Override + public void lithium$removeFromCallback(ListeningBlockStatePredicate blockGroup, SectionedBlockChangeTracker tracker) { + if (this.changeListener != null) { + this.listeningMask = this.changeListener.removeTracker(tracker, blockGroup); + } + } + + @Override + @Unique + public void lithium$invalidateListeningSection(ChunkSectionPos sectionPos) { + if (this.listeningMask != 0) { + this.changeListener.onChunkSectionInvalidated(sectionPos); + this.listeningMask = 0; + } + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/WrappedBlockEntityTickInvokerAccessor.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/WrappedBlockEntityTickInvokerAccessor.java new file mode 100644 index 0000000..bb77675 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/WrappedBlockEntityTickInvokerAccessor.java @@ -0,0 +1,15 @@ +package net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping; + +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.gen.Invoker; + +@Mixin(targets = "net/minecraft/world/level/chunk/LevelChunk$RebindableTickingBlockEntityWrapper") +public interface WrappedBlockEntityTickInvokerAccessor { + @Invoker + void callRebind(TickingBlockEntity wrapped); + + @Accessor + TickingBlockEntity getTicker(); +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/CampfireBlockEntityMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/CampfireBlockEntityMixin.java new file mode 100644 index 0000000..2fdfe32 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/CampfireBlockEntityMixin.java @@ -0,0 +1,69 @@ +package net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.campfire; + +import net.gensokyoreimagined.nitori.common.block.entity.SleepingBlockEntity; +import net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.WrappedBlockEntityTickInvokerAccessor; +import net.minecraft.world.entity.Entity; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import net.minecraft.world.entity.LivingEntity; +import net.minecraft.world.item.ItemStack; +import net.minecraft.nbt.CompoundTag; +import net.minecraft.core.HolderLookup; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +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.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; + +@Mixin(CampfireBlockEntity.class) +public class CampfireBlockEntityMixin extends BlockEntity implements SleepingBlockEntity { + + private WrappedBlockEntityTickInvokerAccessor tickWrapper = null; + private TickingBlockEntity sleepingTicker = null; + + public CampfireBlockEntityMixin(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + @Override + public WrappedBlockEntityTickInvokerAccessor lithium$getTickWrapper() { + return tickWrapper; + } + + @Override + public void lithium$setTickWrapper(WrappedBlockEntityTickInvokerAccessor tickWrapper) { + this.tickWrapper = tickWrapper; + this.lithium$setSleepingTicker(null); + } + + @Override + public TickingBlockEntity lithium$getSleepingTicker() { + return sleepingTicker; + } + + @Override + public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { + this.sleepingTicker = sleepingTicker; + } + + + @Inject( + method = "placeFood", + at = @At(value = "INVOKE", target = "Lnet/minecraft/core/NonNullList;set(ILjava/lang/Object;)Ljava/lang/Object;") + ) + private void wakeUpOnAddItem(Entity user, ItemStack stack, int cookTime, CallbackInfoReturnable cir) { + this.wakeUpNow(); + } + + @Inject( + method = "loadAdditional", + at = @At(value = "RETURN") + ) + private void wakeUpOnReadNbt(CompoundTag nbt, HolderLookup.Provider registryLookup, CallbackInfo ci) { + this.wakeUpNow(); + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/lit/CampfireBlockEntityMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/lit/CampfireBlockEntityMixin.java new file mode 100644 index 0000000..53c3099 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/lit/CampfireBlockEntityMixin.java @@ -0,0 +1,40 @@ +package net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.campfire.lit; + +import net.gensokyoreimagined.nitori.common.block.entity.SleepingBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.SimpleContainer; +import net.minecraft.world.item.ItemStack; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +import org.bukkit.craftbukkit.inventory.CraftItemStack; +import org.bukkit.event.block.BlockCookEvent; +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.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +import java.util.Optional; + +@Mixin(CampfireBlockEntity.class) +public abstract class CampfireBlockEntityMixin extends BlockEntity implements SleepingBlockEntity { + + public CampfireBlockEntityMixin(BlockPos pos, BlockState state) { + super(BlockEntityType.CAMPFIRE, pos, state); + } + + @Inject( + method = "cookTick", + at = @At("RETURN"), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void trySleepLit(Level world, BlockPos pos, BlockState state, CampfireBlockEntity campfire, CallbackInfo ci, boolean flag) { + if (!flag) { + CampfireBlockEntityMixin self = (CampfireBlockEntityMixin) (Object) campfire; + self.nitori$startSleeping(); + } + } +} diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/unlit/CampfireBlockEntityMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/unlit/CampfireBlockEntityMixin.java new file mode 100644 index 0000000..c2cda7b --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/campfire/unlit/CampfireBlockEntityMixin.java @@ -0,0 +1,34 @@ +package net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.campfire.unlit; + +import net.gensokyoreimagined.nitori.common.block.entity.SleepingBlockEntity; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.entity.CampfireBlockEntity; +import net.minecraft.world.level.block.state.BlockState; +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.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.LocalCapture; + +@Mixin(CampfireBlockEntity.class) +public abstract class CampfireBlockEntityMixin extends BlockEntity implements SleepingBlockEntity { + + public CampfireBlockEntityMixin(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + @Inject( + method = "cooldownTick", + at = @At("RETURN"), + locals = LocalCapture.CAPTURE_FAILHARD + ) + private static void trySleepLit(Level world, BlockPos pos, BlockState state, CampfireBlockEntity campfire, CallbackInfo ci, boolean flag) { + if (!flag) { + CampfireBlockEntityMixin self = (CampfireBlockEntityMixin) (Object) campfire; + self.nitori$startSleeping(); + } + } +} \ No newline at end of file diff --git a/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/furnace/AbstractFurnaceBlockEntityMixin.java b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/furnace/AbstractFurnaceBlockEntityMixin.java new file mode 100644 index 0000000..3394c69 --- /dev/null +++ b/src/main/java/net/gensokyoreimagined/nitori/mixin/world/block_entity_ticking/sleeping/furnace/AbstractFurnaceBlockEntityMixin.java @@ -0,0 +1,88 @@ +package net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.furnace; + +import net.gensokyoreimagined.nitori.common.block.entity.SleepingBlockEntity; +import net.gensokyoreimagined.nitori.mixin.world.block_entity_ticking.sleeping.WrappedBlockEntityTickInvokerAccessor; +import net.minecraft.core.BlockPos; +import net.minecraft.world.level.Level; +import net.minecraft.world.level.block.Blocks; +import net.minecraft.world.level.block.entity.AbstractFurnaceBlockEntity; +import net.minecraft.world.level.block.entity.BlockEntity; +import net.minecraft.world.level.block.entity.BlockEntityType; +import net.minecraft.world.level.block.state.BlockState; +import net.minecraft.world.level.block.entity.TickingBlockEntity; +import org.spongepowered.asm.mixin.Intrinsic; +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(AbstractFurnaceBlockEntity.class) +public abstract class AbstractFurnaceBlockEntityMixin extends BlockEntity implements SleepingBlockEntity { + + @Shadow + protected abstract boolean isLit(); + + @Shadow + public int cookingProgress; + private WrappedBlockEntityTickInvokerAccessor tickWrapper = null; + private TickingBlockEntity sleepingTicker = null; + + public AbstractFurnaceBlockEntityMixin(BlockEntityType type, BlockPos pos, BlockState state) { + super(type, pos, state); + } + + @Override + public WrappedBlockEntityTickInvokerAccessor lithium$getTickWrapper() { + return tickWrapper; + } + + @Override + public void lithium$setTickWrapper(WrappedBlockEntityTickInvokerAccessor tickWrapper) { + this.tickWrapper = tickWrapper; + this.lithium$setSleepingTicker(null); + } + + @Override + public TickingBlockEntity lithium$getSleepingTicker() { + return sleepingTicker; + } + + @Override + public void lithium$setSleepingTicker(TickingBlockEntity sleepingTicker) { + this.sleepingTicker = sleepingTicker; + } + + @Inject(method = "serverTick", at = @At("RETURN")) + private static void checkSleep(Level world, BlockPos pos, BlockState state, AbstractFurnaceBlockEntity blockEntity, CallbackInfo ci) { + ((AbstractFurnaceBlockEntityMixin) (Object) blockEntity).checkSleep(state); + } + + private void checkSleep(BlockState state) { + if (!this.isLit() && this.cookingProgress == 0 && (state.is(Blocks.FURNACE) || state.is(Blocks.BLAST_FURNACE) || state.is(Blocks.SMOKER)) && this.level != null) { + this.nitori$startSleeping(); + } + } + + @Inject(method = "loadAdditional", at = @At("RETURN" )) + private void wakeUpAfterFromTag(CallbackInfo ci) { + if (this.isSleeping() && this.level != null && !this.level.isClientSide) { + this.wakeUpNow(); + } + } + + @Override + @Intrinsic + public void setChanged() { + super.setChanged(); + } + + @SuppressWarnings({"MixinAnnotationTarget", "UnresolvedMixinReference"}) + @Inject(method = "setChanged()V", at = @At("RETURN")) + private void wakeOnMarkDirty(CallbackInfo ci) { + if (this.isSleeping() && this.level != null && !this.level.isClientSide) { + this.wakeUpNow(); + } + } + +} \ No newline at end of file diff --git a/src/main/resources/ignite.mod.json b/src/main/resources/ignite.mod.json index e5e9d5c..f3c5000 100755 --- a/src/main/resources/ignite.mod.json +++ b/src/main/resources/ignite.mod.json @@ -1,6 +1,6 @@ { "id": "Nitori", - "version": "1.2-SNAPSHOT", + "version": "1.3-SNAPSHOT", "mixins": [ "mixins.core.json" ] diff --git a/src/main/resources/mixins.core.json b/src/main/resources/mixins.core.json index 00168bb..01cf8b2 100755 --- a/src/main/resources/mixins.core.json +++ b/src/main/resources/mixins.core.json @@ -13,7 +13,6 @@ "MixinBlockPos", "MixinChunkEntitySlices", "MixinCraftPlayer", - "MixinDirection", "MixinEntity", "MixinEntitySectionStorage", "MixinGameRules", @@ -21,7 +20,7 @@ "MixinLevel", "MixinLevelStorageAccess", "MixinMob", - "MixinMth", + "math.sine_lut.MixinMth", "MixinNoiseBasedChunkGenerator", "MixinPlayer", "MixinPlayerList", @@ -32,11 +31,26 @@ "MixinWorldGenRegion", "alloc.chunk_ticking.ServerChunkManagerMixin", "alloc.blockstate.StateMixin", + "alloc.composter.ComposterMixin$ComposterBlockFullComposterInventoryMixin", + "alloc.composter.ComposterMixin$ComposterBlockDummyInventoryMixin", + "alloc.composter.ComposterMixin$ComposterBlockComposterInventoryMixin", "util.MixinLevelBlockEntityRetrieval", "cached_hashcode.BlockNeighborGroupMixin", "shapes.blockstate_cache.BlockMixin", + "shapes.lazy_shape_context.EntityShapeContextMixin", + "entity.fast_retrieval.SectionedEntityCacheMixin", "math.fast_blockops.DirectionMixin", "math.fast_blockops.BlockPosMixin", - "ai.sensor.secondary_poi.SecondaryPointsOfInterestSensorMixin", + "math.fast_util.AxisCycleDirectionMixin$ForwardMixin", + "math.fast_util.AxisCycleDirectionMixin$BackwardMixin", + "math.fast_util.DirectionMixin", + "collections.entity_filtering.TypeFilterableListMixin", + "collections.entity_by_type.TypeFilterableListMixin", + "collections.block_entity_tickers.WorldChunkMixin", + "world.block_entity_ticking.sleeping.WrappedBlockEntityTickInvokerAccessor", + "world.block_entity_ticking.sleeping.campfire.CampfireBlockEntityMixin", + "world.block_entity_ticking.sleeping.campfire.unlit.CampfireBlockEntityMixin", + "world.block_entity_ticking.sleeping.campfire.lit.CampfireBlockEntityMixin", + "world.block_entity_ticking.sleeping.furnace.AbstractFurnaceBlockEntityMixin" ] }