implement chests

This commit is contained in:
p2r3
2025-08-28 23:51:00 +03:00
parent 1ae481ed3d
commit ca6f8afbea
7 changed files with 173 additions and 20 deletions

View File

@@ -374,7 +374,8 @@ async function convert () {
itemsAndBlocks.blockRegistry["oak_planks"], itemsAndBlocks.blockRegistry["oak_planks"],
itemsAndBlocks.blockRegistry["oak_wood"], itemsAndBlocks.blockRegistry["oak_wood"],
itemsAndBlocks.blockRegistry["oak_slab"], itemsAndBlocks.blockRegistry["oak_slab"],
itemsAndBlocks.blockRegistry["crafting_table"] itemsAndBlocks.blockRegistry["crafting_table"],
itemsAndBlocks.blockRegistry["chest"]
], ],
"mineable/shovel": [ "mineable/shovel": [
itemsAndBlocks.blockRegistry["grass_block"], itemsAndBlocks.blockRegistry["grass_block"],

View File

@@ -41,6 +41,11 @@
#define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT #define SCALE_MOVEMENT_UPDATES_TO_PLAYER_COUNT
// If defined, calculates fluid flow when blocks are updated near fluids // If defined, calculates fluid flow when blocks are updated near fluids
#define DO_FLUID_FLOW #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 // If defined, enables flight for all players
#define ENABLE_PLAYER_FLIGHT #define ENABLE_PLAYER_FLIGHT

View File

@@ -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 hurtEntity (int entity_id, int attacker_id, uint8_t damage_type, uint8_t damage);
void handleServerTick (int64_t time_since_last_tick); 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 #endif

View File

@@ -261,17 +261,15 @@ void getCraftingOutput (PlayerData *player, uint8_t *count, uint16_t *item) {
break; break;
case 8: case 8:
switch (first_item) {
case I_cobblestone:
if (identical && player->craft_items[first + 4] == 0) { if (identical && player->craft_items[first + 4] == 0) {
*item = I_furnace; switch (first_item) {
*count = 1; case I_cobblestone: *item = I_furnace; *count = 1; return;
return; #ifdef ALLOW_CHESTS
} case I_oak_planks: *item = I_chest; *count = 1; return;
break; #endif
default: break; default: break;
} }
}
break; break;
case 9: case 9:

View File

@@ -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 // be overlayed here. This seems to be cheaper than sending actual
// block light data. // block light data.
for (int i = 0; i < block_changes_count; i ++) { for (int i = 0; i < block_changes_count; i ++) {
#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; 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].x < x || block_changes[i].x >= x + 16) continue;
if (block_changes[i].z < z || block_changes[i].z >= z + 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; return 0;
@@ -587,16 +591,43 @@ int cs_clickContainer (int client_fd) {
uint16_t item; uint16_t item;
int tmp; 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 ++) { for (int i = 0; i < changes_count; i ++) {
slot = clientSlotToServerSlot(window_id, readUint16(client_fd)); slot = clientSlotToServerSlot(window_id, readUint16(client_fd));
// slots outside of the inventory overflow into the crafting buffer // slots outside of the inventory overflow into the crafting buffer
if (slot > 40 && apply_changes) craft = true; 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 (!readByte(client_fd)) { // no item?
if (slot != 255 && apply_changes) { if (slot != 255 && apply_changes) {
player->inventory_items[slot] = 0; *p_item = 0;
player->inventory_count[slot] = 0; *p_count = 0;
#ifdef ALLOW_CHESTS
if (window_id == 2 && slot > 40) {
broadcastChestUpdate(client_fd, storage_ptr, 0, 0, slot - 41);
}
#endif
} }
continue; continue;
} }
@@ -611,8 +642,13 @@ int cs_clickContainer (int client_fd) {
recv_all(client_fd, recv_buffer, tmp, false); recv_all(client_fd, recv_buffer, tmp, false);
if (count > 0 && apply_changes) { if (count > 0 && apply_changes) {
player->inventory_items[slot] = item; *p_item = item;
player->inventory_count[slot] = count; *p_count = count;
#ifdef ALLOW_CHESTS
if (window_id == 2 && slot > 40) {
broadcastChestUpdate(client_fd, storage_ptr, item, count, slot - 41);
}
#endif
} }
} }
@@ -740,13 +776,16 @@ int cs_closeContainer (int client_fd) {
if (getPlayerData(client_fd, &player)) return 1; if (getPlayerData(client_fd, &player)) return 1;
// return all items in crafting slots to the player // 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 ++) { for (uint8_t i = 0; i < 9; i ++) {
if (window_id != 2) {
givePlayerItem(player, player->craft_items[i], player->craft_count[i]); givePlayerItem(player, player->craft_items[i], player->craft_count[i]);
player->craft_items[i] = 0;
player->craft_count[i] = 0;
uint8_t client_slot = serverSlotToClientSlot(window_id, 41 + 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); 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;
}
givePlayerItem(player, player->flagval_16, player->flagval_8); givePlayerItem(player, player->flagval_16, player->flagval_8);
sc_setCursorItem(client_fd, 0, 0); sc_setCursorItem(client_fd, 0, 0);

View File

@@ -181,6 +181,17 @@ uint8_t clientSlotToServerSlot (int window_id, uint8_t slot) {
if (slot >= 3 && slot <= 38) return clientSlotToServerSlot(0, slot + 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 >= 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; return 255;
} }
@@ -297,6 +308,10 @@ uint8_t getBlockChange (short x, uint8_t y, short z) {
block_changes[i].y == y && block_changes[i].y == y &&
block_changes[i].z == z block_changes[i].z == z
) return block_changes[i].block; ) return block_changes[i].block;
#ifdef ALLOW_CHESTS
// Skip chest contents
if (block_changes[i].block == B_chest) i += 14;
#endif
} }
return 0xFF; 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].y == y &&
block_changes[i].z == z 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; if (is_base_block) block_changes[i].block = 0xFF;
else block_changes[i].block = block; else block_changes[i].block = block;
return; 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 // Don't create a new entry if it contains the base terrain block
if (is_base_block) return; 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 // Fall back to storing the change at the first possible gap
block_changes[first_gap].x = x; block_changes[first_gap].x = x;
block_changes[first_gap].y = y; block_changes[first_gap].y = y;
@@ -841,6 +897,37 @@ void handlePlayerUseItem (PlayerData *player, short x, short y, short z, uint8_t
return; 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 // 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

View File

@@ -380,7 +380,11 @@ uint8_t buildChunkSection (int cx, int cy, int cz) {
// runs per block, as this is more expensive than terrain generation. // runs per block, as this is more expensive than terrain generation.
for (int i = 0; i < block_changes_count; i ++) { for (int i = 0; i < block_changes_count; i ++) {
if (block_changes[i].block == 0xFF) continue; 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; 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 if ( // Check if block is within this chunk section
block_changes[i].x >= cx && block_changes[i].x < cx + 16 && block_changes[i].x >= cx && block_changes[i].x < cx + 16 &&
block_changes[i].y >= cy && block_changes[i].y < cy + 16 && block_changes[i].y >= cy && block_changes[i].y < cy + 16 &&