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
This commit is contained in:
OLEGSHA 2020-12-01 22:37:18 +03:00
parent 377241c529
commit f8e763c0d6
11 changed files with 280 additions and 24 deletions

View File

@ -35,10 +35,21 @@ public final class BlockFace extends BlockRelation {
private static final ImmutableList<BlockFace> ALL_FACES = private static final ImmutableList<BlockFace> ALL_FACES =
ImmutableList.of(TOP, BOTTOM, NORTH, SOUTH, WEST, EAST); ImmutableList.of(TOP, BOTTOM, NORTH, SOUTH, WEST, EAST);
static {
link(TOP, BOTTOM);
link(NORTH, SOUTH);
link(WEST, EAST);
}
private static final ImmutableList<BlockFace> PRIMARY_FACES = private static final ImmutableList<BlockFace> PRIMARY_FACES =
ImmutableList.of(TOP, NORTH, WEST); ALL_FACES.stream().filter(BlockFace::isPrimary).collect(ImmutableList.toImmutableList());
private static final ImmutableList<BlockFace> 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 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<BlockFace> getFaces() { public static ImmutableList<BlockFace> getFaces() {
return ALL_FACES; return ALL_FACES;
@ -48,10 +59,8 @@ public final class BlockFace extends BlockRelation {
return PRIMARY_FACES; return PRIMARY_FACES;
} }
static { public static ImmutableList<BlockFace> getSecondaryFaces() {
link(TOP, BOTTOM); return SECONDARY_FACES;
link(NORTH, SOUTH);
link(WEST, EAST);
} }
private static void link(BlockFace a, BlockFace b) { private static void link(BlockFace a, BlockFace b) {
@ -108,6 +117,10 @@ public final class BlockFace extends BlockRelation {
return counterFace; return counterFace;
} }
public boolean isSecondary() {
return !isPrimary;
}
public BlockFace getSecondary() { public BlockFace getSecondary() {
if (isPrimary) return counterFace; if (isPrimary) return counterFace;
else return this; else return this;

View File

@ -5,6 +5,8 @@ import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.function.Consumer; import java.util.function.Consumer;
import org.apache.logging.log4j.LogManager;
import ru.windcorp.jputil.functions.ThrowingRunnable; import ru.windcorp.jputil.functions.ThrowingRunnable;
import ru.windcorp.progressia.common.util.TaskQueue; import ru.windcorp.progressia.common.util.TaskQueue;
import ru.windcorp.progressia.common.world.WorldData; import ru.windcorp.progressia.common.world.WorldData;
@ -16,6 +18,10 @@ import ru.windcorp.progressia.server.world.ticking.Evaluation;
public class Server { 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() { public static Server getCurrentServer() {
return ServerThread.getCurrentServer(); return ServerThread.getCurrentServer();
} }
@ -30,6 +36,8 @@ public class Server {
private final TaskQueue taskQueue = new TaskQueue(this::isServerThread); private final TaskQueue taskQueue = new TaskQueue(this::isServerThread);
private final Collection<Consumer<Server>> repeatingTasks = Collections.synchronizedCollection(new ArrayList<>()); private final Collection<Consumer<Server>> repeatingTasks = Collections.synchronizedCollection(new ArrayList<>());
private final TickingSettings tickingSettings = new TickingSettings();
public Server(WorldData world) { public Server(WorldData world) {
this.world = new WorldLogic(world, this); this.world = new WorldLogic(world, this);
this.serverThread = new ServerThread(this); this.serverThread = new ServerThread(this);
@ -37,6 +45,10 @@ public class Server {
invokeEveryTick(this::scheduleChunkTicks); invokeEveryTick(this::scheduleChunkTicks);
} }
/**
* Returns this server's world.
* @return this server's {@link WorldLogic}
*/
public WorldLogic getWorld() { public WorldLogic getWorld() {
return world; return world;
} }
@ -110,29 +122,70 @@ public class Server {
serverThread.getTicker().requestEvaluation(evaluation); 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() { public double getTickLength() {
return this.serverThread.getTicker().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() { public WorldAccessor getWorldAccessor() {
return worldAccessor; 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() { public void start() {
this.serverThread.start(); this.serverThread.start();
} }
/**
* Performs the tasks from tasks queues and repeating tasks.
*/
public void tick() { public void tick() {
taskQueue.runTasks(); taskQueue.runTasks();
repeatingTasks.forEach(t -> t.accept(this)); 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) { public void shutdown(String message) {
// Do nothing LogManager.getLogger().warn("Server.shutdown() is not yet implemented");
serverThread.stop();
} }
private void scheduleChunkTicks(Server server) { private void scheduleChunkTicks(Server server) {
server.getWorld().getChunks().forEach(chunk -> requestEvaluation(chunk.getTickTask())); 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();
}
} }

View File

@ -59,6 +59,16 @@ public class ServerThread implements Runnable {
LogManager.getLogger(getClass()).error("Got an exception in server thread", e); 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() { public Server getServer() {
return server; return server;

View File

@ -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;
}
}

View File

@ -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.EntityLogic;
import ru.windcorp.progressia.server.world.entity.EntityLogicRegistry; import ru.windcorp.progressia.server.world.entity.EntityLogicRegistry;
import ru.windcorp.progressia.server.world.tasks.TickChunk; 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.TickableTile;
import ru.windcorp.progressia.server.world.tile.TileLogic; import ru.windcorp.progressia.server.world.tile.TileLogic;
import ru.windcorp.progressia.server.world.tile.TileLogicRegistry; import ru.windcorp.progressia.server.world.tile.TileLogicRegistry;
@ -63,7 +64,7 @@ public class ChunkLogic {
Coordinates.getInWorld(getData().getPosition(), blockInChunk, null) Coordinates.getInWorld(getData().getPosition(), blockInChunk, null)
); );
if (((TickableBlock) block).doesTickRegularly(blockTickContext)) { if (((TickableBlock) block).getTickingPolicy(blockTickContext) == TickingPolicy.REGULAR) {
tickingBlocks.add(new Vec3i(blockInChunk)); tickingBlocks.add(new Vec3i(blockInChunk));
} }
} }
@ -80,7 +81,7 @@ public class ChunkLogic {
loc.layer loc.layer
); );
if (((TickableTile) tile).doesTickRegularly(tileTickContext)) { if (((TickableTile) tile).getTickingPolicy(tileTickContext) == TickingPolicy.REGULAR) {
tickingTiles.add(new TileLocation(loc)); tickingTiles.add(new TileLocation(loc));
} }
} }

View File

@ -20,4 +20,16 @@ public interface BlockTickContext extends ChunkTickContext {
return getWorldData().getBlock(getBlockInWorld()); 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);
}
} }

View File

@ -1,15 +1,11 @@
package ru.windcorp.progressia.server.world.block; package ru.windcorp.progressia.server.world.block;
import ru.windcorp.progressia.server.world.ticking.TickingPolicy;
public interface TickableBlock { public interface TickableBlock {
void tick(BlockTickContext context); void tick(BlockTickContext context);
default boolean doesTickRegularly(BlockTickContext context) { TickingPolicy getTickingPolicy(BlockTickContext context);
return false;
}
default boolean doesTickRandomly(BlockTickContext context) {
return false;
}
} }

View File

@ -1,20 +1,49 @@
package ru.windcorp.progressia.server.world.tasks; 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 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.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.Server;
import ru.windcorp.progressia.server.world.ChunkLogic; import ru.windcorp.progressia.server.world.ChunkLogic;
import ru.windcorp.progressia.server.world.MutableBlockTickContext; import ru.windcorp.progressia.server.world.MutableBlockTickContext;
import ru.windcorp.progressia.server.world.MutableTileTickContext; import ru.windcorp.progressia.server.world.MutableTileTickContext;
import ru.windcorp.progressia.server.world.TickAndUpdateUtil; 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.block.TickableBlock;
import ru.windcorp.progressia.server.world.ticking.Evaluation; 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.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 { 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<Consumer<Server>> 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; private final ChunkLogic chunk;
public TickChunk(ChunkLogic chunk) { public TickChunk(ChunkLogic chunk) {
this.chunk = chunk; this.chunk = chunk;
} }
@ -62,7 +91,90 @@ public class TickChunk extends Evaluation {
} }
private void tickRandom(Server server) { 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<TileData> 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) { private void tickEntities(Server server) {

View File

@ -5,9 +5,11 @@ import java.util.function.Consumer;
import glm.vec._3.i.Vec3i; import glm.vec._3.i.Vec3i;
import ru.windcorp.progressia.common.util.MultiLOC; import ru.windcorp.progressia.common.util.MultiLOC;
import ru.windcorp.progressia.common.world.block.BlockData; 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.block.BlockFace;
import ru.windcorp.progressia.common.world.entity.EntityData; import ru.windcorp.progressia.common.world.entity.EntityData;
import ru.windcorp.progressia.common.world.tile.TileData; 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.Server;
import ru.windcorp.progressia.server.world.ticking.TickerTask; import ru.windcorp.progressia.server.world.ticking.TickerTask;
@ -38,12 +40,20 @@ public class WorldAccessor {
change.initialize(blockInWorld, block); change.initialize(blockInWorld, block);
server.requestChange(change); 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) { public void addTile(Vec3i blockInWorld, BlockFace face, TileData tile) {
AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class); AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class);
change.initialize(blockInWorld, face, tile, true); change.initialize(blockInWorld, face, tile, true);
server.requestChange(change); 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) { public void removeTile(Vec3i blockInWorld, BlockFace face, TileData tile) {
AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class); AddOrRemoveTile change = cache.grab(AddOrRemoveTile.class);

View File

@ -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
* <pre>
* Server.getTickingSettings().getRandomTickFrequency()</pre>
* 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;
}

View File

@ -1,15 +1,11 @@
package ru.windcorp.progressia.server.world.tile; package ru.windcorp.progressia.server.world.tile;
import ru.windcorp.progressia.server.world.ticking.TickingPolicy;
public interface TickableTile { public interface TickableTile {
void tick(TileTickContext context); void tick(TileTickContext context);
default boolean doesTickRegularly(TileTickContext context) { TickingPolicy getTickingPolicy(TileTickContext context);
return false;
}
default boolean doesTickRandomly(TileTickContext context) {
return false;
}
} }