diff --git a/include/globals.h b/include/globals.h index dfcae0e..3bb3502 100644 --- a/include/globals.h +++ b/include/globals.h @@ -30,6 +30,9 @@ #define VIEW_DISTANCE 2 // Time between server ticks in microseconds (default = 1s) #define TIME_BETWEEN_TICKS 1000000 +// Calculated from TIME_BETWEEN_TICKS +#define TICKS_PER_SECOND (1000000 / TIME_BETWEEN_TICKS) +#define TICKS_TO_EAT (unsigned int)(1.6f * TICKS_PER_SECOND) // How many visited chunks to "remember" // The server will not re-send chunks that the player has recently been in #define VISITED_HISTORY 4 @@ -88,16 +91,23 @@ typedef struct { uint8_t grounded_y; uint8_t health; uint8_t hunger; + uint16_t saturation; uint8_t hotbar; uint16_t inventory_items[41]; uint16_t craft_items[9]; - uint16_t cursor_item; uint8_t inventory_count[41]; uint8_t craft_count[9]; - uint8_t cursor_count; + // Usage depends on player's flags, see below + // When no flags are set, acts as cursor item ID + uint16_t flagval_16; + // Usage depends on player's flags, see below + // When no flags are set, acts as cursor item count + uint8_t flagval_8; // 0x01 - attack cooldown // 0x02 - has not spawned yet // 0x04 - sneaking + // 0x08 - sprinting + // 0x10 - eating, makes extra8 act as eating timer uint8_t flags; } PlayerData; diff --git a/include/packets.h b/include/packets.h index db0d5a2..0a3eebf 100644 --- a/include/packets.h +++ b/include/packets.h @@ -8,6 +8,7 @@ int cs_clientInformation (int client_fd); int cs_pluginMessage (int client_fd); int cs_playerAction (int client_fd); int cs_useItemOn (int client_fd); +int cs_useItem (int client_fd); int cs_setPlayerPositionAndRotation (int client_fd, double *x, double *y, double *z, float *yaw, float *pitch, uint8_t *on_ground); int cs_setPlayerPosition (int client_fd, double *x, double *y, double *z, uint8_t *on_ground); int cs_setPlayerRotation (int client_fd, float *yaw, float *pitch, uint8_t *on_ground); @@ -16,7 +17,10 @@ int cs_setHeldItem (int client_fd); int cs_clickContainer (int client_fd); int cs_closeContainer (int client_fd); int cs_clientStatus (int client_fd); -int cs_chat(int client_fd); +int cs_chat (int client_fd); +int cs_interact (int client_fd); +int cs_playerInput (int client_fd); +int cs_playerCommand (int client_fd); // Clientbound packets int sc_loginSuccess (int client_fd, uint8_t *uuid, char *name); @@ -47,10 +51,8 @@ int sc_damageEvent (int client_fd, int id, int type); int sc_setHealth (int client_fd, uint8_t health, uint8_t food); int sc_respawn (int client_fd); int sc_systemChat (int client_fd, char* message, uint16_t len); -int cs_interact (int client_fd); int sc_entityEvent (int client_fd, int entity_id, uint8_t status); int sc_removeEntity (int client_fd, int entity_id); -int cs_playerInput (int client_fd); int sc_registries (int client_fd); #endif diff --git a/src/globals.c b/src/globals.c index 583d74a..112b26e 100644 --- a/src/globals.c +++ b/src/globals.c @@ -29,7 +29,7 @@ uint8_t recv_buffer[256] = {0}; uint32_t world_seed = 0xA103DE6C; uint32_t rng_seed = 0xE2B9419; -uint16_t world_time = 0; +uint16_t world_time = 13000; uint16_t client_count; diff --git a/src/main.c b/src/main.c index 8176e69..39462ef 100644 --- a/src/main.c +++ b/src/main.c @@ -216,6 +216,20 @@ void handlePacket (int client_fd, int length, int packet_id) { // Don't continue if all we got was rotation data if (packet_id == 0x1F) break; + // Players send movement packets roughly 20 times per second when + // moving, and much less frequently when standing still. We can + // use this correlation between actions and packet count to cheaply + // simulate hunger with a timer-based system, where the timer ticks + // down with each position packet. The timer value itself then + // naturally works as a substitute for saturation. + if (player->saturation == 0) { + if (player->hunger > 0) player->hunger--; + player->saturation = 200; + sc_setHealth(client_fd, player->health, player->hunger); + } else if (player->flags & 0x08) { + player->saturation -= 1; + } + // Cast the values to short to get integer position short cx = x, cy = y, cz = z; // Determine the player's chunk coordinates @@ -337,6 +351,10 @@ void handlePacket (int client_fd, int length, int packet_id) { } break; + case 0x29: + if (state == STATE_PLAY) cs_playerCommand(client_fd); + break; + case 0x2A: if (state == STATE_PLAY) cs_playerInput(client_fd); break; @@ -353,6 +371,10 @@ void handlePacket (int client_fd, int length, int packet_id) { if (state == STATE_PLAY) cs_useItemOn(client_fd); break; + case 0x40: + if (state == STATE_PLAY) cs_useItem(client_fd); + break; + default: #ifdef DEV_LOG_UNKNOWN_PACKETS printf("Unknown packet: 0x"); diff --git a/src/packets.c b/src/packets.c index f0c9ee8..57f85b6 100644 --- a/src/packets.c +++ b/src/packets.c @@ -477,6 +477,23 @@ int sc_openScreen (int client_fd, uint8_t window, const char *title, uint16_t le return 0; } +// C->S Use Item +int cs_useItem (int client_fd) { + + uint8_t hand = readByte(client_fd); + int sequence = readVarInt(client_fd); + + // Ignore yaw/pitch + recv_all(client_fd, recv_buffer, 8, false); + + PlayerData *player; + if (getPlayerData(client_fd, &player)) return 1; + + handlePlayerUseItem(player, 0, 0, 0, 255); + + return 0; +} + // C->S Use Item On int cs_useItemOn (int client_fd) { @@ -535,13 +552,13 @@ int cs_clickContainer (int client_fd) { } else if (mode == 0 && clicked_slot == -999) { // when clicking outside inventory, return the dropped item to the player if (button == 0) { - givePlayerItem(player, player->cursor_item, player->cursor_count); - player->cursor_item = 0; - player->cursor_count = 0; + givePlayerItem(player, player->flagval_16, player->flagval_8); + player->flagval_16 = 0; + player->flagval_8 = 0; } else { - givePlayerItem(player, player->cursor_item, 1); - player->cursor_count -= 1; - if (player->cursor_count == 0) player->cursor_item = 0; + givePlayerItem(player, player->flagval_16, 1); + player->flagval_8 -= 1; + if (player->flagval_8 == 0) player->flagval_16 = 0; } apply_changes = false; } @@ -593,16 +610,16 @@ int cs_clickContainer (int client_fd) { // assign cursor-carried item slot if (readByte(client_fd)) { - player->cursor_item = readVarInt(client_fd); - player->cursor_count = readVarInt(client_fd); + player->flagval_16 = readVarInt(client_fd); + player->flagval_8 = readVarInt(client_fd); // ignore components tmp = readVarInt(client_fd); recv_all(client_fd, recv_buffer, tmp, false); tmp = readVarInt(client_fd); recv_all(client_fd, recv_buffer, tmp, false); } else { - player->cursor_item = 0; - player->cursor_count = 0; + player->flagval_16 = 0; + player->flagval_8 = 0; } return 0; @@ -711,10 +728,10 @@ int cs_closeContainer (int client_fd) { if (client_slot != 255) sc_setContainerSlot(player->client_fd, window_id, client_slot, 0, 0); } - givePlayerItem(player, player->cursor_item, player->cursor_count); + givePlayerItem(player, player->flagval_16, player->flagval_8); sc_setCursorItem(client_fd, 0, 0); - player->cursor_item = 0; - player->cursor_count = 0; + player->flagval_16 = 0; + player->flagval_8 = 0; return 0; } @@ -1054,6 +1071,22 @@ int cs_playerInput (int client_fd) { return 0; } +int cs_playerCommand (int client_fd) { + + readVarInt(client_fd); // Ignore entity ID + uint8_t action = readByte(client_fd); + readVarInt(client_fd); // Ignore "Jump Boost" value + + PlayerData *player; + if (getPlayerData(client_fd, &player)) return 1; + + // Handle sprinting + if (action == 1) player->flags |= 0x08; + else if (action == 2) player->flags &= ~0x08; + + return 0; +} + // S->C Registry Data (multiple packets) and Update Tags (configuration, multiple packets) int sc_registries (int client_fd) { diff --git a/src/procedures.c b/src/procedures.c index 145b2cb..be28a0e 100644 --- a/src/procedures.c +++ b/src/procedures.c @@ -47,6 +47,7 @@ int getClientIndex (int client_fd) { void resetPlayerData (PlayerData *player) { player->health = 20; player->hunger = 20; + player->saturation = 2500; player->x = 8; player->z = 8; player->y = 80; @@ -571,7 +572,62 @@ uint8_t getItemStackSize (uint16_t item) { return 64; } -void handlePlayerAction (PlayerData *player, int action, short x, short y, short z) { +// 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; + 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_setHealth(player->client_fd, player->health, player->hunger); + sc_entityEvent(player->client_fd, player->client_fd, 9); + sc_setContainerSlot( + player->client_fd, 0, + serverSlotToClientSlot(0, player->hotbar), + *held_count, *held_item + ); + + return true; +} + +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) { @@ -584,6 +640,13 @@ void handlePlayerAction (PlayerData *player, int action, short x, short y, short 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; @@ -621,11 +684,43 @@ void handlePlayerAction (PlayerData *player, int action, short x, short y, short y_offset ++; block_above = getBlockAt(x, y + y_offset, z); } - } void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t face) { + // If the selected slot doesn't hold any items, exit + uint8_t *count = &player->inventory_count[player->hotbar]; + if (*count == 0) return; + + // Check special item handling + uint16_t *item = &player->inventory_items[player->hotbar]; + if (*item == I_bone_meal) { + uint8_t target = getBlockAt(x, y, z); + 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; + } + + // Exit if no coordinates were provided + if (face == 255) return; + // Check interaction with containers when not sneaking if (!(player->flags & 0x04)) { uint8_t target = getBlockAt(x, y, z); @@ -655,32 +750,6 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t } } - // If the selected slot doesn't hold any items, exit - uint8_t *count = &player->inventory_count[player->hotbar]; - if (*count == 0) return; - - // Check special item handling - uint16_t *item = &player->inventory_items[player->hotbar]; - if (*item == I_bone_meal) { - uint8_t target = getBlockAt(x, y, z); - 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); - } - } - } - // If the selected item doesn't correspond to a block, exit uint8_t block = I_to_B(*item); if (block == 0) return; @@ -817,16 +886,35 @@ void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t da // Takes the time since the last tick in microseconds as the only arguemnt void handleServerTick (int64_t time_since_last_tick) { - // Send Keep Alive and Update Time packets to all in-game clients + // Update world time world_time = (world_time + time_since_last_tick / 50000) % 24000; + + // Update player events for (int i = 0; i < MAX_PLAYERS; i ++) { if (player_data[i].client_fd == -1) continue; + // Send Keep Alive and Update Time packets sc_keepAlive(player_data[i].client_fd); sc_updateTime(player_data[i].client_fd, world_time); - // Reset attack cooldown if at least a second has passed - if (time_since_last_tick >= 1000000) { - player_data[i].flags &= ~0x01; + // Reset player attack cooldown + player_data[i].flags &= ~0x01; + // Handle eating animation + if (player_data[i].flags & 0x10) { + if (player_data[i].flagval_16 >= TICKS_TO_EAT) { + handlePlayerEating(&player_data[i], false); + player_data[i].flags &= ~0x10; + player_data[i].flagval_16 = 0; + } else player_data[i].flagval_16 ++; } + // Heal from saturation + if (player_data[i].health >= 20) continue; + if (player_data[i].saturation >= 600) { + player_data[i].saturation -= 600; + player_data[i].health ++; + } else if (player_data[i].hunger > 17) { + player_data[i].hunger --; + player_data[i].health ++; + } + sc_setHealth(player_data[i].client_fd, player_data[i].health, player_data[i].hunger); } /**