diff --git a/build_registries.js b/build_registries.js index 9c6beaa..9ccd633 100644 --- a/build_registries.js +++ b/build_registries.js @@ -308,9 +308,13 @@ async function convert () { } if (registry.endsWith("variant")) { // The mob "variants" only require one valid variant to be accepted - // Send the shortest one to save memory - const shortest = registries[registry].sort((a, b) => a.length - b.length)[0]; - registryBuffers.push(serializeRegistry(registry, [shortest])); + // Send "temperate" if available, otherwise shortest string to save memory + if (registries[registry].includes("temperate")) { + registryBuffers.push(serializeRegistry(registry, ["temperate"])); + } else { + const shortest = registries[registry].sort((a, b) => a.length - b.length)[0]; + registryBuffers.push(serializeRegistry(registry, [shortest])); + } } else { registryBuffers.push(serializeRegistry(registry, registries[registry])); } diff --git a/include/globals.h b/include/globals.h index 66e7510..5d8cbf8 100644 --- a/include/globals.h +++ b/include/globals.h @@ -15,10 +15,19 @@ #define true 1 #define false 0 +// TCP port, Minecraft's default is 25565 #define PORT 25565 +// How many players to keep in memory, NOT the amount of concurrent players +// Even when offline, players who have logged on before take up a slot #define MAX_PLAYERS 16 +// How many mobs to allocate memory for +#define MAX_MOBS (MAX_PLAYERS) +// Server game mode: 0 - survival; 1 - creative; 2 - adventure; 3 - spectator #define GAMEMODE 0 +// Max render distance, determines how many chunks to send #define VIEW_DISTANCE 2 +// Time between server ticks in microseconds (default = 2s) +#define TIME_BETWEEN_TICKS 2000000 // How many visited chunks to "remember" // The server will not re-send chunks that the player has recently been in #define VISITED_HISTORY 4 @@ -77,6 +86,13 @@ typedef struct { uint8_t craft_count[9]; } PlayerData; +typedef struct { + uint8_t type; + short x; + uint8_t y; + short z; +} MobData; + #pragma pack(pop) extern BlockChange block_changes[20000]; @@ -84,4 +100,6 @@ extern int block_changes_count; extern PlayerData player_data[MAX_PLAYERS]; +extern MobData mob_data[MAX_MOBS]; + #endif diff --git a/include/packets.h b/include/packets.h index aa17f2f..98db4dc 100644 --- a/include/packets.h +++ b/include/packets.h @@ -34,7 +34,7 @@ int sc_blockUpdate (int client_fd, int64_t x, int64_t y, int64_t z, uint8_t bloc int sc_openScreen (int client_fd, uint8_t window, const char *title, uint16_t length); int sc_acknowledgeBlockChange (int client_fd, int sequence); int sc_playerInfoUpdateAddPlayer (int client_fd, PlayerData player); -int sc_spawnEntity (int client_fd, int id, uint8_t *uuid, int type, double x, double y, double z, double yaw, double pitch); +int sc_spawnEntity (int client_fd, int id, uint8_t *uuid, int type, double x, double y, double z, uint8_t yaw, uint8_t pitch); int sc_spawnEntityPlayer (int client_fd, PlayerData player); int sc_teleportEntity (int client_fd, int id, double x, double y, double z, float yaw, float pitch); int sc_setHeadRotation (int client_fd, int id, uint8_t yaw); diff --git a/include/tools.h b/include/tools.h index 6ad5458..306ce2a 100644 --- a/include/tools.h +++ b/include/tools.h @@ -10,7 +10,7 @@ inline int mod_abs (int a, int b) { return ((a % b) + b) % b; } inline int div_floor (int a, int b) { - return a < 0 ? (a - b) / b : a / b; + return a % b < 0 ? (a - b) / b : a / b; } ssize_t recv_all (int client_fd, void *buf, size_t n, uint8_t require_first); @@ -36,6 +36,12 @@ void readString (int client_fd); uint32_t fast_rand (); uint64_t splitmix64 (uint64_t state); +#ifdef ESP_PLATFORM + #define get_program_time esp_timer_get_time +#else + int64_t get_program_time (); +#endif + extern int client_states[MAX_PLAYERS * 2]; void setClientState (int client_fd, int new_state); @@ -61,4 +67,7 @@ uint16_t getMiningResult (uint16_t held_item, uint8_t block); void bumpToolDurability (PlayerData *player); void handlePlayerAction (PlayerData *player, int action, short x, short y, short z); +void spawnMob (uint8_t type, short x, uint8_t y, short z); +void handleServerTick (int64_t time_since_last_tick); + #endif diff --git a/include/worldgen.h b/include/worldgen.h index 2aa8c26..ce341f9 100644 --- a/include/worldgen.h +++ b/include/worldgen.h @@ -23,7 +23,8 @@ typedef struct { uint32_t getChunkHash (short x, short z); uint8_t getChunkBiome (short x, short z); -int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome); +int getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome); +int getHeightAt (int x, int z); uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor); uint8_t getBlockAt (int x, int y, int z); diff --git a/src/globals.c b/src/globals.c index d67897c..9395331 100644 --- a/src/globals.c +++ b/src/globals.c @@ -37,3 +37,5 @@ BlockChange block_changes[20000]; int block_changes_count = 0; PlayerData player_data[MAX_PLAYERS]; + +MobData mob_data[MAX_MOBS]; diff --git a/src/main.c b/src/main.c index 013b9a7..915daa8 100644 --- a/src/main.c +++ b/src/main.c @@ -99,6 +99,17 @@ void handlePacket (int client_fd, int length, int packet_id) { sc_spawnEntityPlayer(player_data[i].client_fd, *player); } + // Send information about all other entities (mobs) + // For more info on the arguments, see the spawnMob function + for (int i = 0; i < MAX_MOBS; i ++) { + if (mob_data[i].type == 0) continue; + sc_spawnEntity( + client_fd, 65536 + i, recv_buffer, + mob_data[i].type, mob_data[i].x, mob_data[i].y, mob_data[i].z, + 0, 0 + ); + } + return; } break; @@ -255,6 +266,16 @@ void handlePacket (int client_fd, int length, int packet_id) { clock_t start, end; start = clock(); + uint32_t r = fast_rand(); + if ((r & 3) == 0) { + short mob_x = (_x + dx * VIEW_DISTANCE) * 16 + ((r >> 4) & 15); + short mob_z = (_z + dz * VIEW_DISTANCE) * 16 + ((r >> 8) & 15); + uint8_t mob_y = getHeightAt(mob_x, mob_z) + 1; + if (getBlockAt(mob_x, mob_y, mob_z) == B_air) { + spawnMob(95, mob_x, mob_y, mob_z); + } + } + while (dx != 0) { sc_chunkDataAndUpdateLight(client_fd, _x + dx * VIEW_DISTANCE, _z); count ++; @@ -387,11 +408,8 @@ int main () { int flags = fcntl(server_fd, F_GETFL, 0); fcntl(server_fd, F_SETFL, flags | O_NONBLOCK); - // Track client keep-alives - struct timespec time_now; - struct timespec keepalive_last; - clock_gettime(CLOCK_REALTIME, &time_now); - clock_gettime(CLOCK_REALTIME, &keepalive_last); + // Track time of last server tick + int64_t last_tick_time = get_program_time(); /** * Cycles through all connected clients, handling one packet at a time @@ -421,28 +439,11 @@ int main () { if (client_index == MAX_PLAYERS) client_index = 0; if (clients[client_index] == -1) continue; - // Handle infrequent periodic events every few seconds - clock_gettime(CLOCK_REALTIME, &time_now); - time_t seconds_since_update = time_now.tv_sec - keepalive_last.tv_sec; - if (seconds_since_update > 10) { - // Send Keep Alive and Update Time packets to all in-game clients - world_time += 20 * seconds_since_update; - for (int i = 0; i < MAX_PLAYERS; i ++) { - if (clients[i] == -1) continue; - if (getClientState(clients[i]) != STATE_PLAY) continue; - sc_keepAlive(clients[i]); - sc_updateTime(clients[i], world_time); - } - // Reset keep-alive timer - clock_gettime(CLOCK_REALTIME, &keepalive_last); - /** - * 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; + // Handle periodic events (server ticks) + int64_t time_since_last_tick = get_program_time() - last_tick_time; + if (time_since_last_tick > TIME_BETWEEN_TICKS) { + handleServerTick(time_since_last_tick); + last_tick_time = get_program_time(); } // Handle this individual client diff --git a/src/tools.c b/src/tools.c index 83f9880..2b65c4f 100644 --- a/src/tools.c +++ b/src/tools.c @@ -6,9 +6,14 @@ #ifdef ESP_PLATFORM #include "lwip/sockets.h" #include "lwip/netdb.h" + #include "esp_timer.h" #else #include #include + #include + #ifndef CLOCK_MONOTONIC + #define CLOCK_MONOTONIC 1 + #endif #endif #include "globals.h" @@ -184,6 +189,14 @@ uint64_t splitmix64 (uint64_t state) { return z ^ (z >> 31); } +#ifndef ESP_PLATFORM +int64_t get_program_time () { + struct timespec ts; + clock_gettime(CLOCK_MONOTONIC, &ts); + return (int64_t)ts.tv_sec * 1000000LL + ts.tv_nsec / 1000LL; +} +#endif + int client_states[MAX_PLAYERS * 2]; void setClientState (int client_fd, int new_state) { @@ -379,11 +392,7 @@ void spawnPlayer (PlayerData *player) { if (player->y == -32767) { // Is this a new player? // Determine spawning Y coordinate based on terrain height - int _x = 8 / CHUNK_SIZE; - int _z = 8 / CHUNK_SIZE; - int rx = 8 % CHUNK_SIZE; - int rz = 8 % CHUNK_SIZE; - spawn_y = getHeightAt(rx, rz, _x, _z, getChunkHash(_x, _z), getChunkBiome(_x, _z)) + 1; + spawn_y = getHeightAt(8, 8) + 1; } else { // Not a new player // Calculate spawn position from player data spawn_x = player->x > 0 ? (float)player->x + 0.5 : (float)player->x - 0.5; @@ -663,3 +672,110 @@ void handlePlayerAction (PlayerData *player, int action, short x, short y, short } } + +void spawnMob (uint8_t type, short x, uint8_t y, short z) { + + 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; + + // 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, + 65536 + i, // Try to avoid conflict with client file descriptors + recv_buffer, // The UUID doesn't matter, feed it garbage + type, x, y, z, + // Face opposite of the player, as if looking at them when spawning + (player_data[j].yaw + 127) & 255, 0 + ); + } + + break; + } + +} + +// 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) { + + // Send Keep Alive and Update Time packets to all in-game clients + world_time += 20 * time_since_last_tick / 1000000; + for (int i = 0; i < MAX_PLAYERS; i ++) { + if (player_data[i].client_fd == -1) continue; + sc_keepAlive(player_data[i].client_fd); + sc_updateTime(player_data[i].client_fd, world_time); + } + + /** + * 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; + + uint32_t r = fast_rand(); + + // Skip 50% of ticks randomly + if (r & 1) continue; + + // Move by one block on the X or Z axis + // Yaw is set to face in the direction of motion + short new_x = mob_data[i].x, new_z = mob_data[i].z; + uint8_t yaw; + 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; } + } + // Vary the yaw angle to look just a little less robotic + yaw += ((r >> 6) & 15) - 8; + + // Check if the block we're moving into is passable: + // if yes, and the block below is solid, keep the same Y level; + // if yes, but the block below isn't solid, drop down one block; + // if not, go up by up to one block; + // if going up isn't possible, skip this iteration. + uint8_t new_y = mob_data[i].y; + uint8_t block = getBlockAt(new_x, new_y, new_z); + if (block != B_air) { + if (getBlockAt(new_x, new_y + 1, new_z) == B_air) new_y += 1; + else continue; + } else if (getBlockAt(new_x, new_y - 1, new_z) == B_air) new_y -= 1; + + // Store new mob position + mob_data[i].x = new_x; + mob_data[i].y = new_y; + mob_data[i].z = new_z; + + // Broadcast relevant entity movement packets + for (int j = 0; j < MAX_PLAYERS; j ++) { + if (player_data[j].client_fd == -1) continue; + int entity_id = 65536 + i; + 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); + } + + } + +} diff --git a/src/worldgen.c b/src/worldgen.c index 156f5b9..ee1f6ec 100644 --- a/src/worldgen.c +++ b/src/worldgen.c @@ -119,7 +119,7 @@ int interpolate (int a, int b, int c, int d, int x, int z) { return (top * (CHUNK_SIZE - z) + bottom * z) / (CHUNK_SIZE * CHUNK_SIZE); } -int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome) { +int getHeightAtFromHash (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t biome) { if (rx == 0 && rz == 0) { int height = getCornerHeight(chunk_hash, biome); @@ -135,6 +135,19 @@ int getHeightAt (int rx, int rz, int _x, int _z, uint32_t chunk_hash, uint8_t bi } +int getHeightAt (int x, int z) { + + int _x = div_floor(x, CHUNK_SIZE); + int _z = div_floor(z, CHUNK_SIZE); + int rx = mod_abs(x, CHUNK_SIZE); + int rz = mod_abs(z, CHUNK_SIZE); + uint32_t chunk_hash = getChunkHash(_x, _z); + uint8_t biome = getChunkBiome(_x, _z); + + return getHeightAtFromHash(rx, rz, _x, _z, chunk_hash, biome); + +} + uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) { if (y > 80) return B_air; @@ -144,7 +157,7 @@ uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) { if (rx < 0) rx += CHUNK_SIZE; if (rz < 0) rz += CHUNK_SIZE; - int height = getHeightAt(rx, rz, anchor.x, anchor.z, anchor.hash, anchor.biome); + int height = getHeightAtFromHash(rx, rz, anchor.x, anchor.z, anchor.hash, anchor.biome); if (y < 64 || y < height) goto skip_feature; @@ -170,7 +183,7 @@ uint8_t getTerrainAt (int x, int y, int z, ChunkAnchor anchor) { switch (anchor.biome) { case W_plains: { // Generate trees in the plains biome - uint8_t feature_y = getHeightAt( + uint8_t feature_y = getHeightAtFromHash( feature_x < 0 ? feature_x % CHUNK_SIZE + CHUNK_SIZE : feature_x % CHUNK_SIZE, feature_z < 0 ? feature_z % CHUNK_SIZE + CHUNK_SIZE : feature_z % CHUNK_SIZE, anchor.x, anchor.z, anchor.hash, anchor.biome