#include #include #include #include #ifdef _WIN32 #include #endif #include "globals.h" #include "tools.h" #include "varnum.h" #include "packets.h" #include "registries.h" #include "worldgen.h" #include "structures.h" #include "serialize.h" #include "procedures.h" int client_states[MAX_PLAYERS * 2]; void setClientState (int client_fd, int new_state) { // Look for a client state with a matching file descriptor for (int i = 0; i < MAX_PLAYERS * 2; i += 2) { if (client_states[i] != client_fd) continue; client_states[i + 1] = new_state; return; } // If the above failed, look for an unused client state slot for (int i = 0; i < MAX_PLAYERS * 2; i += 2) { if (client_states[i] != -1) continue; client_states[i] = client_fd; client_states[i + 1] = new_state; return; } } int getClientState (int client_fd) { for (int i = 0; i < MAX_PLAYERS * 2; i += 2) { if (client_states[i] != client_fd) continue; return client_states[i + 1]; } return STATE_NONE; } int getClientIndex (int client_fd) { for (int i = 0; i < MAX_PLAYERS * 2; i += 2) { if (client_states[i] != client_fd) continue; return i; } return -1; } // Restores player data to initial state (fresh spawn) void resetPlayerData (PlayerData *player) { player->health = 20; player->hunger = 20; player->saturation = 2500; player->x = 8; player->z = 8; player->y = 80; player->flags |= 0x02; player->grounded_y = 0; for (int i = 0; i < 41; i ++) { player->inventory_items[i] = 0; player->inventory_count[i] = 0; } for (int i = 0; i < 9; i ++) { player->craft_items[i] = 0; player->craft_count[i] = 0; } } // Assigns the given data to a player_data entry int reservePlayerData (int client_fd, uint8_t *uuid, char *name) { for (int i = 0; i < MAX_PLAYERS; i ++) { // Found existing player entry (UUID match) if (memcmp(player_data[i].uuid, uuid, 16) == 0) { // Set network file descriptor and username player_data[i].client_fd = client_fd; memcpy(player_data[i].name, name, 16); // Flag player as loading player_data[i].flags |= 0x20; player_data[i].flagval_16 = 0; // Reset their recently visited chunk list for (int j = 0; j < VISITED_HISTORY; j ++) { player_data[i].visited_x[j] = 32767; player_data[i].visited_z[j] = 32767; } return 0; } // Search for unallocated player slots uint8_t empty = true; for (uint8_t j = 0; j < 16; j ++) { if (player_data[i].uuid[j] != 0) { empty = false; break; } } // Found free space for a player, initialize default parameters if (empty) { if (player_data_count >= MAX_PLAYERS) return 1; player_data[i].client_fd = client_fd; player_data[i].flags |= 0x20; player_data[i].flagval_16 = 0; memcpy(player_data[i].uuid, uuid, 16); memcpy(player_data[i].name, name, 16); resetPlayerData(&player_data[i]); player_data_count ++; return 0; } } return 1; } int getPlayerData (int client_fd, PlayerData **output) { for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == client_fd) { *output = &player_data[i]; return 0; } } return 1; } // Returns the player with the given name, or NULL if not found PlayerData *getPlayerByName (int start_offset, int end_offset, uint8_t *buffer) { for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == -1) continue; int j; for (j = start_offset; j < end_offset && j < 256 && buffer[j] != ' '; j++) { if (player_data[i].name[j - start_offset] != buffer[j]) break; } if ((j == end_offset || buffer[j] == ' ') && j < 256) { return &player_data[i]; } } return NULL; } // Marks a client as disconnected and cleans up player data void handlePlayerDisconnect (int client_fd) { // Search for a corresponding player in the player data array for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd != client_fd) continue; // Mark the player as being offline player_data[i].client_fd = -1; // Prepare leave message for broadcast uint8_t player_name_len = strlen(player_data[i].name); strcpy((char *)recv_buffer, player_data[i].name); strcpy((char *)recv_buffer + player_name_len, " left the game"); // Broadcast this player's leave to all other connected clients for (int j = 0; j < MAX_PLAYERS; j ++) { if (player_data[j].client_fd == client_fd) continue; if (player_data[j].flags & 0x20) continue; // Send chat message sc_systemChat(player_data[j].client_fd, (char *)recv_buffer, 14 + player_name_len); // Remove leaving player's entity sc_removeEntity(player_data[j].client_fd, client_fd); } break; } // Find the client state entry and reset it for (int i = 0; i < MAX_PLAYERS * 2; i += 2) { if (client_states[i] == client_fd) { client_states[i] = -1; return; } } } // Marks a client as connected and broadcasts their data to other players void handlePlayerJoin (PlayerData* player) { // Prepare join message for broadcast uint8_t player_name_len = strlen(player->name); strcpy((char *)recv_buffer, player->name); strcpy((char *)recv_buffer + player_name_len, " joined the game"); // Inform other clients (and the joining client) of the player's name and entity for (int i = 0; i < MAX_PLAYERS; i ++) { sc_systemChat(player_data[i].client_fd, (char *)recv_buffer, 16 + player_name_len); sc_playerInfoUpdateAddPlayer(player_data[i].client_fd, *player); if (player_data[i].client_fd != player->client_fd) { sc_spawnEntityPlayer(player_data[i].client_fd, *player); } } // Clear "client loading" flag and fallback timer player->flags &= ~0x20; player->flagval_16 = 0; } void disconnectClient (int *client_fd, int cause) { if (*client_fd == -1) return; client_count --; setClientState(*client_fd, STATE_NONE); handlePlayerDisconnect(*client_fd); #ifdef _WIN32 closesocket(*client_fd); printf("Disconnected client %d, cause: %d, errno: %d\n", *client_fd, cause, WSAGetLastError()); #else close(*client_fd); printf("Disconnected client %d, cause: %d, errno: %d\n\n", *client_fd, cause, errno); #endif *client_fd = -1; } uint8_t serverSlotToClientSlot (int window_id, uint8_t slot) { if (window_id == 0) { // player inventory if (slot < 9) return slot + 36; if (slot >= 9 && slot <= 35) return slot; if (slot == 40) return 45; if (slot >= 36 && slot <= 39) return 44 - slot; if (slot >= 41 && slot <= 44) return slot - 40; } else if (window_id == 12) { // crafting table if (slot >= 41 && slot <= 49) return slot - 40; return serverSlotToClientSlot(0, slot - 1); } else if (window_id == 14) { // furnace if (slot >= 41 && slot <= 43) return slot - 41; return serverSlotToClientSlot(0, slot + 6); } return 255; } uint8_t clientSlotToServerSlot (int window_id, uint8_t slot) { if (window_id == 0) { // player inventory if (slot >= 36 && slot <= 44) return slot - 36; if (slot >= 9 && slot <= 35) return slot; if (slot == 45) return 40; if (slot >= 5 && slot <= 8) return 44 - slot; // map inventory crafting slots to player data crafting grid (semi-hack) // this abuses the fact that the buffers are adjacent in player data if (slot == 1) return 41; if (slot == 2) return 42; if (slot == 3) return 44; if (slot == 4) return 45; } else if (window_id == 12) { // crafting table // same crafting offset overflow hack as above if (slot >= 1 && slot <= 9) return 40 + slot; // the rest of the slots are identical, just shifted by one if (slot >= 10 && slot <= 45) return clientSlotToServerSlot(0, slot - 1); } else if (window_id == 14) { // furnace // move furnace items to the player's crafting grid // this lets us put them back in the inventory once the window closes if (slot <= 2) return 41 + slot; // the rest of the slots are identical, just shifted by 6 if (slot >= 3 && slot <= 38) return clientSlotToServerSlot(0, slot + 6); } #ifdef ALLOW_CHESTS else if (window_id == 2) { // chest // overflow chest slots into crafting grid // technically invalid, expected to be handled on a per-case basis if (slot <= 26) return 41 + slot; // the rest of the slots are identical, just shifted by 18 if (slot >= 27 && slot <= 62) return clientSlotToServerSlot(0, slot - 18); } #endif return 255; } int givePlayerItem (PlayerData *player, uint16_t item, uint8_t count) { if (item == 0 || count == 0) return 0; uint8_t slot = 255; uint8_t stack_size = getItemStackSize(item); for (int i = 0; i < 41; i ++) { if (player->inventory_items[i] == item && player->inventory_count[i] <= stack_size - count) { slot = i; break; } } if (slot == 255) { for (int i = 0; i < 41; i ++) { if (player->inventory_count[i] == 0) { slot = i; break; } } } // Fail to assign item if slot is outside of main inventory if (slot >= 36) return 1; player->inventory_items[slot] = item; player->inventory_count[slot] += count; sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, slot), player->inventory_count[slot], item); return 0; } // Sends the full sequence for spawning the player to the client void spawnPlayer (PlayerData *player) { // Player spawn coordinates, initialized to placeholders float spawn_x = 8.5f, spawn_y = 80.0f, spawn_z = 8.5f; float spawn_yaw = 0.0f, spawn_pitch = 0.0f; if (player->flags & 0x02) { // Is this a new player? // Determine spawning Y coordinate based on terrain height spawn_y = getHeightAt(8, 8) + 1; player->y = spawn_y; player->flags &= ~0x02; } else { // Not a new player // Calculate spawn position from player data spawn_x = (float)player->x + 0.5; spawn_y = player->y; spawn_z = (float)player->z + 0.5; spawn_yaw = player->yaw * 180 / 127; spawn_pitch = player->pitch * 90 / 127; } // Teleport player to spawn coordinates (first pass) sc_synchronizePlayerPosition(player->client_fd, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch); task_yield(); // Check task timer between packets // Sync client inventory and hotbar for (uint8_t i = 0; i < 41; i ++) { sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, i), player->inventory_count[i], player->inventory_items[i]); } sc_setHeldItem(player->client_fd, player->hotbar); // Sync client health and hunger sc_setHealth(player->client_fd, player->health, player->hunger, player->saturation); // Sync client clock time sc_updateTime(player->client_fd, world_time); #ifdef ENABLE_PLAYER_FLIGHT if (GAMEMODE != 1 && GAMEMODE != 3) { // Give the player flight (for testing) sc_playerAbilities(player->client_fd, 0x04); } #endif // Calculate player's chunk coordinates short _x = div_floor(player->x, 16), _z = div_floor(player->z, 16); // Indicate that we're about to send chunk data sc_setDefaultSpawnPosition(player->client_fd, 8, 80, 8); sc_startWaitingForChunks(player->client_fd); sc_setCenterChunk(player->client_fd, _x, _z); task_yield(); // Check task timer between packets // Send spawn chunk first sc_chunkDataAndUpdateLight(player->client_fd, _x, _z); for (int i = -VIEW_DISTANCE; i <= VIEW_DISTANCE; i ++) { for (int j = -VIEW_DISTANCE; j <= VIEW_DISTANCE; j ++) { if (i == 0 && j == 0) continue; sc_chunkDataAndUpdateLight(player->client_fd, _x + i, _z + j); } } // Re-teleport player after all chunks have been sent sc_synchronizePlayerPosition(player->client_fd, spawn_x, spawn_y, spawn_z, spawn_yaw, spawn_pitch); task_yield(); // Check task timer between packets } // Broadcasts a player's entity metadata (sneak/sprint state) to other players void broadcastPlayerMetadata (PlayerData *player) { uint8_t sneaking = (player->flags & 0x04) != 0; uint8_t sprinting = (player->flags & 0x08) != 0; uint8_t entity_bit_mask = 0; if (sneaking) entity_bit_mask |= 0x02; if (sprinting) entity_bit_mask |= 0x08; int pose = 0; if (sneaking) pose = 5; EntityData metadata[] = { { 0, // Index (Entity Bit Mask) 0, // Type (Byte) entity_bit_mask, // Value }, { 6, // Index (Pose), 21, // Type (Pose), pose, // Value (Standing) } }; for (int i = 0; i < MAX_PLAYERS; i ++) { PlayerData* other_player = &player_data[i]; int client_fd = other_player->client_fd; if (client_fd == -1) continue; if (client_fd == player->client_fd) continue; if (other_player->flags & 0x20) continue; sc_setEntityMetadata(client_fd, player->client_fd, metadata, 2); } } // Sends a mob's entity metadata to the given player. // If client_fd is -1, broadcasts to all player void broadcastMobMetadata (int client_fd, int entity_id) { MobData *mob = &mob_data[-entity_id - 2]; EntityData *metadata; size_t length; switch (mob->type) { case 106: // Sheep if (!((mob->data >> 5) & 1)) // Don't send metadata if sheep isn't sheared return; metadata = malloc(sizeof *metadata); metadata[0] = (EntityData){ 17, // Index (Sheep Bit Mask), 0, // Type (Byte), (uint8_t)0x10, // Value }; length = 1; break; default: return; } if (client_fd == -1) { for (int i = 0; i < MAX_PLAYERS; i ++) { PlayerData* player = &player_data[i]; client_fd = player->client_fd; if (client_fd == -1) continue; if (player->flags & 0x20) continue; sc_setEntityMetadata(client_fd, entity_id, metadata, length); } } else { sc_setEntityMetadata(client_fd, entity_id, metadata, length); } free(metadata); } uint8_t getBlockChange (short x, uint8_t y, short z) { for (int i = 0; i < block_changes_count; i ++) { if (block_changes[i].block == 0xFF) continue; if ( block_changes[i].x == x && block_changes[i].y == y && block_changes[i].z == z ) return block_changes[i].block; #ifdef ALLOW_CHESTS // Skip chest contents if (block_changes[i].block == B_chest) i += 14; #endif } return 0xFF; } // Handle running out of memory for new block changes void failBlockChange (short x, uint8_t y, short z, uint8_t block) { // Get previous block at this location uint8_t before = getBlockAt(x, y, z); // Broadcast a new update to all players for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == -1) continue; if (player_data[i].flags & 0x20) continue; // Reset the block they tried to change sc_blockUpdate(player_data[i].client_fd, x, y, z, before); // Broadcast a chat message warning about the limit sc_systemChat(player_data[i].client_fd, "Block changes limit exceeded. Restore original terrain to continue.", 67); } } uint8_t makeBlockChange (short x, uint8_t y, short z, uint8_t block) { // Transmit block update to all in-game clients for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == -1) continue; if (player_data[i].flags & 0x20) continue; sc_blockUpdate(player_data[i].client_fd, x, y, z, block); } // Calculate terrain at these coordinates and compare it to the input block. // Since block changes get overlayed on top of terrain, we don't want to // store blocks that don't differ from the base terrain. ChunkAnchor anchor = { x / CHUNK_SIZE, z / CHUNK_SIZE }; if (x % CHUNK_SIZE < 0) anchor.x --; if (z % CHUNK_SIZE < 0) anchor.z --; anchor.hash = getChunkHash(anchor.x, anchor.z); anchor.biome = getChunkBiome(anchor.x, anchor.z); uint8_t is_base_block = block == getTerrainAt(x, y, z, anchor); // In the block_changes array, 0xFF indicates a missing/restored entry. // We track the position of the first such "gap" for when the operation // isn't replacing an existing block change. int first_gap = block_changes_count; // Prioritize replacing entries with matching coordinates // This prevents having conflicting entries for one set of coordinates for (int i = 0; i < block_changes_count; i ++) { if (block_changes[i].block == 0xFF) { if (first_gap == block_changes_count) first_gap = i; continue; } if ( block_changes[i].x == x && block_changes[i].y == y && block_changes[i].z == z ) { #ifdef ALLOW_CHESTS // When replacing chests, clear following 14 entries too (item data) if (block_changes[i].block == B_chest) { for (int j = 1; j < 15; j ++) block_changes[i + j].block = 0xFF; } #endif if (is_base_block) block_changes[i].block = 0xFF; else { #ifdef ALLOW_CHESTS // When placing chests, just unallocate the target block and fall // through to the chest-specific routine below. if (block == B_chest) { block_changes[i].block = 0xFF; if (first_gap > i) first_gap = i; #ifndef DISK_SYNC_BLOCKS_ON_INTERVAL writeBlockChangesToDisk(i, i); #endif break; } #endif block_changes[i].block = block; } #ifndef DISK_SYNC_BLOCKS_ON_INTERVAL writeBlockChangesToDisk(i, i); #endif return 0; } } // Don't create a new entry if it contains the base terrain block if (is_base_block) return 0; #ifdef ALLOW_CHESTS if (block == B_chest) { // Chests require 15 entries total, so for maximum space-efficiency, // we have to find a continuous gap that's at least 15 slots wide. // By design, this loop also continues past the current search range, // which naturally appends the chest to the end if a gap isn't found. int last_real_entry = first_gap - 1; for (int i = first_gap; i <= block_changes_count + 15; i ++) { if (i >= MAX_BLOCK_CHANGES) break; // No more space, trigger failBlockChange if (block_changes[i].block != 0xFF) { last_real_entry = i; continue; } if (i - last_real_entry != 15) continue; // A wide enough gap has been found, assign the chest block_changes[last_real_entry + 1].x = x; block_changes[last_real_entry + 1].y = y; block_changes[last_real_entry + 1].z = z; block_changes[last_real_entry + 1].block = block; // Zero out the following 14 entries for item data for (int i = 2; i <= 15; i ++) { block_changes[last_real_entry + i].x = 0; block_changes[last_real_entry + i].y = 0; block_changes[last_real_entry + i].z = 0; block_changes[last_real_entry + i].block = 0; } // Extend future search range if necessary if (i >= block_changes_count) { block_changes_count = i + 1; } // Write changes to disk (if applicable) #ifndef DISK_SYNC_BLOCKS_ON_INTERVAL writeBlockChangesToDisk(last_real_entry + 1, last_real_entry + 15); #endif return 0; } // If we're here, no changes were made failBlockChange(x, y, z, block); return 1; } #endif // Handle running out of memory for new block changes if (first_gap == MAX_BLOCK_CHANGES) { failBlockChange(x, y, z, block); return 1; } // Fall back to storing the change at the first possible gap block_changes[first_gap].x = x; block_changes[first_gap].y = y; block_changes[first_gap].z = z; block_changes[first_gap].block = block; // Write change to disk (if applicable) #ifndef DISK_SYNC_BLOCKS_ON_INTERVAL writeBlockChangesToDisk(first_gap, first_gap); #endif // Extend future search range if we've appended to the end if (first_gap == block_changes_count) { block_changes_count ++; } return 0; } // Returns the result of mining a block, taking into account the block type and tools // Probability numbers obtained with this formula: N = floor(P * 32 ^ 2) uint16_t getMiningResult (uint16_t held_item, uint8_t block) { switch (block) { case B_oak_leaves: if (held_item == I_shears) return I_oak_leaves; uint32_t r = fast_rand(); if (r < 21474836) return I_apple; // 0.5% if (r < 85899345) return I_stick; // 2% if (r < 214748364) return I_oak_sapling; // 5% return 0; break; case B_stone: case B_cobblestone: case B_stone_slab: case B_cobblestone_slab: case B_sandstone: case B_furnace: case B_coal_ore: case B_iron_ore: case B_iron_block: case B_gold_block: case B_diamond_block: case B_redstone_block: case B_coal_block: // Check if player is holding (any) pickaxe if ( held_item != I_wooden_pickaxe && held_item != I_stone_pickaxe && held_item != I_iron_pickaxe && held_item != I_golden_pickaxe && held_item != I_diamond_pickaxe && held_item != I_netherite_pickaxe ) return 0; break; case B_gold_ore: case B_redstone_ore: case B_diamond_ore: // Check if player is holding an iron (or better) pickaxe if ( held_item != I_iron_pickaxe && held_item != I_golden_pickaxe && held_item != I_diamond_pickaxe && held_item != I_netherite_pickaxe ) return 0; break; case B_snow: // Check if player is holding (any) shovel if ( held_item != I_wooden_shovel && held_item != I_stone_shovel && held_item != I_iron_shovel && held_item != I_golden_shovel && held_item != I_diamond_shovel && held_item != I_netherite_shovel ) return 0; break; default: break; } return B_to_I[block]; } // Rolls a random number to determine whether the player's tool should break void bumpToolDurability (PlayerData *player) { uint16_t held_item = player->inventory_items[player->hotbar]; // In order to avoid storing durability data, items break randomly with // the probability weighted based on vanilla durability. uint32_t r = fast_rand(); if ( ((held_item == I_wooden_pickaxe || held_item == I_wooden_axe || held_item == I_wooden_shovel) && r < 72796055) || ((held_item == I_stone_pickaxe || held_item == I_stone_axe || held_item == I_stone_shovel) && r < 32786009) || ((held_item == I_iron_pickaxe || held_item == I_iron_axe || held_item == I_iron_shovel) && r < 17179869) || ((held_item == I_golden_pickaxe || held_item == I_golden_axe || held_item == I_golden_shovel) && r < 134217728) || ((held_item == I_diamond_pickaxe || held_item == I_diamond_axe || held_item == I_diamond_shovel) && r < 2751420) || ((held_item == I_netherite_pickaxe || held_item == I_netherite_axe || held_item == I_netherite_shovel) && r < 2114705) || (held_item == I_shears && r < 18046081) ) { player->inventory_items[player->hotbar] = 0; player->inventory_count[player->hotbar] = 0; sc_entityEvent(player->client_fd, player->client_fd, 47); sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), 0, 0); } } // Checks whether the given block would be mined instantly with the held tool uint8_t isInstantlyMined (PlayerData *player, uint8_t block) { uint16_t held_item = player->inventory_items[player->hotbar]; if ( block == B_snow || block == B_snow_block ) return ( held_item == I_stone_shovel || held_item == I_iron_shovel || held_item == I_diamond_shovel || held_item == I_netherite_shovel || held_item == I_golden_shovel ); if (block == B_oak_leaves) return held_item == I_shears; return ( block == B_dead_bush || block == B_short_grass || block == B_torch || block == B_lily_pad || block == B_oak_sapling ); } // Checks whether the given block has to have something beneath it uint8_t isColumnBlock (uint8_t block) { return ( block == B_snow || block == B_moss_carpet || block == B_cactus || block == B_short_grass || block == B_dead_bush || block == B_sand || block == B_torch || block == B_oak_sapling ); } // Checks whether the given block is non-solid uint8_t isPassableBlock (uint8_t block) { return ( block == B_air || (block >= B_water && block < B_water + 8) || (block >= B_lava && block < B_lava + 4) || block == B_snow || block == B_moss_carpet || block == B_short_grass || block == B_dead_bush || block == B_torch ); } // 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)) { return 0; } return isPassableBlock(block); } // Checks whether the given block can be replaced by another block uint8_t isReplaceableBlock (uint8_t block) { return ( block == B_air || (block >= B_water && block < B_water + 8) || (block >= B_lava && block < B_lava + 4) || block == B_short_grass || block == B_snow ); } uint8_t isReplaceableFluid (uint8_t block, uint8_t level, uint8_t fluid) { if (block >= fluid && block - fluid < 8) { return block - fluid > level; } return isReplaceableBlock(block); } // Checks whether the given item can be used in a composter // Returns the probability (out of 2^32) to return bone meal uint32_t isCompostItem (uint16_t item) { // Output values calculated using the following formula: // P = 2^32 / (7 / compost_chance) if ( // Compost chance: 30% item == I_oak_leaves || item == I_short_grass || item == I_wheat_seeds || item == I_oak_sapling || item == I_moss_carpet ) return 184070026; if ( // Compost chance: 50% item == I_cactus || item == I_sugar_cane ) return 306783378; if ( // Compost chance: 65% item == I_apple || item == I_lily_pad ) return 398818392; return 0; } // Returns the maximum stack size of an item uint8_t getItemStackSize (uint16_t item) { if ( // Pickaxes item == I_wooden_pickaxe || item == I_stone_pickaxe || item == I_iron_pickaxe || item == I_golden_pickaxe || item == I_diamond_pickaxe || item == I_netherite_pickaxe || // Axes item == I_wooden_axe || item == I_stone_axe || item == I_iron_axe || item == I_golden_axe || item == I_diamond_axe || item == I_netherite_axe || // Shovels item == I_wooden_shovel || item == I_stone_shovel || item == I_iron_shovel || item == I_golden_shovel || item == I_diamond_shovel || item == I_netherite_shovel || // Swords item == I_wooden_sword || item == I_stone_sword || item == I_iron_sword || item == I_golden_sword || item == I_diamond_sword || item == I_netherite_sword || // Hoes item == I_wooden_hoe || item == I_stone_hoe || item == I_iron_hoe || item == I_golden_hoe || item == I_diamond_hoe || item == I_netherite_hoe || // Shears item == I_shears ) return 1; if ( item == I_snowball ) return 16; return 64; } // Returns defense points for the given piece of armor // If the input item is not armor, returns 0 uint8_t getItemDefensePoints (uint16_t item) { switch (item) { case I_leather_helmet: return 1; case I_golden_helmet: return 2; case I_iron_helmet: return 2; case I_diamond_helmet: // Same as netherite case I_netherite_helmet: return 3; case I_leather_chestplate: return 3; case I_golden_chestplate: return 5; case I_iron_chestplate: return 6; case I_diamond_chestplate: // Same as netherite case I_netherite_chestplate: return 8; case I_leather_leggings: return 2; case I_golden_leggings: return 3; case I_iron_leggings: return 5; case I_diamond_leggings: // Same as netherite case I_netherite_leggings: return 6; case I_leather_boots: return 1; case I_golden_boots: return 1; case I_iron_boots: return 2; case I_diamond_boots: // Same as netherite case I_netherite_boots: return 3; default: break; } return 0; } // Calculates total defense points for the player's equipped armor uint8_t getPlayerDefensePoints (PlayerData *player) { return ( // Helmet getItemDefensePoints(player->inventory_items[39]) + // Chestplate getItemDefensePoints(player->inventory_items[38]) + // Leggings getItemDefensePoints(player->inventory_items[37]) + // Boots getItemDefensePoints(player->inventory_items[36]) ); } // Returns the designated server slot for the given piece of armor // If input item is not armor, returns 255 uint8_t getArmorItemSlot (uint16_t item) { switch (item) { case I_leather_helmet: case I_golden_helmet: case I_iron_helmet: case I_diamond_helmet: case I_netherite_helmet: return 39; case I_leather_chestplate: case I_golden_chestplate: case I_iron_chestplate: case I_diamond_chestplate: case I_netherite_chestplate: return 38; case I_leather_leggings: case I_golden_leggings: case I_iron_leggings: case I_diamond_leggings: case I_netherite_leggings: return 37; case I_leather_boots: case I_golden_boots: case I_iron_boots: case I_diamond_boots: case I_netherite_boots: return 36; default: break; } return 255; } // Handles the player eating their currently held item // Returns whether the operation was succesful (item was consumed) // If `just_check` is set to true, the item doesn't get consumed uint8_t handlePlayerEating (PlayerData *player, uint8_t just_check) { // Exit early if player is unable to eat if (player->hunger >= 20) return false; uint16_t *held_item = &player->inventory_items[player->hotbar]; uint8_t *held_count = &player->inventory_count[player->hotbar]; // Exit early if player isn't holding anything if (*held_item == 0 || *held_count == 0) return false; uint8_t food = 0; uint16_t saturation = 0; // The saturation ratio from vanilla to here is about 1:500 switch (*held_item) { case I_chicken: food = 2; saturation = 600; break; case I_beef: food = 3; saturation = 900; break; case I_porkchop: food = 3; saturation = 300; break; case I_mutton: food = 2; saturation = 600; break; case I_cooked_chicken: food = 6; saturation = 3600; break; case I_cooked_beef: food = 8; saturation = 6400; break; case I_cooked_porkchop: food = 8; saturation = 6400; break; case I_cooked_mutton: food = 6; saturation = 4800; break; case I_rotten_flesh: food = 4; saturation = 0; break; case I_apple: food = 4; saturation = 1200; break; default: break; } // If just checking the item, return before making any changes if (just_check) return food != 0; // Apply saturation and food boost player->saturation += saturation; player->hunger += food; if (player->hunger > 20) player->hunger = 20; // Consume held item *held_count -= 1; if (*held_count == 0) *held_item = 0; // Update the client of these changes sc_entityEvent(player->client_fd, player->client_fd, 9); sc_setHealth(player->client_fd, player->health, player->hunger, player->saturation); sc_setContainerSlot( player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), *held_count, *held_item ); return true; } void handleFluidMovement (short x, uint8_t y, short z, uint8_t fluid, uint8_t block) { // Get fluid level (0-7) // The terminology here is a bit different from vanilla: // a higher fluid "level" means the fluid has traveled farther uint8_t level = block - fluid; // Query blocks adjacent to this fluid stream uint8_t adjacent[4] = { getBlockAt(x + 1, y, z), getBlockAt(x - 1, y, z), getBlockAt(x, y, z + 1), getBlockAt(x, y, z - 1) }; // Handle maintaining connections to a fluid source if (level != 0) { // Check if this fluid is connected to a block exactly one level lower uint8_t connected = false; for (int i = 0; i < 4; i ++) { if (adjacent[i] == block - 1) { connected = true; break; } } // 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]); return; } } // Check if water should flow down, prioritize that over lateral flow 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); } // Stop flowing laterally at the maximum level if (level == 3 && fluid == B_lava) return; if (level == 7) 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); } if (isReplaceableFluid(adjacent[1], level, fluid)) { makeBlockChange(x - 1, y, z, block + 1); handleFluidMovement(x - 1, y, z, fluid, block + 1); } if (isReplaceableFluid(adjacent[2], level, fluid)) { makeBlockChange(x, y, z + 1, block + 1); handleFluidMovement(x, y, z + 1, fluid, block + 1); } if (isReplaceableFluid(adjacent[3], level, fluid)) { makeBlockChange(x, y, z - 1, block + 1); handleFluidMovement(x, y, z - 1, fluid, block + 1); } } 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); } #ifdef ENABLE_PICKUP_ANIMATION // Plays the item pickup animation with the given item at the given coordinates void playPickupAnimation (PlayerData *player, uint16_t item, double x, double y, double z) { // Spawn a new item entity at the input coordinates // ID -1 is safe, as elsewhere it's reserved as a placeholder // The player's name is used as the UUID as it's cheap and unique enough sc_spawnEntity(player->client_fd, -1, (uint8_t *)player->name, 69, x + 0.5, y + 0.5, z + 0.5, 0, 0); // Write a Set Entity Metadata packet for the item // There's no packets.c entry for this, as it's not cheaply generalizable writeVarInt(player->client_fd, 12 + sizeVarInt(item)); writeByte(player->client_fd, 0x5C); writeVarInt(player->client_fd, -1); // Describe slot data array entry writeByte(player->client_fd, 8); writeByte(player->client_fd, 7); // Send slot data writeByte(player->client_fd, 1); writeVarInt(player->client_fd, item); writeByte(player->client_fd, 0); writeByte(player->client_fd, 0); // Terminate entity metadata array writeByte(player->client_fd, 0xFF); // Send the Pickup Item packet targeting this entity sc_pickupItem(player->client_fd, -1, player->client_fd, 1); // Remove the item entity from the client right away sc_removeEntity(player->client_fd, -1); } #endif void handlePlayerAction (PlayerData *player, int action, short x, short y, short z) { // Re-sync slot when player drops an item if (action == 3 || action == 4) { sc_setContainerSlot( player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), player->inventory_count[player->hotbar], player->inventory_items[player->hotbar] ); return; } // "Finish eating" action, called any time eating stops if (action == 5) { // Reset eating timer and clear eating flag player->flagval_16 = 0; player->flags &= ~0x10; } // Ignore further actions not pertaining to mining blocks if (action != 0 && action != 2) return; // 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; } 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); } } void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face) { // Get targeted block (if coordinates are provided) uint8_t target = face == 255 ? 0 : getBlockAt(x, y, z); // Get held item properties uint8_t *count = &player->inventory_count[player->hotbar]; uint16_t *item = &player->inventory_items[player->hotbar]; // Check interaction with containers when not sneaking if (!(player->flags & 0x04) && face != 255) { if (target == B_crafting_table) { sc_openScreen(player->client_fd, 12, "Crafting", 8); return; } else if (target == B_furnace) { sc_openScreen(player->client_fd, 14, "Furnace", 7); return; } else if (target == B_composter) { // Check if the player is holding anything if (*count == 0) return; // Check if the item is a valid compost item uint32_t compost_chance = isCompostItem(*item); if (compost_chance != 0) { // Take away composted item if ((*count -= 1) == 0) *item = 0; sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), *count, *item); // Test compost chance and give bone meal on success if (fast_rand() < compost_chance) { givePlayerItem(player, I_bone_meal, 1); } return; } } #ifdef ALLOW_CHESTS else if (target == B_chest) { // Get a pointer to the entry following this chest in block_changes uint8_t *storage_ptr = NULL; for (int i = 0; i < block_changes_count; i ++) { if (block_changes[i].block != B_chest) continue; if (block_changes[i].x != x || block_changes[i].y != y || block_changes[i].z != z) continue; storage_ptr = (uint8_t *)(&block_changes[i + 1]); break; } if (storage_ptr == NULL) return; // Terrible memory hack!! // Copy the pointer into the player's crafting table item array. // This allows us to save some memory by repurposing a feature that // is mutually exclusive with chests, though it is otherwise a // terrible idea for obvious reasons. memcpy(player->craft_items, &storage_ptr, sizeof(storage_ptr)); // Show the player the chest UI sc_openScreen(player->client_fd, 2, "Chest", 5); // Load the slots of the chest from the block_changes array. // This is a similarly dubious memcpy hack, but at least we're not // mixing data types? Kind of? for (int i = 0; i < 27; i ++) { uint16_t item; uint8_t count; memcpy(&item, storage_ptr + i * 3, 2); memcpy(&count, storage_ptr + i * 3 + 2, 1); sc_setContainerSlot(player->client_fd, 2, i, count, item); } return; } #endif } // If the selected slot doesn't hold any items, exit if (*count == 0) return; // Check special item handling if (*item == I_bone_meal) { uint8_t target_below = getBlockAt(x, y - 1, z); if (target == B_oak_sapling) { // Consume the bone meal (yes, even before checks) // Wasting bone meal on misplanted saplings is vanilla behavior if ((*count -= 1) == 0) *item = 0; sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), *count, *item); if ( // Saplings can only grow when placed on these blocks target_below == B_dirt || target_below == B_grass_block || target_below == B_snowy_grass_block || target_below == B_mud ) { // Bone meal has a 25% chance of growing a tree from a sapling if ((fast_rand() & 3) == 0) placeTreeStructure(x, y, z); } } } else if (handlePlayerEating(player, true)) { // Reset eating timer and set eating flag player->flagval_16 = 0; player->flags |= 0x10; } else if (getItemDefensePoints(*item) != 0) { // For some reason, this action is sent twice when looking at a block // Ignore the variant that has coordinates if (face != 255) return; // Swap to held piece of armor uint8_t slot = getArmorItemSlot(*item); uint16_t prev_item = player->inventory_items[slot]; player->inventory_items[slot] = *item; player->inventory_count[slot] = 1; player->inventory_items[player->hotbar] = prev_item; player->inventory_count[player->hotbar] = 1; // Update client inventory sc_setContainerSlot(player->client_fd, -2, serverSlotToClientSlot(0, slot), 1, *item); sc_setContainerSlot(player->client_fd, -2, serverSlotToClientSlot(0, player->hotbar), 1, prev_item); return; } // Don't proceed with block placement if no coordinates were provided if (face == 255) return; // If the selected item doesn't correspond to a block, exit uint8_t block = I_to_B(*item); if (block == 0) return; switch (face) { case 0: y -= 1; break; case 1: y += 1; break; case 2: z -= 1; break; case 3: z += 1; break; case 4: x -= 1; break; case 5: x += 1; break; default: break; } // Check if the block's placement conditions are met if ( !( // Is player in the way? !isPassableBlock(block) && x == player->x && (y == player->y || y == player->y + 1) && z == player->z ) && isReplaceableBlock(getBlockAt(x, y, z)) && (!isColumnBlock(block) || getBlockAt(x, y - 1, z) != B_air) ) { // Apply server-side block change if (makeBlockChange(x, y, z, block)) return; // Decrease item amount in selected slot *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 } // Sync hotbar contents to player sc_setContainerSlot(player->client_fd, 0, serverSlotToClientSlot(0, player->hotbar), *count, *item); } void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health) { for (int i = 0; i < MAX_MOBS; i ++) { // Look for type 0 (unallocated) if (mob_data[i].type != 0) continue; // Assign it the input parameters mob_data[i].type = type; mob_data[i].x = x; mob_data[i].y = y; mob_data[i].z = z; mob_data[i].data = health & 31; // Forge a UUID from a random number and the mob's index uint8_t uuid[16]; uint32_t r = fast_rand(); memcpy(uuid, &r, 4); memcpy(uuid + 4, &i, 4); // Broadcast entity creation to all players for (int j = 0; j < MAX_PLAYERS; j ++) { if (player_data[j].client_fd == -1) continue; sc_spawnEntity( player_data[j].client_fd, -2 - i, // Use negative IDs to avoid conflicts with player IDs uuid, // Use the UUID generated above type, (double)x + 0.5f, y, (double)z + 0.5f, // Face opposite of the player, as if looking at them when spawning (player_data[j].yaw + 127) & 255, 0 ); } // Freshly spawned mobs currently don't need metadata updates. // If this changes, uncomment this line. // broadcastMobMetadata(-1, i); break; } } void interactEntity (int entity_id, int interactor_id) { PlayerData *player; if (getPlayerData(interactor_id, &player)) return; MobData *mob = &mob_data[-entity_id - 2]; switch (mob->type) { case 106: // Sheep if (player->inventory_items[player->hotbar] != I_shears) return; if ((mob->data >> 5) & 1) // Check if sheep has already been sheared return; mob->data |= 1 << 5; // Set sheared to true bumpToolDurability(player); #ifdef ENABLE_PICKUP_ANIMATION playPickupAnimation(player, I_white_wool, mob->x, mob->y, mob->z); #endif uint8_t item_count = 1 + (fast_rand() & 1); // 1-2 givePlayerItem(player, I_white_wool, item_count); for (int i = 0; i < MAX_PLAYERS; i ++) { PlayerData* player = &player_data[i]; int client_fd = player->client_fd; if (client_fd == -1) continue; if (player->flags & 0x20) continue; sc_entityAnimation(client_fd, interactor_id, 0); } broadcastMobMetadata(-1, entity_id); break; } } void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t damage) { if (attacker_id > 0) { // Attacker is a player PlayerData *player; if (getPlayerData(attacker_id, &player)) return; // Check if attack cooldown flag is set if (player->flags & 0x01) return; // Scale damage based on held item uint16_t held_item = player->inventory_items[player->hotbar]; if (held_item == I_wooden_sword) damage *= 4; else if (held_item == I_golden_sword) damage *= 4; else if (held_item == I_stone_sword) damage *= 5; else if (held_item == I_iron_sword) damage *= 6; else if (held_item == I_diamond_sword) damage *= 7; else if (held_item == I_netherite_sword) damage *= 8; // Enable attack cooldown player->flags |= 0x01; player->flagval_8 = 0; } // Whether this attack caused the target entity to die uint8_t entity_died = false; if (entity_id > 0) { // The attacked entity is a player PlayerData *player; if (getPlayerData(entity_id, &player)) return; // Don't continue if the player is already dead if (player->health == 0) return; // Calculate damage reduction from player's armor uint8_t defense = getPlayerDefensePoints(player); // This uses the old (pre-1.9) protection calculation. Factors are // scaled up 256 times to avoid floating point math. Due to lost // precision, the 4% reduction factor drops to ~3.9%, although the // the resulting effective damage is then also rounded down. uint8_t effective_damage = damage * (256 - defense * 10) / 256; // Process health change on the server if (player->health <= effective_damage) { player->health = 0; entity_died = true; // Prepare death message in recv_buffer uint8_t player_name_len = strlen(player->name); strcpy((char *)recv_buffer, player->name); if (damage_type == D_fall && damage > 8) { // Killed by a greater than 5 block fall strcpy((char *)recv_buffer + player_name_len, " fell from a high place"); recv_buffer[player_name_len + 23] = '\0'; } else if (damage_type == D_fall) { // Killed by a less than 5 block fall strcpy((char *)recv_buffer + player_name_len, " hit the ground too hard"); recv_buffer[player_name_len + 24] = '\0'; } else if (damage_type == D_lava) { // Killed by being in lava strcpy((char *)recv_buffer + player_name_len, " tried to swim in lava"); recv_buffer[player_name_len + 22] = '\0'; } else if (attacker_id < -1) { // Killed by a mob strcpy((char *)recv_buffer + player_name_len, " was slain by a mob"); recv_buffer[player_name_len + 19] = '\0'; } else if (attacker_id > 0) { // Killed by a player PlayerData *attacker; if (getPlayerData(attacker_id, &attacker)) return; strcpy((char *)recv_buffer + player_name_len, " was slain by "); strcpy((char *)recv_buffer + player_name_len + 14, attacker->name); recv_buffer[player_name_len + 14 + strlen(attacker->name)] = '\0'; } else if (damage_type == D_cactus) { // Killed by being near a cactus strcpy((char *)recv_buffer + player_name_len, " was pricked to death"); recv_buffer[player_name_len + 21] = '\0'; } else { // Unknown death reason strcpy((char *)recv_buffer + player_name_len, " died"); recv_buffer[player_name_len + 5] = '\0'; } } else player->health -= effective_damage; // Update health on the client sc_setHealth(entity_id, player->health, player->hunger, player->saturation); } else { // The attacked entity is a mob MobData *mob = &mob_data[-entity_id - 2]; uint8_t mob_health = mob->data & 31; // Don't continue if the mob is already dead if (mob_health == 0) return; // Set the mob's panic timer mob->data |= (3 << 6); // Process health change on the server if (mob_health <= damage) { mob->data -= mob_health; mob->y = 0; entity_died = true; // Handle mob drops if (attacker_id > 0) { PlayerData *player; if (getPlayerData(attacker_id, &player)) return; switch (mob->type) { case 25: givePlayerItem(player, I_chicken, 1); break; case 28: givePlayerItem(player, I_beef, 1 + (fast_rand() % 3)); break; case 95: givePlayerItem(player, I_porkchop, 1 + (fast_rand() % 3)); break; case 106: givePlayerItem(player, I_mutton, 1 + (fast_rand() & 1)); break; case 145: givePlayerItem(player, I_rotten_flesh, (fast_rand() % 3)); break; default: break; } } } else mob->data -= damage; } // Broadcast damage event to all players for (int i = 0; i < MAX_PLAYERS; i ++) { int client_fd = player_data[i].client_fd; if (client_fd == -1) continue; sc_damageEvent(client_fd, entity_id, damage_type); // Below this, handle death events if (!entity_died) continue; sc_entityEvent(client_fd, entity_id, 3); if (entity_id >= 0) { // If a player died, broadcast their death message sc_systemChat(client_fd, (char *)recv_buffer, strlen((char *)recv_buffer)); } } } // Simulates events scheduled for regular intervals // Takes the time since the last tick in microseconds as the only arguemnt void handleServerTick (int64_t time_since_last_tick) { // Update world time world_time = (world_time + time_since_last_tick / 50000) % 24000; // Increment server tick counter server_ticks ++; // Update player events for (int i = 0; i < MAX_PLAYERS; i ++) { PlayerData *player = &player_data[i]; if (player->client_fd == -1) continue; // Skip offline players if (player->flags & 0x20) { // Check "client loading" flag // If 3 seconds (60 vanilla ticks) have passed, assume player has loaded player->flagval_16 ++; if (player->flagval_16 > (uint16_t)(3 * TICKS_PER_SECOND)) { handlePlayerJoin(player); } else continue; } // Reset player attack cooldown if (player->flags & 0x01) { if (player->flagval_8 >= (uint8_t)(0.6f * TICKS_PER_SECOND)) { player->flags &= ~0x01; player->flagval_8 = 0; } else player->flagval_8 ++; } // Handle eating animation if (player->flags & 0x10) { if (player->flagval_16 >= (uint16_t)(1.6f * TICKS_PER_SECOND)) { handlePlayerEating(&player_data[i], false); player->flags &= ~0x10; player->flagval_16 = 0; } else player->flagval_16 ++; } // Reset movement update cooldown if not broadcasting every update // Effectively ties player movement updates to the tickrate #ifndef BROADCAST_ALL_MOVEMENT player->flags &= ~0x40; #endif // Below this, process events that happen once per second if (server_ticks % (uint32_t)TICKS_PER_SECOND != 0) continue; // Send Keep Alive and Update Time packets sc_keepAlive(player->client_fd); sc_updateTime(player->client_fd, world_time); // Tick damage from lava uint8_t block = getBlockAt(player->x, player->y, player->z); if (block >= B_lava && block < B_lava + 4) { hurtEntity(player->client_fd, -1, D_lava, 8); } #ifdef ENABLE_CACTUS_DAMAGE // Tick damage from a cactus block if one is under/inside or around the player. if (block == B_cactus || getBlockAt(player->x + 1, player->y, player->z) == B_cactus || getBlockAt(player->x - 1, player->y, player->z) == B_cactus || getBlockAt(player->x, player->y, player->z + 1) == B_cactus || getBlockAt(player->x, player->y, player->z - 1) == B_cactus ) hurtEntity(player->client_fd, -1, D_cactus, 4); #endif // Heal from saturation if player is able and has enough food if (player->health >= 20 || player->health == 0) continue; if (player->hunger < 18) continue; if (player->saturation >= 600) { player->saturation -= 600; player->health ++; } else { player->hunger --; player->health ++; } sc_setHealth(player->client_fd, player->health, player->hunger, player->saturation); } // Perform regular checks for if it's time to write to disk writeDataToDiskOnInterval(); /** * If the RNG seed ever hits 0, it'll never generate anything * else. This is because the fast_rand function uses a simple * XORshift. This isn't a common concern, so we only check for * this periodically. If it does become zero, we reset it to * the world seed as a good-enough fallback. */ if (rng_seed == 0) rng_seed = world_seed; // Tick mob behavior for (int i = 0; i < MAX_MOBS; i ++) { if (mob_data[i].type == 0) continue; int entity_id = -2 - i; // Handle deallocation on mob death if ((mob_data[i].data & 31) == 0) { if (mob_data[i].y < (unsigned int)TICKS_PER_SECOND) { mob_data[i].y ++; continue; } mob_data[i].type = 0; for (int j = 0; j < MAX_PLAYERS; j ++) { if (player_data[j].client_fd == -1) continue; // Spawn death smoke particles sc_entityEvent(player_data[j].client_fd, entity_id, 60); // Remove the entity from the client sc_removeEntity(player_data[j].client_fd, entity_id); } continue; } uint8_t passive = ( mob_data[i].type == 25 || // Chicken mob_data[i].type == 28 || // Cow mob_data[i].type == 95 || // Pig mob_data[i].type == 106 // Sheep ); // Mob "panic" timer, set to 3 after being hit // Currently has no effect on hostile mobs uint8_t panic = (mob_data[i].data >> 6) & 3; // Burn hostile mobs if above ground during sunlight if (!passive && (world_time < 13000 || world_time > 23460) && mob_data[i].y > 48) { hurtEntity(entity_id, -1, D_on_fire, 2); } uint32_t r = fast_rand(); if (passive) { if (panic) { // If panicking, move randomly at up to 4 times per second if (TICKS_PER_SECOND >= 4) { uint32_t ticks_per_panic = (uint32_t)(TICKS_PER_SECOND / 4); if (server_ticks % ticks_per_panic != 0) continue; } // Reset panic state after timer runs out // Each panic timer tick takes one second if (server_ticks % (uint32_t)TICKS_PER_SECOND == 0) { mob_data[i].data -= (1 << 6); } } else { // When not panicking, move idly once per 4 seconds on average if (r % (4 * (unsigned int)TICKS_PER_SECOND) != 0) continue; } } else { // Update hostile mobs once per second if (server_ticks % (uint32_t)TICKS_PER_SECOND != 0) continue; } // Find the player closest to this mob PlayerData* closest_player = &player_data[0]; uint32_t closest_dist = 2147483647; for (int j = 0; j < MAX_PLAYERS; j ++) { if (player_data[j].client_fd == -1) continue; uint16_t curr_dist = ( abs(mob_data[i].x - player_data[j].x) + abs(mob_data[i].z - player_data[j].z) ); if (curr_dist < closest_dist) { closest_dist = curr_dist; closest_player = &player_data[j]; } } // Despawn mobs past a certain distance from nearest player if (closest_dist > MOB_DESPAWN_DISTANCE) { mob_data[i].type = 0; continue; } short old_x = mob_data[i].x, old_z = mob_data[i].z; uint8_t old_y = mob_data[i].y; short new_x = old_x, new_z = old_z; uint8_t new_y = old_y, yaw = 0; if (passive) { // Passive mob movement handling // Move by one block on the X or Z axis // Yaw is set to face in the direction of motion if ((r >> 2) & 1) { if ((r >> 1) & 1) { new_x += 1; yaw = 192; } else { new_x -= 1; yaw = 64; } } else { if ((r >> 1) & 1) { new_z += 1; yaw = 0; } else { new_z -= 1; yaw = 128; } } } else { // Hostile mob movement handling // If we're already next to the player, hurt them and skip movement if (closest_dist < 3 && abs(old_y - closest_player->y) < 2) { hurtEntity(closest_player->client_fd, entity_id, D_generic, 6); continue; } // Move towards the closest player on 8 axis // The condition nesting ensures a correct yaw at 45 degree turns if (closest_player->x < old_x) { new_x -= 1; yaw = 64; if (closest_player->z < old_z) { new_z -= 1; yaw += 32; } else if (closest_player->z > old_z) { new_z += 1; yaw -= 32; } } else if (closest_player->x > old_x) { new_x += 1; yaw = 192; if (closest_player->z < old_z) { new_z -= 1; yaw -= 32; } else if (closest_player->z > old_z) { new_z += 1; yaw += 32; } } else { if (closest_player->z < old_z) { new_z -= 1; yaw = 128; } else if (closest_player->z > old_z) { new_z += 1; yaw = 0; } } } // Holds the block that the mob is moving into uint8_t block = getBlockAt(new_x, new_y, new_z); // Holds the block above the target block, i.e. the "head" block uint8_t block_above = getBlockAt(new_x, new_y + 1, new_z); // Validate movement on X axis if (new_x != old_x && ( !isPassableBlock(getBlockAt(new_x, new_y + 1, old_z)) || ( !isPassableBlock(getBlockAt(new_x, new_y, old_z)) && !isPassableBlock(getBlockAt(new_x, new_y + 2, old_z)) ) )) { new_x = old_x; block = getBlockAt(old_x, new_y, new_z); block_above = getBlockAt(old_x, new_y + 1, new_z); } // Validate movement on Z axis if (new_z != old_z && ( !isPassableBlock(getBlockAt(old_x, new_y + 1, new_z)) || ( !isPassableBlock(getBlockAt(old_x, new_y, new_z)) && !isPassableBlock(getBlockAt(old_x, new_y + 2, new_z)) ) )) { new_z = old_z; block = getBlockAt(new_x, new_y, old_z); block_above = getBlockAt(new_x, new_y + 1, old_z); } // Validate diagonal movement if (new_x != old_x && new_z != old_z && ( !isPassableBlock(block_above) || ( !isPassableBlock(block) && !isPassableBlock(getBlockAt(new_x, new_y + 2, new_z)) ) )) { // We know that movement along just one axis is fine thanks to the // checks above, pick one based on proximity. int dist_x = abs(old_x - closest_player->x); int dist_z = abs(old_z - closest_player->z); if (dist_x < dist_z) new_z = old_z; else new_x = old_x; block = getBlockAt(new_x, new_y, new_z); } // Check if we're supposed to climb/drop one block // The checks above already ensure that there's enough space to climb if (!isPassableBlock(block)) new_y += 1; else if (isPassableBlock(getBlockAt(new_x, new_y - 1, new_z))) new_y -= 1; // Exit early if all movement was cancelled if (new_x == mob_data[i].x && new_z == old_z && new_y == old_y) continue; // Prevent collisions with other mobs uint8_t colliding = false; for (int j = 0; j < MAX_MOBS; j ++) { if (j == i) continue; if (mob_data[j].type == 0) continue; if ( mob_data[j].x == new_x && mob_data[j].z == new_z && abs((int)mob_data[j].y - (int)new_y) < 2 ) { colliding = true; break; } } if (colliding) continue; if ( // Hurt mobs that stumble into lava (block >= B_lava && block < B_lava + 4) || (block_above >= B_lava && block_above < B_lava + 4) ) hurtEntity(entity_id, -1, D_lava, 8); // Store new mob position mob_data[i].x = new_x; mob_data[i].y = new_y; mob_data[i].z = new_z; // Vary the yaw angle to look just a little less robotic yaw += ((r >> 7) & 31) - 16; // Broadcast relevant entity movement packets for (int j = 0; j < MAX_PLAYERS; j ++) { if (player_data[j].client_fd == -1) continue; sc_teleportEntity ( player_data[j].client_fd, entity_id, (double)new_x + 0.5, new_y, (double)new_z + 0.5, yaw * 360 / 256, 0 ); sc_setHeadRotation(player_data[j].client_fd, entity_id, yaw); } } } #ifdef ALLOW_CHESTS // Broadcasts a chest slot update to all clients who have that chest open, // except for the client who initiated the update. void broadcastChestUpdate (int origin_fd, uint8_t *storage_ptr, uint16_t item, uint8_t count, uint8_t slot) { for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == -1) continue; if (player_data[i].flags & 0x20) continue; // Filter for players that have this chest open if (memcmp(player_data[i].craft_items, &storage_ptr, sizeof(storage_ptr)) != 0) continue; // Send slot update packet sc_setContainerSlot(player_data[i].client_fd, 2, slot, count, item); } #ifndef DISK_SYNC_BLOCKS_ON_INTERVAL writeChestChangesToDisk(storage_ptr, slot); #endif } #endif ssize_t writeEntityData (int client_fd, EntityData *data) { writeByte(client_fd, data->index); writeVarInt(client_fd, data->type); switch (data->type) { case 0: // Byte return writeByte(client_fd, data->value.byte); case 21: // Pose writeVarInt(client_fd, data->value.pose); return 0; default: return -1; } } // Returns the networked size of an EntityData entry int sizeEntityData (EntityData *data) { int value_size; switch (data->type) { case 0: // Byte value_size = 1; break; case 21: // Pose value_size = sizeVarInt(data->value.pose); break; default: return -1; } return 1 + sizeVarInt(data->type) + value_size; } // Returns the networked size of an array of EntityData entries int sizeEntityMetadata (EntityData *metadata, size_t length) { int total_size = 0; for (size_t i = 0; i < length; i ++) { int size = sizeEntityData(&metadata[i]); if (size == -1) return -1; total_size += size; } return total_size; }