From eb97b80d1c45e77b54657e1637e68f31b6deb98d Mon Sep 17 00:00:00 2001 From: Sollace Date: Thu, 19 Sep 2024 21:27:53 +0100 Subject: [PATCH] Track which spells are active on which blocks to make tile-by-tile interactions easier. Requireed by #412 --- .../com/minelittlepony/unicopia/Unicopia.java | 2 + .../unicopia/server/world/Ether.java | 59 +++++++++++-- .../unicopia/server/world/WorldOverlay.java | 2 +- .../unicopia/server/world/chunk/Chunk.java | 64 +++++++++++++++ .../server/world/chunk/PositionalDataMap.java | 63 ++++++++++++++ .../unicopia/server/world/chunk/Section.java | 82 +++++++++++++++++++ 6 files changed, 264 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/minelittlepony/unicopia/server/world/chunk/Chunk.java create mode 100644 src/main/java/com/minelittlepony/unicopia/server/world/chunk/PositionalDataMap.java create mode 100644 src/main/java/com/minelittlepony/unicopia/server/world/chunk/Section.java diff --git a/src/main/java/com/minelittlepony/unicopia/Unicopia.java b/src/main/java/com/minelittlepony/unicopia/Unicopia.java index 8142560d..a6057f85 100644 --- a/src/main/java/com/minelittlepony/unicopia/Unicopia.java +++ b/src/main/java/com/minelittlepony/unicopia/Unicopia.java @@ -31,6 +31,7 @@ import com.minelittlepony.unicopia.item.enchantment.UEnchantments; import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.particle.UParticles; import com.minelittlepony.unicopia.server.world.BlockDestructionManager; +import com.minelittlepony.unicopia.server.world.Ether; import com.minelittlepony.unicopia.server.world.NocturnalSleepManager; import com.minelittlepony.unicopia.server.world.UGameRules; import com.minelittlepony.unicopia.server.world.UWorldGen; @@ -71,6 +72,7 @@ public class Unicopia implements ModInitializer { ((BlockDestructionManager.Source)w).getDestructionManager().tick(); ZapAppleStageStore.get(w).tick(); WeatherConditions.get(w).tick(); + Ether.get(w).tick(); if (Debug.SPELLBOOK_CHAPTERS) { SpellbookChapterLoader.INSTANCE.sendUpdate(w.getServer()); } diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/Ether.java b/src/main/java/com/minelittlepony/unicopia/server/world/Ether.java index 840e66fa..08c6a9a2 100644 --- a/src/main/java/com/minelittlepony/unicopia/server/world/Ether.java +++ b/src/main/java/com/minelittlepony/unicopia/server/world/Ether.java @@ -13,14 +13,18 @@ import com.minelittlepony.unicopia.ability.magic.Caster; import com.minelittlepony.unicopia.ability.magic.spell.Spell; import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; import com.minelittlepony.unicopia.entity.EntityReference; +import com.minelittlepony.unicopia.server.world.chunk.PositionalDataMap; import com.minelittlepony.unicopia.util.NbtSerialisable; +import com.minelittlepony.unicopia.util.Tickable; + import net.minecraft.nbt.*; import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; import net.minecraft.util.math.MathHelper; import net.minecraft.world.PersistentState; import net.minecraft.world.World; -public class Ether extends PersistentState { +public class Ether extends PersistentState implements Tickable { private static final Identifier ID = Unicopia.id("ether"); public static Ether get(World world) { @@ -28,6 +32,7 @@ public class Ether extends PersistentState { } private final Map>>> endpoints; + private final PositionalDataMap> positionData = new PositionalDataMap<>(); private final Object locker = new Object(); @@ -70,23 +75,36 @@ public class Ether extends PersistentState { markDirty(); return new Entry<>(spell, caster); }); - if (entry.removed) { - entry.removed = false; - markDirty(); - } + if (entry.spell.get() != spell) { entry.spell = new WeakReference<>(spell); markDirty(); } + if (entry.removed) { + entry.removed = false; + positionData.update(entry); + markDirty(); + } return entry; } } + @Override + public void tick() { + endpoints.values().forEach(byType -> { + byType.values().forEach(entries -> { + entries.values().forEach(Entry::update); + }); + }); + } + public void remove(SpellType spellType, UUID entityId) { synchronized (locker) { endpoints.computeIfPresent(spellType.getId(), (typeId, entries) -> { - if (entries.remove(entityId) != null) { + Map> data = entries.remove(entityId); + if (data != null) { markDirty(); + data.values().forEach(positionData::remove); } return entries.isEmpty() ? null : entries; }); @@ -150,6 +168,10 @@ public class Ether extends PersistentState { return false; } + public Set> getAtPosition(BlockPos pos) { + return world.isClient() ? Set.of() : positionData.getState(pos); + } + private void pruneNodes() { this.endpoints.values().removeIf(entities -> { entities.values().removeIf(spells -> { @@ -160,7 +182,7 @@ public class Ether extends PersistentState { }); } - public class Entry implements NbtSerialisable { + public class Entry implements PositionalDataMap.Hotspot, NbtSerialisable { public final EntityReference entity; @Nullable @@ -176,6 +198,9 @@ public class Ether extends PersistentState { private final Set claimants = new HashSet<>(); + private BlockPos currentPos = BlockPos.ORIGIN; + private BlockPos previousPos = BlockPos.ORIGIN; + private Entry(NbtElement nbt) { this.entity = new EntityReference<>(); this.spell = new WeakReference<>(null); @@ -186,6 +211,15 @@ public class Ether extends PersistentState { this.entity = new EntityReference<>(caster.asEntity()); this.spell = new WeakReference<>(spell); spellId = spell.getUuid(); + update(); + } + + void update() { + previousPos = currentPos; + currentPos = entity.getTarget().map(t -> BlockPos.ofFloored(t.pos())).orElse(BlockPos.ORIGIN); + if (!currentPos.equals(previousPos)) { + positionData.update(this); + } } public boolean hasChanged() { @@ -216,6 +250,13 @@ public class Ether extends PersistentState { markDirty(); } + + @Override + public BlockPos getCenter() { + return currentPos; + } + + @Override public float getRadius() { return radius; } @@ -223,6 +264,9 @@ public class Ether extends PersistentState { public void setRadius(float radius) { if (!MathHelper.approximatelyEquals(this.radius, radius)) { this.radius = radius; + if ((int)radius != (int)this.radius) { + positionData.update(this); + } changed.set(true); } markDirty(); @@ -247,6 +291,7 @@ public class Ether extends PersistentState { public void markDead() { Unicopia.LOGGER.debug("Marking " + entity.getTarget().orElse(null) + " as dead"); removed = true; + positionData.remove(this); claimants.clear(); markDirty(); } diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java b/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java index 6ab36008..a094a5aa 100644 --- a/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java +++ b/src/main/java/com/minelittlepony/unicopia/server/world/WorldOverlay.java @@ -98,7 +98,7 @@ public class WorldOverlay extends PersistentState } private Chunk getChunk(BlockPos pos) { - return chunks.computeIfAbsent(new ChunkPos(pos).toLong(), Chunk::new); + return chunks.computeIfAbsent(ChunkPos.toLong(pos), Chunk::new); } public void setState(BlockPos pos, @Nullable T state) { diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Chunk.java b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Chunk.java new file mode 100644 index 00000000..d59919db --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Chunk.java @@ -0,0 +1,64 @@ +package com.minelittlepony.unicopia.server.world.chunk; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.ChunkSectionPos; + +public class Chunk { + private final Long2ObjectMap> sections = new Long2ObjectOpenHashMap<>(); + private final Map>> entryToSections = new WeakHashMap<>(); + + Chunk(long pos) { } + + public synchronized Set getState(BlockPos pos) { + Section section = sections.get(ChunkSectionPos.getSectionCoord(pos.getY())); + return section == null ? Set.of() : section.getState(pos); + } + + public synchronized boolean remove(T entry) { + Set> sections = entryToSections.remove(entry); + if (sections != null) { + sections.forEach(section -> { + if (section.remove(entry) && section.isEmpty()) { + this.sections.remove(section.pos); + } + }); + return true; + } + return false; + } + + public synchronized boolean update(T entry, Box entryBox) { + Set> oldSections = entryToSections.get(entry); + Set> newSections = getIntersectingSections(entryBox); + if (oldSections != null) { + oldSections.forEach(section -> { + if (!newSections.contains(section) && section.remove(entry) && section.isEmpty()) { + this.sections.remove(section.pos); + } + }); + } + newSections.forEach(chunk -> chunk.update(entry, entryBox)); + entryToSections.put(entry, newSections); + return true; + } + + private Set> getIntersectingSections(Box entryBox) { + Set> sections = new HashSet<>(); + + int minY = ChunkSectionPos.getSectionCoord(entryBox.minY); + int maxY = ChunkSectionPos.getSectionCoord(entryBox.maxY); + for (int y = minY; y <= maxY; y++) { + sections.add(this.sections.computeIfAbsent(y, Section::new)); + } + + return sections; + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/chunk/PositionalDataMap.java b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/PositionalDataMap.java new file mode 100644 index 00000000..eb2ee335 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/PositionalDataMap.java @@ -0,0 +1,63 @@ +package com.minelittlepony.unicopia.server.world.chunk; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.WeakHashMap; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.util.math.ChunkSectionPos; +import net.minecraft.util.math.MathHelper; + +public class PositionalDataMap { + private final Long2ObjectMap> chunks = new Long2ObjectOpenHashMap<>(); + private final Map>> entryToChunks = new WeakHashMap<>(); + + public Set getState(BlockPos pos) { + Chunk chunk = chunks.get(ChunkPos.toLong(pos)); + return chunk == null ? Set.of() : chunk.getState(pos); + } + + public void remove(T entry) { + Set> chunks = entryToChunks.remove(entry); + if (chunks != null) { + chunks.forEach(chunk -> chunk.remove(entry)); + } + } + + public void update(T entry) { + Box entryBox = new Box(entry.getCenter()).expand(MathHelper.ceil(entry.getRadius())); + Set> oldChunks = entryToChunks.get(entry); + Set> newChunks = getIntersectingChunks(entryBox); + if (oldChunks != null) { + oldChunks.forEach(chunk -> chunk.remove(entry)); + } + newChunks.forEach(chunk -> chunk.update(entry, entryBox)); + entryToChunks.put(entry, newChunks); + } + + private Set> getIntersectingChunks(Box entryBox) { + int minX = ChunkSectionPos.getSectionCoord(entryBox.minX); + int maxX = ChunkSectionPos.getSectionCoord(entryBox.maxX); + int minZ = ChunkSectionPos.getSectionCoord(entryBox.minZ); + int maxZ = ChunkSectionPos.getSectionCoord(entryBox.maxZ); + + Set> chunks = new HashSet<>(); + for (int x = minX; x <= maxX; x++) { + for (int z = minZ; z <= maxZ; z++) { + chunks.add(this.chunks.computeIfAbsent(ChunkPos.toLong(x, z), Chunk::new)); + } + } + return chunks; + } + + public interface Hotspot { + float getRadius(); + + BlockPos getCenter(); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Section.java b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Section.java new file mode 100644 index 00000000..7024acdc --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/server/world/chunk/Section.java @@ -0,0 +1,82 @@ +package com.minelittlepony.unicopia.server.world.chunk; + +import java.util.Collections; +import java.util.Set; +import java.util.WeakHashMap; + +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.Box; +import net.minecraft.util.math.MathHelper; + +public class Section { + private final Set entries = weakSet(); + private Set[] states; + + final long pos; + + public Section(long pos) { + this.pos = pos; + } + + public boolean isEmpty() { + return entries.isEmpty(); + } + + public boolean remove(T entry) { + if (entries.remove(entry)) { + states = null; + return true; + } + return false; + } + + public boolean update(T entry, Box box) { + entries.add(entry); + states = null; + return true; + } + + @SuppressWarnings("unchecked") + public Set getState(BlockPos pos) { + int localPos = toLocalIndex(pos); + if (states == null) { + states = new Set[16 * 16 * 16]; + } + Set state = states[localPos]; + return state == null ? (states[localPos] = calculateState(pos)) : state; + } + + private Set calculateState(BlockPos pos) { + Set state = weakSet(); + + for (T entry : entries) { + BlockPos center = entry.getCenter(); + int radius = MathHelper.ceil(entry.getRadius()); + + if (pos.equals(center) + || (isInRange(pos.getX(), center.getX(), radius) + && isInRange(pos.getZ(), center.getZ(), radius) + && isInRange(pos.getY(), center.getY(), radius) + && center.isWithinDistance(pos, radius))) { + state.add(entry); + } + } + + return state; + } + + static boolean isInRange(int value, int center, int radius) { + return value >= center - radius && value <= center + radius; + } + + static int toLocalIndex(BlockPos pos) { + int x = pos.getX() % 16; + int y = pos.getY() % 16; + int z = pos.getZ() % 16; + return x + (y * 16) + (z * 16 * 16); + } + + static Set weakSet() { + return Collections.newSetFromMap(new WeakHashMap<>()); + } +}