diff --git a/build_registries.js b/build_registries.js index 2114614..0761a87 100644 --- a/build_registries.js +++ b/build_registries.js @@ -374,7 +374,8 @@ async function convert () { itemsAndBlocks.blockRegistry["oak_planks"], itemsAndBlocks.blockRegistry["oak_wood"], itemsAndBlocks.blockRegistry["oak_slab"], - itemsAndBlocks.blockRegistry["crafting_table"] + itemsAndBlocks.blockRegistry["crafting_table"], + itemsAndBlocks.blockRegistry["chest"] ], "mineable/shovel": [ itemsAndBlocks.blockRegistry["grass_block"], diff --git a/include/globals.h b/include/globals.h index 78a675f..586e170 100644 --- a/include/globals.h +++ b/include/globals.h @@ -41,6 +41,11 @@ #define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT // If defined, calculates fluid flow when blocks are updated near fluids #define DO_FLUID_FLOW +// If defined, allows players to craft and use chests. +// Chests take up 15 block change slots each, require additional checks, +// and use some terrible memory hacks to function. On some platforms, this +// could cause bad performance or even crashes during gameplay. +#define ALLOW_CHESTS // If defined, enables flight for all players #define ENABLE_PLAYER_FLIGHT diff --git a/include/procedures.h b/include/procedures.h index 9bf34a1..a2b327a 100644 --- a/include/procedures.h +++ b/include/procedures.h @@ -43,4 +43,6 @@ void spawnMob (uint8_t type, short x, uint8_t y, short z, uint8_t health); void hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t damage); void handleServerTick (int64_t time_since_last_tick); +void broadcastChestUpdate (int origin_fd, uint8_t *storage_ptr, uint16_t item, uint8_t count, uint8_t slot); + #endif diff --git a/src/crafting.c b/src/crafting.c index 26cf782..036f5a5 100644 --- a/src/crafting.c +++ b/src/crafting.c @@ -261,16 +261,14 @@ void getCraftingOutput (PlayerData *player, uint8_t *count, uint16_t *item) { break; case 8: - switch (first_item) { - case I_cobblestone: - if (identical && player->craft_items[first + 4] == 0) { - *item = I_furnace; - *count = 1; - return; - } - break; - - default: break; + if (identical && player->craft_items[first + 4] == 0) { + switch (first_item) { + case I_cobblestone: *item = I_furnace; *count = 1; return; + #ifdef ALLOW_CHESTS + case I_oak_planks: *item = I_chest; *count = 1; return; + #endif + default: break; + } } break; diff --git a/src/packets.c b/src/packets.c index 627d849..6085140 100644 --- a/src/packets.c +++ b/src/packets.c @@ -392,10 +392,14 @@ int sc_chunkDataAndUpdateLight (int client_fd, int _x, int _z) { // be overlayed here. This seems to be cheaper than sending actual // block light data. for (int i = 0; i < block_changes_count; i ++) { - if (block_changes[i].block != B_torch) continue; + #ifdef ALLOW_CHESTS + if (block_changes[i].block != B_torch && block_changes[i].block != B_chest) continue; + #else + if (block_changes[i].block != B_torch) continue; + #endif if (block_changes[i].x < x || block_changes[i].x >= x + 16) continue; if (block_changes[i].z < z || block_changes[i].z >= z + 16) continue; - sc_blockUpdate(client_fd, block_changes[i].x, block_changes[i].y, block_changes[i].z, B_torch); + sc_blockUpdate(client_fd, block_changes[i].x, block_changes[i].y, block_changes[i].z, block_changes[i].block); } return 0; @@ -587,16 +591,43 @@ int cs_clickContainer (int client_fd) { uint16_t item; int tmp; + uint16_t *p_item; + uint8_t *p_count; + + #ifdef ALLOW_CHESTS + // See the handlePlayerUseItem function for more info on this hack + uint8_t *storage_ptr; + memcpy(&storage_ptr, player->craft_items, sizeof(storage_ptr)); + #endif + for (int i = 0; i < changes_count; i ++) { slot = clientSlotToServerSlot(window_id, readUint16(client_fd)); // slots outside of the inventory overflow into the crafting buffer if (slot > 40 && apply_changes) craft = true; + #ifdef ALLOW_CHESTS + if (window_id == 2 && slot > 40) { + // Get item pointers from the player's storage pointer + // See the handlePlayerUseItem function for more info on this hack + p_item = (uint16_t *)(storage_ptr + (slot - 41) * 3); + p_count = storage_ptr + (slot - 41) * 3 + 2; + } else + #endif + { + p_item = &player->inventory_items[slot]; + p_count = &player->inventory_count[slot]; + } + if (!readByte(client_fd)) { // no item? if (slot != 255 && apply_changes) { - player->inventory_items[slot] = 0; - player->inventory_count[slot] = 0; + *p_item = 0; + *p_count = 0; + #ifdef ALLOW_CHESTS + if (window_id == 2 && slot > 40) { + broadcastChestUpdate(client_fd, storage_ptr, 0, 0, slot - 41); + } + #endif } continue; } @@ -611,8 +642,13 @@ int cs_clickContainer (int client_fd) { recv_all(client_fd, recv_buffer, tmp, false); if (count > 0 && apply_changes) { - player->inventory_items[slot] = item; - player->inventory_count[slot] = count; + *p_item = item; + *p_count = count; + #ifdef ALLOW_CHESTS + if (window_id == 2 && slot > 40) { + broadcastChestUpdate(client_fd, storage_ptr, item, count, slot - 41); + } + #endif } } @@ -740,12 +776,15 @@ int cs_closeContainer (int client_fd) { if (getPlayerData(client_fd, &player)) return 1; // return all items in crafting slots to the player + // or, in the case of chests, simply clear the storage pointer for (uint8_t i = 0; i < 9; i ++) { - givePlayerItem(player, player->craft_items[i], player->craft_count[i]); + if (window_id != 2) { + givePlayerItem(player, player->craft_items[i], player->craft_count[i]); + uint8_t client_slot = serverSlotToClientSlot(window_id, 41 + i); + if (client_slot != 255) sc_setContainerSlot(player->client_fd, window_id, client_slot, 0, 0); + } player->craft_items[i] = 0; player->craft_count[i] = 0; - uint8_t client_slot = serverSlotToClientSlot(window_id, 41 + i); - if (client_slot != 255) sc_setContainerSlot(player->client_fd, window_id, client_slot, 0, 0); } givePlayerItem(player, player->flagval_16, player->flagval_8); diff --git a/src/procedures.c b/src/procedures.c index 9e19dc4..d953c9c 100644 --- a/src/procedures.c +++ b/src/procedures.c @@ -181,6 +181,17 @@ uint8_t clientSlotToServerSlot (int window_id, uint8_t slot) { 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 >= 0 && 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; } @@ -297,6 +308,10 @@ uint8_t getBlockChange (short x, uint8_t y, short z) { 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; } @@ -342,6 +357,12 @@ void makeBlockChange (short x, uint8_t y, short z, uint8_t block) { 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 block_changes[i].block = block; return; @@ -351,6 +372,41 @@ void makeBlockChange (short x, uint8_t y, short z, uint8_t block) { // Don't create a new entry if it contains the base terrain block if (is_base_block) return; + #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 (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; + } + break; + } + return; + } + #endif + // Fall back to storing the change at the first possible gap block_changes[first_gap].x = x; block_changes[first_gap].y = y; @@ -841,6 +897,37 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t 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; + 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; + } + // 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 @@ -1214,3 +1301,20 @@ void handleServerTick (int64_t time_since_last_tick) { } } + +#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); + } + +} +#endif diff --git a/src/worldgen.c b/src/worldgen.c index 048d8d6..2107bbc 100644 --- a/src/worldgen.c +++ b/src/worldgen.c @@ -380,7 +380,11 @@ uint8_t buildChunkSection (int cx, int cy, int cz) { // runs per block, as this is more expensive than terrain generation. for (int i = 0; i < block_changes_count; i ++) { if (block_changes[i].block == 0xFF) continue; + // Skip blocks that behave better when sent using a block update if (block_changes[i].block == B_torch) continue; + #ifdef ALLOW_CHESTS + if (block_changes[i].block == B_chest) continue; + #endif if ( // Check if block is within this chunk section block_changes[i].x >= cx && block_changes[i].x < cx + 16 && block_changes[i].y >= cy && block_changes[i].y < cy + 16 &&