Skip to content

Commit

Permalink
Track which spells are active on which blocks to make tile-by-tile in…
Browse files Browse the repository at this point in the history
…teractions easier. Requireed by #412
  • Loading branch information
Sollace committed Oct 8, 2024
1 parent dc33678 commit 420f78e
Show file tree
Hide file tree
Showing 6 changed files with 264 additions and 8 deletions.
2 changes: 2 additions & 0 deletions src/main/java/com/minelittlepony/unicopia/Unicopia.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@
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;
Expand Down Expand Up @@ -71,6 +72,7 @@ public void onInitialize() {
((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());
}
Expand Down
59 changes: 52 additions & 7 deletions src/main/java/com/minelittlepony/unicopia/server/world/Ether.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,21 +13,26 @@
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) {
return WorldOverlay.getPersistableStorage(world, ID, Ether::new, Ether::new);
}

private final Map<Identifier, Map<UUID, Map<UUID, Entry<?>>>> endpoints;
private final PositionalDataMap<Entry<?>> positionData = new PositionalDataMap<>();

private final Object locker = new Object();

Expand Down Expand Up @@ -70,23 +75,36 @@ public <T extends Spell> Entry<T> getOrCreate(T spell, Caster<?> caster) {
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 <T extends Spell> void remove(SpellType<T> spellType, UUID entityId) {
synchronized (locker) {
endpoints.computeIfPresent(spellType.getId(), (typeId, entries) -> {
if (entries.remove(entityId) != null) {
Map<UUID, Entry<?>> data = entries.remove(entityId);
if (data != null) {
markDirty();
data.values().forEach(positionData::remove);
}
return entries.isEmpty() ? null : entries;
});
Expand Down Expand Up @@ -150,6 +168,10 @@ public <T extends Spell> boolean anyMatch(SpellType<T> spellType, Predicate<Entr
return false;
}

public Set<Entry<?>> getAtPosition(BlockPos pos) {
return world.isClient() ? Set.of() : positionData.getState(pos);
}

private void pruneNodes() {
this.endpoints.values().removeIf(entities -> {
entities.values().removeIf(spells -> {
Expand All @@ -160,7 +182,7 @@ private void pruneNodes() {
});
}

public class Entry<T extends Spell> implements NbtSerialisable {
public class Entry<T extends Spell> implements PositionalDataMap.Hotspot, NbtSerialisable {
public final EntityReference<?> entity;

@Nullable
Expand All @@ -176,6 +198,9 @@ public class Entry<T extends Spell> implements NbtSerialisable {

private final Set<UUID> 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);
Expand All @@ -186,6 +211,15 @@ public Entry(T spell, Caster<?> caster) {
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() {
Expand Down Expand Up @@ -216,13 +250,23 @@ public void setYaw(float yaw) {
markDirty();
}


@Override
public BlockPos getCenter() {
return currentPos;
}

@Override
public float getRadius() {
return radius;
}

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();
Expand All @@ -247,6 +291,7 @@ public UUID getSpellId() {
public void markDead() {
Unicopia.LOGGER.debug("Marking " + entity.getTarget().orElse(null) + " as dead");
removed = true;
positionData.remove(this);
claimants.clear();
markDirty();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ public T getOrCreateState(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) {
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
@@ -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<>());
}
}

0 comments on commit 420f78e

Please sign in to comment.