Track which spells are active on which blocks to make tile-by-tile interactions easier. Requireed by #412

This commit is contained in:
Sollace 2024-09-19 21:27:53 +01:00
parent dc33678062
commit 420f78eaaf
No known key found for this signature in database
GPG key ID: E52FACE7B5C773DB
6 changed files with 264 additions and 8 deletions

View file

@ -31,6 +31,7 @@ import com.minelittlepony.unicopia.item.enchantment.UEnchantments;
import com.minelittlepony.unicopia.network.Channel; import com.minelittlepony.unicopia.network.Channel;
import com.minelittlepony.unicopia.particle.UParticles; import com.minelittlepony.unicopia.particle.UParticles;
import com.minelittlepony.unicopia.server.world.BlockDestructionManager; 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.NocturnalSleepManager;
import com.minelittlepony.unicopia.server.world.UGameRules; import com.minelittlepony.unicopia.server.world.UGameRules;
import com.minelittlepony.unicopia.server.world.UWorldGen; import com.minelittlepony.unicopia.server.world.UWorldGen;
@ -71,6 +72,7 @@ public class Unicopia implements ModInitializer {
((BlockDestructionManager.Source)w).getDestructionManager().tick(); ((BlockDestructionManager.Source)w).getDestructionManager().tick();
ZapAppleStageStore.get(w).tick(); ZapAppleStageStore.get(w).tick();
WeatherConditions.get(w).tick(); WeatherConditions.get(w).tick();
Ether.get(w).tick();
if (Debug.SPELLBOOK_CHAPTERS) { if (Debug.SPELLBOOK_CHAPTERS) {
SpellbookChapterLoader.INSTANCE.sendUpdate(w.getServer()); SpellbookChapterLoader.INSTANCE.sendUpdate(w.getServer());
} }

View file

@ -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.Spell;
import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType;
import com.minelittlepony.unicopia.entity.EntityReference; 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.NbtSerialisable;
import com.minelittlepony.unicopia.util.Tickable;
import net.minecraft.nbt.*; import net.minecraft.nbt.*;
import net.minecraft.util.Identifier; import net.minecraft.util.Identifier;
import net.minecraft.util.math.BlockPos;
import net.minecraft.util.math.MathHelper; import net.minecraft.util.math.MathHelper;
import net.minecraft.world.PersistentState; import net.minecraft.world.PersistentState;
import net.minecraft.world.World; 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"); private static final Identifier ID = Unicopia.id("ether");
public static Ether get(World world) { public static Ether get(World world) {
@ -28,6 +32,7 @@ public class Ether extends PersistentState {
} }
private final Map<Identifier, Map<UUID, Map<UUID, Entry<?>>>> endpoints; private final Map<Identifier, Map<UUID, Map<UUID, Entry<?>>>> endpoints;
private final PositionalDataMap<Entry<?>> positionData = new PositionalDataMap<>();
private final Object locker = new Object(); private final Object locker = new Object();
@ -70,23 +75,36 @@ public class Ether extends PersistentState {
markDirty(); markDirty();
return new Entry<>(spell, caster); return new Entry<>(spell, caster);
}); });
if (entry.removed) {
entry.removed = false;
markDirty();
}
if (entry.spell.get() != spell) { if (entry.spell.get() != spell) {
entry.spell = new WeakReference<>(spell); entry.spell = new WeakReference<>(spell);
markDirty(); markDirty();
} }
if (entry.removed) {
entry.removed = false;
positionData.update(entry);
markDirty();
}
return entry; return entry;
} }
} }
@Override
public void tick() {
endpoints.values().forEach(byType -> {
byType.values().forEach(entries -> {
entries.values().forEach(Entry::update);
});
});
}
public <T extends Spell> void remove(SpellType<T> spellType, UUID entityId) { public <T extends Spell> void remove(SpellType<T> spellType, UUID entityId) {
synchronized (locker) { synchronized (locker) {
endpoints.computeIfPresent(spellType.getId(), (typeId, entries) -> { endpoints.computeIfPresent(spellType.getId(), (typeId, entries) -> {
if (entries.remove(entityId) != null) { Map<UUID, Entry<?>> data = entries.remove(entityId);
if (data != null) {
markDirty(); markDirty();
data.values().forEach(positionData::remove);
} }
return entries.isEmpty() ? null : entries; return entries.isEmpty() ? null : entries;
}); });
@ -150,6 +168,10 @@ public class Ether extends PersistentState {
return false; return false;
} }
public Set<Entry<?>> getAtPosition(BlockPos pos) {
return world.isClient() ? Set.of() : positionData.getState(pos);
}
private void pruneNodes() { private void pruneNodes() {
this.endpoints.values().removeIf(entities -> { this.endpoints.values().removeIf(entities -> {
entities.values().removeIf(spells -> { entities.values().removeIf(spells -> {
@ -160,7 +182,7 @@ public class Ether extends PersistentState {
}); });
} }
public class Entry<T extends Spell> implements NbtSerialisable { public class Entry<T extends Spell> implements PositionalDataMap.Hotspot, NbtSerialisable {
public final EntityReference<?> entity; public final EntityReference<?> entity;
@Nullable @Nullable
@ -176,6 +198,9 @@ public class Ether extends PersistentState {
private final Set<UUID> claimants = new HashSet<>(); private final Set<UUID> claimants = new HashSet<>();
private BlockPos currentPos = BlockPos.ORIGIN;
private BlockPos previousPos = BlockPos.ORIGIN;
private Entry(NbtElement nbt) { private Entry(NbtElement nbt) {
this.entity = new EntityReference<>(); this.entity = new EntityReference<>();
this.spell = new WeakReference<>(null); this.spell = new WeakReference<>(null);
@ -186,6 +211,15 @@ public class Ether extends PersistentState {
this.entity = new EntityReference<>(caster.asEntity()); this.entity = new EntityReference<>(caster.asEntity());
this.spell = new WeakReference<>(spell); this.spell = new WeakReference<>(spell);
spellId = spell.getUuid(); 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() { public boolean hasChanged() {
@ -216,6 +250,13 @@ public class Ether extends PersistentState {
markDirty(); markDirty();
} }
@Override
public BlockPos getCenter() {
return currentPos;
}
@Override
public float getRadius() { public float getRadius() {
return radius; return radius;
} }
@ -223,6 +264,9 @@ public class Ether extends PersistentState {
public void setRadius(float radius) { public void setRadius(float radius) {
if (!MathHelper.approximatelyEquals(this.radius, radius)) { if (!MathHelper.approximatelyEquals(this.radius, radius)) {
this.radius = radius; this.radius = radius;
if ((int)radius != (int)this.radius) {
positionData.update(this);
}
changed.set(true); changed.set(true);
} }
markDirty(); markDirty();
@ -247,6 +291,7 @@ public class Ether extends PersistentState {
public void markDead() { public void markDead() {
Unicopia.LOGGER.debug("Marking " + entity.getTarget().orElse(null) + " as dead"); Unicopia.LOGGER.debug("Marking " + entity.getTarget().orElse(null) + " as dead");
removed = true; removed = true;
positionData.remove(this);
claimants.clear(); claimants.clear();
markDirty(); markDirty();
} }

View file

@ -94,7 +94,7 @@ public class WorldOverlay<T extends WorldOverlay.State> extends PersistentState
} }
private Chunk getChunk(BlockPos pos) { 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) { public void setState(BlockPos pos, @Nullable T state) {

View file

@ -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<T extends PositionalDataMap.Hotspot> {
private final Long2ObjectMap<Section<T>> sections = new Long2ObjectOpenHashMap<>();
private final Map<T, Set<Section<T>>> entryToSections = new WeakHashMap<>();
Chunk(long pos) { }
public synchronized Set<T> getState(BlockPos pos) {
Section<T> section = sections.get(ChunkSectionPos.getSectionCoord(pos.getY()));
return section == null ? Set.of() : section.getState(pos);
}
public synchronized boolean remove(T entry) {
Set<Section<T>> 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<Section<T>> oldSections = entryToSections.get(entry);
Set<Section<T>> 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<Section<T>> getIntersectingSections(Box entryBox) {
Set<Section<T>> 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;
}
}

View file

@ -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<T extends PositionalDataMap.Hotspot> {
private final Long2ObjectMap<Chunk<T>> chunks = new Long2ObjectOpenHashMap<>();
private final Map<T, Set<Chunk<T>>> entryToChunks = new WeakHashMap<>();
public Set<T> getState(BlockPos pos) {
Chunk<T> chunk = chunks.get(ChunkPos.toLong(pos));
return chunk == null ? Set.of() : chunk.getState(pos);
}
public void remove(T entry) {
Set<Chunk<T>> 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<Chunk<T>> oldChunks = entryToChunks.get(entry);
Set<Chunk<T>> 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<Chunk<T>> 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<Chunk<T>> 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();
}
}

View file

@ -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<T extends PositionalDataMap.Hotspot> {
private final Set<T> entries = weakSet();
private Set<T>[] 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<T> getState(BlockPos pos) {
int localPos = toLocalIndex(pos);
if (states == null) {
states = new Set[16 * 16 * 16];
}
Set<T> state = states[localPos];
return state == null ? (states[localPos] = calculateState(pos)) : state;
}
private Set<T> calculateState(BlockPos pos) {
Set<T> 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<T> Set<T> weakSet() {
return Collections.newSetFromMap(new WeakHashMap<>());
}
}