diff --git a/src/main/java/com/fusionflux/portalcubed/accessor/LevelExt.java b/src/main/java/com/fusionflux/portalcubed/accessor/LevelExt.java index d67841aa..21f90ad5 100644 --- a/src/main/java/com/fusionflux/portalcubed/accessor/LevelExt.java +++ b/src/main/java/com/fusionflux/portalcubed/accessor/LevelExt.java @@ -1,5 +1,6 @@ package com.fusionflux.portalcubed.accessor; +import com.fusionflux.portalcubed.entity.beams.EmittedEntity; import com.fusionflux.portalcubed.mechanics.PortalCubedDamageSources; import net.minecraft.world.entity.Entity; import org.jetbrains.annotations.Nullable; @@ -11,4 +12,8 @@ public interface LevelExt { @Nullable Entity getEntityByUuid(UUID uuid); + + void pc$addBlockChangeListener(long sectionPos, EmittedEntity entity); + + void pc$removeBlockChangeListener(long sectionPos, EmittedEntity entity); } diff --git a/src/main/java/com/fusionflux/portalcubed/entity/beams/EmittedEntity.java b/src/main/java/com/fusionflux/portalcubed/entity/beams/EmittedEntity.java index 2cb380c8..8dfa957f 100644 --- a/src/main/java/com/fusionflux/portalcubed/entity/beams/EmittedEntity.java +++ b/src/main/java/com/fusionflux/portalcubed/entity/beams/EmittedEntity.java @@ -1,5 +1,6 @@ package com.fusionflux.portalcubed.entity.beams; +import com.fusionflux.portalcubed.accessor.LevelExt; import com.fusionflux.portalcubed.entity.Portal; import com.fusionflux.portalcubed.entity.PortalCubedEntities; import com.fusionflux.portalcubed.entity.PortalListeningEntity; @@ -7,6 +8,7 @@ import net.minecraft.core.BlockPos; import net.minecraft.core.Direction; import net.minecraft.core.Direction.Axis; +import net.minecraft.core.SectionPos; import net.minecraft.nbt.CompoundTag; import net.minecraft.nbt.Tag; import net.minecraft.network.FriendlyByteBuf; @@ -17,7 +19,6 @@ import net.minecraft.world.entity.Entity; import net.minecraft.world.entity.EntityType; import net.minecraft.world.level.Level; -import net.minecraft.world.level.entity.EntityTypeTest; import net.minecraft.world.phys.AABB; import net.minecraft.world.phys.Vec3; import org.jetbrains.annotations.NotNull; @@ -33,7 +34,6 @@ public abstract class EmittedEntity extends PortalListeningEntity implements QuiltExtendedSpawnDataEntity { public static final int DEFAULT_MAX_LENGTH = 100; - public static final EntityTypeTest TYPE_TEST = EntityTypeTest.forClass(EmittedEntity.class); public static final EntityDataAccessor FACING = SynchedEntityData.defineId(EmittedEntity.class, EntityDataSerializers.DIRECTION); public static final EntityDataAccessor LENGTH = SynchedEntityData.defineId(EmittedEntity.class, EntityDataSerializers.FLOAT); @@ -43,6 +43,7 @@ public abstract class EmittedEntity extends PortalListeningEntity implements Qui private Vec3 center = Vec3.ZERO; private AABB listeningArea = new AABB(BlockPos.ZERO); private int reEmitTimer; + private AABB cachedBounds; // UUID of next in line. resolving this will always work, since the first entity // is the only one that can be unloaded (others are chunk loaded by portals) @@ -89,14 +90,27 @@ public boolean listensTo(BlockPos pos) { @Override @NotNull protected AABB makeBoundingBox() { + // updating bounds is expensive, minecraft calls this method constantly because of setPos + if (cachedBounds == null) + cachedBounds = actuallyMakeBoundingBox(); + return cachedBounds; + } + + protected AABB actuallyMakeBoundingBox() { AABB base = makeBaseBoundingBox(); Vec3 pos = position(); Vec3 offset = pos.relative(getFacing(), getLength()).subtract(pos); // relative offset along facing by length this.center = base.getCenter(); AABB bounds = base.expandTowards(offset); + Direction facing = getFacing(); Vec3 facingNormal = Vec3.ZERO.with(facing.getAxis(), facing.getAxisDirection().getStep()); + + if (listeningArea != null) + stopListeningToArea(listeningArea); this.listeningArea = bounds.expandTowards(facingNormal); + startListeningToArea(listeningArea); + return bounds; } @@ -162,6 +176,7 @@ public void tick() { @Override public void remove(RemovalReason reason) { super.remove(reason); + stopListeningToArea(listeningArea); if (reason.shouldDestroy()) removeNextEntity(); } @@ -227,6 +242,7 @@ protected void readAdditionalSaveData(CompoundTag tag) { } private void updateBounds() { + this.cachedBounds = actuallyMakeBoundingBox(); setBoundingBox(makeBoundingBox()); modelUpdater.accept(this); } @@ -254,4 +270,24 @@ public void onPortalRemove(Portal portal) { public void onLinkedPortalCreate(Portal portal, Portal linked) { reEmitTimer = 3; } + + public void startListeningToArea(AABB area) { + if (level() instanceof ServerLevel level) { + forEachSection(area, section -> ((LevelExt) level).pc$addBlockChangeListener(section.asLong(), this)); + } + } + + public void stopListeningToArea(AABB area) { + if (level() instanceof ServerLevel level) { + forEachSection(area, section -> ((LevelExt) level).pc$removeBlockChangeListener(section.asLong(), this)); + } + } + + public static void forEachSection(AABB bounds, Consumer consumer) { + BlockPos minBlock = BlockPos.containing(bounds.minX, bounds.minY, bounds.minZ); + BlockPos maxBlock = BlockPos.containing(bounds.maxX, bounds.maxY, bounds.maxZ); + SectionPos min = SectionPos.of(minBlock); + SectionPos max = SectionPos.of(maxBlock); + SectionPos.betweenClosedStream(min.x(), min.y(), min.z(), max.x(), max.y(), max.z()).forEach(consumer); + } } diff --git a/src/main/java/com/fusionflux/portalcubed/mixin/LevelMixin.java b/src/main/java/com/fusionflux/portalcubed/mixin/LevelMixin.java index da9fd8e7..ae14fae6 100644 --- a/src/main/java/com/fusionflux/portalcubed/mixin/LevelMixin.java +++ b/src/main/java/com/fusionflux/portalcubed/mixin/LevelMixin.java @@ -7,8 +7,9 @@ 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.AbortableIterationConsumer.Continuation; +import net.minecraft.server.level.ServerLevel; import net.minecraft.util.profiling.ProfilerFiller; import net.minecraft.world.entity.Entity; import net.minecraft.world.level.Level; @@ -18,6 +19,9 @@ import net.minecraft.world.level.entity.LevelEntityGetter; import net.minecraft.world.level.storage.WritableLevelData; +import com.google.common.collect.AbstractIterator; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import org.jetbrains.annotations.Nullable; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.Shadow; @@ -26,13 +30,19 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Iterator; +import java.util.List; import java.util.UUID; import java.util.function.Supplier; @Mixin(Level.class) public abstract class LevelMixin implements LevelAccessor, LevelExt { @Unique - PortalCubedDamageSources pc$damageSources; + private PortalCubedDamageSources pc$damageSources; + @Unique + private Long2ObjectMap> pc$blockChangeListeners = new Long2ObjectOpenHashMap<>(); @Shadow protected abstract LevelEntityGetter getEntities(); @@ -55,12 +65,38 @@ private void createDamageSources( @Inject(method = "setBlocksDirty", at = @At("HEAD")) private void updateEmittedEntities(BlockPos pos, BlockState old, BlockState updated, CallbackInfo ci) { - if (!isClientSide() && old.getCollisionShape(this, pos) != updated.getCollisionShape(this, pos)) { - getEntities().get(EmittedEntity.TYPE_TEST, emitted -> { - if (emitted.listensTo(pos)) - emitted.reEmit(); - return Continuation.CONTINUE; - }); + if ((Object) this instanceof ServerLevel) { + getListeners(pos).forEachRemaining(EmittedEntity::reEmit); + } + } + + @Unique + private Iterator getListeners(BlockPos pos) { + long sectionPos = SectionPos.asLong(pos); + List listeners = pc$blockChangeListeners.get(sectionPos); + return listeners == null || listeners.isEmpty() ? Collections.emptyIterator() : new AbstractIterator<>() { + private final Iterator entities = List.copyOf(listeners).iterator(); // copy to avoid CMEs + + @Override + protected EmittedEntity computeNext() { + if (!entities.hasNext()) + return endOfData(); + EmittedEntity next = entities.next(); + return next.listensTo(pos) ? next : computeNext(); + } + }; + } + + @Override + public void pc$addBlockChangeListener(long sectionPos, EmittedEntity entity) { + pc$blockChangeListeners.computeIfAbsent(sectionPos, $ -> new ArrayList<>()).add(entity); + } + + @Override + public void pc$removeBlockChangeListener(long sectionPos, EmittedEntity entity) { + List listeners = pc$blockChangeListeners.get(sectionPos); + if (listeners != null) { + listeners.remove(entity); } }