"deferred block updates": falling sand and flowing water

This commit is contained in:
2025-09-27 17:47:28 +02:00
parent 90a7a8b48e
commit 7e7a98bd41
6 changed files with 226 additions and 80 deletions

View File

@@ -34,8 +34,8 @@
// Max render distance, determines how many chunks to send // Max render distance, determines how many chunks to send
#define VIEW_DISTANCE 2 #define VIEW_DISTANCE 2
// Time between server ticks in microseconds (default = 1s) // Time between server ticks in microseconds (default = 0.05s)
#define TIME_BETWEEN_TICKS 1000000 #define TIME_BETWEEN_TICKS 50000
// Calculated from TIME_BETWEEN_TICKS // Calculated from TIME_BETWEEN_TICKS
#define TICKS_PER_SECOND ((float)1000000 / TIME_BETWEEN_TICKS) #define TICKS_PER_SECOND ((float)1000000 / TIME_BETWEEN_TICKS)
@@ -74,6 +74,10 @@
// Determines the fixed amount of memory allocated to blocks // Determines the fixed amount of memory allocated to blocks
#define MAX_BLOCK_CHANGES 20000 #define MAX_BLOCK_CHANGES 20000
// How many "deferred" (happen at a later time than triggered) block updates to store.
// Determines the fixed amount of memory allocated to that list
#define MAX_DEFERRED_BLOCK_UPDATES 64
// If defined, writes and reads world data to/from disk (or flash). // If defined, writes and reads world data to/from disk (or flash).
// This is a synchronous operation, and can cause performance issues if // This is a synchronous operation, and can cause performance issues if
// frequent random disk access is slow. Data is still stored in and // frequent random disk access is slow. Data is still stored in and
@@ -195,6 +199,24 @@ typedef struct {
uint8_t block; uint8_t block;
} BlockChange; } BlockChange;
#define UPDATE_BASIC (1 << 0)
// the sand at this position will be moved down immediately when this is processed
#define UPDATE_FALL_SAND (1 << 1)
#define UPDATE_FLOW_WATER (1 << 2)
// the sand below this block will fall soon
#define UPDATE_CHECK_SAND_FALL (1 << 3)
#define UPDATE_CHECK_WATER (1 << 4)
#define UPDATE_NOW (UPDATE_BASIC | UPDATE_CHECK_SAND_FALL | UPDATE_CHECK_WATER)
typedef struct {
short update_kind;
short x;
short z;
uint8_t y;
uint8_t await_ticks;
} DeferredBlockUpdate;
#pragma pack(push, 1) #pragma pack(push, 1)
typedef struct { typedef struct {
@@ -267,6 +289,10 @@ typedef struct {
extern BlockChange block_changes[MAX_BLOCK_CHANGES]; extern BlockChange block_changes[MAX_BLOCK_CHANGES];
extern int block_changes_count; extern int block_changes_count;
extern DeferredBlockUpdate deferred_block_updates[MAX_DEFERRED_BLOCK_UPDATES];
extern int deferred_block_updates_count;
extern uint8_t is_processing_deferred_block_updates;
extern PlayerData player_data[MAX_PLAYERS]; extern PlayerData player_data[MAX_PLAYERS];
extern int player_data_count; extern int player_data_count;

View File

@@ -32,9 +32,11 @@ uint8_t makeBlockChange (short x, uint8_t y, short z, uint8_t block);
uint8_t isInstantlyMined (PlayerData *player, uint8_t block); uint8_t isInstantlyMined (PlayerData *player, uint8_t block);
uint8_t isColumnBlock (uint8_t block); uint8_t isColumnBlock (uint8_t block);
uint8_t isFallingBlock (uint8_t block);
uint8_t isPassableBlock (uint8_t block); uint8_t isPassableBlock (uint8_t block);
uint8_t isPassableSpawnBlock (uint8_t block); uint8_t isPassableSpawnBlock (uint8_t block);
uint8_t isReplaceableBlock (uint8_t block); uint8_t isReplaceableBlock (uint8_t block);
uint8_t getFluid (uint8_t block);
uint32_t isCompostItem (uint16_t item); uint32_t isCompostItem (uint16_t item);
uint8_t getItemStackSize (uint16_t item); uint8_t getItemStackSize (uint16_t item);
@@ -43,7 +45,10 @@ void bumpToolDurability (PlayerData *player);
void handlePlayerAction (PlayerData *player, int action, short x, short y, short z); void handlePlayerAction (PlayerData *player, int action, short x, short y, short z);
void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face); void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face);
void checkFluidUpdate (short x, uint8_t y, short z, uint8_t block); void processBlockUpdate (short x, uint8_t y, short z, uint8_t block, short update_kind);
void updateXZNeighbors (short x, uint8_t y, short z, short update_kind);
void updateXYZNeighbors (short x, uint8_t y, short z, short update_kind);
void deferBlockUpdate (short x, uint8_t y, short z, uint8_t await_ticks, short update_kind);
void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health); void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health);
void interactEntity (int entity_id, int interactor_id); void interactEntity (int entity_id, int interactor_id);

