dragonblocks_alpha/src/server/server_terrain.c

431 lines
10 KiB
C

#define _GNU_SOURCE // don't worry, GNU extensions are only used when available
#include <dragonstd/queue.h>
#include <features.h>
#include <stdatomic.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include "interrupt.h"
#include "server/database.h"
#include "server/schematic.h"
#include "server/server_config.h"
#include "server/server_node.h"
#include "server/server_terrain.h"
#include "server/terrain_gen.h"
#include "terrain.h"
// this file is too long
Terrain *server_terrain;
static atomic_bool cancel; // remove the smooth
static Queue terrain_gen_tasks; // this is terry the fat shark
static pthread_t *terrain_gen_threads; // thread pool
static s32 spawn_height; // elevation to spawn players at
static unsigned int num_gen_chunks; // number of enqueued / generating chunks
static pthread_mutex_t mtx_num_gen_chunks; // lock to protect the above
// utility functions
// return true if a player is close enough to a chunk to access it
static bool within_load_distance(ServerPlayer *player, v3s32 cpos, u32 dist)
{
pthread_rwlock_rdlock(&player->lock_pos);
v3s32 ppos = terrain_chunkp((v3s32) {player->pos.x, player->pos.y, player->pos.z});
pthread_rwlock_unlock(&player->lock_pos);
return abs(ppos.x - cpos.x) <= (s32) dist
&& abs(ppos.y - cpos.y) <= (s32) dist
&& abs(ppos.z - cpos.z) <= (s32) dist;
}
// send a chunk to a client and reset chunk request
static void send_chunk_to_client(ServerPlayer *player, TerrainChunk *chunk)
{
if (!within_load_distance(player, chunk->pos, server_config.load_distance))
return;
pthread_rwlock_rdlock(&player->lock_peer);
if (player->peer)
dragonnet_peer_send_ToClientChunk(player->peer, &(ToClientChunk) {
.pos = chunk->pos,
.data = ((TerrainChunkMeta *) chunk->extra)->data,
});
pthread_rwlock_unlock(&player->lock_peer);
}
// me when the
static void terrain_gen_step()
{
// big chunkus
TerrainChunk *chunk = queue_deq(&terrain_gen_tasks, NULL);
if (!chunk)
return;
TerrainChunkMeta *meta = chunk->extra;
List changed_chunks;
list_ini(&changed_chunks);
list_apd(&changed_chunks, chunk);
terrain_gen_chunk(chunk, &changed_chunks);
pthread_mutex_lock(&meta->mtx);
meta->state = CHUNK_READY;
pthread_mutex_unlock(&meta->mtx);
server_terrain_lock_and_send_chunks(&changed_chunks);
pthread_mutex_lock(&mtx_num_gen_chunks);
num_gen_chunks--;
pthread_mutex_unlock(&mtx_num_gen_chunks);
}
// there was a time when i wrote actually useful comments lol
static void *terrain_gen_thread()
{
#ifdef __GLIBC__ // check whether bloat is enabled
pthread_setname_np(pthread_self(), "terrain_gen");
#endif // __GLIBC__
// extremely advanced logic
while (!cancel)
terrain_gen_step();
return NULL;
}
// enqueue chunk
static void generate_chunk(TerrainChunk *chunk)
{
if (cancel)
return;
pthread_mutex_lock(&mtx_num_gen_chunks);
num_gen_chunks++;
pthread_mutex_unlock(&mtx_num_gen_chunks);
TerrainChunkMeta *meta = chunk->extra;
meta->state = CHUNK_GENERATING;
queue_enq(&terrain_gen_tasks, chunk);
}
// callback for initializing a newly created chunk
// load chunk from database or initialize state, tgstage buffer and data
static void on_create_chunk(TerrainChunk *chunk)
{
TerrainChunkMeta *meta = chunk->extra = malloc(sizeof *meta);
pthread_mutex_init(&meta->mtx, NULL);
if (database_load_chunk(chunk)) {
meta->data = terrain_serialize_chunk(server_terrain, chunk, &server_node_serialize_client);
} else {
meta->state = CHUNK_CREATED;
meta->data = (Blob) {0, NULL};
CHUNK_ITERATE {
chunk->data[x][y][z] = server_node_create(NODE_AIR);
meta->tgsb.raw.nodes[x][y][z] = STAGE_VOID;
}
}
}
// callback for deleting a chunk
// free meta data
static void on_delete_chunk(TerrainChunk *chunk)
{
TerrainChunkMeta *meta = chunk->extra;
pthread_mutex_destroy(&meta->mtx);
Blob_free(&meta->data);
free(meta);
}
// callback for determining whether a chunk should be returned by terrain_get_chunk
// hold back chunks that are not fully generated except when the create flag is set to true
static bool on_get_chunk(TerrainChunk *chunk, bool create)
{
if (create)
return true;
TerrainChunkMeta *meta = chunk->extra;
pthread_mutex_lock(&meta->mtx);
bool ret = meta->state == CHUNK_READY;
pthread_mutex_unlock(&meta->mtx);
return ret;
}
// generate a hut for new players to spawn in
static void generate_spawn_hut()
{
List changed_chunks;
list_ini(&changed_chunks);
List spawn_hut;
schematic_load(&spawn_hut, RESSOURCE_PATH "schematics/spawn_hut.txt", (SchematicMapping[]) {
{
.color = {0x7d, 0x54, 0x35},
.type = NODE_OAK_WOOD,
.use_color = true,
},
{
.color = {0x50, 0x37, 0x28},
.type = NODE_OAK_WOOD,
.use_color = true,
},
}, 2);
schematic_place(&spawn_hut, (v3s32) {0, spawn_height, 0},
STAGE_PLAYER, &changed_chunks);
schematic_delete(&spawn_hut);
// dynamic part of spawn hut - cannot be generated by a schematic
v2s32 posts[6] = {
{-4, -2},
{-4, +3},
{+3, -2},
{+3, +3},
{+4, +0},
{+4, +1},
};
v3f32 wood_color = {
(f32) 0x7d / 0xff,
(f32) 0x54 / 0xff,
(f32) 0x35 / 0xff};
for (int i = 0; i < 6; i++) {
for (s32 y = spawn_height - 1;; y--) {
v3s32 pos = {posts[i].x, y, posts[i].y};
NodeType node = terrain_get_node(server_terrain, pos).type;
if (i >= 4) {
if (node != NODE_AIR)
break;
pos.y++;
}
if (node_def[node].solid)
break;
server_terrain_gen_node(pos, node == NODE_LAVA
? server_node_create(NODE_VULCANO_STONE)
: server_node_create_color(NODE_OAK_WOOD, wood_color),
STAGE_PLAYER, &changed_chunks);
}
}
server_terrain_lock_and_send_chunks(&changed_chunks);
}
// public functions
// called on server startup
void server_terrain_init()
{
server_terrain = terrain_create();
server_terrain->callbacks.create_chunk = &on_create_chunk;
server_terrain->callbacks.delete_chunk = &on_delete_chunk;
server_terrain->callbacks.get_chunk = &on_get_chunk;
server_terrain->callbacks.delete_node = &server_node_delete;
cancel = false;
queue_ini(&terrain_gen_tasks);
terrain_gen_threads = malloc(sizeof *terrain_gen_threads * server_config.terrain_gen_threads);
num_gen_chunks = 0;
pthread_mutex_init(&mtx_num_gen_chunks, NULL);
for (unsigned int i = 0; i < server_config.terrain_gen_threads; i++)
pthread_create(&terrain_gen_threads[i], NULL, (void *) &terrain_gen_thread, NULL);
}
// called on server shutdown
void server_terrain_deinit()
{
queue_fin(&terrain_gen_tasks);
cancel = true;
queue_cnl(&terrain_gen_tasks);
for (unsigned int i = 0; i < server_config.terrain_gen_threads; i++)
pthread_join(terrain_gen_threads[i], NULL);
free(terrain_gen_threads);
pthread_mutex_destroy(&mtx_num_gen_chunks);
queue_dst(&terrain_gen_tasks);
terrain_delete(server_terrain);
}
// handle chunk request from client (thread safe)
void server_terrain_requested_chunk(ServerPlayer *player, v3s32 pos)
{
if (within_load_distance(player, pos, server_config.load_distance)) {
TerrainChunk *chunk = terrain_get_chunk(server_terrain, pos, true);
TerrainChunkMeta *meta = chunk->extra;
pthread_mutex_lock(&meta->mtx);
switch (meta->state) {
case CHUNK_CREATED:
generate_chunk(chunk);
break;
case CHUNK_GENERATING:
break;
case CHUNK_READY:
send_chunk_to_client(player, chunk);
break;
};
pthread_mutex_unlock(&meta->mtx);
}
}
static void update_percentage()
{
static s32 total = 3 * 3 * 21;
static s32 done = -1;
static s32 last_percentage = -1;
if (done < total)
done++;
pthread_mutex_lock(&mtx_num_gen_chunks);
s32 percentage = 100.0 * (done - num_gen_chunks) / total;
pthread_mutex_unlock(&mtx_num_gen_chunks);
if (percentage > last_percentage) {
last_percentage = percentage;
printf("[verbose] preparing spawn... %d%%\n", percentage);
}
}
// prepare spawn region
void server_terrain_prepare_spawn()
{
update_percentage();
for (s32 x = -1; x <= (s32) 1; x++) {
for (s32 y = -10; y <= (s32) 10; y++) {
for (s32 z = -1; z <= (s32) 1; z++) {
if (interrupt.set)
return;
TerrainChunk *chunk = terrain_get_chunk(server_terrain, (v3s32) {x, y, z}, true);
TerrainChunkMeta *meta = chunk->extra;
pthread_mutex_lock(&meta->mtx);
if (meta->state == CHUNK_CREATED)
generate_chunk(chunk);
pthread_mutex_unlock(&meta->mtx);
update_percentage();
}
}
}
for (;;) {
update_percentage();
pthread_mutex_lock(&mtx_num_gen_chunks);
bool done = (num_gen_chunks == 0);
pthread_mutex_unlock(&mtx_num_gen_chunks);
if (done)
break;
sched_yield();
}
s64 saved_spawn_height;
if (database_load_meta("spawn_height", &saved_spawn_height)) {
spawn_height = saved_spawn_height;
} else {
spawn_height = -1;
while (terrain_get_node(server_terrain, (v3s32) {0, ++spawn_height, 0}).type != NODE_AIR);
spawn_height += 5;
database_save_meta("spawn_height", spawn_height);
generate_spawn_hut();
}
}
void server_terrain_gen_node(v3s32 pos, TerrainNode node, TerrainGenStage new_tgs, List *changed_chunks)
{
v3s32 offset;
TerrainChunk *chunk = terrain_get_chunk_nodep(server_terrain, pos, &offset, true);
TerrainChunkMeta *meta = chunk->extra;
terrain_lock_chunk(chunk);
u32 *tgs = &meta->tgsb.raw.nodes[offset.x][offset.y][offset.z];
if (new_tgs < *tgs) {
pthread_mutex_unlock(&chunk->mtx);
server_node_delete(&node);
return;
}
*tgs = new_tgs;
chunk->data[offset.x][offset.y][offset.z] = node;
if (changed_chunks)
list_add(changed_chunks, chunk, chunk, &cmp_ref, NULL);
else
server_terrain_send_chunk(chunk);
pthread_mutex_unlock(&chunk->mtx);
}
s32 server_terrain_spawn_height()
{
// wow, so useful!
return spawn_height;
}
// send chunk to near clients
// meta mutex has to be locked
void server_terrain_send_chunk(TerrainChunk *chunk)
{
TerrainChunkMeta *meta = chunk->extra;
if (meta->state == CHUNK_GENERATING)
return;
terrain_lock_chunk(chunk);
Blob_free(&meta->data);
meta->data = terrain_serialize_chunk(server_terrain, chunk, &server_node_serialize_client);
database_save_chunk(chunk);
pthread_mutex_unlock(&chunk->mtx);
if (meta->state == CHUNK_CREATED)
return;
server_player_iterate(&send_chunk_to_client, chunk);
}
void server_terrain_lock_and_send_chunk(TerrainChunk *chunk)
{
TerrainChunkMeta *meta = chunk->extra;
pthread_mutex_lock(&meta->mtx);
server_terrain_send_chunk(chunk);
pthread_mutex_unlock(&meta->mtx);
}
void server_terrain_lock_and_send_chunks(List *changed_chunks)
{
list_clr(changed_chunks, &server_terrain_lock_and_send_chunk, NULL, NULL);
}