From f8e763c0d62f625ed54c204910c124b7bb09cc1e Mon Sep 17 00:00:00 2001 From: OLEGSHA Date: Tue, 1 Dec 2020 22:37:18 +0300 Subject: [PATCH] Fixed #3 - implemented random ticks, refactored Ticking* interfaces - Random ticks now happen - Ticking* interfaces now have a single getTickingPolicy method instead of two doesTick* methods. This makes more sense since an object cannot both tick randomly and regularly. - Added Server.stop - Added some convenience methods related to setting blocks and tiles - Documented some stuff --- .../common/world/block/BlockFace.java | 23 +++- .../ru/windcorp/progressia/server/Server.java | 55 ++++++++- .../progressia/server/ServerThread.java | 10 ++ .../progressia/server/TickingSettings.java | 17 +++ .../progressia/server/world/ChunkLogic.java | 5 +- .../server/world/block/BlockTickContext.java | 12 ++ .../server/world/block/TickableBlock.java | 10 +- .../server/world/tasks/TickChunk.java | 116 +++++++++++++++++- .../server/world/tasks/WorldAccessor.java | 10 ++ .../server/world/ticking/TickingPolicy.java | 36 ++++++ .../server/world/tile/TickableTile.java | 10 +- 11 files changed, 280 insertions(+), 24 deletions(-) create mode 100644 src/main/java/ru/windcorp/progressia/server/TickingSettings.java create mode 100644 src/main/java/ru/windcorp/progressia/server/world/ticking/TickingPolicy.java diff --git a/src/main/java/ru/windcorp/progressia/common/world/block/BlockFace.java b/src/main/java/ru/windcorp/progressia/common/world/block/BlockFace.java index 7fa3d26..e0aa6e1 100644 --- a/src/main/java/ru/windcorp/progressia/common/world/block/BlockFace.java +++ b/src/main/java/ru/windcorp/progressia/common/world/block/BlockFace.java @@ -35,10 +35,21 @@ public final class BlockFace extends BlockRelation { private static final ImmutableList ALL_FACES = ImmutableList.of(TOP, BOTTOM, NORTH, SOUTH, WEST, EAST); + static { + link(TOP, BOTTOM); + link(NORTH, SOUTH); + link(WEST, EAST); + } + private static final ImmutableList PRIMARY_FACES = - ImmutableList.of(TOP, NORTH, WEST); + ALL_FACES.stream().filter(BlockFace::isPrimary).collect(ImmutableList.toImmutableList()); + + private static final ImmutableList SECONDARY_FACES = + ALL_FACES.stream().filter(BlockFace::isSecondary).collect(ImmutableList.toImmutableList()); public static final int BLOCK_FACE_COUNT = ALL_FACES.size(); + public static final int PRIMARY_BLOCK_FACE_COUNT = PRIMARY_FACES.size(); + public static final int SECONDARY_BLOCK_FACE_COUNT = SECONDARY_FACES.size(); public static ImmutableList getFaces() { return ALL_FACES; @@ -48,10 +59,8 @@ public final class BlockFace extends BlockRelation { return PRIMARY_FACES; } - static { - link(TOP, BOTTOM); - link(NORTH, SOUTH); - link(WEST, EAST); + public static ImmutableList getSecondaryFaces() { + return SECONDARY_FACES; } private static void link(BlockFace a, BlockFace b) { @@ -108,6 +117,10 @@ public final class BlockFace extends BlockRelation { return counterFace; } + public boolean isSecondary() { + return !isPrimary; + } + public BlockFace getSecondary() { if (isPrimary) return counterFace; else return this; diff --git a/src/main/java/ru/windcorp/progressia/server/Server.java b/src/main/java/ru/windcorp/progressia/server/Server.java index 67d1239..4a5352c 100644 --- a/src/main/java/ru/windcorp/progressia/server/Server.java +++ b/src/main/java/ru/windcorp/progressia/server/Server.java @@ -5,6 +5,8 @@ import java.util.Collection; import java.util.Collections; import java.util.function.Consumer; +import org.apache.logging.log4j.LogManager; + import ru.windcorp.jputil.functions.ThrowingRunnable; import ru.windcorp.progressia.common.util.TaskQueue; import ru.windcorp.progressia.common.world.WorldData; @@ -16,6 +18,10 @@ import ru.windcorp.progressia.server.world.ticking.Evaluation; public class Server { + /** + * Returns the {@link Server} instance whose main thread is the current thread. + * @return the server that operates in this thread + */ public static Server getCurrentServer() { return ServerThread.getCurrentServer(); } @@ -30,6 +36,8 @@ public class Server { private final TaskQueue taskQueue = new TaskQueue(this::isServerThread); private final Collection> repeatingTasks = Collections.synchronizedCollection(new ArrayList<>()); + private final TickingSettings tickingSettings = new TickingSettings(); + public Server(WorldData world) { this.world = new WorldLogic(world, this); this.serverThread = new ServerThread(this); @@ -37,6 +45,10 @@ public class Server { invokeEveryTick(this::scheduleChunkTicks); } + /** + * Returns this server's world. + * @return this server's {@link WorldLogic} + */ public WorldLogic getWorld() { return world; } @@ -110,29 +122,70 @@ public class Server { serverThread.getTicker().requestEvaluation(evaluation); } + /** + * Returns the duration of the last server tick. Server logic should assume that this much in-world time has passed. + * @return the length of the last server tick + */ public double getTickLength() { return this.serverThread.getTicker().getTickLength(); } + /** + * Returns the {@link WorldAccessor} object for this server. Use the provided accessor to + * request common {@link Evaluation}s and {@link Change}s. + * @return a {@link WorldAccessor} + * @see #requestChange(Change) + * @see #requestEvaluation(Evaluation) + */ public WorldAccessor getWorldAccessor() { return worldAccessor; } + + /** + * Returns the ticking settings for this server. + * @return a {@link TickingSettings} object + */ + public TickingSettings getTickingSettings() { + return tickingSettings; + } + /** + * Starts the server. This method blocks until the server enters normal operation or fails to start. + */ public void start() { this.serverThread.start(); } + /** + * Performs the tasks from tasks queues and repeating tasks. + */ public void tick() { taskQueue.runTasks(); repeatingTasks.forEach(t -> t.accept(this)); } + /** + * Shuts the server down, disconnecting the clients with the provided message. + * This method blocks until the shutdown is complete. + * @param message the message to send to the clients as the disconnect reason + */ public void shutdown(String message) { - // Do nothing + LogManager.getLogger().warn("Server.shutdown() is not yet implemented"); + serverThread.stop(); } private void scheduleChunkTicks(Server server) { server.getWorld().getChunks().forEach(chunk -> requestEvaluation(chunk.getTickTask())); } + /** + * Returns an instance of {@link java.util.Random Random} that can be used as a source of indeterministic + * randomness. World generation and other algorithms that must have random but reproducible results should + * not use this. + * @return a thread-safe indeterministic instance of {@link java.util.Random}. + */ + public java.util.Random getAdHocRandom() { + return java.util.concurrent.ThreadLocalRandom.current(); + } + } diff --git a/src/main/java/ru/windcorp/progressia/server/ServerThread.java b/src/main/java/ru/windcorp/progressia/server/ServerThread.java index e28c63a..6350b81 100644 --- a/src/main/java/ru/windcorp/progressia/server/ServerThread.java +++ b/src/main/java/ru/windcorp/progressia/server/ServerThread.java @@ -59,6 +59,16 @@ public class ServerThread implements Runnable { LogManager.getLogger(getClass()).error("Got an exception in server thread", e); } } + + public void stop() { + try { + executor.awaitTermination(10, TimeUnit.MINUTES); + } catch (InterruptedException e) { + LogManager.getLogger().warn("Received interrupt in ServerThread.stop(), aborting wait"); + } + + getTicker().stop(); + } public Server getServer() { return server; diff --git a/src/main/java/ru/windcorp/progressia/server/TickingSettings.java b/src/main/java/ru/windcorp/progressia/server/TickingSettings.java new file mode 100644 index 0000000..0ac8777 --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/TickingSettings.java @@ -0,0 +1,17 @@ +package ru.windcorp.progressia.server; + +import ru.windcorp.progressia.common.Units; + +public class TickingSettings { + + private float randomTickFrequency = Units.get("1 min^-1"); + + /** + * Returns the average rate of random ticks in a single block. + * @return ticking frequency + */ + public float getRandomTickFrequency() { + return randomTickFrequency; + } + +} diff --git a/src/main/java/ru/windcorp/progressia/server/world/ChunkLogic.java b/src/main/java/ru/windcorp/progressia/server/world/ChunkLogic.java index d4f54d5..d51a4b2 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/ChunkLogic.java +++ b/src/main/java/ru/windcorp/progressia/server/world/ChunkLogic.java @@ -23,6 +23,7 @@ import ru.windcorp.progressia.server.world.block.TickableBlock; import ru.windcorp.progressia.server.world.entity.EntityLogic; import ru.windcorp.progressia.server.world.entity.EntityLogicRegistry; import ru.windcorp.progressia.server.world.tasks.TickChunk; +import ru.windcorp.progressia.server.world.ticking.TickingPolicy; import ru.windcorp.progressia.server.world.tile.TickableTile; import ru.windcorp.progressia.server.world.tile.TileLogic; import ru.windcorp.progressia.server.world.tile.TileLogicRegistry; @@ -63,7 +64,7 @@ public class ChunkLogic { Coordinates.getInWorld(getData().getPosition(), blockInChunk, null) ); - if (((TickableBlock) block).doesTickRegularly(blockTickContext)) { + if (((TickableBlock) block).getTickingPolicy(blockTickContext) == TickingPolicy.REGULAR) { tickingBlocks.add(new Vec3i(blockInChunk)); } } @@ -80,7 +81,7 @@ public class ChunkLogic { loc.layer ); - if (((TickableTile) tile).doesTickRegularly(tileTickContext)) { + if (((TickableTile) tile).getTickingPolicy(tileTickContext) == TickingPolicy.REGULAR) { tickingTiles.add(new TileLocation(loc)); } } diff --git a/src/main/java/ru/windcorp/progressia/server/world/block/BlockTickContext.java b/src/main/java/ru/windcorp/progressia/server/world/block/BlockTickContext.java index 128cd1c..d537b60 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/block/BlockTickContext.java +++ b/src/main/java/ru/windcorp/progressia/server/world/block/BlockTickContext.java @@ -20,4 +20,16 @@ public interface BlockTickContext extends ChunkTickContext { return getWorldData().getBlock(getBlockInWorld()); } + /* + * Convenience methods - changes + */ + + default void setThisBlock(BlockData block) { + getAccessor().setBlock(getBlockInWorld(), block); + } + + default void setThisBlock(String id) { + getAccessor().setBlock(getBlockInWorld(), id); + } + } diff --git a/src/main/java/ru/windcorp/progressia/server/world/block/TickableBlock.java b/src/main/java/ru/windcorp/progressia/server/world/block/TickableBlock.java index 603aa3b..fb804b0 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/block/TickableBlock.java +++ b/src/main/java/ru/windcorp/progressia/server/world/block/TickableBlock.java @@ -1,15 +1,11 @@ package ru.windcorp.progressia.server.world.block; +import ru.windcorp.progressia.server.world.ticking.TickingPolicy; + public interface TickableBlock { void tick(BlockTickContext context); - default boolean doesTickRegularly(BlockTickContext context) { - return false; - } - - default boolean doesTickRandomly(BlockTickContext context) { - return false; - } + TickingPolicy getTickingPolicy(BlockTickContext context); } \ No newline at end of file diff --git a/src/main/java/ru/windcorp/progressia/server/world/tasks/TickChunk.java b/src/main/java/ru/windcorp/progressia/server/world/tasks/TickChunk.java index e7b1f60..8975894 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/tasks/TickChunk.java +++ b/src/main/java/ru/windcorp/progressia/server/world/tasks/TickChunk.java @@ -1,20 +1,49 @@ package ru.windcorp.progressia.server.world.tasks; +import java.util.List; +import java.util.Random; +import java.util.function.Consumer; + +import com.google.common.collect.ImmutableList; + import glm.vec._3.i.Vec3i; +import ru.windcorp.progressia.common.util.FloatMathUtils; +import ru.windcorp.progressia.common.world.ChunkData; import ru.windcorp.progressia.common.world.Coordinates; +import ru.windcorp.progressia.common.world.block.BlockFace; +import ru.windcorp.progressia.common.world.tile.TileData; import ru.windcorp.progressia.server.Server; import ru.windcorp.progressia.server.world.ChunkLogic; import ru.windcorp.progressia.server.world.MutableBlockTickContext; import ru.windcorp.progressia.server.world.MutableTileTickContext; import ru.windcorp.progressia.server.world.TickAndUpdateUtil; +import ru.windcorp.progressia.server.world.block.BlockLogic; +import ru.windcorp.progressia.server.world.block.BlockTickContext; import ru.windcorp.progressia.server.world.block.TickableBlock; import ru.windcorp.progressia.server.world.ticking.Evaluation; +import ru.windcorp.progressia.server.world.ticking.TickingPolicy; import ru.windcorp.progressia.server.world.tile.TickableTile; +import ru.windcorp.progressia.server.world.tile.TileLogic; +import ru.windcorp.progressia.server.world.tile.TileLogicRegistry; + +import static ru.windcorp.progressia.common.world.ChunkData.BLOCKS_PER_CHUNK; public class TickChunk extends Evaluation { + private static final int CHUNK_VOLUME = + ChunkData.BLOCKS_PER_CHUNK * + ChunkData.BLOCKS_PER_CHUNK * + ChunkData.BLOCKS_PER_CHUNK; + + private final List> randomTickMethods = ImmutableList.of( + s -> this.tickRandomBlock(s), + s -> this.tickRandomTile(s, BlockFace.NORTH), + s -> this.tickRandomTile(s, BlockFace.TOP), + s -> this.tickRandomTile(s, BlockFace.WEST) + ); + private final ChunkLogic chunk; - + public TickChunk(ChunkLogic chunk) { this.chunk = chunk; } @@ -62,7 +91,90 @@ public class TickChunk extends Evaluation { } private void tickRandom(Server server) { - // TODO Implement + float ticks = computeRandomTicks(server); + + /* + * If we are expected to run 3.25 random ticks per tick + * on average, then run 3 random ticks unconditionally + * and run one extra random tick with 0.25 chance + */ + float unconditionalTicks = FloatMathUtils.floor(ticks); + float extraTickChance = ticks - unconditionalTicks; + + for (int i = 0; i < unconditionalTicks; ++i) { + tickRandomOnce(server); + } + + if (server.getAdHocRandom().nextFloat() < extraTickChance) { + tickRandomOnce(server); + } + } + + private void tickRandomOnce(Server server) { + // Pick a target at random: a block or one of 3 primary block faces + randomTickMethods.get( + server.getAdHocRandom().nextInt(randomTickMethods.size()) + ).accept(server); + } + + private void tickRandomBlock(Server server) { + Random random = server.getAdHocRandom(); + + Vec3i blockInChunk = new Vec3i( + random.nextInt(BLOCKS_PER_CHUNK), + random.nextInt(BLOCKS_PER_CHUNK), + random.nextInt(BLOCKS_PER_CHUNK) + ); + + BlockLogic block = this.chunk.getBlock(blockInChunk); + + if (!(block instanceof TickableBlock)) return; + TickableBlock tickable = (TickableBlock) block; + + BlockTickContext context = TickAndUpdateUtil.getBlockTickContext( + server, + Coordinates.getInWorld(this.chunk.getPosition(), blockInChunk, null) + ); + + if (tickable.getTickingPolicy(context) != TickingPolicy.RANDOM) return; + tickable.tick(context); + } + + private void tickRandomTile(Server server, BlockFace face) { + Random random = server.getAdHocRandom(); + + Vec3i blockInChunk = new Vec3i( + random.nextInt(BLOCKS_PER_CHUNK), + random.nextInt(BLOCKS_PER_CHUNK), + random.nextInt(BLOCKS_PER_CHUNK) + ); + + List tiles = this.chunk.getData().getTilesOrNull(blockInChunk, face); + if (tiles == null) return; + + MutableTileTickContext context = new MutableTileTickContext(); + Vec3i blockInWorld = Coordinates.getInWorld(this.chunk.getPosition(), blockInChunk, null); + + for (int layer = 0; layer < tiles.size(); ++layer) { + TileData data = tiles.get(layer); + + TileLogic logic = TileLogicRegistry.getInstance().get(data.getId()); + if (!(logic instanceof TickableTile)) return; + TickableTile tickable = (TickableTile) logic; + + context.init(server, blockInWorld, face, layer); + + if (tickable.getTickingPolicy(context) != TickingPolicy.RANDOM) return; + tickable.tick(context); + } + } + + private float computeRandomTicks(Server server) { + return (float) ( + server.getTickingSettings().getRandomTickFrequency() * + CHUNK_VOLUME * randomTickMethods.size() * + server.getTickLength() + ); } private void tickEntities(Server server) { diff --git a/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java b/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java index 9f24aff..206d426 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java +++ b/src/main/java/ru/windcorp/progressia/server/world/tasks/WorldAccessor.java @@ -5,9 +5,11 @@ import java.util.function.Consumer; import glm.vec._3.i.Vec3i; import ru.windcorp.progressia.common.util.MultiLOC; import ru.windcorp.progressia.common.world.block.BlockData; +import ru.windcorp.progressia.common.world.block.BlockDataRegistry; import ru.windcorp.progressia.common.world.block.BlockFace; import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.tile.TileData; +import ru.windcorp.progressia.common.world.tile.TileDataRegistry; import ru.windcorp.progressia.server.Server; import ru.windcorp.progressia.server.world.ticking.TickerTask; @@ -38,12 +40,20 @@ public class WorldAccessor { change.initialize(blockInWorld, block); server.requestChange(change); } + + public void setBlock(Vec3i blockInWorld, String id) { + setBlock(blockInWorld, BlockDataRegistry.getInstance().get(id)); + } public void addTile(Vec3i blockInWorld, BlockFace face, TileData tile) { AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class); change.initialize(blockInWorld, face, tile, true); server.requestChange(change); } + + public void addTile(Vec3i blockInWorld, BlockFace face, String id) { + addTile(blockInWorld, face, TileDataRegistry.getInstance().get(id)); + } public void removeTile(Vec3i blockInWorld, BlockFace face, TileData tile) { AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class); diff --git a/src/main/java/ru/windcorp/progressia/server/world/ticking/TickingPolicy.java b/src/main/java/ru/windcorp/progressia/server/world/ticking/TickingPolicy.java new file mode 100644 index 0000000..70e3a6b --- /dev/null +++ b/src/main/java/ru/windcorp/progressia/server/world/ticking/TickingPolicy.java @@ -0,0 +1,36 @@ +package ru.windcorp.progressia.server.world.ticking; + +import ru.windcorp.progressia.server.world.block.TickableBlock; +import ru.windcorp.progressia.server.world.tile.TickableTile; + +/** + * Various ticking policies that {@link TickableBlock} or {@link TickableTile} can have. + * Ticking policy determines when, and if, the block or tile is ticked. + * @author javapony + */ +public enum TickingPolicy { + + /** + * The ticking policy that requests that no ticks happen. + * This is typically used for blocks or tiles that only tick under certain conditions, + * which are not meant at the moment. + */ + NONE, + + /** + * The ticking policy that requests that the object is ticked every server tick exactly once. + * This should not be used for objects that only change rarely; consider using {@link RANDOM} + * instead. + */ + REGULAR, + + /** + * The ticking policy that requests that the object is ticked only once every + *
+	 * Server.getTickingSettings().getRandomTickFrequency()
+ * seconds on average (this value is only determined at runtime). Note that + * the block might sometimes tick more than once per single server tick. + */ + RANDOM; + +} diff --git a/src/main/java/ru/windcorp/progressia/server/world/tile/TickableTile.java b/src/main/java/ru/windcorp/progressia/server/world/tile/TickableTile.java index 2389523..cf49856 100644 --- a/src/main/java/ru/windcorp/progressia/server/world/tile/TickableTile.java +++ b/src/main/java/ru/windcorp/progressia/server/world/tile/TickableTile.java @@ -1,15 +1,11 @@ package ru.windcorp.progressia.server.world.tile; +import ru.windcorp.progressia.server.world.ticking.TickingPolicy; + public interface TickableTile { void tick(TileTickContext context); - default boolean doesTickRegularly(TileTickContext context) { - return false; - } - - default boolean doesTickRandomly(TileTickContext context) { - return false; - } + TickingPolicy getTickingPolicy(TileTickContext context); } \ No newline at end of file