View File

@@ -50,6 +50,10 @@ uint16_t client_count;
BlockChange block_changes[MAX_BLOCK_CHANGES]; BlockChange block_changes[MAX_BLOCK_CHANGES];
int block_changes_count = 0; int block_changes_count = 0;
DeferredBlockUpdate deferred_block_updates[MAX_DEFERRED_BLOCK_UPDATES];
int deferred_block_updates_count = 0;
uint8_t is_processing_deferred_block_updates = 0;
PlayerData player_data[MAX_PLAYERS]; PlayerData player_data[MAX_PLAYERS];
int player_data_count = 0; int player_data_count = 0;

View File

@@ -595,6 +595,10 @@ int main () {
// Check if it's time to yield to the idle task // Check if it's time to yield to the idle task
task_yield(); task_yield();
if (deferred_block_updates_count == MAX_DEFERRED_BLOCK_UPDATES) {
printf("WARNING: Deferred block update queue maxed out\n");
}
// Attempt to accept a new connection // Attempt to accept a new connection
for (int i = 0; i < MAX_PLAYERS; i ++) { for (int i = 0; i < MAX_PLAYERS; i ++) {
if (clients[i] != -1) continue; if (clients[i] != -1) continue;

View File

@@ -1,3 +1,4 @@
#include <stdint.h>
#include <stdio.h> #include <stdio.h>
#include <stdlib.h> #include <stdlib.h>
#include <string.h> #include <string.h>
@@ -761,6 +762,13 @@ uint8_t isInstantlyMined (PlayerData *player, uint8_t block) {
} }
// Checks whether the given block can fall down (like sand, anvils, ...)
uint8_t isFallingBlock (uint8_t block) {
return (
block == B_sand
);
}
// Checks whether the given block has to have something beneath it // Checks whether the given block has to have something beneath it
uint8_t isColumnBlock (uint8_t block) { uint8_t isColumnBlock (uint8_t block) {
return ( return (
@@ -790,8 +798,7 @@ uint8_t isPassableBlock (uint8_t block) {
} }
// Checks whether the given block is non-solid and spawnable // Checks whether the given block is non-solid and spawnable
uint8_t isPassableSpawnBlock (uint8_t block) { uint8_t isPassableSpawnBlock (uint8_t block) {
if ((block >= B_water && block < B_water + 8) || if (getFluid(block))
(block >= B_lava && block < B_lava + 4))
{ {
return 0; return 0;
} }
@@ -1037,6 +1044,17 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
// a higher fluid "level" means the fluid has traveled farther // a higher fluid "level" means the fluid has traveled farther
uint8_t level = block - fluid; uint8_t level = block - fluid;
uint8_t max_level;
switch (fluid) {
case B_lava:
max_level = 3;
break;
case B_water:
max_level = 7;
break;
}
// Query blocks adjacent to this fluid stream // Query blocks adjacent to this fluid stream
uint8_t adjacent[4] = { uint8_t adjacent[4] = {
getBlockAt(x + 1, y, z), getBlockAt(x + 1, y, z),
@@ -1058,10 +1076,7 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
// If not connected, clear this block and recalculate surrounding flow // If not connected, clear this block and recalculate surrounding flow
if (!connected) { if (!connected) {
makeBlockChange(x, y, z, B_air); makeBlockChange(x, y, z, B_air);
checkFluidUpdate(x + 1, y, z, adjacent[0]); updateXYZNeighbors(x, y, z, UPDATE_NOW);
checkFluidUpdate(x - 1, y, z, adjacent[1]);
checkFluidUpdate(x, y, z + 1, adjacent[2]);
checkFluidUpdate(x, y, z - 1, adjacent[3]);
return; return;
} }
} }
@@ -1070,42 +1085,38 @@ void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t bl
uint8_t block_below = getBlockAt(x, y - 1, z); uint8_t block_below = getBlockAt(x, y - 1, z);
if (isReplaceableBlock(block_below)) { if (isReplaceableBlock(block_below)) {
makeBlockChange(x, y - 1, z, fluid); makeBlockChange(x, y - 1, z, fluid);
return handleFluidMovement(x, y - 1, z, fluid, fluid); updateXYZNeighbors(x, y, z, UPDATE_NOW);
return;
} }
// Stop flowing laterally at the maximum level // Stop flowing laterally at the maximum level
if (level == 3 && fluid == B_lava) return; if (level == max_level) return;
if (level == 7) return;
// Handle lateral water flow, increasing level by 1 // Handle lateral water flow, increasing level by 1
if (isReplaceableFluid(adjacent[0], level, fluid)) { if (isReplaceableFluid(adjacent[0], level, fluid)) {
makeBlockChange(x + 1, y, z, block + 1); makeBlockChange(x + 1, y, z, block + 1);
handleFluidMovement(x + 1, y, z, fluid, block + 1); updateXYZNeighbors(x, y, z, UPDATE_NOW);
} }
if (isReplaceableFluid(adjacent[1], level, fluid)) { if (isReplaceableFluid(adjacent[1], level, fluid)) {
makeBlockChange(x - 1, y, z, block + 1); makeBlockChange(x - 1, y, z, block + 1);
handleFluidMovement(x - 1, y, z, fluid, block + 1); updateXYZNeighbors(x, y, z, UPDATE_NOW);
} }
if (isReplaceableFluid(adjacent[2], level, fluid)) { if (isReplaceableFluid(adjacent[2], level, fluid)) {
makeBlockChange(x, y, z + 1, block + 1); makeBlockChange(x, y, z + 1, block + 1);
handleFluidMovement(x, y, z + 1, fluid, block + 1); updateXYZNeighbors(x, y, z, UPDATE_NOW);
} }
if (isReplaceableFluid(adjacent[3], level, fluid)) { if (isReplaceableFluid(adjacent[3], level, fluid)) {
makeBlockChange(x, y, z - 1, block + 1); makeBlockChange(x, y, z - 1, block + 1);
handleFluidMovement(x, y, z - 1, fluid, block + 1); updateXYZNeighbors(x, y, z, UPDATE_NOW);
} }
} }
void checkFluidUpdate (short x, uint8_t y, short z, uint8_t block) { uint8_t getFluid(uint8_t block) {
if (block >= B_water && block < B_water + 8)
uint8_t fluid; return B_water;
if (block >= B_water && block < B_water + 8) fluid = B_water; else if (block >= B_lava && block < B_lava + 4)
else if (block >= B_lava && block < B_lava + 4) fluid = B_lava; return B_lava;
else return; return 0;
handleFluidMovement(x, y, z, fluid, block);
} }
#ifdef ENABLE_PICKUP_ANIMATION #ifdef ENABLE_PICKUP_ANIMATION
@@ -1143,6 +1154,111 @@ void playPickupAnimation (PlayerData *player, uint16_t item, double x, double y,
} }
#endif #endif
// can not be zero!!
#define SAND_FALL_T 15
#define SAND_FALL_PROPAGATE_T 2
void processBlockUpdate (short x, uint8_t y, short z, uint8_t block, short update_kind) {
if (update_kind & UPDATE_BASIC) {
// "normal" block update
}
if (update_kind & UPDATE_CHECK_WATER) {
uint8_t fluid = getFluid(block);
if (fluid) {
uint8_t flow_speed;
switch (fluid) {
case B_lava:
flow_speed = 30;
break;
case B_water:
flow_speed = 5;
break;
}
deferBlockUpdate(x, y, z, flow_speed, UPDATE_FLOW_WATER);
}
}
if (update_kind & UPDATE_FLOW_WATER) {
uint8_t fluid = getFluid(block);
if (fluid) {
handleFluidMovement(x, y, z, fluid, block);
}
}
if (update_kind & UPDATE_CHECK_SAND_FALL) {
// we have a separate UPDATE_CHECK_SAND_FALL,
// to make chains of falling sand fall as one,
// with less delay between each
if (isFallingBlock(block) && y) {
uint8_t below = getBlockAt(x, y - 1, z);
if (isReplaceableBlock(below)) {
// move the sand down in 15 ticks
deferBlockUpdate(x, y, z, SAND_FALL_T - 1, UPDATE_FALL_SAND);
if (y != 255 && isFallingBlock(getBlockAt(x, y + 1, z))) {
// also tell the block above that the sand will be gone soon
deferBlockUpdate(x, y + 1, z, SAND_FALL_PROPAGATE_T, UPDATE_CHECK_SAND_FALL);
}
}
}
}
if (update_kind & UPDATE_FALL_SAND) {
// make sure that is a fallable:tm: block, and we are not in the floor
if (isFallingBlock(block) && y) {
uint8_t below = getBlockAt(x, y - 1, z);
// TODO: what to do if below block breaks sand
if (isReplaceableBlock(below)) {
// TODO: drop item of below block
makeBlockChange(x, y, z, B_air);
makeBlockChange(x, y - 1, z, B_air);
makeBlockChange(x, y - 1, z, block);
// update this block after moved
processBlockUpdate (x, y - 1, z, block, UPDATE_NOW);
// also update the neighbors
updateXZNeighbors(x, y, z, UPDATE_NOW);
updateXZNeighbors(x, y - 1, z, UPDATE_NOW);
if (y != 255) {
processBlockUpdate(x, y + 1, z, getBlockAt(x, y + 1, z), UPDATE_NOW);
}
}
}
}
}
void updateXZNeighbors (short x, uint8_t y, short z, short update_kind) {
processBlockUpdate(x - 1, y, z, getBlockAt(x - 1, y, z), update_kind);
processBlockUpdate(x + 1, y, z, getBlockAt(x + 1, y, z), update_kind);
processBlockUpdate(x, y, z - 1, getBlockAt(x, y, z - 1), update_kind);
processBlockUpdate(x, y, z + 1, getBlockAt(x, y, z + 1), update_kind);
}
void updateXYZNeighbors (short x, uint8_t y, short z, short update_kind) {
processBlockUpdate(x - 1, y, z, getBlockAt(x - 1, y, z), update_kind);
processBlockUpdate(x + 1, y, z, getBlockAt(x + 1, y, z), update_kind);
processBlockUpdate(x, y, z - 1, getBlockAt(x, y, z - 1), update_kind);
processBlockUpdate(x, y, z + 1, getBlockAt(x, y, z + 1), update_kind);
processBlockUpdate(x, y - 1, z, getBlockAt(x, y - 1, z), update_kind);
processBlockUpdate(x, y + 1, z, getBlockAt(x, y + 1, z), update_kind);
}
void deferBlockUpdate (short x, uint8_t y, short z, uint8_t await_ticks, short update_kind) {
if (deferred_block_updates_count == MAX_DEFERRED_BLOCK_UPDATES) {
return;
}
DeferredBlockUpdate *u = &deferred_block_updates[deferred_block_updates_count ++];
u->x = x;
u->y = y;
u->z = z;
u->await_ticks = await_ticks + is_processing_deferred_block_updates;
u->update_kind = update_kind;
}
void handlePlayerAction (PlayerData *player, int action, short x, short y, short z) { void handlePlayerAction (PlayerData *player, int action, short x, short y, short z) {
// Re-sync slot when player drops an item // Re-sync slot when player drops an item
@@ -1166,55 +1282,34 @@ void handlePlayerAction (PlayerData *player, int action, short x, short y, short
// Ignore further actions not pertaining to mining blocks // Ignore further actions not pertaining to mining blocks
if (action != 0 && action != 2) return; if (action != 0 && action != 2) return;
uint8_t block = getBlockAt(x, y, z);
// In creative, only the "start mining" action is sent // In creative, only the "start mining" action is sent
// No additional verification is performed, the block is simply removed // No additional verification is performed, the block is simply removed
if (action == 0 && GAMEMODE == 1) { if (action == 0 && GAMEMODE == 1) {
makeBlockChange(x, y, z, 0); makeBlockChange(x, y, z, B_air);
return; }
else {
// If this is a "start mining" packet, the block must be instamine
if (action == 0 && !isInstantlyMined(player, block)) return;
// Don't continue if the block change failed
if (makeBlockChange(x, y, z, B_air)) return;
uint16_t held_item = player->inventory_items[player->hotbar];
uint16_t item = getMiningResult(held_item, block);
bumpToolDurability(player);
if (item) {
#ifdef ENABLE_PICKUP_ANIMATION
playPickupAnimation(player, item, x, y, z);
#endif
givePlayerItem(player, item, 1);
}
} }
uint8_t block = getBlockAt(x, y, z); // Update nearby blocks
updateXYZNeighbors(x, y, z, UPDATE_NOW);
// If this is a "start mining" packet, the block must be instamine
if (action == 0 && !isInstantlyMined(player, block)) return;
// Don't continue if the block change failed
if (makeBlockChange(x, y, z, 0)) return;
uint16_t held_item = player->inventory_items[player->hotbar];
uint16_t item = getMiningResult(held_item, block);
bumpToolDurability(player);
if (item) {
#ifdef ENABLE_PICKUP_ANIMATION
playPickupAnimation(player, item, x, y, z);
#endif
givePlayerItem(player, item, 1);
}
// Update nearby fluids
uint8_t block_above = getBlockAt(x, y + 1, z);
#ifdef DO_FLUID_FLOW
checkFluidUpdate(x, y + 1, z, block_above);
checkFluidUpdate(x - 1, y, z, getBlockAt(x - 1, y, z));
checkFluidUpdate(x + 1, y, z, getBlockAt(x + 1, y, z));
checkFluidUpdate(x, y, z - 1, getBlockAt(x, y, z - 1));
checkFluidUpdate(x, y, z + 1, getBlockAt(x, y, z + 1));
#endif
// Check if any blocks above this should break, and if so,
// iterate upward over all blocks in the column and break them
uint8_t y_offset = 1;
while (isColumnBlock(block_above)) {
// Destroy the next block
makeBlockChange(x, y + y_offset, z, 0);
// Check for item drops *without a tool*
uint16_t item = getMiningResult(0, block_above);
if (item) givePlayerItem(player, item, 1);
// Select the next block in the column
y_offset ++;
block_above = getBlockAt(x, y + y_offset, z);
}
} }
void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face) { void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face) {
@@ -1350,8 +1445,7 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
(y == player->y || y == player->y + 1) && (y == player->y || y == player->y + 1) &&
z == player->z z == player->z
) && ) &&
isReplaceableBlock(getBlockAt(x, y, z)) && isReplaceableBlock(getBlockAt(x, y, z))
(!isColumnBlock(block) || getBlockAt(x, y - 1, z) != B_air)
) { ) {
// Apply server-side block change // Apply server-side block change
if (makeBlockChange(x, y, z, block)) return; if (makeBlockChange(x, y, z, block)) return;
@@ -1359,14 +1453,9 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
*count -= 1; *count -= 1;
// Clear item id in slot if amount is zero // Clear item id in slot if amount is zero
if (*count == 0) *item = 0; if (*count == 0) *item = 0;
// Calculate fluid flow // Send updates
#ifdef DO_FLUID_FLOW processBlockUpdate(x, y, z, block, UPDATE_NOW);
checkFluidUpdate(x, y + 1, z, getBlockAt(x, y + 1, z)); updateXYZNeighbors(x, y, z, UPDATE_NOW);
checkFluidUpdate(x - 1, y, z, getBlockAt(x - 1, y, z));
checkFluidUpdate(x + 1, y, z, getBlockAt(x + 1, y, z));
checkFluidUpdate(x, y, z - 1, getBlockAt(x, y, z - 1));
checkFluidUpdate(x, y, z + 1, getBlockAt(x, y, z + 1));
#endif
} }
// Sync hotbar contents to player // Sync hotbar contents to player
@@ -1685,6 +1774,22 @@ void handleServerTick (int64_t time_since_last_tick) {
*/ */
if (rng_seed == 0) rng_seed = world_seed; if (rng_seed == 0) rng_seed = world_seed;
// block updates might add more deferred block updates,
// so we temporarily make all new block updates add one more tick to the defer tick counter
is_processing_deferred_block_updates = 1;
int next_update_idx = 0;
for (int i = 0; i < deferred_block_updates_count; i ++) {
DeferredBlockUpdate *u = &deferred_block_updates[i];
if (u->await_ticks) {
u->await_ticks --;
deferred_block_updates[next_update_idx ++] = *u;
} else {
processBlockUpdate(u->x, u->y, u->z, getBlockAt(u->x, u->y, u->z), u->update_kind);
}
}
deferred_block_updates_count = next_update_idx;
is_processing_deferred_block_updates = 0;
// Tick mob behavior // Tick mob behavior
for (int i = 0; i < MAX_MOBS; i ++) { for (int i = 0; i < MAX_MOBS; i ++) {
if (mob_data[i].type == 0) continue; if (mob_data[i].type == 0) continue;

View File

@@ -16,6 +16,8 @@
int64_t last_disk_sync_time = 0; int64_t last_disk_sync_time = 0;
// TODO: store deferred block updates
// Restores world data from disk, or writes world file if it doesn't exist // Restores world data from disk, or writes world file if it doesn't exist
int initSerializer () { int initSerializer () {