Snapshot API (#722)

This commit is contained in:
TheMode 2022-03-03 07:44:57 +01:00 committed by GitHub
parent 3e184abc0f
commit f7d44c4774
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
23 changed files with 845 additions and 5 deletions

View File

@ -18,6 +18,7 @@ import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.socket.Server;
import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.scoreboard.TeamManager;
import net.minestom.server.snapshot.Snapshotable;
import net.minestom.server.thread.ThreadDispatcher;
import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.world.DimensionTypeManager;
@ -29,7 +30,7 @@ import java.net.SocketAddress;
@ApiStatus.Experimental
@ApiStatus.NonExtendable
public interface ServerProcess {
public interface ServerProcess extends Snapshotable {
/**
* Handles incoming connections/players.
*/

View File

@ -1,8 +1,10 @@
package net.minestom.server;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.advancements.AdvancementManager;
import net.minestom.server.adventure.bossbar.BossBarManager;
import net.minestom.server.command.CommandManager;
import net.minestom.server.entity.Entity;
import net.minestom.server.event.EventDispatcher;
import net.minestom.server.event.GlobalEventHandler;
import net.minestom.server.event.server.ServerTickMonitorEvent;
@ -21,20 +23,27 @@ import net.minestom.server.network.PacketProcessor;
import net.minestom.server.network.socket.Server;
import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.scoreboard.TeamManager;
import net.minestom.server.snapshot.*;
import net.minestom.server.terminal.MinestomTerminal;
import net.minestom.server.thread.Acquirable;
import net.minestom.server.thread.ThreadDispatcher;
import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.collection.MappedCollection;
import net.minestom.server.world.DimensionTypeManager;
import net.minestom.server.world.biomes.BiomeManager;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.net.SocketAddress;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;
final class ServerProcessImpl implements ServerProcess {
private final static Logger LOGGER = LoggerFactory.getLogger(ServerProcessImpl.class);
@ -252,6 +261,33 @@ final class ServerProcessImpl implements ServerProcess {
return started.get() && !stopped.get();
}
@Override
public @NotNull Snapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
List<AtomicReference<InstanceSnapshot>> instanceRefs = new ArrayList<>();
Int2ObjectOpenHashMap<AtomicReference<EntitySnapshot>> entityRefs = new Int2ObjectOpenHashMap<>();
for (Instance instance : instance.getInstances()) {
instanceRefs.add(updater.reference(instance));
for (Entity entity : instance.getEntities()) {
entityRefs.put(entity.getEntityId(), updater.reference(entity));
}
}
return new SnapshotImpl(MappedCollection.plainReferences(instanceRefs), entityRefs);
}
record SnapshotImpl(Collection<InstanceSnapshot> instances,
Int2ObjectOpenHashMap<AtomicReference<EntitySnapshot>> entityRefs) implements ServerSnapshot {
@Override
public @NotNull Collection<EntitySnapshot> entities() {
return MappedCollection.plainReferences(entityRefs.values());
}
@Override
public @UnknownNullability EntitySnapshot entity(int id) {
var ref = entityRefs.get(id);
return ref != null ? ref.getPlain() : null;
}
}
private final class TickerImpl implements Ticker {
@Override
public void tick(long nanoTime) {

View File

@ -33,12 +33,17 @@ import net.minestom.server.permission.PermissionHandler;
import net.minestom.server.potion.Potion;
import net.minestom.server.potion.PotionEffect;
import net.minestom.server.potion.TimedPotion;
import net.minestom.server.snapshot.EntitySnapshot;
import net.minestom.server.snapshot.SnapshotUpdater;
import net.minestom.server.snapshot.Snapshotable;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.thread.Acquirable;
import net.minestom.server.timer.Schedulable;
import net.minestom.server.timer.Scheduler;
import net.minestom.server.timer.TaskSchedule;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.async.AsyncUtils;
import net.minestom.server.utils.block.BlockIterator;
@ -72,7 +77,7 @@ import java.util.function.UnaryOperator;
* <p>
* To create your own entity you probably want to extends {@link LivingEntity} or {@link EntityCreature} instead.
*/
public class Entity implements Viewable, Tickable, Schedulable, TagHandler, PermissionHandler, HoverEventSource<ShowEntity>, Sound.Emitter {
public class Entity implements Viewable, Tickable, Schedulable, Snapshotable, TagHandler, PermissionHandler, HoverEventSource<ShowEntity>, Sound.Emitter {
private static final Int2ObjectSyncMap<Entity> ENTITY_BY_ID = Int2ObjectSyncMap.hashmap();
private static final Map<UUID, Entity> ENTITY_BY_UUID = new ConcurrentHashMap<>();
@ -1526,6 +1531,18 @@ public class Entity implements Viewable, Tickable, Schedulable, TagHandler, Perm
return scheduler;
}
@Override
public @NotNull EntitySnapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
final Chunk chunk = currentChunk;
final int[] viewersId = this.viewEngine.viewableOption.bitSet.toIntArray();
final int[] passengersId = ArrayUtils.mapToIntArray(passengers, Entity::getEntityId);
final Entity vehicle = this.vehicle;
return new EntitySnapshotImpl.Entity(entityType, uuid, id, position, velocity,
updater.reference(instance), chunk.getChunkX(), chunk.getChunkZ(),
viewersId, passengersId, vehicle == null ? -1 : vehicle.getEntityId(),
TagReadable.fromCompound(nbtCompound.toCompound()));
}
/**
* Applies knockback to the entity
*

View File

@ -0,0 +1,115 @@
package net.minestom.server.entity;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.snapshot.ChunkSnapshot;
import net.minestom.server.snapshot.EntitySnapshot;
import net.minestom.server.snapshot.InstanceSnapshot;
import net.minestom.server.snapshot.PlayerSnapshot;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.collection.IntMappedArray;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicReference;
final class EntitySnapshotImpl {
record Entity(EntityType type, UUID uuid, int id, Pos position, Vec velocity,
AtomicReference<InstanceSnapshot> instanceRef, int chunkX, int chunkZ,
int[] viewersId, int[] passengersId, int vehicleId,
TagReadable tagReadable) implements EntitySnapshot {
@Override
public <T> @Nullable T getTag(@NotNull Tag<T> tag) {
return tagReadable.getTag(tag);
}
@Override
public @NotNull InstanceSnapshot instance() {
return instanceRef.getPlain();
}
@Override
public @NotNull ChunkSnapshot chunk() {
return Objects.requireNonNull(instance().chunk(chunkX, chunkZ));
}
@Override
public @NotNull Collection<@NotNull PlayerSnapshot> viewers() {
return new IntMappedArray<>(viewersId, id -> (PlayerSnapshot) instance().server().entity(id));
}
@Override
public @NotNull Collection<@NotNull EntitySnapshot> passengers() {
return new IntMappedArray<>(passengersId, id -> instance().server().entity(id));
}
@Override
public @Nullable EntitySnapshot vehicle() {
if (vehicleId == -1) return null;
return instance().server().entity(vehicleId);
}
}
record Player(EntitySnapshot snapshot, String username,
GameMode gameMode) implements PlayerSnapshot {
@Override
public @NotNull EntityType type() {
return snapshot.type();
}
@Override
public @NotNull UUID uuid() {
return snapshot.uuid();
}
@Override
public int id() {
return snapshot.id();
}
@Override
public @NotNull Pos position() {
return snapshot.position();
}
@Override
public @NotNull Vec velocity() {
return snapshot.velocity();
}
@Override
public @NotNull InstanceSnapshot instance() {
return snapshot.instance();
}
@Override
public @NotNull ChunkSnapshot chunk() {
return snapshot.chunk();
}
@Override
public @NotNull Collection<@NotNull PlayerSnapshot> viewers() {
return snapshot.viewers();
}
@Override
public @NotNull Collection<@NotNull EntitySnapshot> passengers() {
return snapshot.passengers();
}
@Override
public @Nullable EntitySnapshot vehicle() {
return snapshot.vehicle();
}
@Override
public <T> @Nullable T getTag(@NotNull Tag<T> tag) {
return snapshot.getTag(tag);
}
}
}

View File

@ -64,6 +64,9 @@ import net.minestom.server.recipe.RecipeManager;
import net.minestom.server.resourcepack.ResourcePack;
import net.minestom.server.scoreboard.BelowNameTag;
import net.minestom.server.scoreboard.Team;
import net.minestom.server.snapshot.EntitySnapshot;
import net.minestom.server.snapshot.PlayerSnapshot;
import net.minestom.server.snapshot.SnapshotUpdater;
import net.minestom.server.statistic.PlayerStatistic;
import net.minestom.server.timer.SchedulerManager;
import net.minestom.server.utils.MathUtils;
@ -1958,6 +1961,12 @@ public class Player extends LivingEntity implements CommandSender, Localizable,
return Locale.forLanguageTag(locale.replace("_", "-"));
}
@Override
public @NotNull PlayerSnapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
final EntitySnapshot snapshot = super.updateSnapshot(updater);
return new EntitySnapshotImpl.Player(snapshot, username, gameMode);
}
/**
* Sets the player's locale. This will only set the locale of the player as it
* is stored in the server. This will also be reset if the settings are refreshed.

View File

@ -8,6 +8,7 @@ import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.PFColumnarSpace;
import net.minestom.server.instance.block.Block;
import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.snapshot.Snapshotable;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.utils.chunk.ChunkSupplier;
@ -34,7 +35,7 @@ import java.util.UUID;
* You generally want to avoid storing references of this object as this could lead to a huge memory leak,
* you should store the chunk coordinates instead.
*/
public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, Biome.Setter, Viewable, Tickable, TagHandler {
public abstract class Chunk implements Block.Getter, Block.Setter, Biome.Getter, Biome.Setter, Viewable, Tickable, TagHandler, Snapshotable {
public static final int CHUNK_SIZE_X = 16;
public static final int CHUNK_SIZE_Z = 16;
public static final int CHUNK_SECTION_SIZE = 16;

View File

@ -4,6 +4,7 @@ import com.extollit.gaming.ai.path.model.ColumnarOcclusionFieldList;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.coordinate.Point;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.Player;
import net.minestom.server.entity.pathfinding.PFBlock;
import net.minestom.server.instance.block.Block;
@ -13,6 +14,11 @@ import net.minestom.server.network.packet.server.play.ChunkDataPacket;
import net.minestom.server.network.packet.server.play.UpdateLightPacket;
import net.minestom.server.network.packet.server.play.data.ChunkData;
import net.minestom.server.network.packet.server.play.data.LightData;
import net.minestom.server.snapshot.ChunkSnapshot;
import net.minestom.server.snapshot.SnapshotUpdater;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.MathUtils;
import net.minestom.server.utils.Utils;
import net.minestom.server.utils.binary.BinaryWriter;
@ -238,4 +244,15 @@ public class DynamicChunk extends Chunk {
skyLights, blockLights);
}
@Override
public @NotNull ChunkSnapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
Section[] clonedSections = new Section[sections.size()];
for (int i = 0; i < clonedSections.length; i++)
clonedSections[i] = sections.get(i).clone();
var entities = instance.getEntityTracker().chunkEntities(chunkX, chunkZ, EntityTracker.Target.ENTITIES);
final int[] entityIds = ArrayUtils.mapToIntArray(entities, Entity::getEntityId);
return new InstanceSnapshotImpl.Chunk(minSection, chunkX, chunkZ,
clonedSections, entries.clone(), entityIds, updater.reference(instance),
TagReadable.fromCompound(Objects.requireNonNull(getTag(Tag.NBT))));
}
}

View File

@ -3,6 +3,7 @@ package net.minestom.server.instance;
import it.unimi.dsi.fastutil.objects.ObjectArraySet;
import net.kyori.adventure.identity.Identity;
import net.kyori.adventure.pointer.Pointers;
import net.minestom.server.MinecraftServer;
import net.minestom.server.Tickable;
import net.minestom.server.adventure.audience.PacketGroupingAudience;
import net.minestom.server.coordinate.Point;
@ -17,11 +18,17 @@ import net.minestom.server.instance.block.Block;
import net.minestom.server.instance.block.BlockHandler;
import net.minestom.server.network.packet.server.play.BlockActionPacket;
import net.minestom.server.network.packet.server.play.TimeUpdatePacket;
import net.minestom.server.snapshot.ChunkSnapshot;
import net.minestom.server.snapshot.InstanceSnapshot;
import net.minestom.server.snapshot.SnapshotUpdater;
import net.minestom.server.snapshot.Snapshotable;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagHandler;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.thread.ThreadDispatcher;
import net.minestom.server.timer.Schedulable;
import net.minestom.server.timer.Scheduler;
import net.minestom.server.utils.ArrayUtils;
import net.minestom.server.utils.PacketUtils;
import net.minestom.server.utils.chunk.ChunkUtils;
import net.minestom.server.utils.time.Cooldown;
@ -37,6 +44,7 @@ import org.jglrxavpok.hephaistos.nbt.mutable.MutableNBTCompound;
import java.time.Duration;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Consumer;
import java.util.stream.Collectors;
@ -50,7 +58,7 @@ import java.util.stream.Collectors;
* with {@link InstanceManager#registerInstance(Instance)}, and
* you need to be sure to signal the {@link ThreadDispatcher} of every partition/element changes.
*/
public abstract class Instance implements Block.Getter, Block.Setter, Tickable, Schedulable, TagHandler, PacketGroupingAudience {
public abstract class Instance implements Block.Getter, Block.Setter, Tickable, Schedulable, Snapshotable, TagHandler, PacketGroupingAudience {
private boolean registered;
@ -608,6 +616,15 @@ public abstract class Instance implements Block.Getter, Block.Setter, Tickable,
return scheduler;
}
@Override
public @NotNull InstanceSnapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
final Map<Long, AtomicReference<ChunkSnapshot>> chunksMap = updater.referencesMapLong(getChunks(), ChunkUtils::getChunkIndex);
final int[] entities = ArrayUtils.mapToIntArray(entityTracker.entities(), Entity::getEntityId);
return new InstanceSnapshotImpl.Instance(updater.reference(MinecraftServer.process()),
getDimensionType(), getWorldAge(), getTime(), chunksMap, entities,
TagReadable.fromCompound(Objects.requireNonNull(getTag(Tag.NBT))));
}
/**
* Creates an explosion at the given position with the given strength.
* The algorithm used to compute damages is provided by {@link #getExplosionSupplier()}.

View File

@ -0,0 +1,107 @@
package net.minestom.server.instance;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import net.minestom.server.MinecraftServer;
import net.minestom.server.instance.block.Block;
import net.minestom.server.snapshot.ChunkSnapshot;
import net.minestom.server.snapshot.EntitySnapshot;
import net.minestom.server.snapshot.InstanceSnapshot;
import net.minestom.server.snapshot.ServerSnapshot;
import net.minestom.server.tag.Tag;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.utils.collection.IntMappedArray;
import net.minestom.server.utils.collection.MappedCollection;
import net.minestom.server.world.DimensionType;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Collection;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReference;
import static net.minestom.server.utils.chunk.ChunkUtils.*;
final class InstanceSnapshotImpl {
record Instance(AtomicReference<ServerSnapshot> serverRef,
DimensionType dimensionType, long worldAge, long time,
Map<Long, AtomicReference<ChunkSnapshot>> chunksMap,
int[] entitiesIds,
TagReadable tagReadable) implements InstanceSnapshot {
@Override
public @Nullable ChunkSnapshot chunk(int chunkX, int chunkZ) {
var ref = chunksMap.get(getChunkIndex(chunkX, chunkZ));
return Objects.requireNonNull(ref, "Chunk not found").getPlain();
}
@Override
public @NotNull Collection<@NotNull ChunkSnapshot> chunks() {
return MappedCollection.plainReferences(chunksMap.values());
}
@Override
public @NotNull Collection<EntitySnapshot> entities() {
return new IntMappedArray<>(entitiesIds, id -> server().entity(id));
}
@Override
public @NotNull ServerSnapshot server() {
return serverRef.getPlain();
}
@Override
public <T> @Nullable T getTag(@NotNull Tag<T> tag) {
return tagReadable.getTag(tag);
}
}
record Chunk(int minSection, int chunkX, int chunkZ,
Section[] sections,
Int2ObjectOpenHashMap<Block> blockEntries,
int[] entitiesIds,
AtomicReference<InstanceSnapshot> instanceRef,
TagReadable tagReadable) implements ChunkSnapshot {
@Override
public @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) {
// Verify if the block object is present
if (condition != Condition.TYPE) {
final Block entry = !blockEntries.isEmpty() ?
blockEntries.get(getBlockIndex(x, y, z)) : null;
if (entry != null || condition == Condition.CACHED) {
return entry;
}
}
// Retrieve the block from state id
final Section section = sections[getChunkCoordinate(y) - minSection];
final int blockStateId = section.blockPalette()
.get(toSectionRelativeCoordinate(x), toSectionRelativeCoordinate(y), toSectionRelativeCoordinate(z));
return Objects.requireNonNullElse(Block.fromStateId((short) blockStateId), Block.AIR);
}
@Override
public @NotNull Biome getBiome(int x, int y, int z) {
final Section section = sections[getChunkCoordinate(y) - minSection];
final int id = section.biomePalette()
.get(toSectionRelativeCoordinate(x) / 4, toSectionRelativeCoordinate(y) / 4, toSectionRelativeCoordinate(z) / 4);
return MinecraftServer.getBiomeManager().getById(id);
}
@Override
public <T> @Nullable T getTag(@NotNull Tag<T> tag) {
return tagReadable.getTag(tag);
}
@Override
public @NotNull InstanceSnapshot instance() {
return instanceRef.getPlain();
}
@Override
public @NotNull Collection<@NotNull EntitySnapshot> entities() {
return new IntMappedArray<>(entitiesIds, id -> instance().server().entity(id));
}
}
}

View File

@ -11,7 +11,7 @@ import java.util.function.IntUnaryOperator;
/**
* Palette that switches between its backend based on the use case.
*/
final class AdaptivePalette implements Palette {
final class AdaptivePalette implements Palette, Cloneable {
final byte dimension, defaultBitsPerEntry, maxBitsPerEntry;
SpecializedPalette palette;

View File

@ -0,0 +1,18 @@
package net.minestom.server.snapshot;
import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
public interface ChunkSnapshot extends Snapshot, Block.Getter, Biome.Getter, TagReadable {
int chunkX();
int chunkZ();
@NotNull InstanceSnapshot instance();
@NotNull Collection<@NotNull EntitySnapshot> entities();
}

View File

@ -0,0 +1,33 @@
package net.minestom.server.snapshot;
import net.minestom.server.coordinate.Pos;
import net.minestom.server.coordinate.Vec;
import net.minestom.server.entity.EntityType;
import net.minestom.server.tag.TagReadable;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.Collection;
import java.util.UUID;
public interface EntitySnapshot extends Snapshot, TagReadable {
@NotNull EntityType type();
@NotNull UUID uuid();
int id();
@NotNull Pos position();
@NotNull Vec velocity();
@NotNull InstanceSnapshot instance();
@NotNull ChunkSnapshot chunk();
@NotNull Collection<@NotNull PlayerSnapshot> viewers();
@NotNull Collection<@NotNull EntitySnapshot> passengers();
@Nullable EntitySnapshot vehicle();
}

View File

@ -0,0 +1,47 @@
package net.minestom.server.snapshot;
import net.minestom.server.coordinate.Point;
import net.minestom.server.instance.block.Block;
import net.minestom.server.tag.TagReadable;
import net.minestom.server.world.DimensionType;
import net.minestom.server.world.biomes.Biome;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Collection;
import java.util.Objects;
import static net.minestom.server.utils.chunk.ChunkUtils.getChunkCoordinate;
public interface InstanceSnapshot extends Snapshot, Block.Getter, Biome.Getter, TagReadable {
@NotNull DimensionType dimensionType();
long worldAge();
long time();
@Override
default @UnknownNullability Block getBlock(int x, int y, int z, @NotNull Condition condition) {
ChunkSnapshot chunk = chunk(getChunkCoordinate(x), getChunkCoordinate(z));
return Objects.requireNonNull(chunk).getBlock(x, y, z, condition);
}
@Override
default @NotNull Biome getBiome(int x, int y, int z) {
ChunkSnapshot chunk = chunk(getChunkCoordinate(x), getChunkCoordinate(z));
return Objects.requireNonNull(chunk).getBiome(x, y, z);
}
@Nullable ChunkSnapshot chunk(int chunkX, int chunkZ);
default @Nullable ChunkSnapshot chunkAt(@NotNull Point point) {
return chunk(point.chunkX(), point.chunkZ());
}
@NotNull Collection<@NotNull ChunkSnapshot> chunks();
@NotNull Collection<@NotNull EntitySnapshot> entities();
@NotNull ServerSnapshot server();
}

View File

@ -0,0 +1,10 @@
package net.minestom.server.snapshot;
import net.minestom.server.entity.GameMode;
import org.jetbrains.annotations.NotNull;
public interface PlayerSnapshot extends EntitySnapshot {
@NotNull String username();
@NotNull GameMode gameMode();
}

View File

@ -0,0 +1,26 @@
package net.minestom.server.snapshot;
import net.minestom.server.MinecraftServer;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.UnknownNullability;
import java.util.Collection;
/**
* Represents the complete state of the server at a given moment.
*/
public interface ServerSnapshot extends Snapshot {
@NotNull Collection<@NotNull InstanceSnapshot> instances();
@NotNull Collection<EntitySnapshot> entities();
@UnknownNullability EntitySnapshot entity(int id);
@ApiStatus.Experimental
static ServerSnapshot update() {
return SnapshotUpdater.update(MinecraftServer.process());
}
}

View File

@ -0,0 +1,12 @@
package net.minestom.server.snapshot;
import org.jetbrains.annotations.ApiStatus;
/**
* Represents a snapshot of a game object.
* <p>
* Implementations must be valued-based (immutable and not relying on identity).
*/
@ApiStatus.Experimental
public interface Snapshot {
}

View File

@ -0,0 +1,70 @@
package net.minestom.server.snapshot;
import it.unimi.dsi.fastutil.ints.Int2ObjectOpenHashMap;
import it.unimi.dsi.fastutil.longs.Long2ObjectOpenHashMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
import java.util.function.ToIntFunction;
import java.util.function.ToLongFunction;
import java.util.stream.Collectors;
/**
* Represents the context of a snapshot build.
* Used in {@link Snapshotable#updateSnapshot(SnapshotUpdater)} to create snapshot references and avoid circular dependencies.
* Updaters must never leave scope, as its data may be state related (change according to the currently processed snapshot).
* <p>
* Implementations do not need to be thread-safe and cannot be re-used.
*/
@ApiStatus.Experimental
public sealed interface SnapshotUpdater permits SnapshotUpdaterImpl {
/**
* Updates the snapshot of the given snapshotable.
* <p>
* Method must be called during a safe-point (when the server state is stable).
*
* @param snapshotable the snapshot container
* @param <T> the snapshot type
* @return the new updated snapshot
*/
static <T extends Snapshot> @NotNull T update(@NotNull Snapshotable snapshotable) {
return SnapshotUpdaterImpl.update(snapshotable);
}
<T extends Snapshot> @NotNull AtomicReference<T> reference(@NotNull Snapshotable snapshotable);
@Contract("!null -> !null")
default <T extends Snapshot> AtomicReference<T> optionalReference(Snapshotable snapshotable) {
return snapshotable != null ? reference(snapshotable) : null;
}
default <T extends Snapshot, S extends Snapshotable, K> @NotNull Map<K, AtomicReference<T>> referencesMap(@NotNull Collection<S> snapshotables,
@NotNull Function<S, K> mappingFunction) {
return snapshotables.stream().collect(Collectors.toUnmodifiableMap(mappingFunction, this::reference));
}
default <T extends Snapshot, S extends Snapshotable> @NotNull Map<Long, AtomicReference<T>> referencesMapLong(@NotNull Collection<S> snapshotables,
@NotNull ToLongFunction<S> mappingFunction) {
Long2ObjectOpenHashMap<AtomicReference<T>> map = new Long2ObjectOpenHashMap<>(snapshotables.size());
for (S snapshotable : snapshotables) {
map.put(mappingFunction.applyAsLong(snapshotable), reference(snapshotable));
}
map.trim();
return map;
}
default <T extends Snapshot, S extends Snapshotable> @NotNull Map<Integer, AtomicReference<T>> referencesMapInt(@NotNull Collection<S> snapshotables,
@NotNull ToIntFunction<S> mappingFunction) {
Int2ObjectOpenHashMap<AtomicReference<T>> map = new Int2ObjectOpenHashMap<>(snapshotables.size());
for (S snapshotable : snapshotables) {
map.put(mappingFunction.applyAsInt(snapshotable), reference(snapshotable));
}
map.trim();
return map;
}
}

View File

@ -0,0 +1,51 @@
package net.minestom.server.snapshot;
import org.jetbrains.annotations.NotNull;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicReference;
final class SnapshotUpdaterImpl implements SnapshotUpdater {
private final Map<Snapshotable, AtomicReference<Snapshot>> referenceMap = new ConcurrentHashMap<>();
private List<Entry> queue = new ArrayList<>();
static <T extends Snapshot> @NotNull T update(@NotNull Snapshotable snapshotable) {
var updater = new SnapshotUpdaterImpl();
var ref = updater.reference(snapshotable);
updater.update();
return (T) ref.getPlain();
}
@Override
public <T extends Snapshot> @NotNull AtomicReference<T> reference(@NotNull Snapshotable snapshotable) {
AtomicReference<Snapshot> ref = new AtomicReference<>();
var prev = referenceMap.putIfAbsent(snapshotable, ref);
if (prev == null) {
synchronized (this) {
queue.add(new Entry(snapshotable, ref));
}
return (AtomicReference<T>) ref;
}
return (AtomicReference<T>) prev;
}
record Entry(Snapshotable snapshotable, AtomicReference<Snapshot> ref) {
}
void update() {
List<Entry> temp;
while (!(temp = new ArrayList<>(queue)).isEmpty()) {
queue = new ArrayList<>();
temp.parallelStream().forEach(entry -> {
Snapshotable snap = entry.snapshotable;
entry.ref.setPlain(Objects.requireNonNull(snap.updateSnapshot(this), "Snapshot must not be null after an update!"));
});
}
}
}

View File

@ -0,0 +1,26 @@
package net.minestom.server.snapshot;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
/**
* Represents an object which is regularly saved into a snapshot.
*/
@ApiStatus.Experimental
public interface Snapshotable {
/**
* Updates the currently cached snapshot if required.
* The updater can be used to retrieve references to other snapshots while avoiding circular dependency.
* Be careful to do not store {@code updater} anywhere as its data will change when building requested references.
* <p>
* This method is not thread-safe, and targeted at internal use
* since its execution rely on safe-points (e.g. end of ticks)
*
* @param updater the snapshot updater/context
* @return the updated snapshot
*/
default @NotNull Snapshot updateSnapshot(@NotNull SnapshotUpdater updater) {
throw new UnsupportedOperationException("Snapshot is not supported for this object");
}
}

View File

@ -4,7 +4,9 @@ import it.unimi.dsi.fastutil.objects.Object2ObjectArrayMap;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Map;
import java.util.function.ToIntFunction;
@ApiStatus.Internal
public final class ArrayUtils {
@ -26,6 +28,19 @@ public final class ArrayUtils {
return result;
}
public static <T> int[] mapToIntArray(Collection<T> collection, ToIntFunction<T> function) {
final int size = collection.size();
if (size == 0)
return new int[0];
int[] result = new int[size];
int i = 0;
for (T object : collection) {
result[i++] = function.applyAsInt(object);
}
assert i == size;
return result;
}
public static <K, V> Map<K, V> toMap(@NotNull K[] keys, @NotNull V[] values, int length) {
assert keys.length >= length && keys.length == values.length;
return switch (length) {

View File

@ -0,0 +1,30 @@
package net.minestom.server.utils.collection;
import org.jetbrains.annotations.ApiStatus;
import java.util.AbstractList;
import java.util.function.IntFunction;
@ApiStatus.Internal
public final class IntMappedArray<R> extends AbstractList<R> {
private final int[] elements;
private final IntFunction<R> function;
public IntMappedArray(int[] elements, IntFunction<R> function) {
this.elements = elements;
this.function = function;
}
@Override
public R get(int index) {
final int[] elements = this.elements;
if (index < 0 || index >= elements.length)
throw new IndexOutOfBoundsException("Index " + index + " is out of bounds for length " + elements.length);
return function.apply(elements[index]);
}
@Override
public int size() {
return elements.length;
}
}

View File

@ -0,0 +1,102 @@
package net.minestom.server.utils.collection;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.NotNull;
import java.util.Collection;
import java.util.Iterator;
import java.util.concurrent.atomic.AtomicReference;
import java.util.function.Function;
@ApiStatus.Internal
public record MappedCollection<O, R>(@NotNull Collection<O> original,
@NotNull Function<O, R> mapper) implements Collection<R> {
public static <O extends AtomicReference<R>, R> MappedCollection<O, R> plainReferences(@NotNull Collection<O> original) {
return new MappedCollection<>(original, AtomicReference::getPlain);
}
@Override
public int size() {
return original.size();
}
@Override
public boolean isEmpty() {
return original.isEmpty();
}
@Override
public boolean contains(Object o) {
for (var entry : original) {
if (mapper.apply(entry).equals(o)) return true;
}
return false;
}
@Override
public @NotNull Iterator<R> iterator() {
var iterator = original.iterator();
return new Iterator<>() {
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public R next() {
return mapper.apply(iterator.next());
}
};
}
@Override
public @NotNull Object @NotNull [] toArray() {
// TODO
throw new UnsupportedOperationException("Unsupported array object");
}
@Override
public <T> @NotNull T @NotNull [] toArray(@NotNull T @NotNull [] a) {
// TODO
throw new UnsupportedOperationException("Unsupported array generic");
}
@Override
public boolean containsAll(@NotNull Collection<?> c) {
if (c.size() > original.size()) return false;
for (var entry : c) {
if (!contains(entry)) return false;
}
return true;
}
@Override
public boolean add(R t) {
throw new UnsupportedOperationException("Unmodifiable collection");
}
@Override
public boolean remove(Object o) {
throw new UnsupportedOperationException("Unmodifiable collection");
}
@Override
public boolean addAll(@NotNull Collection<? extends R> c) {
throw new UnsupportedOperationException("Unmodifiable collection");
}
@Override
public boolean removeAll(@NotNull Collection<?> c) {
throw new UnsupportedOperationException("Unmodifiable collection");
}
@Override
public boolean retainAll(@NotNull Collection<?> c) {
throw new UnsupportedOperationException("Unmodifiable collection");
}
@Override
public void clear() {
throw new UnsupportedOperationException("Unmodifiable collection");
}
}

View File

@ -0,0 +1,80 @@
package net.minestom.server.snapshot;
import net.minestom.server.api.Env;
import net.minestom.server.api.EnvTest;
import net.minestom.server.entity.Entity;
import net.minestom.server.entity.EntityType;
import net.minestom.server.instance.block.Block;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
@EnvTest
public class SnapshotIntegrationTest {
@Test
public void instance(Env env) {
env.createFlatInstance();
var snapshot = ServerSnapshot.update();
// Ensure that the collection is immutable
{
var instances = snapshot.instances();
assertEquals(1, instances.size());
env.createFlatInstance();
instances = snapshot.instances();
assertEquals(1, instances.size());
}
var inst = snapshot.instances().iterator().next();
assertEquals(snapshot, inst.server(), "Instance must have access to the server snapshot");
assertEquals(0, inst.time());
assertEquals(0, inst.worldAge());
assertEquals(0, inst.chunks().size());
assertEquals(0, inst.entities().size());
}
@Test
public void chunk(Env env) {
var instance = env.createFlatInstance();
instance.setBlock(0, 0, 0, Block.STONE);
var snapshot = ServerSnapshot.update();
var inst = snapshot.instances().iterator().next();
assertEquals(Block.STONE, inst.getBlock(0, 0, 0));
assertEquals(1, inst.chunks().size());
var chunk = inst.chunks().iterator().next();
assertEquals(Block.STONE, chunk.getBlock(0, 0, 0));
}
@Test
public void entity(Env env) {
var instance = env.createFlatInstance();
var ent = new Entity(EntityType.ZOMBIE);
ent.setInstance(instance).join();
var snapshot = ServerSnapshot.update();
var inst = snapshot.instances().iterator().next();
var entities = inst.entities();
assertEquals(1, entities.size());
var entity = entities.iterator().next();
assertEquals(EntityType.ZOMBIE, entity.type());
assertEquals(ent.getUuid(), entity.uuid());
assertEquals(ent.getEntityId(), entity.id());
assertEquals(ent.getPosition(), entity.position());
assertEquals(ent.getVelocity(), entity.velocity());
assertEquals(inst, entity.instance());
assertEquals(inst.chunkAt(entity.position()), entity.chunk());
assertEquals(ent.getViewers().size(), entity.viewers().size());
assertEquals(ent.getPassengers().size(), entity.passengers().size());
assertNull(ent.getVehicle());
assertNull(entity.vehicle());
}
}