diff --git a/include/globals.h b/include/globals.h index 1974473..d9bd84e 100644 --- a/include/globals.h +++ b/include/globals.h @@ -34,8 +34,8 @@ // Max render distance, determines how many chunks to send #define VIEW_DISTANCE 2 -// Time between server ticks in microseconds (default = 1s) -#define TIME_BETWEEN_TICKS 1000000 +// Time between server ticks in microseconds (default = 0.05s) +#define TIME_BETWEEN_TICKS 50000 // Calculated from 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 #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). // This is a synchronous operation, and can cause performance issues if // frequent random disk access is slow. Data is still stored in and @@ -195,6 +199,24 @@ typedef struct { uint8_t block; } 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) typedef struct { @@ -267,6 +289,10 @@ typedef struct { extern BlockChange block_changes[MAX_BLOCK_CHANGES]; 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 int player_data_count; diff --git a/include/procedures.h b/include/procedures.h index b7273c8..a51b75e 100644 --- a/include/procedures.h +++ b/include/procedures.h @@ -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 isColumnBlock (uint8_t block); +uint8_t isFallingBlock (uint8_t block); uint8_t isPassableBlock (uint8_t block); uint8_t isPassableSpawnBlock (uint8_t block); uint8_t isReplaceableBlock (uint8_t block); +uint8_t getFluid (uint8_t block); uint32_t isCompostItem (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 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 interactEntity (int entity_id, int interactor_id); diff --git a/src/globals.c b/src/globals.c index 1b5b462..1b66086 100644 --- a/src/globals.c +++ b/src/globals.c @@ -50,6 +50,10 @@ uint16_t client_count; BlockChange block_changes[MAX_BLOCK_CHANGES]; 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]; int player_data_count = 0; diff --git a/src/main.c b/src/main.c index 1d8bddb..75d8d82 100644 --- a/src/main.c +++ b/src/main.c @@ -595,6 +595,10 @@ int main () { // Check if it's time to yield to the idle task 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 for (int i = 0; i < MAX_PLAYERS; i ++) { if (clients[i] != -1) continue; diff --git a/src/procedures.c b/src/procedures.c index 59330fc..41815ee 100644 --- a/src/procedures.c +++ b/src/procedures.c @@ -1,3 +1,4 @@ +#include #include #include #include @@ -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 uint8_t isColumnBlock (uint8_t block) { return ( @@ -790,8 +798,7 @@ uint8_t isPassableBlock (uint8_t block) { } // Checks whether the given block is non-solid and spawnable uint8_t isPassableSpawnBlock (uint8_t block) { - if ((block >= B_water && block < B_water + 8) || - (block >= B_lava && block < B_lava + 4)) + if (getFluid(block)) { 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 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 uint8_t adjacent[4] = { 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 (!connected) { makeBlockChange(x, y, z, B_air); - checkFluidUpdate(x + 1, y, z, adjacent[0]); - checkFluidUpdate(x - 1, y, z, adjacent[1]); - checkFluidUpdate(x, y, z + 1, adjacent[2]); - checkFluidUpdate(x, y, z - 1, adjacent[3]); + updateXYZNeighbors(x, y, z, UPDATE_NOW); 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); if (isReplaceableBlock(block_below)) { 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 - if (level == 3 && fluid == B_lava) return; - if (level == 7) return; + if (level == max_level) return; // Handle lateral water flow, increasing level by 1 if (isReplaceableFluid(adjacent[0], level, fluid)) { 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)) { 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)) { 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)) { 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 fluid; - if (block >= B_water && block < B_water + 8) fluid = B_water; - else if (block >= B_lava && block < B_lava + 4) fluid = B_lava; - else return; - - handleFluidMovement(x, y, z, fluid, block); - +uint8_t getFluid(uint8_t block) { + if (block >= B_water && block < B_water + 8) + return B_water; + else if (block >= B_lava && block < B_lava + 4) + return B_lava; + return 0; } #ifdef ENABLE_PICKUP_ANIMATION @@ -1143,6 +1154,111 @@ void playPickupAnimation (PlayerData *player, uint16_t item, double x, double y, } #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) { // 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 if (action != 0 && action != 2) return; + uint8_t block = getBlockAt(x, y, z); + // In creative, only the "start mining" action is sent // No additional verification is performed, the block is simply removed if (action == 0 && GAMEMODE == 1) { - makeBlockChange(x, y, z, 0); - return; + makeBlockChange(x, y, z, B_air); + } + 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); - - // 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); - } + // Update nearby blocks + updateXYZNeighbors(x, y, z, UPDATE_NOW); } 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) && z == player->z ) && - isReplaceableBlock(getBlockAt(x, y, z)) && - (!isColumnBlock(block) || getBlockAt(x, y - 1, z) != B_air) + isReplaceableBlock(getBlockAt(x, y, z)) ) { // Apply server-side block change 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; // Clear item id in slot if amount is zero if (*count == 0) *item = 0; - // Calculate fluid flow - #ifdef DO_FLUID_FLOW - checkFluidUpdate(x, y + 1, z, getBlockAt(x, y + 1, z)); - 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 + // Send updates + processBlockUpdate(x, y, z, block, UPDATE_NOW); + updateXYZNeighbors(x, y, z, UPDATE_NOW); } // 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; + // 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 for (int i = 0; i < MAX_MOBS; i ++) { if (mob_data[i].type == 0) continue; diff --git a/src/serialize.c b/src/serialize.c index 35ab74b..a742860 100644 --- a/src/serialize.c +++ b/src/serialize.c @@ -16,6 +16,8 @@ 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 int initSerializer () {