From 728d49f7b6847c02085229e7047d5976fc60f2e2 Mon Sep 17 00:00:00 2001 From: p2r3 Date: Fri, 29 Aug 2025 02:49:41 +0300 Subject: [PATCH] implement syncing world to file on disk --- .gitignore | 1 + include/globals.h | 10 ++- include/serialize.h | 21 ++++++ src/main.c | 5 +- src/procedures.c | 12 +++- src/serialize.c | 169 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 3 deletions(-) create mode 100644 include/serialize.h create mode 100644 src/serialize.c diff --git a/.gitignore b/.gitignore index 27f123f..98ed329 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ notchian bareiron src/registries.c include/registries.h +*.bin diff --git a/include/globals.h b/include/globals.h index 586e170..25d4b45 100644 --- a/include/globals.h +++ b/include/globals.h @@ -35,6 +35,14 @@ // How many visited chunk coordinates to "remember" // The server will not re-send chunks that the player has recently been in #define VISITED_HISTORY 4 +// How many player-made block changes to allow +// Determines the fixed amount of memory allocated to blocks +#define MAX_BLOCK_CHANGES 20000 +// If defined, writes and reads world data to/from disk (PC only). +// This is a synchronous operation, and can cause performance issues if +// frequent random disk access is slow. Data is still stored in and +// accessed from memory - reading from disk is only done on startup. +#define SYNC_WORLD_TO_DISK // If defined, scales the frequency at which player movement updates are // broadcast based on the amount of players, reducing overhead for higher // player counts. For very many players, makes movement look jittery. @@ -137,7 +145,7 @@ typedef struct { #pragma pack(pop) -extern BlockChange block_changes[20000]; +extern BlockChange block_changes[MAX_BLOCK_CHANGES]; extern int block_changes_count; extern PlayerData player_data[MAX_PLAYERS]; diff --git a/include/serialize.h b/include/serialize.h new file mode 100644 index 0000000..172cd2e --- /dev/null +++ b/include/serialize.h @@ -0,0 +1,21 @@ +#ifndef SERIALIZE_H +#define SERIALIZE_H + +#include + +#include "globals.h" + +#if defined(SYNC_WORLD_TO_DISK) && !defined(ESP_PLATFORM) + int initSerializer (); + void writeBlockChangesToDisk (int from, int to); + void writeChestChangesToDisk (uint8_t *storage_ptr, uint8_t slot); + void writePlayerDataToDisk (); +#else + // Define no-op placeholders for when disk syncing isn't enabled + #define writeBlockChangesToDisk(a, b) + #define writeChestChangesToDisk(a, b) + #define writePlayerDataToDisk() + #define initSerializer() 0 +#endif + +#endif diff --git a/src/main.c b/src/main.c index bfdd4cd..8e1b9c9 100644 --- a/src/main.c +++ b/src/main.c @@ -31,6 +31,7 @@ #include "worldgen.h" #include "registries.h" #include "procedures.h" +#include "serialize.h" void handlePacket (int client_fd, int length, int packet_id, int state) { @@ -456,10 +457,12 @@ int main () { for (int i = 3; i >= 0; i --) printf("%X", (unsigned int)((rng_seed >> (8 * i)) & 255)); printf("\n\n"); - for (int i = 0; i < sizeof(block_changes) / sizeof(BlockChange); i ++) { + for (int i = 0; i < MAX_BLOCK_CHANGES; i ++) { block_changes[i].block = 0xFF; } + if (initSerializer()) exit(EXIT_FAILURE); + int server_fd, opt = 1; struct sockaddr_in server_addr, client_addr; socklen_t addr_len = sizeof(client_addr); diff --git a/src/procedures.c b/src/procedures.c index b2fbdb8..77daee7 100644 --- a/src/procedures.c +++ b/src/procedures.c @@ -8,6 +8,7 @@ #include "registries.h" #include "worldgen.h" #include "structures.h" +#include "serialize.h" #include "procedures.h" int client_states[MAX_PLAYERS * 2]; @@ -365,7 +366,7 @@ void makeBlockChange (short x, uint8_t y, short z, uint8_t block) { #endif if (is_base_block) block_changes[i].block = 0xFF; else block_changes[i].block = block; - return; + return writeBlockChangesToDisk(i, i); } } @@ -401,6 +402,8 @@ void makeBlockChange (short x, uint8_t y, short z, uint8_t block) { if (i >= block_changes_count) { block_changes_count = i + 1; } + // Write changes to disk (if applicable) + writeBlockChangesToDisk(last_real_entry + 1, last_real_entry + 15); break; } return; @@ -412,6 +415,8 @@ void makeBlockChange (short x, uint8_t y, short z, uint8_t block) { block_changes[first_gap].y = y; block_changes[first_gap].z = z; block_changes[first_gap].block = block; + // Write change to disk (if applicable) + writeBlockChangesToDisk(first_gap, first_gap); // Extend future search range if we've appended to the end if (first_gap == block_changes_count) { block_changes_count ++; @@ -1145,6 +1150,9 @@ void handleServerTick (int64_t time_since_last_tick) { sc_setHealth(player_data[i].client_fd, player_data[i].health, player_data[i].hunger, player_data[i].saturation); } + // Write player data to file (if applicable) + writePlayerDataToDisk(); + /** * If the RNG seed ever hits 0, it'll never generate anything * else. This is because the fast_rand function uses a simple @@ -1316,5 +1324,7 @@ void broadcastChestUpdate (int origin_fd, uint8_t *storage_ptr, uint16_t item, u sc_setContainerSlot(player_data[i].client_fd, 2, slot, count, item); } + writeChestChangesToDisk(storage_ptr, slot); + } #endif diff --git a/src/serialize.c b/src/serialize.c new file mode 100644 index 0000000..627947d --- /dev/null +++ b/src/serialize.c @@ -0,0 +1,169 @@ +#include "globals.h" + +#if defined(SYNC_WORLD_TO_DISK) && !defined(ESP_PLATFORM) + +#include +#include + +#include "registries.h" +#include "serialize.h" + +// Restores world data from disk, or writes world file if it doesn't exist +int initSerializer () { + + // Attempt to open existing world file + FILE *file = fopen("world.bin", "rb"); + if (file) { + + // Read block changes from the start of the file directly into memory + size_t read = fread(block_changes, 1, sizeof(block_changes), file); + if (read != sizeof(block_changes)) { + printf("Read %u bytes from \"world.bin\", expected %u (block changes). Aborting.\n", read, sizeof(block_changes)); + return 1; + } + // Find the index of the last occupied entry to recover block_changes_count + for (int i = 0; i < MAX_BLOCK_CHANGES; i ++) { + if (block_changes[i].block == 0xFF) continue; + if (block_changes[i].block == B_chest) i += 14; + if (i >= block_changes_count) block_changes_count = i + 1; + } + // Seek past block changes to start reading player data + if (fseek(file, sizeof(block_changes), SEEK_SET) != 0) { + perror("Failed to seek to player data in \"world.bin\". Aborting."); + return 1; + } + // Read player data directly into memory + read = fread(player_data, 1, sizeof(player_data), file); + fclose(file); + if (read != sizeof(player_data)) { + printf("Read %u bytes from \"world.bin\", expected %u (player data). Aborting.\n", read, sizeof(player_data)); + return 1; + } + + } else { // World file doesn't exist or failed to open + printf("No \"world.bin\" file found, creating one...\n\n"); + + // Try to create the file in binary write mode + file = fopen("world.bin", "wb"); + if (!file) { + perror( + "Failed to open \"world.bin\" for writing.\n" + "Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"." + ); + return 1; + } + // Write initial block changes array + // This should be done after all entries have had `block` set to 0xFF + size_t written = fwrite(block_changes, 1, sizeof(block_changes), file); + if (written != sizeof(block_changes)) { + perror( + "Failed to write initial block data to \"world.bin\".\n" + "Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"." + ); + return 1; + } + // Seek past written block changes to start writing player data + if (fseek(file, sizeof(block_changes), SEEK_SET) != 0) { + perror( + "Failed to seek past block changes in \"world.bin\"." + "Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"." + ); + return 1; + } + // Write initial player data to disk (should be just nulls?) + written = fwrite(player_data, 1, sizeof(player_data), file); + fclose(file); + if (written != sizeof(player_data)) { + perror( + "Failed to write initial player data to \"world.bin\".\n" + "Consider checking permissions or disabling SYNC_WORLD_TO_DISK in \"globals.h\"." + ); + return 1; + } + + } + +} + +// Writes a range of block change entries to disk +void writeBlockChangesToDisk (int from, int to) { + + // Try to open the file in rw (without overwriting) + FILE *file = fopen("world.bin", "r+b"); + if (!file) { + perror("Failed to open \"world.bin\". Block updates have been dropped."); + return; + } + + for (int i = from; i <= to; i ++) { + // Seek to relevant offset in file + if (fseek(file, i * sizeof(BlockChange), SEEK_SET) != 0) { + fclose(file); + perror("Failed to seek in \"world.bin\". Block updates have been dropped."); + return; + } + // Write block change entry to file + if (fwrite(&block_changes[i], 1, sizeof(BlockChange), file) != sizeof(BlockChange)) { + fclose(file); + perror("Failed to write to \"world.bin\". Block updates have been dropped."); + return; + } + } + + fclose(file); +} + +// Writes all player data to disk +void writePlayerDataToDisk () { + + // Try to open the file in rw (without overwriting) + FILE *file = fopen("world.bin", "r+b"); + if (!file) { + perror("Failed to open \"world.bin\". Player updates have been dropped."); + return; + } + // Seek past block changes in file + if (fseek(file, sizeof(block_changes), SEEK_SET) != 0) { + fclose(file); + perror("Failed to seek in \"world.bin\". Player updates have been dropped."); + return; + } + // Write full player data array to file + // Since this is a bigger write, it should ideally be done infrequently + if (fwrite(&player_data, 1, sizeof(player_data), file) != sizeof(player_data)) { + fclose(file); + perror("Failed to write to \"world.bin\". Player updates have been dropped."); + return; + } + + fclose(file); +} + +#ifdef ALLOW_CHESTS +// Writes a chest slot change to disk +void writeChestChangesToDisk (uint8_t *storage_ptr, uint8_t slot) { + /** + * More chest-related memory hacks!! + * + * Since chests are implemented in the block_changes array, any + * changes to the contents of a chest have to be synced to the block + * changes part of the world file. The index of the "blocks" is + * determined as such: + * + * The storage pointer points to the block entry directly following + * the chest itself. To get the index of this entry, we can subtract + * the pointer to the block changes array (cast to uint8_t*) from the + * storage pointer. This gets us the amount of bytes between the start + * of the block changes array and the chest's item data, as a pointer. + * To get the actual block index, we cast this weird pointer to an + * integer, and divide it by the byte size of the BlockChange struct. + * Finally, the chest slot divided by 2 is added to this index to get + * the block entry pertaining to the relevant chest slot, as each + * entry encodes exactly 2 slots. + */ + int index = (int)(storage_ptr - (uint8_t *)block_changes) / sizeof(BlockChange) + slot / 2; + writeBlockChangesToDisk(index, index); +} +#endif + +#endif