diff --git a/src/main/java/com/minelittlepony/unicopia/BlockDestructionManager.java b/src/main/java/com/minelittlepony/unicopia/BlockDestructionManager.java deleted file mode 100644 index fe427f2f..00000000 --- a/src/main/java/com/minelittlepony/unicopia/BlockDestructionManager.java +++ /dev/null @@ -1,227 +0,0 @@ -package com.minelittlepony.unicopia; - -import java.util.Arrays; -import java.util.List; -import java.util.function.Supplier; - -import com.google.common.base.Suppliers; -import com.minelittlepony.unicopia.network.Channel; -import com.minelittlepony.unicopia.network.MsgBlockDestruction; -import com.minelittlepony.unicopia.util.NbtSerialisable; - -import it.unimi.dsi.fastutil.longs.Long2ObjectMap; -import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; -import net.minecraft.block.BlockState; -import net.minecraft.nbt.NbtCompound; -import net.minecraft.server.network.ServerPlayerEntity; -import net.minecraft.server.world.ServerWorld; -import net.minecraft.server.world.ThreadedAnvilChunkStorage; -import net.minecraft.util.math.BlockPos; -import net.minecraft.util.math.ChunkPos; -import net.minecraft.world.PersistentState; -import net.minecraft.world.World; - -public class BlockDestructionManager extends PersistentState { - public static final int DESTRUCTION_COOLDOWN = 50; - public static final int UNSET_DAMAGE = -1; - public static final int MAX_DAMAGE = 10; - - private final Destruction emptyDestruction = new Destruction(); - - private final World world; - - private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); - - private final Object locker = new Object(); - - public static Supplier create(World world) { - if (world instanceof ServerWorld serverWorld) { - return Suppliers.memoize(() -> { - return serverWorld.getPersistentStateManager().getOrCreate( - compound -> new BlockDestructionManager(world, compound), - () -> new BlockDestructionManager(world), - "unicopia:destruction_manager" - ); - }); - } - return Suppliers.memoize(() -> new BlockDestructionManager(world)); - } - - BlockDestructionManager(World world) { - this.world = world; - } - - BlockDestructionManager(World world, NbtCompound compound) { - this(world); - NbtCompound d = compound.getCompound("chunks"); - d.getKeys().forEach(id -> { - chunks.computeIfAbsent(Long.valueOf(id), Chunk::new).fromNBT(d.getCompound(id)); - }); - } - - @Override - public NbtCompound writeNbt(NbtCompound compound) { - NbtCompound destructions = new NbtCompound(); - this.chunks.forEach((id, chunk) -> { - destructions.put(id.toString(), chunk.toNBT()); - }); - compound.put("chunks", destructions); - return compound; - } - - public int getBlockDestruction(BlockPos pos) { - return getChunk(pos).getBlockDestruction(pos); - } - - private Chunk getChunk(BlockPos pos) { - return chunks.computeIfAbsent(new ChunkPos(pos).toLong(), Chunk::new); - } - - public void setBlockDestruction(BlockPos pos, int amount) { - synchronized (locker) { - getChunk(pos).setBlockDestruction(pos, amount); - markDirty(); - } - } - - public int damageBlock(BlockPos pos, int amount) { - if (amount == 0) { - return getBlockDestruction(pos); - } - amount = Math.max(getBlockDestruction(pos), 0) + amount; - setBlockDestruction(pos, amount); - return amount; - } - - public void onBlockChanged(BlockPos pos, BlockState oldState, BlockState newstate) { - if (oldState.getBlock() != newstate.getBlock()) { - setBlockDestruction(pos, UNSET_DAMAGE); - } - } - - public void tick() { - synchronized (locker) { - chunks.long2ObjectEntrySet().removeIf(entry -> entry.getValue().tick()); - - if (world instanceof ServerWorld) { - chunks.forEach((chunkPos, chunk) -> chunk.sendUpdates((ServerWorld)world)); - } - } - } - - private class Chunk implements NbtSerialisable { - private final Long2ObjectMap destructions = new Long2ObjectOpenHashMap<>(); - - private final long pos; - - Chunk(long pos) { - this.pos = pos; - } - - public int getBlockDestruction(BlockPos pos) { - return destructions.getOrDefault(pos.asLong(), emptyDestruction).amount; - } - - public void setBlockDestruction(BlockPos pos, int amount) { - destructions.computeIfAbsent(pos.asLong(), p -> new Destruction()).set(amount); - } - - boolean tick() { - destructions.long2ObjectEntrySet().removeIf(e -> e.getValue().tick()); - return destructions.isEmpty(); - } - - void sendUpdates(ServerWorld world) { - if (!world.getChunkManager().isChunkLoaded(ChunkPos.getPackedX(pos), ChunkPos.getPackedZ(pos))) { - return; - } - - ThreadedAnvilChunkStorage storage = world.getChunkManager().threadedAnvilChunkStorage; - - List players = storage.getPlayersWatchingChunk(new ChunkPos(pos), false); - - if (!players.isEmpty()) { - Long2ObjectOpenHashMap values = new Long2ObjectOpenHashMap<>(); - - destructions.forEach((blockPos, item) -> { - if (item.dirty) { - item.dirty = false; - values.put(blockPos.longValue(), (Integer)item.amount); - } - }); - - MsgBlockDestruction msg = new MsgBlockDestruction(values); - - if (msg.toBuffer().writerIndex() > 1048576) { - throw new IllegalStateException("Payload may not be larger than 1048576 bytes. Here's what we were trying to send: [" - + values.size() + "]\n" - + Arrays.toString(values.values().stream().mapToInt(Integer::intValue).toArray())); - } - - players.forEach(player -> { - if (player instanceof ServerPlayerEntity) { - Channel.SERVER_BLOCK_DESTRUCTION.send(player, msg); - } - }); - } - } - - @Override - public void toNBT(NbtCompound compound) { - NbtCompound states = new NbtCompound(); - destructions.forEach((id, state) -> { - states.put(id.toString(), state.toNBT()); - }); - compound.put("states", states); - } - - @Override - public void fromNBT(NbtCompound compound) { - NbtCompound d = compound.getCompound("states"); - chunks.clear(); - d.getKeys().forEach(id -> { - destructions.computeIfAbsent(Long.valueOf(id), i -> new Destruction()).fromNBT(d.getCompound(id)); - }); - } - } - - private class Destruction implements NbtSerialisable { - int amount = UNSET_DAMAGE; - int age = DESTRUCTION_COOLDOWN; - boolean dirty; - - boolean tick() { - if (age-- > 0) { - return false; - } - - if (amount >= 0) { - set(amount - 1); - } - return amount < 0 || age-- <= 0; - } - - void set(int amount) { - this.age = DESTRUCTION_COOLDOWN; - this.amount = amount >= 0 && amount < MAX_DAMAGE ? amount : UNSET_DAMAGE; - this.dirty = true; - } - - @Override - public void toNBT(NbtCompound compound) { - compound.putInt("destruction", amount); - compound.putInt("age", age); - } - - @Override - public void fromNBT(NbtCompound compound) { - amount = compound.getInt("destruction"); - age = compound.getInt("age"); - dirty = true; - } - } - - public interface Source { - BlockDestructionManager getDestructionManager(); - } -} diff --git a/src/main/java/com/minelittlepony/unicopia/Unicopia.java b/src/main/java/com/minelittlepony/unicopia/Unicopia.java index c82874eb..1a3a6ec2 100644 --- a/src/main/java/com/minelittlepony/unicopia/Unicopia.java +++ b/src/main/java/com/minelittlepony/unicopia/Unicopia.java @@ -17,6 +17,7 @@ import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; import com.minelittlepony.unicopia.ability.magic.spell.trait.TraitLoader; import com.minelittlepony.unicopia.advancement.UCriteria; import com.minelittlepony.unicopia.block.UBlocks; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; import com.minelittlepony.unicopia.block.state.StateMapLoader; import com.minelittlepony.unicopia.command.Commands; import com.minelittlepony.unicopia.container.SpellbookChapterLoader; diff --git a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java index f067d254..32bba993 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyKickAbility.java @@ -5,11 +5,11 @@ import java.util.List; import org.jetbrains.annotations.Nullable; import com.google.common.collect.Lists; -import com.minelittlepony.unicopia.BlockDestructionManager; import com.minelittlepony.unicopia.Race; import com.minelittlepony.unicopia.ability.data.Hit; import com.minelittlepony.unicopia.ability.data.Pos; import com.minelittlepony.unicopia.ability.data.tree.TreeType; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; import com.minelittlepony.unicopia.client.minelittlepony.MineLPConnector; import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation; import com.minelittlepony.unicopia.entity.player.Pony; diff --git a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyStompAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyStompAbility.java index 3d21a0bc..09b44fd1 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyStompAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/EarthPonyStompAbility.java @@ -2,9 +2,9 @@ package com.minelittlepony.unicopia.ability; import org.jetbrains.annotations.Nullable; -import com.minelittlepony.unicopia.BlockDestructionManager; import com.minelittlepony.unicopia.Race; import com.minelittlepony.unicopia.ability.data.Hit; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.item.UItems; diff --git a/src/main/java/com/minelittlepony/unicopia/ability/UnicornCastingAbility.java b/src/main/java/com/minelittlepony/unicopia/ability/UnicornCastingAbility.java index 5f04ef07..43d183b4 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/UnicornCastingAbility.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/UnicornCastingAbility.java @@ -8,6 +8,7 @@ import com.minelittlepony.unicopia.ability.data.Hit; import com.minelittlepony.unicopia.ability.magic.spell.HomingSpell; import com.minelittlepony.unicopia.ability.magic.spell.Spell; import com.minelittlepony.unicopia.ability.magic.spell.effect.CustomisedSpellType; +import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; import com.minelittlepony.unicopia.client.render.PlayerPoser.Animation; import com.minelittlepony.unicopia.entity.player.Pony; import com.minelittlepony.unicopia.item.AmuletItem; @@ -105,9 +106,9 @@ public class UnicornCastingAbility implements Ability { if (newSpell.getResult() != ActionResult.FAIL) { CustomisedSpellType spell = newSpell.getValue(); - boolean remove = player.getSpellSlot().removeIf(spell, true); - player.subtractEnergyCost(remove ? 2 : 4); - if (!remove) { + boolean removed = player.getSpellSlot().removeIf(spell.isEmpty() ? spell : SpellType.PORTAL.negate().and(spell), true); + player.subtractEnergyCost(removed ? 2 : 4); + if (!removed) { Spell s = spell.apply(player); if (s == null) { player.spawnParticles(ParticleTypes.LARGE_SMOKE, 6); diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/PlaceableSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/PlaceableSpell.java index d976aaeb..b52023e5 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/PlaceableSpell.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/PlaceableSpell.java @@ -108,6 +108,19 @@ public class PlaceableSpell extends AbstractDelegatingSpell { return !isDead(); } + /** + * Detaches this spell from the placed version. + * This spell and the placed entity effectively become independent. + * + * @return The previous cast spell entity if one existed, otherwise empty. + */ + protected Optional detach(Caster source) { + return getSpellEntity(source).map(e -> { + castEntity.set(null); + return e; + }); + } + @Override public void onDestroyed(Caster source) { if (!source.isClient()) { diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/PortalSpell.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/PortalSpell.java new file mode 100644 index 00000000..1d910250 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/PortalSpell.java @@ -0,0 +1,109 @@ +package com.minelittlepony.unicopia.ability.magic.spell.effect; + +import com.minelittlepony.unicopia.USounds; +import com.minelittlepony.unicopia.ability.magic.Caster; +import com.minelittlepony.unicopia.ability.magic.spell.Situation; +import com.minelittlepony.unicopia.block.data.Ether; +import com.minelittlepony.unicopia.entity.CastSpellEntity; +import com.minelittlepony.unicopia.entity.EntityReference; +import com.minelittlepony.unicopia.particle.*; +import com.minelittlepony.unicopia.particle.ParticleHandle.Attachment; +import com.minelittlepony.unicopia.util.shape.Sphere; + +import net.minecraft.nbt.NbtCompound; +import net.minecraft.util.math.Vec3d; + +public class PortalSpell extends AbstractSpell { + private final EntityReference teleportationTarget = new EntityReference<>(); + + private boolean publishedPosition; + + private final ParticleHandle particlEffect = new ParticleHandle(); + + protected PortalSpell(CustomisedSpellType type) { + super(type); + } + + @Override + public boolean apply(Caster caster) { + return toPlaceable().apply(caster); + } + + @Override + public boolean tick(Caster source, Situation situation) { + if (situation == Situation.GROUND) { + teleportationTarget.getPosition().ifPresentOrElse( + targetPos -> tickWithTargetLink(source, targetPos), + () -> advertiseAvailable(source) + ); + + if (!publishedPosition) { + publishedPosition = true; + Ether.get(source.getReferenceWorld()).put(getType(), source); + } + + if (source.isClient()) { + Vec3d origin = source.getOriginVector(); + + source.spawnParticles(origin, new Sphere(true, 2, 1, 0, 1), 17, pos -> { + source.addParticle(new MagicParticleEffect(getType().getColor()), pos, Vec3d.ZERO); + }); + } + } + + return true; + } + + private void tickWithTargetLink(Caster source, Vec3d targetPos) { + particlEffect.update(getUuid(), source, spawner -> { + spawner.addParticle(new SphereParticleEffect(UParticles.DISK, getType().getColor(), 0.9F, 2), source.getOriginVector(), Vec3d.ZERO); + }).ifPresent(p -> { + p.setAttribute(Attachment.ATTR_COLOR, getType().getColor()); + }); + + Vec3d center = source.getOriginVector(); + source.findAllEntitiesInRange(1).filter(e -> true).forEach(entity -> { + if (!entity.hasPortalCooldown() && entity.timeUntilRegen <= 0) { + Vec3d destination = entity.getPos().subtract(center).add(targetPos); + entity.resetPortalCooldown(); + entity.timeUntilRegen = 100; + + entity.playSound(USounds.ENTITY_PLAYER_UNICORN_TELEPORT, 1, 1); + entity.teleport(destination.x, destination.y, destination.z); + } + ParticleUtils.spawnParticles(new MagicParticleEffect(getType().getColor()), entity, 7); + }); + } + + @SuppressWarnings("unchecked") + private void advertiseAvailable(Caster source) { + Ether ether = Ether.get(source.getReferenceWorld()); + ether.getIds(getType()) + .stream() + .filter(id -> !id.referenceEquals(source.getEntity())) + .findAny() + .ifPresent(ref -> { + ether.remove(getType(), ref.getId().get()); + teleportationTarget.copyFrom((EntityReference)ref); + }); + } + + @Override + public void onDestroyed(Caster caster) { + Ether.get(caster.getReferenceWorld()).remove(getType(), caster.getEntity().getUuid()); + } + + @Override + public void toNBT(NbtCompound compound) { + super.toNBT(compound); + compound.putBoolean("publishedPosition", publishedPosition); + compound.put("teleportationTarget", teleportationTarget.toNBT()); + } + + @Override + public void fromNBT(NbtCompound compound) { + super.fromNBT(compound); + publishedPosition = compound.getBoolean("publishedPosition"); + teleportationTarget.fromNBT(compound.getCompound("teleportationTarget")); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java index 3e90d543..51be7776 100644 --- a/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java +++ b/src/main/java/com/minelittlepony/unicopia/ability/magic/spell/effect/SpellType.java @@ -62,6 +62,7 @@ public final class SpellType implements Affine, SpellPredicate< public static final SpellType CATAPULT = register("catapult", Affinity.GOOD, 0x22FF00, true, CatapultSpell.DEFAULT_TRAITS, CatapultSpell::new); public static final SpellType FIRE_BOLT = register("fire_bolt", Affinity.GOOD, 0xFF8811, true, FireBoltSpell.DEFAULT_TRAITS, FireBoltSpell::new); public static final SpellType LIGHT = register("light", Affinity.GOOD, 0xEEFFAA, true, LightSpell.DEFAULT_TRAITS, LightSpell::new); + public static final SpellType PORTAL = register("portal", Affinity.GOOD, 0x99FFFF, true, LightSpell.DEFAULT_TRAITS, PortalSpell::new); public static void bootstrap() {} diff --git a/src/main/java/com/minelittlepony/unicopia/block/data/BlockDestructionManager.java b/src/main/java/com/minelittlepony/unicopia/block/data/BlockDestructionManager.java new file mode 100644 index 00000000..80d0253c --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/block/data/BlockDestructionManager.java @@ -0,0 +1,131 @@ +package com.minelittlepony.unicopia.block.data; + +import java.util.Arrays; +import java.util.List; +import java.util.function.Supplier; + +import com.google.common.base.Suppliers; +import com.minelittlepony.unicopia.Unicopia; +import com.minelittlepony.unicopia.network.Channel; +import com.minelittlepony.unicopia.network.MsgBlockDestruction; +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.block.BlockState; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.world.World; + +public class BlockDestructionManager { + private static final Identifier ID = Unicopia.id("destruction_manager"); + + public static final int DESTRUCTION_COOLDOWN = 50; + public static final int UNSET_DAMAGE = -1; + public static final int MAX_DAMAGE = 10; + + private final WorldOverlay chunks; + + public static Supplier create(World world) { + return Suppliers.memoize(() -> new BlockDestructionManager(world)); + } + + private BlockDestructionManager(World world) { + this.chunks = WorldOverlay.getOverlay(world, ID, w -> new WorldOverlay<>(world, Destruction::new, this::sendUpdates)); + } + + public int getBlockDestruction(BlockPos pos) { + Destruction destr = chunks.getState(pos); + return destr == null ? UNSET_DAMAGE : destr.amount; + } + + public void setBlockDestruction(BlockPos pos, int amount) { + chunks.getOrCreateState(pos).set(amount); + chunks.markDirty(); + } + + public int damageBlock(BlockPos pos, int amount) { + if (amount == 0) { + return getBlockDestruction(pos); + } + amount = Math.max(getBlockDestruction(pos), 0) + amount; + setBlockDestruction(pos, amount); + return amount; + } + + public void onBlockChanged(BlockPos pos, BlockState oldState, BlockState newstate) { + if (oldState.getBlock() != newstate.getBlock()) { + setBlockDestruction(pos, UNSET_DAMAGE); + } + } + + public void tick() { + chunks.tick(); + } + + private void sendUpdates(Long2ObjectMap destructions, List players) { + Long2ObjectOpenHashMap values = new Long2ObjectOpenHashMap<>(); + + destructions.forEach((blockPos, item) -> { + if (item.dirty) { + item.dirty = false; + values.put(blockPos.longValue(), (Integer)item.amount); + } + }); + + MsgBlockDestruction msg = new MsgBlockDestruction(values); + + if (msg.toBuffer().writerIndex() > 1048576) { + throw new IllegalStateException("Payload may not be larger than 1048576 bytes. Here's what we were trying to send: [" + + values.size() + "]\n" + + Arrays.toString(values.values().stream().mapToInt(Integer::intValue).toArray())); + } + + players.forEach(player -> { + if (player instanceof ServerPlayerEntity) { + Channel.SERVER_BLOCK_DESTRUCTION.send(player, msg); + } + }); + } + + private class Destruction implements WorldOverlay.State { + int amount = UNSET_DAMAGE; + int age = DESTRUCTION_COOLDOWN; + boolean dirty; + + @Override + public boolean tick() { + if (age-- > 0) { + return false; + } + + if (amount >= 0) { + set(amount - 1); + } + return amount < 0 || age-- <= 0; + } + + void set(int amount) { + this.age = DESTRUCTION_COOLDOWN; + this.amount = amount >= 0 && amount < MAX_DAMAGE ? amount : UNSET_DAMAGE; + this.dirty = true; + } + + @Override + public void toNBT(NbtCompound compound) { + compound.putInt("destruction", amount); + compound.putInt("age", age); + } + + @Override + public void fromNBT(NbtCompound compound) { + amount = compound.getInt("destruction"); + age = compound.getInt("age"); + dirty = true; + } + } + + public interface Source { + BlockDestructionManager getDestructionManager(); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/block/data/Ether.java b/src/main/java/com/minelittlepony/unicopia/block/data/Ether.java new file mode 100644 index 00000000..67b611f9 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/block/data/Ether.java @@ -0,0 +1,87 @@ +package com.minelittlepony.unicopia.block.data; + +import java.util.*; + +import com.minelittlepony.unicopia.Unicopia; +import com.minelittlepony.unicopia.ability.magic.Caster; +import com.minelittlepony.unicopia.ability.magic.spell.effect.SpellType; +import com.minelittlepony.unicopia.entity.EntityReference; + +import net.minecraft.nbt.*; +import net.minecraft.util.Identifier; +import net.minecraft.util.Util; +import net.minecraft.world.PersistentState; +import net.minecraft.world.World; + +public class Ether extends PersistentState { + 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>> advertisingEndpoints = new HashMap<>(); + + private final Object locker = new Object(); + + Ether(World world, NbtCompound compound) { + this(world); + compound.getKeys().forEach(key -> { + Identifier typeId = Identifier.tryParse(key); + if (typeId != null) { + Set> uuids = getIds(typeId); + compound.getList(key, NbtElement.COMPOUND_TYPE).forEach(entry -> { + uuids.add(new EntityReference<>((NbtCompound)entry)); + }); + } + }); + } + + Ether(World world) { + + } + + @Override + public NbtCompound writeNbt(NbtCompound compound) { + synchronized (locker) { + advertisingEndpoints.forEach((id, uuids) -> { + NbtList list = new NbtList(); + uuids.forEach(uuid -> list.add(uuid.toNBT())); + compound.put(id.toString(), list); + }); + + return compound; + } + } + + public void put(SpellType spellType, Caster caster) { + synchronized (locker) { + getIds(spellType.getId()).add(new EntityReference<>(caster.getEntity())); + } + markDirty(); + } + + public void remove(SpellType spellType, UUID id) { + synchronized (locker) { + Identifier typeId = spellType.getId(); + Set> refs = advertisingEndpoints.get(typeId); + if (refs != null) { + refs.removeIf(ref -> ref.getId().orElse(Util.NIL_UUID).equals(id)); + if (refs.isEmpty()) { + advertisingEndpoints.remove(typeId); + } + markDirty(); + } + } + } + + public Set> getIds(SpellType spellType) { + return getIds(spellType.getId()); + } + + private Set> getIds(Identifier typeId) { + synchronized (locker) { + return advertisingEndpoints.computeIfAbsent(typeId, i -> new HashSet<>()); + } + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/block/data/WorldOverlay.java b/src/main/java/com/minelittlepony/unicopia/block/data/WorldOverlay.java new file mode 100644 index 00000000..d5464117 --- /dev/null +++ b/src/main/java/com/minelittlepony/unicopia/block/data/WorldOverlay.java @@ -0,0 +1,182 @@ +package com.minelittlepony.unicopia.block.data; + +import java.util.List; +import java.util.function.*; + +import org.jetbrains.annotations.Nullable; + +import com.minelittlepony.unicopia.util.NbtSerialisable; + +import it.unimi.dsi.fastutil.longs.Long2ObjectMap; +import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; +import net.minecraft.nbt.NbtCompound; +import net.minecraft.server.network.ServerPlayerEntity; +import net.minecraft.server.world.ServerWorld; +import net.minecraft.server.world.ThreadedAnvilChunkStorage; +import net.minecraft.util.Identifier; +import net.minecraft.util.math.BlockPos; +import net.minecraft.util.math.ChunkPos; +import net.minecraft.world.PersistentState; +import net.minecraft.world.World; + +public class WorldOverlay extends PersistentState { + private final World world; + + private final Long2ObjectMap chunks = new Long2ObjectOpenHashMap<>(); + + private final Object locker = new Object(); + + private final Supplier factory; + @Nullable + private final BiConsumer, List> updateSender; + + public static T getPersistableStorage(World world, Identifier id, BiFunction loadFunc, Function factory) { + if (world instanceof ServerWorld serverWorld) { + return serverWorld.getPersistentStateManager().getOrCreate( + compound -> loadFunc.apply(world, compound), + () -> factory.apply(world), + id.toString() + ); + } + return factory.apply(world); + } + + public static WorldOverlay getOverlay(World world, Identifier id, Supplier factory, @Nullable BiConsumer, List> updateSender) { + return getOverlay(world, id, w -> new WorldOverlay<>(w, factory, updateSender)); + } + + public static WorldOverlay getOverlay(World world, Identifier id, Function> overlayFactory) { + return getPersistableStorage(world, id, (w, tag) -> { + WorldOverlay overlay = overlayFactory.apply(w); + overlay.readNbt(tag); + return overlay; + }, overlayFactory); + } + + WorldOverlay(World world, Supplier factory, @Nullable BiConsumer, List> updateSender) { + this.world = world; + this.factory = factory; + this.updateSender = updateSender; + } + + @Override + public NbtCompound writeNbt(NbtCompound compound) { + NbtCompound destructions = new NbtCompound(); + this.chunks.forEach((id, chunk) -> { + destructions.put(id.toString(), chunk.toNBT()); + }); + compound.put("chunks", destructions); + return compound; + } + + public void readNbt(NbtCompound compound) { + NbtCompound d = compound.getCompound("chunks"); + d.getKeys().forEach(id -> { + chunks.computeIfAbsent(Long.valueOf(id), Chunk::new).fromNBT(d.getCompound(id)); + }); + } + + @Nullable + public T getState(BlockPos pos) { + return getChunk(pos).getState(pos); + } + + public T getOrCreateState(BlockPos pos) { + synchronized (locker) { + return getChunk(pos).getOrCreateState(pos); + } + } + + private Chunk getChunk(BlockPos pos) { + return chunks.computeIfAbsent(new ChunkPos(pos).toLong(), Chunk::new); + } + + public void setState(BlockPos pos, @Nullable T state) { + synchronized (locker) { + getChunk(pos).setState(pos, state); + markDirty(); + } + } + + public void tick() { + synchronized (locker) { + chunks.long2ObjectEntrySet().removeIf(entry -> entry.getValue().tick()); + + if (world instanceof ServerWorld) { + chunks.forEach((chunkPos, chunk) -> chunk.sendUpdates((ServerWorld)world)); + } + } + } + + private class Chunk implements NbtSerialisable { + private final Long2ObjectMap states = new Long2ObjectOpenHashMap<>(); + + private final long pos; + + Chunk(long pos) { + this.pos = pos; + } + + @Nullable + public T getState(BlockPos pos) { + return states.get(pos.asLong()); + } + + public T getOrCreateState(BlockPos pos) { + return states.computeIfAbsent(pos.asLong(), l -> factory.get()); + } + + public void setState(BlockPos pos, @Nullable T state) { + if (state == null) { + states.remove(pos.asLong()); + } else { + states.put(pos.asLong(), state); + } + } + + boolean tick() { + states.long2ObjectEntrySet().removeIf(e -> e.getValue().tick()); + return states.isEmpty(); + } + + void sendUpdates(ServerWorld world) { + if (updateSender == null) { + return; + } + + if (!world.getChunkManager().isChunkLoaded(ChunkPos.getPackedX(pos), ChunkPos.getPackedZ(pos))) { + return; + } + + ThreadedAnvilChunkStorage storage = world.getChunkManager().threadedAnvilChunkStorage; + + List players = storage.getPlayersWatchingChunk(new ChunkPos(pos), false); + + if (!players.isEmpty()) { + updateSender.accept(states, players); + } + } + + @Override + public void toNBT(NbtCompound compound) { + NbtCompound states = new NbtCompound(); + this.states.forEach((id, state) -> { + states.put(id.toString(), state.toNBT()); + }); + compound.put("states", states); + } + + @Override + public void fromNBT(NbtCompound compound) { + NbtCompound d = compound.getCompound("states"); + chunks.clear(); + d.getKeys().forEach(id -> { + states.computeIfAbsent(Long.valueOf(id), i -> factory.get()).fromNBT(d.getCompound(id)); + }); + } + } + + public interface State extends NbtSerialisable { + boolean tick(); + } +} diff --git a/src/main/java/com/minelittlepony/unicopia/client/ClientBlockDestructionManager.java b/src/main/java/com/minelittlepony/unicopia/client/ClientBlockDestructionManager.java index 99965b65..35e3fe85 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/ClientBlockDestructionManager.java +++ b/src/main/java/com/minelittlepony/unicopia/client/ClientBlockDestructionManager.java @@ -2,7 +2,8 @@ package com.minelittlepony.unicopia.client; import java.util.SortedSet; import com.google.common.collect.Sets; -import com.minelittlepony.unicopia.BlockDestructionManager; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; + import it.unimi.dsi.fastutil.longs.Long2ObjectMap; import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap; import net.minecraft.client.render.BlockBreakingInfo; diff --git a/src/main/java/com/minelittlepony/unicopia/client/particle/DiskParticle.java b/src/main/java/com/minelittlepony/unicopia/client/particle/DiskParticle.java index ce1c812a..2a2b6783 100644 --- a/src/main/java/com/minelittlepony/unicopia/client/particle/DiskParticle.java +++ b/src/main/java/com/minelittlepony/unicopia/client/particle/DiskParticle.java @@ -13,7 +13,7 @@ public class DiskParticle extends SphereParticle { public DiskParticle(SphereParticleEffect effect, ClientWorld w, double x, double y, double z, double rX, double rY, double rZ) { super(effect, w, x, y, z, 0, 0, 0); - rotation = new Quaternion((float)rX, (float)rY, (float)rZ, true); + rotation = new Quaternion((float)effect.getOffset().x, (float)effect.getOffset().y, (float)effect.getOffset().z, true); } @Override diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinServerWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinServerWorld.java index 67d8a6e2..49edc53c 100644 --- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinServerWorld.java +++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinServerWorld.java @@ -5,7 +5,7 @@ import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import com.minelittlepony.unicopia.BlockDestructionManager; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; import net.minecraft.block.BlockState; import net.minecraft.server.world.ServerWorld; diff --git a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java b/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java index 02749daa..f5c7bb6f 100644 --- a/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java +++ b/src/main/java/com/minelittlepony/unicopia/mixin/MixinWorld.java @@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.Inject; import org.spongepowered.asm.mixin.injection.ModifyVariable; import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import com.minelittlepony.unicopia.BlockDestructionManager; +import com.minelittlepony.unicopia.block.data.BlockDestructionManager; import com.minelittlepony.unicopia.entity.collision.EntityCollisions; import com.minelittlepony.unicopia.entity.duck.RotatedView; diff --git a/src/main/resources/assets/unicopia/lang/en_us.json b/src/main/resources/assets/unicopia/lang/en_us.json index 014ebdc1..15e20acc 100644 --- a/src/main/resources/assets/unicopia/lang/en_us.json +++ b/src/main/resources/assets/unicopia/lang/en_us.json @@ -152,6 +152,8 @@ "spell.unicopia.vortex.lore": "Creates a magnetic force that pulls in other targets", "spell.unicopia.dark_vortex": "Dark Vortex", "spell.unicopia.dark_vortex.lore": "Creates a black hole from which nothing can escape", + "spell.unicopia.portal": "Teleportation", + "spell.unicopia.portal.lore": "Connects two points in space for fast travel between", "spell.unicopia.necromancy": "Necromancy", "spell.unicopia.necromancy.lore": "Summons undead minions from beyond the grave", "spell.unicopia.siphoning": "Life Sapping",