From df0e3b8aa4523876892f2275fa9360a33ad75d0e Mon Sep 17 00:00:00 2001 From: DualFroz Date: Mon, 24 Aug 2026 19:49:48 +0200 Subject: [PATCH] feat(core): add scheduled block-tick queue for delayed updates --- core/include/vf/BlockTick.h | 59 +++++++++++++++++++ core/src/BlockTick.cpp | 112 ++++++++++++++++++++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 core/include/vf/BlockTick.h create mode 100644 core/src/BlockTick.cpp diff --git a/core/include/vf/BlockTick.h b/core/include/vf/BlockTick.h new file mode 100644 index 0000000..a2b658d --- /dev/null +++ b/core/include/vf/BlockTick.h @@ -0,0 +1,59 @@ +#pragma once + +#include +#include +#include +#include "World.h" +#include "Block.h" + +namespace vf { + +struct ScheduledTick { + Vec3i pos; + int dueTick; + uint32_t sequence; +}; + +inline bool isFallingBlock(Block b) { return b == Block::Sand || b == Block::Gravel; } + +inline bool tryGrowGrass(World& world, const Vec3i& pos) { + if (world.blockAt(pos.x, pos.y, pos.z) != Block::Dirt) return false; + if (world.solidAt(pos.x, pos.y + 1, pos.z)) return false; + world.setBlock(pos.x, pos.y, pos.z, Block::Grass); + return true; +} + +using RandomTickHook = std::function; + +class BlockTickScheduler { +public: + explicit BlockTickScheduler(uint32_t seed = 1u, int randomTicksPerChunk = 3); + + void schedule(const Vec3i& pos, int delayTicks); + void cancel(const Vec3i& pos); + bool isScheduled(const Vec3i& pos) const; + + void setRandomTickHook(RandomTickHook hook) { randomTickHook = std::move(hook); } + + std::vector tick(World& world); + + const std::vector& lastRandomTicks() const { return randomTickPositions; } + int currentTick() const { return tickCounter; } + std::size_t pendingCount() const { return pending.size(); } + +private: + uint32_t nextRandom(); + void fireScheduled(std::vector& fired); + void sampleRandomTicks(World& world); + void applyGravity(World& world); + + int tickCounter = 0; + uint32_t rngState; + int randomTicksPerChunk; + uint32_t sequenceCounter = 0; + std::vector pending; + std::vector randomTickPositions; + RandomTickHook randomTickHook; +}; + +} // namespace vf diff --git a/core/src/BlockTick.cpp b/core/src/BlockTick.cpp new file mode 100644 index 0000000..2f8a677 --- /dev/null +++ b/core/src/BlockTick.cpp @@ -0,0 +1,112 @@ +#include "vf/BlockTick.h" + +#include + +namespace vf { + +BlockTickScheduler::BlockTickScheduler(uint32_t seed, int randomTicksPerChunk) + : rngState(seed == 0 ? 1u : seed), randomTicksPerChunk(randomTicksPerChunk) {} + +uint32_t BlockTickScheduler::nextRandom() { + rngState ^= rngState << 13; + rngState ^= rngState >> 17; + rngState ^= rngState << 5; + return rngState; +} + +void BlockTickScheduler::schedule(const Vec3i& pos, int delayTicks) { + int delay = delayTicks < 1 ? 1 : delayTicks; + pending.push_back({pos, tickCounter + delay, sequenceCounter++}); +} + +void BlockTickScheduler::cancel(const Vec3i& pos) { + pending.erase(std::remove_if(pending.begin(), pending.end(), + [&pos](const ScheduledTick& t) { return t.pos == pos; }), + pending.end()); +} + +bool BlockTickScheduler::isScheduled(const Vec3i& pos) const { + for (const ScheduledTick& t : pending) + if (t.pos == pos) return true; + return false; +} + +void BlockTickScheduler::fireScheduled(std::vector& fired) { + std::vector due; + std::vector remaining; + for (const ScheduledTick& t : pending) { + if (t.dueTick <= tickCounter) due.push_back(t); + else remaining.push_back(t); + } + std::sort(due.begin(), due.end(), [](const ScheduledTick& a, const ScheduledTick& b) { + if (a.dueTick != b.dueTick) return a.dueTick < b.dueTick; + return a.sequence < b.sequence; + }); + pending.swap(remaining); + for (const ScheduledTick& t : due) fired.push_back(t.pos); +} + +namespace { + +std::vector sortedChunkCoords(const World& world) { + std::vector coords; + coords.reserve(world.allChunks().size()); + for (const auto& entry : world.allChunks()) coords.push_back(entry.first); + std::sort(coords.begin(), coords.end(), [](const Vec3i& a, const Vec3i& b) { + if (a.x != b.x) return a.x < b.x; + if (a.z != b.z) return a.z < b.z; + return a.y < b.y; + }); + return coords; +} + +} // namespace + +void BlockTickScheduler::sampleRandomTicks(World& world) { + randomTickPositions.clear(); + std::vector chunkCoords = sortedChunkCoords(world); + + for (const Vec3i& chunkCoord : chunkCoords) { + for (int i = 0; i < randomTicksPerChunk; ++i) { + int lx = static_cast(nextRandom() % chunkSize); + int ly = static_cast(nextRandom() % chunkSize); + int lz = static_cast(nextRandom() % chunkSize); + Vec3i pos{chunkCoord.x * chunkSize + lx, chunkCoord.y * chunkSize + ly, + chunkCoord.z * chunkSize + lz}; + randomTickPositions.push_back(pos); + if (randomTickHook) randomTickHook(world, pos, world.blockAt(pos.x, pos.y, pos.z)); + } + } +} + +void BlockTickScheduler::applyGravity(World& world) { + std::vector chunkCoords = sortedChunkCoords(world); + + for (const Vec3i& chunkCoord : chunkCoords) { + for (int ly = 0; ly < chunkSize; ++ly) { + for (int lz = 0; lz < chunkSize; ++lz) { + for (int lx = 0; lx < chunkSize; ++lx) { + int gx = chunkCoord.x * chunkSize + lx; + int gy = chunkCoord.y * chunkSize + ly; + int gz = chunkCoord.z * chunkSize + lz; + Block b = world.blockAt(gx, gy, gz); + if (!isFallingBlock(b)) continue; + if (world.blockAt(gx, gy - 1, gz) != Block::Air) continue; + world.setBlock(gx, gy, gz, Block::Air); + world.setBlock(gx, gy - 1, gz, b); + } + } + } + } +} + +std::vector BlockTickScheduler::tick(World& world) { + ++tickCounter; + std::vector fired; + fireScheduled(fired); + sampleRandomTicks(world); + applyGravity(world); + return fired; +} + +} // namespace vf